JavaScript 权威指南第七版(GPT 重译)(五)(2)https://developer.aliyun.com/article/1485372
13.2.7 顺序执行的 Promises
Promise.all()
让并行运行任意数量的 Promises 变得容易。Promise 链使得表达一系列固定数量的 Promises 变得容易。然而,按顺序运行任意数量的 Promises 就比较棘手了。例如,假设你有一个要获取的 URL 数组,但为了避免过载网络,你希望一次只获取一个。如果数组长度和内容未知,你无法提前编写 Promise 链,因此需要动态构建一个,代码如下:
function fetchSequentially(urls) { // We'll store the URL bodies here as we fetch them const bodies = []; // Here's a Promise-returning function that fetches one body function fetchOne(url) { return fetch(url) .then(response => response.text()) .then(body => { // We save the body to the array, and we're purposely // omitting a return value here (returning undefined) bodies.push(body); }); } // Start with a Promise that will fulfill right away (with value undefined) let p = Promise.resolve(undefined); // Now loop through the desired URLs, building a Promise chain // of arbitrary length, fetching one URL at each stage of the chain for(url of urls) { p = p.then(() => fetchOne(url)); } // When the last Promise in that chain is fulfilled, then the // bodies array is ready. So let's return a Promise for that // bodies array. Note that we don't include any error handlers: // we want to allow errors to propagate to the caller. return p.then(() => bodies); }
有了定义的 fetchSequentially()
函数,我们可以一次获取一个 URL,代码与我们之前用来演示 Promise.all()
的并行获取代码类似:
fetchSequentially(urls) .then(bodies => { /* do something with the array of strings */ }) .catch(e => console.error(e));
fetchSequentially()
函数首先创建一个 Promise,在返回后立即实现。然后,它基于该初始 Promise 构建一个长的线性 Promise 链,并返回链中的最后一个 Promise。这就像设置一排多米诺骨牌,然后推倒第一个。
我们可以采取另一种(可能更优雅)的方法。与其提前创建 Promises,我们可以让每个 Promise 的回调创建并返回下一个 Promise。也就是说,我们不是创建和链接一堆 Promises,而是创建解析为其他 Promises 的 Promises。我们不是创建一条多米诺般的 Promise 链,而是创建一个嵌套在另一个内部的 Promise 序列,就像一组套娃一样。采用这种方法,我们的代码可以返回第一个(最外层)Promise,知道它最终会实现(或拒绝!)与序列中最后一个(最内层)Promise 相同的值。接下来的 promiseSequence()
函数编写为通用的,不特定于 URL 获取。它在我们讨论 Promises 的最后,因为它很复杂。然而,如果你仔细阅读了本章,希望你能理解它是如何工作的。特别要注意的是,promiseSequence()
中的嵌套函数似乎递归调用自身,但因为“递归”调用是通过 then()
方法进行的,实际上并没有传统的递归发生:
// This function takes an array of input values and a "promiseMaker" function. // For any input value x in the array, promiseMaker(x) should return a Promise // that will fulfill to an output value. This function returns a Promise // that fulfills to an array of the computed output values. // // Rather than creating the Promises all at once and letting them run in // parallel, however, promiseSequence() only runs one Promise at a time // and does not call promiseMaker() for a value until the previous Promise // has fulfilled. function promiseSequence(inputs, promiseMaker) { // Make a private copy of the array that we can modify inputs = [...inputs]; // Here's the function that we'll use as a Promise callback // This is the pseudorecursive magic that makes this all work. function handleNextInput(outputs) { if (inputs.length === 0) { // If there are no more inputs left, then return the array // of outputs, finally fulfilling this Promise and all the // previous resolved-but-not-fulfilled Promises. return outputs; } else { // If there are still input values to process, then we'll // return a Promise object, resolving the current Promise // with the future value from a new Promise. let nextInput = inputs.shift(); // Get the next input value, return promiseMaker(nextInput) // compute the next output value, // Then create a new outputs array with the new output value .then(output => outputs.concat(output)) // Then "recurse", passing the new, longer, outputs array .then(handleNextInput); } } // Start with a Promise that fulfills to an empty array and use // the function above as its callback. return Promise.resolve([]).then(handleNextInput); }
这个 promiseSequence()
函数是故意通用的。我们可以用它来获取 URL,代码如下:
// Given a URL, return a Promise that fulfills to the URL body text function fetchBody(url) { return fetch(url).then(r => r.text()); } // Use it to sequentially fetch a bunch of URL bodies promiseSequence(urls, fetchBody) .then(bodies => { /* do something with the array of strings */ }) .catch(console.error);
13.3 async 和 await
ES2017 引入了两个新关键字——async
和 await
——代表了 JavaScript 异步编程的范式转变。这些新关键字极大地简化了 Promises 的使用,并允许我们编写基于 Promise 的异步代码,看起来像阻塞的同步代码,等待网络响应或其他异步事件。虽然理解 Promises 如何工作仍然很重要,但当与 async
和 await
一起使用时,它们的复杂性(有时甚至是它们的存在本身!)会消失。
正如我们在本章前面讨论的那样,异步代码无法像常规同步代码那样返回值或抛出异常。这就是 Promises 设计的原因。已实现的 Promise 的值就像同步函数的返回值一样。而拒绝的 Promise 的值就像同步函数抛出的值一样。后者的相似性通过 .catch()
方法的命名得到明确体现。async
和 await
采用高效的基于 Promise 的代码,并隐藏了 Promises,使得你的异步代码可以像低效、阻塞的同步代码一样易于阅读和推理。
13.3.1 await 表达式
await
关键字接受一个 Promise 并将其转换为返回值或抛出异常。给定一个 Promise 对象p
,表达式await p
会等待直到p
完成。如果p
成功,那么await p
的值就是p
的成功值。另一方面,如果p
被拒绝,那么await p
表达式会抛出p
的拒绝值。我们通常不会使用一个保存 Promise 的变量来使用await
;相反,我们会在返回 Promise 的函数调用之前使用它:
let response = await fetch("/api/user/profile"); let profile = await response.json();
立即理解await
关键字不会导致程序阻塞并直到指定的 Promise 完成。代码仍然是异步的,await
只是掩饰了这一事实。这意味着任何使用await
的代码本身都是异步的。
13.3.2 async 函数
因为任何使用await
的代码都是异步的,有一个关键规则:只能在使用async
关键字声明的函数内部使用await
关键字。以下是本章前面提到的getHighScore()
函数的一个使用async
和await
重写的版本:
async function getHighScore() { let response = await fetch("/api/user/profile"); let profile = await response.json(); return profile.highScore; }
声明一个函数为async
意味着函数的返回值将是一个 Promise,即使函数体中没有任何与 Promise 相关的代码。如果一个async
函数看起来正常返回,那么作为真正返回值的 Promise 对象将解析为该表面返回值。如果一个async
函数看起来抛出异常,那么它返回的 Promise 对象将被拒绝并带有该异常。
getHighScore()
函数被声明为async
,因此它返回一个 Promise。由于它返回一个 Promise,我们可以使用await
关键字:
displayHighScore(await getHighScore());
但请记住,那行代码只有在另一个async
函数内部才能起作用!你可以无限嵌套await
表达式在async
函数内部。但如果你在顶层²或者由于某种原因在一个非async
函数内部,那么你就不能使用await
,而必须以常规方式处理返回的 Promise:
getHighScore().then(displayHighScore).catch(console.error);
你可以在任何类型的函数中使用async
关键字。它可以与function
关键字一起作为语句或表达式使用。它可以与箭头函数一起使用,也可以与类和对象字面量中的方法快捷形式一起使用。(有关编写函数的各种方式,请参见第八章。)
13.3.3 等待多个 Promises
假设我们使用async
编写了我们的getJSON()
函数:
async function getJSON(url) { let response = await fetch(url); let body = await response.json(); return body; }
现在假设我们想要使用这个函数获取两个 JSON 值:
let value1 = await getJSON(url1); let value2 = await getJSON(url2);
这段代码的问题在于它是不必要的顺序执行:第二个 URL 的获取将等到第一个 URL 的获取完成后才开始。如果第二个 URL 不依赖于从第一个 URL 获取的值,那么我们可能应该尝试同时获取这两个值。这是async
函数的基于 Promise 的特性的一个案例。为了等待一组并发执行的async
函数,我们使用Promise.all()
,就像直接使用 Promises 一样:
let [value1, value2] = await Promise.all([getJSON(url1), getJSON(url2)]);
13.3.4 实现细节
最后,为了理解async
函数的工作原理,可能有助于思考底层发生了什么。
假设你写了一个这样的async
函数:
async function f(x) { /* body */ }
你可以将这看作是一个包装在原始函数体周围的返回 Promise 的函数:
function f(x) { return new Promise(function(resolve, reject) { try { resolve((function(x) { /* body */ })(x)); } catch(e) { reject(e); } }); }
用这种方式来表达await
关键字比较困难。但可以将await
关键字看作是一个标记,将函数体分解为单独的同步块。ES2017 解释器可以将函数体分解为一系列单独的子函数,每个子函数都会传递给前面的await
标记的 Promise 的then()
方法。
13.4 异步迭代
我们从回调和基于事件的异步性讨论开始了本章,当我们介绍 Promise 时,我们注意到它们对于单次异步计算很有用,但不适用于重复异步事件的源,比如setInterval()
、Web 浏览器中的“click”事件或 Node 流上的“data”事件。因为单个 Promise 不能用于序列的异步事件,所以我们也不能使用常规的async
函数和await
语句来处理这些事情。
然而,ES2018 提供了一个解决方案。异步迭代器类似于第十二章中描述的迭代器,但它们是基于 Promise 的,并且旨在与一种新形式的for/of
循环一起使用:for/await
。
13.4.1 for/await
循环
Node 12 使其可读流异步可迭代。这意味着您可以使用像这样的for/await
循环从流中读取连续的数据块:
const fs = require("fs"); async function parseFile(filename) { let stream = fs.createReadStream(filename, { encoding: "utf-8"}); for await (let chunk of stream) { parseChunk(chunk); // Assume parseChunk() is defined elsewhere } }
像常规的await
表达式一样,for/await
循环是基于 Promise 的。粗略地说,异步迭代器生成一个 Promise,for/await
循环等待该 Promise 实现,将实现值分配给循环变量,并运行循环体。然后它重新开始,从迭代器获取另一个 Promise 并等待该新 Promise 实现。
假设您有一个 URL 数组:
const urls = [url1, url2, url3];
您可以对每个 URL 调用fetch()
以获取 Promise 数组:
const promises = urls.map(url => fetch(url));
我们在本章的前面看到,现在我们可以使用Promise.all()
等待数组中所有 Promise 被实现。但假设我们希望在第一个 fetch 的结果变为可用时获取结果,并且不想等待所有 URL 被获取。 (当然,第一个 fetch 可能比其他任何 fetch 都要花费更长的时间,因此这不一定比使用Promise.all()
更快。)数组是可迭代的,因此我们可以使用常规的for/of
循环遍历 Promise 数组:
for(const promise of promises) { response = await promise; handle(response); }
这个示例代码使用了一个常规的for/of
循环和一个常规的迭代器。但由于这个迭代器返回的是 Promise,我们也可以使用新的for/await
来编写稍微更简单的代码:
for await (const response of promises) { handle(response); }
在这种情况下,for/await
循环只是将await
调用嵌入到循环中,使我们的代码稍微更加紧凑,但这两个例子实际上做的事情是完全一样的。重要的是,这两个例子只有在声明为async
的函数内部才能工作;for/await
循环在这方面与常规的await
表达式没有区别。
然而,重要的是要意识到,在这个例子中我们使用for/await
与一个常规迭代器。使用完全异步迭代器会更有趣。
13.4.2 异步迭代器
让我们回顾一下第十二章中的一些术语。可迭代对象是可以与for/of
循环一起使用的对象。它定义了一个符号名称为Symbol.iterator
的方法。该方法返回一个迭代器对象。迭代器对象具有一个next()
方法,可以重复调用以获取可迭代对象的值。迭代器对象的next()
方法返回迭代结果对象。迭代结果对象具有一个value
属性和/或一个done
属性。
异步迭代器与常规迭代器非常相似,但有两个重要的区别。首先,异步可迭代对象实现了一个符号名称为Symbol.asyncIterator
的方法,而不是Symbol.iterator
。 (正如我们之前看到的,for/await
与常规可迭代对象兼容,但它更喜欢异步可迭代对象,并在尝试Symbol.iterator
方法之前尝试Symbol.asyncIterator
方法。)其次,异步迭代器的next()
方法返回一个解析为迭代器结果对象的 Promise,而不是直接返回迭代器结果对象。
注意
在前一节中,当我们在常规的同步可迭代的 Promise 数组上使用for/await
时,我们正在处理同步迭代器结果对象,其中value
属性是一个 Promise 对象,但done
属性是同步的。真正的异步迭代器会返回 Promise 以进行迭代结果对象,并且value
和done
属性都是异步的。区别是微妙的:使用异步迭代器时,关于何时结束迭代的选择可以异步进行。
13.4.3 异步生成器
正如我们在第十二章中看到的,实现迭代器的最简单方法通常是使用生成器。对于异步迭代器也是如此,我们可以使用声明为async
的生成器函数来实现。异步生成器具有异步函数和生成器的特性:你可以像在常规异步函数中一样使用await
,也可以像在常规生成器中一样使用yield
。但你yield
的值会自动包装在 Promise 中。甚至异步生成器的语法也是一个组合:async function
和function *
组合成async function *
。下面是一个示例,展示了如何使用异步生成器和for/await
循环以循环语法而不是setInterval()
回调函数重复在固定间隔运行代码:
// A Promise-based wrapper around setTimeout() that we can use await with. // Returns a Promise that fulfills in the specified number of milliseconds function elapsedTime(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // An async generator function that increments a counter and yields it // a specified (or infinite) number of times at a specified interval. async function* clock(interval, max=Infinity) { for(let count = 1; count <= max; count++) { // regular for loop await elapsedTime(interval); // wait for time to pass yield count; // yield the counter } } // A test function that uses the async generator with for/await async function test() { // Async so we can use for/await for await (let tick of clock(300, 100)) { // Loop 100 times every 300ms console.log(tick); } }
13.4.4 实现异步迭代器
除了使用异步生成器来实现异步迭代器外,还可以通过定义一个具有返回一个返回解析为迭代器结果对象的 Promise 的next()
方法的对象的Symbol.asyncIterator()
方法来直接实现它们。在下面的代码中,我们重新实现了前面示例中的clock()
函数,使其不是一个生成器,而是只返回一个异步可迭代对象。请注意,在这个示例中,next()
方法并没有显式返回一个 Promise;相反,我们只是声明next()
为 async:
function clock(interval, max=Infinity) { // A Promise-ified version of setTimeout that we can use await with. // Note that this takes an absolute time instead of an interval. function until(time) { return new Promise(resolve => setTimeout(resolve, time - Date.now())); } // Return an asynchronously iterable object return { startTime: Date.now(), // Remember when we started count: 1, // Remember which iteration we're on async next() { // The next() method makes this an iterator if (this.count > max) { // Are we done? return { done: true }; // Iteration result indicating done } // Figure out when the next iteration should begin, let targetTime = this.startTime + this.count * interval; // wait until that time, await until(targetTime); // and return the count value in an iteration result object. return { value: this.count++ }; }, // This method means that this iterator object is also an iterable. [Symbol.asyncIterator]() { return this; } }; }
这个基于迭代器的clock()
函数版本修复了基于生成器的版本中的一个缺陷。请注意,在这个更新的代码中,我们针对每次迭代应该开始的绝对时间,并从中减去当前时间以计算传递给setTimeout()
的间隔。如果我们在for/await
循环中使用clock()
,这个版本将更精确地按照指定的间隔运行循环迭代,因为它考虑了实际运行循环体所需的时间。但这个修复不仅仅是关于时间精度。for/await
循环总是在开始下一次迭代之前等待一个迭代返回的 Promise 被实现。但如果你在没有for/await
循环的情况下使用异步迭代器,就没有任何阻止你在想要的任何时候调用next()
方法。使用基于生成器的clock()
版本,如果连续调用next()
方法三次,你将得到三个几乎在同一时间实现的 Promise,这可能不是你想要的。我们在这里实现的基于迭代器的版本没有这个问题。
异步迭代器的好处在于它们允许我们表示异步事件或数据流。之前讨论的clock()
函数相对简单,因为异步性的源是我们自己进行的setTimeout()
调用。但是当我们尝试处理其他异步源时,比如触发事件处理程序,实现异步迭代器就变得相当困难——通常我们有一个响应事件的单个事件处理程序函数,但是迭代器的每次调用next()
方法必须返回一个不同的 Promise 对象,并且在第一个 Promise 解析之前可能会多次调用next()
。这意味着任何异步迭代器方法必须能够维护一个内部 Promise 队列,以便按顺序解析异步事件。如果我们将这种 Promise 队列行为封装到一个 AsyncQueue 类中,那么基于 AsyncQueue 编写异步迭代器就会变得更容易。³
接下来的 AsyncQueue 类具有enqueue()
和dequeue()
方法,就像你期望的队列类一样。然而,dequeue()
方法返回一个 Promise 而不是实际值,这意味着在调用enqueue()
之前调用dequeue()
是可以的。AsyncQueue 类也是一个异步迭代器,并且旨在与一个for/await
循环一起使用,其主体在每次异步排队新值时运行一次。 (AsyncQueue 有一个close()
方法。一旦调用,就不能再排队更多的值。当一个关闭的队列为空时,for/await
循环将停止循环。)
请注意,AsyncQueue 的实现不使用async
或await
,而是直接使用 Promises。这段代码有些复杂,你可以用它来测试你对我们在这一长章节中涵盖的内容的理解。即使你不完全理解 AsyncQueue 的实现,也请看一下后面的简短示例:它在 AsyncQueue 的基础上实现了一个简单但非常有趣的异步迭代器。
/** * An asynchronously iterable queue class. Add values with enqueue() * and remove them with dequeue(). dequeue() returns a Promise, which * means that values can be dequeued before they are enqueued. The * class implements [Symbol.asyncIterator] and next() so that it can * be used with the for/await loop (which will not terminate until * the close() method is called.) */ class AsyncQueue { constructor() { // Values that have been queued but not dequeued yet are stored here this.values = []; // When Promises are dequeued before their corresponding values are // queued, the resolve methods for those Promises are stored here. this.resolvers = []; // Once closed, no more values can be enqueued, and no more unfulfilled // Promises returned. this.closed = false; } enqueue(value) { if (this.closed) { throw new Error("AsyncQueue closed"); } if (this.resolvers.length > 0) { // If this value has already been promised, resolve that Promise const resolve = this.resolvers.shift(); resolve(value); } else { // Otherwise, queue it up this.values.push(value); } } dequeue() { if (this.values.length > 0) { // If there is a queued value, return a resolved Promise for it const value = this.values.shift(); return Promise.resolve(value); } else if (this.closed) { // If no queued values and we're closed, return a resolved // Promise for the "end-of-stream" marker return Promise.resolve(AsyncQueue.EOS); } else { // Otherwise, return an unresolved Promise, // queuing the resolver function for later use return new Promise((resolve) => { this.resolvers.push(resolve); }); } } close() { // Once the queue is closed, no more values will be enqueued. // So resolve any pending Promises with the end-of-stream marker while(this.resolvers.length > 0) { this.resolvers.shift()(AsyncQueue.EOS); } this.closed = true; } // Define the method that makes this class asynchronously iterable [Symbol.asyncIterator]() { return this; } // Define the method that makes this an asynchronous iterator. The // dequeue() Promise resolves to a value or the EOS sentinel if we're // closed. Here, we need to return a Promise that resolves to an // iterator result object. next() { return this.dequeue().then(value => (value === AsyncQueue.EOS) ? { value: undefined, done: true } : { value: value, done: false }); } } // A sentinel value returned by dequeue() to mark "end of stream" when closed AsyncQueue.EOS = Symbol("end-of-stream");
因为这个 AsyncQueue 类定义了异步迭代的基础,我们可以通过异步排队值来创建自己的更有趣的异步迭代器。下面是一个示例,它使用 AsyncQueue 生成一个可以用for/await
循环处理的 web 浏览器事件流:
// Push events of the specified type on the specified document element // onto an AsyncQueue object, and return the queue for use as an event stream function eventStream(elt, type) { const q = new AsyncQueue(); // Create a queue elt.addEventListener(type, e=>q.enqueue(e)); // Enqueue events return q; } async function handleKeys() { // Get a stream of keypress events and loop once for each one for await (const event of eventStream(document, "keypress")) { console.log(event.key); } }
13.5 总结
在本章中,你已经学到了:
- 大多数真实世界的 JavaScript 编程是异步的。
- 传统上,异步性是通过事件和回调函数来处理的。然而,这可能会变得复杂,因为你可能会得到多层嵌套在其他回调内部的回调,并且很难进行健壮的错误处理。
- Promises 提供了一种新的组织回调函数的方式。如果使用正确(不幸的是,Promises 很容易被错误使用),它们可以将原本嵌套的异步代码转换为
then()
调用的线性链,其中一个计算的异步步骤跟随另一个。此外,Promises 允许你将错误处理代码集中到一条catch()
调用中,放在then()
调用链的末尾。 async
和await
关键字允许我们编写基于 Promise 的异步代码,但看起来像同步代码。这使得代码更容易理解和推理。如果一个函数声明为async
,它将隐式返回一个 Promise。在async
函数内部,你可以像同步计算 Promise 值一样await
一个 Promise(或返回 Promise 的函数)。- 可以使用
for/await
循环处理异步可迭代对象。你可以通过实现[Symbol.asyncIterator]()
方法或调用async function *
生成器函数来创建异步可迭代对象。异步迭代器提供了一种替代 Node 中“data”事件的方式,并可用于表示客户端 JavaScript 中用户输入事件的流。
¹ XMLHttpRequest 类与 XML 无关。在现代客户端 JavaScript 中,它大部分被fetch()
API 取代,该 API 在§15.11.1 中有介绍。这里展示的代码示例是本书中仅剩的基于 XMLHttpRequest 的示例。
² 通常可以在浏览器的开发者控制台中的顶层使用await
。而且有一个未决提案,允许在未来版本的 JavaScript 中使用顶层await
。
³ 我从https://2ality.com博客中了解到了这种异步迭代的方法,作者是 Axel Rauschmayer 博士。
第十四章:元编程
本章介绍了一些高级 JavaScript 功能,这些功能在日常编程中并不常用,但对于编写可重用库的程序员可能很有价值,并且对于任何想要深入了解 JavaScript 对象行为细节的人也很有趣。
这里描述的许多功能可以宽泛地描述为“元编程”:如果常规编程是编写代码来操作数据,那么元编程就是编写代码来操作其他代码。在像 JavaScript 这样的动态语言中,编程和元编程之间的界限模糊——甚至简单地使用for/in
循环迭代对象的属性的能力对更习惯于更静态语言的程序员来说可能被认为是“元编程”。
本章涵盖的元编程主题包括:
- §14.1 控制对象属性的可枚举性、可删除性和可配置性
- §14.2 控制对象的可扩展性,并创建“封闭”和“冻结”对象
- §14.3 查询和设置对象的原型
- §14.4 使用众所周知的符号微调类型的行为
- §14.5 使用模板标签函数创建 DSL(领域特定语言)
- §14.6 使用
reflect
方法探查对象 - §14.7 使用代理控制对象行为
14.1 属性特性
JavaScript 对象的属性当然有名称和值,但每个属性还有三个关联属性,指定该属性的行为方式以及您可以对其执行的操作:
- 可写 属性指定属性的值是否可以更改。
- 可枚举 属性指定属性是否由
for/in
循环和Object.keys()
方法枚举。 - 可配置 属性指定属性是否可以被删除,以及属性的属性是否可以更改。
在对象字面量中定义的属性或通过普通赋值给对象的属性是可写的、可枚举的和可配置的。但是,JavaScript 标准库中定义的许多属性并非如此。
本节解释了查询和设置属性特性的 API。这个 API 对于库作者尤为重要,因为:
- 它允许他们向原型对象添加方法并使它们不可枚举,就像内置方法一样。
- 它允许它们“锁定”它们的对象,定义不能被更改或删除的属性。
请回顾§6.10.6,在那里提到,“数据属性”具有值,“访问器属性”则具有 getter 和/或 setter 方法。对于本节的目的,我们将考虑访问器属性的 getter 和 setter 方法为属性特性。按照这种逻辑,我们甚至会说数据属性的值也是一个属性。因此,我们可以说属性有一个名称和四个属性。数据属性的四个属性是值、可写、可枚举和可配置。访问器属性没有值属性或可写属性:它们的可写性取决于是否存在 setter。因此,访问器属性的四个属性是获取、设置、可枚举和可配置。
JavaScript 用于查询和设置属性的方法使用一个称为属性描述符的对象来表示四个属性的集合。属性描述符对象具有与其描述的属性相同名称的属性。因此,数据属性的属性描述符对象具有名为value
、writable
、enumerable
和configurable
的属性。访问器属性的描述符具有get
和set
属性,而不是value
和writable
。writable
、enumerable
和configurable
属性是布尔值,get
和set
属性是函数值。
要获取指定对象的命名属性的属性描述符,请调用Object.getOwnPropertyDescriptor()
:
// Returns {value: 1, writable:true, enumerable:true, configurable:true} Object.getOwnPropertyDescriptor({x: 1}, "x"); // Here is an object with a read-only accessor property const random = { get octet() { return Math.floor(Math.random()*256); }, }; // Returns { get: /*func*/, set:undefined, enumerable:true, configurable:true} Object.getOwnPropertyDescriptor(random, "octet"); // Returns undefined for inherited properties and properties that don't exist. Object.getOwnPropertyDescriptor({}, "x") // => undefined; no such prop Object.getOwnPropertyDescriptor({}, "toString") // => undefined; inherited
如其名称所示,Object.getOwnPropertyDescriptor()
仅适用于自有属性。要查询继承属性的属性,必须显式遍历原型链。(参见§14.3 中的Object.getPrototypeOf()
);另请参阅§14.6 中的类似Reflect.getOwnPropertyDescriptor()
函数。
要设置属性的属性或使用指定属性创建新属性,请调用Object.defineProperty()
,传递要修改的对象、要创建或更改的属性的名称和属性描述符对象:
let o = {}; // Start with no properties at all // Add a non-enumerable data property x with value 1. Object.defineProperty(o, "x", { value: 1, writable: true, enumerable: false, configurable: true }); // Check that the property is there but is non-enumerable o.x // => 1 Object.keys(o) // => [] // Now modify the property x so that it is read-only Object.defineProperty(o, "x", { writable: false }); // Try to change the value of the property o.x = 2; // Fails silently or throws TypeError in strict mode o.x // => 1 // The property is still configurable, so we can change its value like this: Object.defineProperty(o, "x", { value: 2 }); o.x // => 2 // Now change x from a data property to an accessor property Object.defineProperty(o, "x", { get: function() { return 0; } }); o.x // => 0
你传递给Object.defineProperty()
的属性描述符不必包含所有四个属性。如果你正在创建一个新属性,那么被省略的属性被视为false
或undefined
。如果你正在修改一个现有属性,那么你省略的属性将保持不变。请注意,此方法会更改现有的自有属性或创建新的自有属性,但不会更改继承的属性。另请参阅§14.6 中的非常相似的Reflect.defineProperty()
函数。
如果要一次创建或修改多个属性,请使用Object.defineProperties()
。第一个参数是要修改的对象。第二个参数是将要创建或修改的属性的名称映射到这些属性的属性描述符的对象。例如:
let p = Object.defineProperties({}, { x: { value: 1, writable: true, enumerable: true, configurable: true }, y: { value: 1, writable: true, enumerable: true, configurable: true }, r: { get() { return Math.sqrt(this.x*this.x + this.y*this.y); }, enumerable: true, configurable: true } }); p.r // => Math.SQRT2
这段代码从一个空对象开始,然后向其添加两个数据属性和一个只读访问器属性。它依赖于Object.defineProperties()
返回修改后的对象(Object.defineProperty()
也是如此)。
Object.create()
方法是在§6.2 中引入的。我们在那里学到,该方法的第一个参数是新创建对象的原型对象。该方法还接受第二个可选参数,与Object.defineProperties()
的第二个参数相同。如果你向Object.create()
传递一组属性描述符,那么它们将用于向新创建的对象添加属性。
如果尝试创建或修改属性不被允许,Object.defineProperty()
和Object.defineProperties()
会抛出 TypeError。如果你尝试向不可扩展的对象添加新属性,就会发生这种情况(参见§14.2)。这些方法可能抛出 TypeError 的其他原因与属性本身有关。可写属性控制对值属性的更改尝试。可配置属性控制对其他属性的更改尝试(并指定属性是否可以被删除)。然而,规则并不完全直观。例如,如果属性是可配置的,那么即使该属性是不可写的,也可以更改该属性的值。此外,即使属性是不可配置的,也可以将属性从可写更改为不可写。以下是完整的规则。调用Object.defineProperty()
或Object.defineProperties()
尝试违反这些规则会抛出 TypeError:
- 如果一个对象不可扩展,你可以编辑其现有的自有属性,但不能向其添加新属性。
- 如果一个属性不可配置,你就无法改变它的可配置或可枚举属性。
- 如果一个访问器属性不可配置,你就无法更改其 getter 或 setter 方法,也无法将其更改为数据属性。
- 如果一个数据属性不可配置,你就无法将其更改为访问器属性。
- 如果一个数据属性不可配置,你就无法将其可写属性从
false
更改为true
,但你可以将其从true
更改为false
。 - 如果一个数据属性不可配置且不可写,你就无法改变它的值。但是,如果一个属性是可配置但不可写的,你可以改变它的值(因为这与使其可写,然后改变值,然后将其转换回不可写是一样的)。
§6.7 描述了Object.assign()
函数,它将一个或多个源对象的属性值复制到目标对象中。Object.assign()
只复制可枚举属性和属性值,而不是属性属性。这通常是我们想要的,但这意味着,例如,如果一个源对象具有一个访问器属性,那么复制到目标对象的是 getter 函数返回的值,而不是 getter 函数本身。示例 14-1 演示了如何使用Object.getOwnPropertyDescriptor()
和Object.defineProperty()
创建Object.assign()
的变体,该变体复制整个属性描述符而不仅仅是复制属性值。
示例 14-1. 从一个对象复制属性及其属性到另一个对象
/* * Define a new Object.assignDescriptors() function that works like * Object.assign() except that it copies property descriptors from * source objects into the target object instead of just copying * property values. This function copies all own properties, both * enumerable and non-enumerable. And because it copies descriptors, * it copies getter functions from source objects and overwrites setter * functions in the target object rather than invoking those getters and * setters. * * Object.assignDescriptors() propagates any TypeErrors thrown by * Object.defineProperty(). This can occur if the target object is sealed * or frozen or if any of the source properties try to change an existing * non-configurable property on the target object. * * Note that the assignDescriptors property is added to Object with * Object.defineProperty() so that the new function can be created as * a non-enumerable property like Object.assign(). */ Object.defineProperty(Object, "assignDescriptors", { // Match the attributes of Object.assign() writable: true, enumerable: false, configurable: true, // The function that is the value of the assignDescriptors property. value: function(target, ...sources) { for(let source of sources) { for(let name of Object.getOwnPropertyNames(source)) { let desc = Object.getOwnPropertyDescriptor(source, name); Object.defineProperty(target, name, desc); } for(let symbol of Object.getOwnPropertySymbols(source)) { let desc = Object.getOwnPropertyDescriptor(source, symbol); Object.defineProperty(target, symbol, desc); } } return target; } }); let o = {c: 1, get count() {return this.c++;}}; // Define object with getter let p = Object.assign({}, o); // Copy the property values let q = Object.assignDescriptors({}, o); // Copy the property descriptors p.count // => 1: This is now just a data property so p.count // => 1: ...the counter does not increment. q.count // => 2: Incremented once when we copied it the first time, q.count // => 3: ...but we copied the getter method so it increments.
14.2 对象的可扩展性
对象的可扩展属性指定了是否可以向对象添加新属性。普通的 JavaScript 对象默认是可扩展的,但你可以通过本节描述的函数来改变这一点。
要确定一个对象是否可扩展,请将其传递给Object.isExtensible()
。要使对象不可扩展,请将其传递给Object.preventExtensions()
。一旦这样做,任何尝试向对象添加新属性的操作在严格模式下都会抛出 TypeError,在非严格模式下会静默失败而不会报错。此外,尝试更改不可扩展对象的原型(参见§14.3)将始终抛出 TypeError。
请注意,一旦将对象设置为不可扩展,就没有办法再使其可扩展。另外,请注意,调用Object.preventExtensions()
只影响对象本身的可扩展性。如果向不可扩展对象的原型添加新属性,那么不可扩展对象将继承这些新属性。
有两个类似的函数,Reflect.isExtensible()
和Reflect.preventExtensions()
,在§14.6 中描述。
可扩展属性的目的是能够将对象“锁定”到已知状态,并防止外部篡改。对象的可扩展属性通常与属性的可配置和可写属性一起使用,JavaScript 定义了使设置这些属性变得容易的函数:
Object.seal()
的作用类似于Object.preventExtensions()
,但除了使对象不可扩展外,它还使该对象的所有自有属性不可配置。这意味着无法向对象添加新属性,也无法删除或配置现有属性。但是,可写的现有属性仍然可以设置。无法取消密封的对象。你可以使用Object.isSealed()
来确定对象是否被密封。Object.freeze()
更加严格地锁定对象。除了使对象不可扩展和其属性不可配置外,它还使对象的所有自有数据属性变为只读。(如果对象具有具有 setter 方法的访问器属性,则这些属性不受影响,仍然可以通过对属性赋值来调用。)使用Object.isFrozen()
来确定对象是否被冻结。
需要理解的是 Object.seal()
和 Object.freeze()
只会影响它们所传递的对象:它们不会影响该对象的原型。如果你想完全锁定一个对象,可能需要同时封闭或冻结原型链中的对象。
Object.preventExtensions()
, Object.seal()
, 和 Object.freeze()
都会返回它们所传递的对象,这意味着你可以在嵌套函数调用中使用它们:
// Create a sealed object with a frozen prototype and a non-enumerable property let o = Object.seal(Object.create(Object.freeze({x: 1}), {y: {value: 2, writable: true}}));
如果你正在编写一个 JavaScript 库,将对象传递给库用户编写的回调函数,你可能会在这些对象上使用 Object.freeze()
来防止用户的代码修改它们。这样做很容易和方便,但也存在一些权衡:冻结的对象可能会干扰常见的 JavaScript 测试策略,例如。
JavaScript 权威指南第七版(GPT 重译)(五)(4)https://developer.aliyun.com/article/1485374