在笔者对JS还很懵懂的时候,曾认为“原型(链)”是个高深难懂的概念,面试题经常看到,网上也随处可见有关它的文章。
可当我抱着一颗严肃而又敬畏的心去研究的时候,才发现,就只是这样?
对原型的理解固然很重要,但它算不上最难,甚至称不上“难点”,仅仅是一个必须懂的东西而已,不信?听我一一道来。
为何存在
首先你应该已经知道,JavaScript当中,几乎所有数据类型都能当做对象来用,为什么是“当做”,而不是“是”?因为像“数字、字符串”这些,它本来就不是对象(跟显式定义的对象相比),但如果你要像对象那样使用,比如:
1 | var num = 123; |
它也能顺利进行,而且示例的这种用法极为常见,为什么?
这里经历了这么一个过程:
- 内置包装类临时构造了一个Number()对象实例
- Number()实例具有继承自Object()的方法toString()
本次代码执行就由继承而来的toString()来完成。
好,其实到此为止,我们就看到了一条完整“原型链”的东西。
num——>Number——>Object
JavaScript本身并非具备完善面向对象特性的语言,它借助原型链来实现继承。
从对象谈起
既然原型链跟对象有关,就从对象说起,当然,这不是介绍对象的文章,基础略过。
把对象分为三类:
- 直接量和通过Object创建
- 内置包装类(显式或隐式都算)
- 自定义构造函数创建
不论哪一类,道理都一样,只是“原型”和“链”的长度不同而已。
第一种最短,它不是由其他对象创建而来,是没被“污染”的,最“干净”的顶层对象,既没有“包装类”那些系统已经定义的自有方法,又没有开发者定义的属性和方法。
说完对象,下面来看它们是怎么链起来的。
内置包装类
1 | var str = new String(); |
链是这样的
str——>String——>Object
构造函数
1 | function Origin(){ |
这时候链是这样的
obj1——>Origin——>Object
到此,虽然示例很简单,你应该明白了什么。
类比理解
如果还没懂(甚至上面提到的东西不知所云),也完全没关系,因为单纯搞懂原型链是怎么回事并不必要懂JS,优秀的思想和模式都是通用的。
我们拿另一项Web技术CSS来做个类比。
- Object,相当于我们页面啥都还没写的时候,body所具备的样式;
- 内置包装类,即标题、ul、input等有自带样式的元素;
- 构造函数,就是“啥也没有”的div、p、span之类;
当你直接写个div在页面上,再填几个文字进去,它同样有颜色和大小,但是你可以在div上定义新的规则将其覆盖,这就类似于对象属性的继承和覆盖。
当你写个标题或者表单元素,它们会比上面的文字多一些特殊的自有样式,这就类似于String或者Array对象,它们除了继承自Object,还具备特有的属性和方法,当然,你也可以自定义把它们覆盖掉(但通常不建议这么做)。
1 | <body> |
这里的链就是 p——>div——>body
看到这里,是不是舒服多了?来加个餐。
检查、设定
搞懂了”原型链“,顺带分享几个”检查“和”设定“原型的方法。
检查
constructor
这是个不常被提到,但还挺管用的属性。
1 | var date = new Date(); |
如果只是这么写会有问题,它会把整个函数体返回,我们取其名字即可:
1 | date.constructor.name // Date |
但是,constructor并非不可修改,你可以给 Prototype 链中的任意对象添加名为constructor的属性或者对其进行修改,所以它引用的目标可能跟想象的有出入,不推荐使用。
instanceof
这个方法常用来判断数据类型,但它也可以用来判断某对象是否在另一个实例的原型链上,返回布尔值,比如上例
1 | date instanceof Date // true |
自定义构造函数同样适用
1 | function Person(){ |
__proto__
和Object.getPrototypeOf()
1 | date.__proto__ == Date.prototype; // true |
但__proto__
不建议使用,因为它是一个内部属性,而不是开放API,且并未被写入新版本的ES正文中,可能存在浏览器实现的差异,已经有了等效的方法,就是Object.getPrototypeOf()
1 | Object.getPrototypeOf(date) == Date.prototype // true |
isPrototypeOf()
这种方法较新,也更直观和方便,在浏览器支持的情况下,可以优先选择。
1 | Date.prototype.isPrototypeOf(date); // true |
知道了检查方法,再看指定。
指定
聊两个点:
一,把某对象指定为另一个对象的原型
二,在原型上定义属性/方法
指定的目的是“继承”,因为并非所有的对象创建完毕之后就不再改变了,也或者我们需要它继承另外一个对象不一样的东西,这时候就要变换/指定其他原型,我猜你会想到这么个方法。
1 | a.prototype = b.prototype; |
这个方法可以让a引用b,但它的机制可能不是你想要的,当你执行类似 a.prototype.myMethod = … 的赋值语句时也会修改到 b.prototype 对象本身。
所以,想要创建一个更加稳妥的关联对象,要使用如下方法
Object.create()
1 | a.prototype = Object.create(b.prototype) |
这个语句的意思是:“创建一个新的 a.prototype 对象并把它关联到 b.prototype”。
你应该会有个疑问,既然有这个语句,就代表a原本是存在的,那么这样做之后,a.prototype原本关联的对象哪儿去了?
是的,两者的关系被剪断了,它被抛弃了,这也是此方法唯一的瑕疵。
还有一个更标准且可靠的方法来修改对象的Prototype关联,往下看。
Object.setPrototypeOf()
ES6开始可以直接修改现有的 a.prototype
1 | Object.setPrototypeOf( a.prototype, b.prototype ); |
共享
定义原型链属性/方法的一个目的是共享,如
1 | function People(name) { |
这种其实就是组合”构造函数和原型”模式,既共享了年龄,又独有了name,这是一种很常见的方式,是比较好的实践经验。
小结
此文重点分享”原型链“,但求简单易懂,无法兼顾广度和深度,鉴于JS和对象本身的细节很多,难免有遗漏或描述不妥,如需更多了解,可查阅官方文档、《JavaScript权威指南》、《你不知道的js》或其他优质资源,欢迎交流。