diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 000000000..8907f9d4c Binary files /dev/null and b/.DS_Store differ diff --git a/README.md b/README.md index 84de1b82f..5bb40d336 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # ES6 入门教程 -官方镜像:[网道(WangDoc.com)](https://wangdoc.com/es6/) +- [官方镜像](https://wangdoc.com/es6/) +- [JavaScript 教程](https://wangdoc.com/javascript) +- [TypeScript 教程](https://wangdoc.com/typescript) 《ECMAScript 6 入门教程》是一本开源的 JavaScript 语言教程,全面介绍 ECMAScript 6 新引入的语法特性。 @@ -8,17 +10,13 @@ 本书覆盖 ES6 与上一个版本 ES5 的所有不同之处,对涉及的语法知识给予详细介绍,并给出大量简洁易懂的示例代码。 -本书为中级难度,适合已经掌握 ES5 的读者,用来了解这门语言的最新发展;也可当作参考手册,查寻新增的语法点。如果你是 JavaScript 语言的初学者,建议先学完[《JavaScript 语言入门教程》](https://wangdoc.com/javascript/),再来看本书。 +本书为中级难度,适合已经掌握 ES5 的读者,用来了解这门语言的最新发展;也可当作参考手册,查寻新增的语法点。如果你是 JavaScript 语言的初学者,建议先学完[《JavaScript 语言教程》](https://wangdoc.com/javascript/),再来看本书。 -全书已由电子工业出版社出版,2017年9月推出了第三版,书名为《ES6 标准入门》。纸版是基于网站内容排版印刷的。 - -感谢张春雨编辑支持我将全书开源的做法。如果您认可这本书,建议购买纸版。这样可以使出版社不因出版开源书籍而亏钱,进而鼓励更多的作者开源自己的书籍。下面是第三版的购买地址。 +全书已由电子工业出版社出版,2017年9月推出了第三版,书名为《ES6 标准入门》。纸版内容截止到出版时,网站内容一直在修订。 - [淘宝](https://s.taobao.com/search?q=ES6%E6%A0%87%E5%87%86%E5%85%A5%E9%97%A8+%E7%AC%AC3%E7%89%88) - [京东](https://search.jd.com/Search?keyword=ES6%E6%A0%87%E5%87%86%E5%85%A5%E9%97%A8%20%E7%AC%AC3%E7%89%88&enc=utf-8&wq=ES6%E6%A0%87%E5%87%86%E5%85%A5%E9%97%A8%20%E7%AC%AC3%E7%89%88) - [当当](https://product.dangdang.com/25156888.html) -- [亚马逊](https://www.amazon.cn/ES6%E6%A0%87%E5%87%86%E5%85%A5%E9%97%A8-%E9%98%AE%E4%B8%80%E5%B3%B0/dp/B0755547ZZ) -- [China-pub](http://product.china-pub.com/6504650) ### 版权许可 @@ -27,3 +25,4 @@ 只要保持原作者署名和非商用,您可以自由地阅读、分享、修改本书。 详细的法律条文请参见[创意共享](http://creativecommons.org/licenses/by-nc/4.0/)网站。 + diff --git a/docs/array.md b/docs/array.md index 4e235add4..e4ed02e8e 100644 --- a/docs/array.md +++ b/docs/array.md @@ -73,9 +73,9 @@ console.log(...[1, 2]) 上面三种情况,扩展运算符都放在圆括号里面,但是前两种情况会报错,因为扩展运算符所在的括号不是函数调用。 -### 替代函数的 apply 方法 +### 替代函数的 apply() 方法 -由于扩展运算符可以展开数组,所以不再需要`apply`方法,将数组转为函数的参数了。 +由于扩展运算符可以展开数组,所以不再需要`apply()`方法将数组转为函数的参数了。 ```javascript // ES5 的写法 @@ -85,7 +85,7 @@ function f(x, y, z) { var args = [0, 1, 2]; f.apply(null, args); -// ES6的写法 +// ES6 的写法 function f(x, y, z) { // ... } @@ -93,7 +93,7 @@ let args = [0, 1, 2]; f(...args); ``` -下面是扩展运算符取代`apply`方法的一个实际的例子,应用`Math.max`方法,简化求出一个数组最大元素的写法。 +下面是扩展运算符取代`apply()`方法的一个实际的例子,应用`Math.max()`方法,简化求出一个数组最大元素的写法。 ```javascript // ES5 的写法 @@ -106,12 +106,12 @@ Math.max(...[14, 3, 77]) Math.max(14, 3, 77); ``` -上面代码中,由于 JavaScript 不提供求数组最大元素的函数,所以只能套用`Math.max`函数,将数组转为一个参数序列,然后求最大值。有了扩展运算符以后,就可以直接用`Math.max`了。 +上面代码中,由于 JavaScript 不提供求数组最大元素的函数,所以只能套用`Math.max()`函数,将数组转为一个参数序列,然后求最大值。有了扩展运算符以后,就可以直接用`Math.max()`了。 -另一个例子是通过`push`函数,将一个数组添加到另一个数组的尾部。 +另一个例子是通过`push()`函数,将一个数组添加到另一个数组的尾部。 ```javascript -// ES5的 写法 +// ES5 的写法 var arr1 = [0, 1, 2]; var arr2 = [3, 4, 5]; Array.prototype.push.apply(arr1, arr2); @@ -122,13 +122,14 @@ let arr2 = [3, 4, 5]; arr1.push(...arr2); ``` -上面代码的 ES5 写法中,`push`方法的参数不能是数组,所以只好通过`apply`方法变通使用`push`方法。有了扩展运算符,就可以直接将数组传入`push`方法。 +上面代码的 ES5 写法中,`push()`方法的参数不能是数组,所以只好通过`apply()`方法变通使用`push()`方法。有了扩展运算符,就可以直接将数组传入`push()`方法。 下面是另外一个例子。 ```javascript // ES5 new (Date.bind.apply(Date, [null, 2015, 1, 1])) + // ES6 new Date(...[2015, 1, 1]); ``` @@ -213,6 +214,7 @@ a4[0] === a1[0] // true ```javascript // ES5 a = list[0], rest = list.slice(1) + // ES6 [a, ...rest] = list ``` @@ -281,7 +283,7 @@ str.split('').reverse().join('') // 'y\uD83D\uDE80x' ``` -上面代码中,如果不用扩展运算符,字符串的`reverse`操作就不正确。 +上面代码中,如果不用扩展运算符,字符串的`reverse()`操作就不正确。 **(5)实现了 Iterator 接口的对象** @@ -292,7 +294,7 @@ let nodeList = document.querySelectorAll('div'); let array = [...nodeList]; ``` -上面代码中,`querySelectorAll`方法返回的是一个`NodeList`对象。它不是数组,而是一个类似数组的对象。这时,扩展运算符可以将其转为真正的数组,原因就在于`NodeList`对象实现了 Iterator 。 +上面代码中,`querySelectorAll()`方法返回的是一个`NodeList`对象。它不是数组,而是一个类似数组的对象。这时,扩展运算符可以将其转为真正的数组,原因就在于`NodeList`对象实现了 Iterator。 ```javascript Number.prototype[Symbol.iterator] = function*() { @@ -361,9 +363,9 @@ let arr = [...obj]; // TypeError: Cannot spread non-iterable object ## Array.from() -`Array.from`方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。 +`Array.from()`方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。 -下面是一个类似数组的对象,`Array.from`将它转为真正的数组。 +下面是一个类似数组的对象,`Array.from()`将它转为真正的数组。 ```javascript let arrayLike = { @@ -373,32 +375,32 @@ let arrayLike = { length: 3 }; -// ES5的写法 +// ES5 的写法 var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c'] -// ES6的写法 +// ES6 的写法 let arr2 = Array.from(arrayLike); // ['a', 'b', 'c'] ``` -实际应用中,常见的类似数组的对象是 DOM 操作返回的 NodeList 集合,以及函数内部的`arguments`对象。`Array.from`都可以将它们转为真正的数组。 +实际应用中,常见的类似数组的对象是 DOM 操作返回的 NodeList 集合,以及函数内部的`arguments`对象。`Array.from()`都可以将它们转为真正的数组。 ```javascript -// NodeList对象 +// NodeList 对象 let ps = document.querySelectorAll('p'); Array.from(ps).filter(p => { return p.textContent.length > 100; }); -// arguments对象 +// arguments 对象 function foo() { var args = Array.from(arguments); // ... } ``` -上面代码中,`querySelectorAll`方法返回的是一个类似数组的对象,可以将这个对象转为真正的数组,再使用`filter`方法。 +上面代码中,`querySelectorAll()`方法返回的是一个类似数组的对象,可以将这个对象转为真正的数组,再使用`filter()`方法。 -只要是部署了 Iterator 接口的数据结构,`Array.from`都能将其转为数组。 +只要是部署了 Iterator 接口的数据结构,`Array.from()`都能将其转为数组。 ```javascript Array.from('hello') @@ -408,9 +410,9 @@ let namesSet = new Set(['a', 'b']) Array.from(namesSet) // ['a', 'b'] ``` -上面代码中,字符串和 Set 结构都具有 Iterator 接口,因此可以被`Array.from`转为真正的数组。 +上面代码中,字符串和 Set 结构都具有 Iterator 接口,因此可以被`Array.from()`转为真正的数组。 -如果参数是一个真正的数组,`Array.from`会返回一个一模一样的新数组。 +如果参数是一个真正的数组,`Array.from()`会返回一个一模一样的新数组。 ```javascript Array.from([1, 2, 3]) @@ -429,16 +431,16 @@ function foo() { [...document.querySelectorAll('div')] ``` -扩展运算符背后调用的是遍历器接口(`Symbol.iterator`),如果一个对象没有部署这个接口,就无法转换。`Array.from`方法还支持类似数组的对象。所谓类似数组的对象,本质特征只有一点,即必须有`length`属性。因此,任何有`length`属性的对象,都可以通过`Array.from`方法转为数组,而此时扩展运算符就无法转换。 +扩展运算符背后调用的是遍历器接口(`Symbol.iterator`),如果一个对象没有部署这个接口,就无法转换。`Array.from()`方法还支持类似数组的对象。所谓类似数组的对象,本质特征只有一点,即必须有`length`属性。因此,任何有`length`属性的对象,都可以通过`Array.from()`方法转为数组,而此时扩展运算符就无法转换。 ```javascript Array.from({ length: 3 }); // [ undefined, undefined, undefined ] ``` -上面代码中,`Array.from`返回了一个具有三个成员的数组,每个位置的值都是`undefined`。扩展运算符转换不了这个对象。 +上面代码中,`Array.from()`返回了一个具有三个成员的数组,每个位置的值都是`undefined`。扩展运算符转换不了这个对象。 -对于还没有部署该方法的浏览器,可以用`Array.prototype.slice`方法替代。 +对于还没有部署该方法的浏览器,可以用`Array.prototype.slice()`方法替代。 ```javascript const toArray = (() => @@ -446,7 +448,7 @@ const toArray = (() => )(); ``` -`Array.from`还可以接受第二个参数,作用类似于数组的`map`方法,用来对每个元素进行处理,将处理后的值放入返回的数组。 +`Array.from()`还可以接受一个函数作为第二个参数,作用类似于数组的`map()`方法,用来对每个元素进行处理,将处理后的值放入返回的数组。 ```javascript Array.from(arrayLike, x => x * x); @@ -486,7 +488,7 @@ typesOf(null, [], NaN) // ['object', 'object', 'number'] ``` -如果`map`函数里面用到了`this`关键字,还可以传入`Array.from`的第三个参数,用来绑定`this`。 +如果`map()`函数里面用到了`this`关键字,还可以传入`Array.from()`的第三个参数,用来绑定`this`。 `Array.from()`可以将各种值转为真正的数组,并且还提供`map`功能。这实际上意味着,只要有一个原始的数据结构,你就可以先对它的值进行处理,然后转成规范的数组结构,进而就可以使用数量众多的数组方法。 @@ -495,7 +497,7 @@ Array.from({ length: 2 }, () => 'jack') // ['jack', 'jack'] ``` -上面代码中,`Array.from`的第一个参数指定了第二个参数运行的次数。这种特性可以让该方法的用法变得非常灵活。 +上面代码中,`Array.from()`的第一个参数指定了第二个参数运行的次数。这种特性可以让该方法的用法变得非常灵活。 `Array.from()`的另一个应用是,将字符串转为数组,然后返回字符串的长度。因为它能正确处理各种 Unicode 字符,可以避免 JavaScript 将大于`\uFFFF`的 Unicode 字符,算作两个字符的 bug。 @@ -593,9 +595,9 @@ i32a.copyWithin(0, 2); // Int32Array [4, 2, 3, 4, 5] ``` -## 实例方法:find() 和 findIndex() +## 实例方法:find(),findIndex(),findLast(),findLastIndex() -数组实例的`find`方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为`true`的成员,然后返回该成员。如果没有符合条件的成员,则返回`undefined`。 +数组实例的`find()`方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为`true`的成员,然后返回该成员。如果没有符合条件的成员,则返回`undefined`。 ```javascript [1, 4, -5, 10].find((n) => n < 0) @@ -610,9 +612,9 @@ i32a.copyWithin(0, 2); }) // 10 ``` -上面代码中,`find`方法的回调函数可以接受三个参数,依次为当前的值、当前的位置和原数组。 +上面代码中,`find()`方法的回调函数可以接受三个参数,依次为当前的值、当前的位置和原数组。 -数组实例的`findIndex`方法的用法与`find`方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回`-1`。 +数组实例的`findIndex()`方法的用法与`find()`方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回`-1`。 ```javascript [1, 5, 10, 15].findIndex(function(value, index, arr) { @@ -630,9 +632,9 @@ let person = {name: 'John', age: 20}; [10, 12, 26, 15].find(f, person); // 26 ``` -上面的代码中,`find`函数接收了第二个参数`person`对象,回调函数中的`this`对象指向`person`对象。 +上面的代码中,`find()`函数接收了第二个参数`person`对象,回调函数中的`this`对象指向`person`对象。 -另外,这两个方法都可以发现`NaN`,弥补了数组的`indexOf`方法的不足。 +另外,这两个方法都可以发现`NaN`,弥补了数组的`indexOf()`方法的不足。 ```javascript [NaN].indexOf(NaN) @@ -642,7 +644,23 @@ let person = {name: 'John', age: 20}; // 0 ``` -上面代码中,`indexOf`方法无法识别数组的`NaN`成员,但是`findIndex`方法可以借助`Object.is`方法做到。 +上面代码中,`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() @@ -841,7 +859,7 @@ arr.flatMap(function callback(currentValue[, index[, array]]) { 这是因为方括号运算符`[]`在 JavaScript 语言里面,不仅用于数组,还用于对象。对于对象来说,方括号里面就是键名,比如`obj[1]`引用的是键名为字符串`1`的键,同理`obj[-1]`引用的是键名为字符串`-1`的键。由于 JavaScript 的数组是特殊的对象,所以方括号里面的负数无法再有其他语义了,也就是说,不可能添加新语法来支持负索引。 -为了解决这个问题,现在有一个[提案](https://github.com/tc39/proposal-relative-indexing-method/),为数组实例增加了`at()`方法,接受一个整数作为参数,返回对应位置的成员,支持负索引。这个方法不仅可用于数组,也可用于字符串和类型数组(TypedArray)。 +为了解决这个问题,[ES2022](https://github.com/tc39/proposal-relative-indexing-method/) 为数组实例增加了`at()`方法,接受一个整数作为参数,返回对应位置的成员,并支持负索引。这个方法不仅可用于数组,也可用于字符串和类型数组(TypedArray)。 ```javascript const arr = [5, 12, 8, 130, 44]; @@ -851,6 +869,101 @@ 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()`等等。数组只要调用了这些方法,它的值就变了。[ES2023](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].group(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()`构造函数返回的数组都是空位。 diff --git a/docs/arraybuffer.md b/docs/arraybuffer.md index 61325fd7d..863370904 100644 --- a/docs/arraybuffer.md +++ b/docs/arraybuffer.md @@ -16,7 +16,7 @@ 简单说,`ArrayBuffer`对象代表原始的二进制数据,`TypedArray`视图用来读写简单类型的二进制数据,`DataView`视图用来读写复杂类型的二进制数据。 -`TypedArray`视图支持的数据类型一共有 9 种(`DataView`视图支持除`Uint8C`以外的其他 8 种)。 +`TypedArray`视图支持的数据类型一共有12种。 | 数据类型 | 字节长度 | 含义 | 对应的 C 语言类型 | | -------- | -------- | -------------------------------- | ----------------- | @@ -27,6 +27,9 @@ | Uint16 | 2 | 16 位不带符号整数 | unsigned short | | Int32 | 4 | 32 位带符号整数 | int | | Uint32 | 4 | 32 位不带符号的整数 | unsigned int | +| BigInt64 | 8 | 64 位有符号整数 | | +| BigUint64 | 8 | 64 位无符号整数 | | +| Float16 | 2 | 16 位浮点数 | | | Float32 | 4 | 32 位浮点数 | float | | Float64 | 8 | 64 位浮点数 | double | @@ -153,10 +156,13 @@ ArrayBuffer.isView(v) // true - **`Uint16Array`**:16 位无符号整数,长度 2 个字节。 - **`Int32Array`**:32 位有符号整数,长度 4 个字节。 - **`Uint32Array`**:32 位无符号整数,长度 4 个字节。 +- **`BigInt64Array`**: 64 位有符号整数,长度 8 个字节。 +- **`BigUint64Array`**:64 位无符号整数,长度 8 个字节。 +- **`Float16Array`**: 16 位浮点数,长度 2 个字节。 - **`Float32Array`**:32 位浮点数,长度 4 个字节。 - **`Float64Array`**:64 位浮点数,长度 8 个字节。 -这 9 个构造函数生成的数组,统称为`TypedArray`视图。它们很像普通数组,都有`length`属性,都能用方括号运算符(`[]`)获取单个元素,所有数组的方法,在它们上面都能使用。普通数组与 TypedArray 数组的差异主要在以下方面。 +这12个构造函数生成的数组,统称为`TypedArray`视图。它们很像普通数组,都有`length`属性,都能用方括号运算符(`[]`)获取单个元素,所有数组的方法,在它们上面都能使用。普通数组与 TypedArray 数组的差异主要在以下方面。 - TypedArray 数组的所有成员,都是同一种类型。 - TypedArray 数组的成员是连续的,不会有空位。 @@ -165,7 +171,7 @@ ArrayBuffer.isView(v) // true ### 构造函数 -TypedArray 数组提供 9 种构造函数,用来生成相应类型的数组实例。 +TypedArray 数组提供12种构造函数,用来生成相应类型的数组实例。 构造函数有多种用法。 @@ -746,7 +752,7 @@ const dv = new DataView(buffer); - `DataView.prototype.byteLength`:返回占据的内存字节长度 - `DataView.prototype.byteOffset`:返回当前视图从对应的 ArrayBuffer 对象的哪个字节开始 -`DataView`实例提供 8 个方法读取内存。 +`DataView`实例提供11个方法读取内存。 - **`getInt8`**:读取 1 个字节,返回一个 8 位整数。 - **`getUint8`**:读取 1 个字节,返回一个无符号的 8 位整数。 @@ -754,6 +760,9 @@ const dv = new DataView(buffer); - **`getUint16`**:读取 2 个字节,返回一个无符号的 16 位整数。 - **`getInt32`**:读取 4 个字节,返回一个 32 位整数。 - **`getUint32`**:读取 4 个字节,返回一个无符号的 32 位整数。 +- **`getBigInt64`**:读取 8 个字节,返回一个 64 位整数。 +- **`getBigUint64`**:读取 8 个字节,返回一个无符号的 64 位整数。 +- **`getFloat16`**:读取 2 个字节,返回一个 16 位浮点数。 - **`getFloat32`**:读取 4 个字节,返回一个 32 位浮点数。 - **`getFloat64`**:读取 8 个字节,返回一个 64 位浮点数。 @@ -788,7 +797,7 @@ const v2 = dv.getUint16(3, false); const v3 = dv.getUint16(3); ``` -DataView 视图提供 8 个方法写入内存。 +DataView 视图提供11个方法写入内存。 - **`setInt8`**:写入 1 个字节的 8 位整数。 - **`setUint8`**:写入 1 个字节的 8 位无符号整数。 @@ -796,6 +805,9 @@ DataView 视图提供 8 个方法写入内存。 - **`setUint16`**:写入 2 个字节的 16 位无符号整数。 - **`setInt32`**:写入 4 个字节的 32 位整数。 - **`setUint32`**:写入 4 个字节的 32 位无符号整数。 +- **`setBigInt64`**:写入 8 个字节的 64 位整数。 +- **`setBigUint64`**:写入 8 个字节的 64 位无符号整数。 +- **`setFloat16`**:写入 2 个字节的 16 位浮点数。 - **`setFloat32`**:写入 4 个字节的 32 位浮点数。 - **`setFloat64`**:写入 8 个字节的 64 位浮点数。 diff --git a/docs/async.md b/docs/async.md index 5d9376a79..86ce19938 100644 --- a/docs/async.md +++ b/docs/async.md @@ -724,7 +724,7 @@ async function logInOrder(urls) { ## 顶层 await -根据语法规格,`await`命令只能出现在 async 函数内部,否则都会报错。 +早期的语法规定是,`await`命令只能出现在 async 函数内部,否则都会报错。 ```javascript // 报错 @@ -733,7 +733,7 @@ const data = await fetch('https://api.example.com'); 上面代码中,`await`命令独立使用,没有放在 async 函数里面,就会报错。 -目前,有一个[语法提案](https://github.com/tc39/proposal-top-level-await),允许在模块的顶层独立使用`await`命令,使得上面那行代码不会报错了。这个提案的目的,是借用`await`解决模块异步加载的问题。 +从 [ES2022](https://github.com/tc39/proposal-top-level-await) 开始,允许在模块的顶层独立使用`await`命令,使得上面那行代码不会报错了。它的主要目的是使用`await`解决模块异步加载的问题。 ```javascript // awaiting.js @@ -749,19 +749,6 @@ export { output }; 上面代码中,模块`awaiting.js`的输出值`output`,取决于异步操作。我们把异步操作包装在一个 async 函数里面,然后调用这个函数,只有等里面的异步操作都执行,变量`output`才会有值,否则就返回`undefined`。 -上面的代码也可以写成立即执行函数的形式。 - -```javascript -// awaiting.js -let output; -(async function main() { - const dynamic = await import(someMission); - const data = await fetch(url); - output = someProcess(dynamic.default, data); -})(); -export { output }; -``` - 下面是加载这个模块的写法。 ```javascript diff --git a/docs/class-extends.md b/docs/class-extends.md index 1e0395bc3..c5308bd0c 100644 --- a/docs/class-extends.md +++ b/docs/class-extends.md @@ -119,22 +119,9 @@ cp instanceof Point // true 上面示例中,实例对象`cp`同时是`ColorPoint`和`Point`两个类的实例,这与 ES5 的行为完全一致。 -除了私有属性,父类的所有属性和方法,都会被子类继承,其中包括静态方法。 +## 私有属性和私有方法的继承 -```javascript -class A { - static hello() { - console.log('hello world'); - } -} - -class B extends A { -} - -B.hello() // hello world -``` - -上面代码中,`hello()`是`A`类的静态方法,`B`继承`A`,也继承了`A`的静态方法。 +父类所有的属性和方法,都会被子类继承,除了私有的属性和方法。 子类无法继承父类的私有属性,或者说,私有属性只能在定义它的 class 里面使用。 @@ -177,6 +164,64 @@ class Bar extends Foo { 上面示例中,`getP()`是父类用来读取私有属性的方法,通过该方法,子类就可以读到父类的私有属性。 +## 静态属性和静态方法的继承 + +父类的静态属性和静态方法,也会被子类继承。 + +```javascript +class A { + static hello() { + console.log('hello world'); + } +} + +class B extends A { +} + +B.hello() // hello world +``` + +上面代码中,`hello()`是`A`类的静态方法,`B`继承`A`,也继承了`A`的静态方法。 + +注意,静态属性是通过浅拷贝实现继承的。 + +```javascript +class A { static foo = 100; } +class B extends A { + constructor() { + super(); + B.foo--; + } +} + +const b = new B(); +B.foo // 99 +A.foo // 100 +``` + +上面示例中,`foo`是 A 类的静态属性,B 类继承了 A 类,因此也继承了这个属性。但是,在 B 类内部操作`B.foo`这个静态属性,影响不到`A.foo`,原因就是 B 类继承静态属性时,会采用浅拷贝,拷贝父类静态属性的值,因此`A.foo`和`B.foo`是两个彼此独立的属性。 + +但是,由于这种拷贝是浅拷贝,如果父类的静态属性的值是一个对象,那么子类的静态属性也会指向这个对象,因为浅拷贝只会拷贝对象的内存地址。 + +```javascript +class A { + static foo = { n: 100 }; +} + +class B extends A { + constructor() { + super(); + B.foo.n--; + } +} + +const b = new B(); +B.foo.n // 99 +A.foo.n // 99 +``` + +上面示例中,`A.foo`的值是一个对象,浅拷贝导致`B.foo`和`A.foo`指向同一个对象。所以,子类`B`修改这个对象的属性值,会影响到父类`A`。 + ## Object.getPrototypeOf() `Object.getPrototypeOf()`方法可以用来从子类上获取父类。 @@ -196,7 +241,7 @@ Object.getPrototypeOf(ColorPoint) === Point `super`这个关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同。 -第一种情况,`super`作为函数调用时,代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次`super`函数。 +第一种情况,`super`作为函数调用时,代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次`super()`函数。 ```javascript class A {} @@ -208,9 +253,11 @@ class B extends A { } ``` -上面代码中,子类`B`的构造函数之中的`super()`,代表调用父类的构造函数。这是必须的,否则 JavaScript 引擎会报错。 +上面代码中,子类`B`的构造函数之中的`super()`,代表调用父类的构造函数。这是必须的,否则报错。 + +调用`super()`的作用是形成子类的`this`对象,把父类的实例属性和方法放到这个`this`对象上面。子类在调用`super()`之前,是没有`this`对象的,任何对`this`的操作都要放在`super()`的后面。 -注意,`super`虽然代表了父类`A`的构造函数,但是返回的是子类`B`的实例,即`super`内部的`this`指的是`B`的实例,因此`super()`在这里相当于`A.prototype.constructor.call(this)`。 +注意,这里的`super`虽然代表了父类的构造函数,但是因为返回的是子类的`this`(即子类的实例对象),所以`super`内部的`this`代表子类的实例,而不是父类的实例,这里的`super()`相当于`A.prototype.constructor.call(this)`(在子类的`this`上运行父类的构造函数)。 ```javascript class A { @@ -227,7 +274,26 @@ new A() // A new B() // B ``` -上面代码中,`new.target`指向当前正在执行的函数。可以看到,在`super()`执行时,它指向的是子类`B`的构造函数,而不是父类`A`的构造函数。也就是说,`super()`内部的`this`指向的是`B`。 +上面示例中,`new.target`指向当前正在执行的函数。可以看到,在`super()`执行时(`new B()`),它指向的是子类`B`的构造函数,而不是父类`A`的构造函数。也就是说,`super()`内部的`this`指向的是`B`。 + +不过,由于`super()`在子类构造方法中执行时,子类的属性和方法还没有绑定到`this`,所以如果存在同名属性,此时拿到的是父类的属性。 + +```javascript +class A { + name = 'A'; + constructor() { + console.log('My name is ' + this.name); + } +} + +class B extends A { + name = 'B'; +} + +const b = new B(); // My name is A +``` + +上面示例中,最后一行输出的是`A`,而不是`B`,原因就在于`super()`执行时,`B`的`name`属性还没有绑定到`this`,所以`this.name`拿到的是`A`类的`name`属性。 作为函数时,`super()`只能用在子类的构造函数之中,用在其他地方就会报错。 diff --git a/docs/class.md b/docs/class.md index 606848b5b..15c2022fd 100644 --- a/docs/class.md +++ b/docs/class.md @@ -1,8 +1,6 @@ # Class 的基本语法 -## 简介 - -### 类的由来 +## 类的由来 JavaScript 语言中,生成实例对象的传统方法是通过构造函数。下面是一个例子。 @@ -122,7 +120,7 @@ Object.assign(Point.prototype, { }); ``` -`prototype`对象的`constructor()`属性,直接指向“类”的本身,这与 ES5 的行为是一致的。 +`prototype`对象的`constructor`属性,直接指向“类”的本身,这与 ES5 的行为是一致的。 ```javascript Point.prototype.constructor === Point // true @@ -166,7 +164,7 @@ Object.getOwnPropertyNames(Point.prototype) 上面代码采用 ES5 的写法,`toString()`方法就是可枚举的。 -### constructor 方法 +## constructor() 方法 `constructor()`方法是类的默认方法,通过`new`命令生成对象实例时,自动调用该方法。一个类必须有`constructor()`方法,如果没有显式定义,一个空的`constructor()`方法会被默认添加。 @@ -210,9 +208,9 @@ Foo() // TypeError: Class constructor Foo cannot be invoked without 'new' ``` -### 类的实例 +## 类的实例 -生成类的实例的写法,与 ES5 完全一样,也是使用`new`命令。前面说过,如果忘记加上`new`,像函数那样调用`Class`,将会报错。 +生成类的实例的写法,与 ES5 完全一样,也是使用`new`命令。前面说过,如果忘记加上`new`,像函数那样调用`Class()`,将会报错。 ```javascript class Point { @@ -226,12 +224,10 @@ var point = Point(2, 3); var point = new Point(2, 3); ``` -与 ES5 一样,实例的属性除非显式定义在其本身(即定义在`this`对象上),否则都是定义在原型上(即定义在`class`上)。 +类的属性和方法,除非显式定义在其本身(即定义在`this`对象上),否则都是定义在原型上(即定义在`class`上)。 ```javascript -//定义类 class Point { - constructor(x, y) { this.x = x; this.y = y; @@ -240,7 +236,6 @@ class Point { toString() { return '(' + this.x + ', ' + this.y + ')'; } - } var point = new Point(2, 3); @@ -269,7 +264,7 @@ p1.__proto__ === p2.__proto__ 这也意味着,可以通过实例的`__proto__`属性为“类”添加方法。 -> `__proto__` 并不是语言本身的特性,这是各大厂商具体实现时添加的私有属性,虽然目前很多现代浏览器的 JS 引擎中都提供了这个私有属性,但依旧不建议在生产中使用该属性,避免对环境产生依赖。生产环境中,我们可以使用 `Object.getPrototypeOf` 方法来获取实例对象的原型,然后再来为原型添加方法/属性。 +> `__proto__` 并不是语言本身的特性,这是各大厂商具体实现时添加的私有属性,虽然目前很多现代浏览器的 JS 引擎中都提供了这个私有属性,但依旧不建议在生产中使用该属性,避免对环境产生依赖。生产环境中,我们可以使用 `Object.getPrototypeOf()` 方法来获取实例对象的原型,然后再来为原型添加方法/属性。 ```javascript var p1 = new Point(2,3); @@ -286,7 +281,63 @@ p3.printName() // "Oops" 上面代码在`p1`的原型上添加了一个`printName()`方法,由于`p1`的原型就是`p2`的原型,因此`p2`也可以调用这个方法。而且,此后新建的实例`p3`也可以调用这个方法。这意味着,使用实例的`__proto__`属性改写原型,必须相当谨慎,不推荐使用,因为这会改变“类”的原始定义,影响到所有实例。 -### 取值函数(getter)和存值函数(setter) +## 实例属性的新写法 + +[ES2022](https://github.com/tc39/proposal-class-fields) 为类的实例属性,又规定了一种新写法。实例属性现在除了可以定义在`constructor()`方法里面的`this`上面,也可以定义在类内部的最顶层。 + +```javascript +// 原来的写法 +class IncreasingCounter { + constructor() { + this._count = 0; + } + get value() { + console.log('Getting the current value!'); + return this._count; + } + increment() { + this._count++; + } +} +``` + +上面示例中,实例属性`_count`定义在`constructor()`方法里面的`this`上面。 + +现在的新写法是,这个属性也可以定义在类的最顶层,其他都不变。 + +```javascript +class IncreasingCounter { + _count = 0; + get value() { + console.log('Getting the current value!'); + return this._count; + } + increment() { + this._count++; + } +} +``` + +上面代码中,实例属性`_count`与取值函数`value()`和`increment()`方法,处于同一个层级。这时,不需要在实例属性前面加上`this`。 + +注意,新写法定义的属性是实例对象自身的属性,而不是定义在实例对象的原型上面。 + +这种新写法的好处是,所有实例对象自身的属性都定义在类的头部,看上去比较整齐,一眼就能看出这个类有哪些实例属性。 + +```javascript +class foo { + bar = 'hello'; + baz = 'world'; + + constructor() { + // ... + } +} +``` + +上面的代码,一眼就能看出,`foo`类有两个实例属性,一目了然。另外,写起来也比较简洁。 + +## 取值函数(getter)和存值函数(setter) 与 ES5 一样,在“类”的内部可以使用`get`和`set`关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。 @@ -341,7 +392,7 @@ var descriptor = Object.getOwnPropertyDescriptor( 上面代码中,存值函数和取值函数是定义在`html`属性的描述对象上面,这与 ES5 完全一致。 -### 属性表达式 +## 属性表达式 类的属性名,可以采用表达式。 @@ -361,7 +412,7 @@ class Square { 上面代码中,`Square`类的方法名`getArea`,是从表达式得到的。 -### Class 表达式 +## Class 表达式 与函数一样,类也可以使用表达式的形式定义。 @@ -407,142 +458,6 @@ person.sayName(); // "张三" 上面代码中,`person`是一个立即执行的类的实例。 -### 注意点 - -**(1)严格模式** - -类和模块的内部,默认就是严格模式,所以不需要使用`use strict`指定运行模式。只要你的代码写在类或模块之中,就只有严格模式可用。考虑到未来所有的代码,其实都是运行在模块之中,所以 ES6 实际上把整个语言升级到了严格模式。 - -**(2)不存在提升** - -类不存在变量提升(hoist),这一点与 ES5 完全不同。 - -```javascript -new Foo(); // ReferenceError -class Foo {} -``` - -上面代码中,`Foo`类使用在前,定义在后,这样会报错,因为 ES6 不会把类的声明提升到代码头部。这种规定的原因与下文要提到的继承有关,必须保证子类在父类之后定义。 - -```javascript -{ - let Foo = class {}; - class Bar extends Foo { - } -} -``` - -上面的代码不会报错,因为`Bar`继承`Foo`的时候,`Foo`已经有定义了。但是,如果存在`class`的提升,上面代码就会报错,因为`class`会被提升到代码头部,而`let`命令是不提升的,所以导致`Bar`继承`Foo`的时候,`Foo`还没有定义。 - -**(3)name 属性** - -由于本质上,ES6 的类只是 ES5 的构造函数的一层包装,所以函数的许多特性都被`Class`继承,包括`name`属性。 - -```javascript -class Point {} -Point.name // "Point" -``` - -`name`属性总是返回紧跟在`class`关键字后面的类名。 - -**(4)Generator 方法** - -如果某个方法之前加上星号(`*`),就表示该方法是一个 Generator 函数。 - -```javascript -class Foo { - constructor(...args) { - this.args = args; - } - * [Symbol.iterator]() { - for (let arg of this.args) { - yield arg; - } - } -} - -for (let x of new Foo('hello', 'world')) { - console.log(x); -} -// hello -// world -``` - -上面代码中,`Foo`类的`Symbol.iterator`方法前有一个星号,表示该方法是一个 Generator 函数。`Symbol.iterator`方法返回一个`Foo`类的默认遍历器,`for...of`循环会自动调用这个遍历器。 - -**(5)this 的指向** - -类的方法内部如果含有`this`,它默认指向类的实例。但是,必须非常小心,一旦单独使用该方法,很可能报错。 - -```javascript -class Logger { - printName(name = 'there') { - this.print(`Hello ${name}`); - } - - print(text) { - console.log(text); - } -} - -const logger = new Logger(); -const { printName } = logger; -printName(); // TypeError: Cannot read property 'print' of undefined -``` - -上面代码中,`printName`方法中的`this`,默认指向`Logger`类的实例。但是,如果将这个方法提取出来单独使用,`this`会指向该方法运行时所在的环境(由于 class 内部是严格模式,所以 this 实际指向的是`undefined`),从而导致找不到`print`方法而报错。 - -一个比较简单的解决方法是,在构造方法中绑定`this`,这样就不会找不到`print`方法了。 - -```javascript -class Logger { - constructor() { - this.printName = this.printName.bind(this); - } - - // ... -} -``` - -另一种解决方法是使用箭头函数。 - -```javascript -class Obj { - constructor() { - this.getThis = () => this; - } -} - -const myObj = new Obj(); -myObj.getThis() === myObj // true -``` - -箭头函数内部的`this`总是指向定义时所在的对象。上面代码中,箭头函数位于构造函数内部,它的定义生效的时候,是在构造函数执行的时候。这时,箭头函数所在的运行环境,肯定是实例对象,所以`this`会总是指向实例对象。 - -还有一种解决方法是使用`Proxy`,获取方法的时候,自动绑定`this`。 - -```javascript -function selfish (target) { - const cache = new WeakMap(); - const handler = { - get (target, key) { - const value = Reflect.get(target, key); - if (typeof value !== 'function') { - return value; - } - if (!cache.has(value)) { - cache.set(value, value.bind(target)); - } - return cache.get(value); - } - }; - const proxy = new Proxy(target, handler); - return proxy; -} - -const logger = selfish(new Logger()); -``` - ## 静态方法 类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上`static`关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。 @@ -618,57 +533,6 @@ class Bar extends Foo { Bar.classMethod() // "hello, too" ``` -## 实例属性的新写法 - -实例属性除了定义在`constructor()`方法里面的`this`上面,也可以定义在类的最顶层。 - -```javascript -class IncreasingCounter { - constructor() { - this._count = 0; - } - get value() { - console.log('Getting the current value!'); - return this._count; - } - increment() { - this._count++; - } -} -``` - -上面代码中,实例属性`this._count`定义在`constructor()`方法里面。另一种写法是,这个属性也可以定义在类的最顶层,其他都不变。 - -```javascript -class IncreasingCounter { - _count = 0; - get value() { - console.log('Getting the current value!'); - return this._count; - } - increment() { - this._count++; - } -} -``` - -上面代码中,实例属性`_count`与取值函数`value()`和`increment()`方法,处于同一个层级。这时,不需要在实例属性前面加上`this`。 - -这种新写法的好处是,所有实例对象自身的属性都定义在类的头部,看上去比较整齐,一眼就能看出这个类有哪些实例属性。 - -```javascript -class foo { - bar = 'hello'; - baz = 'world'; - - constructor() { - // ... - } -} -``` - -上面的代码,一眼就能看出,`foo`类有两个实例属性,一目了然。另外,写起来也比较简洁。 - ## 静态属性 静态属性指的是 Class 本身的属性,即`Class.propName`,而不是定义在实例对象(`this`)上的属性。 @@ -714,9 +578,9 @@ class Foo { ## 私有方法和私有属性 -### 现有的解决方案 +### 早期解决方案 -私有方法和私有属性,是只能在类的内部访问的方法和属性,外部不能访问。这是常见需求,有利于代码的封装,但 ES6 不提供,只能通过变通方法模拟实现。 +私有方法和私有属性,是只能在类的内部访问的方法和属性,外部不能访问。这是常见需求,有利于代码的封装,但早期的 ES6 不提供,只能通过变通方法模拟实现。 一种做法是在命名上加以区别。 @@ -790,9 +654,9 @@ Reflect.ownKeys(myClass.prototype) 上面代码中,Symbol 值的属性名依然可以从类的外部拿到。 -### 私有属性的提案 +### 私有属性的正式写法 -目前,有一个[提案](https://github.com/tc39/proposal-private-methods),为`class`加了私有属性。方法是在属性名之前,使用`#`表示。 +[ES2022](https://github.com/tc39/proposal-class-fields)正式为`class`添加了私有属性,方法是在属性名之前使用`#`表示。 ```javascript class IncreasingCounter { @@ -815,9 +679,31 @@ counter.#count // 报错 counter.#count = 42 // 报错 ``` -上面代码在类的外部,读取私有属性,就会报错。 +上面示例中,在类的外部,读取或写入私有属性`#count`,都会报错。 + +注意,[从 Chrome 111 开始](https://developer.chrome.com/blog/new-in-devtools-111/#misc),开发者工具里面可以读写私有属性,不会报错,原因是 Chrome 团队认为这样方便调试。 -下面是另一个例子。 +另外,不管在类的内部或外部,读取一个不存在的私有属性,也都会报错。这跟公开属性的行为完全不同,如果读取一个不存在的公开属性,不会报错,只会返回`undefined`。 + +```javascript +class IncreasingCounter { + #count = 0; + get value() { + console.log('Getting the current value!'); + return this.#myCount; // 报错 + } + increment() { + this.#count++; + } +} + +const counter = new IncreasingCounter(); +counter.#myCount // 报错 +``` + +上面示例中,`#myCount`是一个不存在的私有属性,不管在函数内部或外部,读取该属性都会导致报错。 + +注意,私有属性的属性名必须包括`#`,如果不带`#`,会被当作另一个属性。 ```javascript class Point { @@ -839,8 +725,6 @@ class Point { 上面代码中,`#x`就是私有属性,在`Point`类之外是读取不到这个属性的。由于井号`#`是属性名的一部分,使用时必须带有`#`一起使用,所以`#x`和`x`是两个不同的属性。 -之所以要引入一个新的前缀`#`表示私有属性,而没有采用`private`关键字,是因为 JavaScript 是一门动态语言,没有类型声明,使用独立的符号似乎是唯一的比较方便可靠的方法,能够准确地区分一种属性是否为私有属性。另外,Ruby 语言使用`@`表示私有属性,ES6 没有用这个符号而使用`#`,是因为`@`已经被留给了 Decorator。 - 这种写法不仅可以写私有属性,还可以用来写私有方法。 ```javascript @@ -860,7 +744,7 @@ class Foo { } ``` -上面代码中,`#sum()`就是一个私有方法。 +上面示例中,`#sum()`就是一个私有方法。 另外,私有属性也可以设置 getter 和 setter 方法。 @@ -869,18 +753,17 @@ class Counter { #xValue = 0; constructor() { - super(); - // ... + console.log(this.#x); } - get #x() { return #xValue; } + get #x() { return this.#xValue; } set #x(value) { this.#xValue = value; } } ``` -上面代码中,`#x`是一个私有属性,它的读写都通过`get #x()`和`set #x()`来完成。 +上面代码中,`#x`是一个私有属性,它的读写都通过`get #x()`和`set #x()`操作另一个私有属性`#xValue`来完成。 私有属性不限于从`this`引用,只要是在类的内部,实例也可以引用私有属性。 @@ -926,118 +809,71 @@ FakeMath.#computeRandomNumber() // 报错 ### in 运算符 -`try...catch`结构可以用来判断是否存在某个私有属性。 +前面说过,直接访问某个类不存在的私有属性会报错,但是访问不存在的公开属性不会报错。这个特性可以用来判断,某个对象是否为类的实例。 ```javascript -class A { - use(obj) { +class C { + #brand; + + static isC(obj) { try { - obj.#foo; + obj.#brand; + return true; } catch { - // 私有属性 #foo 不存在 + return false; } } } - -const a = new A(); -a.use(a); // 报错 ``` -上面示例中,类`A`并不存在私有属性`#foo`,所以`try...catch`报错了。 +上面示例中,类`C`的静态方法`isC()`就用来判断,某个对象是否为`C`的实例。它采用的方法就是,访问该对象的私有属性`#brand`。如果不报错,就会返回`true`;如果报错,就说明该对象不是当前类的实例,从而`catch`部分返回`false`。 -这样的写法很麻烦,可读性很差,V8 引擎改进了`in`运算符,使它也可以用来判断私有属性。 +因此,`try...catch`结构可以用来判断某个私有属性是否存在。但是,这样的写法很麻烦,代码可读性很差,[ES2022](https://github.com/tc39/proposal-private-fields-in-in) 改进了`in`运算符,使它也可以用来判断私有属性。 ```javascript -class A { - use(obj) { - if (#foo in obj) { - // 私有属性 #foo 存在 +class C { + #brand; + + static isC(obj) { + if (#brand in obj) { + // 私有属性 #brand 存在 + return true; } else { - // 私有属性 #foo 不存在 + // 私有属性 #brand 不存在 + return false; } } } ``` -上面示例中,`in`运算符判断当前类`A`的实例,是否有私有属性`#foo`,如果有返回`true`,否则返回`false`。 +上面示例中,`in`运算符判断某个对象是否有私有属性`#brand`。它不会报错,而是返回一个布尔值。 -`in`也可以跟`this`一起配合使用。 +这种用法的`in`,也可以跟`this`一起配合使用。 ```javascript class A { #foo = 0; m() { console.log(#foo in this); // true - console.log(#bar in this); // false } } ``` -注意,判断私有属性时,`in`只能用在定义该私有属性的类的内部。 +注意,判断私有属性时,`in`只能用在类的内部。另外,判断所针对的私有属性,一定要先声明,否则会报错。 ```javascript class A { - #foo = 0; - static test(obj) { - console.log(#foo in obj); - } -} - -A.test(new A()) // true -A.test({}) // false - -class B { - #foo = 0; -} - -A.test(new B()) // false -``` - -上面示例中,类`A`的私有属性`#foo`,只能在类`A`内部使用`in`运算符判断,而且只对`A`的实例返回`true`,对于其他对象都返回`false`。 - -子类从父类继承的私有属性,也可以使用`in`运算符来判断。 - -```javascript -class A { - #foo = 0; - static test(obj) { - console.log(#foo in obj); - } -} - -class SubA extends A {}; - -A.test(new SubA()) // true -``` - -上面示例中,`SubA`从父类继承了私有属性`#foo`,`in`运算符也有效。 - -注意,`in`运算符对于`Object.create()`、`Object.setPrototypeOf`形成的继承,是无效的,因为这种继承不会传递私有属性。 - -```javascript -class A { - #foo = 0; - static test(obj) { - console.log(#foo in obj); + m() { + console.log(#foo in this); // 报错 } } -const a = new A(); - -const o1 = Object.create(a); -A.test(o1) // false -A.test(o1.__proto__) // true - -const o2 = {}; -Object.setPrototypeOf(o2, a); -A.test(o2) // false -A.test(o2.__proto__) // true ``` -上面示例中,对于修改原型链形成的继承,子类都取不到父类的私有属性,所以`in`运算符无效。 +上面示例中,私有属性`#foo`没有声明,就直接用于`in`运算符的判断,导致报错。 ## 静态块 -静态属性的一个问题是,它的初始化要么写在类的外部,要么写在`constructor()`方法里面。 +静态属性的一个问题是,如果它有初始化逻辑,这个逻辑要么写在类的外部,要么写在`constructor()`方法里面。 ```javascript class C { @@ -1056,9 +892,9 @@ try { } ``` -上面示例中,静态属性`y`和`z`的值依赖静态属性`x`,它们的初始化写在类的外部(上例的`try...catch`代码块)。另一种方法是写到类的`constructor()`方法里面。这两种方法都不是很理想,前者是将类的内部逻辑写到了外部,后者则是每次新建实例都会运行一次。 +上面示例中,静态属性`y`和`z`的值依赖于静态属性`x`的运算结果,这段初始化逻辑写在类的外部(上例的`try...catch`代码块)。另一种方法是写到类的`constructor()`方法里面。这两种方法都不是很理想,前者是将类的内部逻辑写到了外部,后者则是每次新建实例都会运行一次。 -为了解决这个问题,ES2022 引入了[静态块](https://github.com/tc39/proposal-class-static-block)(static block),允许在类的内部设置一个代码块,在类生成时运行一次,主要作用是对静态属性进行初始化。 +为了解决这个问题,ES2022 引入了[静态块](https://github.com/tc39/proposal-class-static-block)(static block),允许在类的内部设置一个代码块,在类生成时运行且只运行一次,主要作用是对静态属性进行初始化。以后,新建类的实例时,这个块就不运行了。 ```javascript class C { @@ -1082,11 +918,11 @@ class C { 上面代码中,类的内部有一个 static 代码块,这就是静态块。它的好处是将静态属性`y`和`z`的初始化逻辑,写入了类的内部,而且只运行一次。 -每个类只能有一个静态块,在静态属性声明后运行。静态块的内部不能有`return`语句。 +每个类允许有多个静态块,每个静态块中只能访问之前声明的静态属性。另外,静态块的内部不能有`return`语句。 静态块内部可以使用类名或`this`,指代当前类。 -```c +```javascript class C { static x = 1; static { @@ -1116,6 +952,142 @@ console.log(getX(new C())); // 1 上面示例中,`#x`是类的私有属性,如果类外部的`getX()`方法希望获取这个属性,以前是要写在类的`constructor()`方法里面,这样的话,每次新建实例都会定义一次`getX()`方法。现在可以写在静态块里面,这样的话,只在类生成时定义一次。 +## 类的注意点 + +### 严格模式 + +类和模块的内部,默认就是严格模式,所以不需要使用`use strict`指定运行模式。只要你的代码写在类或模块之中,就只有严格模式可用。考虑到未来所有的代码,其实都是运行在模块之中,所以 ES6 实际上把整个语言升级到了严格模式。 + +### 不存在提升 + +类不存在变量提升(hoist),这一点与 ES5 完全不同。 + +```javascript +new Foo(); // ReferenceError +class Foo {} +``` + +上面代码中,`Foo`类使用在前,定义在后,这样会报错,因为 ES6 不会把类的声明提升到代码头部。这种规定的原因与下文要提到的继承有关,必须保证子类在父类之后定义。 + +```javascript +{ + let Foo = class {}; + class Bar extends Foo { + } +} +``` + +上面的代码不会报错,因为`Bar`继承`Foo`的时候,`Foo`已经有定义了。但是,如果存在`class`的提升,上面代码就会报错,因为`class`会被提升到代码头部,而定义`Foo`的那一行没有提升,导致`Bar`继承`Foo`的时候,`Foo`还没有定义。 + +### name 属性 + +由于本质上,ES6 的类只是 ES5 的构造函数的一层包装,所以函数的许多特性都被`Class`继承,包括`name`属性。 + +```javascript +class Point {} +Point.name // "Point" +``` + +`name`属性总是返回紧跟在`class`关键字后面的类名。 + +### Generator 方法 + +如果某个方法之前加上星号(`*`),就表示该方法是一个 Generator 函数。 + +```javascript +class Foo { + constructor(...args) { + this.args = args; + } + * [Symbol.iterator]() { + for (let arg of this.args) { + yield arg; + } + } +} + +for (let x of new Foo('hello', 'world')) { + console.log(x); +} +// hello +// world +``` + +上面代码中,`Foo`类的`Symbol.iterator`方法前有一个星号,表示该方法是一个 Generator 函数。`Symbol.iterator`方法返回一个`Foo`类的默认遍历器,`for...of`循环会自动调用这个遍历器。 + +### this 的指向 + +类的方法内部如果含有`this`,它默认指向类的实例。但是,必须非常小心,一旦单独使用该方法,很可能报错。 + +```javascript +class Logger { + printName(name = 'there') { + this.print(`Hello ${name}`); + } + + print(text) { + console.log(text); + } +} + +const logger = new Logger(); +const { printName } = logger; +printName(); // TypeError: Cannot read property 'print' of undefined +``` + +上面代码中,`printName`方法中的`this`,默认指向`Logger`类的实例。但是,如果将这个方法提取出来单独使用,`this`会指向该方法运行时所在的环境(由于 class 内部是严格模式,所以 this 实际指向的是`undefined`),从而导致找不到`print`方法而报错。 + +一个比较简单的解决方法是,在构造方法中绑定`this`,这样就不会找不到`print`方法了。 + +```javascript +class Logger { + constructor() { + this.printName = this.printName.bind(this); + } + + // ... +} +``` + +另一种解决方法是使用箭头函数。 + +```javascript +class Obj { + constructor() { + this.getThis = () => this; + } +} + +const myObj = new Obj(); +myObj.getThis() === myObj // true +``` + +箭头函数内部的`this`总是指向定义时所在的对象。上面代码中,箭头函数位于构造函数内部,它的定义生效的时候,是在构造函数执行的时候。这时,箭头函数所在的运行环境,肯定是实例对象,所以`this`会总是指向实例对象。 + +还有一种解决方法是使用`Proxy`,获取方法的时候,自动绑定`this`。 + +```javascript +function selfish (target) { + const cache = new WeakMap(); + const handler = { + get (target, key) { + const value = Reflect.get(target, key); + if (typeof value !== 'function') { + return value; + } + if (!cache.has(value)) { + cache.set(value, value.bind(target)); + } + return cache.get(value); + } + }; + const proxy = new Proxy(target, handler); + return proxy; +} + +const logger = selfish(new Logger()); +``` + ## new.target 属性 `new`是从构造函数生成实例对象的命令。ES6 为`new`命令引入了一个`new.target`属性,该属性一般用在构造函数之中,返回`new`命令作用于的那个构造函数。如果构造函数不是通过`new`命令或`Reflect.construct()`调用的,`new.target`会返回`undefined`,因此这个属性可以用来确定构造函数是怎么调用的。 diff --git a/docs/decorator.md b/docs/decorator.md index 8d343a1d8..3a46d7119 100644 --- a/docs/decorator.md +++ b/docs/decorator.md @@ -1,10 +1,19 @@ # 装饰器 -[说明] Decorator 提案经过了大幅修改,目前还没有定案,不知道语法会不会再变。下面的内容完全依据以前的提案,已经有点过时了。等待定案以后,需要完全重写。 +[说明] Decorator 提案经历了重大的语法变化,目前处于第三阶段,定案之前不知道是否还有变化。本章现在属于草稿阶段,凡是标注“新语法”的章节,都是基于当前的语法,不过没有详细整理,只是一些原始材料;未标注“新语法”的章节基于以前的语法,是过去遗留的稿子。之所以保留以前的内容,有两个原因,一是 TypeScript 装饰器会用到这些语法,二是里面包含不少有价值的内容。等到标准完全定案,本章将彻底重写:删去过时内容,补充材料,增加解释。(2022年6月) -装饰器(Decorator)是一种与类(class)相关的语法,用来注释或修改类和类方法。许多面向对象的语言都有这项功能,目前有一个[提案](https://github.com/tc39/proposal-decorators)将其引入了 ECMAScript。 +## 简介(新语法) -装饰器是一种函数,写成`@ + 函数名`。它可以放在类和类方法的定义前面。 +装饰器(Decorator)用来增强 JavaScript 类(class)的功能,许多面向对象的语言都有这种语法,目前有一个[提案](https://github.com/tc39/proposal-decorators)将其引入了 ECMAScript。 + +装饰器是一种函数,写成`@ + 函数名`,可以用来装饰四种类型的值。 + +- 类 +- 类的属性 +- 类的方法 +- 属性存取器(accessor) + +下面的例子是装饰器放在类名和类方法名之前,大家可以感受一下写法。 ```javascript @frozen class Foo { @@ -17,7 +26,47 @@ } ``` -上面代码一共使用了四个装饰器,一个用在类本身,另外三个用在类方法。它们不仅增加了代码的可读性,清晰地表达了意图,而且提供一种方便的手段,增加或修改类的功能。 +上面代码一共使用了四个装饰器,一个用在类本身(@frozen),另外三个用在类方法(@configurable()、@enumerable()、@throttle())。它们不仅增加了代码的可读性,清晰地表达了意图,而且提供一种方便的手段,增加或修改类的功能。 + +## 装饰器 API(新语法) + +装饰器是一个函数,API 的类型描述如下(TypeScript 写法)。 + +```typescript +type Decorator = (value: Input, context: { + kind: string; + name: string | symbol; + access: { + get?(): unknown; + set?(value: unknown): void; + }; + private?: boolean; + static?: boolean; + addInitializer?(initializer: () => void): void; +}) => Output | void; +``` + +装饰器函数有两个参数。运行时,JavaScript 引擎会提供这两个参数。 + +- `value`:所要装饰的值,某些情况下可能是`undefined`(装饰属性时)。 +- `context`:上下文信息对象。 + +装饰器函数的返回值,是一个新版本的装饰对象,但也可以不返回任何值(void)。 + +`context`对象有很多属性,其中`kind`属性表示属于哪一种装饰,其他属性的含义如下。 + +- `kind`:字符串,表示装饰类型,可能的取值有`class`、`method`、`getter`、`setter`、`field`、`accessor`。 +- `name`:被装饰的值的名称: The name of the value, or in the case of private elements the description of it (e.g. the readable name). +- `access`:对象,包含访问这个值的方法,即存值器和取值器。 +- `static`: 布尔值,该值是否为静态元素。 +- `private`:布尔值,该值是否为私有元素。 +- `addInitializer`:函数,允许用户增加初始化逻辑。 + +装饰器的执行步骤如下。 + +1. 计算各个装饰器的值,按照从左到右,从上到下的顺序。 +1. 调用方法装饰器。 +1. 调用类装饰器。 ## 类的装饰 @@ -80,8 +129,6 @@ MyClass.isTestable // false 上面代码中,装饰器`testable`可以接受参数,这就等于可以修改装饰器的行为。 -注意,装饰器对类的行为的改变,是代码编译时发生的,而不是在运行时。这意味着,装饰器能在编译阶段运行代码。也就是说,装饰器本质就是编译时执行的函数。 - 前面的例子是为类添加一个静态属性,如果想添加实例属性,可以通过目标类的`prototype`对象操作。 ```javascript @@ -154,6 +201,156 @@ export default class MyReactComponent extends React.Component {} 相对来说,后一种写法看上去更容易理解。 +## 类装饰器(新语法) + +类装饰器的类型描述如下。 + +```typescript +type ClassDecorator = (value: Function, context: { + kind: "class"; + name: string | undefined; + addInitializer(initializer: () => void): void; +}) => Function | void; +``` + +类装饰器的第一个参数,就是被装饰的类。第二个参数是上下文对象,如果被装饰的类是一个匿名类,`name`属性就为`undefined`。 + +类装饰器可以返回一个新的类,取代原来的类,也可以不返回任何值。如果返回的不是构造函数,就会报错。 + +下面是一个例子。 + +```javascript +function logged(value, { kind, name }) { + if (kind === "class") { + return class extends value { + constructor(...args) { + super(...args); + console.log(`constructing an instance of ${name} with arguments ${args.join(", ")}`); + } + } + } + + // ... +} + +@logged +class C {} + +new C(1); +// constructing an instance of C with arguments 1 +``` + +如果不使用装饰器,类装饰器实际上执行的是下面的语法。 + +```javascript +class C {} + +C = logged(C, { + kind: "class", + name: "C", +}) ?? C; + +new C(1); +``` + +## 方法装饰器(新语法) + +方法装饰器会修改类的方法。 + +```javascript +class C { + @trace + toString() { + return 'C'; + } +} + +// 相当于 +C.prototype.toString = trace(C.prototype.toString); +``` + +上面示例中,`@trace`装饰`toString()`方法,就相当于修改了该方法。 + +方法装饰器使用 TypeScript 描述类型如下。 + +```typescript +type ClassMethodDecorator = (value: Function, context: { + kind: "method"; + name: string | symbol; + access: { get(): unknown }; + static: boolean; + private: boolean; + addInitializer(initializer: () => void): void; +}) => Function | void; +``` + +方法装饰器的第一个参数`value`,就是所要装饰的方法。 + +方法装饰器可以返回一个新函数,取代原来的方法,也可以不返回值,表示依然使用原来的方法。如果返回其他类型的值,就会报错。下面是一个例子。 + +```javascript +function replaceMethod() { + return function () { + return `How are you, ${this.name}?`; + } +} + +class Person { + constructor(name) { + this.name = name; + } + @replaceMethod + hello() { + return `Hi ${this.name}!`; + } +} + +const robin = new Person('Robin'); + +robin.hello(), 'How are you, Robin?' +``` + +上面示例中,`@replaceMethod`返回了一个新函数,取代了原来的`hello()`方法。 + +```typescript +function logged(value, { kind, name }) { + if (kind === "method") { + return function (...args) { + console.log(`starting ${name} with arguments ${args.join(", ")}`); + const ret = value.call(this, ...args); + console.log(`ending ${name}`); + return ret; + }; + } +} + +class C { + @logged + m(arg) {} +} + +new C().m(1); +// starting m with arguments 1 +// ending m +``` + +上面示例中,装饰器`@logged`返回一个函数,代替原来的`m()`方法。 + +这里的装饰器实际上是一个语法糖,真正的操作是像下面这样,改掉原型链上面`m()`方法。 + +```javascript +class C { + m(arg) {} +} + +C.prototype.m = logged(C.prototype.m, { + kind: "method", + name: "m", + static: false, + private: false, +}) ?? C.prototype.m; +``` + ## 方法的装饰 装饰器不仅可以装饰类,还可以装饰类的属性。 @@ -366,6 +563,355 @@ function loggingDecorator(wrapped) { const wrapped = loggingDecorator(doSomething); ``` +## 存取器装饰器(新语法) + +存取器装饰器使用 TypeScript 描述的类型如下。 + +```typescript +type ClassGetterDecorator = (value: Function, context: { + kind: "getter"; + name: string | symbol; + access: { get(): unknown }; + static: boolean; + private: boolean; + addInitializer(initializer: () => void): void; +}) => Function | void; + +type ClassSetterDecorator = (value: Function, context: { + kind: "setter"; + name: string | symbol; + access: { set(value: unknown): void }; + static: boolean; + private: boolean; + addInitializer(initializer: () => void): void; +}) => Function | void; +``` + +存取器装饰器的第一个参数就是原始的存值器(setter)和取值器(getter)。 + +存取器装饰器的返回值如果是一个函数,就会取代原来的存取器。本质上,就像方法装饰器一样,修改发生在类的原型对象上。它也可以不返回任何值,继续使用原来的存取器。如果返回其他类型的值,就会报错。 + +存取器装饰器对存值器(setter)和取值器(getter)是分开作用的。下面的例子里面,`@foo`只装饰`get x()`,不装饰`set x()`。 + +```javascript +class C { + @foo + get x() { + // ... + } + + set x(val) { + // ... + } +} +``` + +上一节的`@logged`装饰器稍加修改,就可以用在存取装饰器。 + +```javascript +function logged(value, { kind, name }) { + if (kind === "method" || kind === "getter" || kind === "setter") { + return function (...args) { + console.log(`starting ${name} with arguments ${args.join(", ")}`); + const ret = value.call(this, ...args); + console.log(`ending ${name}`); + return ret; + }; + } +} + +class C { + @logged + set x(arg) {} +} + +new C().x = 1 +// starting x with arguments 1 +// ending x +``` + +如果去掉语法糖,使用传统语法来写,就是改掉了类的原型链。 + +```javascript +class C { + set x(arg) {} +} + +let { set } = Object.getOwnPropertyDescriptor(C.prototype, "x"); +set = logged(set, { + kind: "setter", + name: "x", + static: false, + private: false, +}) ?? set; + +Object.defineProperty(C.prototype, "x", { set }); +``` + +## 属性装饰器(新语法) + +属性装饰器的类型描述如下。 + +```typescript +type ClassFieldDecorator = (value: undefined, context: { + kind: "field"; + name: string | symbol; + access: { get(): unknown, set(value: unknown): void }; + static: boolean; + private: boolean; +}) => (initialValue: unknown) => unknown | void; +``` + +属性装饰器的第一个参数是`undefined`,即不输入值。用户可以选择让装饰器返回一个初始化函数,当该属性被赋值时,这个初始化函数会自动运行,它会收到属性的初始值,然后返回一个新的初始值。属性装饰器也可以不返回任何值。除了这两种情况,返回其他类型的值都会报错。 + +下面是一个例子。 + +```javascript +function logged(value, { kind, name }) { + if (kind === "field") { + return function (initialValue) { + console.log(`initializing ${name} with value ${initialValue}`); + return initialValue; + }; + } + + // ... +} + +class C { + @logged x = 1; +} + +new C(); +// initializing x with value 1 +``` + +如果不使用装饰器语法,属性装饰器的实际作用如下。 + +```javascript +let initializeX = logged(undefined, { + kind: "field", + name: "x", + static: false, + private: false, +}) ?? (initialValue) => initialValue; + +class C { + x = initializeX.call(this, 1); +} +``` + +## accessor 命令(新语法) + +类装饰器引入了一个新命令`accessor`,用来属性的前缀。 + +```javascript +class C { + accessor x = 1; +} +``` + +它是一种简写形式,相当于声明属性`x`是私有属性`#x`的存取接口。上面的代码等同于下面的代码。 + +```javascript +class C { + #x = 1; + + get x() { + return this.#x; + } + + set x(val) { + this.#x = val; + } +} +``` + +`accessor`命令前面,还可以加上`static`命令和`private`命令。 + +```javascript +class C { + static accessor x = 1; + accessor #y = 2; +} +``` + +`accessor`命令前面还可以接受属性装饰器。 + +```javascript +function logged(value, { kind, name }) { + if (kind === "accessor") { + let { get, set } = value; + + return { + get() { + console.log(`getting ${name}`); + + return get.call(this); + }, + + set(val) { + console.log(`setting ${name} to ${val}`); + + return set.call(this, val); + }, + + init(initialValue) { + console.log(`initializing ${name} with value ${initialValue}`); + return initialValue; + } + }; + } + + // ... +} + +class C { + @logged accessor x = 1; +} + +let c = new C(); +// initializing x with value 1 +c.x; +// getting x +c.x = 123; +// setting x to 123 +``` + +上面的示例等同于使用`@logged`装饰器,改写`accessor`属性的 getter 和 setter 方法。 + +用于`accessor`的属性装饰器的类型描述如下。 + +```typescript +type ClassAutoAccessorDecorator = ( + value: { + get: () => unknown; + set(value: unknown) => void; + }, + context: { + kind: "accessor"; + name: string | symbol; + access: { get(): unknown, set(value: unknown): void }; + static: boolean; + private: boolean; + addInitializer(initializer: () => void): void; + } +) => { + get?: () => unknown; + set?: (value: unknown) => void; + initialize?: (initialValue: unknown) => unknown; +} | void; +``` + +`accessor`命令的第一个参数接收到的是一个对象,包含了`accessor`命令定义的属性的存取器 get 和 set。属性装饰器可以返回一个新对象,其中包含了新的存取器,用来取代原来的,即相当于拦截了原来的存取器。此外,返回的对象还可以包括一个`initialize`函数,用来改变私有属性的初始值。装饰器也可以不返回值,如果返回的是其他类型的值,或者包含其他属性的对象,就会报错。 + +## addInitializer() 方法(新语法) + +除了属性装饰器,其他装饰器的上下文对象还包括一个`addInitializer()`方法,用来完成初始化操作。 + +它的运行时间如下。 + +- 类装饰器:在类被完全定义之后。 +- 方法装饰器:在类构造期间运行,在属性初始化之前。 +- 静态方法装饰器:在类定义期间运行,早于静态属性定义,但晚于类方法的定义。 + +下面是一个例子。 + +```javascript +function customElement(name) { + return (value, { addInitializer }) => { + addInitializer(function() { + customElements.define(name, this); + }); + } +} + +@customElement('my-element') +class MyElement extends HTMLElement { + static get observedAttributes() { + return ['some', 'attrs']; + } +} +``` + +上面的代码等同于下面不使用装饰器的代码。 + +```javascript +class MyElement { + static get observedAttributes() { + return ['some', 'attrs']; + } +} + +let initializersForMyElement = []; + +MyElement = customElement('my-element')(MyElement, { + kind: "class", + name: "MyElement", + addInitializer(fn) { + initializersForMyElement.push(fn); + }, +}) ?? MyElement; + +for (let initializer of initializersForMyElement) { + initializer.call(MyElement); +} +``` + +下面是方法装饰器的例子。 + +```javascript +function bound(value, { name, addInitializer }) { + addInitializer(function () { + this[name] = this[name].bind(this); + }); +} + +class C { + message = "hello!"; + + @bound + m() { + console.log(this.message); + } +} + +let { m } = new C(); + +m(); // hello! +``` + +上面的代码等同于下面不使用装饰器的代码。 + +```javascript +class C { + constructor() { + for (let initializer of initializersForM) { + initializer.call(this); + } + + this.message = "hello!"; + } + + m() {} +} + +let initializersForM = [] + +C.prototype.m = bound( + C.prototype.m, + { + kind: "method", + name: "m", + static: false, + private: false, + addInitializer(fn) { + initializersForM.push(fn); + }, + } +) ?? C.prototype.m; +``` + ## core-decorators.js [core-decorators.js](https://github.com/jayphelps/core-decorators.js)是一个第三方模块,提供了几个常见的装饰器,通过它可以更好地理解装饰器。 diff --git a/docs/function.md b/docs/function.md index 6172c013c..99695e420 100644 --- a/docs/function.md +++ b/docs/function.md @@ -17,7 +17,7 @@ log('Hello', 'China') // Hello China log('Hello', '') // Hello World ``` -上面代码检查函数`log`的参数`y`有没有赋值,如果没有,则指定默认值为`World`。这种写法的缺点在于,如果参数`y`赋值了,但是对应的布尔值为`false`,则该赋值不起作用。就像上面代码的最后一行,参数`y`等于空字符,结果被改为默认值。 +上面代码检查函数`log()`的参数`y`有没有赋值,如果没有,则指定默认值为`World`。这种写法的缺点在于,如果参数`y`赋值了,但是对应的布尔值为`false`,则该赋值不起作用。就像上面代码的最后一行,参数`y`等于空字符,结果被改为默认值。 为了避免这个问题,通常需要先判断一下参数`y`是否被赋值,如果没有,再等于默认值。 @@ -93,7 +93,7 @@ x = 100; foo() // 101 ``` -上面代码中,参数`p`的默认值是`x + 1`。这时,每次调用函数`foo`,都会重新计算`x + 1`,而不是默认`p`等于 100。 +上面代码中,参数`p`的默认值是`x + 1`。这时,每次调用函数`foo()`,都会重新计算`x + 1`,而不是默认`p`等于 100。 ### 与解构赋值默认值结合使用 @@ -110,7 +110,7 @@ foo({x: 1, y: 2}) // 1 2 foo() // TypeError: Cannot read property 'x' of undefined ``` -上面代码只使用了对象的解构赋值默认值,没有使用函数参数的默认值。只有当函数`foo`的参数是一个对象时,变量`x`和`y`才会通过解构赋值生成。如果函数`foo`调用时没提供参数,变量`x`和`y`就不会生成,从而报错。通过提供函数参数的默认值,就可以避免这种情况。 +上面代码只使用了对象的解构赋值默认值,没有使用函数参数的默认值。只有当函数`foo()`的参数是一个对象时,变量`x`和`y`才会通过解构赋值生成。如果函数`foo()`调用时没提供参数,变量`x`和`y`就不会生成,从而报错。通过提供函数参数的默认值,就可以避免这种情况。 ```javascript function foo({x, y = 5} = {}) { @@ -136,7 +136,7 @@ fetch('http://example.com') // 报错 ``` -上面代码中,如果函数`fetch`的第二个参数是一个对象,就可以为它的三个属性设置默认值。这种写法不能省略第二个参数,如果结合函数参数的默认值,就可以省略第二个参数。这时,就出现了双重默认值。 +上面代码中,如果函数`fetch()`的第二个参数是一个对象,就可以为它的三个属性设置默认值。这种写法不能省略第二个参数,如果结合函数参数的默认值,就可以省略第二个参数。这时,就出现了双重默认值。 ```javascript function fetch(url, { body = '', method = 'GET', headers = {} } = {}) { @@ -149,7 +149,19 @@ fetch('http://example.com') 上面代码中,函数`fetch`没有第二个参数时,函数参数的默认值就会生效,然后才是解构赋值的默认值生效,变量`method`才会取到默认值`GET`。 -作为练习,请问下面两种写法有什么差别? +注意,函数参数的默认值生效以后,参数解构赋值依然会进行。 + +```javascript +function f({ a, b = 'world' } = { a: 'hello' }) { + console.log(b); +} + +f() // world +``` + +上面示例中,函数`f()`调用时没有参数,所以参数默认值`{ a: 'hello' }`生效,然后再对这个默认值进行解构赋值,从而触发参数变量`b`的默认值生效。 + +作为练习,大家可以思考一下,下面两种函数写法有什么差别? ```javascript // 写法一 @@ -161,11 +173,7 @@ function m1({x = 0, y = 0} = {}) { function m2({x, y} = { x: 0, y: 0 }) { return [x, y]; } -``` -上面两种写法都对函数的参数设定了默认值,区别是写法一函数参数的默认值是空对象,但是设置了对象解构赋值的默认值;写法二函数参数的默认值是一个有具体属性的对象,但是没有设置对象解构赋值的默认值。 - -```javascript // 函数没有参数的情况 m1() // [0, 0] m2() // [0, 0] diff --git a/docs/generator.md b/docs/generator.md index 7d18278ca..f93604c0b 100644 --- a/docs/generator.md +++ b/docs/generator.md @@ -259,7 +259,7 @@ b.next(12) // { value:8, done:false } b.next(13) // { value:42, done:true } ``` -上面代码中,第二次运行`next`方法的时候不带参数,导致 y 的值等于`2 * undefined`(即`NaN`),除以 3 以后还是`NaN`,因此返回对象的`value`属性也等于`NaN`。第三次运行`Next`方法的时候不带参数,所以`z`等于`undefined`,返回对象的`value`属性等于`5 + NaN + undefined`,即`NaN`。 +上面代码中,第二次运行`next`方法的时候不带参数,导致 y 的值等于`2 * undefined`(即`NaN`),除以 3 以后还是`NaN`,因此返回对象的`value`属性也等于`NaN`。第三次运行`next`方法的时候不带参数,所以`z`等于`undefined`,返回对象的`value`属性等于`5 + NaN + undefined`,即`NaN`。 如果向`next`方法提供参数,返回结果就完全不一样了。上面代码第一次调用`b`的`next`方法时,返回`x+1`的值`6`;第二次调用`next`方法,将上一次`yield`表达式的值设为`12`,因此`y`等于`24`,返回`y / 3`的值`8`;第三次调用`next`方法,将上一次`yield`表达式的值设为`13`,因此`z`等于`13`,这时`x`等于`5`,`y`等于`24`,所以`return`语句的值等于`42`。 @@ -552,26 +552,26 @@ g.throw(1); 上面代码中,`g.throw(1)`执行时,`next`方法一次都没有执行过。这时,抛出的错误不会被内部捕获,而是直接在外部抛出,导致程序出错。这种行为其实很好理解,因为第一次执行`next`方法,等同于启动执行 Generator 函数的内部代码,否则 Generator 函数还没有开始执行,这时`throw`方法抛错只可能抛出在函数外部。 -`throw`方法被捕获以后,会附带执行下一条`yield`表达式。也就是说,会附带执行一次`next`方法。 +`throw`方法被内部捕获以后,会附带执行到下一条`yield`表达式,这种情况下等同于执行一次`next`方法。 ```javascript var gen = function* gen(){ try { - yield console.log('a'); + yield 1; } catch (e) { - // ... + yield 2; } - yield console.log('b'); - yield console.log('c'); + yield 3; } var g = gen(); -g.next() // a -g.throw() // b -g.next() // c +g.next() // { value:1, done:false } +g.throw() // { value:2, done:false } +g.next() // { value:3, done:false } +g.next() // { value:undefined, done:true } ``` -上面代码中,`g.throw`方法被捕获以后,自动执行了一次`next`方法,所以会打印`b`。另外,也可以看到,只要 Generator 函数内部部署了`try...catch`代码块,那么遍历器的`throw`方法抛出的错误,不影响下一次遍历。 +上面代码中,`g.throw`方法被内部捕获以后,等同于执行了一次`next`方法,所以返回`{ value:2, done:false }`。另外,也可以看到,只要 Generator 函数内部部署了`try...catch`代码块,那么遍历器的`throw`方法抛出的错误,不影响下一次遍历。 另外,`throw`命令与`g.throw`方法是无关的,两者互不影响。 diff --git a/docs/intro.md b/docs/intro.md index 880cf08b7..ea1695911 100644 --- a/docs/intro.md +++ b/docs/intro.md @@ -68,9 +68,9 @@ ES6 从开始制定到最后发布,整整用了 15 年。 2015 年 6 月,ECMAScript 6 正式通过,成为国际标准。从 2000 年算起,这时已经过去了 15 年。 -目前,各大浏览器对 ES6 的支持可以查看[kangax.github.io/compat-table/es6/](https://kangax.github.io/compat-table/es6/)。 +目前,各大浏览器对 ES6 的支持可以查看[https://compat-table.github.io/compat-table/es6/](https://compat-table.github.io/compat-table/es6/)。 -Node.js 是 JavaScript 的服务器运行环境(runtime)。它对 ES6 的支持度更高。除了那些默认打开的功能,还有一些语法功能已经实现了,但是默认没有打开。使用下面的命令,可以查看 Node.js 默认没有打开的 ES6 实验性语法。 +Node.js 是 JavaScript 的服务器运行环境(runtime)。它对 ES6 的支持度更高。除了那些默认打开的功能,还有一些语法功能已经实现了,但是默认没有打开。使用下面的命令,可以查看 Node.js 默认没有打开的实验性语法。 ```bash // Linux & Mac diff --git a/docs/iterator.md b/docs/iterator.md index 31949d821..198ba2d5a 100644 --- a/docs/iterator.md +++ b/docs/iterator.md @@ -818,3 +818,43 @@ for (var n of fibonacci) { ``` 上面的例子,会输出斐波纳契数列小于等于 1000 的项。如果当前项大于 1000,就会使用`break`语句跳出`for...of`循环。 + +## 遍历器对象的工具方法 + +ES2025 为遍历器接口返回的遍历器对象,添加了一些工具方法,便于处理数据。 + +```javascript +const arr = ['a', '', 'b', '', 'c', '', 'd', '', 'e']; + +arr.values() // creates an iterator + .filter(x => x.length > 0) + .drop(1) + .take(3) + .map(x => `=${x}=`) + .toArray() +// ['=b=', '=c=', '=d='] +``` + +上面示例中,arr 是一个数组,它的 values() 方法返回的是一个遍历器对象,以前要使用 for...of 循环来处理,现在有了工具方法,就可以直接链式处理了。 + +遍历器对象的工具方法,基本上与数组方法是对应的。 + +- 返回遍历器对象的方法 + - iterator.filter(filterFn) + - iterator.map(mapFn) + - iterator.flatMap(mapFn) +- 返回布尔值的方法 + - iterator.some(fn) + - iterator.every(fn) +- 返回其他值的方法 + - iterator.find(fn) + - iterator.reduce(reducer, initialValue?) +- 不返回值的方法 + - iterator.forEach(fn) + +以下是遍历器对象独有的方法。 + +- iterator.drop(limit):返回一个遍历器对象,丢弃前 limit 个成员。 +- iterator.take(limit):返回一个遍历器对象,包含前 limit 个成员。 +- iterator.toArray():返回一个数组,包含所有成员。 + diff --git a/docs/module-loader.md b/docs/module-loader.md index 9a99b59ef..31dd685e7 100644 --- a/docs/module-loader.md +++ b/docs/module-loader.md @@ -397,7 +397,7 @@ import submodule from './node_modules/es-module-package/private-module.js'; } ``` -由于`exports`字段只有支持 ES6 的 Node.js 才认识,所以可以用来兼容旧版本的 Node.js。 +由于`exports`字段只有支持 ES6 的 Node.js 才认识,所以可以搭配`main`字段,来兼容旧版本的 Node.js。 ```javascript { @@ -412,7 +412,7 @@ import submodule from './node_modules/es-module-package/private-module.js'; **(3)条件加载** -利用`.`这个别名,可以为 ES6 模块和 CommonJS 指定不同的入口。目前,这个功能需要在 Node.js 运行的时候,打开`--experimental-conditional-exports`标志。 +利用`.`这个别名,可以为 ES6 模块和 CommonJS 指定不同的入口。 ```javascript { diff --git a/docs/module.md b/docs/module.md index 6c773c4cc..a5b182456 100644 --- a/docs/module.md +++ b/docs/module.md @@ -162,6 +162,8 @@ function f() {} export {f}; ``` +目前,export 命令能够对外输出的就是三种接口:函数(Functions), 类(Classes),var、let、const 声明的变量(Variables)。 + 另外,`export`语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。 ```javascript @@ -556,9 +558,40 @@ import * as ns from "mod"; export {ns}; ``` +## import 属性 + +ES2025 引入了“[import 属性](https://github.com/tc39/proposal-import-attributes)”(import attributes),允许为 import 命令设置属性,主要用于导入非模块的代码,比如 JSON 数据、WebAssembly 代码、CSS 代码。 + +目前,只支持导入 JSON 数据。 + +```javascript +// 静态导入 +import configData from './config-data.json' with { type: 'json' }; + +// 动态导入 +const configData = await import( + './config-data.json', { with: { type: 'json' } } +); +``` + +上面代码中,import 命令使用 with 子句,指定一个属性对象。这个属性对象目前只有一个 type 属性,它的值就是导入代码的类型,现在只能设置为`json`一个值。 + +如果没有 import 属性,导入 JSON 数据只能使用 fetch 命令。 + +```javascript +const response = await fetch('./config.json'); +const json = await response.json(); +``` + +export 命令与 import 命令写在一起,形成一个再导出语句时,也可以使用 import 属性。 + +```javascript +export { default as config } from './config-data.json' with { type: 'json' }; +``` + ## 模块的继承 -模块之间也可以继承。 +模块可以继承。 假设有一个`circleplus`模块,继承了`circle`模块。 @@ -692,7 +725,28 @@ import(`./section-modules/${someVariable}.js`) }); ``` -`import()`函数可以用在任何地方,不仅仅是模块,非模块的脚本也可以使用。它是运行时执行,也就是说,什么时候运行到这一句,就会加载指定的模块。另外,`import()`函数与所加载的模块没有静态连接关系,这点也是与`import`语句不相同。`import()`类似于 Node 的`require`方法,区别主要是前者是异步加载,后者是同步加载。 +`import()`函数可以用在任何地方,不仅仅是模块,非模块的脚本也可以使用。它是运行时执行,也就是说,什么时候运行到这一句,就会加载指定的模块。另外,`import()`函数与所加载的模块没有静态连接关系,这点也是与`import`语句不相同。`import()`类似于 Node.js 的`require()`方法,区别主要是前者是异步加载,后者是同步加载。 + +由于`import()`返回 Promise +对象,所以需要使用`then()`方法指定处理函数。考虑到代码的清晰,更推荐使用`await`命令。 + +```javascript +async function renderWidget() { + const container = document.getElementById('widget'); + if (container !== null) { + // 等同于 + // import("./widget").then(widget => { + // widget.render(container); + // }); + const widget = await import('./widget.js'); + widget.render(container); + } +} + +renderWidget(); +``` + +上面示例中,`await`命令后面就是使用`import()`,对比`then()`的写法明显更简洁易读。 ### 适用场合 @@ -801,3 +855,45 @@ async function main() { main(); ``` +## import.meta + +开发者使用一个模块时,有时需要知道模板本身的一些信息(比如模块的路径)。[ES2020](https://github.com/tc39/proposal-import-meta) 为 import 命令添加了一个元属性`import.meta`,返回当前模块的元信息。 + +`import.meta`只能在模块内部使用,如果在模块外部使用会报错。 + +这个属性返回一个对象,该对象的各种属性就是当前运行的脚本的元信息。具体包含哪些属性,标准没有规定,由各个运行环境自行决定。一般来说,`import.meta`至少会有下面两个属性。 + +**(1)import.meta.url** + +`import.meta.url`返回当前模块的 URL 路径。举例来说,当前模块主文件的路径是`https://foo.com/main.js`,`import.meta.url`就返回这个路径。如果模块里面还有一个数据文件`data.txt`,那么就可以用下面的代码,获取这个数据文件的路径。 + +```javascript +new URL('data.txt', import.meta.url) +``` + +注意,Node.js 环境中,`import.meta.url`返回的总是本地路径,即`file:URL`协议的字符串,比如`file:///home/user/foo.js`。 + +**(2)import.meta.scriptElement** + +`import.meta.scriptElement`是浏览器特有的元属性,返回加载模块的那个` + +// my-module.js 内部执行下面的代码 +import.meta.scriptElement.dataset.foo +// "abc" +``` + +**(3)其他** + +Deno 现在还支持`import.meta.filename`和`import.meta.dirname`属性,对应 CommonJS 模块系统的`__filename`和`__dirname`属性。 + +- `import.meta.filename`:当前模块文件的绝对路径。 +- `import.meta.dirname`:当前模块文件的目录的绝对路径。 + +这两个属性都提供当前平台的正确的路径分隔符,比如 Linux 系统返回`/dev/my_module.ts`,Windows 系统返回`C:\dev\my_module.ts`。 + +本地模块可以使用这两个属性,远程模块也可以使用。 + diff --git a/docs/number.md b/docs/number.md index e9904c567..820c03e28 100644 --- a/docs/number.md +++ b/docs/number.md @@ -652,6 +652,21 @@ Math.hypot(-3); // 3 如果参数不是数值,`Math.hypot`方法会将其转为数值。只要有一个参数无法转为数值,就会返回 NaN。 +### Math.f16round() + +ES2025 新增了 Math.f16round() 方法,返回最接近输入值的16位半精度浮点数。 + +```javascript +Math.f16round(5) // 5 +Math.f16round(5.05) // 5.05078125 +``` + +16位浮点数共使用16个二进制位,其中指数使用5位,符号位使用1位,精度使用10位,因此可以表示 ±65,504 范围内的值,精度可以到达 1/1024。如果一个数超出了值的范围,则该方法返回 infinity。 + +```javascript +Math.f16round(100000) // Infinity +``` + ### 对数方法 ES6 新增了 4 个对数相关方法。 @@ -820,7 +835,7 @@ for (let i = 1; i <= 70; i++) { console.log(p); // 1.197857166996989e+100 ``` -现在支持大整数了,就可以算了,浏览器的开发者工具运行下面代码,就OK。 +现在支持大整数了,就可以算了,浏览器的开发者工具运行下面代码,就 OK。 ```javascript let p = 1n; @@ -869,11 +884,10 @@ BigInt 继承了 Object 对象的两个实例方法。 - `BigInt.prototype.toLocaleString()` -此外,还提供了三个静态方法。 +此外,还提供了两个静态方法。 - `BigInt.asUintN(width, BigInt)`: 给定的 BigInt 转为 0 到 2width - 1 之间对应的值。 - `BigInt.asIntN(width, BigInt)`:给定的 BigInt 转为 -2width - 1 到 2width - 1 - 1 之间对应的值。 -- `BigInt.parseInt(string[, radix])`:近似于`Number.parseInt()`,将一个字符串转换成指定进制的 BigInt。 ```javascript const max = 2n ** (64n - 1n) - 1n; @@ -899,18 +913,6 @@ BigInt.asUintN(32, max) // 4294967295n 上面代码中,`max`是一个64位的 BigInt,如果转为32位,前面的32位都会被舍弃。 -下面是`BigInt.parseInt()`的例子。 - -```javascript -// Number.parseInt() 与 BigInt.parseInt() 的对比 -Number.parseInt('9007199254740993', 10) -// 9007199254740992 -BigInt.parseInt('9007199254740993', 10) -// 9007199254740993n -``` - -上面代码中,由于有效数字超出了最大限度,`Number.parseInt`方法返回的结果是不精确的,而`BigInt.parseInt`方法正确返回了对应的 BigInt。 - 对于二进制数组,BigInt 新增了两个类型`BigUint64Array`和`BigInt64Array`,这两种数据类型返回的都是64位 BigInt。`DataView`对象的实例方法`DataView.prototype.getBigInt64()`和`DataView.prototype.getBigUint64()`,返回的也是 BigInt。 ### 转换规则 diff --git a/docs/object-methods.md b/docs/object-methods.md index a9d8200f3..999496de0 100644 --- a/docs/object-methods.md +++ b/docs/object-methods.md @@ -833,3 +833,31 @@ Object.fromEntries(map) Object.fromEntries(new URLSearchParams('foo=bar&baz=qux')) // { foo: "bar", baz: "qux" } ``` + +## Object.hasOwn() + +JavaScript 对象的属性分成两种:自身的属性和继承的属性。对象实例有一个`hasOwnProperty()`方法,可以判断某个属性是否为原生属性。ES2022 在`Object`对象上面新增了一个静态方法[`Object.hasOwn()`](https://github.com/tc39/proposal-accessible-object-hasownproperty),也可以判断是否为自身的属性。 + +`Object.hasOwn()`可以接受两个参数,第一个是所要判断的对象,第二个是属性名。 + +```javascript +const foo = Object.create({ a: 123 }); +foo.b = 456; + +Object.hasOwn(foo, 'a') // false +Object.hasOwn(foo, 'b') // true +``` + +上面示例中,对象`foo`的属性`a`是继承属性,属性`b`是原生属性。`Object.hasOwn()`对属性`a`返回`false`,对属性`b`返回`true`。 + +`Object.hasOwn()`的一个好处是,对于不继承`Object.prototype`的对象不会报错,而`hasOwnProperty()`是会报错的。 + +```javascript +const obj = Object.create(null); + +obj.hasOwnProperty('foo') // 报错 +Object.hasOwn(obj, 'foo') // false +``` + +上面示例中,`Object.create(null)`返回的对象`obj`是没有原型的,不继承任何属性,这导致调用`obj.hasOwnProperty()`会报错,但是`Object.hasOwn()`就能正确处理这种情况。 + diff --git a/docs/object.md b/docs/object.md index f90684462..8abd76c23 100644 --- a/docs/object.md +++ b/docs/object.md @@ -606,6 +606,23 @@ foo // {0: "h", 1: "e", 2: "l", 3: "l", 4: "o"} ``` +对象的扩展运算符,只会返回参数对象自身的、可枚举的属性,这一点要特别小心,尤其是用于类的实例对象时。 + +```javascript +class C { + p = 12; + m() {} +} + +let c = new C(); +let clone = { ...c }; + +clone.p; // ok +clone.m(); // 报错 +``` + +上面示例中,`c`是`C`类的实例对象,对其进行扩展运算时,只会返回`c`自身的属性`c.p`,而不会返回`c`的方法`c.m()`,因为这个方法定义在`C`的原型对象上(详见 Class 的章节)。 + 对象的扩展运算符等同于使用`Object.assign()`方法。 ```javascript @@ -708,7 +725,7 @@ let aWithXGetter = { ...a }; // 报错 ES2021 标准之中,为了配合新增的`Promise.any()`方法(详见《Promise 对象》一章),还引入一个新的错误对象`AggregateError`,也放在这一章介绍。 -AggregateError 在一个错误对象里面,封装了多个错误。如果某个单一操作,同时引发了多个错误,,需要同时抛出这些错误,那么就可以抛出一个 AggregateError 错误对象,把各种错误都放在这个对象里面。 +AggregateError 在一个错误对象里面,封装了多个错误。如果某个单一操作,同时引发了多个错误,需要同时抛出这些错误,那么就可以抛出一个 AggregateError 错误对象,把各种错误都放在这个对象里面。 AggregateError 本身是一个构造函数,用来生成 AggregateError 实例对象。 @@ -753,3 +770,28 @@ try { } ``` +## 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`属性,写入报错的原因。然后,就可以从实例对象上读取这个属性。 + +`cause`属性可以放置任意内容,不必一定是字符串。 + +```javascript +try { + maybeWorks(); +} catch (err) { + throw new Error('maybeWorks failed!', { cause: err }); +} +``` + +上面示例中,`cause`属性放置的就是一个对象。 + diff --git a/docs/operator.md b/docs/operator.md index 6d72c666c..fea9e9096 100644 --- a/docs/operator.md +++ b/docs/operator.md @@ -307,3 +307,45 @@ function example(opts) { } ``` +## `#!`命令 + +Unix 的命令行脚本都支持`#!`命令,又称为 Shebang 或 Hashbang。这个命令放在脚本的第一行,用来指定脚本的执行器。 + +比如 Bash 脚本的第一行。 + +```bash +#!/bin/sh +``` + +Python 脚本的第一行。 + +```python +#!/usr/bin/env python +``` + +[ES2023](https://github.com/tc39/proposal-hashbang) 为 JavaScript 脚本引入了`#!`命令,写在脚本文件或者模块文件的第一行。 + +```javascript +// 写在脚本文件第一行 +#!/usr/bin/env node +'use strict'; +console.log(1); + +// 写在模块文件第一行 +#!/usr/bin/env node +export {}; +console.log(1); +``` + +有了这一行以后,Unix 命令行就可以直接执行脚本。 + +```bash +# 以前执行脚本的方式 +$ node hello.js + +# hashbang 的方式 +$ ./hello.js +``` + +对于 JavaScript 引擎来说,会把`#!`理解成注释,忽略掉这一行。 + diff --git a/docs/promise.md b/docs/promise.md index 0474ceed5..9f6b3fd06 100644 --- a/docs/promise.md +++ b/docs/promise.md @@ -79,7 +79,7 @@ let promise = new Promise(function(resolve, reject) { }); promise.then(function() { - console.log('resolved.'); + console.log('resolved'); }); console.log('Hi!'); @@ -811,17 +811,7 @@ try { 上面代码中,`Promise.any()`方法的参数数组包含三个 Promise 操作。其中只要有一个变成`fulfilled`,`Promise.any()`返回的 Promise 对象就变成`fulfilled`。如果所有三个操作都变成`rejected`,那么`await`命令就会抛出错误。 -`Promise.any()`抛出的错误,不是一个一般的 Error 错误对象,而是一个 AggregateError 实例。它相当于一个数组,每个成员对应一个被`rejected`的操作所抛出的错误。下面是 AggregateError 的实现示例。 - -```javascript -// new AggregateError() extends Array - -const err = new AggregateError(); -err.push(new Error("first error")); -err.push(new Error("second error")); -// ... -throw err; -``` +`Promise.any()`抛出的错误是一个 AggregateError 实例(详见《对象的扩展》一章),这个 AggregateError 实例对象的`errors`属性是一个数组,包含了所有成员的错误。 下面是一个例子。 @@ -835,7 +825,8 @@ Promise.any([resolved, rejected, alsoRejected]).then(function (result) { }); Promise.any([rejected, alsoRejected]).catch(function (results) { - console.log(results); // [-1, Infinity] + console.log(results instanceof AggregateError); // true + console.log(results.errors); // [-1, Infinity] }); ``` @@ -1091,7 +1082,7 @@ console.log('next'); 上面代码也是使用立即执行的匿名函数,执行`new Promise()`。这种情况下,同步函数也是同步执行的。 -鉴于这是一个很常见的需求,所以现在有一个[提案](https://github.com/ljharb/proposal-promise-try),提供`Promise.try`方法替代上面的写法。 +鉴于这是一个很常见的需求,所以 [ES2025](https://github.com/ljharb/proposal-promise-try) 提供了`Promise.try()`方法替代上面的写法。 ```javascript const f = () => console.log('now'); diff --git a/docs/proposals.md b/docs/proposals.md index 2e7a2282a..f6178b5e0 100644 --- a/docs/proposals.md +++ b/docs/proposals.md @@ -314,21 +314,33 @@ const userAge = userId |> await fetchUserById |> getAgeFromUser; const userAge = getAgeFromUser(await fetchUserById(userId)); ``` +管道运算符对多步骤的数据处理,非常有用。 + +```javascript +const numbers = [10, 20, 30, 40, 50]; + +const processedNumbers = numbers + |> (_ => _.map(n => n / 2)) // [5, 10, 15, 20, 25] + |> (_ => _.filter(n => n > 10)); // [15, 20, 25] +``` + +上面示例中,管道运算符可以清晰表达数据处理的每一步,增加代码的可读性。 + ## Math.signbit() -`Math.sign()`用来判断一个值的正负,但是如果参数是`-0`,它会返回`-0`。 +JavaScript 内部使用64位浮点数(国际标准 IEEE 754)表示数值。IEEE 754 规定,64位浮点数的第一位是符号位,`0`表示正数,`1`表示负数。所以会有两种零,`+0`是符号位为`0`时的零,`-0`是符号位为`1`时的零。实际编程中,判断一个值是`+0`还是`-0`非常麻烦,因为它们是相等的。 ```javascript -Math.sign(-0) // -0 ++0 === -0 // true ``` -这导致对于判断符号位的正负,`Math.sign()`不是很有用。JavaScript 内部使用 64 位浮点数(国际标准 IEEE 754)表示数值,IEEE 754 规定第一位是符号位,`0`表示正数,`1`表示负数。所以会有两种零,`+0`是符号位为`0`时的零值,`-0`是符号位为`1`时的零值。实际编程中,判断一个值是`+0`还是`-0`非常麻烦,因为它们是相等的。 +ES6 新增的`Math.sign()`方法,只能用来判断数值的正负,对于判断数值的符号位用处不大。因为如果参数是`-0`,它会返回`-0`,还是不能直接知道符号位是`1`还是`0`。 ```javascript -+0 === -0 // true +Math.sign(-0) // -0 ``` -目前,有一个[提案](https://jfbastien.github.io/papers/Math.signbit.html),引入了`Math.signbit()`方法判断一个数的符号位是否设置了。 +目前,有一个[提案](https://github.com/tc39/proposal-Math.signbit),引入了`Math.signbit()`方法判断一个数的符号位是否设置了。 ```javascript Math.signbit(2) //false @@ -348,7 +360,7 @@ Math.signbit(-0) //true ## 双冒号运算符 -箭头函数可以绑定`this`对象,大大减少了显式绑定`this`对象的写法(`call`、`apply`、`bind`)。但是,箭头函数并不适用于所有场合,所以现在有一个[提案](https://github.com/zenparsing/es-function-bind),提出了“函数绑定”(function bind)运算符,用来取代`call`、`apply`、`bind`调用。 +箭头函数可以绑定`this`对象,大大减少了显式绑定`this`对象的写法(`call()`、`apply()`、`bind()`)。但是,箭头函数并不适用于所有场合,所以现在有一个[提案](https://github.com/zenparsing/es-function-bind),提出了“函数绑定”(function bind)运算符,用来取代`call()`、`apply()`、`bind()`调用。 函数绑定运算符是并排的两个冒号(`::`),双冒号左边是一个对象,右边是一个函数。该运算符会自动将左边的对象,作为上下文环境(即`this`对象),绑定到右边的函数上面。 @@ -482,120 +494,3 @@ class FakeWindow extends Realm { 上面代码中,`FakeWindow`模拟了一个假的顶层对象`window`。 -## `#!`命令 - -Unix 的命令行脚本都支持`#!`命令,又称为 Shebang 或 Hashbang。这个命令放在脚本的第一行,用来指定脚本的执行器。 - -比如 Bash 脚本的第一行。 - -```bash -#!/bin/sh -``` - -Python 脚本的第一行。 - -```python -#!/usr/bin/env python -``` - -现在有一个[提案](https://github.com/tc39/proposal-hashbang),为 JavaScript 脚本引入了`#!`命令,写在脚本文件或者模块文件的第一行。 - -```javascript -// 写在脚本文件第一行 -#!/usr/bin/env node -'use strict'; -console.log(1); - -// 写在模块文件第一行 -#!/usr/bin/env node -export {}; -console.log(1); -``` - -有了这一行以后,Unix 命令行就可以直接执行脚本。 - -```bash -# 以前执行脚本的方式 -$ node hello.js - -# hashbang 的方式 -$ ./hello.js -``` - -对于 JavaScript 引擎来说,会把`#!`理解成注释,忽略掉这一行。 - -## import.meta - -开发者使用一个模块时,有时需要知道模板本身的一些信息(比如模块的路径)。现在有一个[提案](https://github.com/tc39/proposal-import-meta),为 import 命令添加了一个元属性`import.meta`,返回当前模块的元信息。 - -`import.meta`只能在模块内部使用,如果在模块外部使用会报错。 - -这个属性返回一个对象,该对象的各种属性就是当前运行的脚本的元信息。具体包含哪些属性,标准没有规定,由各个运行环境自行决定。一般来说,`import.meta`至少会有下面两个属性。 - -**(1)import.meta.url** - -`import.meta.url`返回当前模块的 URL 路径。举例来说,当前模块主文件的路径是`https://foo.com/main.js`,`import.meta.url`就返回这个路径。如果模块里面还有一个数据文件`data.txt`,那么就可以用下面的代码,获取这个数据文件的路径。 - -```javascript -new URL('data.txt', import.meta.url) -``` - -注意,Node.js 环境中,`import.meta.url`返回的总是本地路径,即是`file:URL`协议的字符串,比如`file:///home/user/foo.js`。 - -**(2)import.meta.scriptElement** - -`import.meta.scriptElement`是浏览器特有的元属性,返回加载模块的那个` - -// my-module.js 内部执行下面的代码 -import.meta.scriptElement.dataset.foo -// "abc" -``` - -## JSON 模块 - -import 命令目前只能用于加载 ES 模块,现在有一个[提案](https://github.com/tc39/proposal-json-modules),允许加载 JSON 模块。 - -假定有一个 JSON 模块文件`config.json`。 - -```javascript -{ - "appName": "My App" -} -``` - -目前,只能使用`fetch()`加载 JSON 模块。 - -```javascript -const response = await fetch('./config.json'); -const json = await response.json(); -``` - -import 命令能够直接加载 JSON 模块以后,就可以像下面这样写。 - -```javascript -import configData from './config.json' assert { type: "json" }; -console.log(configData.appName); -``` - -上面示例中,整个 JSON 对象被导入为`configData`对象,然后就可以从该对象获取 JSON 数据。 - -`import`命令导入 JSON 模块时,命令结尾的`assert {type: "json"}`不可缺少。这叫做导入断言,用来告诉 JavaScript 引擎,现在加载的是 JSON 模块。你可能会问,为什么不通过`.json`后缀名判断呢?因为浏览器的传统是不通过后缀名判断文件类型,标准委员会希望遵循这种做法,这样也可以避免一些安全问题。 - -导入断言是 JavaScript 导入其他格式模块的标准写法,JSON 模块将是第一个使用这种语法导入的模块。以后,还会支持导入 CSS 模块、HTML 模块等等。 - -动态加载模块的`import()`函数也支持加载 JSON 模块。 - -```javascript -import('./config.json', { assert: { type: 'json' } }) -``` - -脚本加载 JSON 模块以后,还可以再用 export 命令输出。这时,可以将 export 和 import 结合成一个语句。 - -```javascript -export { config } from './config.json' assert { type: 'json' }; -``` - diff --git a/docs/proxy.md b/docs/proxy.md index a65ec7dcc..5885094b4 100644 --- a/docs/proxy.md +++ b/docs/proxy.md @@ -286,7 +286,7 @@ const proxy = new Proxy({}, { proxy.getReceiver === proxy // true ``` -上面代码中,`proxy`对象的`getReceiver`属性是由`proxy`对象提供的,所以`receiver`指向`proxy`对象。 +上面代码中,`proxy`对象的`getReceiver`属性会被`get()`拦截,得到的返回值就是`proxy`对象。 ```javascript const proxy = new Proxy({}, { diff --git a/docs/reference.md b/docs/reference.md index bb3f87c71..359d045b1 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -184,7 +184,7 @@ ## 异步操作和 Async 函数 -- Luke Hoban, [Async Functions for ECMAScript](https://github.com/lukehoban/ecmascript-asyncawait): Async 函数的设计思想,与 Promise、Gernerator 函数的关系 +- Luke Hoban, [Async Functions for ECMAScript](https://github.com/lukehoban/ecmascript-asyncawait): Async 函数的设计思想,与 Promise、Generator 函数的关系 - Jafar Husain, [Asynchronous Generators for ES7](https://github.com/jhusain/asyncgenerator): Async 函数的深入讨论 - Nolan Lawson, [Taming the asynchronous beast with ES7](http://pouchdb.com/2015/03/05/taming-the-async-beast-with-es7.html): async 函数通俗的实例讲解 - Jafar Husain, [Async Generators](https://docs.google.com/file/d/0B4PVbLpUIdzoMDR5dWstRllXblU/view?sle=true): 对 async 与 Generator 混合使用的一些讨论 diff --git a/docs/regex.md b/docs/regex.md index bbe069fb9..b8f03ada9 100644 --- a/docs/regex.md +++ b/docs/regex.md @@ -376,7 +376,7 @@ JavaScript 语言的正则表达式,只支持先行断言(lookahead)和先 ```javascript /(?<=\$)\d+/.exec('Benjamin Franklin is on the $100 bill') // ["100"] -/(?a+)|(?b+)/v; +``` + +上面示例中,具名组匹配``在`|`前后使用了两次。 + ### 解构赋值和替换 有了具名组匹配以后,可以使用解构赋值直接从匹配结果上为变量赋值。 @@ -594,54 +656,54 @@ RE_TWICE.test('abc!abc!abc') // true RE_TWICE.test('abc!abc!ab') // false ``` -## 正则匹配索引 +## d 修饰符:正则匹配索引 -正则匹配结果的开始位置和结束位置,目前获取并不是很方便。正则实例的`exec()`方法,返回结果有一个`index`属性,可以获取整个匹配结果的开始位置,但是如果包含组匹配,每个组匹配的开始位置,很难拿到。 +组匹配的结果,在原始字符串里面的开始位置和结束位置,目前获取并不是很方便。正则实例的`exec()`方法有一个`index`属性,可以获取整个匹配结果的开始位置。但是,组匹配的每个组的开始位置,很难拿到。 -现在有一个[第三阶段提案](https://github.com/tc39/proposal-regexp-match-Indices),为`exec()`方法的返回结果加上`indices`属性,在这个属性上面可以拿到匹配的开始位置和结束位置。 +[ES2022](https://github.com/tc39/proposal-regexp-match-Indices) 新增了`d`修饰符,这个修饰符可以让`exec()`、`match()`的返回结果添加`indices`属性,在该属性上面可以拿到匹配的开始位置和结束位置。 ```javascript const text = 'zabbcdef'; -const re = /ab/; +const re = /ab/d; const result = re.exec(text); result.index // 1 result.indices // [ [1, 3] ] ``` -上面例子中,`exec()`方法的返回结果`result`,它的`index`属性是整个匹配结果(`ab`)的开始位置,而它的`indices`属性是一个数组,成员是每个匹配的开始位置和结束位置的数组。由于该例子的正则表达式没有组匹配,所以`indices`数组只有一个成员,表示整个匹配的开始位置是`1`,结束位置是`3`。 +上面示例中,`exec()`方法的返回结果`result`,它的`index`属性是整个匹配结果(`ab`)的开始位置。由于正则表达式`re`有`d`修饰符,`result`现在就会多出一个`indices`属性。该属性是一个数组,它的每个成员还是一个数组,包含了匹配结果在原始字符串的开始位置和结束位置。由于上例的正则表达式`re`没有包含组匹配,所以`indices`数组只有一个成员,表示整个匹配的开始位置是`1`,结束位置是`3`。 -注意,开始位置包含在匹配结果之中,但是结束位置不包含在匹配结果之中。比如,匹配结果为`ab`,分别是原始字符串的第1位和第2位,那么结束位置就是第3位。 +注意,开始位置包含在匹配结果之中,相当于匹配结果的第一个字符的位置。但是,结束位置不包含在匹配结果之中,是匹配结果的下一个字符。比如,上例匹配结果的最后一个字符`b`的位置,是原始字符串的2号位,那么结束位置`3`就是下一个字符的位置。 如果正则表达式包含组匹配,那么`indices`属性对应的数组就会包含多个成员,提供每个组匹配的开始位置和结束位置。 ```javascript const text = 'zabbcdef'; -const re = /ab+(cd)/; +const re = /ab+(cd)/d; const result = re.exec(text); result.indices // [ [ 1, 6 ], [ 4, 6 ] ] ``` -上面例子中,正则表达式包含一个组匹配,那么`indices`属性数组就有两个成员,第一个成员是整个匹配结果(`abbcd`)的开始位置和结束位置,第二个成员是组匹配(`cd`)的开始位置和结束位置。 +上面例子中,正则表达式`re`包含一个组匹配`(cd)`,那么`indices`属性数组就有两个成员,第一个成员是整个匹配结果(`abbcd`)的开始位置和结束位置,第二个成员是组匹配(`cd`)的开始位置和结束位置。 下面是多个组匹配的例子。 ```javascript const text = 'zabbcdef'; -const re = /ab+(cd(ef))/; +const re = /ab+(cd(ef))/d; const result = re.exec(text); result.indices // [ [1, 8], [4, 8], [6, 8] ] ``` -上面例子中,正则表达式包含两个组匹配,所以`indices`属性数组就有三个成员。 +上面例子中,正则表达式`re`包含两个组匹配,所以`indices`属性数组就有三个成员。 如果正则表达式包含具名组匹配,`indices`属性数组还会有一个`groups`属性。该属性是一个对象,可以从该对象获取具名组匹配的开始位置和结束位置。 ```javascript const text = 'zabbcdef'; -const re = /ab+(?cd)/; +const re = /ab+(?cd)/d; const result = re.exec(text); result.indices.groups // { Z: [ 4, 6 ] } @@ -653,14 +715,14 @@ result.indices.groups // { Z: [ 4, 6 ] } ```javascript const text = 'zabbcdef'; -const re = /ab+(?ce)?/; +const re = /ab+(?ce)?/d; const result = re.exec(text); result.indices[1] // undefined result.indices.groups['Z'] // undefined ``` -上面例子中,由于组匹配不成功,所以`indices`属性数组和`indices.groups`属性对象对应的组匹配成员都是`undefined`。 +上面例子中,由于组匹配`ce`不成功,所以`indices`属性数组和`indices.groups`属性对象对应的组匹配成员`Z`都是`undefined`。 ## String.prototype.matchAll() @@ -712,3 +774,70 @@ for (const match of string.matchAll(regex)) { Array.from(string.matchAll(regex)) ``` +## RegExp.escape() + +ES2025 添加了 RegExp.escape() 方法,它用来对字符串转义,使其可以安全地用于正则表达式。 + +```javascript +RegExp.escape('(*)') +// '\\(\\*\\)' +``` + +上面示例中,原始字符串的三个字符`(`、`*`、`)`在正则表达式都有特殊含义,RegExp.escape() 可以对它们进行转义。 + +注意,转义以后,每个特殊字符之前都加上了两个反斜杠。这是因为当该字符串用于正则表达式,字符串的转义机制会将两个反斜杠先转义成一个反斜杆,即`\\(`变成`\(`,从而正好用于正则表达式。 + +没有特殊含义的字符,不会被转义。 + +```javascript +RegExp.escape('_abc123') +// '_abc123' +``` + +该方法的经典用途是搜索和替换文本。 + +```javascript +function replacePlainText(str, searchText, replace) { + const searchRegExp = new RegExp( + RegExp.escape(searchText), + 'gu' + ); + return str.replace(searchRegExp, replace) +} +``` + +上面示例中,RegExp.escape() 先对用户输入的关键词进行转义,然后就可以将其当作正则表达式处理。 + +## 组匹配修饰符 + +ES2025 为组匹配添加了修饰符(inline flags),即修饰符只对正则表达式的一部分生效,对其他部分不生效。 + +目前,组匹配只能使用下面三个修饰符。 + +- i:忽略大小写 +- m:多行模式,即 ^ 和 $ 对每一行都生效。 +- s:dotAll 模式,即 . 可以匹配任何字符,包含每一行的终止符。 + +```javascript +/^x(?i:HELLO)x$/.test('xHELLOx') +// true + +/^x(?i:HELLO)x$/.test('xhellox') +// true +``` + +上面示例中,`(?i:HELLO)`表示 i 修饰符只用于组匹配`(HELLO)`,即`HELLO`不区分大小写。 + +`(?flag:pattern)`是打开组匹配修饰符的写法,而`(?-flat:pattern)`是关闭组匹配修饰符的写法。 + +```javascript +/^x(?-i:HELLO)x$/i.test('xHELLOx') +// true +``` + +上面示例中,整个正则表达式带有 i 修饰符,表示区分大小写,但是其中有一部分不需要区分,可以就可以使用`(?-i:HELLO)`对 HELLO 关闭区分大小写。 + +如果需要对组匹配打开某些修饰符,同时关闭另一些修饰符,可以写成`(?flag-flag:pattern)`。同一个修饰符不能既打开,同时又关闭。 + +另外,如果不带有修复符,那么`(?:pattern)`就是非捕获组匹配。 + diff --git a/docs/set-map.md b/docs/set-map.md index cc6159db1..4407477bd 100644 --- a/docs/set-map.md +++ b/docs/set-map.md @@ -21,7 +21,7 @@ for (let i of s) { 上面代码通过`add()`方法向 Set 结构加入成员,结果表明 Set 结构不会添加重复的值。 -`Set`函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化。 +`Set()`函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化。 ```javascript // 例一 @@ -88,6 +88,23 @@ set.size // 2 上面代码表示,由于两个空对象不相等,所以它们被视为两个值。 +`Array.from()`方法可以将 Set 结构转为数组。 + +```javascript +const items = new Set([1, 2, 3, 4, 5]); +const array = Array.from(items); +``` + +这就提供了去除数组重复成员的另一种方法。 + +```javascript +function dedupe(array) { + return Array.from(new Set(array)); +} + +dedupe([1, 1, 2, 3]) // [1, 2, 3] +``` + ### Set 实例的属性和方法 Set 结构的实例有以下属性。 @@ -114,11 +131,11 @@ s.has(1) // true s.has(2) // true s.has(3) // false -s.delete(2); +s.delete(2) // true s.has(2) // false ``` -下面是一个对比,看看在判断是否包括一个键上面,`Object`结构和`Set`结构的写法不同。 +下面是一个对比,判断是否包括一个键,`Object`结构和`Set`结构写法的不同。 ```javascript // 对象的写法 @@ -142,23 +159,6 @@ if (properties.has(someName)) { } ``` -`Array.from`方法可以将 Set 结构转为数组。 - -```javascript -const items = new Set([1, 2, 3, 4, 5]); -const array = Array.from(items); -``` - -这就提供了去除数组重复成员的另一种方法。 - -```javascript -function dedupe(array) { - return Array.from(new Set(array)); -} - -dedupe([1, 1, 2, 3]) // [1, 2, 3] -``` - ### 遍历操作 Set 结构的实例有四个遍历方法,可以用于遍历成员。 @@ -302,23 +302,137 @@ set = new Set(Array.from(set, val => val * 2)); 上面代码提供了两种方法,直接在遍历操作中改变原来的 Set 结构。 +### 集合运算 + +[ES2025](https://github.com/tc39/proposal-set-methods) 为 Set 结构添加了以下集合运算方法。 + +- Set.prototype.intersection(other):交集 +- Set.prototype.union(other):并集 +- Set.prototype.difference(other):差集 +- Set.prototype.symmetricDifference(other):对称差集 +- Set.prototype.isSubsetOf(other):判断是否为子集 +- Set.prototype.isSupersetOf(other):判断是否为超集 +- Set.prototype.isDisjointFrom(other):判断是否不相交 + +以上方法的参数都必须是 Set 结构,或者是一个类似于 Set 的结构(拥有`size`属性,以及`keys()`和`has()`方法。 + +`.union()`是并集运算,返回包含两个集合中存在的所有成员的集合。 + +```javascript +const frontEnd = new Set(["JavaScript", "HTML", "CSS"]); +const backEnd = new Set(["Python", "Java", "JavaScript"]); + +const all = frontEnd.union(backEnd); +// Set {"JavaScript", "HTML", "CSS", "Python", "Java"} +``` + +`.intersection()`是交集运算,返回同时包含在两个集合中的成员的集合。 + +```javascript +const frontEnd = new Set(["JavaScript", "HTML", "CSS"]); +const backEnd = new Set(["Python", "Java", "JavaScript"]); + +const frontAndBackEnd = frontEnd.intersection(backEnd); +// Set {"JavaScript"} +``` + +`.difference()`是差集运算,返回第一个集合中存在但第二个集合中不存在的所有成员的集合。 + +```javascript +const frontEnd = new Set(["JavaScript", "HTML", "CSS"]); +const backEnd = new Set(["Python", "Java", "JavaScript"]); + +const onlyFrontEnd = frontEnd.difference(backEnd); +// Set {"HTML", "CSS"} + +const onlyBackEnd = backEnd.difference(frontEnd); +// Set {"Python", "Java"} +``` + +`.symmetryDifference()`是对称差集,返回两个集合的所有独一无二成员的集合,即去除了重复的成员。 + +```javascript +const frontEnd = new Set(["JavaScript", "HTML", "CSS"]); +const backEnd = new Set(["Python", "Java", "JavaScript"]); + +const onlyFrontEnd = frontEnd.symmetricDifference(backEnd); +// Set {"HTML", "CSS", "Python", "Java"} + +const onlyBackEnd = backEnd.symmetricDifference(frontEnd); +// Set {"Python", "Java", "HTML", "CSS"} +``` + +注意,返回结果中的成员顺序,由添加到集合的顺序决定。 + +`.isSubsetOf()`返回一个布尔值,判断第一个集合是否为第二个集合的子集,即第一个集合的所有成员都是第二个集合的成员。 + +```javascript +const frontEnd = new Set(["JavaScript", "HTML", "CSS"]); +const declarative = new Set(["HTML", "CSS"]); + +declarative.isSubsetOf(frontEnd); +// true + +frontEndLanguages.isSubsetOf(declarativeLanguages); +// false +``` + +任何集合都是自身的子集。 + +```javascript +frontEnd.isSubsetOf(frontEnd); +// true +``` + +`isSupersetOf()`返回一个布尔值,表示第一个集合是否为第二个集合的超集,即第二个集合的所有成员都是第一个集合的成员。 + +```javascript +const frontEnd = new Set(["JavaScript", "HTML", "CSS"]); +const declarative = new Set(["HTML", "CSS"]); + +declarative.isSupersetOf(frontEnd); +// false + +frontEnd.isSupersetOf(declarative); +// true +``` + +任何集合都是自身的超集。 + +```javascript +frontEnd.isSupersetOf(frontEnd); +// true +``` + +`.isDisjointFrom()`判断两个集合是否不相交,即没有共同成员。 + +```javascript +const frontEnd = new Set(["JavaScript", "HTML", "CSS"]); +const interpreted = new Set(["JavaScript", "Ruby", "Python"]); +const compiled = new Set(["Java", "C++", "TypeScript"]); + +interpreted.isDisjointFrom(compiled); +// true + +frontEnd.isDisjointFrom(interpreted); +// false +``` + ## WeakSet ### 含义 WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别。 -首先,WeakSet 的成员只能是对象,而不能是其他类型的值。 +首先,WeakSet 的成员只能是对象和 Symbol 值,而不能是其他类型的值。 ```javascript const ws = new WeakSet(); -ws.add(1) -// TypeError: Invalid value used in weak set -ws.add(Symbol()) -// TypeError: invalid value used in weak set +ws.add(1) // 报错 +ws.add(Symbol()) // 不报错 ``` -上面代码试图向 WeakSet 添加一个数值和`Symbol`值,结果报错,因为 WeakSet 只能放置对象。 +上面代码试图向 WeakSet 添加一个数值和`Symbol`值,结果前者报错了,因为 WeakSet 只能放置对象和 Symbol 值。 其次,WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。 @@ -358,8 +472,8 @@ const ws = new WeakSet(b); WeakSet 结构有以下三个方法。 -- **WeakSet.prototype.add(value)**:向 WeakSet 实例添加一个新成员。 -- **WeakSet.prototype.delete(value)**:清除 WeakSet 实例的指定成员。 +- **WeakSet.prototype.add(value)**:向 WeakSet 实例添加一个新成员,返回 WeakSet 结构本身。 +- **WeakSet.prototype.delete(value)**:清除 WeakSet 实例的指定成员,清除成功返回`true`,如果在 WeakSet 中找不到该成员或该成员不是对象,返回`false`。 - **WeakSet.prototype.has(value)**:返回一个布尔值,表示某个值是否在 WeakSet 实例之中。 下面是一个例子。 @@ -373,10 +487,10 @@ ws.add(window); ws.add(obj); ws.has(window); // true -ws.has(foo); // false +ws.has(foo); // false -ws.delete(window); -ws.has(window); // false +ws.delete(window); // true +ws.has(window); // false ``` WeakSet 没有`size`属性,没有办法遍历它的成员。 @@ -633,7 +747,7 @@ m.has(undefined) // true **(5)Map.prototype.delete(key)** -`delete`方法删除某个键,返回`true`。如果删除失败,返回`false`。 +`delete()`方法删除某个键,返回`true`。如果删除失败,返回`false`。 ```javascript const m = new Map(); @@ -646,7 +760,7 @@ m.has(undefined) // false **(6)Map.prototype.clear()** -`clear`方法清除所有成员,没有返回值。 +`clear()`方法清除所有成员,没有返回值。 ```javascript let map = new Map(); @@ -928,19 +1042,16 @@ wm2.get(k2) // "bar" `WeakMap`与`Map`的区别有两点。 -首先,`WeakMap`只接受对象作为键名(`null`除外),不接受其他类型的值作为键名。 +首先,`WeakMap`只接受对象(`null`除外)和 [Symbol 值](https://github.com/tc39/proposal-symbols-as-weakmap-keys)作为键名,不接受其他类型的值作为键名。 ```javascript const map = new WeakMap(); -map.set(1, 2) -// TypeError: 1 is not an object! -map.set(Symbol(), 2) -// TypeError: Invalid value used as weak map key -map.set(null, 2) -// TypeError: Invalid value used as weak map key +map.set(1, 2) // 报错 +map.set(null, 2) // 报错 +map.set(Symbol(), 2) // 不报错 ``` -上面代码中,如果将数值`1`和`Symbol`值作为 WeakMap 的键名,都会报错。 +上面代码中,如果将数值`1`和`null`作为 WeakMap 的键名,都会报错,将 Symbol 值作为键名不会报错。 其次,`WeakMap`的键名所指向的对象,不计入垃圾回收机制。 @@ -1288,3 +1399,7 @@ class Thingy { 由于无法知道清理器何时会执行,所以最好避免使用它。另外,如果浏览器窗口关闭或者进程意外退出,清理器则不会运行。 +## 参考链接 + +- [Union, intersection, difference, and more are coming to JavaScript Sets](https://www.sonarsource.com/blog/union-intersection-difference-javascript-sets/) + diff --git a/docs/string-methods.md b/docs/string-methods.md index 2fc48b4c8..1d3e63a8e 100644 --- a/docs/string-methods.md +++ b/docs/string-methods.md @@ -421,7 +421,7 @@ String.prototype.replaceAll(searchValue, replacement) 上面例子中,`replaceAll()`的第二个参数是一个函数,该函数的返回值会替换掉所有`b`的匹配。 -这个替换函数可以接受多个参数。第一个参数是捕捉到的匹配内容,第二个参数捕捉到是组匹配(有多少个组匹配,就有多少个对应的参数)。此外,最后还可以添加两个参数,倒数第二个参数是捕捉到的内容在整个字符串中的位置,最后一个参数是原字符串。 +这个替换函数可以接受多个参数。第一个参数是捕捉到的匹配内容,第二个参数是捕捉到的组匹配(有多少个组匹配,就有多少个对应的参数)。此外,最后还可以添加两个参数,倒数第二个参数是捕捉到的内容在整个字符串中的位置,最后一个参数是原字符串。 ```javascript const str = '123abc456'; @@ -450,3 +450,38 @@ str.at(-1) // "o" 如果参数位置超出了字符串范围,`at()`返回`undefined`。 该方法来自数组添加的`at()`方法,目前还是一个第三阶段的提案,可以参考《数组》一章的介绍。 + +## 实例方法:toWellFormed() + +ES2024 引入了新的字符串方法`toWellFormed()`,用来处理 Unicode 的代理字符对问题(surrogates)。 + +JavaScript 语言内部使用 UTF-16 格式,表示每个字符。UTF-16 只有16位,只能表示码点在`U+0000`到`U+FFFF`之间的 Unicode 字符。对于码点大于`U+FFFF`的 Unicode 字符(即码点大于16位的字符,`U+10000`到`U+10FFFF`),解决办法是使用代理字符对,即用两个 UTF-16 字符组合表示。 + +具体来说,UTF-16 规定,`U+D800`至`U+DFFF`是空字符段,专门留给代理字符对使用。只要遇到这个范围内的码点,就知道它是代理字符对,本身没有意义,必须两个字符结合在一起解读。其中,前一个字符的范围规定为`0xD800`到`0xDBFF`之间,后一个字符的范围规定为`0xDC00`到`0xDFFF`之间。举例来说,码点`U+1D306`对应的字符为`𝌆`,它写成 UTF-16 就是`0xD834 0xDF06`。 + +但是,字符串里面可能会出现单个代理字符对,即`U+D800`至`U+DFFF`里面的字符,它没有配对的另一个字符,无法进行解读,导致出现各种状况。 + +`.toWellFormed()`就是为了解决这个问题,不改变原始字符串,返回一个新的字符串,将原始字符串里面的单个代理字符对,都替换为`U+FFFD`,从而可以在任何正常处理字符串的函数里面使用。 + +```javascript +"ab\uD800".toWellFormed() // 'ab�' +``` + +上面示例中,`\uD800`是单个的代理字符对,单独使用时没有意义。`toWellFormed()`将这个字符转为`\uFFFD`。 + +再看下面的例子,`encodeURI()`遇到单个的代理字符对,会报错。 + +```javascript +const illFormed = "https://example.com/search?q=\uD800"; + +encodeURI(illFormed) // 报错 +``` + +`toWellFormed()`将其转换格式后,再使用`encodeURI()`就不会报错了。 + +```javascript +const illFormed = "https://example.com/search?q=\uD800"; + +encodeURI(illFormed.toWellFormed()) // 正确 +``` + diff --git a/docs/style.md b/docs/style.md index fc23e2ffa..95fe4c92e 100644 --- a/docs/style.md +++ b/docs/style.md @@ -285,7 +285,7 @@ const boundMethod = (...params) => method.apply(this, params); 简单的、单行的、不会复用的函数,建议采用箭头函数。如果函数体较为复杂,行数较多,还是应该采用传统的函数写法。 -所有配置项都应该集中在一个对象,放在最后一个参数,布尔值不可以直接作为参数。 +所有配置项都应该集中在一个对象,放在最后一个参数,布尔值最好不要直接作为参数,因为代码语义会很差,也不利于将来增加其他配置项。 ```javascript // bad diff --git a/docs/symbol.md b/docs/symbol.md index f420a12a7..4693410b0 100644 --- a/docs/symbol.md +++ b/docs/symbol.md @@ -4,7 +4,7 @@ ES5 的对象属性名都是字符串,这容易造成属性名的冲突。比如,你使用了一个他人提供的对象,但又想为这个对象添加新的方法(mixin 模式),新方法的名字就有可能与现有方法产生冲突。如果有一种机制,保证每个属性的名字都是独一无二的就好了,这样就从根本上防止属性名的冲突。这就是 ES6 引入`Symbol`的原因。 -ES6 引入了一种新的原始数据类型`Symbol`,表示独一无二的值。它属于 JavaScript 语言的数据类型之一,其他数据类型是:`undefined`、`null`、布尔值(Boolean)、字符串(String)、数值(Number)、大整数(BigInt)、对象(Object)。 +ES6 引入了一种新的原始数据类型`Symbol`,表示独一无二的值。它属于 JavaScript 语言的原生数据类型之一,其他数据类型是:`undefined`、`null`、布尔值(Boolean)、字符串(String)、数值(Number)、大整数(BigInt)、对象(Object)。 Symbol 值通过`Symbol()`函数生成。这就是说,对象的属性名现在可以有两种类型,一种是原来就有的字符串,另一种就是新增的 Symbol 类型。凡是属性名属于 Symbol 类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。 @@ -17,9 +17,9 @@ typeof s 上面代码中,变量`s`就是一个独一无二的值。`typeof`运算符的结果,表明变量`s`是 Symbol 数据类型,而不是字符串之类的其他类型。 -注意,`Symbol`函数前不能使用`new`命令,否则会报错。这是因为生成的 Symbol 是一个原始类型的值,不是对象。也就是说,由于 Symbol 值不是对象,所以不能添加属性。基本上,它是一种类似于字符串的数据类型。 +注意,`Symbol()`函数前不能使用`new`命令,否则会报错。这是因为生成的 Symbol 是一个原始类型的值,不是对象,所以不能使用`new`命令来调用。另外,由于 Symbol 值不是对象,所以也不能添加属性。基本上,它是一种类似于字符串的数据类型。 -`Symbol`函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。 +`Symbol()`函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述。这主要是为了在控制台显示,或者转为字符串时,比较容易区分。 ```javascript let s1 = Symbol('foo'); @@ -34,7 +34,7 @@ s2.toString() // "Symbol(bar)" 上面代码中,`s1`和`s2`是两个 Symbol 值。如果不加参数,它们在控制台的输出都是`Symbol()`,不利于区分。有了参数以后,就等于为它们加上了描述,输出的时候就能够分清,到底是哪一个值。 -如果 Symbol 的参数是一个对象,就会调用该对象的`toString`方法,将其转为字符串,然后才生成一个 Symbol 值。 +如果 Symbol 的参数是一个对象,就会调用该对象的`toString()`方法,将其转为字符串,然后才生成一个 Symbol 值。 ```javascript const obj = { @@ -46,7 +46,7 @@ const sym = Symbol(obj); sym // Symbol(abc) ``` -注意,`Symbol`函数的参数只是表示对当前 Symbol 值的描述,因此相同参数的`Symbol`函数的返回值是不相等的。 +注意,`Symbol()`函数的参数只是表示对当前 Symbol 值的描述,因此相同参数的`Symbol`函数的返回值是不相等的。 ```javascript // 没有参数的情况 @@ -62,7 +62,7 @@ let s2 = Symbol('foo'); s1 === s2 // false ``` -上面代码中,`s1`和`s2`都是`Symbol`函数的返回值,而且参数相同,但是它们是不相等的。 +上面代码中,`s1`和`s2`都是`Symbol()`函数的返回值,而且参数相同,但是它们是不相等的。事实上,如果调用100次`Symbol()`,会得到100个互不相等的值。 Symbol 值不能与其他类型的值进行运算,会报错。 @@ -101,13 +101,13 @@ sym + 2 // TypeError ## Symbol.prototype.description -创建 Symbol 的时候,可以添加一个描述。 +前面说过,`Symbol()`函数创建 Symbol 值时,可以用参数添加一个描述。 ```javascript const sym = Symbol('foo'); ``` -上面代码中,`sym`的描述就是字符串`foo`。 +上面代码中,`sym`这个值的描述就是字符串`foo`。 但是,读取这个描述需要将 Symbol 显式转为字符串,即下面的写法。 @@ -118,7 +118,7 @@ String(sym) // "Symbol(foo)" sym.toString() // "Symbol(foo)" ``` -上面的用法不是很方便。[ES2019](https://github.com/tc39/proposal-Symbol-description) 提供了一个实例属性`description`,直接返回 Symbol 的描述。 +上面的用法不是很方便。[ES2019](https://github.com/tc39/proposal-Symbol-description) 提供了一个 Symbol 值的实例属性`description`,直接返回 Symbol 值的描述。 ```javascript const sym = Symbol('foo'); @@ -128,7 +128,7 @@ sym.description // "foo" ## 作为属性名的 Symbol -由于每一个 Symbol 值都是不相等的,这意味着 Symbol 值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性。这对于一个对象由多个模块构成的情况非常有用,能防止某一个键被不小心改写或覆盖。 +由于每一个 Symbol 值都是不相等的,这意味着只要 Symbol 值作为标识符,用于对象的属性名,就能保证不会出现同名的属性。这对于一个对象由多个模块构成的情况非常有用,能防止某一个键被不小心改写或覆盖。 ```javascript let mySymbol = Symbol(); @@ -150,7 +150,7 @@ Object.defineProperty(a, mySymbol, { value: 'Hello!' }); a[mySymbol] // "Hello!" ``` -上面代码通过方括号结构和`Object.defineProperty`,将对象的属性名指定为一个 Symbol 值。 +上面代码通过方括号结构和`Object.defineProperty()`方法,将对象的属性名指定为一个 Symbol 值。 注意,Symbol 值作为对象属性名时,不能用点运算符。 @@ -280,7 +280,7 @@ const shapeType = { ## 属性名的遍历 -Symbol 作为属性名,遍历对象的时候,该属性不会出现在`for...in`、`for...of`循环中,也不会被`Object.keys()`、`Object.getOwnPropertyNames()`、`JSON.stringify()`返回。 +Symbol 值作为属性名,遍历对象的时候,该属性不会出现在`for...in`、`for...of`循环中,也不会被`Object.keys()`、`Object.getOwnPropertyNames()`、`JSON.stringify()`返回。 但是,它也不是私有属性,有一个`Object.getOwnPropertySymbols()`方法,可以获取指定对象的所有 Symbol 属性名。该方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。 @@ -846,7 +846,7 @@ String(obj) // 'str' ### Symbol.toStringTag -对象的`Symbol.toStringTag`属性,指向一个方法。在该对象上面调用`Object.prototype.toString`方法时,如果这个属性存在,它的返回值会出现在`toString`方法返回的字符串之中,表示对象的类型。也就是说,这个属性可以用来定制`[object Object]`或`[object Array]`中`object`后面的那个字符串。 +对象的`Symbol.toStringTag`属性,用来设定一个字符串(设为其他类型的值无效,但不报错)。在目标对象上面调用`Object.prototype.toString()`方法时,如果`Symbol.toStringTag`属性存在,该属性设定的字符串会出现在`toString()`方法返回的字符串之中,表示对象的类型。也就是说,这个属性可以用来定制`[object Object]`或`[object Array]`中`object`后面的那个大写字符串。 ```javascript // 例一 diff --git a/docs/temporal.md b/docs/temporal.md new file mode 100644 index 000000000..e11341192 --- /dev/null +++ b/docs/temporal.md @@ -0,0 +1,322 @@ +# Temporal API + +Temporal 是一个表示日期时间的全新 API,对目前的 Date API 的诸多问题进行修正。 + +它有几个核心概念。 + +- 当前时间:表示此时此刻的时间,位于 Temporal.now 对象。 +- 时点(instant),表示历史上某个唯一时间,其中 Temporal.Instant 对象表示时间戳,Temporal.ZonedDateTime 表示带有时区的日期时间。 +- 时钟时间(wall-clock times),表示本地时间,包含以下几个对象,不涉及时区。 + - Temporal.PlainDateTime:完整的日期和时间。 + - Temporal.PlainDate:仅限于日期。 + - Temporal.PlainYearMonth:仅限于年月。 + - Temporal.PlainMonthDay:仅限于月和日。 + - Temporal.PlainTime:不包含日期的时间。 +- 持续时间(durations),表示两个时间点之间的差异,位于 Temporal.Duration 对象。 + +## Temporal.Now + +`Temporal.Now`表示当前系统的准确时间。 + +- Temporal.Now.instant()- 获取当前系统准确时间 +- Temporal.Now.timeZoneId()- 获取当前系统时区 +- Temporal.Now.zonedDateTimeISO()- 获取系统时区和 ISO-8601 日历中的当前日期和挂钟时间 +- Temporal.Now.plainDateISO()- 获取系统时区和 ISO-8601 日历中的当前日期 +- Temporal.Now.plainTimeISO()- 获取系统时区和 ISO-8601 日历中的当前挂钟时间 +- Temporal.Now.plainDateTimeISO()- 与上面相同,但返回 ISO-8601 日历中的日期时间 + +```javascript +// 返回 UTC 的当前时间 +Temporal.Now.instant().toString() + +// 系统时区的当前时间 +Temporal.Now.plainDateTimeISO() // 2025-01-22T11:46:36.144 + +// 当前时间对应 America/New_York 时区的时间 +Temporal.Now.plainDateTimeISO("America/New_York") // 2025-01-22T05:47:02.555 + +// 返回某个时区的当前日期时间 +Temporal.Now.zonedDateTimeISO('Asia/Shanghai').toString() + +// 返回 ISO 格式当前日期时间 +Temporal.Now.plainDateTimeISO().toString() + +// 返回 ISO 格式的当前时间,不含日期 +Temporal.Now.plainTimeISO().toString() +``` + +下面的例子是获取指定时区的当前时间。 + +```javascript +const now = Temporal.Now.zonedDateTimeISO('America/New_York'); +console.log(now.toString()); +``` + +下面的例子是获取当前时间对应的农历年。 + +```javascript +const currentYear = Temporal.Now.plainDateISO().withCalendar("chinese").year; +``` + +## Temporal.Instant + +`Temporal.Instant`表示某个固定的时点。 + +```javascript +const instant = Temporal.Instant.from('1969-07-20T20:17Z'); +instant.toString(); // => '1969-07-20T20:17:00Z' +instant.epochMilliseconds; // => -14182980000 + +// 某个 Unix 时间戳对应的时点 +const launch = Temporal.Instant.fromEpochMilliseconds(1851222399924); +const now = Temporal.Now.instant(); +const duration = now.until(launch, { smallestUnit: "hour" }); +``` + +## Temporal.ZonedDateTime + +`Temporal.ZonedDateTime`表示某个时区的时间。它会在 ISO8601 的标准格式后面,添加时区后缀和历法后缀。 + +```javascript +2020-08-05T20:06:13+09:00[Asia/Tokyo][u-ca=japanese] +``` + +上面示例中,`2020-08-05T20:06:13+09:00`是 ISO8601 标准格式,`[Asia/Tokyo]`是时区后缀,`[u-ca=japanese]`是历法后缀,表示采用日本历法。 + +默认的历法是 ISO8601 规定的公历,可以省略不写。 + +下面是使用`Temporal.ZonedDateTime.from()`新建 ZonedDateTime 实例对象的例子。 + +```javascript +const zonedDateTime = Temporal.ZonedDateTime.from({ + timeZone: 'America/Los_Angeles', + year: 1995, + month: 12, + day: 7, + hour: 3, + minute: 24, + second: 30, + millisecond: 0, + microsecond: 3, + nanosecond: 500 +}); // => 1995-12-07T03:24:30.0000035-08:00[America/Los_Angeles] +``` + +下面是使用`Temporal.ZonedDateTime.compare()`比较两个 ZonedDateTime 实例对象的例子。 + +```javascript +const one = Temporal.ZonedDateTime.from('2020-11-01T01:45-07:00[America/Los_Angeles]'); +const two = Temporal.ZonedDateTime.from('2020-11-01T01:15-08:00[America/Los_Angeles]'); + +Temporal.ZonedDateTime.compare(one, two); +// -1 +``` + +上面示例中,`Temporal.ZonedDateTime.compare()`返回`-1`,表示第一个时间小于(即早于)第二个时间。如果返回`1`,表示第一个时间大于第二个时间;返回`0`,表示两个时间相等。 + +ZonedDateTime 实例对象有以下属性。 + +- hoursInDay:指定时区的某一天一共有多少个小时,主要用来处理夏令时。 + +```javascript +Temporal.ZonedDateTime.from('2020-01-01T12:00-08:00[America/Los_Angeles]').hoursInDay; +// 24 +Temporal.ZonedDateTime.from('2020-03-08T12:00-07:00[America/Los_Angeles]').hoursInDay; +// 23 +Temporal.ZonedDateTime.from('2020-11-01T12:00-08:00[America/Los_Angeles]').hoursInDay; +// 25 +``` + +- daysInYear +- inLeapYear + +ZonedDateTime 实例对象有以下方法。 + +- .withTimeZone():切换时区。 + +```javascript +zdt = Temporal.ZonedDateTime.from('1995-12-07T03:24:30+09:00[Asia/Tokyo]'); +zdt.toString(); // => '1995-12-07T03:24:30+09:00[Asia/Tokyo]' +zdt.withTimeZone('Africa/Accra').toString(); // => '1995-12-06T18:24:30+00:00[Africa/Accra]' +``` + +- add():增加时间。 + +```javascript +zdt = Temporal.ZonedDateTime.from('2020-03-08T00:00-08:00[America/Los_Angeles]'); + +// 增加一天 +laterDay = zdt.add({ days: 1 }); +// 2020-03-09T00:00:00-07:00[America/Los_Angeles] +// 注意:时区改变了,表示洛杉矶这个日期处于夏令时,比正常情况早一个小时 + +laterDay.since(zdt, { largestUnit: 'hour' }).hours; +// 23 +// 当天只有23小时 + +laterHours = zdt.add({ hours: 24 }); +// 2020-03-09T01:00:00-07:00[America/Los_Angeles] +laterHours.since(zdt, { largestUnit: 'hour' }).hours; // 24 +``` + +- .until():计算两个时间之间的差异。 + +## Temporal.PlainDate + +`Temporal.PlainDate`表示与时区无关的日期。 + +```javascript +const date = Temporal.PlainDate.from({ year: 2006, month: 8, day: 24 }); // => 2006-08-24 +date.year; // => 2006 +date.inLeapYear; // => false +date.toString(); // => '2006-08-24' +``` + +下面的例子是计算某个日期以后的时间。 + +```javascript +const date = Temporal.PlainDate.from('2024-01-01'); +const newDate = date.add({ days: 10 }); +console.log(newDate.toString()); // Outputs '2024-01-11' +``` + +## Temporal.PlainTime + +`Temporal.PlainTime`表示与时区无关的某个时点。 + +```javascript +const time = Temporal.PlainTime.from({ + hour: 19, + minute: 39, + second: 9, + millisecond: 68, + microsecond: 346, + nanosecond: 205 +}); // => 19:39:09.068346205time.second; // => 9 +time.toString(); // => '19:39:09.068346205' +``` + +## Temporal.PlainDateTime + +`Temporal.PlainDateTime`表示时区无关的日期时间。 + +```javascript +const dateTime = Temporal.PlainDateTime.from({ + year: 1995, + month: 12, + day: 7, + hour: 15 +}); // => 1995-12-07T15:00:00 +const dateTime1 = dateTime.with({ + minute: 17, + second: 19 +}); // => 1995-12-07T15:17:19 +``` + +## Temporal.PlainYearMonth + +`Temporal.PlainYearMonth`表示不含日期的年月。 + +```javascript +const yearMonth = Temporal.PlainYearMonth.from({ year: 2020, month: 10 }); // => 2020-10 +yearMonth.daysInMonth; // => 31 +yearMonth.daysInYear; // => 366 +``` + +## Temporal.PlainMonthDay + +`Temporal.PlainMonthDay`表示没有年份的月和日。 + +下面是计算生日的例子。 + +```javascript +const birthday = Temporal.PlainMonthDay.from("12-15"); +// 或者写成 +// const birthday = Temporal.PlainMonthDay.from({ month: 12, day: 15 }) + +const birthdayIn2030 = birthday.toPlainDate({ year: 2030 }); + +birthdayIn2030.toString() // 2030-12-15 +birthdayIn2030.dayOfWeek // 7 +``` + +下面是农历一月一日(大年初一)的例子。 + +```javascript +const chineseNewYear = Temporal.PlainMonthDay.from({ + monthCode: "M01", + day: 1, + calendar: "chinese", +}); + +const currentYear = Temporal.Now.plainDateISO().withCalendar("chinese").year; + +// 获取下一个春节 +let nextCNY = chineseNewYear.toPlainDate({ year: currentYear }); +// 如果 nextCNY 早于当前时间,则向后移动一年 +if (Temporal.PlainDate.compare(nextCNY, Temporal.Now.plainDateISO()) <= 0) { + nextCNY = nextCNY.add({ years: 1 }); +} + +nextCNY.withCalendar("iso8601").toLocaleString() // 1/29/2025 +``` + +## Temporal.Duration + +`Temporal.Duration`表示时长。 + +```javascript +const duration = Temporal.Duration.from({ + hours: 130, + minutes: 20 +}); + +duration.total({ unit: 'second' }); // => 469200 +``` + +## Temporal.TimeZone + +`Temporal.TimeZone`表示某个时区。 + +```javascript +const timeZone = Temporal.TimeZone.from('Africa/Cairo'); +timeZone.getInstantFor('2000-01-01T00:00'); // => 1999-12-31T22:00:00Z +timeZone.getPlainDateTimeFor('2000-01-01T00:00Z'); // => 2000-01-01T02:00:00 +timeZone.getPreviousTransition(Temporal.Now.instant()); // => 2014-09-25T21:00:00Z +timeZone.getNextTransition(Temporal.Now.instant()); // => null +``` + +## Temporal.Calendar + +`Temporal.Calendar`表示某个日历系统。 + +```javascript +const cal = Temporal.Calendar.from('iso8601'); +const date = cal.dateFromFields({ year: 1999, month: 12, day: 31 }, {}); +date.monthsInYear; // => 12 +date.daysInYear; // => 365 +``` + +## Temporal.Duration + +Temporal.Duration 表示一个持续的时间对象。 + +```javascript +const durations = [ + Temporal.Duration.from({ hours: 1 }), + Temporal.Duration.from({ hours: 2 }), + Temporal.Duration.from({ hours: 1, minutes: 30 }), + Temporal.Duration.from({ hours: 1, minutes: 45 }), +]; + +durations.sort(Temporal.Duration.compare); +console.log(durations.map((d) => d.toString())); +// [ 'PT1H', 'PT1H30M', 'PT1H45M', 'PT2H' ] +```` + +## 参考链接 + +- [Temporal documentation](https://tc39.es/proposal-temporal/docs/) +- [JS Dates Are About to Be Fixed](https://docs.timetime.in/blog/js-dates-finally-fixed/) +- [JavaScript Temporal is coming](https://developer.mozilla.org/en-US/blog/javascript-temporal-is-coming/) + diff --git a/index.html b/index.html index 0f88f503c..6430cbbc0 100644 --- a/index.html +++ b/index.html @@ -29,6 +29,7 @@
+