作者 | 张文慧
开场白
大三下学期结束时候, 一个人跑到帝都来参加各厂的面试, 免不了的面试过程中经常被问到的问题就是 JS 中如何实现继承, 当时的自己也是背熟了实现继承的各种方法, 回过头来想想却不知道__proto__是什么, prototype 是什么, 以及各种继承方法的优点和缺点, 想必有好多刚入坑的小伙伴有着跟我一样的体验, 这篇文章将从基础概念出发, 进一步说明 JS 继承, 以及各种继承方法的优缺点, 希望对看这篇文章的你有所帮助, 如果你是见多识广的大佬, 既然看到这里了, 不妨继续看下去, 指点一二, 让新入坑的小伙伴更好的成长.(如果你都看到这了, 透露一下文末有彩蛋嗷!) 下面, 我们进入正题:
设计思想
如果你没看过, 也会听别人说 JavaScript 的继承不同于 Java 和 c++,JS 中没有 "类" 和 "实例" 的区分, 而是靠一种原型链的一级一级的指向来实现继承. 那么当时的创造 JavaScript 这种的语言的人为什么要这样实现 JS 独有的继承, 大家可以阅读阮一峰老师的 JavaScript 继承机制的设计思想 (http://www.ruanyifeng.com/blog/2011/06/designing_ideas_of_inheritance_mechanism_in_javascript.html), 就像讲故事一样, 从古代至现代说明了 JS 继承这种设计模式的缘由.
prototype 对象
了解了 JS 继承的设计思想后, 我们需要学习原型链上的第一个属性 prototype, 这个属性是一个指针, 指向的是原型对象的内存堆. 从阮一峰老师的文章中, 我们可以知道 prototype 是为了解决构造函数的属性和方法不能共享的问题而提出的, 下面我们先实现一个简单的继承:
此时, 实例 1 和实例 2 都有自己的 data 属性, state 属性, isPlay 方法, 造成了资源的浪费, 既然两个实例都需要调用 isPlay 方法, 便可以将 isPlay 方法挂载到构造函数的 prototype 对象上, 实例便有了本地属性方法和引用属性方法, 如下:
我们将 isPlay 方法挂载到 prototype 对象上, 同时增加 isDoing 属性, 既然是共享的属性和方法, 那么修改 prototype 对象的属性和方法, 实例的值都会被修改, 如下:
问题来了, 为什么实例会取到 prototype 对象上的属性和方法, 别急, 没多久就会结合其他问题综合解答.
同时, 你可能会问, 如果修改实例 1 的 isDoing 属性的原型, 实例 2 的 isDoing 会不会受到影响?
问题又来了, 可以看到修改实例 1 的 isDoing 属性, 实例 2 的实例并未受到影响. 这是为什么呢?
那如果修改实例 1 的 isDoing 属性的原型属性, 实例 2 的 isDoing 会不会受到影响? 如下:
问题又又来了, 为什么修改实例 1 的__proto__属性上的 isDoing 的值就会影响到构造函数的原型对象的属性值?
我们先整理一下, 未解决的三个问题:
为什么实例会取到 prototype 对象上的属性和方法?
为什么修改实例 1 的 isDoing 属性, 实例 2 的实例没有受到影响?
为什么修改实例 1 的__proto__属性上的 isDoing 的值就会影响到构造函数的原型对象的属性值?
这时候不得不背后真正的操作者搬出来了, 就是 new 操作符, 同样是面试最火爆的问题之一, new 操作符干了什么? 相信有人也是跟我一样, 已经背的滚瓜烂熟了, 以 Var instance1 = new constructorFn(); 为例, 就是下面三行代码:
第一行声明一个空对象, 因为实例本身就是一个对象. 第二行将实例本身的__proto__属性指向构造函数的原型, obj 新增了构造函数 prototype 对象上挂载的属性和方法. 第三行将构造函数的 this 指向替换成 obj, 再执行构造函数, obj 新增了构造函数本地的属性和方法.
理解了上面三行代码的含义, 那么三个问题也就迎刃而解了.
问题 1: 实例在新建的时候, 本身的__ptoto__指向了构造函数的原型.
问题 2: 实例 1 和实例 2 在新建后, 有了各自的 this, 修改实例 1 的 isDoing 属性, 只是修改了当前对象的 isDoing 的属性值, 并没有影响到构造函数.
问题 3: 修改实例 1 的__proto__, 即修改了构造函数的原型对象的共享属性
到此处, 涉及到的内容大家可以再回头捋一遍, 理解了就会觉得醍醐灌顶.
__proto__
同时, 你可能又会问,__proto__是什么?
简单来说,__proto__是对象的一个隐性属性, 同时也是一个指针, 可以设置实例的原型. 实例的__proto__指向构造函数的原型对象.
需要注意的是,
每个对象都有内置的__proto__属性, 函数对象才会有 prototype 属性.
用 Chrome 和 FF 都可以访问到对象的__proto__属性, IE 不可以.
我们继续用上面的例子来说明:
构造函数的原型对象也是对象, 那么 constructor.prototype.__proto__指向谁呢?
定义中说对象的__proto__指向的是构造函数的原型对象, 下面我们验证一下 constructor.prototype.__proto__的指向:
用图形表示的话, 如下:
可以看出, constructor.prototype.__proto__的指向是 Object 的原型对象.
那么, Object.prototype.__proto__的指向呢?
用图形表示的话, 如下:
可以发现, Object.prototype.__proto__ === null; 这样也就形成了原型链. 通过将实例的原型指向构造函数的原型对象的方式, 连通了实例 - 构造函数 - 构造函数的原型, 原型链的特点就是逐层查找, 从实例开始查找一层一层, 找到就返回, 没有就继续往上找, 直到所有对象的原型 Object.prototype.
继承的方法
了解了上面的基础概念, 就要将学到的用在实际当中, 到底要怎么实现继承呢? 实现的方式有哪些? 下面主要说明实现继承最常用的三用方式, 可以满足基本的开发需求, 想要更深入的了解, 可以参考阮一峰老师的网络博客 (http://www.ruanyifeng.com/blog/).
原型链继承
实现原理: 将父类的实例作为子类的原型
注:
这种继承方式需要将子类的构造函数指回本身, 因为从父类继承时同时也继承了父类的构造函数.
简单的使用 Cat.prototype = Animal.prototype 将会导致两个对象共享相同的原型, 一个改变另一个也会改变.
不要使用 Cat.prototype = Animal, 因为不会执行 Animal 的原型, 而是指向函数 Animal. 因此原型链将会回溯到 Function.prototype, 而不是 Animal.prototype, 因此 canRun 将不会在 Cat 的原型链上.
使用 call,apply 方法实现
实现原理: 改变函数的 this 指向
注:
该方法将子类 Cat 的 this 指向父类 Animal, 但是并没有拿到父类原型对象上的属性和方法
使用混合方法实现
实现原理: 原型链可以继承原型对象的属性和方法, 构造函数可以继承实例的属性且可以给父类传参
每一种继承方式都有自己的优点和不足, 读者可以根据实际情况选择相应的方法. 为了在实际开发中更方便的使用继承, 可以封装一个继承的方法, 如下:
结合一开始的例子, 可以这样实现继承的关系:
JavaScript 的继承远不止这些,, 只希望可以让新学 JS 的小伙伴不那么盲目的去刻意记一些东西, 当然学习最好的办法还是要多写, 最简单的就是直接打开浏览器的控制台, 去验证自己各种奇奇怪怪的想法, 动起来吧~
来源: http://www.tuicool.com/articles/nU7vymr