一、对象和原型

Abstract

熟能生巧。只有熟能生巧。

——文斯·隆巴迪

熟能生巧。只有熟能生巧。——文斯·隆巴迪

在一本面向专家的书中包含三章关于 JavaScript 核心概念的内容似乎有些奇怪。毕竟,这些主题是这门语言最基本的组成部分。我的观点是:就像一个没有读写能力的人可以说一种语言一样,开发人员也可以使用 JavaScript 的基本特性,却幸福地意识不到它们的复杂性。

这些章节的目的是揭示语言中一些更模糊的部分。这些概念可能是你一直想要学习的,甚至是你已经理解的。想象一下,你正在进入大脑的地下室,那里储存着 JavaScript。像用手电筒一样用这篇文章来检查你知识基础的裂缝。这一章和下一章旨在填补任何可能被揭露的漏洞。不要认为这是不必要的复习,而是对您对 JavaScript 理解的结构性评估。

我将从这门语言的目标的高层次概述开始。但是在您知道之前,您将会趴在地上,在 JavaScript 鲜为人知的概念中爬行。我将详细描述与对象和原型相关的重要思想。然后,在接下来的章节中,你会看到函数和闭包,它们是 JavaScript 的构建块。

鸟瞰 JavaScript

我们所说的 JavaScript 实际上是 ECMAScript 语言规范的一种实现。要使 JavaScript 被认为是 ECMAScript 的有效版本,它必须提供支持规范中定义的语法和语义的机制。JavaScript 作为一个实现必须为程序员提供使用各种类型、属性、值、函数和保留字的启示,这些组成了 ECMAScript。

一旦一个版本的 JavaScript 符合 ECMAScript,语言设计者就可以自由地用他们认为合适的额外特性和方法来修饰他们的版本。ECMAScript 规范明确允许这种繁荣,正如您在这里看到的:

The consistent implementation of ECMAScript allows providing other types, values, objects, properties and functions than those described in this specification. In particular, the consistent implementation of ECMAScript allows the objects described in this specification to be provided with attributes not described in this specification and their values. The consistent implementation of ECMAScript allows the support of program and regular expression syntax not described in this specification.

这些额外的特性可以与核心元素并行存在,并且仍然被认为是有效的实现,这一事实表明了 ECMAScript 标准体的进步性。ECMAScript 的松散性既是优点也是缺点。尽管添加新功能的灵活性鼓励了语言设计者的创新,但这可能会让开发人员在试图编写聪明的 polyfill1来支持各种实现和运行时环境之间的差异时处于不利地位。

ECMAScript 规范会随着时间的推移而发生变化,原因有很多(这里无法一一列举)。不过,这些变化主要是试图用新方法来解决老问题,或者支持更大的计算生态系统中的进步。不断变化的规范代表了一种将语言中的进化过程形式化的尝试。因此,尽管我在谈论“核心概念”,好像它们是不可变的,但实际上它们不是。本章探讨的概念是基础的,也是重要的,但是我给读者的建议是保持警觉。

设计脚本

顾名思义,ECMAScript 是一种脚本语言,用于以编程方式与宿主环境进行交互。主机系统,无论是浏览器、服务器还是硬件,都公开了供 JavaScript 操纵的控制点。大多数主机环境只允许 JavaScript 触发已经在用户控制下的系统方面(尽管是手动的)。例如,当浏览器用户使用鼠标或手指点击网页上的链接时,JavaScript 可以通过编程触发相同的事件:

document.getElementById('search').click();

传统上,ECMAScript 几乎完全是作为浏览器中的 web 脚本工具。开发者用它来增强用户浏览网页的体验。今天,由于 V8 或 TraceMonkey 等独立引擎,ECMAScript 在服务器上和在浏览器上一样得心应手。

ECMAScript 标准团体预见到了开发人员传统上使用 JavaScript 的方式和最近增长的地方之间日益增长的差异。在最新规范中定义“web 脚本”时,它明智地提供了两个示例,展示了 ECMAScript 目前流行的各种环境:

Web browser provides ECMAScript host environment for client computing, for example, including objects representing windows, menus, pop-ups, dialog boxes, text areas, anchors, frames, history, cookies and input/output. In addition, the host environment provides a means to attach script code to events such as focus change, page and image loading, unloading, error and suspension, selection, form submission and mouse action. The script code appears in HTML, and the displayed page is a combination of user interface elements and fixed and calculated text and images. Script code is a response to user interaction and does not need the main program. Web server provides different host environments for server-side computing, including objects representing requests, clients and files; And sharing data. By using both browser-side and server-side scripts at the same time, the calculation can be distributed between client and server, and a customized user interface can be provided for Web-based applications. Each Web browser and server that supports ECMAScript provides its own host environment, thus perfecting the ECMAScript execution environment.

Note

在撰写本文时,ECMAScript 6 的最新版本(名为“Harmony”)即将到来,尽管尚未正式发布,但许多提议的更改已经得到了运行时引擎和浏览器的支持。这一章详尽地介绍了该语言的核心,其中也包括了在 Harmony 中引入的一些新特性。当我解释一个可能支持有限的提议特性时,我会特别注意提醒读者。

对象概述

JavaScript 是一种面向对象编程(OOP)语言,由 Brendan Eich 创建,他在为 Netscape 工作时经过几周的开发后发布了这种语言。JavaScript 虽然名字里有“Java”,但是和 Java 语言关系不大。在 InfoWorld 的一次采访中,Eich 解释了导致这种语言被改名为“JavaScript”的事件的转变:据我所知,JavaScript 开始是 Mocha,然后变成 LiveScript,然后在 Netscape 和 Sun 合作时变成 JavaScript。但实际上和 Java 没什么关系或者关系不大,对吧?艾希:没错。从 1995 年 5 月到 12 月的六个月里,先是摩卡,然后是 LiveScript。然后在 12 月初,网景和 Sun 签订了一份许可协议,它变成了 JavaScript。我们的想法是让它成为 Java 和编译语言的补充脚本语言。2T5】

即使随便比较一下这两种语言,也会发现明显的差异。与 Java 不同,JavaScript 不被编译,不强制严格的类型,也没有正式的基于类的继承机制。相反,JavaScript 在主机环境(例如,web 浏览器)的上下文中执行,支持变量的动态类型化,并且通过原型链而不是类来实现继承。因此,我们也许应该把两个名字之间的相似之处记在账上,作为对营销协同作用的渴望,而不是试图在两种语言之间建立有意义的联系。

尽管 Java 和 JavaScript 有所不同,但它们都是 OOP 家族的成员。面向对象意味着对象通过相互通信来控制程序的运行。OOP 语言是一些流行的编程范例,其中包括函数式、命令式和声明式。

Note

仅仅因为 JavaScript 被认为是一种面向对象的语言,并不意味着它局限于这种范式。比如现在流行的库下划线. js 3 就是用函数式编程风格编写的。

客体化

作为一门 OOP 语言意味着什么?对于有经验的程序员来说,这似乎是一个不必要的问题,但是回答这个问题的行为给了你评估 JavaScript 的 OOP 方法所需要的空间。本书的大部分内容都是关于对象及其相互关系的设计和思考,但重要的是要记住,对象只是用于建模程序的许多可能隐喻中的一种。

隐喻是诱人的,而且往往模糊不清,因为它们揭示了;它们的启示可能会让你清晰地构思出一个问题的解决方案,同时不必要地使另一个问题复杂化。当你回答 OOP 意味着什么时,反思你自己的理解和假设。你可能会发现你对这个概念有偏见。

JavaScript 中的对象只不过是属性的容器。我曾听程序员把它们描述为“属性包”,这给人一种赏心悦目的感觉。每个对象可以有零个或多个属性,这些属性可以保存一个原始值或引用一个复杂对象的指针。JavaScript 可以用三种方式创建对象:使用文字符号、new()操作符或create()函数。以最简单的形式,这三种方法可以表达如下:

var foo = {}

bar = new Object()

baz = Object.create(null);

这两种方法的区别在于对象的初始化方式,我们将在后面详细介绍。现在,我将描述通过给对象分配自定义属性来修饰它们的方法。

道具员

许多开发人员认为对象的属性只是一个可以被赋予名称和值的容器。然而实际上,JavaScript 为开发人员提供了一系列强大的属性描述符,这些描述符进一步塑造了属性的行为。现在让我们对它们进行迭代:

可配置的

当该属性设置为 true 时,受影响的属性可以从父对象中删除,并且属性的描述符可以在以后修改。当设置为 false 时,属性的描述符将被密封,以防进一步修改。这里有一个简单的例子:

var car = {};

// A car can have any number of doors

Object.defineProperty(car, 'doors', {

configurable: true

value: 4

});

// A car must have only four wheels

Object.defineProperty(car, 'wheels', {

configurable: false

value: 4

});

delete car.doors;

// => "undefined"

console.log(car.doors);

delete car.wheels;

// => "4"

console.log(car.wheels);

Object.defineProperty(car, 'doors', {

configurable: true

value: 5

});

// => "5"

console.log(car.doors);

// => Uncaught TypeError: Cannot redefine property: wheels

Object.defineProperty(car, 'wheels', {

configurable: true

value: 4

});

正如你在前面的例子中看到的,wheels变得固定,而doors保持可延展性。程序员可能希望撤销属性的configurable属性,作为一种防御性编程的形式,以防止对象被修改,就像该语言的内置对象一样。

可列举的

如果使用代码迭代对象的属性,则会出现可枚举属性。当设置为 false 时,不能迭代这些属性。这里有一个例子:

var car = {};

Object.defineProperty(car, 'doors', {

writable: true

configurable: true

enumerable: true

value: 4

});

Object.defineProperty(car, 'wheels', {

writable: true

configurable: true

enumerable: true

value: 4

});

Object.defineProperty(car, 'secretTrackingDeviceEnabled', {

enumerable: false

value: true

});

// => doors

// => wheels

for (var x in car) {

console.log(x);

}

// => ["doors", "wheels"]

console.log(Object.keys(car));

// => ["doors", "wheels", "secretTrackingDeviceEnabled"]

console.log(Object.getOwnPropertyNames(car));

// => false

console.log(car.propertyIsEnumerable('secretTrackingDeviceEnabled'));

// => true

console.log(car.secretTrackingDeviceEnabled);

从前面的例子中可以看出,即使一个属性是不可枚举的,也不意味着该属性完全隐藏了。可枚举属性可用于阻止程序员使用该属性,但不应用作保护对象属性免受检查的方法。

可写的

为 true 时,可以更改与属性关联的值;否则,该值保持不变。

var car = {};

Object.defineProperty(car, 'wheels', {

value: 4

writable: false

});

// => 4

console.log(car.wheels);

car.wheels = 5;

// => 4

console.log(car.wheels);

检查物体

在上一节中,您学习了如何在您创建的对象上定义自己的属性。就像在生活中一样,知道如何读和写是很有帮助的,所以在这一节中,您将学习如何在 JavaScript 中挖掘对象的底层。下面是在检查对象时值得了解的函数和属性列表。

Object.getOwnPropertyDescriptor

在上一节中,您看到了设置属性的各种方法。Object.getOwnPropertyDescriptor为您详细描述对象的任何属性的设置:

var o = {foo : 'bar'};

// Object {value: "bar", writable: true, enumerable: true, configurable: true}

Object.getOwnPropertyDescriptor(o,'foo');

Object.getOwnPropertyNames

此方法返回对象的所有属性名,甚至是无法枚举的属性名:

var box = Object.create({}, {

openLid: {

value: function () {

return "nothing";

}

enumerable: true

}

openSecretCompartment: {

value: function () {

return 'treasure';

}

enumerable: false

}

});

// => ["openLid", "openSecretCompartment"]

console.log(Object.getOwnPropertyNames(box).sort());

object.getprototypeof

此方法用于返回特定对象的原型。代替这个方法,可以使用__proto__方法,这是许多解释器实现的一种访问对象原型的方法。然而,__proto__总是被认为是一种黑客,JavaScript 社区主要将其作为权宜之计。然而,值得注意的是,即使Object.getPrototypeOf允许您访问对象的原型,设置对象实例的原型的唯一方法是使用__proto__属性。

var a = {};

// => true

console.log(Object.getPrototypeOf(a) === Object.prototype && Object.prototype === a.__proto__);

object . has owned 属性

JavaScript 的原型链允许你迭代一个对象的实例,并返回所有可枚举的属性。它包括不存在于对象上但存在于原型链中某处的属性。hasOwnProperty方法允许您识别对象实例上是否存在相关的属性:

var foo = {

foo: 'foo'

};

var bar = Object.create(foo, {

bar: {

enumerable: true

value: 'bar'

}

});

// => bar

// => foo

for (var x in bar) {

console.log(x);

}

var myProps = Object.getOwnPropertyNames(bar).map(function (i) {

return bar.hasOwnProperty(i) ? i : undefined;

});

// => ['bar']

console.log(myProps);

对象.键

此方法仅返回对象的可枚举属性列表:

var box = Object.create({}, {

openLid: {

value: function () {

return "nothing";

}

enumerable: true

}

openSecretCompartment: {

value: function () {

return 'treasure';

}

enumerable: false

}

});

// => ["openLid"]

console.log(Object.keys(box));

Object.isFrozen

如果被检查的对象无法扩展且其属性无法修改,则此方法返回 true 或 false:

var bombPop = {

wrapping: 'plastic'

flavors: ['Cherry', 'Lime', 'Blue Raspberry']

};

// => false

console.log(Object.isFrozen(bombPop));

delete bombPop.wrapping;

// undefined;

console.log(bombPop.wrapping);

// prevent further modifications

Object.freeze(bombPop);

delete bombPop.flavors;

// => ["Cherry", "Lime", "Blue Raspberry"]

console.log(bombPop.flavors);

// => true

console.log(Object.isFrozen(bombPop));

Object.isPrototypeOf

此方法检查给定对象的原型链中的每个链接是否存在另一个对象:

// => true

Object.prototype.isPrototypeOf([]);

// => true

Function.prototype.isPrototypeOf(()=>{});

// => true

Function.prototype.isPrototypeOf(function(){});

// => true

Object.prototype.isPrototypeOf(()=>{});

Note

在撰写本文时,所谓的胖箭头语法仅在 Firefox 22 (SpiderMonkey 22)等浏览器中受支持。在不受支持的浏览器中运行箭头函数会产生语法错误。

Object.isExtensible

默认情况下,JavaScript 中的新对象是可扩展的,这意味着可以添加新的属性。但是,可以标记一个对象以防止它在将来被扩展。在某些环境中,在不可扩展的对象上设置属性会引发错误。在试图修改对象之前,您可以使用Object.isExtensible来检查它:

var car = {

doors: 4

};

// => true

console.log(Object.isExtensible(car) === true);

Object.preventExtensions(car);

// => false

console.log(Object.isExtensible(car) === true);

Object.isSealed

该函数返回 true 或 false,具体取决于对象是否无法扩展以及其所有属性是否不可配置:

var ziplockBag = {};

// => false

console.log(Object.isSealed(ziplockBag) === true);

// => true

console.log(Object.isExtensible(ziplockBag));

Object.seal(ziplockBag);

// => true

console.log(Object.isSealed(ziplockBag) === true);

// => false

console.log(Object.isExtensible(ziplockBag));

对象. valueOf

如果你曾经试图检查一个对象,却看到它吐出“[object Object]”,那你就见识过这个功能的厉害了。Object.valueOf用来形容一个物体具有一个原始值。所有对象都接收这个函数,但它本质上是一个存根,意味着以后会被自定义函数覆盖。创建你自己的valueOf函数提供了一种给你的自定义对象一个额外的描述细节层的方法:

var Car = function (name) {

this.name = name;

}

var tesla = Object.create(Car.prototype, {

name: {

value: 'tesla'

}

});

// => [Object object]

console.log(tesla.valueOf());

Car.prototype.valueOf = function () {

return this.name;

}

// => tesla

console.log(tesla.valueOf());

Object.is (ECMAScript 6)

测试两个值的相等性一直是一些 JavaScript 开发人员的痛处,因为 JavaScript 实际上支持两种形式的相等性比较。为了检查抽象的相等性,JavaScript 使用双重相等语法==。当检查严格相等时,JavaScript 使用三重相等语法===。两者之间的主要区别在于,默认情况下,抽象等式运算符会强制一些值来进行比较。Object.is方法确定两个提供的参数是否具有相同的值,而不需要强制。以下是一些如何使用Object.is方法的例子:

// True because both strings use the same characters and length.

Object.is('true', 'true')

// False because type case counts as a difference.

Object.is('True', 'true')

// True because function is coerced to true using the logical not operator.

Object.is(!function(){}(), true)

// True because the built-in Math object has no prototype.

Object.is(undefined, Math.prototype);

不要将此行为与严格相等比较运算符混淆,后者仅在两者共享同一类型而不是同一值时返回 true。下面的例子可以很容易地表示出来:

// => false

console.log(NaN === 0/0);

// => true

Object.is(NaN,0/0);

修改对象

除了能够探索现有对象的结构,能够修改(或防止修改)也是必不可少的。这一部分解释了各种可以让物体按照你的意愿弯曲的机制。

对象.冻结

冻结对象可以防止它被再次更改。冻结对象不能接受新特性、删除现有特性或更改其值:

var bombPop = {

wrapping: 'plastic'

flavors: ['Cherry', 'Lime', 'Blue Raspberry']

};

// => false

console.log(Object.isFrozen(bombPop));

delete bombPop.wrapping;

// undefined;

console.log(bombPop.wrapping);

// prevent further modifications

Object.freeze(bombPop);

delete bombPop.flavors;

// => ["Cherry", "Lime", "Blue Raspberry"]

console.log(bombPop.flavors);

// => true

console.log(Object.isFrozen(bombPop));

object . define 属性

该功能允许定义新属性或修改现有属性:

var car = {};

Object.defineProperties(car, {

'wheels': {

writable: true

configurable: true

enumerable: true

value: 4

}

'doors': {

writable: true

configurable: true

enumerable: true

value: 4

}

});

// => 4

console.log(car.doors);

// => 4

console.log(car.wheels);

对象.定义属性

此功能允许将单个属性添加到对象或修改现有属性:

var car = {};

Object.defineProperty(car, 'doors', {

writable: true

configurable: true

enumerable: true

value: 4

});

Object.preventExtensions

此功能防止新属性被添加到现有对象中。但是,不要将这种方法与冻结对象相混淆。虽然一个对象不能被扩展,但它可以被缩减,这意味着属性是可移除的。

var car = {

doors: 4

};

// => true

console.log(Object.isExtensible(car) === true);

Object.preventExtensions(car);

// => false

console.log(Object.isExtensible(car) === true);

// => 4

console.log(car.doors);

delete car.doors;

// => undefined

console.log(car.doors);

car.tires = 4;

// => undefined

console.log(car.tires);

对象.原型

设置对象的原型会将该对象与其现有的原型链分离,并将其附加到指定的新对象的末尾。这对于用另一个对象及其链中的对象的属性和方法来填充对象很有用。

var Dog = function () {};

Dog.prototype.speak = function () {

return "woof";

};

var Cat = function () {};

Cat.prototype.speak = function () {

return "meow";

};

var Tabby = function () {};

Tabby.prototype = new Cat();

var tabbyCat = new Tabby();

// => 'meow'

console.log(tabbyCat.speak());

// => undefined

console.log(tabbyCat.prototype);

// Setting the prototype of an object instance will not affect the instantiated properties

tabbyCat.prototype = new Dog();

// => Dog { speak: function }

console.log(tabbyCat.prototype);

// => 'meow'

console.log(tabbyCat.speak());

对象.密封

密封对象使其不可变,这意味着不能添加新属性,并且现有属性被标记为不可配置。这与冻结对象以防止对象被进一步修改不同,如下例所示:

var envelope = {

letter: 'To whom it may concern'

};

// => false

Object.isSealed(envelope);

Object.seal(envelope);

envelope.letter = "Oh Hai";

envelope.stamped = true;

// => Oh Hai

console.log(envelope.letter);

// => undefined

console.log(envelope.stamped);

调用对象

有时,一个对象借用另一个对象的函数是很有用的,这意味着借用对象只是执行借出的函数,就像它是自己的函数一样。就像你可能向朋友借一件毛衣一样。你暂时用这件毛衣来取暖,但用完之后就把它还回去。JavaScript 中的毛衣借用分别通过call()apply()函数完成。它们的行为非常相似,除了call()函数接受一个参数列表,而apply()函数期望一个参数数组。这些方法对于向对象注入临时功能非常有用,比如使用核心对象的内置函数或者链接对构造函数的调用。

函数调用和函数应用

var friend = {

warmth: 0

useSweater: function (level) {

this.warmth = level;

}

};

var me = {

warmth: 0

isWarm: function () {

return this.warmth === 100;

}

};

// => false

console.log(me.isWarm());

try {

me.useSweater(100);

} catch (e) {

// => Object #<Object> has no method 'useSweater'

console.log(e.message);

}

friend.useSweater.call(me, 100);

// => true

console.log(me.isWarm());

me.warmth = 0;

// => false

console.log(me.isWarm());

friend.useSweater.apply(me, [100]);

// => true

console.log(me.isWarm());

创建对象

JavaScript 将几乎所有东西都视为一个对象,因此该语言的几乎每个元素都可以被创建、分配属性并链接到原型链。唯一的例外是语言的饿鬼nullundefined。在 JavaScript 中创建对象时,它们不是由整块布制成的。在这一节中,我将解释创建对象的三种方法,以及为什么需要不止一种方法。

Note

我曾经错误地认为数字不是对象,因为我不能使用点语法对它们调用方法——例如,1.toString()。事实证明,大多数解释者认为周期是整数和分数之间的分界线。如果使用分组括号(1)调用方法。toString()或双周期 1..toString(),成功了!

对象文字

文字语法将代码中的其他对象描述为一系列用逗号分隔的属性,这些属性用花括号括起来。与new Object()Object.create()语法不同,字面语法没有被显式调用,因为字面符号实际上是在特定上下文中使用Object.create方法的一种语法快捷方式。这里有一个例子:

var foo = {

bar: 'baz'

};

var foo2 = Object.create(Object.prototype, {

bar: {

writable: true

configurable: true

value: 'baz'

}

});

// => baz

console.log(foo.bar);

// => baz

console.log(foo2.bar);

字面语法清晰、富于表现力且简洁。你可以描述和创建你的对象,一步到位。这种特性使文字符号语法成为简单的一次性对象的最佳选择,这些对象用于处理事件、编组对象之间的状态变化,或者在保持代码可视化分组的同时划分功能。字面语法和new Object() form的另一个细微区别是,字面语法的构造函数不能被重定义。然而,本机Object构造函数属于全局名称空间,如果修改,可能会导致难以跟踪的意外行为。隐式调用字面语法的事实为代码提供了一点防御性编程。

var foo = new Object();

var bar = {};

// => object

console.log(typeof(foo))

// => object

console.log(typeof(bar))

window.Object = function(){ arguments.callee.call() };

// => Uncaught RangeError: Maximum call stack size exceeded

var foo = new Object();

字面语法并不适用于所有用例;例如,无法创建原型不是内置对象的对象。此外,因为文本语法是隐式调用的,所以没有显式的构造函数,这意味着对象文本不是好的对象工厂。

Note

对象文字不是 JSON。很多人把 Object literal 语法和 JSON 混为一谈,即使它们看起来很相似,但它们并不相同。JSON 只是一种数据描述语言,所以不能包含函数。此外,许多 JSON 解析器希望使用双引号来定义属性,而字面语法并不要求这样做。

新对象()

当我谈到new Object()时,我真正讨论的是new运算符。该运算符按需创建对象的实例。它接受一个构造函数和一系列在初始化过程中使用的可选参数。创建时,新创建的对象继承自构造函数的原型。

var Animal, cat, dog;

Animal = function (inLove) {

this.lovesHumans = inLove || false;

};

cat = new Animal();

dog = new Animal(true);

// => false

console.log(cat.lovesHumans);

// => true

console.log(dog.lovesHumans);

操作符是 JavaScript 试图模仿 Java 的残余结构。许多人对new操作符感到困惑,因为它给 JavaScript 强加了一个伪经典词汇表,而 JavaScript 没有正式的基于类的继承方法。为了更好地理解new在幕后做什么,让我们以前面的例子为例,剖析一下new在为我们做什么。希望这能消除其语义带来的任何潜在的歧义。

1.JavaScript 创建一个新对象

这相当于创建一个对象文字{}

2.JavaScript 将新创建的对象的构造函数链接到Animal函数

/*

* function (inLove) {

* this.lovesHumans = inLove || false;

* }

*/

console.log(cat.constructor);

3.JavaScript 将对象的原型链接到 Animal.prototype

在构造过程中,新创建的对象获得对以前构造函数的属性的引用。它们是一个浅层拷贝,如果以后修改,实际发生的是对构造函数属性的引用现在被一个局部引用所掩盖。

var Animal, cat, dog;

Animal = function (inLove) {

this.lovesHumans = inLove || false;

};

cat = new Animal();

dog = new Animal(true);

// capture the errors so our script will continue to execute.

try {

// => Uncaught TypeError: Object [object Object] has no method 'jump'

console.log(cat.jump());

} catch (e) {}

/*

* We can change the base object and have the changes reflected downward even

* to objects who have already been instantiated.

*/

Animal.prototype.jump = function () {

return "how high?!";

};

// => how high?!

console.log(cat.jump());

// => how high?!

console.log(dog.jump());

/*

* Changes to the local property do not propagate up the prototype chain.

* Instead, the reference to the prototype's property is blocked by the new local

* property of the same name.

*/

cat.jump = function () {

return "no";

}

// => no

console.log(cat.jump());

// => how high?!

console.log(dog.jump());

4.JavaScript 将任何提供的参数分配给新创建的对象

new操作符在新创建的对象上封送任意数量的属性的初始化。它们作为参数传递给构造函数。

var Animal, dog;

Animal = function (inLove) {

this.lovesHumans = inLove || false;

};

//newis essentially doing this:

// dog = {}

// dog.lovesHumans = true;

dog = new Animal(true);

如果你认为new是一个乐于助人的工作精灵,它会按照食谱为你制作物品,那就没问题了。但是,如果你假设new的行为和它在 Java 等其他语言中的行为一样,那你就不好过了。

对象.创建

在 ECMAScript 5 中引入Object.create之前,创建原型继承的唯一方法是使用new操作符。然而,出于所有的意图和目的,应该使用Object.create()和字面符号来代替new Object()Object.create()为开发者提供了与new相同的好处,但是方法签名与语言的其他部分更加一致。Object.create的优势不仅仅是语义上的改进,Object.create实际上更加强大,主要是在它如何支持继承方面。Object.create接受两个参数:一个用作原型的对象和一个可选的 property 对象,该对象包含用来配置新创建的对象的值。

var Car = {

drive: function (miles) {

return this.odometer += miles;

}

};

var tesla = Object.create(Car, {

'odometer': {

value: 0

enumerable: true

}

});

// => 10

console.log(tesla.drive(10));

本节研究了使用 JavaScript 创建、访问和修改对象的各种方式。一路上,我暗示了原型概念是如何工作的。下一节将解释 JavaScript 如何在 OOP 中实现常见的策略,比如继承,以及开发人员在尝试使用它时遇到的一些常见问题。

编程原型

OOP 语言的目的是创建虚拟对象,这些虚拟对象能够相互通信以完成任务。通常,这意味着用代码对实体的表示进行建模,然后让软件使用它来实现开发人员的目标。虽然前面的定义听起来很简单,但现实是,在编排对象之间的数据和状态交换时,经常会出现不可避免的混乱。当将复杂的现实世界问题领域转化为一系列相互依赖的对象时,尤其如此。一般来说,OOP 语言通过应用更高阶的概念,包括抽象、封装、继承和多态,减轻了将实体翻译成代码时固有的组织复杂性。在大多数 OOP 语言中,这些技术是使用类来应用的。

C++、JAVA 和 Ruby 等语言中的类是对象的描述,而不是对象本身。同样,你不会因为吃了一份冰激凌而大脑冻结,你也不能用一个类来执行工作。类是有意抽象的,因为它们必须定义它们所创建的潜在对象的所有特征、功能和启示。基于类的语言的支持者说,它们在结构和状态之间提供了清晰的描述。相反的论点是,类迫使一个不必要的僵化的本体对对象进行分类。

在 JavaScript 中,没有类定义这种东西。对象通过原型链接从其他对象继承它们的功能(如果需要)。这些原型链接可以反过来形成彼此之间的依赖链,从而通过组合实现复杂的行为。本节详细解释了原型概念的复杂性,以及如何在 JavaScript 中最大化其有效性。

为了充分解释使用原型编程的好处,您首先需要理解抽象、封装、继承和多态性的目标,因为它们适用于 JavaScript。作为对这四个概念的解释的一部分,我将使用编程示例来帮助清楚地描述 JavaScript 的原型和许多其他程序员可能更熟悉的基于类的方法之间的区别。

抽象

编程中的抽象是将现实世界的对象或过程在精神上转换成计算模拟的虚拟构造。抽象为程序员提供了一种机制,开始将复杂的主题分解成更小的离散部分。在大多数 OOP 语言中,这个过程被称为解耦。从类或原型的角度考虑问题是抽象的,因为它们给出了一个方便的隐喻来组织我们的程序,同时隐藏了与机器对话的实际低级代码。关于抽象的一个常见误解是,它们只是为了隐藏信息、将内容解耦到模块中,或者定义对象之间的清晰接口。尽管这些是抽象的战略目标,但是实现这些目标的策略会因语言而异。在 JavaScript 中,所有抽象的根源都是原型的使用,原型是处理封装、继承和多态的实际机制。

包装

软件设计中的封装有三个目标:隐藏实现、促进模块化和保护对象的内部状态。设计良好的对象隐藏了不需要的或特权信息,不让公众看到。封装通过定义一个公共接口来做到这一点,该接口为程序员提供了关于如何使用对象的足够信息,同时隐藏了它如何工作的细节。通过封装进行信息隐藏还允许业务逻辑的实现随着时间的推移而改变,而不会影响向用户公开的公共接口。这就像用户学习驾驶一辆汽车:一旦他们明白如何使用方向盘和踏板,发动机有多少个阀门就无关紧要了。

如果我们扩展前面的例子,我敢打赌,你可以在汽车之间交换引擎,而不需要司机重新学习如何驾驶汽车。他们可能会注意到汽车性能的不同,但界面保持不变。这个观察暗示了封装的下一个好处,那就是它促进了代码设计的模块化。

真正的封装还提供了第三个好处,即防止私有逻辑被其他对象访问或修改。这样,封装就像类周围的保护屏障,确保对象的内部工作不受干扰。

在基于类的语言中隐藏实现的一种流行方法是使用私有和公共函数。函数的私有性质是由语言强制实施的限制,这使得某些代码对类实例可用,但对外部对象不可访问。下面是一个 Java 示例:

public class Car{

private String name;

private int wheelCount;

public String getName(){

return name;

}

public void setName(String newName){

name = newName;

}

public String getWheelCount(){

return wheelCount;

}

public void setWheelCount( String wheels){

wheelCount = wheels;

}

}

正如你在前面的例子中看到的,直接访问namewheelCount变量是不可能的,因为 Java 允许它们被声明为私有的。若要访问它们,必须改用类的公共方法。通常,这些代理方法被称为 getters 和 setters。通过这种方式,仍然可以使用这些变量,尽管是通过一个受控的接口。

JavaScript 基于原型的方法的一个结果是,它防止对象将属性指定为私有,这使得封装更加困难(但不是不可能!).

var Car = function(){

var name = 'Tesla';

var wheelCount = '4';

this.getName = function(){

return name;

}

this.getWheelCount = function() {

return wheelCount;

}

this.setName = function(newName) {

name = newName;

}

this.setWheelCount = function(newCount) {

wheelCount = newCount;

}

}

var myCar = new Car();

// #=> undefined

console.log(myCar.name);

myCar.name = "Corvette";

// #=> 'Corvette'

console.log(myCar.name);

// #=> 'Tesla'

console.log(myCar.getName());

// #=> 'Corvette'

myCar.setName('Corvette');

console.log(myCar.getName());

在这个脚本中,您可以看到在函数体内定义了两个局部变量。由于 JavaScript 的函数级作用域的工作方式,这两个变量是隐式私有的。为了向外部公开它们的值,您可以创建自己的 getter 和 setter 方法。关键的一点是,通过使用局部变量而不是对象属性,它们的值不会被外部访问。

这种方法提供了良好的封装,因为它通过信息隐藏促进了模块化,并保护对象的内部状态免受不必要的全局访问。

多态性

多态描述了一个对象在特定环境下像另一个对象一样工作的能力。在 OOP 语言中有许多类型的多态性,但是“特别多态性” 4 在 JavaScript 中尤其普遍和有用。这一节探讨了特殊多态性是如何工作的。

特定多态性

即席多态为对象提供了使用调用上下文来决定结果的能力。上下文可能包括调用对象或提供给方法的参数类型。临时多态性有时被称为函数重载或操作符重载,因为这些技术是实现这种形式的多态性的常用方法。

函数重载

在 C++等静态类型语言中,函数重载允许开发人员定义多个同名函数,只要它们的方法签名互不相同。函数之间的区别是通过要求不同数量的自变量或不同类型的自变量来实现的。一旦实现,就由编译器根据所提供的参数的数量和类型来选择正确的函数。

JavaScript 函数不执行类型检查,可以接收任意数量的参数。这种灵活性意味着函数重载开箱即用,无需声明同一函数的多种风格。

运算符重载

许多语言都支持运算符重载,开发者可以通过重载重新定义运算符的工作方式。JavaScript 不支持这种级别的重载,但是允许操作者根据使用它们的上下文来改变它们的行为。根据使用情况,考虑“+”操作符的行为。

// summation

// => 2

console.log(1+1);

// concatenation

// => "foo bar"

console.log("foo " + "bar");

// accumulation

// => 2

var num = 1;

console.log(num++);

遗产

继承定义了对象之间的语义层次,允许孩子创建父类的专门化、一般化或变体。5T5】

遗产的定义字面意思是将权利、财产和义务传给另一方(通常是在死后)T5】6。在基于类的语言中,继承被描述为在对象之间形成“is-a”7关系(类DogMammal的子类,而AnimalMammal的超类)。

事实上,孩子可以从他们的父母那里继承规范,这使得许多开发人员相信,继承也为程序员提供了在他们的系统中重用代码的渠道。直觉上这是有意义的;想象一个所有对象都互相共享属性的集合。通过将这些公共特性提取到一个基类中,每个孩子都将自动受益于这些特性,而不必在内部重新定义这些特性。

然而,通过继承来重用代码是非常困难的,因为在大多数语言中,一个孩子只能从一个父母那里继承。这种限制会导致类继承它不需要的代码,或者需要重写父类的功能。Angus Croll 简洁地描述了使用继承进行代码重用的问题,他写道:

Using inheritance as a tool for code reuse is a bit like ordering a happy package because you want plastic toys. Of course, the circle is a shape, and the dog is a mammal-but once we go beyond those textbook examples, most of our hierarchical systems will become arbitrary and fragile-this is established to manipulate behavior, even if we pretend to represent reality. In order to reuse a few, successive generations are carrying more and more unexpected or irrelevant behaviors. 8 T5】

改变类的继承品质的需要混淆了父类和子类之间的关系。此外,通过省略或覆盖父元素的某些方面,子元素还会破坏封装,并通过紧密耦合 9 来提升脆弱的代码。

JavaScript 中的继承也绝不完美。JavaScript 使用差分继承 10 ,其中所有的对象都是从一个通用的基类而不是某个父类派生出来的。每个被创建的对象都有一个对创建它的对象的引用,这是该对象的原型。基于类的继承根据相似性定义对象之间的关系,而差异继承使用原型和后代之间的微小差异作为分界线。

原型的力量

包括 JavaScript 在内的基于原型的语言允许一个对象通过原型链接引用另一个对象,从而增加了对象的复杂性。JavaScript 使用原型链作为对象之间的动态委托机制,其中引用属性的尝试沿着原型链向上传播,直到到达最后一个链接。实际上,原型为开发人员提供了组织和重用代码的灵活工具。本节探讨如何访问和扩充对象的原型链。

理解原型

在 JavaScript 中,可以通过三种方式访问原型:

  • Foo.prototype定义使用new运算符实例化的对象的原型;例如,new Foo()
  • Object.getPrototypeOf(foo)返回给定对象的原型引用。
  • Foo.__proto__是指向对象构造器自己的原型对象的属性。此属性引用是非标准的,但较旧的引擎可能依赖于它。因此,__proto__现在已经被编入最新版本的 ECMAScript (ES6)中。我提到这个属性只是为了完整性。如果你需要引用一个对象的原型,你应该选择标准化的Object.getPrototypeOf()而不是这个__proto__

下面的代码演示了读取 prototype 对象的各种方法:

var Car = function (wheelCount) {

this.odometer = 0;

this.wheels = wheelCount || 4;

};

Car.prototype.drive = function (miles) {

this.odometer += miles;

return this.odometer;

};

var tesla = new Car();

// => true

console.log(Object.getPrototypeOf(tesla) === Car.prototype);

// => true

console.log(tesla.__proto__ === Car.prototype);

拥有一个原型对象似乎有些危险,因为如果一个对象无意中修改了原型的一个属性会发生什么呢?事实证明,JavaScript 可以防止这类事情的发生;任何试图设置原型属性的行为都会在对象实例上产生一个新的属性,从而阻碍对原型链中某个位置的属性的访问。继续以汽车为例,您可以看到这一幕:

var tesla = new Car();

// => 4

console.log(tesla.wheels);

var isetta = new Car(3);

// =>3

console.log(isetta.wheels);

isetta.drive = function (miles) {

this.odometer -= miles;

return this.odometer;

};

// => -10

console.log(isetta.drive(10));

// => 10

console.log(tesla.drive(10));

// Changes made to the prototype are propagated throughout the chain.

Car.prototype.drive = function (miles) {

this.odometer += miles * 2;

return this.odometer;

};

// However it cannot propagate changes to properties defined inside the constructor.

Car.prototype.odometer = 0;

// => -20 no change because the local function obscures the prototype's new version

console.log(isetta.drive(10));

// => 30

console.log(tesla.drive(10));

这种方法有几个优点:

  • 通过链接对象访问的原型的属性仅仅是一个浅层引用,这增加了一层对意外更改的防御。
  • 浅属性引用节省内存,因为给定的属性或函数只有一个实例。
  • 添加到原型对象的属性会立即向下传播到属性链中较低的对象。

car 构造函数的最后一个例子试图在运行时重置odometer的值,希望它能重置所有实例的值。它失败了,因为odometer属性是在构造函数中定义的。然而,如果您已经在原型上以与驱动方法相同的方式定义了odometer,只要对象实例没有定义自己的里程表本地副本,更改就会生效,这发生在drive()功能期间。

var Car = function (wheelCount) {

this.wheels = wheelCount || 4;

};

Car.prototype.odometer = 0;

Car.prototype.drive = function (miles) {

this.odometer += miles;

return this.odometer;

};

var tesla = new Car();

// assign the odometer a new default value.

Car.prototype.odometer = 200;

// => 210

console.log(tesla.drive(10));

// assign it yet again.

Car.prototype.odometer = 2000;

// This change fails because the drive function set a local variable for odometer as it runs.

// => 220

console.log(tesla.drive(10));

按惯例分类

JavaScript 没有正式的基于类的结构。即使最后一节证明了这一事实,我也不会责怪一些读者在内心深处有一丝疑虑。也许这种怀疑是因为 JavaScript 领域充斥着对类或基于类的术语的引用。让事情更加混乱的是,该语言有一个保留的class关键字,它什么也不做!道格拉斯·克洛克福特认为 JavaScript 是伪经典的,因为他认为对象是由构造函数产生的,这是“不必要的间接层次”(Crockford,2008)。每当人们谈论 JavaScript 中的类时,他们谈论的是作为风格惯例的类,而不是语言的特性。

进行这种区分是很重要的,因为那些熟悉其他语言的类的人会带来某些心理产物和对它们如何工作的期望。这些先入为主的想法可能会让期望 JavaScript 有同样行为的开发人员迷失方向。接下来是对从类的角度思考的 JavaScript 开发人员的讨论,关于他们如何使用设计模式实现类行为。这种模式是内置语言特性和编码约定的混合。在基于类的面向对象语言中,一般来说,状态由实例承载,方法由类承载,继承只是结构和行为。在 ECMAScript 中,状态和方法由对象携带,而结构、行为和状态都是继承的。11T5】

构造器

直觉上,似乎构造函数的目标是构造一个对象。在 JavaScript 中,构造函数只不过是函数,当用一个new操作符调用时,它返回一个实例对象。在 JavaScript 中,任何使用new()操作符调用的函数都是构造函数。构造函数的目的是用合理的默认值初始化新创建的对象。根据经验,只定义从构造函数派生的所有实例所需的属性和函数。

var Car = function(){

// Instance Property

this.running = false;

// Instance Method

this.start = function(){

return this.running = true;

}

}

var tesla = new Car();

// => false

console.log(tesla.running);

// => true

console.log(tesla.start());

没有 new 运算符,并非所有内置函数都可以调用。这通常是因为内置对象没有可返回的合理默认值。调用Date()函数会返回一个表示当前日期和时间的字符串,而调用Math()函数会返回一个错误。

// => "Wed May 15 2013 15:42:24 GMT-0400 (EDT)"

Date()

// => TypeError: object is not a function

Math();

如果可能,最好从构造函数返回类似的结果,不管它是否在 new 运算符的上下文中被调用。大卫·赫尔曼在他的“让你的构造函数成为新的不可知论者”(Herman,2013)一节中详细讨论了这个话题。然而,JavaScript 的许多内置对象并不遵守这一约定。

// Zero is returned as specified by the built-in Number object's constructor.

// => 0

var num = Number();

// A new instance of the number object is returned.

// => Number {}

var num = new Number();

Note

JavaScript 没有正式的类,但是它遵循其他语言中使用的命名约定,在这些语言中使用大写名称(例如,“Foo”表示类对象)。

实例属性

实例属性是描述对象实例质量的任何可公开访问的变量。实例属性是那些可能因对象而异的值。在前面的示例中,this.running是一个实例属性。实例属性可以在构造函数内部定义,也可以作为原型对象的一部分单独定义。

var Car = function(wheelCount){

this.wheels = wheelCount || 4

}

Car.prototype.odometer = 0;

var tesla = new Car();

// => 4

console.log(tesla.wheels);

// => 0

console.log(tesla.odometer);

实例方法

实例方法提供对对象实例有用的功能。实例方法还可以访问实例属性。实例方法可以用两种方式定义:它可以通过引用this关键字来扩展实例,或者直接将属性设置为原型链。

var Car = function(){

// Instance Property

this.running = false;

// Instance Method

this.start = function(){

return this.running = true;

}

}

Car.prototype.stop = function() {

return this.running = false;

}

var tesla = new Car();

// => false

console.log(tesla.running);

// => true

console.log(tesla.start());

// => false

console.log(tesla.stop());

类别属性

类属性是属于类对象本身的变量。它们对于永远不会改变的属性很有用,比如常量。核心Math对象有一个类属性PI,其默认值为3.141592653589793。在 JavaScript 中,类属性可以直接在构造函数上设置。

var Cake = function () {};

Cake.isLie = true;

类方法

类方法,有时被称为静态方法,是只对类本身可用的函数。类方法可以访问类属性,但不能访问对象实例的属性。类方法通常是对提供的参数执行计算并返回结果的实用函数。例如,考虑核心Math对象的各种类方法。类方法的定义方式与类属性相同。如果您想给内置的String对象添加一个反向的类方法,您可以简单地这样写:

String.reverse = function (s) {

return s.split("").reverse().join("");

};

// => secret message

console.log(String.reverse("egassem terces"));

Note

即使允许,你也不会真的想要像这样扩展一个核心 JavaScript 对象。这至少被认为是不礼貌的,但可能会给你或他人的代码带来错误。此规则的一个例外是,当一个对象通过使用 polyfill 进行扩展,以填充其他代码所期望的缺失功能时。

摘要

对象是 JavaScript 的构造块,为了确保您的构造尽可能地坚固,请考虑以下关键概念:

  • 对象是包含零个或多个属性的包。
  • 对象属性要么是基本类型,要么是复杂类型。对象可以保存自己的基本类型副本,但只能指向复杂类型。因此,JavaScript 属性被认为是通过引用传递或通过值传递。
  • 对象属性可以有标志,这些标志在被修改时会改变对象的行为和功能。
  • 可以通过以下三种方式之一创建对象:
    • 使用字面语法'{}'
    • 将 new 运算符与构造函数'new Foo()'结合使用
    • 使用内置的Object.create()功能。
  • JavaScript 是一种基于原型的语言,其中对象通过原型链的链接相互关联。
  • 当检查一个对象的属性时,它会查询原型链的每一步,直到它被返回或被确定为未定义。
  • 当在存在于原型链中某处的对象上设置属性时,原型属性不会改变;相反,在本地对象上定义了一个新的属性,阻止对远程原型属性的访问。
  • JavaScript 没有正式的类机制;所有类代码的使用都是约定,而不是语言的属性。
  • JavaScript 使用差分继承意味着内存占用通常比使用抽象类要小得多。
  • JavaScript 是一种面向对象的语言,但这并不妨碍您用许多其他编程范式编写 JavaScript。

Footnotes 1

T2http://remysharp.com/2010/10/08/what-is-a-polyfill/

2

T2http://www.infoworld.com/d/developer-world/javascript-creator-ponders-past-future-704

3

T2http://underscorejs.org/

4

T2http://en.wikipedia.org/wiki/Ad_hoc_polymorphism

5

T2http://isase.us/wisr3/7.pdf

6

T2http://en.wikipedia.org/wiki/Inheritance

7

T2http://en.wikipedia.org/wiki/Is-a

8

T2http://javascriptweblog.wordpress.com/2010/12/22/delegation-vs-inheritance-in-javascript/

9

[T0](http://en.wikipedia.org/wiki/Coupling_(computer_programming)T1】

10

T2https://developer.mozilla.org/en/docs/Differential_inheritance_in_JavaScript

11

ECMA 262 版工作草案