深刻学习 JavaScript —— 原型

前言

这一篇鸽了两个周,实在不能再拖下去了。之因此会拖这么久,除了一方面这一块不像以前那样只是一个小知识点,另外一方面是总想着能写出点什么新东西。如今发现,我还只是一名技术领域的追随者,可以勉强跟得上技术的潮流就已经值得庆幸了;我所学习的东西,大可能是五六年前的标准,七八年前的框架,十几年前的思想。因此,看清本身的位置,立足当下,把它看成本身的学习总结。javascript

正文开始

JavaScript 是一门动态语言,动态语言的哲学决定了咱们很难用一些静态语言类的思想去操纵 JavaScript 的对象。虽然 es6 给出了不少新特性,这些新特性能够帮助咱们在必定程度上模拟类,但不能忽视的是,它们是创建在原型的基础之上的。深刻地了解原型,而不是一味地回避,可让咱们更好地使用 JavaScript。java

接下来我会按照本身的理解,整理关于原型的线索,内容以下:es6

  • 原型
    • 原型对象
      • 原型对象是什么
      • 原型的复制机制
    • 原型链
      • 认识原型链的工具
      • 原型链是什么

原型

先来了解下原型。之因此把原型放在对象前面,是由于我在学习 JavaScript 对象的相关知识时发现,它彻底绕不开原型。先对原型创建个大概印象,再以它为工具,能够更好地发现隐藏在JS语法背后的奥秘。浏览器

因此,原型是什么?大概你已经在不一样的场合见识过原型的介绍了,这里请看MDN官方文档关于对象原型的介绍:bash

JavaScript 常被描述为一种基于原型的语言 (prototype-based language)——每一个对象拥有一个原型对象,对象以其原型为模板、从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链 (prototype chain),它解释了为什么一个对象会拥有定义在其余对象中的属性和方法。markdown

准确地说,这些属性和方法定义在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(上)》第二部分第5、六章。

后者 __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]] 都指向什么呢(包括 Objectfoo 等等)?有两个方法,一个是你本身一个一个找,另外一个是请你看下面这幅图,而且试着作下验证:

你能够着重看下 FunctionObjectFoo 这几个函数的原型对象之间的关系。看的时候确定有不少疑问,**为何光线条的样式就有三种呢?**不要紧先放下继续看下文。

不知道看完上文,特别是上图,你对【原型链】是否有必定的认识?也许你脑中会反应过来刚刚说起的函数的 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格式的统计,包括字母),再写下去太长了。嗯,就这样吧,感谢慧鉴。

相关文章
相关标签/搜索