[关闭]
@frank-shaw 2019-07-03T14:13:31.000000Z 字数 3899 阅读 1153

【译文】关于JavaScript的对象与原型模式的理解

javaScript


译者说:在查看 JS 的原型模式相关的内容的时候,一直无法找到一个说服自己的方式。其逻辑的复杂是一方面,另一个困惑在于:既然每个对象都有 __proto__ 属性,那么函数对象的 __proto__ 到底是啥?

直到看到这篇文章,才终于有了恍然大悟的感觉。

--译文关键词约定

property: 属性
object-literal:对象字面量
function-object:函数对象
instance-object:实例对象

原文地址:https://levelup.gitconnected.com/the-javascript-object-paradigm-and-prototypes-explained-simply-e9cb9eaa49aa

--译文开始

写在前面:希望通过这篇文章,能够将你对于 JavaScript 对象与原型的疑惑厘清,这样你就不会那么厌恶它们了。

对于大多数开发者而言(特别是传统的面向对象背景的开发者),JavaScript 世界似乎是令人感到厌烦的。仅仅随意 Google 关于 JavaScript 相关的代码以及对应的解释,会发现相当多的凌乱代码以及令人感到困惑的术语。为了解决这个难题,本文章尝试对 JavaScript 中基于原型的继承方式做一个通俗的讲解,希望对你有用。

JS中的对象到底是什么?

JS 中的对象(Object),你可以将其想象为一个包含了许多键值对的 list,其中键的类型总是字符串,而值的类型则不受限制。它和其他编程语言中的 Map 有相通之处。在 JS 世界中,只要不是基本类型(undefined、null、boolean、number、string、symbol),都可以称为对象。这种宽松的数据结构,可以对数据进行有效的包装与组织;创建一个新的对象也是极为轻量化的(相比其他面向对象语言)。

当谈论对象的时候,经常会出现一个词:property,属性。在对象中,属性指的是特定的某一个键值对。我们以一个例子来讲解吧:我们创建一只狗,它有两个属性:age 和 weight。 那么就有:

屏幕快照 2019-07-03 下午12.40.05.png-12.3kB

这就是 JS 世界中的一个对象了。

函数也是对象

刚刚提到过,JS 世界中,只要不是基本类型,就都称之为对象。那么函数也是对象了。可能一开始会有点难以想象,函数怎么也是一组键值对呢。因为函数在 JavaScript 中也是对象,那么我们称其为函数对象。一个函数对象实际上就是一组包含有特殊属性的键值对(特殊性在其可以执行封装的代码,可以传递参数等)。晚些时候我们会讲解函数对象中包含有哪些特殊属性。现在我们先来理解一下,为什么函数是重要的。

函数对象被创建出来,有两个理由。第一个是:与其他编程语言中的 methods(方法)类似,我们可以使用函数对象来执行特定的代码逻辑。第二个是:如果我们想要同时封装值与方法,那么我们使用函数对象,这个时候你可以联想其他面向对象编程语言中的类 Class。

在下面的用法中,你会发现函数对象就像其他语言的函数一样,执行特定逻辑,输出对应结果。如下代码块所示,bark 就是一个函数对象:

屏幕快照 2019-07-03 下午12.40.19.png-21.6kB

想想那个之前被我们创造出来的 Dog 对象,如果我们想要创造多只不同的狗呢?那么可能之前的两个属性不够,我们还要增加一些属性:一些静态属性,一些可变属性。这个时候,函数对象就可以很方面加入进来了。当我们使用 new 关键字调用函数对象的时候,一个新的对象(称为实例对象)就被创造出来了,其属性来自于函数对象中的 this 关键字。如下所示:

屏幕快照 2019-07-03 下午12.40.36.png-65.1kB

对象与原型

现在你可能对 JavaScript 的对象与函数对象有了一定的了解。那么我们来讲讲原型吧。或许你听说过:JavaScript 本质是一门基于原型的语言。那么是否意味着原型与对象是同一个事物呢?回答是:不是的。原型prototype是一种特殊的对象,它以函数对象的一种固有属性的方式存在。当我们尝试去获取函数对象的某一个属性的时候,Javascript 会查找其原型,去查看是否对应的属性存在。如果不存在,会沿着原型链继续往上,直到找到为止。为了理解原型链,我们需要明白函数与继承的关系。

函数与继承

当我们通过关键字 new 函数对象的方式创造处一个实例对象的时候,JS就会给这个实例对象增加一个属性 __proto__。这个属性的值来自哪里呢?就来自函数对象中的固有属性 prototype。于是,我们可以知道:

屏幕快照 2019-07-03 下午12.40.52.png-15.3kB

当我们想要在实例对象上查找某一个属性是否存在的时候,首先会查找实例对象本身是否存在。如果不存在,那么我们会去查找其 __proto__ 属性,去询问其对应的函数对象的 prototype 属性中是否存在该属性。作为验证,让我们在 Dog 函数对象的 prototype 属性上增加一个bark属性,当我们调用 Spot['bark'] 或 Bingo['bark'] 的时候,我们会得到相同的值(即使这两个实例对象(Bingo 与 Spot)已经创建,依然有效的):

屏幕快照 2019-07-03 下午12.41.02.png-27.4kB

通过在原型上设置对应方法,只需要一次调用即可生成,而不需要每一次在 new 的时候都重复调用生成。在原型上设置方法可以节省内存,提高性能。

让我们将继承研究更透彻一些

在JavaScript 的继承结构中,Object 是处于中心位置的,它是一个函数对象。所有的实例对象都继承它。

当我们创建一个对象字面量的时候,实际上 JavaScript 调用的是 new Object()。被创建的新对象的 __proto__ 属性指向的是 Object 的原型。所以,所有由对象字面量创建得到的对象,都是 Object 的实例对象。这为我们提供了很多有用的属性,因为继承自 Object(其自带了很多有用的属性)。

当面对的是函数对象的时候,如果想要查看其是否有某个属性,那么我们可以沿着其 prototype 属性,继而沿着 __proto__ 去查找整个原型链。

让我们通过具体的例子,来看看 JavaScript 是如何通过原型链来查找不同对象类型的属性的(以属性 hasOwnProperty 为例):

对象字面量:

屏幕快照 2019-07-03 下午12.42.29.png-65.7kB

实例对象:

屏幕快照 2019-07-03 下午12.42.57.png-53.5kB

那么,直接在函数对象中查找某个属性,会怎样呢?

屏幕快照 2019-07-03 下午12.43.09.png-59.5kB

函数对象的 __proto__ 属性指向的是什么呢?

既然所有的对象都有 __proto__ 属性,那么函数对象的 __proto__ 属性是什么呢?在 Javascript 中,存在一个内建的函数对象:Function。每一个函数的 __proto__ 属性都指向的是 Function 的原型,即 Function.prototype。该对象非常特殊,它是一个函数对象,但是却没有原型属性,调用 Function.prototype.prototype 会返回 undefined。Function.prototype 中定义了所有的函数对象都有的行为。

既然 Function.prototype 是对象,其 __proto__ 指向的就是 Object.prototype.

屏幕快照 2019-07-03 下午12.41.17.png-29.6kB

额,是不是有点晕

或许下面一幅图可以将上面提到的知识点理清一点。值得注意的是:Object.prototype 是所有对象在原型链上最终都会引用的。

1_KNCFqc7YytARCUXJGgYn1Q.png-30.9kB

多层继承

当我们在谈论继承的时候,通常想到的是来自函数中的实例对象。有了 prototype 属性,我们可以做多层继承,并且可以让函数对象继承自另一个函数对象。想要实现函数对象继承自另一个函数对象,只需要让子函数对象的原型属性 prototype 指向另一个实例对象所表示的函数对象的原型即可。这样,父函数对象的所有属性都可以被子函数对象继承。如果父函数接收参数,比如例子 Dog 中的 weight 和 age,那么使用.call 方法在子对象中设置 this 属性即可。

Labrador继承自Dog:

屏幕快照 2019-07-03 下午12.41.31.png-74.6kB

JS中的类 Classes

JS中的类 Classes,在ES6中开始存在,实际上只是在函数对象之上的语法糖而已。正如前面的多层继承中,一个函数对象想要继承另一个函数对象,需定义很多彼此之间的 prototype 相关的关联。有了 class 关键字以后,我们只需要在一个类里面定义多个方法。然后通过 extends 关键字,一个类就可以继承另一个类的方法和属性,而不需要调用 Object.create 与 Object.call。本文作者喜欢使用类,但是需要记住的是,并不是所有的浏览器都支持ES6语法。这也是为什么类似Babel工具出现的原因。

对比一下使用函数对象与使用类关于继承的写法不同(例子中的过程本质是一样的):

使用函数对象:

屏幕快照 2019-07-03 下午12.41.49.png-70.9kB

使用类实现相同的功能:

屏幕快照 2019-07-03 下午12.42.02.png-59.7kB

对象与基本类型

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 背后的原理是必不可少的。祝在前端开发过程中,收获你的乐趣~

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注