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