一、核心机制:原型链
在 JS 中,每个对象都有一个内部属性 [[Prototype]](在浏览器中通常通过__proto__访问),指向它的原型对象。当我们访问一个对象的属性或方法时,如果该对象自身没有,JS 引擎就会沿着__proto__一直往上找,直到找到为止(如果找到 null 还没找到,则返回 undefined)。这条链式查找结构,就是原型链。
关键点区分:
prototype:这是函数特有的属性(构造函数才有),指向该函数构造出的实例对象的原型。
__proto__:这是实例对象特有的属性,指向构造函数的 prototype。
constructor:每个原型对象都有一个 constructor 属性,指向关联的构造函数。
二、继承的六种实现方式
1.原型链继承
function Parent() {
this.colors = ['red', 'blue'];
}
function Child() {}
Child.prototype = new Parent(); // 核心
const c1 = new Child();
c1.colors.push('green');
const c2 = new Child();
console.log(c2.colors); // ['red', 'blue', 'green'] ------ 大问题:引用类型被共享!
注解
1.Child.prototype = new Parent();
这行代码是核心,它相当于通过让Child的原型等于父亲的一个实例。让二者的原型链产生链接,Child的实例可以通过原型链访问到Parent上的属性和方法。
2.引用类型被共享的原因
当c1想要操作colors时,它自身没有,就沿着原型链找,在Parent找到并进行操作,这个操作影响的是原型链上的数据,c2访问colors,自身没有,也是沿着原型链找,最终和c1找到的一样,所以c1的操作影响了c2.
2.借用构造函数继承
function Parent(name) {
this.name = name;
this.colors = ['red'];
}
function Child(name) {
Parent.call(this, name); // 核心:借用父类构造函数
}
const c1 = new Child('c1');
c1.colors.push('blue');
const c2 = new Child('c2');
console.log(c2.colors); // ['red'] ------ 解决了共享问题
console.log(c2.name); // 'c2'
注解
1.Parent.call(this, name);
这行代码让Parent的this指向变成Child的this指向,也就是Child的实例对象。所以在执行Parent函数时,像this.name = name;这样借助this进行的赋值相当于再给Child的实例对象赋值,让Child的实例对象可以获得Parent内部所有显示挂载在this上的属性。
2.缺点
这样的继承原型链是缺失的,执行完Parent.call(this, name),两者就没关系了,Child访问不到Parent原型链上的属性和方法。并且Child的实例只能继承Parent内部所有显示挂载在this上的属性,像let hobby = 'sing'这样的属性他无法继承。
3.组合继承
function Parent(name) {
this.name = name;
this.colors = ['red'];
}
Parent.prototype.sayName = function() { console.log(this.name); };
function Child(name, age) {
Parent.call(this, name); // 第二次调用 Parent
this.age = age;
}
Child.prototype = new Parent(); // 第一次调用 Parent
Child.prototype.constructor = Child; // 修复构造函数指向
Child.prototype.sayAge = function() { console.log(this.age); };
const c = new Child('Tom', 18);
c.colors.push('blue');
console.log(c.colors); // ['red', 'blue']
c.sayName(); // 'Tom'
注解
1.核心逻辑
通过两次调用Parent实现继承,第一次Child.prototype = new Parent();,让Child实例可以通过原型链访问到Parent上的属性和方法。第二次Parent.call(this, name);把所有挂载到this上的属性和方法直接写在实例身上,遇到引用类型直接在自身就能找到,不用在原型链上找,防止共享。
2.Child.prototype.constructor = Child
执行完Child.prototype = new Parent(),会让Child的构造函数指向Parent,及Child.prototype.constructor =Parent,所以需要修复构造函数指向。
3.优缺点
这个方法结合了前两种方法的优点,但如果引用类型的数据没有被挂载在this上,它依然会被共享。并且它调用了两次Parent,会产生两个Parent实例,造成冗余。
4.寄生组合继承
function Parent(name) {
this.name = name;
}
Parent.prototype.sayName = function() { console.log(this.name); };
function Child(name, age) {
Parent.call(this, name); // 只调用一次
this.age = age;
}
// 核心:用一个空对象作为媒介,指向 Parent.prototype
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child; // 修复构造函数
const c = new Child('Tom', 18);
c.sayName(); // 'Tom'
注解
1.Child.prototype = Object.create(Parent.prototype);
这段代码利用Object.create创造一个空对象,并让空对象的prototype指向Parent.prototype,Child.prototype的又指向了空对象的prototype,借此完成原型链的链接,相比较上一个方法,他减少了Parent的调用,没有造成冗余。
5. 原型式继承(Object.create)
const parent = { name: 'parent', colors: ['red'] };
const child1 = Object.create(parent);
const child2 = Object.create(parent);
child1.colors.push('blue');
console.log(child2.colors); // ['red', 'blue'] ------ 引用类型依然共享
注解
这种方法主要还是通过对Object.create的使用完成原型链的链接,完成继承,代码简单,但共享问题依然存在。
6.class继承
class Parent {
constructor(name) {
this.name = name;
this.colors = ['red'];
}
// 定义在原型上
sayName() {
console.log(this.name);
}
}
class Child extends Parent {
constructor(name, age) {
super(name); // 必须调用,相当于 Parent.call(this, name)
this.age = age;
}
sayAge() {
console.log(this.age);
}
}
const c = new Child('Tom', 18);
c.colors.push('blue');
c.sayName(); // 'Tom'
注解
1.class Child extends Parent
这句话让Child实例不仅可以调用Parent构造函数上的属性和方法,还可以调用Parent上面的属性和方法。并且将this标记为未初始化。
2.两个constructor函数
Parent上的constructor主要作用是给Parent的实力挂载上自己的专属属性,另一个constructor主要作用是先将Parent的属性挂载到Child实例身上,然后再挂载上自己的专属属性,并且必须先执行super(name); ,然后才能挂载上自己的专属属性,否则会因为this没有初始化而报错。
ctor函数 Parent上的constructor主要作用是给Parent的实力挂载上自己的专属属性,另一个constructor主要作用是先将Parent的属性挂载到Child实例身上,然后再挂载上自己的专属属性,并且必须先执行super(name); `,然后才能挂载上自己的专属属性,否则会因为this没有初始化而报错。