- 25.单个对象
25.单个对象
原文: http://exploringjs.com/impatient-js/ch_single-objects.html
在本书中,JavaScript 的面向对象编程(OOP)风格分四步介绍。本章介绍步骤 1,下一章涵盖步骤 2-4。步骤是(图 7 ):
- 单个对象: 对象 ,JavaScript 的基本 OOP 构建块如何独立工作?
- 原型链:每个对象都有一个零个或多个 原型对象链 。原型是 JavaScript 的核心继承机制。
- 类:JavaScript 的 类 是对象的工厂。类及其实例之间的关系基于原型继承。
- 子类化: 子类 与其 超类 之间的关系也基于原型继承。
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:
练习:通过对象字面值创建对象
exercises/single-objects/color_point_object_test.js
25.3。传播到对象字面值(...
)
我们已经看到在函数调用中使用扩展(...
),它将迭代的内容转换为参数。
在对象字面值内部, 扩展属性 将另一个对象的属性添加到当前对象:
如果属性键发生冲突,则上次提到的属性为“wins”:
25.3.1。传播的用例:复制对象
您可以使用 spread 来创建对象的副本original
:
警告 - 复制是 浅 :copy
是一个新对象,带有original
的所有属性(键值对)的副本。但是如果属性值是对象,那么不会复制它们;它们在original
和copy
之间共享。以下代码演示了这意味着什么。
JavaScript 不做深拷贝
对象的深拷贝 (所有级别都被复制)是众所周知的难以做到的。因此,JavaScript 没有内置的操作(暂时)。如果您需要这样的操作,则必须自己实现。
25.3.2。传播的用例:缺失属性的默认值
如果代码的其中一个输入是包含数据的对象,则可以在为其指定默认值时使属性成为可选属性。这样做的一种技术是通过其属性包含默认值的对象。在以下示例中,该对象是DEFAULTS
:
结果,对象allData
是通过创建DEFAULTS
的副本并用providedData
覆盖其属性来创建的。
但是您不需要对象来指定默认值,您也可以单独在对象字面值中指定它们:
25.3.3。用例传播:非破坏性变化的属性
到目前为止,我们遇到了一种更改对象属性的方法:我们 设置 并改变对象。也就是说,这种改变属性的方式是 破坏性
通过传播,您可以非破坏性地更改属性:您可以复制属性具有不同值的对象。
例如,此代码非破坏性地更新属性.foo
:
练习:通过传播(固定密钥)非破坏性更新属性
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
始终是隐式参数:
顺便说一句,这意味着实际上有两个不同的点运算符:
- 一个用于访问属性:
obj.prop
- 一个用于进行方法调用:
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
并且这些参数:arg1
,arg2
,arg3
,然后是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()
。相反,我们应该这样做:
练习:提取方法
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
相关的重大陷阱:
一个简单的规则有助于避免第二个陷阱:
“避免关键字
function
”:绝不使用普通函数,只使用箭头函数(用于实际函数)和方法定义。
让我们打破这个规则:
- 如果所有实际函数都是箭头函数,则第二个陷阱永远不会发生。
- 使用方法定义意味着您只能在方法中看到
this
,这使得此功能不那么混乱。
但是,即使我不使用(普通)函数 表达式 ,我还是在语法上喜欢函数 声明 。如果您没有参考其中的this
,您可以安全地使用它们。检查工具 ESLint 有规则,有助于此。
唉,第一个陷阱没有简单的方法:每当你提取一个方法时,你必须小心并正确地做到这一点。例如,通过绑定this
。
25.4.7。 this
在各种情况下的值
this
在各种情况下的值是多少?
在可调用实体中,this
的值取决于调用可调用实体的方式以及它是什么类型的可调用实体:
- 功能调用:
- 普通功能:
this === undefined
- 箭头功能:
this
与周围范围相同(词汇this
)
- 普通功能:
- 方法调用:
this
是呼叫接收方 new
:this
是指新创建的实例
您还可以在所有常见的顶级范围中访问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)。
请注意,用于获取和设置属性的方括号运算符适用于任意表达式:
方法也可以有计算属性键:
我们现在切换回固定属性键,但如果需要计算属性键,则可以始终使用方括号。
练习:通过传播非破坏性更新属性(计算密钥)
exercises/single-objects/update_property_test.js
25.5.3。 in
运算符:是否存在具有给定键的属性?
in
运算符检查对象是否具有给定键的属性:
25.5.3.1。通过真实性检查财产是否存在
您还可以使用真实性检查来确定属性是否存在:
之前的检查有效,因为读取不存在的属性会返回undefined
,这是假的。因为obj.foo
是真实的。
但是,有一个重要的警告:如果属性存在,则真实性检查失败,但具有假值(undefined
,null
,false
,0
,""
等):
25.5.4。删除属性
您可以通过delete
运算符删除属性:
25.5.5。字典陷阱
如果使用普通对象(通过对象字面值创建)作为字典,则必须注意两个陷阱。
第一个缺陷是in
运算符还找到了继承的属性:
我们希望dict
被视为空,但in
运算符会检测它从原型Object.prototype
继承的属性。
第二个缺陷是你不能使用属性键__proto__
,因为它具有特殊的权力(它设置了对象的原型):
那么我们如何解决这些陷阱呢?
只要你可以,使用映射。它们是词典的最佳解决方案。
如果你不能:将库用于可以安全地完成所有操作的对象字典。
如果你不能:使用没有原型的对象。这消除了现代 JavaScript 中的两个陷阱。
练习:使用对象作为字典
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()
列出了可枚举属性的键值对。每对编码为一个双元素数组:
练习:Object.entries()
exercises/single-objects/find_key_test.js
25.5.9。确定性地列出属性
对象的自有(非继承)属性始终按以下顺序列出:
- 具有整数索引的属性(例如,数组索引)
- 按升序数字顺序
- 带字符串键的剩余属性
- 按照添加顺序
- 带符号键的属性
- 按照添加顺序
以下示例演示如何根据以下规则对属性键进行排序:
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()
可以实现如下(我省略了几个检查):
笔记:
Object.defineProperty()
在本章后面中解释。- 官方 polyfill 可通过 npm 包
object.fromentries
获得。
练习: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
:是否可以更改属性的值?enumerable
:Object.keys()
是否列出了该属性?
当您使用其中一个操作来访问属性属性时,通过 属性描述符 指定属性:每个属性代表一个属性的对象。例如,这是您阅读属性obj.foo
的属性的方法:
这就是你设置属性obj.bar
的属性的方法:
有关属性属性和属性描述符的更多信息,请参阅“Speaking JavaScript”。
测验
参见测验应用程序。