《大前端三问》 - JavaScript中的面向对象与原型链2
本章我们将探讨JavaScript中继承
与多态
的设计及实现。
1 JavaScript的继承
首先,关于继承的概念我觉得无需多说了,这基本上属于程序员的入门课程了。
这里要多说的是,继承,其实也是一种代码复用的技术。我们上节说过,抽象与封装是一种代码复用技术,旨在将具有相同属性与行为的事物抽象出类型
(在JavaScript中使用构造函数
与原型
实现)。如果说抽象与封装
复用的是一个类型模板的属性与行为,那继承
则是复用一系列具有相近属性与行为的类型,这个结构有点像俄罗斯套娃,子类继承自父类,就像在父类这个娃娃上套个更大的子类大娃(在父类的基础上构建另外自己独特的部分)。而子类自己也可以有子类,在这之上所有祖先的代码都将复用。
1.1 继承的实现
因为有了第一章原型链的基础,本章我们将更好理解。就像是我很喜欢的一部电影《盗梦空间》空间一样,第一章中我们只是一层梦境,而这章将讲的是多层梦境而已。
从上一章我们知道,新建一个People构造函数function People
,一个<<People.prototype>>
原型就伴随着构造函数的定义而自动产生的。而当我们调用new People()
,构造函数将返回一个已初始化完成的People对象,系统会自动将<<People.prototype>>
原型对象的引用赋给此对象的_prop__
成员。
而正因为__prop__
的存在,产生了所谓的原型链
。当我们访问对象的成员时,如果在本对象的散列表结构中没有找到相应的引用时,解释器会自动帮我们作向上查找,沿着其__proto__
属性找到原型对象<<prototype>>
。如果还是没有,会一直沿着原型的原型一路向上找,直到找到<<Object.prototype>>
, 其__proto__
为null。
那么我们现在就要定义我们的目标,要实现继承的复用原则,就是要让我们新定义的Student
类型可以复用People
类型的属性与方法。 – 一般地,我们使用prototype chan
原型链来实现:
1 | ┌───────────────────────┐ |
上图中虚线框出的就是我们要增加的部分。现在我们增加了一个Student
类型(使用Student函数),这在Java等高级面向对象语言中是那么容易,但是在JS中我们要做很多看起来繁杂的处理。
首先第一个要解决的问题是Student
类型要复用在People
构造函数中定义的成员。解决方案很简单,在Student
的构造函数中再调用People
构造函数就可以了,这个看起来有点像Java中的构造器重载:
1 | function People(name, age) { |
这样当我们let peter = new Student('Peter', 9, 2)
时,实际发生了下面的事:
1 | var peter = new Object(); |
我们这里简介下function.call(this)
,这个方法将调用指定方法,同时指定所调用方法的this
(在介绍this的一章中我们将详细讲解)。
所以我们来分析上面的代码:
var peter = new Object();
- 相当于new Student('Peter', 9, 2)
时,解释器先帮我们基于Object生成了一个普通对象。Student.call(...)
- 然后调用Student
构造器,并将新生成的对象设为该构造器的this
。在Student
构造器中,给对象添加了grade
成员,然后又继续效用此法调用People
构造器,为此对象添加了name、age两个成员。- 最后在返回对象前将其
__proto__
原型链的引用指向<<Student.prototype>>
,使其可以复用类型原型上的成员变量与方法。
此时,生成的peter
对象如上图一样了:
1 | ┌────────────────────────────────────┐ |
1.2 复用父类原型
但是还没完,还有一个问题,就是Student
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Details_of_the_Object_Model
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/this
https://www.cnblogs.com/leijee/p/7490822.html
https://www.jianshu.com/p/5cb692658704
https://www.runoob.com/w3cnote/js-call-apply-bind.html
2 性能
- 在原型链上查找属性比较耗时,对性能有副作用,这在性能要求苛刻的情况下很重要。
- 试图访问不存在的属性时会遍历整个原型链,这个耗费也是略大。
在对效率要求高的场合,建议使用hasOwnProperty()
和 Object.keys()
,因为这两个方法不会遍历原型链。
比如想引用某个属性前,先使用hasOwnProperty()
查看下,该对象的散列结构中是否存在此属性:
1 | if (student.hasOwnProperty("sayHi")) { |
我们说hasOwnProperty()
方法不会遍历原型链的意思就是,比如student
对象的实际散列结构下如:
1 |
hasOwnProperty("sayHi")
查找sayHi
方法时,不会顺着其__proto__
成员沿着原型链向上查找。
3 小结
4 引用
- 《重新介绍JavaScript》- 介绍JS很好的入门材料
- 《JavaScript高级程序设计》- (美)(Nicholas C.Zakas)扎卡斯 学习JS挺好的入门教材
- 《继承与原型链》- MDN web docs 继承与原型链, 很好,讲了性能,和原型实际
- [《Function函数与Object对象的关系》- 网文](https://www.cnblogs.com/nature-tao/p/9504712.html Function函数与Object对象的关系的一篇挺好的探索文章