前言
很久以前学习《Javascript语言精粹》时,写过一个关于js的系列学习笔记。
最近又跟别人讲什么原型和继承什么的,发现这些记忆有些模糊了,然后回头看自己这篇文章,觉得几年前的学习笔记真是简陋。
所以在这里将这篇继承重新更新一下,并且加上ES6的部分,以便下次又对这些记忆模糊了,能凭借这篇文章快速回忆起来。
本篇文章关于ES5的继承方面参考了《Javascript语言精粹》和《JS高程》,后面的ES6部分通过使用Babel转换为ES5代码,然后进行分析。
用构造函数声明对象
既然讲到继承,那么有必要先说一下对象是如何通过构造器构造的。
先看以下代码:
function Parent(){
this.type='人'
this.name='爸爸';
this.body={
weight:50
}
}
当声明了Parent这个构造函数后,Parent会有一个属性prototype,这是Parent的原型对象。
可以相当于有以下这段隐式代码
Parent.prototype={constructor:Parent}
执行构造函数构造对象时,会先根据原型对象创造一个对象A,然后调用构造函数,通过this将属性和方法绑定到这个对象A上,如果构造函数中不返回一个对象,那么就返回这个对象A。
原型链
然后可以看下js最初的继承。
以下为最基本的原型链玩法
function Parent(){
this.type='人'
this.name='爸爸';
this.body={
weight:50
}
}
function Child(){
this.name='儿子';
}
Child.prototype=new Parent();
var child=new Child();
console.info(child.name);//儿子
console.info(child.type);//人
通过以上发现,我们的child继承了原型对象的type。
借用构造函数:保证父级引用对象属性的独立性
原型链最大的问题在于,当继承了引用类型的属性时,子类构造的对象就会共享父类原型对象的引用属性。
我们先来看之前的例子,child不仅继承了parent的type,也继承了body。
修改一下以上的代码:
var child=new Child();
child.body.weight=30;
var man=new Child();
console.info(man.type);//30
这里可以看到,原型中的引用对象被child和man共享了。
子类构造函数构造child和man两个对象。
当他们读取父级属性时,读取的是同一个变量地址。
如果在子级对象中更改这些属性的值,那么就会在子级对象中重新分配一个地址写入新的值,那么就不存在共享了属性。
但是上面的例子中,是更改引用对象body里的值weight,而不是body。
这样的结果就是body的变量地址不变,导致父级引用对象被子级对象共享,失去了各个子级对象应该有的独立性(这里我只能用独立性来说明,为了避免和后面讲到的私有变量弄混)。
于是就有了借用构造函数的玩法:
function Parent(){
this.type='人'
this.name='爸爸';
this.body={
weight:50
}
}
function Child(){
Parent.call(this);
this.name='儿子';
}
Child.prototype=new Parent();
var child=new Child();
console.info(child.name);//儿子
console.info(child.type);//人
实际上借用构造函数是在在子类的构造函数中借用父类的构造函数,然后就在子类中把所有父类的属性都声明了一次。
然后在子类构造对象后,获取属性时,因为子对象已经有了这个属性,那么就不会去查找原型链上的父对象的属性了,从而保证了继承时父类中引用对象的独立性。
组合继承:函数的复用
组合嘛,实际上就是利用了原型链来处理一些需要共享的属性或者方法(通常是函数),以达到复用的目的,又借用父级的构造函数,来实现属性的独立性
在上面的代码中加入
Parent.prototype.eat = function(){
// 吃饭
}
这样Child构造的对象就可以继承到eat这个函数
原型继承:道格拉斯的原型式继承
这个方式是道格拉斯提出来的,也就是写《JavaScript语言精粹》的那个人。
实际上就是Object.create。
它的最初代码如下:
Object.create=function(origin){
function F(){};
F.protoType=origin;
return new F();
}
var child=Object.create(parent);
这个玩法的思路就是以后我们不要用js的那种伪类写法了,摆脱掉类这个概念,而是用对象去继承对象,也就是原型继承,因为相对于那些基于类的语言,js有自己进行代码重用的方式。
但是这个样子依然会有问题,就是在原型继承中,同一个父对象的不同子对象共享了继承自父对象的引用类型。
导致的结果就是一个子对象的值发生了改变,另外一个也就变了。
也就是我们说的引用属性独立性的问题。
寄生式继承:增强原型继承子对象
道格拉斯又提出了寄生式继承:
function createObject(origin){
var clone=Object.create(origin);
clone.eat=function(){
// 吃饭
}
}
这种继承你可以看做毫无意义,因为你一般会写成下面这样:
var clone=Object.create(origin);
clone.eat=function(){
// 吃饭
}
这个样子写也没毛病。
当然你可以认为这是一次重构,从提炼一个业务函数的角度去理解就没毛病了。比如通过人这个原型创造男人这个对象。
寄生组合式继承:保证原型继承中父级引用对象属性的独立性
组合继承的问题在于,会两次调用父级构造函数,第一次是创造子类型原型的时候,另一次是子类型构造函数内部去复用父类型构造函数。
对于一个大的构造函数而言,可能对性能产生影响。
而原型继承以及衍生出的寄生式继承的毛病就是,引用类型的独立性有问题。
那么堪称完美的寄生组合式继承就来了,但是在之前,我们先回顾下这段组合式继承的代码:
function Parent(){
this.type='人'
this.name='爸爸';
this.body={
weight:50
}
}
function Child(){
Parent.call(this);
this.name='儿子';
}
Parent.prototype.eat=function(){
//吃
}
Child.prototype=new Parent();
var child=new Child();
那么现在我们加入寄生式继承的修改:
function Parent(){
this.type='人'
this.name='爸爸';
this.body={
weight:50
}
}
function Child(){
Parent.call(this);
this.name='儿子';
}
Parent.prototype.eat=function(){
//吃
}
function inheritPrototype(childType,parentType){
var prototype=Object.create(Parent.prototype);
prototype.constructor=childType;
childType.prototype=prototype
}
inheritPrototype(Child,Parent)
var child=new Child();
或者我们把inheritPrototype写得更容易懂一点:
function inheritPrototype(childType,parentType){
childType.prototype=Object.create(Parent.prototype);
childType.prototype.constructor=childType;
}
记住这里不能直接写成
childType.prototype=Parent.prototype
表面上看起来可以,但是childType上原型加上函数,那么父级就会加上,这样不合理。
通过inheritPrototype,Child直接以Parent.prototype的原型为原型,而不是new Parent(),那么也就不会继承Parent自己的属性,而又完美继承了Parent原型上的eat方法。
通过借用构造函数又实现了引用属性的独立性。
那么现在我们来看就比较完美了。
只不过这种方式我平常都基本不用的,因为麻烦,更喜欢一个Object.create解决问题,只要注意引用对象属性的继承这个坑点就行。
函数化:解决继承中的私有属性问题
以上所有生成的对象中都是没有私有属性和私有方法的,只要是对象中的属性和方法都是可以访问到的。
这里为了做到私有属性可以通过函数化的方法。
function Parent(){
this.type='人'
this.name='爸爸';
this.body={
weight:50
}
}
function Child(){
Parent.call(this);
this.name='儿子';
}
inheritPrototype(Child,Parent)
var ObjectFactory=function(parent){
var name='troy';//私有变量
result=new Child();
result.GetMyName=function(){//这是要创建的对象有的特有方法
return 'myname:'+name;
}
return result;
};
var boy=ObjectFactory(anotherObj);
这个地方实际上用的是闭包的方式来处理。
拷贝继承
这里其实还有一种复制属性的玩法,继承是通过复制父对象属性到子对象中,但是这种玩法需要for in遍历,如果要保持引用对象独立性,还要进行递归遍历。
这里就不介绍了。
它有它的优点,简单,避开了引用对象独立性,并且避开了从原型链上寻找对象这个过程,调用属性的时候更快,缺点是这个遍历过程,对于属性多层级深的对象用这种玩法,不是很好。
关于ES5继承的一些说法
ES5一直都是有伪类继承(也就是通过构造函数来实现继承)和对象继承(我认为原型和拷贝都算这种)两种玩法,一种带着基于类的思想的玩法,一种纯粹从对象的角度去考虑这个事情。
如果加上类的概念的话,确实麻烦,如果是直接考虑原型继承而不用考虑类的话就简单很多。
所以对ES5而言可以更多从对象角度去考虑继承,而不是从类的角度。
ES6:进化,class的出现
在ES6中出现了class的玩法,这种类的玩法使得我在使用ES6的时候更愿意去站在类的角度去思考继承,因为基于类去实现继承更加简单了。
新的class玩法,并没有改变Javascript的通过原型链继承的本质,它更像是语法糖,只是让代码写起来更加简单明了,更加像是一个面向对象语言。
class Parent {
constructor(name){
super()
this.name = name
}
eat(){
console.log('吃饭');
}
}
class Child extends Parent {
constructor(name,gameLevel){
super(name)
this.gameLevel = gameLevel
}
game(){
console.log(this.gameLevel);
}
}
我们可以通过Babel将它转化为ES5:
'use strict';
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var Parent = function () {
function Parent(name) {
_classCallCheck(this, Parent);
this.name = name;
}
_createClass(Parent, [{
key: 'eat',
value: function eat() {
console.log('吃饭');
}
}]);
return Parent;
}();
var Child = function (_Parent) {
_inherits(Child, _Parent);
function Child(name, gameLevel) {
_classCallCheck(this, Child);
var _this = _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).call(this, name));
_this.gameLevel = gameLevel;
return _this;
}
_createClass(Child, [{
key: 'game',
value: function game() {
console.log(this.gameLevel);
}
}]);
return Child;
}(Parent);
然后在这里我们不考虑兼容性,提炼一下核心代码,并美化代码以便阅读:
'use strict';
var _createClass = function () {
function defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key,descriptor);
}
}
return function (Constructor, protoProps, staticProps)
{
if (protoProps)
defineProperties(Constructor.prototype, protoProps);
if (staticProps)
defineProperties(Constructor, staticProps);
return Constructor;
};
}();
function _inherits(subClass, superClass) {
subClass.prototype = Object.create(superClass.prototype);
subClass.__proto__ = superClass;
}
var Parent = function () {
function Parent(name) {
this.name = name;
}
_createClass(Parent, [{
key: 'eat',
value: function eat() {
console.log('吃饭');
}
}]);
return Parent;
}();
var Child = function (_Parent) {
_inherits(Child, _Parent);
function Child(name, gameLevel) {
Child.__proto__.call(this, name);
var _this = this;
_this.gameLevel = gameLevel;
return _this;
}
_createClass(Child, [{
key: 'game',
value: function game() {
console.log(this.gameLevel);
}
}]);
return Child;
}(Parent);
在这里可以看到转化后的代码很接近我们上面讲述的寄生组合式继承,在_inherits中通过Object.create让子类原型继承父类原型(这里多了一步,Child.proto=Parent),而在Child函数中,通过Child.__proto__借用父级构造函数来构造子类对象。
而_createClass不过是把方法放在Child原型上,并把静态变量放在Child上。
总结
总的来说ES6中,用class就好,但是要理解这个东西不过是ES5的寄生组合式继承玩法的语法糖而已,而不是真的变成那种基于类的语言了。
如果有天还让我写ES5代码,父类中没有引用对象,那么使用Object.create是最方便的,如果有,那么可以根据实际情况考虑拷贝继承和寄生组合式继承。