公有属性继承的7种方式总结(继承系列-4)
继承的几种方式分析
用自己的理解,系统整理下以上几种继承,从简至更简更高级。总结如下:
前言:
1、共享原型的继承方式
2、原型链继承的方式
3、圣杯前身
4、圣杯模式的继承
5、高级模式:变废为宝
6、es6中的继承 - setPrototypeOf
仿写实现:
扩展:Object.getPrototypeOf(b.prototype)
7、Object.create()
公有属性继承的7种方式 - 关键代码总结
总结一下
最后验证代码
思考
前言:
接下来简写字母表示的含义如下:
-
A
表示下例代码中的Person构造函数, -
A实例化对象 a
= new Person() = person, -
B
表示下例代码中的Man构造函数 -
B实例化对象 b
= new Man() = man, -
中转 Temp
函数不变
/* 构造函数 */
function Person(name) {
this.name = name; //私有属性
return this;
}
Person.prototype.eat = function () { //公有属性/方法
console.log('I can eat');
return this;
}
var person = new Person('构造函数');
console.log(person);
function Man(sex) {
Person.call(this, '继承私有属性'); //借用构造函数的方式实现其他对象私有属性的继承。
this.sex = sex;
}
1、共享原型的继承方式
比如让B.prototype = A.prototype
。
这么做的缺点是大家成了绑在一条绳上的蚂蚱,一损俱损、一改都改。
且B构造出来的实例化对象的constructor
成了A,而不是B了。这就乱套了,瞎认祖宗了。
2、原型链继承的方式
比如让B.prototype = new A()
。
实现原理是原型指向构造函数的实例化对象。通过实例化对象的__proto__
去得到构造函数的原型。
缺点也是B构造出来的实例化对象的constructor
成了A,而不是B了。这就乱套了,认贼作父了。
另外缺点还有A构造函数的实例化对象身上的私有属性也被继承了。
所谓“认贼作父”:
A的实例化对象的__proto__
指向的就是A的原型,A原型上的constructor
指向A本身这本没有错。
但因为你把人家B的原型修改了,B的原型成了A的实例化对象,B构造出来的实例化对象b的__proto__
就是A构造出来的实例化对象a,因此导致顺着原型链的关系,A成了B实例化对象b的构造类。但实际b的构造类应该是B。
3、圣杯前身
为了解决以上问题,新方案是建一个第三方Temp函数中转一下。
让 Temp.prototype = A.prototype;
让 B.prototype = new Temp();
弊端:这样还是改变不了B的实例化对象得到的constructor
指向A这种认贼作父的局面。
为什么指向不对?
原因很简单:B的实例化对象(b)沿着__proto__
查找constructor
。
因b.__proto__ == B.prototype = new Temp()
,而new Temp得到的是个实例化对象,普通对象身上没有constructor
,只有原型对象身上有。
所以还得沿着原型链找到Temp实例化对象的__proto__
,即Temp.prototype,而Temp.prototype == A.prototype
,这次终于找到了prototype原型对象、也找到了constructor。但是遗憾的是,这个prototype是A的,A.prototype.constructor === A;
,所以最后结果还得是A。
4、圣杯模式的继承
没办法了,我们知道constructor是谁,但是JS引擎搞混了。
所以,只能在上一个基础上,人为的去修改constructor
,并将这块代码封装起来,得到终极的封装代码“圣杯模式”:
(function () {
var Inherit = function () {};
Inherit.prototype = Person.prototype;
/* 上一句可以优化,可以利用Object.create简化这一步。改成:
var protoType = Object.create(Person.prototype);
Man.prototype = protoType;
这样写, 也就是下边第7条的意思
*/
Man.prototype = new Inherit();
Man.prototype.constructor = Man; //在prototype添加固定属性,中途拦截一下constructor
Man.prototype.uber = Person.prototype; // 设置他的超类
}());
5、高级模式:变废为宝
prototype.__proto__
指向的是Object.prototype
,但Object原型上的内容我们基本不会用、且每个对象原型链的最后都是Object的原型,即使被改了也能最终找到Object原型。那既然这样,为何不从Object的原型下手改一下?
让B原型上实例化对象指向A原型。Man.prototype.__proto__ = Person.prototype;
这堪比将黄河改道啊!简直不要太高级。直呼内行!
冷静一下啊,缺点还是有的,就是__proto__
作为隐式属性,是系统自己的属性,不建议我们去修改,如果理解不透彻容易改错,所以不推荐使用。
这么好的思路不让用岂不是可惜?
不必悲伤,因为官方内部利用这个原理帮我们实现了:
6、es6中的继承 - setPrototypeOf
该方法就是让B的原型指向A的原型。
代码如下
Object.setPrototypeOf(B.prototype, A.prototype)
其原理同第五条,只不过是es6给我们新增的官方用法。更安全、更可靠。
仿写实现:
Object.setPrototypeOf = function(_pro, proto){
_pro.__proto__ = proto;
return _pro;
}
扩展:Object.getPrototypeOf(b.prototype)
MDN:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getPrototypeOf
7、Object.create()
有了上一个Object的方法,联想到另一个es5的Object.create()。
本意用来创建一个新对象,第一个参数是一个对象,用来当做新对象的__proto__
,第二个参数是一个配置对象,和Object.defineProperties()
的第二个参数一致。
因此,我们修改原型指向也可以用这个方法创建出来的对象:
Man.prototype = Object.create(Person.prototype, {
constructor: {
value: Man
}
});
公有属性继承的7种方式 - 关键代码总结
1、原型直接指向原型,简单粗暴
Man.prototype = Person.prototype;
2、原型指向实例化对象
Man.prototype = new Person('继承第二种');
3、中转函数,粗糙圣杯
function Temp(){};
Temp.prototype = Person.prototype;
Man.prototype = new Temp();
4、精美圣杯模式
(function(){
var Inherit = function(){};
Inherit.prototype = Person.prototype;
Man.prototype = new Inherit();
Man.prototype.constructor = Man;// 在prototype添加固定属性,中途拦截一下constructor
Man.prototype.uber = Person.prototype; // 设置他的超类
}());
5、高级模式:直接改变原型上的隐式原型
Man.prototype.__proto__ = Person.prototype;
6、Object.setPrototypeOf(B.prototype, A.prototype);
Object.setPrototypeOf(Man.prototype,Person.prototype); // 第五条原理的官方实现
7、Object.create(A.prototype,{…})
Man.prototype = Object.create(Person.prototype, { // class中的继承原理写法
constructor: {
value: Man
}
});
总结一下
从第五条开始,这个思路高明的地方所在:
这么做也很合理,因为man.__proto__指向Man.prototype,还有自己的使命不能被直接替换,但是Man.prototype.__proto__作为Object.Prototype貌似除了要用Object定义的方法外没啥作用,所以从这一骨节上嫁接一下,把Person.prototype按到这里,幸运的是,Person.prototype.__proto__也有Object.prototype,所以我们不仅赚了夫人,也没赔了兵。
最后验证代码
分别用上边的方案实现继承后,可以用下边的代码验证下效果。
var man = new Man('male');
console.log(man);
console.log(man.constructor);
思考
这些方法其实都很麻烦,为什么还要人为的去搞一下继承?
好像要科学方法改细胞似的。就不能天生继承吗?
这就是class出现的原因了。
class写法更简单,目的更明确。
具体写法和做法,可以看后续es6 - 《class》篇章
让我们一起携手同走前端路!
关注公众号回复【加群】即可