看了冴羽大大的avaScript深入之继承的多种方式和优缺点,记录一些理解
继承到底是要干什么
js里面继承总给人怪怪的感觉,大概是因为js类的概念太奇怪了,和其他语言继承的观感上总有些不同
其他语言里,类继承是根本目的方法和属性的复用,那么js里也这么理解。
需要注意的是,原型链和继承其实不一样,尽管属性访问会往回查找原型链,但是继承需要对象的属性时这个实例独有的,冴羽大大提到说是委托,我觉得叫代理也ok
js的类是个伪概念,首先要确定的是,js类的本质就是一个函数,这个函数会构造一个Object。
那么我们要复用什么呢?首先是属性,子类的对象需要有父类的所有属性。那么就可以调用一下父类的构造函数的属性都初始化到子对象里面。其次是方法。如果是挂在this下面的,调用构造函数的时候就带上了,关键是要是在prototype里面的。既然属性访问会查找原型链,那就把父类的原型添加到子类的原型链中就行了。
我这里说得不够清楚,原型上挂在的包括属性和方法
从上面说的内容继承我们主要要干几件事:
- 把父类的属性和方法想办法绑定到子类上
- 把父类添加到子类的原型链上
从头看继承方法
原型链继承
直接把冴羽大大代码拿过来
function Parent () {
this.name = 'kevin';
}
Parent.prototype.getName = function () {
console.log(this.name);
}
function Child () {
}
Child.prototype = new Parent();
var child1 = new Child();
console.log(child1.getName())
因为Child.prototype是一个Parent对象,所以属性访问就会遍历到父对象,对父类原型方法的访问会遍历到Child.prototype.__proto__时找到,这就是Parent.prototype
但是这个方法中,属性其实并不是子对象自己持有的,严格来说不算继承,还是属于上面说的代理。
还有就是因为变量引用的关系,非基础类型所有子类都是一样的。根本原因就是上面说的,属性并不由对象自己持有。
构造函数继承
function Parent () {
this.names = ['kevin', 'daisy'];
}
function Child () {
Parent.call(this);
}
var child1 = new Child();
child1.names.push('yayu');
console.log(child1.names); // ["kevin", "daisy", "yayu"]
var child2 = new Child();
console.log(child2.names); // ["kevin", "daisy"]
这个办法的核心是用call函数让把子类的this拿给构造函数使用,让所有属性都直接挂在子类的对象中。这和python继承的方式是一样的,很好理解。
那么prototype中的方法能够被访问到吗?
Parent.prototype.t = function () {}
child1.t // undefined
是不能的。原型链没有处理过,自然访问不到。所以这个方法的缺陷是很明显的。
组合继承
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.constructor = Child;
从上面两个可以发现,原型链继承做不到属性独立,经典继承做不到原型链处理,那融合起来就行。
用原型链继承的思路,把父类实例作为子类prototype
再用经典继承的思路,用call调用一次父类构造函数把属性创建在子对象上。
这个方法的问题是,父类的构造函数执行了两次,处理原型链一次,绑定属性一次。太浪费了,这就是寄生继承出现的原因。
寄生继承
我们再强调一遍继承要做的事:
- 把父类的属性和方法想办法绑定到子类上
- 把父类添加到子类的原型链上
实际上组合继承在每一件事上用了一次构造函数。那么我们可不可少一次呢?
绑定子类肯定是需要把子类的this给父类构造一遍的。这砍不掉,那就考虑原型链。
其实看原型链继承的方法,巧妙地利用了原型链的遍历规则,但其实并不是很雅观。直接把一个对象附加到原型上总有地方怪怪的,有种投机取巧的感觉。
那么有没有其他方式呢?就是通过中间量。所以就叫寄生了。
思路是这样的:
我们期望得到的结果是, 能让子类的原型间接和父类联系起来。
如果能有一个原型, prototype是父类的, 然后让子类的prototype指向这个原型,不就联系上了吗?
Obj.prototype = Parent.prototype
Child.prototype = new Obj()
可以发现,prototype被添加到了原型链中,而且并没有调用构造函数
其实可以看到寄生继承的核心就是,构造一个instance,让instance的prototype代理父类的prototype。
看一看代码
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();
有个问题:一定需要中间变量吗,直接把Chile的prototype指向Parent不可以吗?
Child.prototype = Parent.prototype
其实这样做是能有效果的,但是两个prototype指向了同一个对象的引用,子类的独立性就没了。这就是为什么处理原型链时必须new一次。