类、实例、属性、方法
在本书中,JavaScript 的面向对象编程(OOP)风格分四步介绍。本章介绍步骤 1, 下一章 涵盖步骤 2-4。步骤是(图 7 ):
- 单个对象: 对象,JavaScript 的基本 OOP 构建块如何独立工作?
- 原型链:每个对象都有一个零个或多个 原型对象链。原型是 JavaScript 的核心继承机制。
- 类:JavaScript 的 类 是对象的工厂。类及其实例之间的关系基于原型继承。
- 子类化:子类 与其 超类 之间的关系也基于原型继承。
Figure 7: This book introduces object-oriented programming in JavaScript in four steps.
23.1 JavaScript 中对象的两个角色
在 JavaScript 中,对象是一组称为 属性 的键值条目。对象在 JavaScript 中扮演两个角色:
- 记录:作为记录的对象具有固定数量的属性,其密钥在开发时是已知的。它们的值可以有不同的类型。本章首先介绍了这种使用对象的方法。
- 字典:Objects-as-dictionaries 具有可变数量的属性,其密钥在开发时是未知的。它们的所有值都具有相同的类型。通常最好将映射用作词典而不是对象(本章稍后将对此进行介绍)。
23.2 对象作为记录
23.2.1 对象字面值:属性
作为记录的对象是通过所谓的 对象字面值 创建的。对象字面值是 JavaScript 的一个突出特点:它们允许您直接创建对象。不需要上课!这是一个例子:
const jane = {
first: 'Jane',
last: 'Doe', // optional trailing comma
};
在这个例子中,我们通过一个对象字面值创建了一个对象,它以花括号{}
开头和结尾。在其中,我们定义了两个 属性(键值条目):
- 第一个属性具有键
first
和值'Jane'
。 - 第二个属性具有键
last
和值'Doe'
。
如果以这种方式编写它们,则属性键必须遵循 JavaScript 变量名称的规则,但允许使用保留字。访问属性如下:
assert.equal(jane.first, 'Jane'); // get property .first
jane.first = 'John'; // set property .first
assert.equal(jane.first, 'John');
23.2.2 对象字面值:属性值缩写
每当通过变量名定义属性的值并且该名称与键相同时,您可以省略该键。
23.2.3 术语:属性键,属性名称,属性符号
鉴于属性键可以是字符串和符号,因此进行以下区分:
const x = 4;
const y = 1;
assert.deepEqual(
{ x, y },
{ x: x, y: y }
);
- 如果属性键是字符串,则它也称为 属性名称。
- 如果属性键是符号,则它也称为 属性符号。
此术语用于 JavaScript 标准库(“自己”表示“未继承”,将在下一章中介绍):
Object.keys(obj)
:返回obj
的所有属性键Object.getOwnPropertyNames(obj)
Object.getOwnPropertySymbols(obj)
23.2.4 获得属性
这就是你 得到(读)属性的方式:
obj.propKey
如果obj
没有其键为propKey
的属性,则此表达式求值为undefined
:
const obj = {};
assert.equal(obj.propKey, undefined);
23.2.5 设置属性
这就是你 设置 (写入)属性的方式:
obj.propKey = value;
如果obj
已经有一个键为propKey
的属性,则此语句将更改该属性。否则,它会创建一个新属性:
const obj = {};
assert.deepEqual(
Object.keys(obj), []);
obj.propKey = 123;
assert.deepEqual(
Object.keys(obj), ['propKey']);
23.2.6 对象字面值:方法
以下代码显示如何通过对象字面值创建方法.describe()
:
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
。这使方法.says()
能够访问 A 行中的兄弟属性.first
。
23.2.7 对象字面值:访问者
JavaScript 中有两种访问器:
- getter 是 调用 (读取)属性调用的方法。
- setter 是由 设置 (写入)属性调用的方法。
23.2.7.1 getter
通过在方法定义前添加关键字get
来创建 getter:
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');
23.2.7.2 setter
通过在方法定义前添加关键字set
来创建 setter:
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');
23.3 传播到对象字面值(...
)
我们已经看到在函数调用中使用扩展(...
),它将迭代的内容转换为参数。在对象字面值内部,扩展属性 将另一个对象的属性添加到当前对象:
> const obj = {foo: 1, bar: 2};
> {...obj, baz: 3}
{ foo: 1, bar: 2, baz: 3 }
如果属性键发生冲突,则上次提到的属性为“wins”:
> 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 }
23.3.1 传播的用例:复制对象
您可以使用 spread 来创建对象的副本original
:
const copy = {...original};
警告 - 复制是 浅 :copy
是一个新对象,带有original
的所有属性(键值对)的副本。但是如果属性值是对象,那么不会复制它们;它们在original
和copy
之间共享。以下代码演示了这意味着什么。
const original = { a: 1, b: {foo: true} };
const copy = {...original};
// The first level is a true copy:
assert.deepEqual(
copy, { a: 1, b: {foo: true} });
original.a = 2;
assert.deepEqual(
copy, { a: 1, b: {foo: true} }); // no change
// Deeper levels are not copied:
original.b.foo = false;
// The value of property `b` is shared
// between original and copy.
assert.deepEqual(
copy, { a: 1, b: {foo: false} });
JavaScript 不做深拷贝:对象的深拷贝(所有级别都被复制)是众所周知的难以做到的。因此,JavaScript 没有内置的操作(暂时)。如果您需要这样的操作,则必须自己实现。
23.3.2 传播的用例:缺失属性的默认值
如果代码的其中一个输入是包含数据的对象,则可以在为其指定默认值时使属性成为可选属性。这样做的一种技术是通过其属性包含默认值的对象。在以下示例中,该对象是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'});
23.3.3 用例传播:非破坏性变化的属性
到目前为止,我们遇到了一种更改对象属性的方法:我们 设置 并改变对象。也就是说,这种改变属性的方式是 破坏性
通过传播,您可以非破坏性地更改属性:您可以复制属性具有不同值的对象。例如,此代码非破坏性地更新属性.foo
:
const obj = {foo: 'a', bar: 'b'};
const updatedObj = {...obj, foo: 1};
assert.deepEqual(updatedObj, {foo: 1, bar: 'b'});
23.4 方法
23.4.1 方法是值为函数的属性
让我们重温一下用于介绍方法的示例:
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}”`;
},
};
23.4.2 .call()
:显式参数this
请记住,每个函数someFunc
也是一个对象,因此有方法。一种这样的方法是.call()
- 它允许您在明确指定this
时调用函数:
someFunc.call(thisValue, arg1, arg2, arg3);
23.4.2.1 方法和.call()
如果进行方法调用,this
始终是隐式参数:
const obj = {
method(x) {
assert.equal(this, obj); // implicit parameter
assert.equal(x, 'a');
},
};
obj.method('a');
// Equivalent:
obj.method.call(obj, 'a');
顺便说一句,这意味着实际上有两个不同的点运算符:
- 一个用于访问属性:
obj.prop
- 一个用于进行方法调用:
obj.prop()
它们的不同之处在于(2)不仅仅是(1),后面是函数调用运算符()
。相反,(2)另外指定this
的值(如前面的例子所示)。
23.4.2.2 功能和.call()
但是,如果函数调用普通函数,this
也是一个隐式参数:
function func(x) {
assert.equal(this, undefined); // implicit parameter
assert.equal(x, 'a');
}
func('a');
// Equivalent:
func.call(undefined, 'a');
也就是说,在函数调用期间,普通函数具有this
,但它被设置为undefined
,这表示它在这里没有真正的用途。
接下来,我们将研究使用this
的缺陷。在我们能够做到这一点之前,我们还需要一个工具:函数的方法.bind()
。
23.4.3 .bind()
:预填充this
和功能参数
.bind()
是函数对象的另一种方法。调用此方法如下。
const boundFunc = someFunc.bind(thisValue, arg1, arg2, arg3);
.bind()
返回一个新函数boundFunc()
。调用该函数调用someFunc()
并将this
设置为thisValue
并且这些参数:arg1
,arg2
,arg3
,然后是boundFunc()
的参数。也就是说,以下两个函数调用是等效的:
boundFunc('a', 'b')
someFunc.call(thisValue, arg1, arg2, arg3, 'a', 'b')
另一种预填this
和参数的方法是通过箭头功能:
const boundFunc2 = (...args) =>
someFunc.call(thisValue, arg1, arg2, arg3, ...args);
因此,.bind()
可以实现为如下的实际功能:
function bind(func, thisValue, ...boundArgs) {
return (...args) =>
func.call(thisValue, ...boundArgs, ...args);
}
23.4.3.1 示例:绑定实际函数
将.bind()
用于实际功能有点不直观,因为你必须为this
提供一个值。该值通常是undefined
,反映了函数调用期间发生的情况。
在下面的示例中,我们通过将add()
的第一个参数绑定到8
来创建add8()
,这是一个具有一个参数的函数。
function add(x, y) {
return x + y;
}
const add8 = add.bind(undefined, 8);
assert.equal(add8(1), 9);
23.4.3.2 示例:绑定方法
在下面的代码中,我们将方法.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
。
23.4.4 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 行中的函数调用相当于:
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”');
当我们调用func()
时,.bind()
确保this
始终为jane
。您还可以使用箭头函数来提取方法:
const func3 = text => jane.says(text);
assert.equal(func3('hello'), 'Jane says “hello”');
23.4.4.1 示例:提取方法
以下是您在实际 Web 开发中可能看到的代码的简化版本:
class ClickHandler {
constructor(elem) {
elem.addEventListener('click', this.handleClick); // (A)
}
handleClick(event) {
alert('Clicked!');
}
}
在 A 行中,我们没有正确提取方法.handleClick()
。相反,我们应该这样做:
elem.addEventListener('click', this.handleClick.bind(this));
23.4.5 this
陷阱:意外遮蔽this
如果使用普通功能,意外遮蔽this
只是一个问题。请考虑以下问题:当您在普通函数内部时,您无法访问周围范围的this
,因为普通函数有自己的this
。换句话说:内部作用域中的变量将变量隐藏在外部作用域中。这被称为 阴影。以下代码是一个示例:
const obj = {
name: 'Jane',
sayHiTo(friends) {
return friends.map(
function (friend) { // (A)
return `${this.name} says hi to ${friend}`; // (B)
});
}
};
assert.throws(
() => obj.sayHiTo(['Tarzan', 'Cheeta']),
{
name: 'TypeError',
message: "Cannot read property 'name' of undefined",
});
为什么错误? B 行中的this
不是.sayHiTo()
的this
,它是从 B 行开始的普通函数的this
。
有几种方法可以解决这个问题。最简单的方法是使用箭头函数 - 它没有自己的this
,因此阴影不是问题。
const obj = {
name: 'Jane',
sayHiTo(friends) {
return friends.map(
(friend) => {
return `${this.name} says hi to ${friend}`;
});
}
};
assert.deepEqual(
obj.sayHiTo(['Tarzan', 'Cheeta']),
['Jane says hi to Tarzan', 'Jane says hi to Cheeta']);
23.4.6 避免this
的陷阱
我们已经看到了两个与this
相关的重大陷阱:
- 提取方法
- 意外遮蔽
this
一个简单的规则有助于避免第二个陷阱:
“避免关键字
function
”:绝不使用普通函数,只使用箭头函数(用于实际函数)和方法定义。
让我们打破这个规则:
- 如果所有实际函数都是箭头函数,则第二个陷阱永远不会发生。
- 使用方法定义意味着您只能在方法中看到
this
,这使得此功能不那么混乱。
但是,即使我不使用(普通)函数 表达式,我还是在语法上喜欢函数 声明。如果您没有参考其中的this
,您可以安全地使用它们。检查工具 ESLint 有规则,有助于此。
唉,第一个陷阱没有简单的方法:每当你提取一个方法时,你必须小心并正确地做到这一点。例如,通过绑定this
。
23.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
令人困惑且没有用处。
23.5 对象作为词典
对象最适合作为记录。但在 ES6 之前,JavaScript 没有字典的数据结构(ES6 带来了映射)。因此,必须将对象用作字典。因此,键必须是字符串,但值可以是任意类型。
我们首先看一下与字典相关的对象的特征,但偶尔也可用于对象作为记录。本节最后提供了实际使用对象作为词典的提示(剧透:如果可以,请避免使用映射)。
23.5.1 任意固定字符串作为属性键
从对象作为记录到对象作为字典时,一个重要的变化是我们必须能够使用任意字符串作为属性键。本小节解释了如何实现固定字符串键。下一小节将介绍如何动态计算任意键。
到目前为止,我们只看到合法的 JavaScript 标识符作为属性键(符号除外):
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!');
23.5.2 计算属性键
到目前为止,我们受到了对象字面值内部属性键的限制:它们总是固定的,它们总是字符串。如果我们将表达式放在方括号中,我们可以动态计算任意键:
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!');
我们现在切换回固定属性键,但如果需要计算属性键,则可以始终使用方括号。
23.5.3 in
运算符:是否存在具有给定键的属性?
in
运算符检查对象是否具有给定键的属性:
const obj = {
foo: 'abc',
bar: false,
};
assert.equal('foo' in obj, true);
assert.equal('unknownKey' in obj, false);
23.5.3.1 通过真实性检查财产是否存在
您还可以使用真实性检查来确定属性是否存在:
assert.equal(
obj.unknownKey ? 'exists' : 'does not exist',
'does not exist');
assert.equal(
obj.foo ? 'exists' : 'does not exist',
'exists');
之前的检查有效,因为读取不存在的属性会返回undefined
,这是假的。因为obj.foo
是真实的。
但是,有一个重要的警告:如果属性存在,则真实性检查失败,但具有假值(undefined
,null
,false
,0
,""
等):
assert.equal(
obj.bar ? 'exists' : 'does not exist',
'does not exist'); // should be: 'exists'
23.5.4 删除属性
您可以通过delete
运算符删除属性:
const obj = {
foo: 123,
};
assert.deepEqual(Object.keys(obj), ['foo']);
delete obj.foo;
assert.deepEqual(Object.keys(obj), []);
23.5.5 字典陷阱
如果使用普通对象(通过对象字面值创建)作为字典,则必须注意两个陷阱。
第一个缺陷是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), []);
那么我们如何解决这些陷阱呢?
- 只要你可以,使用映射。它们是词典的最佳解决方案。
- 如果你不能:将库用于可以安全地完成所有操作的对象字典。
- 如果你不能:使用没有原型的对象。这消除了现代 JavaScript 中的两个陷阱。
const dict = Object.create(null); // no prototype assert.equal('toString' in dict, false); dict['__proto__'] = 123; assert.deepEqual(Object.keys(dict), ['__proto__']);
23.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 中的每个方法。 上表返回一个带有参数自身属性键的数组。在方法的名称中,您可以看到我们之前讨论过的属性键(字符串和符号),属性名称(仅字符串)和属性符号(仅符号)之间的区别。
可枚举性是属性的 属性。默认情况下,属性是可枚举的,但有一些方法可以改变它(在下一个示例中显示,稍后将详细描述)。例如:
const enumerableSymbolKey = Symbol('enumerableSymbolKey');
const nonEnumSymbolKey = Symbol('nonEnumSymbolKey');
// We create the enumerable properties via an object literal
const obj = {
enumerableStringKey: 1,
[enumerableSymbolKey]: 2,
}
// For the 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 ]);
23.5.7 通过Object.values()
列出属性值
Object.values()
列出对象的所有可枚举属性的值:
const obj = {foo: 1, bar: 2};
assert.deepEqual(
Object.values(obj),
[1, 2]);
23.5.8 通过Object.entries()
列出属性条目
Object.entries()
列出了可枚举属性的键值对。每对编码为一个双元素数组:
const obj = {foo: 1, bar: 2};
assert.deepEqual(
Object.entries(obj),
[
['foo', 1],
['bar', 2],
]);
23.5.9 确定性地列出属性
对象的自有(非继承)属性始终按以下顺序列出:
- 具有整数索引的属性(例如,数组索引)
- 按升序数字顺序
- 带字符串键的剩余属性
- 按照添加顺序
- 带符号键的属性
- 按照添加顺序
以下示例演示如何根据以下规则对属性键进行排序:(您可以在规格中查找详细信息。)
> Object.keys({b:0,a:0, 2:0,1:0})
[ '1', '2', 'b', 'a' ]
23.5.10 通过Object.fromEntries()
组装对象
给定[key,value]对可迭代,Object.fromEntries()
创建一个对象:
assert.deepEqual(
Object.fromEntries([['foo',1], ['bar',2]]),
{
foo: 1,
bar: 2,
}
);
它与 Object.entries()
相反。接下来,我们将使用Object.entries()
和Object.fromEntries()
从库 Underscore 中实现多个工具功能。
23.5.10.1 示例:pick(object, ...keys)
pick()
从object
中删除其键不在keys
中的所有属性。删除 非破坏性:pick()
创建修改后的副本并且不会更改原始文件。例如:
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);
}
23.5.10.2 示例:invert(object)
invert()
非破坏性地交换键和对象的值:
assert.deepEqual(
invert({a: 1, b: 2, c: 3}),
{1: 'a', 2: 'b', 3: 'c'}
);
我们可以像这样实现它:
function invert(object) {
const mappedEntries = Object.entries(object)
.map(([key, value]) => [value, key]);
return Object.fromEntries(mappedEntries);
}
23.5.10.3 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);
}
Object.defineProperty(result, coercedKey, {
value,
writable: true,
enumerable: true,
configurable: true,
});
}
return result;
}
笔记:
Object.defineProperty()
在本章后面中解释。- 官方 polyfill 可通过 npm 包
object.fromentries
获得。
23.6 标准方法
Object.prototype
定义了几个可以覆盖的标准方法。两个重要的是:
.toString()
.valueOf()
粗略地说,.toString()
配置对象如何转换为字符串:
> String({toString() { return 'Hello!' }})
'Hello!'
> String({})
'[object Object]'
并且.valueOf()
配置对象如何转换为数字:
> Number({valueOf() { return 123 }})
123
> Number({})
NaN
23.7 高级主题
以下小节简要概述了超出本书范围的主题。
23.7.1 Object.assign()
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 changed!
assert.deepEqual(target, result);
Object.assign()
的用例类似于传播属性的用例。在某种程度上,它破坏性地传播。有关Object.assign()
的更多信息,请参阅“探索 ES6”。
23.7.2 冻结对象
Object.freeze(obj)
使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
的属性,而不冻结属性中存储的对象。
有关Object.freeze()
的更多信息,请参阅“Speaking JavaScript”。
23.7.3 属性属性和属性描述符
就像对象由属性组成一样,属性由 属性 组成。也就是说,您可以配置的不仅仅是属性的值 - 这只是几个属性中的一个。其他属性包括:
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']);
有关属性属性和属性描述符的更多信息,请参阅“Speaking JavaScript”。