diff --git a/docs/array.md b/docs/array.md index 08dd118..d1f123f 100644 --- a/docs/array.md +++ b/docs/array.md @@ -32,7 +32,7 @@ const numbers = [4, 38]; add(...numbers) // 42 ``` -上面代码中,`array.push(...items)`和`add(...numbers)`这两行,都是函数的调用,它们的都使用了扩展运算符。该运算符将一个数组,变为参数序列。 +上面代码中,`array.push(...items)`和`add(...numbers)`这两行,都是函数的调用,它们都使用了扩展运算符。该运算符将一个数组,变为参数序列。 扩展运算符与正常的函数参数可以结合使用,非常灵活。 @@ -947,3 +947,43 @@ for (let i of arr) { ``` 由于空位的处理规则非常不统一,所以建议避免出现空位。 + +## Array.prototype.sort() 的排序稳定性 + +排序稳定性(stable sorting)是排序算法的重要属性,指的是排序关键字相同的项目,排序前后的顺序不变。 + +```javascript +const arr = [ + 'peach', + 'straw', + 'apple', + 'spork' +]; + +const stableSorting = (s1, s2) => { + if (s1[0] < s2[0]) return -1; + return 1; +}; + +arr.sort(stableSorting) +// ["apple", "peach", "straw", "spork"] +``` + +上面代码对数组`arr`按照首字母进行排序。排序结果中,`straw`在`spork`的前面,跟原始顺序一致,所以排序算法`stableSorting`是稳定排序。 + +```javascript +const unstableSorting = (s1, s2) => { + if (s1[0] <= s2[0]) return -1; + return 1; +}; + +arr.sort(unstableSorting) +// ["apple", "peach", "spork", "straw"] +``` + +上面代码中,排序结果是`spork`在`straw`前面,跟原始顺序相反,所以排序算法`unstableSorting`是不稳定的。 + +常见的排序算法之中,插入排序、合并排序、冒泡排序等都是稳定的,堆排序、快速排序等是不稳定的。不稳定排序的主要缺点是,多重排序时可能会产生问题。假设有一个姓和名的列表,按照“先姓,后名”进行排序。开发者可能会先按名字排序,再按姓氏进行排序。如果排序算法是稳定的,这样也可以达到“先姓,后名”的排序效果。如果是不稳定的,就不行。 + +早先的 ECMAScript 没有规定,`Array.prototype.sort()`的默认排序算法是否稳定,留给浏览器自己决定,这导致某些实现是不稳定的。[ES2019](https://github.com/tc39/ecma262/pull/1340) 明确规定,`Array.prototype.sort()`的默认排序算法必须稳定。这个规定已经做到了,现在 JavaScript 各个主要实现的默认排序算法都是稳定的。 + diff --git a/docs/class.md b/docs/class.md index ba4d637..a235dd6 100644 --- a/docs/class.md +++ b/docs/class.md @@ -681,7 +681,7 @@ Foo.prop // 1 上面的写法为`Foo`类定义了一个静态属性`prop`。 -目前,只有这种写法可行,因为 ES6 明确规定,Class 内部只有静态方法,没有静态属性。现在有一个[提案](https://github.com/tc39/proposal-class-fields)提供了类的静态属性,写法是在实例属性法的前面,加上`static`关键字。 +目前,只有这种写法可行,因为 ES6 明确规定,Class 内部只有静态方法,没有静态属性。现在有一个[提案](https://github.com/tc39/proposal-class-fields)提供了类的静态属性,写法是在实例属性的前面,加上`static`关键字。 ```javascript class MyClass { diff --git a/docs/generator.md b/docs/generator.md index e08a149..c0683c6 100644 --- a/docs/generator.md +++ b/docs/generator.md @@ -700,7 +700,7 @@ g.next() // { value: 1, done: false } g.return() // { value: undefined, done: true } ``` -如果 Generator 函数内部有`try...finally`代码块,且正在执行`try`代码块,那么`return`方法会推迟到`finally`代码块执行完再执行。 +如果 Generator 函数内部有`try...finally`代码块,且正在执行`try`代码块,那么`return`方法会导致立刻进入`finally`代码块,执行完以后,整个函数才会结束。 ```javascript function* numbers () { @@ -722,7 +722,7 @@ g.next() // { value: 5, done: false } g.next() // { value: 7, done: true } ``` -上面代码中,调用`return`方法后,就开始执行`finally`代码块,然后等到`finally`代码块执行完,再执行`return`方法。 +上面代码中,调用`return()`方法后,就开始执行`finally`代码块,不执行`try`里面剩下的代码了,然后等到`finally`代码块执行完,再返回`return()`方法指定的返回值。 ## next()、throw()、return() 的共同点 diff --git a/docs/module.md b/docs/module.md index 9dba64d..4c3141d 100644 --- a/docs/module.md +++ b/docs/module.md @@ -221,7 +221,7 @@ import {a} from './xxx.js' a.foo = 'hello'; // 合法操作 ``` -上面代码中,`a`的属性可以成功改写,并且其他模块也可以读到改写后的值。不过,这种写法很难查错,建议凡是输入的变量,都当作完全只读,轻易不要改变它的属性。 +上面代码中,`a`的属性可以成功改写,并且其他模块也可以读到改写后的值。不过,这种写法很难查错,建议凡是输入的变量,都当作完全只读,不要轻易改变它的属性。 `import`后面的`from`指定模块文件的位置,可以是相对路径,也可以是绝对路径,`.js`后缀可以省略。如果只是模块名,不带有路径,那么必须有配置文件,告诉 JavaScript 引擎该模块的位置。 diff --git a/docs/object.md b/docs/object.md index 8f4aa2c..4ff75b6 100644 --- a/docs/object.md +++ b/docs/object.md @@ -4,7 +4,7 @@ ## 属性的简洁表示法 -ES6 允许直接写入变量和函数,作为对象的属性和方法。这样的书写更加简洁。 +ES6 允许在大括号里面,直接写入变量和函数,作为对象的属性和方法。这样的书写更加简洁。 ```javascript const foo = 'bar'; @@ -15,7 +15,7 @@ baz // {foo: "bar"} const baz = {foo: foo}; ``` -上面代码表明,ES6 允许在对象之中,直接写变量。这时,属性名为变量名, 属性值为变量的值。下面是另一个例子。 +上面代码中,变量`foo`直接写在大括号里面。这时,属性名就是变量名, 属性值就是变量值。下面是另一个例子。 ```javascript function f(x, y) { @@ -125,31 +125,24 @@ const cart = { } ``` -注意,简洁写法的属性名总是字符串,这会导致一些看上去比较奇怪的结果。 +简洁写法在打印对象时也很有用。 ```javascript -const obj = { - class () {} +let user = { + name: 'test' }; -// 等同于 - -var obj = { - 'class': function() {} +let foo = { + bar: 'baz' }; + +console.log(user, foo) +// {name: "test"} {bar: "baz"} +console.log({user, foo}) +// {user: {name: "test"}, foo: {bar: "baz"}} ``` -上面代码中,`class`是字符串,所以不会因为它属于关键字,而导致语法解析报错。 - -如果某个方法的值是一个 Generator 函数,前面需要加上星号。 - -```javascript -const obj = { - * m() { - yield 'hello world'; - } -}; -``` +上面代码中,`console.log`直接输出`user`和`foo`两个对象时,就是两组键值对,可能会混淆。把它们放在大括号里面输出,就变成了对象的简洁表示法,每组键值对前面会打印对象名,这样就比较清晰了。 ## 属性名表达式 diff --git a/docs/promise.md b/docs/promise.md index 27ea878..bd2b631 100644 --- a/docs/promise.md +++ b/docs/promise.md @@ -567,13 +567,13 @@ Promise.reject(3).finally(() => {}) ## Promise.all() -`Promise.all`方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。 +`Promise.all()`方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。 ```javascript const p = Promise.all([p1, p2, p3]); ``` -上面代码中,`Promise.all`方法接受一个数组作为参数,`p1`、`p2`、`p3`都是 Promise 实例,如果不是,就会先调用下面讲到的`Promise.resolve`方法,将参数转为 Promise 实例,再进一步处理。(`Promise.all`方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。) +上面代码中,`Promise.all()`方法接受一个数组作为参数,`p1`、`p2`、`p3`都是 Promise 实例,如果不是,就会先调用下面讲到的`Promise.resolve`方法,将参数转为 Promise 实例,再进一步处理。另外,`Promise.all()`方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。 `p`的状态由`p1`、`p2`、`p3`决定,分成两种情况。 @@ -662,7 +662,7 @@ Promise.all([p1, p2]) ## Promise.race() -`Promise.race`方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。 +`Promise.race()`方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。 ```javascript const p = Promise.race([p1, p2, p3]); @@ -670,7 +670,7 @@ const p = Promise.race([p1, p2, p3]); 上面代码中,只要`p1`、`p2`、`p3`之中有一个实例率先改变状态,`p`的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给`p`的回调函数。 -`Promise.race`方法的参数与`Promise.all`方法一样,如果不是 Promise 实例,就会先调用下面讲到的`Promise.resolve`方法,将参数转为 Promise 实例,再进一步处理。 +`Promise.race()`方法的参数与`Promise.all()`方法一样,如果不是 Promise 实例,就会先调用下面讲到的`Promise.resolve()`方法,将参数转为 Promise 实例,再进一步处理。 下面是一个例子,如果指定时间内没有获得结果,就将 Promise 的状态变为`reject`,否则变为`resolve`。 @@ -689,9 +689,138 @@ p 上面代码中,如果 5 秒之内`fetch`方法无法返回结果,变量`p`的状态就会变为`rejected`,从而触发`catch`方法指定的回调函数。 +## Promise.allSettled() + +`Promise.allSettled()`方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只有等到所有这些参数实例都返回结果,不管是`fulfilled`还是`rejected`,包装实例才会结束。该方法由 [ES2020](https://github.com/tc39/proposal-promise-allSettled) 引入。 + +```javascript +const promises = [ + fetch('/api-1'), + fetch('/api-2'), + fetch('/api-3'), +]; + +await Promise.allSettled(promises); +removeLoadingIndicator(); +``` + +上面代码对服务器发出三个请求,等到三个请求都结束,不管请求成功还是失败,加载的滚动图标就会消失。 + +该方法返回的新的 Promise 实例,一旦结束,状态总是`fulfilled`,不会变成`rejected`。状态变成`fulfilled`后,Promise 的监听函数接收到的参数是一个数组,每个成员对应一个传入`Promise.allSettled()`的 Promise 实例。 + +```javascript +const resolved = Promise.resolve(42); +const rejected = Promise.reject(-1); + +const allSettledPromise = Promise.allSettled([resolved, rejected]); + +allSettledPromise.then(function (results) { + console.log(results); +}); +// [ +// { status: 'fulfilled', value: 42 }, +// { status: 'rejected', reason: -1 } +// ] +``` + +上面代码中,`Promise.allSettled()`的返回值`allSettledPromise`,状态只可能变成`fulfilled`。它的监听函数接收到的参数是数组`results`。该数组的每个成员都是一个对象,对应传入`Promise.allSettled()`的两个 Promise 实例。每个对象都有`status`属性,该属性的值只可能是字符串`fulfilled`或字符串`rejected`。`fulfilled`时,对象有`value`属性,`rejected`时有`reason`属性,对应两种状态的返回值。 + +下面是返回值用法的例子。 + +```javascript +const promises = [ fetch('index.html'), fetch('https://does-not-exist/') ]; +const results = await Promise.allSettled(promises); + +// 过滤出成功的请求 +const successfulPromises = results.filter(p => p.status === 'fulfilled'); + +// 过滤出失败的请求,并输出原因 +const errors = results + .filter(p => p.status === 'rejected') + .map(p => p.reason); +``` + +有时候,我们不关心异步操作的结果,只关心这些操作有没有结束。这时,`Promise.allSettled()`方法就很有用。如果没有这个方法,想要确保所有操作都结束,就很麻烦。`Promise.all()`方法无法做到这一点。 + +```javascript +const urls = [ /* ... */ ]; +const requests = urls.map(x => fetch(x)); + +try { + await Promise.all(requests); + console.log('所有请求都成功。'); +} catch { + console.log('至少一个请求失败,其他请求可能还没结束。'); +} +``` + +上面代码中,`Promise.all()`无法确定所有请求都结束。想要达到这个目的,写起来很麻烦,有了`Promise.allSettled()`,这就很容易了。 + +## Promise.any() + +`Promise.any()`方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只要参数实例有一个变成`fulfilled`状态,包装实例就会变成`fulfilled`状态;如果所有参数实例都变成`rejected`状态,包装实例就会变成`rejected`状态。该方法目前是一个第三阶段的[提案](https://github.com/tc39/proposal-promise-any) 。 + +`Promise.any()`跟`Promise.race()`方法很像,只有一点不同,就是不会因为某个 Promise 变成`rejected`状态而结束。 + +```javascript +const promises = [ + fetch('/endpoint-a').then(() => 'a'), + fetch('/endpoint-b').then(() => 'b'), + fetch('/endpoint-c').then(() => 'c'), +]; +try { + const first = await Promise.any(promises); + console.log(first); +} catch (error) { + console.log(error); +} +``` + +上面代码中,`Promise.any()`方法的参数数组包含三个 Promise 操作。其中只要有一个变成`fulfilled`,`Promise.any()`返回的 Promise 对象就变成`fulfilled`。如果所有三个操作都变成`rejected`,那么就会`await`命令就会抛出错误。 + +`Promise.any()`抛出的错误,不是一个一般的错误,而是一个 AggregateError 实例。它相当于一个数组,每个成员对应一个被`rejected`的操作所抛出的错误。下面是 AggregateError 的实现示例。 + +```javascript +new AggregateError() extends Array -> AggregateError + +const err = new AggregateError(); +err.push(new Error("first error")); +err.push(new Error("second error")); +throw err; +``` + +捕捉错误时,如果不用`try...catch`结构和 await 命令,可以像下面这样写。 + +```javascript +Promise.any(promises).then( + (first) => { + // Any of the promises was fulfilled. + }, + (error) => { + // All of the promises were rejected. + } +); +``` + +下面是一个例子。 + +```javascript +var resolved = Promise.resolve(42); +var rejected = Promise.reject(-1); +var alsoRejected = Promise.reject(Infinity); + +Promise.any([resolved, rejected, alsoRejected]).then(function (result) { + console.log(result); // 42 +}); + +Promise.any([rejected, alsoRejected]).catch(function (results) { + console.log(results); // [-1, Infinity] +}); +``` + ## Promise.resolve() -有时需要将现有对象转为 Promise 对象,`Promise.resolve`方法就起到这个作用。 +有时需要将现有对象转为 Promise 对象,`Promise.resolve()`方法就起到这个作用。 ```javascript const jsPromise = Promise.resolve($.ajax('/whatever.json')); @@ -699,7 +828,7 @@ const jsPromise = Promise.resolve($.ajax('/whatever.json')); 上面代码将 jQuery 生成的`deferred`对象,转为一个新的 Promise 对象。 -`Promise.resolve`等价于下面的写法。 +`Promise.resolve()`等价于下面的写法。 ```javascript Promise.resolve('foo') diff --git a/docs/proposals.md b/docs/proposals.md index 8cf5c0b..f10c1ca 100644 --- a/docs/proposals.md +++ b/docs/proposals.md @@ -273,12 +273,60 @@ const showSplashScreen = response.settings.showSplashScreen ?? true; 上面代码中,默认值只有在属性值为`null`或`undefined`时,才会生效。 -这个运算符可以跟链判断运算符`?.`配合使用。 +这个运算符的一个目的,就是跟链判断运算符`?.`配合使用,为`null`或`undefined`的值设置默认值。 ```javascript const animationDuration = response.settings?.animationDuration ?? 300; ``` +上面代码中,`response.settings`如果是`null`或`undefined`,就会返回默认值300。 + +这个运算符很适合判断函数参数是否赋值。 + +```javascript +function Component(props) { + const enable = props.enabled ?? true; + // … +} +``` + +上面代码判断`props`参数的`enabled`属性是否赋值,等同于下面的写法。 + +```javascript +function Component(props) { + const { + enabled: enable = true, + } = props; + // … +} +``` + +`??`有一个运算优先级问题,它与`&&`和`||`的优先级孰高孰低。现在的规则是,如果多个逻辑运算符一起使用,必须用括号表明优先级,否则会报错。 + +```javascript +// 报错 +lhs && middle ?? rhs +lhs ?? middle && rhs +lhs || middle ?? rhs +lhs ?? middle || rhs +``` + +上面四个表达式都会报错,必须加入表明优先级的括号。 + +```javascript +(lhs && middle) ?? rhs; +lhs && (middle ?? rhs); + +(lhs ?? middle) && rhs; +lhs ?? (middle && rhs); + +(lhs || middle) ?? rhs; +lhs || (middle ?? rhs); + +(lhs ?? middle) || rhs; +lhs ?? (middle || rhs); +``` + ## 函数的部分执行 ### 语法 @@ -646,12 +694,15 @@ BigInt(1.5) // RangeError BigInt('1.5') // SyntaxError ``` -BigInt 对象继承了 Object 提供的实例方法。 +BigInt 对象继承了 Object 对象的两个实例方法。 -- `BigInt.prototype.toLocaleString()` - `BigInt.prototype.toString()` - `BigInt.prototype.valueOf()` +它还继承了 Number 对象的一个实例方法。 + +- `BigInt.prototype.toLocaleString()` + 此外,还提供了三个静态方法。 - `BigInt.asUintN(width, BigInt)`: 给定的 BigInt 转为 0 到 2width - 1 之间对应的值。 diff --git a/docs/proxy.md b/docs/proxy.md index a9b1d4d..0ec65c9 100644 --- a/docs/proxy.md +++ b/docs/proxy.md @@ -1131,7 +1131,7 @@ service.employees().then(json => { function createWebService(baseUrl) { return new Proxy({}, { get(target, propKey, receiver) { - return () => httpGet(baseUrl+'/' + propKey); + return () => httpGet(baseUrl + '/' + propKey); } }); } diff --git a/docs/regex.md b/docs/regex.md index 9fa854f..370b01f 100644 --- a/docs/regex.md +++ b/docs/regex.md @@ -132,6 +132,17 @@ codePointLength(s) // 2 上面代码中,不加`u`修饰符,就无法识别非规范的`K`字符。 +**(6)转义** + +没有`u`修饰符的情况下,正则中没有定义的转义(如逗号的转义`\,`)无效,而在`u`模式会报错。 + +```javascript +/\,/ // /\,/ +/\,/u // 报错 +``` + +上面代码中,没有`u`修饰符时,逗号前面的反斜杠是无效的,加了`u`修饰符就报错。 + ## RegExp.prototype.unicode 属性 正则实例对象新增`unicode`属性,表示是否设置了`u`修饰符。 diff --git a/docs/set-map.md b/docs/set-map.md index 4680c8b..f22a875 100644 --- a/docs/set-map.md +++ b/docs/set-map.md @@ -354,7 +354,7 @@ const ws = new WeakSet(b); // Uncaught TypeError: Invalid value used in weak set(…) ``` -上面代码中,数组`b`的成员不是对象,加入 WeaKSet 就会报错。 +上面代码中,数组`b`的成员不是对象,加入 WeakSet 就会报错。 WeakSet 结构有以下三个方法。 diff --git a/docs/string-methods.md b/docs/string-methods.md index 318d0f7..99233f3 100644 --- a/docs/string-methods.md +++ b/docs/string-methods.md @@ -31,11 +31,11 @@ String.fromCodePoint(0x78, 0x1f680, 0x79) === 'x\uD83D\uDE80y' ES6 还为原生的 String 对象,提供了一个`raw()`方法。该方法返回一个斜杠都被转义(即斜杠前面再加一个斜杠)的字符串,往往用于模板字符串的处理方法。 ```javascript -String.raw`Hi\n${2+3}!`; -// 返回 "Hi\\n5!" +String.raw`Hi\n${2+3}!` +// 实际返回 "Hi\\n5!",显示的是转义后的结果 "Hi\n5!" String.raw`Hi\u000A!`; -// 返回 "Hi\\u000A!" +// 实际返回 "Hi\\u000A!",显示的是转义后的结果 "Hi\u000A!" ``` 如果原字符串的斜杠已经转义,那么`String.raw()`会进行再次转义。 @@ -49,16 +49,16 @@ String.raw`Hi\\n` === "Hi\\\\n" // true `String.raw()`方法可以作为处理模板字符串的基本方法,它会将所有变量替换,而且对斜杠进行转义,方便下一步作为字符串来使用。 -`String.raw()`方法也可以作为正常的函数使用。这时,它的第一个参数,应该是一个具有`raw`属性的对象,且`raw`属性的值应该是一个数组。 +`String.raw()`本质上是一个正常的函数,只是专用于模板字符串的标签函数。如果写成正常函数的形式,它的第一个参数,应该是一个具有`raw`属性的对象,且`raw`属性的值应该是一个数组,对应模板字符串解析后的值。 ```javascript -String.raw({ raw: 'test' }, 0, 1, 2); -// 't0e1s2t' - +// `foo${1 + 2}bar` // 等同于 -String.raw({ raw: ['t','e','s','t'] }, 0, 1, 2); +String.raw({ raw: ['foo', 'bar'] }, 1 + 2) // "foo3bar" ``` +上面代码中,`String.raw()`方法的第一个参数是一个对象,它的`raw`属性等同于原始的模板字符串解析后得到的数组。 + 作为函数,`String.raw()`的代码实现基本如下。 ```javascript