diff --git a/README.md b/README.md index 84de1b82..5bb40d33 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 8c2b6900..e4ed02e8 100644 --- a/docs/array.md +++ b/docs/array.md @@ -881,9 +881,7 @@ sentence.at(100) // undefined ## 实例方法:toReversed(),toSorted(),toSpliced(),with() -很多数组的传统方法会改变原数组,比如`push()`、`pop()`、`shift()`、`unshift()`等等。数组只要调用了这些方法,它的值就变了。现在有一个[提案](https://github.com/tc39/proposal-change-array-by-copy),允许对数组进行操作时,不改变原数组,而返回一个原数组的拷贝。 - -这样的方法一共有四个。 +很多数组的传统方法会改变原数组,比如`push()`、`pop()`、`shift()`、`unshift()`等等。数组只要调用了这些方法,它的值就变了。[ES2023](https://github.com/tc39/proposal-change-array-by-copy)引入了四个新方法,对数组进行操作时,不改变原数组,而返回一个原数组的拷贝。 - `Array.prototype.toReversed() -> Array` - `Array.prototype.toSorted(compareFn) -> Array` @@ -941,7 +939,7 @@ array.group((num, index, array) => { 下面是另一个例子。 ```javascript -[6.1, 4.2, 6.3].groupBy(Math.floor) +[6.1, 4.2, 6.3].group(Math.floor) // { '4': [4.2], '6': [6.1, 6.3] } ``` diff --git a/docs/class-extends.md b/docs/class-extends.md index 5b1ff61f..c5308bd0 100644 --- a/docs/class-extends.md +++ b/docs/class-extends.md @@ -183,7 +183,7 @@ B.hello() // hello world 上面代码中,`hello()`是`A`类的静态方法,`B`继承`A`,也继承了`A`的静态方法。 -注意,静态属性是通过软拷贝实现继承的。 +注意,静态属性是通过浅拷贝实现继承的。 ```javascript class A { static foo = 100; } diff --git a/docs/class.md b/docs/class.md index cbdde3bb..0906a0fe 100644 --- a/docs/class.md +++ b/docs/class.md @@ -681,6 +681,8 @@ counter.#count = 42 // 报错 上面示例中,在类的外部,读取或写入私有属性`#count`,都会报错。 +注意,[从 Chrome 111 开始](https://developer.chrome.com/blog/new-in-devtools-111/#misc),开发者工具里面可以读写私有属性,不会报错,原因是 Chrome 团队认为这样方便调试。 + 另外,不管在类的内部或外部,读取一个不存在的私有属性,也都会报错。这跟公开属性的行为完全不同,如果读取一个不存在的公开属性,不会报错,只会返回`undefined`。 ```javascript @@ -844,7 +846,7 @@ class C { } ``` -上面示例中,`in`运算符判断某个对象是否有私有属性`#foo`。它不会报错,而是返回一个布尔值。 +上面示例中,`in`运算符判断某个对象是否有私有属性`#brand`。它不会报错,而是返回一个布尔值。 这种用法的`in`,也可以跟`this`一起配合使用。 @@ -853,52 +855,21 @@ class A { #foo = 0; m() { console.log(#foo in this); // true - console.log(#bar in this); // false } } ``` -注意,判断私有属性时,`in`只能用在类的内部。 - -子类从父类继承的私有属性,也可以使用`in`运算符来判断。 +注意,判断私有属性时,`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`运算符的判断,导致报错。 ## 静态块 @@ -1006,7 +977,7 @@ class Foo {} } ``` -上面的代码不会报错,因为`Bar`继承`Foo`的时候,`Foo`已经有定义了。但是,如果存在`class`的提升,上面代码就会报错,因为`class`会被提升到代码头部,而`let`命令是不提升的,所以导致`Bar`继承`Foo`的时候,`Foo`还没有定义。 +上面的代码不会报错,因为`Bar`继承`Foo`的时候,`Foo`已经有定义了。但是,如果存在`class`的提升,上面代码就会报错,因为`class`会被提升到代码头部,而定义`Foo`的那一行没有提升,导致`Bar`继承`Foo`的时候,`Foo`还没有定义。 ### name 属性 diff --git a/docs/decorator.md b/docs/decorator.md index 0577d18b..3a46d711 100644 --- a/docs/decorator.md +++ b/docs/decorator.md @@ -129,8 +129,6 @@ MyClass.isTestable // false 上面代码中,装饰器`testable`可以接受参数,这就等于可以修改装饰器的行为。 -注意,装饰器对类的行为的改变,是代码编译时发生的,而不是在运行时。这意味着,装饰器能在编译阶段运行代码。也就是说,装饰器本质就是编译时执行的函数。 - 前面的例子是为类添加一个静态属性,如果想添加实例属性,可以通过目标类的`prototype`对象操作。 ```javascript diff --git a/docs/generator.md b/docs/generator.md index 7d18278c..f93604c0 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 880cf08b..ea169591 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/module-loader.md b/docs/module-loader.md index ce8082cc..31dd685e 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 { diff --git a/docs/module.md b/docs/module.md index 0f248158..207c3830 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 @@ -853,3 +855,14 @@ 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 e9904c56..1b3cfbbf 100644 --- a/docs/number.md +++ b/docs/number.md @@ -820,7 +820,7 @@ for (let i = 1; i <= 70; i++) { console.log(p); // 1.197857166996989e+100 ``` -现在支持大整数了,就可以算了,浏览器的开发者工具运行下面代码,就OK。 +现在支持大整数了,就可以算了,浏览器的开发者工具运行下面代码,就 OK。 ```javascript let p = 1n; diff --git a/docs/object.md b/docs/object.md index cd817ec1..8abd76c2 100644 --- a/docs/object.md +++ b/docs/object.md @@ -783,7 +783,7 @@ actual.cause; // 'Error cause' 上面示例中,生成 Error 实例时,使用描述对象给出`cause`属性,写入报错的原因。然后,就可以从实例对象上读取这个属性。 -`casue`属性可以放置任意内容,不必一定是字符串。 +`cause`属性可以放置任意内容,不必一定是字符串。 ```javascript try { diff --git a/docs/promise.md b/docs/promise.md index ee6a7aab..758bb2ed 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!'); diff --git a/docs/proposals.md b/docs/proposals.md index cfa19bc3..2626982b 100644 --- a/docs/proposals.md +++ b/docs/proposals.md @@ -314,6 +314,18 @@ 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() JavaScript 内部使用64位浮点数(国际标准 IEEE 754)表示数值。IEEE 754 规定,64位浮点数的第一位是符号位,`0`表示正数,`1`表示负数。所以会有两种零,`+0`是符号位为`0`时的零,`-0`是符号位为`1`时的零。实际编程中,判断一个值是`+0`还是`-0`非常麻烦,因为它们是相等的。 diff --git a/docs/proxy.md b/docs/proxy.md index a65ec7dc..5885094b 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 bb3f87c7..359d045b 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 157778a9..02f2439e 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"] -/(? 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 之中。 @@ -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,21 +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 的键名,都会报错。 - -不过,现在有一个[提案](https://github.com/tc39/proposal-symbols-as-weakmap-keys),允许 Symbol 值也可以作为 WeakMap 的键名。一旦纳入标准,就意味着键名存在两种可能:对象和 Symbol 值。 +上面代码中,如果将数值`1`和`null`作为 WeakMap 的键名,都会报错,将 Symbol 值作为键名不会报错。 其次,`WeakMap`的键名所指向的对象,不计入垃圾回收机制。 @@ -1290,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 2fc48b4c..1d3e63a8 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 fc23e2ff..95fe4c92 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 2297e10d..4693410b 100644 --- a/docs/symbol.md +++ b/docs/symbol.md @@ -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 00000000..e1134119 --- /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/) +