...
) [ES2018]this
this
.call()
.bind()
this
陷阱:提取方法this
陷阱:意外遮蔽 this
this
在各种上下文中的值(高级)in
运算符:是否存在具有给定键的属性?Object.values()
列出属性值Object.entries()
列出属性条目 [ES2017]Object.fromEntries()
组装对象 [ES2019].toString()
.valueOf()
Object.assign()
[ES6]在本书中,JavaScript 的面向对象编程 (OOP) 风格分四步介绍。本章涵盖步骤 1;下一章 涵盖步骤 2-4。步骤如下(图 8)
在 JavaScript 中
对象在 JavaScript 中扮演着两个角色
这些角色会影响本章中对对象的解释方式
让我们首先探讨对象的 *记录* 角色。
*对象字面量* 是创建作为记录的对象的一种方式。它们是 JavaScript 的一个突出特性:我们可以直接创建对象——不需要类!这是一个例子
const jane = {
first: 'Jane',
last: 'Doe', // optional trailing comma
; }
在这个例子中,我们通过一个对象字面量创建了一个对象,它以花括号 {}
开始和结束。在它里面,我们定义了两个 *属性*(键值对)
first
,值为 'Jane'
。last
,值为 'Doe'
。从 ES5 开始,对象字面量中允许使用尾随逗号。
稍后我们将看到其他指定属性键的方法,但使用这种指定方法时,它们必须遵循 JavaScript 变量名的规则。例如,我们可以使用 first_name
作为属性键,但不能使用 first-name
)。但是,允许使用保留字
const obj = {
if: true,
const: true,
; }
为了检查各种操作对对象的影响,我们将在本章的这一部分偶尔使用 Object.keys()
。它列出属性键
> Object.keys({a:1, b:2})[ 'a', 'b' ]
每当属性的值是通过变量名定义的,并且该名称与键相同时,我们可以省略该键。
function createPoint(x, y) {
return {x, y};
}.deepEqual(
assertcreatePoint(9, 2),
x: 9, y: 2 }
{ ; )
这就是我们 *获取*(读取)属性的方式(A 行)
const jane = {
first: 'Jane',
last: 'Doe',
;
}
// Get property .first
.equal(jane.first, 'Jane'); // (A) assert
获取未知属性会产生 undefined
.equal(jane.unknownProperty, undefined); assert
这就是我们 *设置*(写入)属性的方式
const obj = {
prop: 1,
;
}.equal(obj.prop, 1);
assert.prop = 2; // (A)
obj.equal(obj.prop, 2); assert
我们刚刚通过设置更改了一个现有属性。如果我们设置一个未知属性,我们会创建一个新的条目
const obj = {}; // empty object
.deepEqual(
assertObject.keys(obj), []);
.unknownProperty = 'abc';
obj.deepEqual(
assertObject.keys(obj), ['unknownProperty']);
以下代码展示了如何通过对象字面量创建方法 .says()
const jane = {
first: 'Jane', // data property
says(text) { // method
return `${this.first} says “${text}”`; // (A)
, // comma as separator (optional at end)
};
}.equal(jane.says('hello'), 'Jane says “hello”'); assert
在方法调用 jane.says('hello')
期间,jane
被称为方法调用的 *接收者*,并被分配给特殊变量 this
(有关 this
的更多信息,请参阅 §28.4 “方法和特殊变量 this
”)。这使得方法 .says()
能够访问 A 行中的兄弟属性 .first
。
JavaScript 中有两种访问器
获取器是通过在方法定义前加上修饰符 get
来创建的
const jane = {
first: 'Jane',
last: 'Doe',
full() {
get return `${this.first} ${this.last}`;
,
};
}
.equal(jane.full, 'Jane Doe');
assert.first = 'John';
jane.equal(jane.full, 'John Doe'); assert
设置器是通过在方法定义前加上修饰符 set
来创建的
const jane = {
first: 'Jane',
last: 'Doe',
full(fullName) {
set const parts = fullName.split(' ');
this.first = parts[0];
this.last = parts[1];
,
};
}
.full = 'Richard Roe';
jane.equal(jane.first, 'Richard');
assert.equal(jane.last, 'Roe'); assert
**练习:通过对象字面量创建对象**
exercises/single-objects/color_point_object_test.mjs
...
) [ES2018]在函数调用中,展开 (...
) 将 *可迭代对象* 的迭代值转换为参数。
在对象字面量中,*展开属性* 将另一个对象的属性添加到当前对象中
> const obj = {foo: 1, bar: 2};
> {...obj, baz: 3}{ foo: 1, bar: 2, baz: 3 }
如果属性键发生冲突,则最后提到的属性“获胜”
> const obj = {foo: 1, bar: 2, baz: 3};
> {...obj, foo: true}{ foo: true, bar: 2, baz: 3 }
> {foo: true, ...obj}{ foo: 1, bar: 2, baz: 3 }
所有值都是可展开的,甚至是 undefined
和 null
> {...undefined}{}
> {...null}{}
> {...123}{}
> {...'abc'}{ '0': 'a', '1': 'b', '2': 'c' }
> {...['a', 'b']}{ '0': 'a', '1': 'b' }
字符串和数组的属性 .length
在这种操作中是隐藏的(它不可 *枚举*;有关更多信息,请参阅 §28.8.3 “属性特性和属性描述符 [ES5]”)。
我们可以使用展开来创建对象 original
的副本
const copy = {...original};
注意——复制是 *浅* 的:copy
是一个新对象,其中包含 original
的所有属性(键值对)的副本。但是,如果属性值是对象,则不会复制这些对象本身;它们在 original
和 copy
之间共享。让我们看一个例子
const original = { a: 1, b: {foo: true} };
const copy = {...original};
copy
的第一层确实是一个副本:如果我们更改该层上的任何属性,它不会影响原始对象
.a = 2;
copy.deepEqual(
assert, { a: 1, b: {foo: true} }); // no change original
但是,不会复制更深的层。例如,.b
的值在原始对象和副本之间共享。在副本中更改 .b
也会在原始对象中更改它。
.b.foo = false;
copy.deepEqual(
assert, { a: 1, b: {foo: false} }); original
**JavaScript 不支持内置的深度复制**
*深度复制* 对象(复制所有层)通常很难通用地完成。因此,JavaScript 没有针对它们的内置操作(目前)。如果我们需要这样的操作,我们必须自己实现它。
如果我们代码的一个输入是一个包含数据的对象,我们可以通过指定在缺少这些属性时使用的默认值来使属性可选。一种实现此目的的技术是通过一个对象,其属性包含默认值。在以下示例中,该对象是 DEFAULTS
const DEFAULTS = {foo: 'a', bar: 'b'};
const providedData = {foo: 1};
const allData = {...DEFAULTS, ...providedData};
.deepEqual(allData, {foo: 1, bar: 'b'}); assert
结果对象 allData
是通过复制 DEFAULTS
并使用 providedData
的属性覆盖其属性来创建的。
但我们不需要一个对象来指定默认值;我们也可以在对象字面量中单独指定它们
const providedData = {foo: 1};
const allData = {foo: 'a', bar: 'b', ...providedData};
.deepEqual(allData, {foo: 1, bar: 'b'}); assert
到目前为止,我们已经遇到了一种更改对象的属性 .foo
的方法:我们 *设置* 它(A 行)并改变对象。也就是说,这种更改属性的方式是 *破坏性* 的。
const obj = {foo: 'a', bar: 'b'};
.foo = 1; // (A)
obj.deepEqual(obj, {foo: 1, bar: 'b'}); assert
通过展开,我们可以 *非破坏性* 地更改 .foo
——我们创建一个 obj
的副本,其中 .foo
具有不同的值
const obj = {foo: 'a', bar: 'b'};
const updatedObj = {...obj, foo: 1};
.deepEqual(updatedObj, {foo: 1, bar: 'b'}); assert
**练习:通过展开非破坏性地更新属性(固定键)**
exercises/single-objects/update_name_test.mjs
this
让我们回顾一下用于介绍方法的示例
const jane = {
first: 'Jane',
says(text) {
return `${this.first} says “${text}”`;
,
}; }
有点令人惊讶的是,方法是函数
.equal(typeof jane.says, 'function'); assert
为什么呢?我们在 关于可调用值的章节 中了解到,普通函数扮演着多种角色。*方法* 就是其中一种角色。因此,在底层,jane
大致如下所示。
const jane = {
first: 'Jane',
says: function (text) {
return `${this.first} says “${text}”`;
,
}; }
this
考虑以下代码
const obj = {
someMethod(x, y) {
.equal(this, obj); // (A)
assert.equal(x, 'a');
assert.equal(y, 'b');
assert
};
}.someMethod('a', 'b'); // (B) obj
在 B 行中,obj
是方法调用的 *接收者*。它通过一个名为 this
的隐式(隐藏)参数传递给存储在 obj.someMethod
中的函数(A 行)。
这是一个重点:理解 this
最佳的方式是将其视为普通函数(因此也是方法)的隐式参数。
.call()
方法是函数,在 §25.7 “函数的方法: .call()
、.apply()
、.bind()
” 中,我们看到函数本身也有方法。其中一个方法是 .call()
。让我们看一个例子来理解这个方法是如何工作的。
在上一节中,有这样一个方法调用
.someMethod('a', 'b') obj
此调用等效于
.someMethod.call(obj, 'a', 'b'); obj
也等效于
const func = obj.someMethod;
.call(obj, 'a', 'b'); func
.call()
使通常隐式的参数 this
显式化:当通过 .call()
调用函数时,第一个参数是 this
,后面跟着常规的(显式)函数参数。
顺便说一句,这意味着实际上有两个不同的点运算符
obj.prop
obj.prop()
它们的不同之处在于 (2) 不仅仅是 (1) 后面跟着函数调用运算符 ()
。相反,(2) 还提供了一个 this
的值。
.bind()
.bind()
是函数对象的另一个方法。在下面的代码中,我们使用 .bind()
将方法 .says()
转换为独立函数 func()
const jane = {
first: 'Jane',
says(text) {
return `${this.first} says “${text}”`; // (A)
,
};
}
const func = jane.says.bind(jane, 'hello');
.equal(func(), 'Jane says “hello”'); assert
通过 .bind()
将 this
设置为 jane
在这里至关重要。否则,func()
将无法正常工作,因为在 A 行中使用了 this
。在下一节中,我们将探讨原因。
this
陷阱:提取方法我们现在对函数和方法有了相当多的了解,可以看看涉及方法和 this
的最大陷阱:如果我们不小心,从对象中提取方法并进行函数调用可能会失败。
在下面的例子中,当我们提取方法 jane.says()
,将其存储在变量 func
中,然后函数调用 func()
时,就会失败。
const jane = {
first: 'Jane',
says(text) {
return `${this.first} says “${text}”`;
,
};
}const func = jane.says; // extract the method
.throws(
assert=> func('hello'), // (A)
()
{name: 'TypeError',
message: "Cannot read property 'first' of undefined",
; })
在 A 行中,我们正在进行一个普通的函数调用。在普通的函数调用中,this
是 undefined
(如果 严格模式 处于活动状态,几乎总是如此)。因此,A 行等效于
.throws(
assert=> jane.says.call(undefined, 'hello'), // `this` is undefined!
()
{name: 'TypeError',
message: "Cannot read property 'first' of undefined",
; })
我们如何解决这个问题?我们需要使用 .bind()
来提取方法 .says()
const func2 = jane.says.bind(jane);
.equal(func2('hello'), 'Jane says “hello”'); assert
.bind()
确保在我们调用 func()
时,this
始终是 jane
。
我们也可以使用箭头函数来提取方法
const func3 = text => jane.says(text);
.equal(func3('hello'), 'Jane says “hello”'); assert
以下是我们在实际 Web 开发中可能会看到的代码的简化版本
class ClickHandler {
constructor(id, elem) {
this.id = id;
.addEventListener('click', this.handleClick); // (A)
elem
}handleClick(event) {
alert('Clicked ' + this.id);
} }
在 A 行中,我们没有正确地提取方法 .handleClick()
。相反,我们应该这样做
.addEventListener('click', this.handleClick.bind(this)); elem
唉,没有简单的方法可以绕过提取方法的陷阱:每当我们提取方法时,我们都必须小心谨慎地正确操作——例如,通过绑定 this
或使用箭头函数。
练习:提取方法
exercises/single-objects/method_extraction_exrc.mjs
this
陷阱:意外遮蔽 this
意外遮蔽 this
只是普通函数的问题
箭头函数不会遮蔽 this
。
考虑以下问题:当我们在普通函数内部时,我们无法访问外部作用域的 this
,因为普通函数有自己的 this
。换句话说,内部作用域中的变量会隐藏外部作用域中的变量。这被称为 遮蔽。下面的代码是一个例子
const prefixer = {
prefix: '==> ',
prefixStringArray(stringArray) {
return stringArray.map(
function (x) {
return this.prefix + x; // (A)
;
}),
};
}.throws(
assert=> prefixer.prefixStringArray(['a', 'b']),
() /^TypeError: Cannot read property 'prefix' of undefined$/);
在 A 行中,我们想访问 .prefixStringArray()
的 this
。但我们不能,因为外部的普通函数有自己的 this
,它遮蔽(阻止访问)了方法的 this
。由于回调函数被函数调用,前一个 this
的值是 undefined
。这就解释了错误信息。
解决这个问题最简单的方法是使用箭头函数,它没有自己的 this
,因此不会遮蔽任何东西
const prefixer = {
prefix: '==> ',
prefixStringArray(stringArray) {
return stringArray.map(
=> {
(x) return this.prefix + x;
;
}),
};
}.deepEqual(
assert.prefixStringArray(['a', 'b']),
prefixer'==> a', '==> b']); [
我们也可以将 this
存储在另一个变量中(A 行),这样它就不会被遮蔽
prefixStringArray(stringArray) {
const that = this; // (A)
return stringArray.map(
function (x) {
return that.prefix + x;
;
}), }
另一种选择是通过 .bind()
为回调函数指定一个固定的 this
(A 行)
prefixStringArray(stringArray) {
return stringArray.map(
function (x) {
return this.prefix + x;
.bind(this)); // (A)
}, }
最后,.map()
允许我们为 this
指定一个值(A 行),它在调用回调函数时使用该值
prefixStringArray(stringArray) {
return stringArray.map(
function (x) {
return this.prefix + x;
,
}this); // (A)
, }
this
的陷阱如果您遵循 §25.3.4 “建议:优先使用专用函数而不是普通函数” 中的建议,则可以避免意外遮蔽 this
的陷阱。以下是总结
使用箭头函数作为匿名内联函数。它们没有 this
作为隐式参数,也不会遮蔽它。
对于命名的独立函数声明,您可以使用箭头函数或函数声明。如果您使用后者,请确保它们的函数体中没有提到 this
。
this
在各种上下文中的值(高级)this
在各种上下文中的值是什么?
在可调用实体内部,this
的值取决于如何调用可调用实体以及它是什么类型的可调用实体
this === undefined
(在 严格模式 下)this
与外部作用域中的相同(词法 this
)this
是调用的接收者new
:this
指的是新创建的实例我们还可以在所有常见的顶级作用域中访问 this
<script>
元素:this === globalThis
this === undefined
this === module.exports
提示:假装顶级作用域中不存在 this
我喜欢这样做,因为顶级 this
令人困惑,而且很少有用。
存在以下几种可选链操作
?.prop // optional static property access
obj?.[«expr»] // optional dynamic property access
obj?.(«arg0», «arg1») // optional function or method call func
大致思路是
undefined
也不是 null
,则执行问号后的操作。undefined
。考虑以下数据
const persons = [
{surname: 'Zoe',
address: {
street: {
name: 'Sesame Street',
number: '123',
,
},
},
}
{surname: 'Mariner',
,
}
{surname: 'Carmen',
address: {
,
},
}; ]
我们可以使用可选链来安全地提取街道名称
const streetNames = persons.map(
=> p.address?.street?.name);
p .deepEqual(
assert, ['Sesame Street', undefined, undefined]
streetNames; )
空值合并运算符 允许我们使用默认值 '(无街道)'
而不是 undefined
const streetNames = persons.map(
=> p.address?.street?.name ?? '(no name)');
p .deepEqual(
assert, ['Sesame Street', '(no name)', '(no name)']
streetNames; )
以下两个表达式是等效的
?.prop
o!== undefined && o !== null) ? o.prop : undefined (o
示例
.equal(undefined?.prop, undefined);
assert.equal(null?.prop, undefined);
assert.equal({prop:1}?.prop, 1); assert
以下两个表达式是等效的
?.[«expr»]
o!== undefined && o !== null) ? o[«expr»] : undefined (o
示例
const key = 'prop';
.equal(undefined?.[key], undefined);
assert.equal(null?.[key], undefined);
assert.equal({prop:1}?.[key], 1); assert
以下两个表达式是等效的
?.(arg0, arg1)
f!== undefined && f !== null) ? f(arg0, arg1) : undefined (f
示例
.equal(undefined?.(123), undefined);
assert.equal(null?.(123), undefined);
assert.equal(String?.(123), '123'); assert
请注意,如果此运算符的左侧不可调用,则会产生错误
.throws(
assert=> true?.(123),
() TypeError);
为什么?其理念是该运算符只容忍故意的省略。不可调用的值(undefined
和 null
除外)很可能是一个错误,应该报告,而不是绕过。
在属性访问和函数/方法调用链中,一旦第一个可选运算符在其左侧遇到 undefined
或 null
,求值就会停止
function isInvoked(obj) {
let invoked = false;
?.a.b.m(invoked = true);
objreturn invoked;
}
.equal(
assertisInvoked({a: {b: {m() {}}}}), true);
// The left-hand side of ?. is undefined
// and the assignment is not executed
.equal(
assertisInvoked(undefined), false);
此行为不同于普通运算符/函数,在普通运算符/函数中,JavaScript 始终在求值运算符/函数之前求值所有操作数/参数。这被称为短路求值。其他短路求值运算符:
a && b
a || b
c ? t : e
o?.[x]
和 f?.()
中有点?以下两个可选运算符的语法并不理想
?.[«expr»] // better: obj?[«expr»]
obj?.(«arg0», «arg1») // better: func?(«arg0», «arg1») func
唉,必须使用不太优雅的语法,因为将理想语法(第一个表达式)与条件运算符(第二个表达式)区分开来太复杂了
?['a', 'b', 'c'].map(x => x+x)
obj? ['a', 'b', 'c'].map(x => x+x) : [] obj
null?.prop
的结果是 undefined
而不是 null
?运算符 ?.
主要与其右侧有关:属性 .prop
是否存在?如果不存在,则提前停止。因此,保留有关其左侧的信息很少有用。但是,只有一个“提前终止”值确实可以简化事情。
对象最适合作为记录使用。但在 ES6 之前,JavaScript 没有用于字典的数据结构(ES6 带来了 Map)。因此,对象必须用作字典,这带来了一个很大的限制:键必须是字符串(符号也是在 ES6 中引入的)。
我们首先来看一下与字典相关的对象特性,这些特性对作为记录的对象也很有用。本节最后将介绍实际使用对象作为字典的技巧(剧透:如果可以的话,请使用 Map)。
到目前为止,我们一直将对象用作记录。属性键是必须是有效标识符的固定标记,并且在内部变成了字符串
const obj = {
mustBeAnIdentifier: 123,
;
}
// Get property
.equal(obj.mustBeAnIdentifier, 123);
assert
// Set property
.mustBeAnIdentifier = 'abc';
obj.equal(obj.mustBeAnIdentifier, 'abc'); assert
下一步,我们将超越属性键的这一限制:在本节中,我们将使用任意固定字符串作为键。在 下一小节 中,我们将动态计算键。
有两种技术允许我们使用任意字符串作为属性键。
首先,当通过对象字面量创建属性键时,我们可以用引号(单引号或双引号)将属性键括起来
const obj = {
'Can be any string!': 123,
; }
其次,当获取或设置属性时,我们可以使用方括号,并在其中包含字符串
// Get property
.equal(obj['Can be any string!'], 123);
assert
// Set property
'Can be any string!'] = 'abc';
obj[.equal(obj['Can be any string!'], 'abc'); assert
我们也可以对方法使用这些技术
const obj = {
'A nice method'() {
return 'Yes!';
,
};
}
.equal(obj['A nice method'](), 'Yes!'); assert
到目前为止,属性键始终是对象字面量中的固定字符串。在本节中,我们将学习如何动态计算属性键。这使我们能够使用任意字符串或符号。
对象字面量中动态计算的属性键的语法灵感来自动态访问属性。也就是说,我们可以使用方括号将表达式括起来
const obj = {
'Hello world!']: true,
['f'+'o'+'o']: 123,
[Symbol.toStringTag]: 'Goodbye', // (A)
[;
}
.equal(obj['Hello world!'], true);
assert.equal(obj.foo, 123);
assert.equal(obj[Symbol.toStringTag], 'Goodbye'); assert
计算键的主要用例是将符号作为属性键(A 行)。
请注意,用于获取和设置属性的方括号运算符适用于任意表达式
.equal(obj['f'+'o'+'o'], 123);
assert.equal(obj['==> foo'.slice(-3)], 123); assert
方法也可以具有计算的属性键
const methodKey = Symbol();
const obj = {
[methodKey]() {return 'Yes!';
,
};
}
.equal(obj[methodKey](), 'Yes!'); assert
在本章的其余部分,我们将主要再次使用固定的属性键(因为它们的语法更方便)。但所有特性也适用于任意字符串和符号。
练习:通过展开运算符非破坏性地更新属性(计算键)
exercises/single-objects/update_property_test.mjs
in
运算符:是否存在具有给定键的属性?in
运算符检查对象是否具有具有给定键的属性
const obj = {
foo: 'abc',
bar: false,
;
}
.equal('foo' in obj, true);
assert.equal('unknownKey' in obj, false); assert
我们还可以使用真值检查来确定属性是否存在
.equal(
assert.foo ? 'exists' : 'does not exist',
obj'exists');
.equal(
assert.unknownKey ? 'exists' : 'does not exist',
obj'does not exist');
前面的检查之所以有效,是因为 obj.foo
是真值,并且因为读取缺少的属性会返回 undefined
(它是假值)。
但是,有一个重要的注意事项:如果属性存在但其值为假值(undefined
、null
、false
、0
、""
等),则真值检查会失败
.equal(
assert.bar ? 'exists' : 'does not exist',
obj'does not exist'); // should be: 'exists'
我们可以使用 delete
运算符删除属性
const obj = {
foo: 123,
;
}.deepEqual(Object.keys(obj), ['foo']);
assert
delete obj.foo;
.deepEqual(Object.keys(obj), []); assert
可枚举 | 不可枚举 | 字符串 | 符号 | |
---|---|---|---|---|
Object.keys() |
✔ |
✔ |
||
Object.getOwnPropertyNames() |
✔ |
✔ |
✔ |
|
Object.getOwnPropertySymbols() |
✔ |
✔ |
✔ |
|
Reflect.ownKeys() |
✔ |
✔ |
✔ |
✔ |
表 19 中的每个方法都返回一个数组,其中包含参数的自有属性键。在方法的名称中,我们可以看到做出了以下区分
下一节将介绍术语可枚举,并演示每个方法。
可枚举性是属性的特性。不可枚举的属性会被某些操作忽略,例如,会被 Object.keys()
(参见表 19)和展开属性忽略。默认情况下,大多数属性都是可枚举的。下面的示例展示了如何更改它。它还演示了列出属性键的各种方法。
const enumerableSymbolKey = Symbol('enumerableSymbolKey');
const nonEnumSymbolKey = Symbol('nonEnumSymbolKey');
// We create enumerable properties via an object literal
const obj = {
enumerableStringKey: 1,
: 2,
[enumerableSymbolKey]
}
// For non-enumerable properties, we need a more powerful tool
Object.defineProperties(obj, {
nonEnumStringKey: {
value: 3,
enumerable: false,
,
}: {
[nonEnumSymbolKey]value: 4,
enumerable: false,
,
};
})
.deepEqual(
assertObject.keys(obj),
'enumerableStringKey' ]);
[ .deepEqual(
assertObject.getOwnPropertyNames(obj),
'enumerableStringKey', 'nonEnumStringKey' ]);
[ .deepEqual(
assertObject.getOwnPropertySymbols(obj),
, nonEnumSymbolKey ]);
[ enumerableSymbolKey.deepEqual(
assertReflect.ownKeys(obj),
['enumerableStringKey', 'nonEnumStringKey',
, nonEnumSymbolKey,
enumerableSymbolKey; ])
Object.defineProperties()
将在本章稍后解释。
Object.values()
列出属性值Object.values()
列出对象所有可枚举属性的值
const obj = {foo: 1, bar: 2};
.deepEqual(
assertObject.values(obj),
1, 2]); [
Object.entries()
列出属性条目 [ES2017]Object.entries()
列出可枚举属性的键值对。每对都编码为一个包含两个元素的数组
const obj = {foo: 1, bar: 2};
.deepEqual(
assertObject.entries(obj),
['foo', 1],
['bar', 2],
[; ])
练习:Object.entries()
exercises/single-objects/find_key_test.mjs
对象的自身(非继承)属性始终按以下顺序列出
以下示例演示了如何根据这些规则对属性键进行排序
> Object.keys({b:0,a:0, 10:0,2:0})[ '2', '10', 'b', 'a' ]
属性的顺序
ECMAScript 规范更详细地描述了属性的排序方式。
Object.fromEntries()
组装对象 [ES2019]给定一个 [键,值] 对的可迭代对象,Object.fromEntries()
会创建一个对象
.deepEqual(
assertObject.fromEntries([['foo',1], ['bar',2]]),
{foo: 1,
bar: 2,
}; )
Object.fromEntries()
与 Object.entries()
相反。
为了演示两者,我们将在接下来的小节中使用它们来实现库 Underscore 中的两个工具函数。
pick(object, ...keys)
pick
返回 object
的副本,该副本仅包含那些键在参数中提到的属性
const address = {
street: 'Evergreen Terrace',
number: '742',
city: 'Springfield',
state: 'NT',
zip: '49007',
;
}.deepEqual(
assertpick(address, 'street', 'number'),
{street: 'Evergreen Terrace',
number: '742',
}; )
我们可以按如下方式实现 pick()
function pick(object, ...keys) {
const filteredEntries = Object.entries(object)
.filter(([key, _value]) => keys.includes(key));
return Object.fromEntries(filteredEntries);
}
invert(object)
invert
返回 object
的副本,其中所有属性的键和值都已交换
.deepEqual(
assertinvert({a: 1, b: 2, c: 3}),
1: 'a', 2: 'b', 3: 'c'}
{; )
我们可以像这样实现 invert()
function invert(object) {
const reversedEntries = Object.entries(object)
.map(([key, value]) => [value, key]);
return Object.fromEntries(reversedEntries);
}
Object.fromEntries()
的简单实现以下函数是 Object.fromEntries()
的简化版本
function fromEntries(iterable) {
const result = {};
for (const [key, value] of iterable) {
let coercedKey;
if (typeof key === 'string' || typeof key === 'symbol') {
= key;
coercedKey else {
} = String(key);
coercedKey
}= value;
result[coercedKey]
}return result;
}
练习:Object.entries()
和 Object.fromEntries()
exercises/single-objects/omit_properties_test.mjs
如果我们使用普通对象(通过对象字面量创建)作为字典,我们必须注意两个陷阱。
第一个陷阱是 in
运算符也会查找继承的属性
const dict = {};
.equal('toString' in dict, true); assert
我们希望将 dict
视为空的,但 in
运算符会检测它从其原型 Object.prototype
继承的属性。
第二个陷阱是我们不能使用属性键 __proto__
,因为它具有特殊的功能(它设置对象的原型)
const dict = {};
'__proto__'] = 123;
dict[// No property was added to dict:
.deepEqual(Object.keys(dict), []); assert
那么如何避免这两个陷阱呢?
以下代码演示了使用没有原型的对象作为字典
const dict = Object.create(null); // no prototype
.equal('toString' in dict, false); // (A)
assert
'__proto__'] = 123;
dict[.deepEqual(Object.keys(dict), ['__proto__']); assert
我们避免了这两个陷阱
__proto__
是通过 Object.prototype
实现的。这意味着如果 Object.prototype
不在原型链中,它就会被关闭。练习:使用对象作为字典
exercises/single-objects/simple_dict_test.mjs
Object.prototype
定义了几个标准方法,可以通过覆盖这些方法来配置语言如何处理对象。其中两个重要的是
.toString()
.valueOf()
.toString()
.toString()
确定如何将对象转换为字符串
> String({toString() { return 'Hello!' }})'Hello!'
> String({})'[object Object]'
.valueOf()
.valueOf()
确定如何将对象转换为数字
> Number({valueOf() { return 123 }})123
> Number({})NaN
以下小节简要概述了一些高级主题。
Object.assign()
[ES6]Object.assign()
是一个工具方法
Object.assign(target, source_1, source_2, ···)
此表达式将 source_1
的所有属性分配给 target
,然后分配 source_2
的所有属性,依此类推。最后,它返回 target
,例如
const target = { foo: 1 };
const result = Object.assign(
,
targetbar: 2},
{baz: 3, bar: 4});
{
.deepEqual(
assert, { foo: 1, bar: 4, baz: 3 });
result// target was modified and returned:
.equal(result, target); assert
Object.assign()
的用例与展开属性的用例类似。在某种程度上,它是破坏性地展开。
Object.freeze(obj)
使 obj
完全不可变:我们无法更改属性、添加属性或更改其原型,例如
const frozen = Object.freeze({ x: 2, y: 5 });
.throws(
assert=> { frozen.x = 7 },
()
{name: 'TypeError',
message: /^Cannot assign to read only property 'x'/,
; })
有一点需要注意:Object.freeze(obj)
是浅层冻结。也就是说,只冻结 obj
的属性,而不冻结存储在属性中的对象。
更多信息
有关冻结和其他锁定对象方法的更多信息,请参阅 深入理解 JavaScript。
正如对象由属性组成一样,属性也由特性组成。属性的值只是众多特性之一。其他特性包括
writable
:是否可以更改属性的值?enumerable
:Object.keys()
、展开等是否会考虑该属性?当我们使用其中一种操作来处理属性特性时,特性是通过属性描述符指定的:对象,其中每个属性表示一个特性。例如,这就是我们读取属性 obj.foo
的特性的方式
const obj = { foo: 123 };
.deepEqual(
assertObject.getOwnPropertyDescriptor(obj, 'foo'),
{value: 123,
writable: true,
enumerable: true,
configurable: true,
; })
这就是我们设置属性 obj.bar
的特性的方式
const obj = {
foo: 1,
bar: 2,
;
}
.deepEqual(Object.keys(obj), ['foo', 'bar']);
assert
// Hide property `bar` from Object.keys()
Object.defineProperty(obj, 'bar', {
enumerable: false,
;
})
.deepEqual(Object.keys(obj), ['foo']); assert
延伸阅读
测验
请参阅 测验应用程序。