前言
这一篇鸽了两个周,实在不能再拖下去了。之所以会拖这么久,除了一方面这一块不像之前那样只是一个小知识点,另一方面是总想着能写出点什么新东西。现在发现,我还只是一名技术领域的追随者,能够勉强跟得上技术的潮流就已经值得庆幸了;我所学习的东西,大多是五六年前的标准,七八年前的框架,十几年前的思想。所以,看清自己的位置,立足当下,把它当作自己的学习总结。
正文开始
JavaScript 是一门动态语言,动态语言的哲学决定了我们很难用一些静态语言类的思想去操纵 JavaScript 的对象。虽然 es6 给出了很多新特性,这些新特性可以帮助我们在一定程度上模拟类,但不能忽视的是,它们是建立在原型的基础之上的。深入地了解原型,而不是一味地回避,可以让我们更好地使用 JavaScript。
接下来我会按照自己的理解,整理关于原型的线索,内容如下:
- 原型
- 原型对象
- 原型对象是什么
- 原型的复制机制
- 原型链
- 认识原型链的工具
- 原型链是什么
- 原型对象
原型
先来了解下原型。之所以把原型放在对象前面,是因为我在学习 JavaScript 对象的相关知识时发现,它完全绕不开原型。先对原型建立个大概印象,再以它为工具,可以更好地发现隐藏在JS语法背后的奥秘。
所以,原型是什么?大概你已经在不同的场合见识过原型的介绍了,这里请看MDN官方文档关于对象原型的介绍:
JavaScript 常被描述为一种基于原型的语言 (prototype-based language)——每个对象拥有一个原型对象,对象以其原型为模板、从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链 (prototype chain),它解释了为何一个对象会拥有定义在其他对象中的属性和方法。
准确地说,这些属性和方法定义在Object的构造器函数(constructor functions)之上的
prototype
属性上,而非对象实例本身。在传统的 OOP 中,首先定义“类”,此后创建对象实例时,类中定义的所有属性和方法都被复制到实例中。在 JavaScript 中并不如此复制——而是在对象实例和它的构造器之间建立一个链接(它是
__proto__
属性,是从构造函数的prototype
属性派生的),之后通过上溯原型链,在构造器中找到这些属性和方法。注意:理解对象的原型(可以通过
Object.getPrototypeOf(obj)
或者已被弃用的__proto__
属性获得)与构造函数的prototype
属性之间的区别是很重要的。前者是每个实例上都有的属性,后者是构造函数的属性。也就是说,Object.getPrototypeOf(new Foobar())
和Foobar.prototype
指向着同一个对象。
初学者看这段介绍肯定是懵的,没关系接下来一一作介绍。
原型对象
原型对象是什么
这里还是先沿用官方的说明和例子。
在javascript中,函数可以有属性(注:可以认为JS中函数是特殊的对象)。每个函数都有一个特殊的属性叫作 prototype
(注,mdn中文文档这里翻译为【原型】,但我认为不翻译比较好,后面也是如此),正如下面所展示的。
function Foo(){}
console.log( Foo.prototype );
// 你如何声明函数并不重要,
// 在javascript中函数都会有一个默认的
// prototype 属性。
var Foo = function(){};
console.log( Foo.prototype );
复制代码
它们都会返回同一个对象。是的没错,这些函数的 prototype
指向了同一个特殊的对象,一般称为【原型对象】:
{
constructor: ƒ Foo(),
__proto__: {
constructor: ƒ Object(),
hasOwnProperty: ƒ hasOwnProperty(),
isPrototypeOf: ƒ isPrototypeOf(),
propertyIsEnumerable: ƒ propertyIsEnumerable(),
toLocaleString: ƒ toLocaleString(),
toString: ƒ toString(),
valueOf: ƒ valueOf()
}
}
复制代码
上面那些奇奇怪怪的函数,有些等会会提及到,有些本文无法顾及。我们先重点来看看该原型对象的两个属性名:constructor
和 __proto__
。
前者 constructor
,可以翻译成【构造器】,看起来它又指回原来的函数了。它们的关系似乎是这样的:
所以,是不是就意味着一个函数的原型对象的 constructor
一定指向该函数呢?肯定不是,既然它是一个可访问属性,那么它的对象肯定就可以修改。具体怎么修改,这里暂且不提,如果有读者感兴趣可以留言,或者自行阅读《你不知道的 JavaScript(上)》第二部分第五、六章。
后者 __proto__
,看起来是一个很奇怪的属性名,它有另一个称呼你可能见过,[[Prototype]]
。嗯?这个怎么看起来和之前的 prototype
属性那么像啊。两者有什么关联吗?
这里我暂时找不到关于 [[Prototype]]
的官方定义,ECMAScript 可能有但我懒得找了。不过,无论是 MDN 还是《你所不知道的 JavaScript》都提到它是一个内部属性。什么意思呢?虽然JS中没有私有属性的概念,但是每个对象都有一些内部属性,其中就有 [[Prototype]]
。在 ES 标准中,该属性你是无法通过常规的访问方式访问和设置的——包括点访问法和括号访问法(.__proto__
不是标准实现,它只是个别浏览器厂商的内部实现)。甚至在 ES5 之前,除了 new
操作外无法通过其它途径操作该属性(后文会介绍 ES5 支持的新方法)。
上面说到,__proto__
,即 [[Prototype]]
是一个属性,也指向了一个对象。该对象也有一个 construcotr
属性,难道它也是原型对象?没错,并且JS 还有很多内置函数,包括 Function
的 __proto__
都指向这个原型对象。后文会提到,它其实就是 Object
的原型对象。
你可能看得云里雾里,没关系看看下面代码你就清楚了:
console.log(Foo.prototype.__proto__ === Object.prototype) // 谷歌浏览器下
// true
复制代码
原型的复制机制
再回到 MDN 官方文档的介绍,里面有一句话提到JS原型的复制机制:
在对象实例和它的构造器之间建立一个链接(它是
__proto__
属性,是从构造函数的prototype
属性派生的)
这句话怎么理解呢?请看下面这个代码示例,环境是谷歌开发者工具:
var Foo = function(){}
// undefined
var foo = new Foo // 无参数时可省略括号
// undefined
Foo.prototype === foo.__proto__
// true
复制代码
看起来它们之间的关系是这样的:
需要注意的是,foo
没有属性 prototype
。这里官方文档也有提到:**[[Prototype]]
是每个实例上都有的属性,prototype
是构造函数的属性。**这里,每个实例应是指对象(包括函数),也就是说JS中每个对象都有 [[Prototype]]
属性(而且是内部属性)。你可能会好奇,那 Foo
的 [[Prototype]]
属性指向什么呢?
这里其实就涉及到原型链的知识了。
原型链
那么,Foo
的 [[Prototype]]
属性指向什么呢?前文说到,Foo
的原型对象的 [[Prototype]]
属性指向 Object
的原型对象。那么如果不是该函数本身是否也指向 Object
的原型对象呢?尝试下看看:
console.log(Foo.__proto__ === Object.prototype) // 谷歌浏览器下
// false
复制代码
看起来不是。这里直接给出答案吧,它其实指向 Function
的原型对象,你可以用同样的方法检测一下:
console.log(Foo.__proto__ === Function.prototype) // 谷歌浏览器下
// true
复制代码
估计有些人会满脑子疑问,那 Function
的 [[Prototype]]
,及该函数原型对象的 [[Prototype]]
都指向什么呢(包括 Object
、foo
等等)?有两个方法,一个是你自己一个一个找,另一个是请你看下面这幅图,并且试着做下验证:
你可以着重看下 Function
、Object
和 Foo
这几个函数的原型对象之间的关系。看的时候肯定有很多疑问,**为什么光线条的样式就有三种呢?**没关系先放下继续看下文。
不知道看完上文,特别是上图,你对【原型链】是否有一定的认识?也许你脑中会反应过来刚刚提及的函数的 prototype
,及所有实例即对象的 [[Prototype]]
(请记住,两者不同一回事)。没错,当我们谈及原型链时,肯定绕不开这两个属性。
认识原型链的工具
工欲善其事,必先利其器。我们先了解下操纵这两个属性的工具。前者其实不用说了,它是一个能够直接修改的属性;后者前文说过了,它是一个内部属性,ES5 后有三个标准实现可以操纵它:
- Object.getPrototypeOf
- Object.setPrototypeOf(注意,只有它是es6的新方法,其余两个都是es5的)
- Object.isPrototypeOf
除此之外,在 ES6 之前没有 setPrototypeOf
方法时,有两个替代性的方法:
- Object.create (es5方法)
- new 构造(es5 之前唯一修改
[[Prototype]]
的方法)
前文说过,__proto__
是个别浏览器厂商的内部实现,还不是标准。综上,这些方法,基本上就是 es6 后能够了解 [[Prototype]]
的工具了。它们的作用应该很好猜,如果不确定的话还请自行搜索一下吧。
原型链是什么
那么,我们现在再回到最开始的问题,原型链是什么?
这里用一些很容易搞错的问题,作为引子。请看下面代码(后面不特殊说明,环境都是谷歌开发者工具):
var Foo = function(){}
console.log(Foo.constructor === Function)
// true
console.log(Object.constructor === Function)
// true
console.log(Function.constructor === Function)
// true
复制代码
看起来很奇怪,尤其是最后一个。嗯,前文的原型对象似乎有提到这个 constructor
,像 Foo
的原型对象的 constructor
正是它本身。前文也提到,该属性是可直接访问和修改的。看起来,该属性应该只有原型对象才有的,这些构造函数应该不可能有。嗯,这话说对了一半,请看下例:
Foo.hasOwnProperty("constructor")
// false
Object.hasOwnProperty("constructor")
// false
Function.hasOwnProperty("constructor")
// false
复制代码
问题就来了,既然这三个函数都没有该属性,为什么之前的例子又是那样输出的呢?浏览器肯定没犯毛病,问题的根源就在原型链上。
回到原型链,我们先找找 MDN 官方文档中对它的描述:
每个对象拥有一个原型对象,对象以其原型为模板、从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链 (prototype chain),它解释了为何一个对象会拥有定义在其他对象中的属性和方法。
简单点说,原型链可以让一个对象访问定义在其它对象中的属性和方法。具体的访问过程是怎样的,这里不细讲,下一篇博客【对象部分】会讨论这个问题。你这里就可以先按照你的喜好简单理解。
那么,我们用这个刚认识的原型链来剖析一下上面的问题吧。我们都知道了,那三个函数肯定是没有 constructor
属性的。那么根据原型链的定义,它们肯定是访问了其它对象的该属性,而且很可能还是同一个对象。那是哪个对象呢?我不妨再放一次图(可是辛辛苦苦做了两个小时的),这一副重点描了一个红框,它其实就同时是这三个函数所访问的那个“受害人”,以及三个红圈,它们所在的三条线其实就是造成上述问题的“罪魁祸首”。
嗯,原型链简直是魔鬼。这里我为了方便大家理解,将制作的这幅图一部分线条用红色和蓝色描出来。红线是关于 Object
的原型链,蓝线是关于 Function
的原型链。
当你理解这幅图时,你也就理解了,为什么函数可以访问一些特殊方法,对象又可以访问一些特殊方法。其实都是原型链的功劳。至于都有哪些特殊方法,我贴一副《你不知道的 JavaScript》书中的插图:
其中左边的红色椭圆圈住 Function
的原型对象,右边的红色方框圈住 Object
的原型对象,省略号部分可自行搜索。另外,上面红色方框圈住的 construct
是我认为有问题之处。根据之前的代码示例,Object
没有 constructor
,这里应该指的是原型委托,但委托的对象又出了差错。总之,还请读者自行辨认。
后记
短短一篇博客还分前言后记是挺搞笑的。只不过我这篇确实写了两三天,工作量挺大的,光是例图可能就花了三个多小时,所以也想请看了此篇后,觉得有帮助的读者给我点个赞吧~
另外做一个预告,下一篇应该是关于 JS 的对象了,它与原型本紧密结合,应放一起写才对;只不过此篇就写了七千多字(markdown格式的统计,包括字母),再写下去太长了。嗯,就这样吧,感谢慧鉴。