From ffb39b10b2627631e1fb98c9e7978392aa4b055d Mon Sep 17 00:00:00 2001 From: wxf1024 <372301467@qq.com> Date: Sat, 28 Jan 2023 17:20:00 +0800 Subject: [PATCH 1/2] basics --- docs/array.md | 853 ------------------------------------------ docs/destructuring.md | 303 --------------- docs/function.md | 676 --------------------------------- docs/object.md | 377 ------------------- docs/string.md | 518 ------------------------- readme.ppz.md | 16 + 6 files changed, 16 insertions(+), 2727 deletions(-) create mode 100644 readme.ppz.md diff --git a/docs/array.md b/docs/array.md index 8c2b69000..cdeef70ab 100644 --- a/docs/array.md +++ b/docs/array.md @@ -73,67 +73,6 @@ console.log(...[1, 2]) 上面三种情况,扩展运算符都放在圆括号里面,但是前两种情况会报错,因为扩展运算符所在的括号不是函数调用。 -### 替代函数的 apply() 方法 - -由于扩展运算符可以展开数组,所以不再需要`apply()`方法将数组转为函数的参数了。 - -```javascript -// ES5 的写法 -function f(x, y, z) { - // ... -} -var args = [0, 1, 2]; -f.apply(null, args); - -// ES6 的写法 -function f(x, y, z) { - // ... -} -let args = [0, 1, 2]; -f(...args); -``` - -下面是扩展运算符取代`apply()`方法的一个实际的例子,应用`Math.max()`方法,简化求出一个数组最大元素的写法。 - -```javascript -// ES5 的写法 -Math.max.apply(null, [14, 3, 77]) - -// ES6 的写法 -Math.max(...[14, 3, 77]) - -// 等同于 -Math.max(14, 3, 77); -``` - -上面代码中,由于 JavaScript 不提供求数组最大元素的函数,所以只能套用`Math.max()`函数,将数组转为一个参数序列,然后求最大值。有了扩展运算符以后,就可以直接用`Math.max()`了。 - -另一个例子是通过`push()`函数,将一个数组添加到另一个数组的尾部。 - -```javascript -// ES5 的写法 -var arr1 = [0, 1, 2]; -var arr2 = [3, 4, 5]; -Array.prototype.push.apply(arr1, arr2); - -// ES6 的写法 -let arr1 = [0, 1, 2]; -let arr2 = [3, 4, 5]; -arr1.push(...arr2); -``` - -上面代码的 ES5 写法中,`push()`方法的参数不能是数组,所以只好通过`apply()`方法变通使用`push()`方法。有了扩展运算符,就可以直接将数组传入`push()`方法。 - -下面是另外一个例子。 - -```javascript -// ES5 -new (Date.bind.apply(Date, [null, 2015, 1, 1])) - -// ES6 -new Date(...[2015, 1, 1]); -``` - ### 扩展运算符的应用 **(1)复制数组** @@ -325,795 +264,3 @@ let arr = [...arrayLike]; ``` 上面代码中,`arrayLike`是一个类似数组的对象,但是没有部署 Iterator 接口,扩展运算符就会报错。这时,可以改为使用`Array.from`方法将`arrayLike`转为真正的数组。 - -**(6)Map 和 Set 结构,Generator 函数** - -扩展运算符内部调用的是数据结构的 Iterator 接口,因此只要具有 Iterator 接口的对象,都可以使用扩展运算符,比如 Map 结构。 - -```javascript -let map = new Map([ - [1, 'one'], - [2, 'two'], - [3, 'three'], -]); - -let arr = [...map.keys()]; // [1, 2, 3] -``` - -Generator 函数运行后,返回一个遍历器对象,因此也可以使用扩展运算符。 - -```javascript -const go = function*(){ - yield 1; - yield 2; - yield 3; -}; - -[...go()] // [1, 2, 3] -``` - -上面代码中,变量`go`是一个 Generator 函数,执行后返回的是一个遍历器对象,对这个遍历器对象执行扩展运算符,就会将内部遍历得到的值,转为一个数组。 - -如果对没有 Iterator 接口的对象,使用扩展运算符,将会报错。 - -```javascript -const obj = {a: 1, b: 2}; -let arr = [...obj]; // TypeError: Cannot spread non-iterable object -``` - -## Array.from() - -`Array.from()`方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。 - -下面是一个类似数组的对象,`Array.from()`将它转为真正的数组。 - -```javascript -let arrayLike = { - '0': 'a', - '1': 'b', - '2': 'c', - length: 3 -}; - -// ES5 的写法 -var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c'] - -// ES6 的写法 -let arr2 = Array.from(arrayLike); // ['a', 'b', 'c'] -``` - -实际应用中,常见的类似数组的对象是 DOM 操作返回的 NodeList 集合,以及函数内部的`arguments`对象。`Array.from()`都可以将它们转为真正的数组。 - -```javascript -// NodeList 对象 -let ps = document.querySelectorAll('p'); -Array.from(ps).filter(p => { - return p.textContent.length > 100; -}); - -// arguments 对象 -function foo() { - var args = Array.from(arguments); - // ... -} -``` - -上面代码中,`querySelectorAll()`方法返回的是一个类似数组的对象,可以将这个对象转为真正的数组,再使用`filter()`方法。 - -只要是部署了 Iterator 接口的数据结构,`Array.from()`都能将其转为数组。 - -```javascript -Array.from('hello') -// ['h', 'e', 'l', 'l', 'o'] - -let namesSet = new Set(['a', 'b']) -Array.from(namesSet) // ['a', 'b'] -``` - -上面代码中,字符串和 Set 结构都具有 Iterator 接口,因此可以被`Array.from()`转为真正的数组。 - -如果参数是一个真正的数组,`Array.from()`会返回一个一模一样的新数组。 - -```javascript -Array.from([1, 2, 3]) -// [1, 2, 3] -``` - -值得提醒的是,扩展运算符(`...`)也可以将某些数据结构转为数组。 - -```javascript -// arguments对象 -function foo() { - const args = [...arguments]; -} - -// NodeList对象 -[...document.querySelectorAll('div')] -``` - -扩展运算符背后调用的是遍历器接口(`Symbol.iterator`),如果一个对象没有部署这个接口,就无法转换。`Array.from()`方法还支持类似数组的对象。所谓类似数组的对象,本质特征只有一点,即必须有`length`属性。因此,任何有`length`属性的对象,都可以通过`Array.from()`方法转为数组,而此时扩展运算符就无法转换。 - -```javascript -Array.from({ length: 3 }); -// [ undefined, undefined, undefined ] -``` - -上面代码中,`Array.from()`返回了一个具有三个成员的数组,每个位置的值都是`undefined`。扩展运算符转换不了这个对象。 - -对于还没有部署该方法的浏览器,可以用`Array.prototype.slice()`方法替代。 - -```javascript -const toArray = (() => - Array.from ? Array.from : obj => [].slice.call(obj) -)(); -``` - -`Array.from()`还可以接受一个函数作为第二个参数,作用类似于数组的`map()`方法,用来对每个元素进行处理,将处理后的值放入返回的数组。 - -```javascript -Array.from(arrayLike, x => x * x); -// 等同于 -Array.from(arrayLike).map(x => x * x); - -Array.from([1, 2, 3], (x) => x * x) -// [1, 4, 9] -``` - -下面的例子是取出一组 DOM 节点的文本内容。 - -```javascript -let spans = document.querySelectorAll('span.name'); - -// map() -let names1 = Array.prototype.map.call(spans, s => s.textContent); - -// Array.from() -let names2 = Array.from(spans, s => s.textContent) -``` - -下面的例子将数组中布尔值为`false`的成员转为`0`。 - -```javascript -Array.from([1, , 2, , 3], (n) => n || 0) -// [1, 0, 2, 0, 3] -``` - -另一个例子是返回各种数据的类型。 - -```javascript -function typesOf () { - return Array.from(arguments, value => typeof value) -} -typesOf(null, [], NaN) -// ['object', 'object', 'number'] -``` - -如果`map()`函数里面用到了`this`关键字,还可以传入`Array.from()`的第三个参数,用来绑定`this`。 - -`Array.from()`可以将各种值转为真正的数组,并且还提供`map`功能。这实际上意味着,只要有一个原始的数据结构,你就可以先对它的值进行处理,然后转成规范的数组结构,进而就可以使用数量众多的数组方法。 - -```javascript -Array.from({ length: 2 }, () => 'jack') -// ['jack', 'jack'] -``` - -上面代码中,`Array.from()`的第一个参数指定了第二个参数运行的次数。这种特性可以让该方法的用法变得非常灵活。 - -`Array.from()`的另一个应用是,将字符串转为数组,然后返回字符串的长度。因为它能正确处理各种 Unicode 字符,可以避免 JavaScript 将大于`\uFFFF`的 Unicode 字符,算作两个字符的 bug。 - -```javascript -function countSymbols(string) { - return Array.from(string).length; -} -``` - -## Array.of() - -`Array.of()`方法用于将一组值,转换为数组。 - -```javascript -Array.of(3, 11, 8) // [3,11,8] -Array.of(3) // [3] -Array.of(3).length // 1 -``` - -这个方法的主要目的,是弥补数组构造函数`Array()`的不足。因为参数个数的不同,会导致`Array()`的行为有差异。 - -```javascript -Array() // [] -Array(3) // [, , ,] -Array(3, 11, 8) // [3, 11, 8] -``` - -上面代码中,`Array()`方法没有参数、一个参数、三个参数时,返回的结果都不一样。只有当参数个数不少于 2 个时,`Array()`才会返回由参数组成的新数组。参数只有一个正整数时,实际上是指定数组的长度。 - -`Array.of()`基本上可以用来替代`Array()`或`new Array()`,并且不存在由于参数不同而导致的重载。它的行为非常统一。 - -```javascript -Array.of() // [] -Array.of(undefined) // [undefined] -Array.of(1) // [1] -Array.of(1, 2) // [1, 2] -``` - -`Array.of()`总是返回参数值组成的数组。如果没有参数,就返回一个空数组。 - -`Array.of()`方法可以用下面的代码模拟实现。 - -```javascript -function ArrayOf(){ - return [].slice.call(arguments); -} -``` - -## 实例方法:copyWithin() - -数组实例的`copyWithin()`方法,在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。也就是说,使用这个方法,会修改当前数组。 - -```javascript -Array.prototype.copyWithin(target, start = 0, end = this.length) -``` - -它接受三个参数。 - -- target(必需):从该位置开始替换数据。如果为负值,表示倒数。 -- start(可选):从该位置开始读取数据,默认为 0。如果为负值,表示从末尾开始计算。 -- end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示从末尾开始计算。 - -这三个参数都应该是数值,如果不是,会自动转为数值。 - -```javascript -[1, 2, 3, 4, 5].copyWithin(0, 3) -// [4, 5, 3, 4, 5] -``` - -上面代码表示将从 3 号位直到数组结束的成员(4 和 5),复制到从 0 号位开始的位置,结果覆盖了原来的 1 和 2。 - -下面是更多例子。 - -```javascript -// 将3号位复制到0号位 -[1, 2, 3, 4, 5].copyWithin(0, 3, 4) -// [4, 2, 3, 4, 5] - -// -2相当于3号位,-1相当于4号位 -[1, 2, 3, 4, 5].copyWithin(0, -2, -1) -// [4, 2, 3, 4, 5] - -// 将3号位复制到0号位 -[].copyWithin.call({length: 5, 3: 1}, 0, 3) -// {0: 1, 3: 1, length: 5} - -// 将2号位到数组结束,复制到0号位 -let i32a = new Int32Array([1, 2, 3, 4, 5]); -i32a.copyWithin(0, 2); -// Int32Array [3, 4, 5, 4, 5] - -// 对于没有部署 TypedArray 的 copyWithin 方法的平台 -// 需要采用下面的写法 -[].copyWithin.call(new Int32Array([1, 2, 3, 4, 5]), 0, 3, 4); -// Int32Array [4, 2, 3, 4, 5] -``` - -## 实例方法:find(),findIndex(),findLast(),findLastIndex() - -数组实例的`find()`方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为`true`的成员,然后返回该成员。如果没有符合条件的成员,则返回`undefined`。 - -```javascript -[1, 4, -5, 10].find((n) => n < 0) -// -5 -``` - -上面代码找出数组中第一个小于 0 的成员。 - -```javascript -[1, 5, 10, 15].find(function(value, index, arr) { - return value > 9; -}) // 10 -``` - -上面代码中,`find()`方法的回调函数可以接受三个参数,依次为当前的值、当前的位置和原数组。 - -数组实例的`findIndex()`方法的用法与`find()`方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回`-1`。 - -```javascript -[1, 5, 10, 15].findIndex(function(value, index, arr) { - return value > 9; -}) // 2 -``` - -这两个方法都可以接受第二个参数,用来绑定回调函数的`this`对象。 - -```javascript -function f(v){ - return v > this.age; -} -let person = {name: 'John', age: 20}; -[10, 12, 26, 15].find(f, person); // 26 -``` - -上面的代码中,`find()`函数接收了第二个参数`person`对象,回调函数中的`this`对象指向`person`对象。 - -另外,这两个方法都可以发现`NaN`,弥补了数组的`indexOf()`方法的不足。 - -```javascript -[NaN].indexOf(NaN) -// -1 - -[NaN].findIndex(y => Object.is(NaN, y)) -// 0 -``` - -上面代码中,`indexOf()`方法无法识别数组的`NaN`成员,但是`findIndex()`方法可以借助`Object.is()`方法做到。 - -`find()`和`findIndex()`都是从数组的0号位,依次向后检查。[ES2022](https://github.com/tc39/proposal-array-find-from-last) 新增了两个方法`findLast()`和`findLastIndex()`,从数组的最后一个成员开始,依次向前检查,其他都保持不变。 - -```javascript -const array = [ - { value: 1 }, - { value: 2 }, - { value: 3 }, - { value: 4 } -]; - -array.findLast(n => n.value % 2 === 1); // { value: 3 } -array.findLastIndex(n => n.value % 2 === 1); // 2 -``` - -上面示例中,`findLast()`和`findLastIndex()`从数组结尾开始,寻找第一个`value`属性为奇数的成员。结果,该成员是`{ value: 3 }`,位置是2号位。 - -## 实例方法:fill() - -`fill`方法使用给定值,填充一个数组。 - -```javascript -['a', 'b', 'c'].fill(7) -// [7, 7, 7] - -new Array(3).fill(7) -// [7, 7, 7] -``` - -上面代码表明,`fill`方法用于空数组的初始化非常方便。数组中已有的元素,会被全部抹去。 - -`fill`方法还可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置。 - -```javascript -['a', 'b', 'c'].fill(7, 1, 2) -// ['a', 7, 'c'] -``` - -上面代码表示,`fill`方法从 1 号位开始,向原数组填充 7,到 2 号位之前结束。 - -注意,如果填充的类型为对象,那么被赋值的是同一个内存地址的对象,而不是深拷贝对象。 - -```javascript -let arr = new Array(3).fill({name: "Mike"}); -arr[0].name = "Ben"; -arr -// [{name: "Ben"}, {name: "Ben"}, {name: "Ben"}] - -let arr = new Array(3).fill([]); -arr[0].push(5); -arr -// [[5], [5], [5]] -``` - -## 实例方法:entries(),keys() 和 values() - -ES6 提供三个新的方法——`entries()`,`keys()`和`values()`——用于遍历数组。它们都返回一个遍历器对象(详见《Iterator》一章),可以用`for...of`循环进行遍历,唯一的区别是`keys()`是对键名的遍历、`values()`是对键值的遍历,`entries()`是对键值对的遍历。 - -```javascript -for (let index of ['a', 'b'].keys()) { - console.log(index); -} -// 0 -// 1 - -for (let elem of ['a', 'b'].values()) { - console.log(elem); -} -// 'a' -// 'b' - -for (let [index, elem] of ['a', 'b'].entries()) { - console.log(index, elem); -} -// 0 "a" -// 1 "b" -``` - -如果不使用`for...of`循环,可以手动调用遍历器对象的`next`方法,进行遍历。 - -```javascript -let letter = ['a', 'b', 'c']; -let entries = letter.entries(); -console.log(entries.next().value); // [0, 'a'] -console.log(entries.next().value); // [1, 'b'] -console.log(entries.next().value); // [2, 'c'] -``` - -## 实例方法:includes() - -`Array.prototype.includes`方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的`includes`方法类似。ES2016 引入了该方法。 - -```javascript -[1, 2, 3].includes(2) // true -[1, 2, 3].includes(4) // false -[1, 2, NaN].includes(NaN) // true -``` - -该方法的第二个参数表示搜索的起始位置,默认为`0`。如果第二个参数为负数,则表示倒数的位置,如果这时它大于数组长度(比如第二个参数为`-4`,但数组长度为`3`),则会重置为从`0`开始。 - -```javascript -[1, 2, 3].includes(3, 3); // false -[1, 2, 3].includes(3, -1); // true -``` - -没有该方法之前,我们通常使用数组的`indexOf`方法,检查是否包含某个值。 - -```javascript -if (arr.indexOf(el) !== -1) { - // ... -} -``` - -`indexOf`方法有两个缺点,一是不够语义化,它的含义是找到参数值的第一个出现位置,所以要去比较是否不等于`-1`,表达起来不够直观。二是,它内部使用严格相等运算符(`===`)进行判断,这会导致对`NaN`的误判。 - -```javascript -[NaN].indexOf(NaN) -// -1 -``` - -`includes`使用的是不一样的判断算法,就没有这个问题。 - -```javascript -[NaN].includes(NaN) -// true -``` - -下面代码用来检查当前环境是否支持该方法,如果不支持,部署一个简易的替代版本。 - -```javascript -const contains = (() => - Array.prototype.includes - ? (arr, value) => arr.includes(value) - : (arr, value) => arr.some(el => el === value) -)(); -contains(['foo', 'bar'], 'baz'); // => false -``` - -另外,Map 和 Set 数据结构有一个`has`方法,需要注意与`includes`区分。 - -- Map 结构的`has`方法,是用来查找键名的,比如`Map.prototype.has(key)`、`WeakMap.prototype.has(key)`、`Reflect.has(target, propertyKey)`。 -- Set 结构的`has`方法,是用来查找值的,比如`Set.prototype.has(value)`、`WeakSet.prototype.has(value)`。 - -## 实例方法:flat(),flatMap() - -数组的成员有时还是数组,`Array.prototype.flat()`用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数据没有影响。 - -```javascript -[1, 2, [3, 4]].flat() -// [1, 2, 3, 4] -``` - -上面代码中,原数组的成员里面有一个数组,`flat()`方法将子数组的成员取出来,添加在原来的位置。 - -`flat()`默认只会“拉平”一层,如果想要“拉平”多层的嵌套数组,可以将`flat()`方法的参数写成一个整数,表示想要拉平的层数,默认为1。 - -```javascript -[1, 2, [3, [4, 5]]].flat() -// [1, 2, 3, [4, 5]] - -[1, 2, [3, [4, 5]]].flat(2) -// [1, 2, 3, 4, 5] -``` - -上面代码中,`flat()`的参数为2,表示要“拉平”两层的嵌套数组。 - -如果不管有多少层嵌套,都要转成一维数组,可以用`Infinity`关键字作为参数。 - -```javascript -[1, [2, [3]]].flat(Infinity) -// [1, 2, 3] -``` - -如果原数组有空位,`flat()`方法会跳过空位。 - -```javascript -[1, 2, , 4, 5].flat() -// [1, 2, 4, 5] -``` - -`flatMap()`方法对原数组的每个成员执行一个函数(相当于执行`Array.prototype.map()`),然后对返回值组成的数组执行`flat()`方法。该方法返回一个新数组,不改变原数组。 - -```javascript -// 相当于 [[2, 4], [3, 6], [4, 8]].flat() -[2, 3, 4].flatMap((x) => [x, x * 2]) -// [2, 4, 3, 6, 4, 8] -``` - -`flatMap()`只能展开一层数组。 - -```javascript -// 相当于 [[[2]], [[4]], [[6]], [[8]]].flat() -[1, 2, 3, 4].flatMap(x => [[x * 2]]) -// [[2], [4], [6], [8]] -``` - -上面代码中,遍历函数返回的是一个双层的数组,但是默认只能展开一层,因此`flatMap()`返回的还是一个嵌套数组。 - -`flatMap()`方法的参数是一个遍历函数,该函数可以接受三个参数,分别是当前数组成员、当前数组成员的位置(从零开始)、原数组。 - -```javascript -arr.flatMap(function callback(currentValue[, index[, array]]) { - // ... -}[, thisArg]) -``` - -`flatMap()`方法还可以有第二个参数,用来绑定遍历函数里面的`this`。 - -## 实例方法:at() - -长久以来,JavaScript 不支持数组的负索引,如果要引用数组的最后一个成员,不能写成`arr[-1]`,只能使用`arr[arr.length - 1]`。 - -这是因为方括号运算符`[]`在 JavaScript 语言里面,不仅用于数组,还用于对象。对于对象来说,方括号里面就是键名,比如`obj[1]`引用的是键名为字符串`1`的键,同理`obj[-1]`引用的是键名为字符串`-1`的键。由于 JavaScript 的数组是特殊的对象,所以方括号里面的负数无法再有其他语义了,也就是说,不可能添加新语法来支持负索引。 - -为了解决这个问题,[ES2022](https://github.com/tc39/proposal-relative-indexing-method/) 为数组实例增加了`at()`方法,接受一个整数作为参数,返回对应位置的成员,并支持负索引。这个方法不仅可用于数组,也可用于字符串和类型数组(TypedArray)。 - -```javascript -const arr = [5, 12, 8, 130, 44]; -arr.at(2) // 8 -arr.at(-2) // 130 -``` - -如果参数位置超出了数组范围,`at()`返回`undefined`。 - -```javascript -const sentence = 'This is a sample sentence'; - -sentence.at(0); // 'T' -sentence.at(-1); // 'e' - -sentence.at(-100) // undefined -sentence.at(100) // undefined -``` - -## 实例方法:toReversed(),toSorted(),toSpliced(),with() - -很多数组的传统方法会改变原数组,比如`push()`、`pop()`、`shift()`、`unshift()`等等。数组只要调用了这些方法,它的值就变了。现在有一个[提案](https://github.com/tc39/proposal-change-array-by-copy),允许对数组进行操作时,不改变原数组,而返回一个原数组的拷贝。 - -这样的方法一共有四个。 - -- `Array.prototype.toReversed() -> Array` -- `Array.prototype.toSorted(compareFn) -> Array` -- `Array.prototype.toSpliced(start, deleteCount, ...items) -> Array` -- `Array.prototype.with(index, value) -> Array` - -它们分别对应数组的原有方法。 - -- `toReversed()`对应`reverse()`,用来颠倒数组成员的位置。 -- `toSorted()`对应`sort()`,用来对数组成员排序。 -- `toSpliced()`对应`splice()`,用来在指定位置,删除指定数量的成员,并插入新成员。 -- `with(index, value)`对应`splice(index, 1, value)`,用来将指定位置的成员替换为新的值。 - -上面是这四个新方法对应的原有方法,含义和用法完全一样,唯一不同的是不会改变原数组,而是返回原数组操作后的拷贝。 - -下面是示例。 - -```javascript -const sequence = [1, 2, 3]; -sequence.toReversed() // [3, 2, 1] -sequence // [1, 2, 3] - -const outOfOrder = [3, 1, 2]; -outOfOrder.toSorted() // [1, 2, 3] -outOfOrder // [3, 1, 2] - -const array = [1, 2, 3, 4]; -array.toSpliced(1, 2, 5, 6, 7) // [1, 5, 6, 7, 4] -array // [1, 2, 3, 4] - -const correctionNeeded = [1, 1, 3]; -correctionNeeded.with(1, 2) // [1, 2, 3] -correctionNeeded // [1, 1, 3] -``` - -## 实例方法:group(),groupToMap() - -数组成员分组是一个常见需求,比如 SQL 有`GROUP BY`子句和函数式编程有 MapReduce 方法。现在有一个[提案](https://github.com/tc39/proposal-array-grouping),为 JavaScript 新增了数组实例方法`group()`和`groupToMap()`,它们可以根据分组函数的运行结果,将数组成员分组。 - -`group()`的参数是一个分组函数,原数组的每个成员都会依次执行这个函数,确定自己是哪一个组。 - -```javascript -const array = [1, 2, 3, 4, 5]; - -array.group((num, index, array) => { - return num % 2 === 0 ? 'even': 'odd'; -}); -// { odd: [1, 3, 5], even: [2, 4] } -``` - -`group()`的分组函数可以接受三个参数,依次是数组的当前成员、该成员的位置序号、原数组(上例是`num`、`index`和`array`)。分组函数的返回值应该是字符串(或者可以自动转为字符串),以作为分组后的组名。 - -`group()`的返回值是一个对象,该对象的键名就是每一组的组名,即分组函数返回的每一个字符串(上例是`even`和`odd`);该对象的键值是一个数组,包括所有产生当前键名的原数组成员。 - -下面是另一个例子。 - -```javascript -[6.1, 4.2, 6.3].groupBy(Math.floor) -// { '4': [4.2], '6': [6.1, 6.3] } -``` - -上面示例中,`Math.floor`作为分组函数,对原数组进行分组。它的返回值原本是数值,这时会自动转为字符串,作为分组的组名。原数组的成员根据分组函数的运行结果,进入对应的组。 - -`group()`还可以接受一个对象,作为第二个参数。该对象会绑定分组函数(第一个参数)里面的`this`,不过如果分组函数是一个箭头函数,该对象无效,因为箭头函数内部的`this`是固化的。 - -`groupToMap()`的作用和用法与`group()`完全一致,唯一的区别是返回值是一个 Map 结构,而不是对象。Map 结构的键名可以是各种值,所以不管分组函数返回什么值,都会直接作为组名(Map 结构的键名),不会强制转为字符串。这对于分组函数返回值是对象的情况,尤其有用。 - -```javascript -const array = [1, 2, 3, 4, 5]; - -const odd = { odd: true }; -const even = { even: true }; -array.groupToMap((num, index, array) => { - return num % 2 === 0 ? even: odd; -}); -// Map { {odd: true}: [1, 3, 5], {even: true}: [2, 4] } -``` - -上面示例返回的是一个 Map 结构,它的键名就是分组函数返回的两个对象`odd`和`even`。 - -总之,按照字符串分组就使用`group()`,按照对象分组就使用`groupToMap()`。 - -## 数组的空位 - -数组的空位指的是,数组的某一个位置没有任何值,比如`Array()`构造函数返回的数组都是空位。 - -```javascript -Array(3) // [, , ,] -``` - -上面代码中,`Array(3)`返回一个具有 3 个空位的数组。 - -注意,空位不是`undefined`,某一个位置的值等于`undefined`,依然是有值的。空位是没有任何值,`in`运算符可以说明这一点。 - -```javascript -0 in [undefined, undefined, undefined] // true -0 in [, , ,] // false -``` - -上面代码说明,第一个数组的 0 号位置是有值的,第二个数组的 0 号位置没有值。 - -ES5 对空位的处理,已经很不一致了,大多数情况下会忽略空位。 - -- `forEach()`, `filter()`, `reduce()`, `every()` 和`some()`都会跳过空位。 -- `map()`会跳过空位,但会保留这个值 -- `join()`和`toString()`会将空位视为`undefined`,而`undefined`和`null`会被处理成空字符串。 - -```javascript -// forEach方法 -[,'a'].forEach((x,i) => console.log(i)); // 1 - -// filter方法 -['a',,'b'].filter(x => true) // ['a','b'] - -// every方法 -[,'a'].every(x => x==='a') // true - -// reduce方法 -[1,,2].reduce((x,y) => x+y) // 3 - -// some方法 -[,'a'].some(x => x !== 'a') // false - -// map方法 -[,'a'].map(x => 1) // [,1] - -// join方法 -[,'a',undefined,null].join('#') // "#a##" - -// toString方法 -[,'a',undefined,null].toString() // ",a,," -``` - -ES6 则是明确将空位转为`undefined`。 - -`Array.from()`方法会将数组的空位,转为`undefined`,也就是说,这个方法不会忽略空位。 - -```javascript -Array.from(['a',,'b']) -// [ "a", undefined, "b" ] -``` - -扩展运算符(`...`)也会将空位转为`undefined`。 - -```javascript -[...['a',,'b']] -// [ "a", undefined, "b" ] -``` - -`copyWithin()`会连空位一起拷贝。 - -```javascript -[,'a','b',,].copyWithin(2,0) // [,"a",,"a"] -``` - -`fill()`会将空位视为正常的数组位置。 - -```javascript -new Array(3).fill('a') // ["a","a","a"] -``` - -`for...of`循环也会遍历空位。 - -```javascript -let arr = [, ,]; -for (let i of arr) { - console.log(1); -} -// 1 -// 1 -``` - -上面代码中,数组`arr`有两个空位,`for...of`并没有忽略它们。如果改成`map()`方法遍历,空位是会跳过的。 - -`entries()`、`keys()`、`values()`、`find()`和`findIndex()`会将空位处理成`undefined`。 - -```javascript -// entries() -[...[,'a'].entries()] // [[0,undefined], [1,"a"]] - -// keys() -[...[,'a'].keys()] // [0,1] - -// values() -[...[,'a'].values()] // [undefined,"a"] - -// find() -[,'a'].find(x => true) // undefined - -// findIndex() -[,'a'].findIndex(x => true) // 0 -``` - -由于空位的处理规则非常不统一,所以建议避免出现空位。 - -## 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/destructuring.md b/docs/destructuring.md index e587ed37c..ca3c163a2 100644 --- a/docs/destructuring.md +++ b/docs/destructuring.md @@ -85,31 +85,6 @@ let [foo] = {}; 上面的语句都会报错,因为等号右边的值,要么转为对象以后不具备 Iterator 接口(前五个表达式),要么本身就不具备 Iterator 接口(最后一个表达式)。 -对于 Set 结构,也可以使用数组的解构赋值。 - -```javascript -let [x, y, z] = new Set(['a', 'b', 'c']); -x // "a" -``` - -事实上,只要某种数据结构具有 Iterator 接口,都可以采用数组形式的解构赋值。 - -```javascript -function* fibs() { - let a = 0; - let b = 1; - while (true) { - yield a; - [a, b] = [b, a + b]; - } -} - -let [first, second, third, fourth, fifth, sixth] = fibs(); -sixth // 5 -``` - -上面代码中,`fibs`是一个 Generator 函数(参见《Generator 函数》一章),原生具有 Iterator 接口。解构赋值会依次从这个接口获取值。 - ### 默认值 解构赋值允许指定默认值。 @@ -360,89 +335,6 @@ x // null 上面代码中,属性`x`等于`null`,因为`null`与`undefined`不严格相等,所以是个有效的赋值,导致默认值`3`不会生效。 -### 注意点 - -(1)如果要将一个已经声明的变量用于解构赋值,必须非常小心。 - -```javascript -// 错误的写法 -let x; -{x} = {x: 1}; -// SyntaxError: syntax error -``` - -上面代码的写法会报错,因为 JavaScript 引擎会将`{x}`理解成一个代码块,从而发生语法错误。只有不将大括号写在行首,避免 JavaScript 将其解释为代码块,才能解决这个问题。 - -```javascript -// 正确的写法 -let x; -({x} = {x: 1}); -``` - -上面代码将整个解构赋值语句,放在一个圆括号里面,就可以正确执行。关于圆括号与解构赋值的关系,参见下文。 - -(2)解构赋值允许等号左边的模式之中,不放置任何变量名。因此,可以写出非常古怪的赋值表达式。 - -```javascript -({} = [true, false]); -({} = 'abc'); -({} = []); -``` - -上面的表达式虽然毫无意义,但是语法是合法的,可以执行。 - -(3)由于数组本质是特殊的对象,因此可以对数组进行对象属性的解构。 - -```javascript -let arr = [1, 2, 3]; -let {0 : first, [arr.length - 1] : last} = arr; -first // 1 -last // 3 -``` - -上面代码对数组进行对象解构。数组`arr`的`0`键对应的值是`1`,`[arr.length - 1]`就是`2`键,对应的值是`3`。方括号这种写法,属于“属性名表达式”(参见《对象的扩展》一章)。 - -## 字符串的解构赋值 - -字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象。 - -```javascript -const [a, b, c, d, e] = 'hello'; -a // "h" -b // "e" -c // "l" -d // "l" -e // "o" -``` - -类似数组的对象都有一个`length`属性,因此还可以对这个属性解构赋值。 - -```javascript -let {length : len} = 'hello'; -len // 5 -``` - -## 数值和布尔值的解构赋值 - -解构赋值时,如果等号右边是数值和布尔值,则会先转为对象。 - -```javascript -let {toString: s} = 123; -s === Number.prototype.toString // true - -let {toString: s} = true; -s === Boolean.prototype.toString // true -``` - -上面代码中,数值和布尔值的包装对象都有`toString`属性,因此变量`s`都能取到值。 - -解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于`undefined`和`null`无法转为对象,所以对它们进行解构赋值,都会报错。 - -```javascript -let { prop: x } = undefined; // TypeError -let { prop: y } = null; // TypeError -``` - ## 函数参数的解构赋值 函数的参数也可以使用解构赋值。 @@ -500,198 +392,3 @@ move(); // [0, 0] [1, undefined, 3].map((x = 'yes') => x); // [ 1, 'yes', 3 ] ``` - -## 圆括号问题 - -解构赋值虽然很方便,但是解析起来并不容易。对于编译器来说,一个式子到底是模式,还是表达式,没有办法从一开始就知道,必须解析到(或解析不到)等号才能知道。 - -由此带来的问题是,如果模式中出现圆括号怎么处理。ES6 的规则是,只要有可能导致解构的歧义,就不得使用圆括号。 - -但是,这条规则实际上不那么容易辨别,处理起来相当麻烦。因此,建议只要有可能,就不要在模式中放置圆括号。 - -### 不能使用圆括号的情况 - -以下三种解构赋值不得使用圆括号。 - -(1)变量声明语句 - -```javascript -// 全部报错 -let [(a)] = [1]; - -let {x: (c)} = {}; -let ({x: c}) = {}; -let {(x: c)} = {}; -let {(x): c} = {}; - -let { o: ({ p: p }) } = { o: { p: 2 } }; -``` - -上面 6 个语句都会报错,因为它们都是变量声明语句,模式不能使用圆括号。 - -(2)函数参数 - -函数参数也属于变量声明,因此不能带有圆括号。 - -```javascript -// 报错 -function f([(z)]) { return z; } -// 报错 -function f([z,(x)]) { return x; } -``` - -(3)赋值语句的模式 - -```javascript -// 全部报错 -({ p: a }) = { p: 42 }; -([a]) = [5]; -``` - -上面代码将整个模式放在圆括号之中,导致报错。 - -```javascript -// 报错 -[({ p: a }), { x: c }] = [{}, {}]; -``` - -上面代码将一部分模式放在圆括号之中,导致报错。 - -### 可以使用圆括号的情况 - -可以使用圆括号的情况只有一种:赋值语句的非模式部分,可以使用圆括号。 - -```javascript -[(b)] = [3]; // 正确 -({ p: (d) } = {}); // 正确 -[(parseInt.prop)] = [3]; // 正确 -``` - -上面三行语句都可以正确执行,因为首先它们都是赋值语句,而不是声明语句;其次它们的圆括号都不属于模式的一部分。第一行语句中,模式是取数组的第一个成员,跟圆括号无关;第二行语句中,模式是`p`,而不是`d`;第三行语句与第一行语句的性质一致。 - -## 用途 - -变量的解构赋值用途很多。 - -**(1)交换变量的值** - -```javascript -let x = 1; -let y = 2; - -[x, y] = [y, x]; -``` - -上面代码交换变量`x`和`y`的值,这样的写法不仅简洁,而且易读,语义非常清晰。 - -**(2)从函数返回多个值** - -函数只能返回一个值,如果要返回多个值,只能将它们放在数组或对象里返回。有了解构赋值,取出这些值就非常方便。 - -```javascript -// 返回一个数组 - -function example() { - return [1, 2, 3]; -} -let [a, b, c] = example(); - -// 返回一个对象 - -function example() { - return { - foo: 1, - bar: 2 - }; -} -let { foo, bar } = example(); -``` - -**(3)函数参数的定义** - -解构赋值可以方便地将一组参数与变量名对应起来。 - -```javascript -// 参数是一组有次序的值 -function f([x, y, z]) { ... } -f([1, 2, 3]); - -// 参数是一组无次序的值 -function f({x, y, z}) { ... } -f({z: 3, y: 2, x: 1}); -``` - -**(4)提取 JSON 数据** - -解构赋值对提取 JSON 对象中的数据,尤其有用。 - -```javascript -let jsonData = { - id: 42, - status: "OK", - data: [867, 5309] -}; - -let { id, status, data: number } = jsonData; - -console.log(id, status, number); -// 42, "OK", [867, 5309] -``` - -上面代码可以快速提取 JSON 数据的值。 - -**(5)函数参数的默认值** - -```javascript -jQuery.ajax = function (url, { - async = true, - beforeSend = function () {}, - cache = true, - complete = function () {}, - crossDomain = false, - global = true, - // ... more config -} = {}) { - // ... do stuff -}; -``` - -指定参数的默认值,就避免了在函数体内部再写`var foo = config.foo || 'default foo';`这样的语句。 - -**(6)遍历 Map 结构** - -任何部署了 Iterator 接口的对象,都可以用`for...of`循环遍历。Map 结构原生支持 Iterator 接口,配合变量的解构赋值,获取键名和键值就非常方便。 - -```javascript -const map = new Map(); -map.set('first', 'hello'); -map.set('second', 'world'); - -for (let [key, value] of map) { - console.log(key + " is " + value); -} -// first is hello -// second is world -``` - -如果只想获取键名,或者只想获取键值,可以写成下面这样。 - -```javascript -// 获取键名 -for (let [key] of map) { - // ... -} - -// 获取键值 -for (let [,value] of map) { - // ... -} -``` - -**(7)输入模块的指定方法** - -加载模块时,往往需要指定输入哪些方法。解构赋值使得输入语句非常清晰。 - -```javascript -const { SourceMapConsumer, SourceNode } = require("source-map"); -``` diff --git a/docs/function.md b/docs/function.md index 99695e420..c5727541c 100644 --- a/docs/function.md +++ b/docs/function.md @@ -194,213 +194,6 @@ m1({z: 3}) // [0, 0] m2({z: 3}) // [undefined, undefined] ``` -### 参数默认值的位置 - -通常情况下,定义了默认值的参数,应该是函数的尾参数。因为这样比较容易看出来,到底省略了哪些参数。如果非尾部的参数设置默认值,实际上这个参数是没法省略的。 - -```javascript -// 例一 -function f(x = 1, y) { - return [x, y]; -} - -f() // [1, undefined] -f(2) // [2, undefined] -f(, 1) // 报错 -f(undefined, 1) // [1, 1] - -// 例二 -function f(x, y = 5, z) { - return [x, y, z]; -} - -f() // [undefined, 5, undefined] -f(1) // [1, 5, undefined] -f(1, ,2) // 报错 -f(1, undefined, 2) // [1, 5, 2] -``` - -上面代码中,有默认值的参数都不是尾参数。这时,无法只省略该参数,而不省略它后面的参数,除非显式输入`undefined`。 - -如果传入`undefined`,将触发该参数等于默认值,`null`则没有这个效果。 - -```javascript -function foo(x = 5, y = 6) { - console.log(x, y); -} - -foo(undefined, null) -// 5 null -``` - -上面代码中,`x`参数对应`undefined`,结果触发了默认值,`y`参数等于`null`,就没有触发默认值。 - -### 函数的 length 属性 - -指定了默认值以后,函数的`length`属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,`length`属性将失真。 - -```javascript -(function (a) {}).length // 1 -(function (a = 5) {}).length // 0 -(function (a, b, c = 5) {}).length // 2 -``` - -上面代码中,`length`属性的返回值,等于函数的参数个数减去指定了默认值的参数个数。比如,上面最后一个函数,定义了 3 个参数,其中有一个参数`c`指定了默认值,因此`length`属性等于`3`减去`1`,最后得到`2`。 - -这是因为`length`属性的含义是,该函数预期传入的参数个数。某个参数指定默认值以后,预期传入的参数个数就不包括这个参数了。同理,后文的 rest 参数也不会计入`length`属性。 - -```javascript -(function(...args) {}).length // 0 -``` - -如果设置了默认值的参数不是尾参数,那么`length`属性也不再计入后面的参数了。 - -```javascript -(function (a = 0, b, c) {}).length // 0 -(function (a, b = 1, c) {}).length // 1 -``` - -### 作用域 - -一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的。 - -```javascript -var x = 1; - -function f(x, y = x) { - console.log(y); -} - -f(2) // 2 -``` - -上面代码中,参数`y`的默认值等于变量`x`。调用函数`f`时,参数形成一个单独的作用域。在这个作用域里面,默认值变量`x`指向第一个参数`x`,而不是全局变量`x`,所以输出是`2`。 - -再看下面的例子。 - -```javascript -let x = 1; - -function f(y = x) { - let x = 2; - console.log(y); -} - -f() // 1 -``` - -上面代码中,函数`f`调用时,参数`y = x`形成一个单独的作用域。这个作用域里面,变量`x`本身没有定义,所以指向外层的全局变量`x`。函数调用时,函数体内部的局部变量`x`影响不到默认值变量`x`。 - -如果此时,全局变量`x`不存在,就会报错。 - -```javascript -function f(y = x) { - let x = 2; - console.log(y); -} - -f() // ReferenceError: x is not defined -``` - -下面这样写,也会报错。 - -```javascript -var x = 1; - -function foo(x = x) { - // ... -} - -foo() // ReferenceError: Cannot access 'x' before initialization -``` - -上面代码中,参数`x = x`形成一个单独作用域。实际执行的是`let x = x`,由于暂时性死区的原因,这行代码会报错。 - -如果参数的默认值是一个函数,该函数的作用域也遵守这个规则。请看下面的例子。 - -```javascript -let foo = 'outer'; - -function bar(func = () => foo) { - let foo = 'inner'; - console.log(func()); -} - -bar(); // outer -``` - -上面代码中,函数`bar`的参数`func`的默认值是一个匿名函数,返回值为变量`foo`。函数参数形成的单独作用域里面,并没有定义变量`foo`,所以`foo`指向外层的全局变量`foo`,因此输出`outer`。 - -如果写成下面这样,就会报错。 - -```javascript -function bar(func = () => foo) { - let foo = 'inner'; - console.log(func()); -} - -bar() // ReferenceError: foo is not defined -``` - -上面代码中,匿名函数里面的`foo`指向函数外层,但是函数外层并没有声明变量`foo`,所以就报错了。 - -下面是一个更复杂的例子。 - -```javascript -var x = 1; -function foo(x, y = function() { x = 2; }) { - var x = 3; - y(); - console.log(x); -} - -foo() // 3 -x // 1 -``` - -上面代码中,函数`foo`的参数形成一个单独作用域。这个作用域里面,首先声明了变量`x`,然后声明了变量`y`,`y`的默认值是一个匿名函数。这个匿名函数内部的变量`x`,指向同一个作用域的第一个参数`x`。函数`foo`内部又声明了一个内部变量`x`,该变量与第一个参数`x`由于不是同一个作用域,所以不是同一个变量,因此执行`y`后,内部变量`x`和外部全局变量`x`的值都没变。 - -如果将`var x = 3`的`var`去除,函数`foo`的内部变量`x`就指向第一个参数`x`,与匿名函数内部的`x`是一致的,所以最后输出的就是`2`,而外层的全局变量`x`依然不受影响。 - -```javascript -var x = 1; -function foo(x, y = function() { x = 2; }) { - x = 3; - y(); - console.log(x); -} - -foo() // 2 -x // 1 -``` - -### 应用 - -利用参数默认值,可以指定某一个参数不得省略,如果省略就抛出一个错误。 - -```javascript -function throwIfMissing() { - throw new Error('Missing parameter'); -} - -function foo(mustBeProvided = throwIfMissing()) { - return mustBeProvided; -} - -foo() -// Error: Missing parameter -``` - -上面代码的`foo`函数,如果调用的时候没有参数,就会调用默认值`throwIfMissing`函数,从而抛出一个错误。 - -从上面代码还可以看到,参数`mustBeProvided`的默认值等于`throwIfMissing`函数的运行结果(注意函数名`throwIfMissing`之后有一对圆括号),这表明参数的默认值不是在定义时执行,而是在运行时执行。如果参数已经赋值,默认值中的函数就不会运行。 - -另外,可以将参数默认值设为`undefined`,表明这个参数是可以省略的。 - -```javascript -function foo(optional = undefined) { ··· } -``` - ## rest 参数 ES6 引入 rest 参数(形式为`...变量名`),用于获取函数的多余参数,这样就不需要使用`arguments`对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。 @@ -542,58 +335,6 @@ const doSomething = (function () { }()); ``` -## name 属性 - -函数的`name`属性,返回该函数的函数名。 - -```javascript -function foo() {} -foo.name // "foo" -``` - -这个属性早就被浏览器广泛支持,但是直到 ES6,才将其写入了标准。 - -需要注意的是,ES6 对这个属性的行为做出了一些修改。如果将一个匿名函数赋值给一个变量,ES5 的`name`属性,会返回空字符串,而 ES6 的`name`属性会返回实际的函数名。 - -```javascript -var f = function () {}; - -// ES5 -f.name // "" - -// ES6 -f.name // "f" -``` - -上面代码中,变量`f`等于一个匿名函数,ES5 和 ES6 的`name`属性返回的值不一样。 - -如果将一个具名函数赋值给一个变量,则 ES5 和 ES6 的`name`属性都返回这个具名函数原本的名字。 - -```javascript -const bar = function baz() {}; - -// ES5 -bar.name // "baz" - -// ES6 -bar.name // "baz" -``` - -`Function`构造函数返回的函数实例,`name`属性的值为`anonymous`。 - -```javascript -(new Function).name // "anonymous" -``` - -`bind`返回的函数,`name`属性值会加上`bound`前缀。 - -```javascript -function foo() {}; -foo.bind({}).name // "bound foo" - -(function(){}).bind({}).name // "bound " -``` - ## 箭头函数 ### 基本用法 @@ -983,420 +724,3 @@ var fix = f => (x => f(v => x(x)(v))) ``` 上面两种写法,几乎是一一对应的。由于 λ 演算对于计算机科学非常重要,这使得我们可以用 ES6 作为替代工具,探索计算机科学。 - -## 尾调用优化 - -### 什么是尾调用? - -尾调用(Tail Call)是函数式编程的一个重要概念,本身非常简单,一句话就能说清楚,就是指某个函数的最后一步是调用另一个函数。 - -```javascript -function f(x){ - return g(x); -} -``` - -上面代码中,函数`f`的最后一步是调用函数`g`,这就叫尾调用。 - -以下三种情况,都不属于尾调用。 - -```javascript -// 情况一 -function f(x){ - let y = g(x); - return y; -} - -// 情况二 -function f(x){ - return g(x) + 1; -} - -// 情况三 -function f(x){ - g(x); -} -``` - -上面代码中,情况一是调用函数`g`之后,还有赋值操作,所以不属于尾调用,即使语义完全一样。情况二也属于调用后还有操作,即使写在一行内。情况三等同于下面的代码。 - -```javascript -function f(x){ - g(x); - return undefined; -} -``` - -尾调用不一定出现在函数尾部,只要是最后一步操作即可。 - -```javascript -function f(x) { - if (x > 0) { - return m(x) - } - return n(x); -} -``` - -上面代码中,函数`m`和`n`都属于尾调用,因为它们都是函数`f`的最后一步操作。 - -### 尾调用优化 - -尾调用之所以与其他调用不同,就在于它的特殊的调用位置。 - -我们知道,函数调用会在内存形成一个“调用记录”,又称“调用帧”(call frame),保存调用位置和内部变量等信息。如果在函数`A`的内部调用函数`B`,那么在`A`的调用帧上方,还会形成一个`B`的调用帧。等到`B`运行结束,将结果返回到`A`,`B`的调用帧才会消失。如果函数`B`内部还调用函数`C`,那就还有一个`C`的调用帧,以此类推。所有的调用帧,就形成一个“调用栈”(call stack)。 - -尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了。 - -```javascript -function f() { - let m = 1; - let n = 2; - return g(m + n); -} -f(); - -// 等同于 -function f() { - return g(3); -} -f(); - -// 等同于 -g(3); -``` - -上面代码中,如果函数`g`不是尾调用,函数`f`就需要保存内部变量`m`和`n`的值、`g`的调用位置等信息。但由于调用`g`之后,函数`f`就结束了,所以执行到最后一步,完全可以删除`f(x)`的调用帧,只保留`g(3)`的调用帧。 - -这就叫做“尾调用优化”(Tail call optimization),即只保留内层函数的调用帧。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用帧只有一项,这将大大节省内存。这就是“尾调用优化”的意义。 - -注意,只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧,否则就无法进行“尾调用优化”。 - -```javascript -function addOne(a){ - var one = 1; - function inner(b){ - return b + one; - } - return inner(a); -} -``` - -上面的函数不会进行尾调用优化,因为内层函数`inner`用到了外层函数`addOne`的内部变量`one`。 - -注意,目前只有 Safari 浏览器支持尾调用优化,Chrome 和 Firefox 都不支持。 - -### 尾递归 - -函数调用自身,称为递归。如果尾调用自身,就称为尾递归。 - -递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)。但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。 - -```javascript -function factorial(n) { - if (n === 1) return 1; - return n * factorial(n - 1); -} - -factorial(5) // 120 -``` - -上面代码是一个阶乘函数,计算`n`的阶乘,最多需要保存`n`个调用记录,复杂度 O(n) 。 - -如果改写成尾递归,只保留一个调用记录,复杂度 O(1) 。 - -```javascript -function factorial(n, total) { - if (n === 1) return total; - return factorial(n - 1, n * total); -} - -factorial(5, 1) // 120 -``` - -还有一个比较著名的例子,就是计算 Fibonacci 数列,也能充分说明尾递归优化的重要性。 - -非尾递归的 Fibonacci 数列实现如下。 - -```javascript -function Fibonacci (n) { - if ( n <= 1 ) {return 1}; - - return Fibonacci(n - 1) + Fibonacci(n - 2); -} - -Fibonacci(10) // 89 -Fibonacci(100) // 超时 -Fibonacci(500) // 超时 -``` - -尾递归优化过的 Fibonacci 数列实现如下。 - -```javascript -function Fibonacci2 (n , ac1 = 1 , ac2 = 1) { - if( n <= 1 ) {return ac2}; - - return Fibonacci2 (n - 1, ac2, ac1 + ac2); -} - -Fibonacci2(100) // 573147844013817200000 -Fibonacci2(1000) // 7.0330367711422765e+208 -Fibonacci2(10000) // Infinity -``` - -由此可见,“尾调用优化”对递归操作意义重大,所以一些函数式编程语言将其写入了语言规格。ES6 亦是如此,第一次明确规定,所有 ECMAScript 的实现,都必须部署“尾调用优化”。这就是说,ES6 中只要使用尾递归,就不会发生栈溢出(或者层层递归造成的超时),相对节省内存。 - -### 递归函数的改写 - -尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身。做到这一点的方法,就是把所有用到的内部变量改写成函数的参数。比如上面的例子,阶乘函数 factorial 需要用到一个中间变量`total`,那就把这个中间变量改写成函数的参数。这样做的缺点就是不太直观,第一眼很难看出来,为什么计算`5`的阶乘,需要传入两个参数`5`和`1`? - -两个方法可以解决这个问题。方法一是在尾递归函数之外,再提供一个正常形式的函数。 - -```javascript -function tailFactorial(n, total) { - if (n === 1) return total; - return tailFactorial(n - 1, n * total); -} - -function factorial(n) { - return tailFactorial(n, 1); -} - -factorial(5) // 120 -``` - -上面代码通过一个正常形式的阶乘函数`factorial`,调用尾递归函数`tailFactorial`,看起来就正常多了。 - -函数式编程有一个概念,叫做柯里化(currying),意思是将多参数的函数转换成单参数的形式。这里也可以使用柯里化。 - -```javascript -function currying(fn, n) { - return function (m) { - return fn.call(this, m, n); - }; -} - -function tailFactorial(n, total) { - if (n === 1) return total; - return tailFactorial(n - 1, n * total); -} - -const factorial = currying(tailFactorial, 1); - -factorial(5) // 120 -``` - -上面代码通过柯里化,将尾递归函数`tailFactorial`变为只接受一个参数的`factorial`。 - -第二种方法就简单多了,就是采用 ES6 的函数默认值。 - -```javascript -function factorial(n, total = 1) { - if (n === 1) return total; - return factorial(n - 1, n * total); -} - -factorial(5) // 120 -``` - -上面代码中,参数`total`有默认值`1`,所以调用时不用提供这个值。 - -总结一下,递归本质上是一种循环操作。纯粹的函数式编程语言没有循环操作命令,所有的循环都用递归实现,这就是为什么尾递归对这些语言极其重要。对于其他支持“尾调用优化”的语言(比如 Lua,ES6),只需要知道循环可以用递归代替,而一旦使用递归,就最好使用尾递归。 - -### 严格模式 - -ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。 - -这是因为在正常模式下,函数内部有两个变量,可以跟踪函数的调用栈。 - -- `func.arguments`:返回调用时函数的参数。 -- `func.caller`:返回调用当前函数的那个函数。 - -尾调用优化发生时,函数的调用栈会改写,因此上面两个变量就会失真。严格模式禁用这两个变量,所以尾调用模式仅在严格模式下生效。 - -```javascript -function restricted() { - 'use strict'; - restricted.caller; // 报错 - restricted.arguments; // 报错 -} -restricted(); -``` - -### 尾递归优化的实现 - -尾递归优化只在严格模式下生效,那么正常模式下,或者那些不支持该功能的环境中,有没有办法也使用尾递归优化呢?回答是可以的,就是自己实现尾递归优化。 - -它的原理非常简单。尾递归之所以需要优化,原因是调用栈太多,造成溢出,那么只要减少调用栈,就不会溢出。怎么做可以减少调用栈呢?就是采用“循环”换掉“递归”。 - -下面是一个正常的递归函数。 - -```javascript -function sum(x, y) { - if (y > 0) { - return sum(x + 1, y - 1); - } else { - return x; - } -} - -sum(1, 100000) -// Uncaught RangeError: Maximum call stack size exceeded(…) -``` - -上面代码中,`sum`是一个递归函数,参数`x`是需要累加的值,参数`y`控制递归次数。一旦指定`sum`递归 100000 次,就会报错,提示超出调用栈的最大次数。 - -蹦床函数(trampoline)可以将递归执行转为循环执行。 - -```javascript -function trampoline(f) { - while (f && f instanceof Function) { - f = f(); - } - return f; -} -``` - -上面就是蹦床函数的一个实现,它接受一个函数`f`作为参数。只要`f`执行后返回一个函数,就继续执行。注意,这里是返回一个函数,然后执行该函数,而不是函数里面调用函数,这样就避免了递归执行,从而就消除了调用栈过大的问题。 - -然后,要做的就是将原来的递归函数,改写为每一步返回另一个函数。 - -```javascript -function sum(x, y) { - if (y > 0) { - return sum.bind(null, x + 1, y - 1); - } else { - return x; - } -} -``` - -上面代码中,`sum`函数的每次执行,都会返回自身的另一个版本。 - -现在,使用蹦床函数执行`sum`,就不会发生调用栈溢出。 - -```javascript -trampoline(sum(1, 100000)) -// 100001 -``` - -蹦床函数并不是真正的尾递归优化,下面的实现才是。 - -```javascript -function tco(f) { - var value; - var active = false; - var accumulated = []; - - return function accumulator() { - accumulated.push(arguments); - if (!active) { - active = true; - while (accumulated.length) { - value = f.apply(this, accumulated.shift()); - } - active = false; - return value; - } - }; -} - -var sum = tco(function(x, y) { - if (y > 0) { - return sum(x + 1, y - 1) - } - else { - return x - } -}); - -sum(1, 100000) -// 100001 -``` - -上面代码中,`tco`函数是尾递归优化的实现,它的奥妙就在于状态变量`active`。默认情况下,这个变量是不激活的。一旦进入尾递归优化的过程,这个变量就激活了。然后,每一轮递归`sum`返回的都是`undefined`,所以就避免了递归执行;而`accumulated`数组存放每一轮`sum`执行的参数,总是有值的,这就保证了`accumulator`函数内部的`while`循环总是会执行。这样就很巧妙地将“递归”改成了“循环”,而后一轮的参数会取代前一轮的参数,保证了调用栈只有一层。 - -## 函数参数的尾逗号 - -ES2017 [允许](https://github.com/jeffmo/es-trailing-function-commas)函数的最后一个参数有尾逗号(trailing comma)。 - -此前,函数定义和调用时,都不允许最后一个参数后面出现逗号。 - -```javascript -function clownsEverywhere( - param1, - param2 -) { /* ... */ } - -clownsEverywhere( - 'foo', - 'bar' -); -``` - -上面代码中,如果在`param2`或`bar`后面加一个逗号,就会报错。 - -如果像上面这样,将参数写成多行(即每个参数占据一行),以后修改代码的时候,想为函数`clownsEverywhere`添加第三个参数,或者调整参数的次序,就势必要在原来最后一个参数后面添加一个逗号。这对于版本管理系统来说,就会显示添加逗号的那一行也发生了变动。这看上去有点冗余,因此新的语法允许定义和调用时,尾部直接有一个逗号。 - -```javascript -function clownsEverywhere( - param1, - param2, -) { /* ... */ } - -clownsEverywhere( - 'foo', - 'bar', -); -``` - -这样的规定也使得,函数参数与数组和对象的尾逗号规则,保持一致了。 - -## Function.prototype.toString() - -[ES2019](https://github.com/tc39/Function-prototype-toString-revision) 对函数实例的`toString()`方法做出了修改。 - -`toString()`方法返回函数代码本身,以前会省略注释和空格。 - -```javascript -function /* foo comment */ foo () {} - -foo.toString() -// function foo() {} -``` - -上面代码中,函数`foo`的原始代码包含注释,函数名`foo`和圆括号之间有空格,但是`toString()`方法都把它们省略了。 - -修改后的`toString()`方法,明确要求返回一模一样的原始代码。 - -```javascript -function /* foo comment */ foo () {} - -foo.toString() -// "function /* foo comment */ foo () {}" -``` - -## catch 命令的参数省略 - -JavaScript 语言的`try...catch`结构,以前明确要求`catch`命令后面必须跟参数,接受`try`代码块抛出的错误对象。 - -```javascript -try { - // ... -} catch (err) { - // 处理错误 -} -``` - -上面代码中,`catch`命令后面带有参数`err`。 - -很多时候,`catch`代码块可能用不到这个参数。但是,为了保证语法正确,还是必须写。[ES2019](https://github.com/tc39/proposal-optional-catch-binding) 做出了改变,允许`catch`语句省略参数。 - -```javascript -try { - // ... -} catch { - // ... -} -``` - diff --git a/docs/object.md b/docs/object.md index cd817ec1e..b05abea9f 100644 --- a/docs/object.md +++ b/docs/object.md @@ -158,308 +158,6 @@ new obj.f() // 报错 上面代码中,`f`是一个简写的对象方法,所以`obj.f`不能当作构造函数使用。 -## 属性名表达式 - -JavaScript 定义对象的属性,有两种方法。 - -```javascript -// 方法一 -obj.foo = true; - -// 方法二 -obj['a' + 'bc'] = 123; -``` - -上面代码的方法一是直接用标识符作为属性名,方法二是用表达式作为属性名,这时要将表达式放在方括号之内。 - -但是,如果使用字面量方式定义对象(使用大括号),在 ES5 中只能使用方法一(标识符)定义属性。 - -```javascript -var obj = { - foo: true, - abc: 123 -}; -``` - -ES6 允许字面量定义对象时,用方法二(表达式)作为对象的属性名,即把表达式放在方括号内。 - -```javascript -let propKey = 'foo'; - -let obj = { - [propKey]: true, - ['a' + 'bc']: 123 -}; -``` - -下面是另一个例子。 - -```javascript -let lastWord = 'last word'; - -const a = { - 'first word': 'hello', - [lastWord]: 'world' -}; - -a['first word'] // "hello" -a[lastWord] // "world" -a['last word'] // "world" -``` - -表达式还可以用于定义方法名。 - -```javascript -let obj = { - ['h' + 'ello']() { - return 'hi'; - } -}; - -obj.hello() // hi -``` - -注意,属性名表达式与简洁表示法,不能同时使用,会报错。 - -```javascript -// 报错 -const foo = 'bar'; -const bar = 'abc'; -const baz = { [foo] }; - -// 正确 -const foo = 'bar'; -const baz = { [foo]: 'abc'}; -``` - -注意,属性名表达式如果是一个对象,默认情况下会自动将对象转为字符串`[object Object]`,这一点要特别小心。 - -```javascript -const keyA = {a: 1}; -const keyB = {b: 2}; - -const myObject = { - [keyA]: 'valueA', - [keyB]: 'valueB' -}; - -myObject // Object {[object Object]: "valueB"} -``` - -上面代码中,`[keyA]`和`[keyB]`得到的都是`[object Object]`,所以`[keyB]`会把`[keyA]`覆盖掉,而`myObject`最后只有一个`[object Object]`属性。 - -## 方法的 name 属性 - -函数的`name`属性,返回函数名。对象方法也是函数,因此也有`name`属性。 - -```javascript -const person = { - sayName() { -    console.log('hello!'); - }, -}; - -person.sayName.name // "sayName" -``` - -上面代码中,方法的`name`属性返回函数名(即方法名)。 - -如果对象的方法使用了取值函数(`getter`)和存值函数(`setter`),则`name`属性不是在该方法上面,而是该方法的属性的描述对象的`get`和`set`属性上面,返回值是方法名前加上`get`和`set`。 - -```javascript -const obj = { - get foo() {}, - set foo(x) {} -}; - -obj.foo.name -// TypeError: Cannot read property 'name' of undefined - -const descriptor = Object.getOwnPropertyDescriptor(obj, 'foo'); - -descriptor.get.name // "get foo" -descriptor.set.name // "set foo" -``` - -有两种特殊情况:`bind`方法创造的函数,`name`属性返回`bound`加上原函数的名字;`Function`构造函数创造的函数,`name`属性返回`anonymous`。 - -```javascript -(new Function()).name // "anonymous" - -var doSomething = function() { - // ... -}; -doSomething.bind().name // "bound doSomething" -``` - -如果对象的方法是一个 Symbol 值,那么`name`属性返回的是这个 Symbol 值的描述。 - -```javascript -const key1 = Symbol('description'); -const key2 = Symbol(); -let obj = { - [key1]() {}, - [key2]() {}, -}; -obj[key1].name // "[description]" -obj[key2].name // "" -``` - -上面代码中,`key1`对应的 Symbol 值有描述,`key2`没有。 - -## 属性的可枚举性和遍历 - -### 可枚举性 - -对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为。`Object.getOwnPropertyDescriptor`方法可以获取该属性的描述对象。 - -```javascript -let obj = { foo: 123 }; -Object.getOwnPropertyDescriptor(obj, 'foo') -// { -// value: 123, -// writable: true, -// enumerable: true, -// configurable: true -// } -``` - -描述对象的`enumerable`属性,称为“可枚举性”,如果该属性为`false`,就表示某些操作会忽略当前属性。 - -目前,有四个操作会忽略`enumerable`为`false`的属性。 - -- `for...in`循环:只遍历对象自身的和继承的可枚举的属性。 -- `Object.keys()`:返回对象自身的所有可枚举的属性的键名。 -- `JSON.stringify()`:只串行化对象自身的可枚举的属性。 -- `Object.assign()`: 忽略`enumerable`为`false`的属性,只拷贝对象自身的可枚举的属性。 - -这四个操作之中,前三个是 ES5 就有的,最后一个`Object.assign()`是 ES6 新增的。其中,只有`for...in`会返回继承的属性,其他三个方法都会忽略继承的属性,只处理对象自身的属性。实际上,引入“可枚举”(`enumerable`)这个概念的最初目的,就是让某些属性可以规避掉`for...in`操作,不然所有内部属性和方法都会被遍历到。比如,对象原型的`toString`方法,以及数组的`length`属性,就通过“可枚举性”,从而避免被`for...in`遍历到。 - -```javascript -Object.getOwnPropertyDescriptor(Object.prototype, 'toString').enumerable -// false - -Object.getOwnPropertyDescriptor([], 'length').enumerable -// false -``` - -上面代码中,`toString`和`length`属性的`enumerable`都是`false`,因此`for...in`不会遍历到这两个继承自原型的属性。 - -另外,ES6 规定,所有 Class 的原型的方法都是不可枚举的。 - -```javascript -Object.getOwnPropertyDescriptor(class {foo() {}}.prototype, 'foo').enumerable -// false -``` - -总的来说,操作中引入继承的属性会让问题复杂化,大多数时候,我们只关心对象自身的属性。所以,尽量不要用`for...in`循环,而用`Object.keys()`代替。 - -### 属性的遍历 - -ES6 一共有 5 种方法可以遍历对象的属性。 - -**(1)for...in** - -`for...in`循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)。 - -**(2)Object.keys(obj)** - -`Object.keys`返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名。 - -**(3)Object.getOwnPropertyNames(obj)** - -`Object.getOwnPropertyNames`返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名。 - -**(4)Object.getOwnPropertySymbols(obj)** - -`Object.getOwnPropertySymbols`返回一个数组,包含对象自身的所有 Symbol 属性的键名。 - -**(5)Reflect.ownKeys(obj)** - -`Reflect.ownKeys`返回一个数组,包含对象自身的(不含继承的)所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。 - -以上的 5 种方法遍历对象的键名,都遵守同样的属性遍历的次序规则。 - -- 首先遍历所有数值键,按照数值升序排列。 -- 其次遍历所有字符串键,按照加入时间升序排列。 -- 最后遍历所有 Symbol 键,按照加入时间升序排列。 - -```javascript -Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 }) -// ['2', '10', 'b', 'a', Symbol()] -``` - -上面代码中,`Reflect.ownKeys`方法返回一个数组,包含了参数对象的所有属性。这个数组的属性次序是这样的,首先是数值属性`2`和`10`,其次是字符串属性`b`和`a`,最后是 Symbol 属性。 - -## super 关键字 - -我们知道,`this`关键字总是指向函数所在的当前对象,ES6 又新增了另一个类似的关键字`super`,指向当前对象的原型对象。 - -```javascript -const proto = { - foo: 'hello' -}; - -const obj = { - foo: 'world', - find() { - return super.foo; - } -}; - -Object.setPrototypeOf(obj, proto); -obj.find() // "hello" -``` - -上面代码中,对象`obj.find()`方法之中,通过`super.foo`引用了原型对象`proto`的`foo`属性。 - -注意,`super`关键字表示原型对象时,只能用在对象的方法之中,用在其他地方都会报错。 - -```javascript -// 报错 -const obj = { - foo: super.foo -} - -// 报错 -const obj = { - foo: () => super.foo -} - -// 报错 -const obj = { - foo: function () { - return super.foo - } -} -``` - -上面三种`super`的用法都会报错,因为对于 JavaScript 引擎来说,这里的`super`都没有用在对象的方法之中。第一种写法是`super`用在属性里面,第二种和第三种写法是`super`用在一个函数里面,然后赋值给`foo`属性。目前,只有对象方法的简写法可以让 JavaScript 引擎确认,定义的是对象的方法。 - -JavaScript 引擎内部,`super.foo`等同于`Object.getPrototypeOf(this).foo`(属性)或`Object.getPrototypeOf(this).foo.call(this)`(方法)。 - -```javascript -const proto = { - x: 'hello', - foo() { - console.log(this.x); - }, -}; - -const obj = { - x: 'world', - foo() { - super.foo(); - } -} - -Object.setPrototypeOf(obj, proto); - -obj.foo() // "world" -``` - -上面代码中,`super.foo`指向原型对象`proto`的`foo`方法,但是绑定的`this`却还是当前对象`obj`,因此输出的就是`world`。 - ## 对象的扩展运算符 《数组的扩展》一章中,已经介绍过扩展运算符(`...`)。ES2018 将这个运算符[引入](https://github.com/sebmarkbage/ecmascript-rest-spread)了对象。 @@ -720,78 +418,3 @@ let aWithXGetter = { ...a }; // 报错 ``` 上面例子中,取值函数`get`在扩展`a`对象时会自动执行,导致报错。 - -## AggregateError 错误对象 - -ES2021 标准之中,为了配合新增的`Promise.any()`方法(详见《Promise 对象》一章),还引入一个新的错误对象`AggregateError`,也放在这一章介绍。 - -AggregateError 在一个错误对象里面,封装了多个错误。如果某个单一操作,同时引发了多个错误,需要同时抛出这些错误,那么就可以抛出一个 AggregateError 错误对象,把各种错误都放在这个对象里面。 - -AggregateError 本身是一个构造函数,用来生成 AggregateError 实例对象。 - -```javascript -AggregateError(errors[, message]) -``` - -`AggregateError()`构造函数可以接受两个参数。 - -- errors:数组,它的每个成员都是一个错误对象。该参数是必须的。 -- message:字符串,表示 AggregateError 抛出时的提示信息。该参数是可选的。 - -```javascript -const error = new AggregateError([ - new Error('ERROR_11112'), - new TypeError('First name must be a string'), - new RangeError('Transaction value must be at least 1'), - new URIError('User profile link must be https'), -], 'Transaction cannot be processed') -``` - -上面示例中,`AggregateError()`的第一个参数数组里面,一共有四个错误实例。第二个参数字符串则是这四个错误的一个整体的提示。 - -`AggregateError`的实例对象有三个属性。 - -- name:错误名称,默认为“AggregateError”。 -- message:错误的提示信息。 -- errors:数组,每个成员都是一个错误对象。 - -下面是一个示例。 - -```javascript -try { - throw new AggregateError([ - new Error("some error"), - ], 'Hello'); -} catch (e) { - console.log(e instanceof AggregateError); // true - console.log(e.message); // "Hello" - console.log(e.name); // "AggregateError" - console.log(e.errors); // [ Error: "some error" ] -} -``` - -## Error 对象的 cause 属性 - -Error 对象用来表示代码运行时的异常情况,但是从这个对象拿到的上下文信息,有时很难解读,也不够充分。[ES2022](https://github.com/tc39/proposal-error-cause) 为 Error 对象添加了一个`cause`属性,可以在生成错误时,添加报错原因的描述。 - -它的用法是`new Error()`生成 Error 实例时,给出一个描述对象,该对象可以设置`cause`属性。 - -```javascript -const actual = new Error('an error!', { cause: 'Error cause' }); -actual.cause; // 'Error cause' -``` - -上面示例中,生成 Error 实例时,使用描述对象给出`cause`属性,写入报错的原因。然后,就可以从实例对象上读取这个属性。 - -`casue`属性可以放置任意内容,不必一定是字符串。 - -```javascript -try { - maybeWorks(); -} catch (err) { - throw new Error('maybeWorks failed!', { cause: err }); -} -``` - -上面示例中,`cause`属性放置的就是一个对象。 - diff --git a/docs/string.md b/docs/string.md index c311b860f..f5e780d4d 100644 --- a/docs/string.md +++ b/docs/string.md @@ -1,142 +1,5 @@ # 字符串的扩展 -本章介绍 ES6 对字符串的改造和增强,下一章介绍字符串对象的新增方法。 - -## 字符的 Unicode 表示法 - -ES6 加强了对 Unicode 的支持,允许采用`\uxxxx`形式表示一个字符,其中`xxxx`表示字符的 Unicode 码点。 - -```javascript -"\u0061" -// "a" -``` - -但是,这种表示法只限于码点在`\u0000`~`\uFFFF`之间的字符。超出这个范围的字符,必须用两个双字节的形式表示。 - -```javascript -"\uD842\uDFB7" -// "𠮷" - -"\u20BB7" -// " 7" -``` - -上面代码表示,如果直接在`\u`后面跟上超过`0xFFFF`的数值(比如`\u20BB7`),JavaScript 会理解成`\u20BB+7`。由于`\u20BB`是一个不可打印字符,所以只会显示一个空格,后面跟着一个`7`。 - -ES6 对这一点做出了改进,只要将码点放入大括号,就能正确解读该字符。 - -```javascript -"\u{20BB7}" -// "𠮷" - -"\u{41}\u{42}\u{43}" -// "ABC" - -let hello = 123; -hell\u{6F} // 123 - -'\u{1F680}' === '\uD83D\uDE80' -// true -``` - -上面代码中,最后一个例子表明,大括号表示法与四字节的 UTF-16 编码是等价的。 - -有了这种表示法之后,JavaScript 共有 6 种方法可以表示一个字符。 - -```javascript -'\z' === 'z' // true -'\172' === 'z' // true -'\x7A' === 'z' // true -'\u007A' === 'z' // true -'\u{7A}' === 'z' // true -``` - -## 字符串的遍历器接口 - -ES6 为字符串添加了遍历器接口(详见《Iterator》一章),使得字符串可以被`for...of`循环遍历。 - -```javascript -for (let codePoint of 'foo') { - console.log(codePoint) -} -// "f" -// "o" -// "o" -``` - -除了遍历字符串,这个遍历器最大的优点是可以识别大于`0xFFFF`的码点,传统的`for`循环无法识别这样的码点。 - -```javascript -let text = String.fromCodePoint(0x20BB7); - -for (let i = 0; i < text.length; i++) { - console.log(text[i]); -} -// " " -// " " - -for (let i of text) { - console.log(i); -} -// "𠮷" -``` - -上面代码中,字符串`text`只有一个字符,但是`for`循环会认为它包含两个字符(都不可打印),而`for...of`循环会正确识别出这一个字符。 - -## 直接输入 U+2028 和 U+2029 - -JavaScript 字符串允许直接输入字符,以及输入字符的转义形式。举例来说,“中”的 Unicode 码点是 U+4e2d,你可以直接在字符串里面输入这个汉字,也可以输入它的转义形式`\u4e2d`,两者是等价的。 - -```javascript -'中' === '\u4e2d' // true -``` - -但是,JavaScript 规定有5个字符,不能在字符串里面直接使用,只能使用转义形式。 - -- U+005C:反斜杠(reverse solidus) -- U+000D:回车(carriage return) -- U+2028:行分隔符(line separator) -- U+2029:段分隔符(paragraph separator) -- U+000A:换行符(line feed) - -举例来说,字符串里面不能直接包含反斜杠,一定要转义写成`\\`或者`\u005c`。 - -这个规定本身没有问题,麻烦在于 JSON 格式允许字符串里面直接使用 U+2028(行分隔符)和 U+2029(段分隔符)。这样一来,服务器输出的 JSON 被`JSON.parse`解析,就有可能直接报错。 - -```javascript -const json = '"\u2028"'; -JSON.parse(json); // 可能报错 -``` - -JSON 格式已经冻结(RFC 7159),没法修改了。为了消除这个报错,[ES2019](https://github.com/tc39/proposal-json-superset) 允许 JavaScript 字符串直接输入 U+2028(行分隔符)和 U+2029(段分隔符)。 - -```javascript -const PS = eval("'\u2029'"); -``` - -根据这个提案,上面的代码不会报错。 - -注意,模板字符串现在就允许直接输入这两个字符。另外,正则表达式依然不允许直接输入这两个字符,这是没有问题的,因为 JSON 本来就不允许直接包含正则表达式。 - -## JSON.stringify() 的改造 - -根据标准,JSON 数据必须是 UTF-8 编码。但是,现在的`JSON.stringify()`方法有可能返回不符合 UTF-8 标准的字符串。 - -具体来说,UTF-8 标准规定,`0xD800`到`0xDFFF`之间的码点,不能单独使用,必须配对使用。比如,`\uD834\uDF06`是两个码点,但是必须放在一起配对使用,代表字符`𝌆`。这是为了表示码点大于`0xFFFF`的字符的一种变通方法。单独使用`\uD834`和`\uDF06`这两个码点是不合法的,或者颠倒顺序也不行,因为`\uDF06\uD834`并没有对应的字符。 - -`JSON.stringify()`的问题在于,它可能返回`0xD800`到`0xDFFF`之间的单个码点。 - -```javascript -JSON.stringify('\u{D834}') // "\u{D834}" -``` - -为了确保返回的是合法的 UTF-8 字符,[ES2019](https://github.com/tc39/proposal-well-formed-stringify) 改变了`JSON.stringify()`的行为。如果遇到`0xD800`到`0xDFFF`之间的单个码点,或者不存在的配对形式,它会返回转义字符串,留给应用自己决定下一步的处理。 - -```javascript -JSON.stringify('\u{D834}') // ""\\uD834"" -JSON.stringify('\uDF06\uD834') // ""\\udf06\\ud834"" -``` - ## 模板字符串 传统的 JavaScript 语言,输出模板通常是这样写的(下面使用了 jQuery 的方法)。 @@ -300,384 +163,3 @@ console.log(tmpl(data)); // // ``` - -如果需要引用模板字符串本身,在需要时执行,可以写成函数。 - -```javascript -let func = (name) => `Hello ${name}!`; -func('Jack') // "Hello Jack!" -``` - -上面代码中,模板字符串写成了一个函数的返回值。执行这个函数,就相当于执行这个模板字符串了。 - -## 实例:模板编译 - -下面,我们来看一个通过模板字符串,生成正式模板的实例。 - -```javascript -let template = ` - -`; -``` - -上面代码在模板字符串之中,放置了一个常规模板。该模板使用`<%...%>`放置 JavaScript 代码,使用`<%= ... %>`输出 JavaScript 表达式。 - -怎么编译这个模板字符串呢? - -一种思路是将其转换为 JavaScript 表达式字符串。 - -```javascript -echo(''); -``` - -这个转换使用正则表达式就行了。 - -```javascript -let evalExpr = /<%=(.+?)%>/g; -let expr = /<%([\s\S]+?)%>/g; - -template = template - .replace(evalExpr, '`); \n echo( $1 ); \n echo(`') - .replace(expr, '`); \n $1 \n echo(`'); - -template = 'echo(`' + template + '`);'; -``` - -然后,将`template`封装在一个函数里面返回,就可以了。 - -```javascript -let script = -`(function parse(data){ - let output = ""; - - function echo(html){ - output += html; - } - - ${ template } - - return output; -})`; - -return script; -``` - -将上面的内容拼装成一个模板编译函数`compile`。 - -```javascript -function compile(template){ - const evalExpr = /<%=(.+?)%>/g; - const expr = /<%([\s\S]+?)%>/g; - - template = template - .replace(evalExpr, '`); \n echo( $1 ); \n echo(`') - .replace(expr, '`); \n $1 \n echo(`'); - - template = 'echo(`' + template + '`);'; - - let script = - `(function parse(data){ - let output = ""; - - function echo(html){ - output += html; - } - - ${ template } - - return output; - })`; - - return script; -} -``` - -`compile`函数的用法如下。 - -```javascript -let parse = eval(compile(template)); -div.innerHTML = parse({ supplies: [ "broom", "mop", "cleaner" ] }); -// -``` - -## 标签模板 - -模板字符串的功能,不仅仅是上面这些。它可以紧跟在一个函数名后面,该函数将被调用来处理这个模板字符串。这被称为“标签模板”功能(tagged template)。 - -```javascript -alert`hello` -// 等同于 -alert(['hello']) -``` - -标签模板其实不是模板,而是函数调用的一种特殊形式。“标签”指的就是函数,紧跟在后面的模板字符串就是它的参数。 - -但是,如果模板字符里面有变量,就不是简单的调用了,而是会将模板字符串先处理成多个参数,再调用函数。 - -```javascript -let a = 5; -let b = 10; - -tag`Hello ${ a + b } world ${ a * b }`; -// 等同于 -tag(['Hello ', ' world ', ''], 15, 50); -``` - -上面代码中,模板字符串前面有一个标识名`tag`,它是一个函数。整个表达式的返回值,就是`tag`函数处理模板字符串后的返回值。 - -函数`tag`依次会接收到多个参数。 - -```javascript -function tag(stringArr, value1, value2){ - // ... -} - -// 等同于 - -function tag(stringArr, ...values){ - // ... -} -``` - -`tag`函数的第一个参数是一个数组,该数组的成员是模板字符串中那些没有变量替换的部分,也就是说,变量替换只发生在数组的第一个成员与第二个成员之间、第二个成员与第三个成员之间,以此类推。 - -`tag`函数的其他参数,都是模板字符串各个变量被替换后的值。由于本例中,模板字符串含有两个变量,因此`tag`会接受到`value1`和`value2`两个参数。 - -`tag`函数所有参数的实际值如下。 - -- 第一个参数:`['Hello ', ' world ', '']` -- 第二个参数: 15 -- 第三个参数:50 - -也就是说,`tag`函数实际上以下面的形式调用。 - -```javascript -tag(['Hello ', ' world ', ''], 15, 50) -``` - -我们可以按照需要编写`tag`函数的代码。下面是`tag`函数的一种写法,以及运行结果。 - -```javascript -let a = 5; -let b = 10; - -function tag(s, v1, v2) { - console.log(s[0]); - console.log(s[1]); - console.log(s[2]); - console.log(v1); - console.log(v2); - - return "OK"; -} - -tag`Hello ${ a + b } world ${ a * b}`; -// "Hello " -// " world " -// "" -// 15 -// 50 -// "OK" -``` - -下面是一个更复杂的例子。 - -```javascript -let total = 30; -let msg = passthru`The total is ${total} (${total*1.05} with tax)`; - -function passthru(literals) { - let result = ''; - let i = 0; - - while (i < literals.length) { - result += literals[i++]; - if (i < arguments.length) { - result += arguments[i]; - } - } - - return result; -} - -msg // "The total is 30 (31.5 with tax)" -``` - -上面这个例子展示了,如何将各个参数按照原来的位置拼合回去。 - -`passthru`函数采用 rest 参数的写法如下。 - -```javascript -function passthru(literals, ...values) { - let output = ""; - let index; - for (index = 0; index < values.length; index++) { - output += literals[index] + values[index]; - } - - output += literals[index] - return output; -} -``` - -“标签模板”的一个重要应用,就是过滤 HTML 字符串,防止用户输入恶意内容。 - -```javascript -let message = - SaferHTML`

${sender} has sent you a message.

`; - -function SaferHTML(templateData) { - let s = templateData[0]; - for (let i = 1; i < arguments.length; i++) { - let arg = String(arguments[i]); - - // Escape special characters in the substitution. - s += arg.replace(/&/g, "&") - .replace(//g, ">"); - - // Don't escape special characters in the template. - s += templateData[i]; - } - return s; -} -``` - -上面代码中,`sender`变量往往是用户提供的,经过`SaferHTML`函数处理,里面的特殊字符都会被转义。 - -```javascript -let sender = ''; // 恶意代码 -let message = SaferHTML`

${sender} has sent you a message.

`; - -message -//

<script>alert("abc")</script> has sent you a message.

-``` - -标签模板的另一个应用,就是多语言转换(国际化处理)。 - -```javascript -i18n`Welcome to ${siteName}, you are visitor number ${visitorNumber}!` -// "欢迎访问xxx,您是第xxxx位访问者!" -``` - -模板字符串本身并不能取代 Mustache 之类的模板库,因为没有条件判断和循环处理功能,但是通过标签函数,你可以自己添加这些功能。 - -```javascript -// 下面的hashTemplate函数 -// 是一个自定义的模板处理函数 -let libraryHtml = hashTemplate` - -`; -``` - -除此之外,你甚至可以使用标签模板,在 JavaScript 语言之中嵌入其他语言。 - -```javascript -jsx` -
- - ${this.state.value} -
-` -``` - -上面的代码通过`jsx`函数,将一个 DOM 字符串转为 React 对象。你可以在 GitHub 找到`jsx`函数的[具体实现](https://gist.github.com/lygaret/a68220defa69174bdec5)。 - -下面则是一个假想的例子,通过`java`函数,在 JavaScript 代码之中运行 Java 代码。 - -```javascript -java` -class HelloWorldApp { - public static void main(String[] args) { - System.out.println("Hello World!"); // Display the string. - } -} -` -HelloWorldApp.main(); -``` - -模板处理函数的第一个参数(模板字符串数组),还有一个`raw`属性。 - -```javascript -console.log`123` -// ["123", raw: Array[1]] -``` - -上面代码中,`console.log`接受的参数,实际上是一个数组。该数组有一个`raw`属性,保存的是转义后的原字符串。 - -请看下面的例子。 - -```javascript -tag`First line\nSecond line` - -function tag(strings) { - console.log(strings.raw[0]); - // strings.raw[0] 为 "First line\\nSecond line" - // 打印输出 "First line\nSecond line" -} -``` - -上面代码中,`tag`函数的第一个参数`strings`,有一个`raw`属性,也指向一个数组。该数组的成员与`strings`数组完全一致。比如,`strings`数组是`["First line\nSecond line"]`,那么`strings.raw`数组就是`["First line\\nSecond line"]`。两者唯一的区别,就是字符串里面的斜杠都被转义了。比如,strings.raw 数组会将`\n`视为`\\`和`n`两个字符,而不是换行符。这是为了方便取得转义之前的原始模板而设计的。 - -## 模板字符串的限制 - -前面提到标签模板里面,可以内嵌其他语言。但是,模板字符串默认会将字符串转义,导致无法嵌入其他语言。 - -举例来说,标签模板里面可以嵌入 LaTEX 语言。 - -```javascript -function latex(strings) { - // ... -} - -let document = latex` -\newcommand{\fun}{\textbf{Fun!}} // 正常工作 -\newcommand{\unicode}{\textbf{Unicode!}} // 报错 -\newcommand{\xerxes}{\textbf{King!}} // 报错 - -Breve over the h goes \u{h}ere // 报错 -` -``` - -上面代码中,变量`document`内嵌的模板字符串,对于 LaTEX 语言来说完全是合法的,但是 JavaScript 引擎会报错。原因就在于字符串的转义。 - -模板字符串会将`\u00FF`和`\u{42}`当作 Unicode 字符进行转义,所以`\unicode`解析时报错;而`\x56`会被当作十六进制字符串转义,所以`\xerxes`会报错。也就是说,`\u`和`\x`在 LaTEX 里面有特殊含义,但是 JavaScript 将它们转义了。 - -为了解决这个问题,ES2018 [放松](https://tc39.github.io/proposal-template-literal-revision/)了对标签模板里面的字符串转义的限制。如果遇到不合法的字符串转义,就返回`undefined`,而不是报错,并且从`raw`属性上面可以得到原始字符串。 - -```javascript -function tag(strs) { - strs[0] === undefined - strs.raw[0] === "\\unicode and \\u{55}"; -} -tag`\unicode and \u{55}` -``` - -上面代码中,模板字符串原本是应该报错的,但是由于放松了对字符串转义的限制,所以不报错了,JavaScript 引擎将第一个字符设置为`undefined`,但是`raw`属性依然可以得到原始字符串,因此`tag`函数还是可以对原字符串进行处理。 - -注意,这种对字符串转义的放松,只在标签模板解析字符串时生效,不是标签模板的场合,依然会报错。 - -```javascript -let bad = `bad escape sequence: \unicode`; // 报错 -``` diff --git a/readme.ppz.md b/readme.ppz.md new file mode 100644 index 000000000..d550d89ea --- /dev/null +++ b/readme.ppz.md @@ -0,0 +1,16 @@ +# Summary + +* [3. 变量的解构赋值](docs/destructuring.md) +* [4. 字符串的扩展](docs/string.md) +* [7. 函数的扩展](docs/function.md) +* [8. 数组的扩展](docs/array.md) +* [9. 对象的扩展](docs/object.md) + +以下也是相当基础,但未删减的内容,以后再看 +* [14. Promise 对象](docs/promise.md) +* [15. Iterator 和 for...of 循环](docs/iterator.md) +* [18. async 函数](docs/async.md) +* [19. Class 的基本语法](docs/class.md) +* [20. Class 的继承](docs/class-extends.md) +* [22. Module 的语法](docs/module.md) +* [23. Module 的加载实现](docs/module-loader.md) \ No newline at end of file From cfb4d48ceaf406cfd24c854a6e1e533d5dd56f10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=B3=E4=BA=8E?= <372301467@qq.com> Date: Sat, 28 Jan 2023 17:23:35 +0800 Subject: [PATCH 2/2] Update function.md --- docs/function.md | 76 ------------------------------------------------ 1 file changed, 76 deletions(-) diff --git a/docs/function.md b/docs/function.md index c5727541c..cf959f50c 100644 --- a/docs/function.md +++ b/docs/function.md @@ -259,82 +259,6 @@ function f(a, ...b, c) { (function(a, ...b) {}).length // 1 ``` -## 严格模式 - -从 ES5 开始,函数内部可以设定为严格模式。 - -```javascript -function doSomething(a, b) { - 'use strict'; - // code -} -``` - -ES2016 做了一点修改,规定只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错。 - -```javascript -// 报错 -function doSomething(a, b = a) { - 'use strict'; - // code -} - -// 报错 -const doSomething = function ({a, b}) { - 'use strict'; - // code -}; - -// 报错 -const doSomething = (...a) => { - 'use strict'; - // code -}; - -const obj = { - // 报错 - doSomething({a, b}) { - 'use strict'; - // code - } -}; -``` - -这样规定的原因是,函数内部的严格模式,同时适用于函数体和函数参数。但是,函数执行的时候,先执行函数参数,然后再执行函数体。这样就有一个不合理的地方,只有从函数体之中,才能知道参数是否应该以严格模式执行,但是参数却应该先于函数体执行。 - -```javascript -// 报错 -function doSomething(value = 070) { - 'use strict'; - return value; -} -``` - -上面代码中,参数`value`的默认值是八进制数`070`,但是严格模式下不能用前缀`0`表示八进制,所以应该报错。但是实际上,JavaScript 引擎会先成功执行`value = 070`,然后进入函数体内部,发现需要用严格模式执行,这时才会报错。 - -虽然可以先解析函数体代码,再执行参数代码,但是这样无疑就增加了复杂性。因此,标准索性禁止了这种用法,只要参数使用了默认值、解构赋值、或者扩展运算符,就不能显式指定严格模式。 - -两种方法可以规避这种限制。第一种是设定全局性的严格模式,这是合法的。 - -```javascript -'use strict'; - -function doSomething(a, b = a) { - // code -} -``` - -第二种是把函数包在一个无参数的立即执行函数里面。 - -```javascript -const doSomething = (function () { - 'use strict'; - return function(value = 42) { - return value; - }; -}()); -``` - ## 箭头函数 ### 基本用法