Open phenomLi opened 6 years ago
Javascript中的继承一直是一个难点,因为js有别于其他面向对象的语言,js是基于 原型(prototype) 的。
prototype是一个很难琢磨得透也很难掌握的东西,当然也许有人会跳出来说现在都用ES6,typescript啦,谁还学prototype。这样想就错了,首先,ES6和typescript远远没有你想的这么普及,谁敢把不经过编译的es6和ts直接放到线上跑?编译后还不是一样回到prototype。其次,prototype能干的事情多的去了,不单单只是new或继承几个类,有兴趣可以看看Vue的响应式数组方法是怎么做的。
扯远了,今天写这篇东西是因为刚刚看到了一篇关于js继承的文章,想把一些思考和总结记下来。
为什么在js中继承很麻烦?
因为js中没有extends关键字(ES6前)
extends
因为js既能访问到实例属性也能访问到原型属性
js对象是引用类型
一点一点来分析。
首先在ES6之前,js中是没有又甜又可爱的extends语法糖的,那要继承怎么办,只能自己在现有的js语法里面各种找方法实现(而且还不好找)。
其次,在一个js对象中,既有来自构造函数的属性,也能访问到其_proto_,也就是构造函数的prototype的属性(通常是方法)。这么理解这句话呢?看下面的例子:
_proto_
prototype
const Foo = function() { this.count = 20; }; Foo.prototype.getTotal = function() { return 400; }; const foo = new Foo(); console.log(foo.count); //输出200 console.log(foo.getTotal()); //输出400 console.log(foo.__proto__.getTotal()); //输出400 /* * 输出 { count: 20 } * 可以看到在foo中并没有getTotal这个方法 */ console.log(foo); /* * 输出 { getTotal: [Function] } * 而getTotal是在__proto__中 */ console.log(foo.__proto__);
可以看到,getTotal是绑定在foo的构造函数的prototype中的一个方法,在实例化Foo后,foo既能访问它自身的属性count,也能访问getTotal方法。但是getTotal方法并没有在foo对象里面,所以很明显,当要访问一个对象的某个属性/方法时,js引擎首先在对象里面找,如果找不到,再顺着原型链往上找。foo和foo.__proto__关系如下:
getTotal
foo
Foo
count
foo.__proto__
如果对proto和prototype的关系不了解,或者对原型链有疑惑的,建议先去了解一下,本篇文章不会细讲。
那么,也就是说,想要在js中继承一个类,就必须要做到两点:
子类要继承父类中的属性/方法
子类的prototype要继承父类的prototype
做不到第二点的都不是完整继承。 第一点的常规实现方法是:
//父类 const SuperClass = function() { this.a = 1; }; //子类 const SubClass = function() { /* * 很巧妙的一直做法,因为父类的属性都定义在构造函数里面, * 所以只要在子类的构造函数里面用子类的上下文(this)调用一下父类的构造函数, * 父类的属性就都绑定到了子类的this上面去了 */ SuperClass.call(this); } //实例化子类 const sub = new SubClass(); console.log(sub.a); //输出1
在子类的构造函数里面用一下call(当然也可以用apply,个人比较喜欢用call)调用父类的构造函数就行。
call
apply
然后第二点,思路是这样子:
//简单粗暴地将父类的prototype指向子类的prototype SubClass.prototype = SuperClass.prototype;
由于prototype是对象而不是函数,所以没法用call或者apply的方法了。
但是事情没有这么简单,仔细观察上面的代码:
SubClass.prototype = SuperClass.prototype;
发现问题了吗?js中的对象都是引用类型(对什么是引用类型不了解的可以看看这篇文章:JS中的深拷贝),对象的直接赋值都是改变指针指向的地址,也就是说: 子类的prototype和父类的prototype共享同一片内存空间了。 会造成什么问题?会造成当你想要往子类的prototype里添加属性/方法的时候,父类的prototype也会被修改:
//往子类的prototype添加了一个属性b SubClass.prototype.b = 'phenom'; //父类的prototype也被加上了 console.log(SuperClass.prototype.b); //输出 phenom
这显然不好,但是怎么改进?不就是想要有独立分配的内存空间嘛,很简单,我们有父类SuperClass,我们直接让子类的 prototype 指向父类的实例:
SuperClass
//往父类的prototype里添加一个方法getA SuperClass.prototype.getA = function() { return this.a; }; //子类的prototype指向父类的一个实例 SubClass.prototype = new SuperClass(); //实例化一个子类 const sub = new SubClass(); console.log(sub.getA()); //输出1
为什么这种方法可以?其实父类的实例里面是没有getA这个方法的,getA在父类的prototype里面,但是前面已经说过了,js找一个对象的属性/方法是会顺着原型链往上找的:
getA
/** * 输出 { a: 1 },没有getA方法 */ console.log(SubClass.prototype); /** * 输出 { getA: [Function] },getA方法在这里 * 顺着原型链找到了这里 */ console.log(SubClass.prototype.__proto__);
目前为止他们的恩怨情仇大概是这个样子:
看上图不难发现,有一个地方貌似不太合理(我故意画出来了),就是子类的prorotype的constructor指针居然指向了父类的构造函数:
prorotype
constructor
/** * 输出 [Function: SuperClass] * 原因是我们将父类实例赋值给子类的prototype时,把其constructor属性也一同覆盖了 * 正常情况下子类的prototype的constructor应该指向子类的构造函数的 */ console.log(SubClass.prototype.constructor);
所以我们要做一个修正:
//修正 SubClass.prototype.constructor = SubClass;
我们把上面的所有实现都糅合起来,放进一个函数里面,并且命名为myExtends(不能直接用extends因为是保留字):
myExtends
//父类 const SuperClass = function() { this.a = 1; }; SuperClass.prototype.getA = function() { return this.a; }; //子类 const SubClass = function() { SuperClass.call(this); } const myExtends = function(subClass, superClass) { //子类的prototype指向父类的一个实例 subClass.prototype = new superClass(); //修正 subClass.prototype.constructor = subClass; return subClass; }; myExtends(SubClass, SuperClass); //实例化一个子类 const sub = new SubClass();
这样看起来清晰多了,但是还不完美。上面的代码中,父类的构造函数SuperClass一共被调用了两次,这不是好事,我们要想办法优化一下。
想一下,其实子类的prototype并不关心父类的构造函数中定义了哪些内容,因为父类构造函数中的内容已经在SuperClass.call(this);中继承给了子类的构造函数,我们要的只是父类prototype的内容,所以subClass.prototype = new superClass();是产生了一点冗余的,但是又不能直接赋值,因为父子两个类的prototype需要有独立的内存空间。所以,我们可以找一个能提供独立空间存放父类prototype的‘中间人’:
SuperClass.call(this);
subClass.prototype = new superClass();
const myExtends = function(subClass, superClass) { //中间人函数 const F = function() {}; //存放父类的prototype F.prototype = superClass.prototype; /** * 子类的prototype指向中间人函数的一个实例,具有独立的内存空间, * 但是内存占用非常小,因为这个函数是没有内容的,而且这个中间人函数外界没法修改, * 所以也不会影响父类的prototype */ subClass.prototype = new F(); //修正 subClass.prototype.constructor = subClass; return subClass; };
这样就完美了。 下面贴上完整代码:
//父类 const SuperClass = function() { this.a = 1; }; SuperClass.prototype.getA = function() { return this.a; }; //子类 const SubClass = function() { SuperClass.call(this); } const myExtends = function(subClass, superClass) { //中间人函数 const F = function() {}; //存放父类的prototype F.prototype = superClass.prototype; /** * 子类的prototype指向中间人函数的一个实例,具有独立的内存空间, * 但是内存占用非常小,因为这个函数是没有内容的,而且这个中间人函数外界没法修改, * 所以也不会影响父类的prototype */ subClass.prototype = new F(); //修正 subClass.prototype.constructor = subClass; return subClass; }; myExtends(SubClass, SuperClass); //实例化一个子类 const sub = new SubClass(); console.log(sub.getA()); //输出 1
其实上面这种实现,就是js中使用最普遍的寄生组合继承的实现。结合了各种继承的优点,而且实现起来也比较简单,容易理解。 --EOF--
Javascript中的继承一直是一个难点,因为js有别于其他面向对象的语言,js是基于 原型(prototype) 的。
prototype是一个很难琢磨得透也很难掌握的东西,当然也许有人会跳出来说现在都用ES6,typescript啦,谁还学prototype。这样想就错了,首先,ES6和typescript远远没有你想的这么普及,谁敢把不经过编译的es6和ts直接放到线上跑?编译后还不是一样回到prototype。其次,prototype能干的事情多的去了,不单单只是new或继承几个类,有兴趣可以看看Vue的响应式数组方法是怎么做的。
扯远了,今天写这篇东西是因为刚刚看到了一篇关于js继承的文章,想把一些思考和总结记下来。
如何实现继承
为什么在js中继承很麻烦?
因为js中没有
extends
关键字(ES6前)因为js既能访问到实例属性也能访问到原型属性
js对象是引用类型
一点一点来分析。
壹
首先在ES6之前,js中是没有又甜又可爱的
extends
语法糖的,那要继承怎么办,只能自己在现有的js语法里面各种找方法实现(而且还不好找)。貳
其次,在一个js对象中,既有来自构造函数的属性,也能访问到其
_proto_
,也就是构造函数的prototype
的属性(通常是方法)。这么理解这句话呢?看下面的例子:可以看到,![](https://github.com/phenomLi/myBlog/raw/master/photos/WX20180126-204943.png)
getTotal
是绑定在foo
的构造函数的prototype
中的一个方法,在实例化Foo
后,foo
既能访问它自身的属性count
,也能访问getTotal
方法。但是getTotal
方法并没有在foo
对象里面,所以很明显,当要访问一个对象的某个属性/方法时,js引擎首先在对象里面找,如果找不到,再顺着原型链往上找。foo
和foo.__proto__
关系如下:那么,也就是说,想要在js中继承一个类,就必须要做到两点:
子类要继承父类中的属性/方法
子类的prototype要继承父类的prototype
做不到第二点的都不是完整继承。
第一点的常规实现方法是:
在子类的构造函数里面用一下
call
(当然也可以用apply
,个人比较喜欢用call
)调用父类的构造函数就行。然后第二点,思路是这样子:
由于
prototype
是对象而不是函数,所以没法用call
或者apply
的方法了。叁
但是事情没有这么简单,仔细观察上面的代码:
发现问题了吗?js中的对象都是引用类型(对什么是引用类型不了解的可以看看这篇文章:JS中的深拷贝),对象的直接赋值都是改变指针指向的地址,也就是说:
子类的
prototype
和父类的prototype
共享同一片内存空间了。会造成什么问题?会造成当你想要往子类的
prototype
里添加属性/方法的时候,父类的prototype
也会被修改:这显然不好,但是怎么改进?不就是想要有独立分配的内存空间嘛,很简单,我们有父类
SuperClass
,我们直接让子类的prototype
指向父类的实例:为什么这种方法可以?其实父类的实例里面是没有
getA
这个方法的,getA
在父类的prototype
里面,但是前面已经说过了,js找一个对象的属性/方法是会顺着原型链往上找的:目前为止他们的恩怨情仇大概是这个样子:
看上图不难发现,有一个地方貌似不太合理(我故意画出来了),就是子类的
prorotype
的constructor
指针居然指向了父类的构造函数:所以我们要做一个修正:
寄生组合继承
我们把上面的所有实现都糅合起来,放进一个函数里面,并且命名为
myExtends
(不能直接用extends
因为是保留字):这样看起来清晰多了,但是还不完美。上面的代码中,父类的构造函数
SuperClass
一共被调用了两次,这不是好事,我们要想办法优化一下。想一下,其实子类的
prototype
并不关心父类的构造函数中定义了哪些内容,因为父类构造函数中的内容已经在SuperClass.call(this);
中继承给了子类的构造函数,我们要的只是父类prototype
的内容,所以subClass.prototype = new superClass();
是产生了一点冗余的,但是又不能直接赋值,因为父子两个类的prototype
需要有独立的内存空间。所以,我们可以找一个能提供独立空间存放父类prototype
的‘中间人’:这样就完美了。
下面贴上完整代码:
其实上面这种实现,就是js中使用最普遍的寄生组合继承的实现。结合了各种继承的优点,而且实现起来也比较简单,容易理解。
--EOF--