深入学习 JavaScript —— 原型

前言

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

正文开始

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]] 都指向什么呢(包括 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格式的统计,包括字母),再写下去太长了。嗯,就这样吧,感谢慧鉴。

转载于:https://juejin.im/post/5ce6acfa6fb9a07f0052b512

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值