@frank-shaw
2019-07-03T14:13:31.000000Z
字数 3899
阅读 1144
javaScript
译者说:在查看 JS 的原型模式相关的内容的时候,一直无法找到一个说服自己的方式。其逻辑的复杂是一方面,另一个困惑在于:既然每个对象都有 __proto__ 属性,那么函数对象的 __proto__ 到底是啥?
直到看到这篇文章,才终于有了恍然大悟的感觉。
--译文关键词约定
property: 属性
object-literal:对象字面量
function-object:函数对象
instance-object:实例对象
--译文开始
写在前面:希望通过这篇文章,能够将你对于 JavaScript 对象与原型的疑惑厘清,这样你就不会那么厌恶它们了。
对于大多数开发者而言(特别是传统的面向对象背景的开发者),JavaScript 世界似乎是令人感到厌烦的。仅仅随意 Google 关于 JavaScript 相关的代码以及对应的解释,会发现相当多的凌乱代码以及令人感到困惑的术语。为了解决这个难题,本文章尝试对 JavaScript 中基于原型的继承方式做一个通俗的讲解,希望对你有用。
JS 中的对象(Object),你可以将其想象为一个包含了许多键值对的 list,其中键的类型总是字符串,而值的类型则不受限制。它和其他编程语言中的 Map 有相通之处。在 JS 世界中,只要不是基本类型(undefined、null、boolean、number、string、symbol),都可以称为对象。这种宽松的数据结构,可以对数据进行有效的包装与组织;创建一个新的对象也是极为轻量化的(相比其他面向对象语言)。
当谈论对象的时候,经常会出现一个词:property,属性。在对象中,属性指的是特定的某一个键值对。我们以一个例子来讲解吧:我们创建一只狗,它有两个属性:age 和 weight。 那么就有:
这就是 JS 世界中的一个对象了。
刚刚提到过,JS 世界中,只要不是基本类型,就都称之为对象。那么函数也是对象了。可能一开始会有点难以想象,函数怎么也是一组键值对呢。因为函数在 JavaScript 中也是对象,那么我们称其为函数对象。一个函数对象实际上就是一组包含有特殊属性的键值对(特殊性在其可以执行封装的代码,可以传递参数等)。晚些时候我们会讲解函数对象中包含有哪些特殊属性。现在我们先来理解一下,为什么函数是重要的。
函数对象被创建出来,有两个理由。第一个是:与其他编程语言中的 methods(方法)类似,我们可以使用函数对象来执行特定的代码逻辑。第二个是:如果我们想要同时封装值与方法,那么我们使用函数对象,这个时候你可以联想其他面向对象编程语言中的类 Class。
在下面的用法中,你会发现函数对象就像其他语言的函数一样,执行特定逻辑,输出对应结果。如下代码块所示,bark 就是一个函数对象:
想想那个之前被我们创造出来的 Dog 对象,如果我们想要创造多只不同的狗呢?那么可能之前的两个属性不够,我们还要增加一些属性:一些静态属性,一些可变属性。这个时候,函数对象就可以很方面加入进来了。当我们使用 new 关键字调用函数对象的时候,一个新的对象(称为实例对象)就被创造出来了,其属性来自于函数对象中的 this 关键字。如下所示:
现在你可能对 JavaScript 的对象与函数对象有了一定的了解。那么我们来讲讲原型吧。或许你听说过:JavaScript 本质是一门基于原型的语言。那么是否意味着原型与对象是同一个事物呢?回答是:不是的。原型prototype是一种特殊的对象,它以函数对象的一种固有属性的方式存在。当我们尝试去获取函数对象的某一个属性的时候,Javascript 会查找其原型,去查看是否对应的属性存在。如果不存在,会沿着原型链继续往上,直到找到为止。为了理解原型链,我们需要明白函数与继承的关系。
当我们通过关键字 new 函数对象的方式创造处一个实例对象的时候,JS就会给这个实例对象增加一个属性 __proto__。这个属性的值来自哪里呢?就来自函数对象中的固有属性 prototype。于是,我们可以知道:
当我们想要在实例对象上查找某一个属性是否存在的时候,首先会查找实例对象本身是否存在。如果不存在,那么我们会去查找其 __proto__ 属性,去询问其对应的函数对象的 prototype 属性中是否存在该属性。作为验证,让我们在 Dog 函数对象的 prototype 属性上增加一个bark属性,当我们调用 Spot['bark'] 或 Bingo['bark'] 的时候,我们会得到相同的值(即使这两个实例对象(Bingo 与 Spot)已经创建,依然有效的):
通过在原型上设置对应方法,只需要一次调用即可生成,而不需要每一次在 new 的时候都重复调用生成。在原型上设置方法可以节省内存,提高性能。
在JavaScript 的继承结构中,Object 是处于中心位置的,它是一个函数对象。所有的实例对象都继承它。
当我们创建一个对象字面量的时候,实际上 JavaScript 调用的是 new Object()。被创建的新对象的 __proto__ 属性指向的是 Object 的原型。所以,所有由对象字面量创建得到的对象,都是 Object 的实例对象。这为我们提供了很多有用的属性,因为继承自 Object(其自带了很多有用的属性)。
当面对的是函数对象的时候,如果想要查看其是否有某个属性,那么我们可以沿着其 prototype 属性,继而沿着 __proto__ 去查找整个原型链。
让我们通过具体的例子,来看看 JavaScript 是如何通过原型链来查找不同对象类型的属性的(以属性 hasOwnProperty 为例):
对象字面量:
实例对象:
那么,直接在函数对象中查找某个属性,会怎样呢?
既然所有的对象都有 __proto__ 属性,那么函数对象的 __proto__ 属性是什么呢?在 Javascript 中,存在一个内建的函数对象:Function。每一个函数的 __proto__ 属性都指向的是 Function 的原型,即 Function.prototype。该对象非常特殊,它是一个函数对象,但是却没有原型属性,调用 Function.prototype.prototype 会返回 undefined。Function.prototype 中定义了所有的函数对象都有的行为。
既然 Function.prototype 是对象,其 __proto__ 指向的就是 Object.prototype.
或许下面一幅图可以将上面提到的知识点理清一点。值得注意的是:Object.prototype 是所有对象在原型链上最终都会引用的。
当我们在谈论继承的时候,通常想到的是来自函数中的实例对象。有了 prototype 属性,我们可以做多层继承,并且可以让函数对象继承自另一个函数对象。想要实现函数对象继承自另一个函数对象,只需要让子函数对象的原型属性 prototype 指向另一个实例对象所表示的函数对象的原型即可。这样,父函数对象的所有属性都可以被子函数对象继承。如果父函数接收参数,比如例子 Dog 中的 weight 和 age,那么使用.call 方法在子对象中设置 this 属性即可。
Labrador继承自Dog:
JS中的类 Classes,在ES6中开始存在,实际上只是在函数对象之上的语法糖而已。正如前面的多层继承中,一个函数对象想要继承另一个函数对象,需定义很多彼此之间的 prototype 相关的关联。有了 class 关键字以后,我们只需要在一个类里面定义多个方法。然后通过 extends 关键字,一个类就可以继承另一个类的方法和属性,而不需要调用 Object.create 与 Object.call。本文作者喜欢使用类,但是需要记住的是,并不是所有的浏览器都支持ES6语法。这也是为什么类似Babel工具出现的原因。
对比一下使用函数对象与使用类关于继承的写法不同(例子中的过程本质是一样的):
使用函数对象:
使用类实现相同的功能:
JavaScript 代码本质上由两种类型组成:对象和基本类型。JavaScript 中有6中基本类型:undefined、null、boolean、number、string、symbol (ES6中定义)。基本类型只是一个简单的值,在其之上并没有更多的属性。其中,三种基本类型(boolean,number,string)在 JavaScript 中有对应的包装对象,在执行特定操作的时候,可以做转换(这与 JAVA 中的基本类型与包装类一个概念)。举例说明:执行"some string".length,内在的操作会调用new String(),将基本类型 string 转换为对应的包装对象 String 的实例对象,这样即可调用 length 属性了。
当一连串无止境的 prototype 和 __proto__ 出现的时候,我想每个人都会头大。作者认为,JavaScript 中的继承方式过于错综复杂了,实际上很多并不会在日常工作中用到。如果你立志成为一个基础扎实的前端开发工程师,了解 JavaScript 背后的原理是必不可少的。祝在前端开发过程中,收获你的乐趣~