继承是面相对象编程语言的一个特色,一般分为两类:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。在 JS 中,没有函数签名,因此,JS 只支持实现继承,主要是通过原型链实现的。
构造函数,原型对象和实例之间的关系
先来回顾下构造函数,原型对象,实例,三者之间的关系:
每创建一个函数,就会为函数创建一个 prototype
属性,指向原型对象;
原型对象,默认情况下,会自动获得一个 constructor
属性,指回构造函数;
通过构造函数创建的实例,会有一个内部属性 [[prototype]]
(也叫__prototype__
),指向原型对象。
关系如下图:
原型链继承
让子类的原型对象 等于 父类的实例,Child.prototype = new Parent()
;
function Parent() {
this.parentAge = 56;
}
Parent.prototype.getParentAge = function() {
return this.parentAge;
};
function Child() {
this.childAge = 18;
}
Child.prototype = new Parent();
Child.prototype.getChildAge = function () {
return this.childAge;
};
var xiaoming = new Child();
console.log(xiaoming.getParentAge()); // 56
关系图如下:
注意 上图中 Child()
的原型对象 new Parent()
的 constructor
并不指向 Child()
(此时它指向 Parent())。后续我们会手动让它指向 Child()
。
通过 Child.prototype.constructor
可以查看
默认原型
事实上,上图少了一环,默认的原型。我们知道所有的引用类型都默认继承了 Object,而这个继承也是通过原型链实现的。所有函数的默认原型都是 Object 的实例,因此,默认原型都会包含一个内部指针,指向 Object.prototype。这也是所有自定义类型都会继承 toString(),valueOf等方法的原因所在。
从上图中,可以清晰的看到原型链逐级向上查找的路径,原型链的关键是 实例的 [[prototype]]
属性。
原型和实例关系
xiaoming
同时是 Child
,Parent
,Object
的实例;
xiaoming instanceof Child; // true
xiaoming instanceof Parent; // true
xiaoming instanceof Object; // true
Child.Prototype
,Child.Prototype
, Child.Prototype
都是 xiaoming
的原型
Child.prototype.isPrototypeOf(xiaoming); // true
Parent.prototype.isPrototypeOf(xiaoming); // true
Object.prototype.isPrototypeOf(xiaoming); // true
谨慎定义方法
给原型添加方法,一定要放在替换原型语句后。
问题
纯粹的原型链继承,虽然强大,实现起来也简单,但是它也有些问题。
第一个问题是,包含引用类型的原型属性。
在 重拾JS——创建对象 中说过,包含引用类型的原型属性,会被共享(事实上,是所有原型属性都会被共享,只是 引用类型的共享通常不是我们所希望的)。而这也正是为什么要在构造函数中,而不是原型对象中定义属性的原因。
在通过原型继承时,原型会变成另一个类型的实例,于是,原先的实例属性也就顺理成章的成为了原型属性。如下:
function Parent() {
this.friend = ['aa', 'bb'];
}
function Child() {}
Child.prototype = new Parent();
var xiaoming = new Child();
xiaoming.friend.push('cc');
console.log(xiaoming.friend) // ["aa", "bb", "cc"]
var xiaohong = new Child();
console.log(xiaohong.friend) // ["aa", "bb", "cc"]
第二个问题是,在创建子类的实例时,无法向父类传递参数。
因此,实践中很少单独使用原型链。
借用构造函数
思想:在子类构造函数内部,调用父类构造函数。结合 call()
或 apply()
方法,可以在实例上执行父类构造函数,并传参:
function Parent() {
this.friend = ['aa', 'bb'];
}
function Child() {
// 继承了 父类属性
Parent.call(this);
}
var xiaoming = new Child();
xiaoming.friend.push('cc');
console.log(xiaoming.friend) // ["aa", "bb", "cc"]
var xiaohong = new Child();
console.log(xiaohong.friend) // ["aa", "bb"]
传参
相对于原型链继承而言,借用构造函数模式有个很大的优势,就是可以向父类传递参数:
function Parent(skill) {
this.skill = skill;
}
function Child() {
// 继承了 父类属性。可以实现多继承,call多个父类对象
Parent.call(this, 'code');
// 实例属性
this.age = 18;
}
var xiaoming = new Child();
console.log(xiaoming.skill) // code
问题
既然是借用构造函数,那么也无法避免构造函数模式存在的问题:方法都在构造函数中定义,因此函数复用就无从谈起了。
组合继承
组合继承就是同时采用 原型链继承 和 构造函数继承,发挥二者之长,解决各自不足。
通过原型链实现原型属性和方法的继承,通过构造函数实现对实例属性的继承。
function Parent(skill) {
this.skill = skill;
this.parentAge = 56;
this.friend = ['aa', 'bb']
}
Parent.prototype.getParentAge = function() {
return this.parentAge;
};
function Child(skill, age) {
// 继承了 父类属性(也可以叫原型属性)
Parent.call(this, skill); // 第二次调用父类
// 实例属性
this.childAge = age;
}
// 继承方法
Child.prototype = new Parent(); // 第一次调用父类
Child.prototype.constructor = Child; // 重新指向子类
// 实例方法
Child.prototype.getChildAge = function() {
return this.childAge;
};
var xiaoming = new Child('code', 18);
var xiaohong = new Child('sing', 16);
这种继承方法是 JS 中最常用的继承模式。
注意上面 Child.prototype.constructor = Child
重新指向了 Child
,因此 前面那个图中浅色的 constructor
连线就恢复了。
问题
无论在什么情况下,都会调用两次父类构造函数。第一次调用父类构造函数时,子类的原型
会得到 parentAge friend 等属性;第二次调用父类构造函数时,会在新对象
上创建了 parentAge friend 等属性。当我们访问这两个属性时,实例中的属性会屏蔽掉子类原型中的属性。
要解决这个问题,可以使用 寄生组合式继承
,而这个模式又依赖 寄生式继承
,而 寄生式继承
又依赖 原型式继承
,因此,先来看看 原型式继承
和。
原型式继承
function object(o) {
function F(){};
F.prototype = o;
return new F();
}
本质上来讲,object()
方法对传入的对象执行了一次浅复制。
es5 对这种模式进行了规范化,新增 Object.create()
方法。
Object.create()
方法接收两个参数,传一个参数的时候,跟 object()
方法行为相同;第二个参数跟 Object.defineProperties()
方法的第二个参数一样,可以通过描述符自定义每个属性的行为。
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = Object.create(person, {
name: {
value: "Greg",
}
});
console.log(anotherPerson.name); // "Greg"
console.log(anotherPerson.friends); // ["Shelby", "Court", "Van"]
在没有必要兴师动众的创建构造函数,而只是想让一个对象与另一个对象保持类似的情况下,原型式继承完全可以胜任。
不足
引用类型共享问题。
寄生式继承
function createAnother(original){
var clone = object(original);
clone.sayHi = function(){
alert("hi");
};
return clone;
}
寄生组合式继承
function object(o) {
function F(){};
F.prototype = o;
return new F();
}
function inheritPrototype(child, parent) {
var prototype = object(parent.prototype);
prototype.constructor = child;
child.prototype = prototype;
};
function Parent(skill) {
this.skill = skill;
this.parentAge = 56;
this.friend = ['aa', 'bb']
}
Parent.prototype.getParentAge = function() {
return this.parentAge;
};
function Child(skill, age) {
// 继承了 父类属性(也可以叫原型属性)
Parent.call(this, skill);
// 实例属性
this.childAge = age;
}
// Child.prototype = new Parent();
inheritPrototype(Child, Parent);
// Child.prototype = Object.assign({}, { ...Parent.prototype }, { constructor: Child })
// 实例方法
Child.prototype.getChildAge = function() {
return this.childAge;
};
var xiaoming = new Child('code', 18);
var xiaohong = new Child('sing', 16);
开发人员普遍认为这种模式是最理想的继承范式。
观察代码,发现,除了将 Child.prototype = new Parent()
替换为 inheritPrototype(Child, Parent)
外,其他的都一样。
继续查看 inheritPrototype
函数,发现它首先复制了父类的原型对象,然后将其赋值给子类的原型。
怎么复制父类的原型对象呢?是通过 object()
方法实现的(也可以通过其他方式实现,比如 Object.create()方法实现)。
思考:是不是只要能将父类的原型对象包含在子类的原型对象中,应该就能达到类似的效果呢? 例如 Child.prototype = Object.assign({}, { ...Parent.prototype }, { constructor: Child })
function Parent(skill) {
this.skill = skill;
this.parentAge = 56;
this.friend = ['aa', 'bb']
}
Parent.prototype.getParentAge = function() {
return this.parentAge;
};
function Child(skill, age) {
// 继承了 父类属性(也可以叫原型属性)
Parent.call(this, skill);
// 实例属性
this.childAge = age;
}
Child.prototype = Object.assign({}, { ...Parent.prototype }, { constructor: Child })
// 实例方法
Child.prototype.getChildAge = function() {
return this.childAge;
};
var xiaoming = new Child('code', 18);
var xiaohong = new Child('sing', 16);
注意,上图是我个人理解的,不知道对不对,欢迎讨论,指正。
最后
相当于把书抄了一遍。书读百遍,其义自见,加油。
关系图是个人理解画出来的,不知道正确不正确,仅供参考。