10_面向对象的原型链和继承实现

JavaScript 中的类和对象

当我们编写如下代码的时候,我们会如何来称呼这个 Person 呢?

function Person() {}

var p1 = new Person();
var p2 = new Person();
  • 在 JavaScript 中 Person 应该被称之为是一个构造函数
  • 在很多面向对象语言过来的开发者,也习惯称之为类,因为类可以帮助我们创建出来对象 p1、p2
  • 如果从面向对象的编程范式角度来看,Person 确实是可以称之为类的

面向对象的特性-继承

面向对象有三大特征:封装、继承、多态

  • 封装:我们前面将属性和方法封装到一个类中,可以称之为封装的过程;
  • 继承:继承是面向对象中非常重要的,不仅仅可以减少重复代码的数量,也是多态的前提(纯面向对象中);
  • 多态:不同的对象在执行时表现出不同的形态

这里核心讲解继承

继承是做什么呢?

  • 继承可以帮助我们将重复的代码和逻辑抽取到父类中,子类只需要直接继承过来使用即可

那么 JavaScript 中如何实现继承呢?

  • 首先我们得先了解 JavaScript 原型链的机制
  • 再利用原型链的机制实现一下继承

JavaScript 原型链

在真正实现继承之前,我们先来理解一个非常重要的概念:原型链

  • 我们知道,从一个对象上获取属性,如果在当前对象中没有获取到就会去它的原型上面获取

其实就是查找的过程就是沿着我们的原型链进行查找

var obj = {
  name: 'beanbag'
  age: 18
}

obj.__proto__ = {}

obj.__proto__.__proto__ = {}

obj.__proto__.__proto__.__proto__ = {
  address: '北京市'
}

20220604220640

Object 的原型

  • 那么什么地方是原型链的尽头呢?比如第三个对象是否也是有原型proto属性呢?

console.log(obj.__proto__.__proto__.__proto__.__proto__)

  • 我们会发现它打印的是 [Object: null prototype] {}

    • 事实上这个原型就是我们最顶层的原型了
    • 从 Object 直接创建出来的对象的原型都是[Object: null prototype] {}
  • 那么我们可能会问: [Object: null prototype] {} 原型有什么特殊吗?

    • 特殊一:该对象有原型属性,但是它的原型属性已经指向的是 null,也就是已经是顶层原型了
    • 特殊二:该对象上有很多默认的属性和方法

20220604221101

原型链关系的内存图

Object 是所有类的父类

从我们上面的 Object 原型我们可以得出一个结论:原型链最顶层的原型对象就是 Object 的原型对象

function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.running = function () {
  console.log(this.name + " running~");
};

var p1 = new Person("why", 18);

console.log(p1);
console.log(p1.valueOf());
console.log(p1.toString());

Object是所有类的父类

总结

  1. 原型链查找:首先在自己的对象中查找,找不到的话找到自己的隐式原型属性__proto__所指向的对象去找(也就是自己的原型对象),原型对象中找不到,继续去原型对象的隐式原型属性__proto__所指向的对象查找(其实就是 Object 对象),若这个对象上面没有的话,返回 undefined
  2. Object 对象是最顶层的原型,有很多默认的属性和方法:valueOf、 toString...

通过原型链实现继承

为什么需要继承?

如果没有继承,我们在实现两个差不多的类的时候,需要编写大量重复的代码,无疑是不合理的,继承就可以让我们相同的属性封装到一个父类中,减少重复代码的编写。

如何实现原型链继承

需要关心的就是 Student.prototype = new Person();这个步骤 将 Student 构造函数的显示原型属性指向为通过 new 方法返回的 Person 对象

function Person() {
  this.name = "why";
}

Person.prototype.eating = function () {
  console.log(this.name + " eating");
};

function Student() {
  this.sno = 100;
}

Student.prototype = new Person();

Student.prototype.studying = function () {
  console.log(this.name + " studying");
};

var stu1 = new Student();
console.log(stu1.name);
stu1.eating();

内存表现如下图:

原型链实现继承方式内存表现

分析一下:

  1. 首先会存在 Person 和 Student 两个构造函数,构造函数就有对应的显示原型属性 prototype;
  2. 执行到Student.prototype = new Person();时,会将原本指向 Student 原型对象的指向改变为 person 对象(这个不是 Person 构造函数要注意,是通过 new 关键词返回的 person 对象)
  3. 这就导致了原本存在引用关系的 Student 的原型对象变得没有引用了,所以会被垃圾回收
  4. 以上是最基本的一种继承方式,但是有很多缺陷

原型链继承的弊端

  1. 我们通过直接打印对象是看不到这个属性的(不会打印__proto__上的属性),只会打印对象本身已经存在的属性,是一种机制
  2. 这个属性会被多个对象共享,如果这个对象是一个引用类型,那么就会造成问题,如果一个对象修改了引用类型的属性,则所有对象访问都会被修改
  3. 不能给 Person 传递参数,因为这个对象是一次性创建的(没办法定制化)

借用构造函数实现继承

为了解决原型链继承中存在的问题,开发人员提供了一种新的技术: constructor stealing(有很多名称:借用构造函数或者经典继承或者伪造对象)

  • steal 是偷窃、剽窃的意思,但是这里翻译为借用

借用继承的做法非常简单:在子类构造函数的内部调用父类构造函数

  • 因为函数可以在任意的时刻被调用
  • 因此通过 apply()和 call()方法也可以在新创建的对象上执行构造函数

具体实现:

function Person(name, friends) {
  this.name = name;
  this.friends = friends;
}

Person.prototype.eating = function () {
  console.log(this.name + " eating");
};

function Student(name, sno, friends) {
  // constructor stealing
  Person.call(this, name, friends);
  this.sno = sno;
}

Student.prototype = new Person();

Student.prototype.studying = function () {
  console.log(this.name + " studying" + this.sno);
};

var stu1 = new Student("beanbag", 100, ["lilei"]);
var stu2 = new Student("why", 200, ["lucy"]);

stu1.friends.push("hanmeimei");
console.log(stu1, stu2);

内存表现:

借用构造函数继承

分析一下:

  1. 借用构造函数实现继承的方式是在最基本的原型链继承的基础上巧妙利用了函数可以在任何地方运行的特点,在子类的构造函数中利用 call 方法调用一次父类的构造函数实现的
  2. 通过在子类的构造函数中调用父类的构造函数,使我们的所有对象都会保存在子类中,这样就解决了原型链继承的三大缺陷

缺陷:

  1. 父类创建出的对象上(stu1 的原型对象上)保存了不必要的一些子类上的共有属性,例如这里的 name 和 friends,他们的值一直为 undefined,这是不必要的,这是因为在执行Student.prototype = new Person();的时候创建出来的

  2. 创建出对象的时候,至少调用两次父类的构造函数,一次是 new Person() ,一次是使用 call 方法进行绑定

Last Updated:
Contributors: hqchqc