四种继承方式划分
- 原型链继承
- 经典继承(借用父类构造函数)
- 组合继承
- 寄生组合继承
new调用过程(前置知识)
在介绍js的各种继承时,先简单介绍一下new过程,方便后面学习继承时,知道new做了什么。
new调用实际上是构造函数调用。它会执行以下操作:
- 创建一个全新的对象
- 这个新对象会执行原型链接
- 这个新对象会绑定到函数调用的this
- 如果函数没有返回对象,new函数调用会自动返回这个新对象
很简单的四部曲是吧,其中最主要的就是第二,三步。他是new可以实现继承的核心。,下面我们开始进入主题!
1.原型链继承
缺点:
- 引用类型的属性被所有实例共享
- 在创建child实例时,不能向Parent传参
// 父构造函数
function Parent () {
this.name = ['Kevin', 'daisy'];
this.age= 17;
}
Parent.prototype.getName = function () {
console.log(this.name);
}
// 子构造函数
function Child () {}
Child.prototype = new Parent(); // 将Child.prototype指向Parent.prototype
var child1 = new Child();
child1.name.push("Tony");
child1.age = 18;
console.log(child1.name); // [ 'Kevin', 'daisy', 'Tony' ]
console.log(child1.age); // 18
var child2 = new Child();
console.log(child2.name); // [ 'Kevin', 'daisy', 'Tony' ] 被共享了
console.log(child2.age); // 17 没有被共享
解释: 由new的调用过程可知,Child.prototype = new Parent() 语句,使Child.prototype指向了一个Parent实例对象,这个实例有一个name属性。因此通过Child创建的实例(child1,child2)的原型都指向了这个Parent实例对象。此时,当我们访问child1和child2的属性name时,会沿着原型链去查找,它们都找到了同一个Parent实例对象中的name属性。所以,当在任意实例对象中修改name的值时,所有实例的name值返回的结果都是一样的(因为name指针指向同一个内存区域)。
疑惑: 既然child实例指向的原型都是同一个,那为什么age属性为什么没有被共享?
这就涉及到对象的属性设置和屏蔽问题了:
当age不直接存在于child1中而是存在于原型链上层时,child1.age = 18会检测原型上该属性是否为普通数据访问属性并且没有被标记为只读(writeable:false),那就会直接在child1中添加一个名为age的新属性,它是屏蔽属性,屏蔽了原型上的age。
此时:
console.log(child1.hasOwnProperty("age")); // true
console.log(child2.hasOwnProperty("age")); // false
2.经典继承(借用父类构造函数)
优点: 解决了原型链继承的两个问题(如上)
缺点: 父类原型链上的方法不会被子类继承
function Parent (age) {
this.name = ['Kevin', 'daisy'];
this.age = age;
}
function Child (age) {
Parent.call(this, age); // 借用Parent的构造函数
// 定义其他方法
}
var child1 = new Child(18);
child1.name.push('Tony');
console.log(child1.name); // ['Kevin', 'daisy', 'Tony']
console.log(child1.age); // 18
var child2 = new Child(20);
console.log(child2.name); // ['Kevin', 'daisy'] 没有被共享
console.log(child2.age); // 20
解释: 在new Child(18)创建实例的时候,通过Parent.call向Parent传递了参数,并且此时的this指向了Child的实例,因此Parent中的name和age属性都变成了Child的实例属性,又因为child1和child2是两个不同的实例,所以他们的属性不会共享(name的指针指向不同了)。有因为Parent.call(this, age) 方法只能继承父类的属性,所以父类的方法不会被子类继承。
3.组合继承
优点: 融合原型链继承和经典继承的优点,是JavaScript中最常用的继承模式
缺点: 会调用两次父构造函数,导致子类原型上也有父类的实例属性
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.getName = function () {
console.log(this.name);
}
function Child (name, age) {
Parent.call(this, name); // 借用构造函数, 第一次调用父构造函数
this.age = age;
}
Child.prototype = new Parent(); // 将Child.prototype指向Parent.prototype, 第二次调用父构造函数
var child1 = new Child('Kevin', 18);
child1.colors.push('black');
console.log(child1.name);
console.log(child1.age);
console.log(child1.colors); // [ 'red', 'blue', 'green', 'black' ]
var child2 = new Child('daisy', 20);
console.log(child2.name);
console.log(child2.age);
console.log(child2.colors); // [ 'red', 'blue', 'green' ] 没有被共享
解释: 前面讲解经典继承的时候,它只能继承父类的属性,但是父类的原型方法没有被继承,所以组合继承为了解决这个问题,使用了原型链继承中的Child.prototype = new Parent() 语句。不难发现,该语句会使Child.prototype指向Parent的实例,导致子类原型会包含父类的实例属性。
4.寄生组合继承
优点: 解决了组合继承的问题
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.getName = function () {
console.log(this.name);
}
function Child (name, age) {
Parent.call(this, name);
this.age = age;
}
// 核心
var F = function () {};
F.prototype = Parent.prototype;
child.prototype = new F();
// 或者可以使用Object.create()使Child.prototype指向Parent.prototype
// Child.prototype = Object.create(Parent.prototype);
var child1 = new Child('kevin', 18);
console.log(child1);
解释: 为了解决组合继承中,子类原型包含父类实例属性的问题,引入了一个空构造函数F,并将F的原型指向父类的原型,最后把子类原型指向F的实例对象。由于空构造函数F没有实例属性,所以子类原型并不会包含父类实例属性了,其实Object.create的内部实现也是借用了空构造函数,所以也可以用Object.create代替实现。
总结
原生JS这四种常见的继承方式各有千秋,比较常用的是组合继承和寄生组合继承,所以重点掌握这两种继承方式是十分必要的。
参考资料
《你不知道的Javascript》