网上充斥着各种关于Js的继承的文章,一看标题大约都是“Js继承的5种方式、8种办法”,当然总结得也很好,只是有些地方个人觉得有凑数的嫌疑,仔细分析似乎有些牵强附会的意思。这篇文章就通过原型的角度重新讲讲到底什么才是Js的继承,如果你对Js的原型还不是很熟悉,请移步。
一、什么情况才算是子继承了父?
继承,在现实生活中是富二代发家致富的必备手段,在我们程序语言的世界里面同样也是程序“发家致富”的手段。顾名思义,继承就是“把你的东西给我”,当然不能白给你,除非你是人家儿子,或者你认人家当干爹,总之,就是继承了之后你和被继承者之间有一定的继承关系。
这种继承关系在Js语言中怎么判断?怎么才知道我是不是继承的你?讨论这个之前,先了解一个关键字:instanceof。
instanceof用来判断某个function的prototype属性是否存在于另外一个对象的原型链上。
什么意思?某个function的prototype属性存在另外一个对象的原型链上意味着什么?通过这篇讨论我们知道,在Js中,所有的值都是由function构建而来,再通过这篇讨论我们又知道,function的prototype属性是提供给由它所构建的值的__proto__属性引用的。
也就是说,某个function的prototype属性,一定存在于由它所构建的对象的原型链上,除非手动修改
function Foo() {}
let foo = new Foo();
// 可以知道
foo.__proto__ === Foo.prototype;
// 因为Foo的prototype属性存在于foo的原型链上,所以
foo instanceof Foo === true
所以说,如果某个function的prototype属性存在于另外一个对象的原型链上,我们就视为这个对象是由该function构建而来,所以该对象instanceof该function。那么回答上面的问题:继承关系在Js语言中怎么判断,或者说怎么体现?
一个对象如果是由子function构建而来,那么它一定也要被视为由父function构建而来,也就是说子function构建出来的对象一定也要 instanceof父function。
换言之,就是如果A继承了B,那么A的prototype属性和B的prototype都要存在于new A()对象的原型链上。
function A() {this.name_a = 'name_a';}
function B() {this.name_b = 'name_b';}
let a = new A();
a.__proto__ === A.prototype;
a instance A === true;
这是毋庸质疑的,是由Js语言的new关键字从语言层面实现的,因为a.__proto__只能引用一个值,所以要想B.prototype存在于a的原型链上,只能是通过A.prototype.__proto__属性往下接。
A.prototype.__proto__ = B.prototype;
a instance A === true;
a instance B === true; // 哇哦!
// 但是
a.name_a === 'name_a';
a.name_b === undefined;
捋一下,因为name_b属性在a自己的属性列表里面没有找到,
然后会去a.__proto__,也就是A.prototype里面找,也没有,
然后会去A.prototype.__proto_
_,也就是B.prototype里面找,也没有,
然后会去B.prototype.__proto__,也就是Object.prototype里面找,也没有,
然后会去Object.prototype.__proto__里面找,然而发现Object.prototype.__proto__是null,所以最终没有找到,返回undefined。
所以以上方法虽然语法上继承了,但是没有实质的意义,a只能继承B.prototype的属性,因为虽然B.prototype在a的原型链上了,但是name_b并不在a的原型链上,注意name_b不是B的属性,而是由B构建的对象的属性。所以要想name_b在a的原型链上,就需要由B构建的对象也要在a.原型链上。
let b = new B();
A.prototype.__proto__ = b;
a instance A === true;
a instance B === true;
a.name_a === 'name_a';
a.name_b === 'name_b';
因为,B.prototype在b的原型链上,b又在a的原型链上,所以B.prototype也就在a的原型链上了,这样a不仅可以继承b的属性,也可以继承B.prototype的所有属性,实现了整个Js原型继承的逻辑自洽。
也有这样做的
A.prototype = b; // 而不是A.prototype.__proto__ = b,相当于原型链中删掉原有的A.prototype,直接将A.prototype替换成b,和A.prototype.__proto__ = b有一点区别
// 请看区别
function A() {
this.name_a = 'name_a';
}
A.prototype.say = function() {
console.log(this.name_a);
}
A.prototype = b; // say方法就没有了,所以必须将A.prototype.xxx写在这句话后面。
A.prototype.__proto__ = b; // say 方法健在,因为并没有删除A.prototype,而是把A.prototype.__proto__修改了,A.prototype.__proto__原先指向Object.prototype
二、换汤不换药的其他写法之一详解
由于Js语言的灵活性,看似可以通过各种方式实现继承,其实本质都是一样的。
function P() {
this.name = 'p';
this.say = function() {
console.log(this.name);
}
}
function C() {
this.collage = 'collage';
P.call(this);
}
let c = new C();
c.collage === 'collage';
c.say(); // p
貌似还不错,甚至有一些人看到P.call(this)这样的写法觉得很优雅,很牛逼的样子,其实就是调用P,然后把this指向C,它比new的方式调用P少做了一些事情。假如父类方法有参数呢?
function P(name, age) {
this.name = name;
this.age = age;
this.say = function() {
console.log(this.name + ': ' + this.age);
}
}
function C() {
this.collage = 'collage';
P.apply(this, arguments);
}
let c = new C('boyce', 18);
c.collage === 'collage';
c.say(); // p: 18
上面的继承方式虽然很别扭的支持了参数,之所以别扭,是因为非要把灵活的Js当作死板的Java用,或者说非要把 基于function的Js当作基于Class的Java用。当然上面的方式出了别扭还有其他更严重的问题。
P.prototype.hello = function() {
console.log('hello: ' + this.name);
}
// 第一个问题
c instanceof P === false;
// 第二个问题
c.hello(); //报错
其实上面两个问题是一个问题,就是P.prototype不在c的原型链中。解决方法也很简单
C.prototype.__proto__ = P.prototype;
// 这里为什么不是像原型链继承一样是 = new P() 呢?而是 = P.prototype
// 那是因为 new P()的目的是为了继承P所构建对象的属性,
// 而这种方式已经通过P.apply(this, arguments); 继承了P所构建对象的属性了。
c instanceof P === true;
c.hello(); // hello boyce
思考
C.prototype.__proto__ = P.prototype;
C.prototype = P.prototype; // 可以这么写么?
完整代码
function P(name, age) {
this.name = name;
this.age = age;
this.say = function() {
console.log(this.name + ': ' + this.age);
}
}
P.prototype.hello = function() {
console.log('hello: ' + this.name);
}
function C() {
this.collage = 'collage';
P.apply(this, arguments);
}
C.prototype.getCollage = function() {
return this.collage;
}
C.prototype.__proto__ = P.prototype;
let c = new C('boyce', 18);
c instanceof C === true;
c instanceof P === true;
c.say(); // p: 18
c.hello(); // hello: boyce
c.collage === 'collage';
c.getCollage() === 'collage';
三、object之间的分享
上面描述的都是一个function继承另一个function,实际上不是function继承,而是通过在function上做手脚,使function构建的对象之间实现继承。在function上做手脚的好处是所有由该function构建出来的对象都继承了父function的属性,当然我们也可以通过修改原型链实现object之间的属性分享。
var p = {
name: 'p',
hello: function() {
console.log('hello ' + this.name);
}
};
// p是Object这个function构建而来,所以p.__proto__ == Object.prototype
// 要想c继承p很简单,修改c的__proto__属性
var c = {name:'c'};
c.__proto__ = p;
c.hello(); // hello c
c instanceof p
// 直接报错,因为p不是一个function,也可以理解,因为c并不是由p构建而来,而是他俩都是有Object function构建而来,应该不算是继承,更像是兄弟分享。
四、有些事,你搞着搞着就晕了
还是那句话,由于Js语言的灵活特性,你还可以想出很多所谓实现继承的方式,比如
function C() {
let p = new P(); // 把call的调用方式换成new的调用方式
p.other = 'other';
return p;
}
再比如
function C() {
let p = new P();
// 通过遍历p的所有自有属性 设置到C.prototype中
}
无论什么方式,都没有原型继承优雅(原型就是为了继承而生的),或者说都是原型继承的变体,归根结底就是怎么复制父类属性的问题。还有就是无论什么继承都需要处理原型链,才能实现继承的逻辑自洽。
所以,请忽略其他乱七八糟的继承,你大概也不会真正用到它们,知道怎么回事就行。但是关于Js的原型,一定得掌握,这可以说是Js继承的本质。