25.单个对象

原文: http://exploringjs.com/impatient-js/ch_single-objects.html

在本书中,JavaScript 的面向对象编程(OOP)风格分四步介绍。本章介绍步骤 1,下一章涵盖步骤 2-4。步骤是(图 7 ):

  1. 单个对象: 对象 ,JavaScript 的基本 OOP 构建块如何独立工作?
  2. 原型链:每个对象都有一个零个或多个 原型对象链 。原型是 JavaScript 的核心继承机制。
  3. 类:JavaScript 的 是对象的工厂。类及其实例之间的关系基于原型继承。
  4. 子类化: 子类 与其 超类 之间的关系也基于原型继承。

Figure 7: This book introduces object-oriented programming in JavaScript in four steps.

Figure 7: This book introduces object-oriented programming in JavaScript in four steps.

25.1。 JavaScript 中对象的两个角色

在 JavaScript 中,对象是一组称为 属性 的键值条目。

对象在 JavaScript 中扮演两个角色:

  • 记录:作为记录的对象具有固定数量的属性,其密钥在开发时是已知的。它们的值可以有不同的类型。本章首先介绍了这种使用对象的方法。

  • 字典:Objects-as-dictionaries 具有可变数量的属性,其密钥在开发时是未知的。它们的所有值都具有相同的类型。通常最好将映射用作词典而不是对象(本章稍后将对此进行介绍)。

25.2。对象作为记录

25.2.1。对象字面值:属性

作为记录的对象是通过所谓的 对象字面值 创建的。对象字面值是 JavaScript 的一个突出特点:它们允许您直接创建对象。不需要上课!这是一个例子:

在这个例子中,我们通过一个对象字面值创建了一个对象,它以花括号{}开头和结尾。在其中,我们定义了两个 属性 (键值条目):

  • 第一个属性具有键first和值'Jane'
  • 第二个属性具有键last和值'Doe'

如果以这种方式编写它们,则属性键必须遵循 JavaScript 变量名称的规则,但允许使用保留字。

访问属性如下:

25.2.2。对象字面值:属性值缩写

每当通过变量名定义属性的值并且该名称与键相同时,您可以省略该键。

25.2.3。术语:属性键,属性名称,属性符号

鉴于属性键可以是字符串和符号,因此进行以下区分:

  • 如果属性键是字符串,则它也称为 属性名称
  • 如果属性键是符号,则它也称为 属性符号

此术语用于 JavaScript 标准库(“自己”表示“未继承”,将在下一章中介绍):

  • Object.keys(obj):返回obj的所有属性键
  • Object.getOwnPropertyNames(obj)
  • Object.getOwnPropertySymbols(obj)

25.2.4。获得属性

这就是你 得到 (读)财产的方式:

如果obj没有其键为propKey的属性,则此表达式求值为undefined

25.2.5。设置属性

这就是你 设置 (写入)属性的方式:

如果obj已经有一个键为propKey的属性,则此语句将更改该属性。否则,它会创建一个新属性:

25.2.6。对象字面值:方法

以下代码显示如何通过对象字面值创建方法.describe()

在方法调用jane.says('hello')期间,jane被称为方法调用的 接收器 ,并被分配给特殊变量this。这使方法.says()能够访问 A 行中的兄弟属性.first

25.2.7。对象字面值:访问者

JavaScript 中有两种访问器:

  • getter 调用 (读取)属性调用的方法。
  • setter 是由 设置 (写入)属性调用的方法。
25.2.7.1。吸气剂

通过在方法定义前添加关键字get来创建 getter:

25.2.7.2。塞特斯

通过在方法定义前添加关键字set来创建 setter:

25.单个对象 - 图2 练习:通过对象字面值创建对象

exercises/single-objects/color_point_object_test.js

25.3。传播到对象字面值(...

我们已经看到在函数调用中使用扩展(...),它将迭代的内容转换为参数。

在对象字面值内部, 扩展属性 将另一个对象的属性添加到当前对象:

如果属性键发生冲突,则上次提到的属性为“wins”:

25.3.1。传播的用例:复制对象

您可以使用 spread 来创建对象的副本original

警告 - 复制是 copy是一个新对象,带有original的所有属性(键值对)的副本。但是如果属性值是对象,那么不会复制它们;它们在originalcopy之间共享。以下代码演示了这意味着什么。

25.单个对象 - 图3 JavaScript 不做深拷贝

对象的深拷贝 (所有级别都被复制)是众所周知的难以做到的。因此,JavaScript 没有内置的操作(暂时)。如果您需要这样的操作,则必须自己实现。

25.3.2。传播的用例:缺失属性的默认值

如果代码的其中一个输入是包含数据的对象,则可以在为其指定默认值时使属性成为可选属性。这样做的一种技术是通过其属性包含默认值的对象。在以下示例中,该对象是DEFAULTS

结果,对象allData是通过创建DEFAULTS的副本并用providedData覆盖其属性来创建的。

但是您不需要对象来指定默认值,您也可以单独在对象字面值中指定它们:

25.3.3。用例传播:非破坏性变化的属性

到目前为止,我们遇到了一种更改对象属性的方法:我们 设置 并改变对象。也就是说,这种改变属性的方式是 破坏性

通过传播,您可以非破坏性地更改属性:您可以复制属性具有不同值的对象。

例如,此代码非破坏性地更新属性.foo

25.单个对象 - 图4 练习:通过传播(固定密钥)非破坏性更新属性

exercises/single-objects/update_name_test.js

25.4。方法

25.4.1。方法是值为函数的属性

让我们重温一下用于介绍方法的示例:

有些令人惊讶的是,方法是函数:

这是为什么?请记住,在关于可调用实体的章节中,我们了解到普通函数扮演了几个角色。 方法 是其中一个角色。因此,在引擎盖下,jane大致如下所示。

25.4.2。 .call():显式参数this

请记住,每个函数someFunc也是一个对象,因此有方法。一种这样的方法是.call() - 它允许您在明确指定this时调用函数:

25.4.2.1。方法和.call()

如果进行方法调用,this始终是隐式参数:

顺便说一句,这意味着实际上有两个不同的点运算符:

  1. 一个用于访问属性:obj.prop
  2. 一个用于进行方法调用:obj.prop()

它们的不同之处在于(2)不仅仅是(1),后面是函数调用运算符()。相反,(2)另外指定this的值(如前面的例子所示)。

25.4.2.2。功能和.call()

但是,如果函数调用普通函数,this也是一个隐式参数:

也就是说,在函数调用期间,普通函数具有this,但它被设置为undefined,这表示它在这里没有真正的用途。

接下来,我们将研究使用this的缺陷。在我们能够做到这一点之前,我们还需要一个工具:函数的方法.bind()

25.4.3。 .bind():预填充this和功能参数

.bind()是函数对象的另一种方法。调用此方法如下。

.bind()返回一个新函数boundFunc()。调用该函数调用someFunc()并将this设置为thisValue并且这些参数:arg1arg2arg3,然后是boundFunc()的参数。

也就是说,以下两个函数调用是等效的:

另一种预填this和参数的方法是通过箭头功能:

因此,.bind()可以实现为如下的实际功能:

25.4.3.1。示例:绑定实际函数

.bind()用于实际功能有点不直观,因为你必须为this提供一个值。该值通常是undefined,反映了函数调用期间发生的情况。

在下面的示例中,我们通过将add()的第一个参数绑定到8来创建add8(),这是一个具有一个参数的函数。

25.4.3.2。示例:绑定方法

在下面的代码中,我们将方法.says()转换为独立函数func()

通过.bind()this设置为jane至关重要。否则,func()将无法正常工作,因为在行 A 中使用了this

25.4.4。 this陷阱:提取方法

我们现在对函数和方法有了很多了解,并准备好了解涉及方法和this的最大缺陷:如果你不小心,函数调用从对象中提取的方法可能会失败。

在下面的例子中,当我们提取方法jane.says()时,我们失败,将它存储在变量func和函数调用func()中。

A 行中的函数调用相当于:

那么我们如何解决这个问题呢?我们需要使用.bind()来提取方法.says()

当我们调用func()时,.bind()确保this始终为jane

您还可以使用箭头函数来提取方法:

25.4.4.1。示例:提取方法

以下是您在实际 Web 开发中可能看到的代码的简化版本:

在 A 行中,我们没有正确提取方法.handleClick()。相反,我们应该这样做:

25.单个对象 - 图5 练习:提取方法

exercises/single-objects/method_extraction_exrc.js

25.4.5。 this陷阱:意外遮蔽this

如果使用普通功能,意外遮蔽this只是一个问题。

请考虑以下问题:当您在普通函数内部时,您无法访问周围范围的this,因为普通函数有自己的this。换句话说:内部作用域中的变量将变量隐藏在外部作用域中。这被称为 阴影 。以下代码是一个示例:

为什么错误? B 行中的this不是.sayHiTo()this,它是从 B 行开始的普通函数的this

有几种方法可以解决这个问题。最简单的方法是使用箭头函数 - 它没有自己的this,因此阴影不是问题。

25.4.6。避免this的陷阱

我们已经看到了两个与this相关的重大陷阱:

  1. 提取方法
  2. 意外遮蔽this

一个简单的规则有助于避免第二个陷阱:

“避免关键字function”:绝不使用普通函数,只使用箭头函数(用于实际函数)和方法定义。

让我们打破这个规则:

  • 如果所有实际函数都是箭头函数,则第二个陷阱永远不会发生。
  • 使用方法定义意味着您只能在方法中看到this,这使得此功能不那么混乱。

但是,即使我不使用(普通)函数 表达式 ,我还是在语法上喜欢函数 声明 。如果您没有参考其中的this,您可以安全地使用它们。检查工具 ESLint 有规则,有助于此。

唉,第一个陷阱没有简单的方法:每当你提取一个方法时,你必须小心并正确地做到这一点。例如,通过绑定this

25.4.7。 this在各种情况下的值

this在各种情况下的值是多少?

在可调用实体中,this的值取决于调用可调用实体的方式以及它是什么类型的可调用实体:

  • 功能调用:
    • 普通功能:this === undefined
    • 箭头功能:this与周围范围相同(词汇this
  • 方法调用:this是呼叫接收方
  • newthis是指新创建的实例

您还可以在所有常见的顶级范围中访问this

  • <script>元素:this === window
  • ES 模块:this === undefined
  • CommonJS 模块:this === module.exports

但是,我喜欢假装您无法访问顶级作用域中的this,因为顶级this令人困惑且没有用处。

25.5。对象作为词典

对象最适合作为记录。但在 ES6 之前,JavaScript 没有字典的数据结构(ES6 带来了映射)。因此,必须将对象用作字典。因此,键必须是字符串,但值可以是任意类型。

我们首先看一下与字典相关的对象的特征,但偶尔也可用于对象作为记录。本节最后提供了实际使用对象作为词典的提示(剧透:如果可以,请避免使用映射)。

25.5.1。任意固定字符串作为属性键

从对象作为记录到对象作为字典时,一个重要的变化是我们必须能够使用任意字符串作为属性键。本小节解释了如何实现固定字符串键。下一小节将介绍如何动态计算任意键。

到目前为止,我们只看到合法的 JavaScript 标识符作为属性键(符号除外):

两种技术允许我们使用任意字符串作为属性键。

首先 - 当通过对象字面值创建属性键时,我们可以引用属性键(带单引号或双引号):

第二 - 获取或设置属性时,我们可以使用带有字符串的方括号:

您还可以引用方法的键:

25.5.2。计算属性键

到目前为止,我们受到了对象字面值内部属性键的限制:它们总是固定的,它们总是字符串。如果我们将表达式放在方括号中,我们可以动态计算任意键:

计算键的主要用例是将符号作为属性键(行 A)。

请注意,用于获取和设置属性的方括号运算符适用于任意表达式:

方法也可以有计算属性键:

我们现在切换回固定属性键,但如果需要计算属性键,则可以始终使用方括号。

25.单个对象 - 图6 练习:通过传播非破坏性更新属性(计算密钥)

exercises/single-objects/update_property_test.js

25.5.3。 in运算符:是否存在具有给定键的属性?

in运算符检查对象是否具有给定键的属性:

25.5.3.1。通过真实性检查财产是否存在

您还可以使用真实性检查来确定属性是否存在:

之前的检查有效,因为读取不存在的属性会返回undefined,这是假的。因为obj.foo是真实的。

但是,有一个重要的警告:如果属性存在,则真实性检查失败,但具有假值(undefinednullfalse0""等):

25.5.4。删除属性

您可以通过delete运算符删除属性:

25.5.5。字典陷阱

如果使用普通对象(通过对象字面值创建)作为字典,则必须注意两个陷阱。

第一个缺陷是in运算符还找到了继承的属性:

我们希望dict被视为空,但in运算符会检测它从原型Object.prototype继承的属性。

第二个缺陷是你不能使用属性键__proto__,因为它具有特殊的权力(它设置了对象的原型):

那么我们如何解决这些陷阱呢?

  • 只要你可以,使用映射。它们是词典的最佳解决方案。

  • 如果你不能:将库用于可以安全地完成所有操作的对象字典。

  • 如果你不能:使用没有原型的对象。这消除了现代 JavaScript 中的两个陷阱。

25.单个对象 - 图7 练习:使用对象作为字典

exercises/single-objects/simple_dict_test.js

25.5.6。列出属性键

Table 19: Standard library methods for listing own (non-inherited) property keys. All of them return Arrays with strings and/or symbols.

枚举 没有。 符号
Object.keys()
Object.getOwnPropertyNames()
Object.getOwnPropertySymbols()
Reflect.ownKeys()

tbl 中的每个方法。 19 返回一个带有参数自身属性键的数组。在方法的名称中,您可以看到我们之前讨论过的属性键(字符串和符号),属性名称(仅字符串)和属性符号(仅符号)之间的区别。

可枚举性是属性的 属性 。默认情况下,属性是可枚举的,但有一些方法可以改变它(在下一个示例中显示,稍后将详细描述)。

例如:

25.5.7。通过Object.values()列出属性值

Object.values()列出对象的所有可枚举属性的值:

25.5.8。通过Object.entries()列出属性条目

Object.entries()列出了可枚举属性的键值对。每对编码为一个双元素数组:

25.单个对象 - 图8 练习:Object.entries()

exercises/single-objects/find_key_test.js

25.5.9。确定性地列出属性

对象的自有(非继承)属性始终按以下顺序列出:

  1. 具有整数索引的属性(例如,数组索引)
  • 按升序数字顺序
  1. 带字符串键的剩余属性
  • 按照添加顺序
  1. 带符号键的属性
  • 按照添加顺序

以下示例演示如何根据以下规则对属性键进行排序:

您可以在规格中查找详细信息。

10 年 5 月 25 日。通过Object.fromEntries()组装对象

给定[key,value]对可迭代,Object.fromEntries()创建一个对象:

它与 Object.entries() 相反。

接下来,我们将使用Object.entries()Object.fromEntries()从库 Underscore 中实现多个工具功能。

25.5.10.1。示例:pick(object, ...keys)

pick()object中删除其键不在keys中的所有属性。删除 非破坏性 pick()创建修改后的副本并且不会更改原始文件。例如:

我们可以按如下方式实现pick()

25.5.10.2。示例:invert(object)

invert() 非破坏性地交换键和对象的值:

我们可以像这样实现它:

25.5.10.3。 Object.fromEntries()的简单实现

Object.fromEntries()可以实现如下(我省略了几个检查):

笔记:

25.单个对象 - 图9 练习:Object.entries()Object.fromEntries()

exercises/single-objects/omit_properties_test.js

25.6。标准方法

Object.prototype定义了几个可以覆盖的标准方法。两个重要的是:

  • .toString()
  • .valueOf()

粗略地说,.toString()配置对象如何转换为字符串:

并且.valueOf()配置对象如何转换为数字:

25.7。高级主题

以下小节简要概述了超出本书范围的主题。

25.7.1。 Object.assign()

Object.assign()是一种工具方法:

这个表达式(破坏性地)将source_1合并到target,然后source_2等。最后,它返回target。例如:

Object.assign()的用例类似于传播属性的用例。在某种程度上,它破坏性地传播。

有关Object.assign()的更多信息,请参阅“探索 ES6”

25.7.2。冻结对象

Object.freeze(obj)使obj不可变:您无法更改或添加属性或更改obj的原型。

例如:

有一点需要注意:Object.freeze(obj)浅薄地冻结。也就是说,只冻结obj的属性,而不冻结属性中存储的对象。

有关Object.freeze()的更多信息,请参阅“Speaking JavaScript”

25.7.3。属性属性和属性描述符

就像对象由属性组成一样,属性由 属性 组成。也就是说,您可以配置的不仅仅是属性的值 - 这只是几个属性中的一个。其他属性包括:

  • writable:是否可以更改属性的值?
  • enumerableObject.keys()是否列出了该属性?

当您使用其中一个操作来访问属性属性时,通过 属性描述符 指定属性:每个属性代表一个属性的对象。例如,这是您阅读属性obj.foo的属性的方法:

这就是你设置属性obj.bar的属性的方法:

有关属性属性和属性描述符的更多信息,请参阅“Speaking JavaScript”

25.单个对象 - 图10 测验

参见测验应用程序