# LearnECMAScript6 **Repository Path**: mrwhitebare/learn-ecmascript6 ## Basic Information - **Project Name**: LearnECMAScript6 - **Description**: 学习阮一峰老师《ECMAScript 6 入门》 - **Primary Language**: JavaScript - **License**: GPL-3.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 2 - **Forks**: 0 - **Created**: 2022-03-24 - **Last Updated**: 2023-09-21 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # LearnECMAScript6 ## 介绍 学习阮一峰老师《ECMAScript 6 入门》 [ECMAScript 6 入门 - 《阮一峰 ECMAScript 6 (ES6) 标准入门教程 第三版》 - 书栈网 · BookStack](https://www.bookstack.cn/read/es6-3rd/sidebar.md) ## 单点登录 > 单点登录(Single Sign On),`SSO`。**在多个应用系统中,只需登录一次,就可以访问其他相互信任的应用系统。** > > ![单点登录](https://yqfile.alicdn.com/721f02ebe06639e6232b59535d6423db75086693.png) ### 同域下的单点登录 一个企业一般情况下只有一个域名,通过二级域名区分不同的系统。 > **两个问题** > > - Cookie不能跨域,我们的Cookie的domain属性是sso.a.com,在app1.a.com和app2.a.com 发送请求无法携带; > - sso、app1和app2是不同的应用,他们的session存在自己的应用内,不共享。 > > **解决方案** > > ![]( https://yqfile.alicdn.com/4e31c204eea22ee07154df928a5ff5350da03d7a.png) > > 第一个问题,sso登录之后,可以将Cookie的域设置为顶域,即.a.com,这样所有的子域的系统都可以访问到顶域的Cookie。设置Cookie只能设置顶域和自己的域,不能设置其他的域。 > > 第二个问题,通过共享3个系统的Session,使用spring-Session. ### 不同域下的单点登录 ![](https://yqfile.alicdn.com/dcb743204f8a201be53df5338fc34affe5fa1059.png) 上图是CAS官网上的标准流程,具体流程如下: 1. 用户访问app系统,app系统是需要登录的,但用户现在没有登录。 2. 跳转到CAS server,即SSO登录系统,CAS Server即SSO系统。 SSO系统也没有登录,弹出用户登录页。 3. 用户填写用户名、密码,SSO系统进行认证后,将登录状态写入SSO的session,浏览器(Browser)中写入SSO域下的Cookie。 4. SSO系统登录完成后会生成一个ST(Service Ticket),然后跳转到app系统,同时将ST作为参数传递给app系统。 5. app系统拿到ST后,从后台向SSO发送请求,验证ST是否有效。 6. 验证通过后,app系统将登录状态写入session并设置app域下的Cookie。 至此,跨域单点登录就完成了。以后我们再访问app系统时,app就是登录的。接下来,我们再看看访问app2系统时的流程。 1. 用户访问app2系统,app2系统没有登录,跳转到SSO。 2. 由于SSO已经登录了,不需要重新登录认证。 3. SSO生成ST,浏览器跳转到app2系统,并将ST作为参数传递给app2。 4. app2拿到ST,后台访问SSO,验证ST是否有效。 5. 验证成功后,app2将登录状态写入session,并在app2域下写入Cookie。 ## 1. Babel 转码器 --- Babel 是一个广泛使用的 ES6 转码器,可以将 ES6 代码转为 ES5 代码,从而在老版本的浏览器执行。 这意味着,你可以用 ES6 的方式编写程序,又不用担心现有环境是否支持。 ```js // 转码前 input.map(item => item + 1); // 转码后 input.map(function (item) { return item + 1; }); ``` ### 1.1 配置文件 .babelrc Babel 的配置文件是`.babelrc`,存放在项目的根目录下。使用 Babel 的第一步,就是配置这个文件。 该文件用于设置转码规则和插件 ```json { "presets":[], "plugins":[] } ``` `presets` 字段设定转码规则,官方提供一下规则集,根据需求安装。 ```shell # 最新转码规则 $ npm install --save-dev @babel/preset-env # react 转码规则 $ npm install --save-dev @babel/preset-react ``` 规则加入`.babelrc` ```json { "presets": [ "@babel/env", "@babel/preset-react" ], "plugins": [] } ``` ### 1.2 命令行转码 `Babel` 提供命令行工具`@babel/cli`,用于命令行转码 ```shell npm install --save-dev @babel/cli ``` ```shell # 转码结果输出到标准输出 $ npx babel example.js # 转码结果写入一个文件 # --out-file 或 -o 参数指定输出文件 $ npx babel example.js --out-file compiled.js # 或者 $ npx babel example.js -o compiled.js # 整个目录转码 # --out-dir 或 -d 参数指定输出目录 $ npx babel src --out-dir lib # 或者 $ npx babel src -d lib # -s 参数生成source map文件 $ npx babel src -d lib -s ``` ### 1.3 babel-node `@babel/node`模块的`babel-node`命令,提供一个支持ES6的REPL环境,它支持 Node 的 REPL 环境的所有功能,而且可以直接运行 ES6 代码。 ``` shell #安装模块 $ npm install --save-dev @babel/node #执行babel-node 进入REPL环境 $ npx babel-node #babel-node 环境可以直接运行es6 ``` ### 1.4 @babel/register 模块 @babel/register模块改写require命令,为它加上一个钩子。此后,每当使用require加载.js、.jsx、.es和.es6后缀名的文件,就会先用 Babel 进行转码。 ```shell $ npm install --save-dev @babel/register ``` 使用时必须加载`@babel/register` ```js //index.js require("@babel/register"); require("./es6.js"); ``` ### 1.4 polyfill Babel 默认只转换新的 JavaScript 句法(syntax),而不转换新的 API,比如Iterator、Generator、Set、Map、Proxy、Reflect、Symbol、Promise等全局对象,以及一些定义在全局对象上的方法(比如Object.assign)都不会转码。 举例来说,ES6 在Array对象上新增了Array.from方法。Babel 就不会转码这个方法。如果想让这个方法运行,可以使用core-js和regenerator-runtime(后者提供generator函数的转码),为当前环境提供一个垫片。 ```shell $ npm install --save-dev core-js regenerator-runtime ``` 脚本头部添加 ```js import 'core-js'; import 'regenerator-runtime/runtime'; // 或者 require('core-js'); require('regenerator-runtime/runtime); ``` ## 2. let和const命令 ### 2.1 let命令 #### 基本用法 ES6 新增了let命令,用来声明变量。它的用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效。 ```js { let a=10; var b=2; } a;// Uncaught ReferenceError: a is not defined b;//2 ``` #### 暂时性死区 只要块级作用域存在`let`命令,它所声明的变量就绑定(binding)这个区域,不受外部的影响。 ```js var temp=123; if(true){ tmp="abc";//ReferceError let tmp; } ``` ### 2.2 块级作用域 #### 解决问题 块级作用域解决了`ES5`内层变量会覆盖外层变量的问题和用于计数的变量会泄露为全局变量的问题。 #### ES6的块级作用域 `let`实际为JavaScript新增了块级作用域。 块级作用域的出现,使得广泛应用的匿名立即执行函数表达式(匿名`IIFE`)不在必要了 ```js // IIFE 写法 (function () { var tmp ="..."; //... }()); // 块级作用域写法 { let tmp = "..."; //... } ``` ES6 引入了块级作用域,明确允许在块级作用域之中声明函数。ES6 规定,块级作用域之中, 函数声明语句的行为类似于let,在块级作用域之外不可引用。 ```js function f() { console.log('I am outside!'); } (function () { if (false) { // 重复声明一次函数f function f() { console.log('I am inside!'); } } f(); }()); ``` ### 2.3 const命令 #### 基本用法 `const`声明一个只读的常量。一当声明,常量的值就不能改变。 ```js const PI=3.1415; PI;// 3.1415 PI=3; //Uncaught TypeError: Assignment to constant variable. ``` #### 本质 `const`实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,`const`只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。 ```js const foo={}; foo.prop=123; foo.prop; // 123 foo={}; // Uncaught TypeError: Assignment to constant variable. const a=[]; a.push("Hello"); a.lenth=0; a=['Dave'];//改变了引用地址,报错 ``` 如果想要将对象冻结,应该使用`Object.freeze`方法 ```js const foo=Object.freeze({}); //常规模式下,不起作用 //严格模式下,该行报错 foo.prop=123; ``` 以下为彻底冻结对象的函数 ```js const constantize=(obj)=>{ Object.freeze(obj); Object.keys(obj).forEach((key,i)=>{ if(typeof obj[key]==='object'){ constructor(obj[key]); } }) } ``` ### 2.4 globalThis 对象 JavaScript 语言存在一个顶层对象,它提供全局环境(即全局作用域),所有代码都是在这个环境中运行。但是,顶层对象在各种实现里面是不统一的。 - 浏览器里面,顶层对象是`window`,但 Node 和 Web Worker 没有`window`。 - 浏览器和 Web Worker 里面,`self`也指向顶层对象,但是 Node 没有`self`。 - Node 里面,顶层对象是`global`,但其他环境都不支持。 同一段代码为了能够在各种环境,都能取到顶层对象,现在一般是使用`this`变量,但是有局限性。 - 全局环境中,`this`会返回顶层对象。但是,Node 模块和 ES6 模块中,`this`返回的是当前模块。 - 函数里面的`this`,如果函数不是作为对象的方法运行,而是单纯作为函数运行,`this`会指向顶层对象。但是,严格模式下,这时`this`会返回`undefined`。 - 不管是严格模式,还是普通模式,`new Function('return this')()`,总是会返回全局对象。但是,如果浏览器用了 CSP(Content Security Policy,内容安全策略),那么`eval`、`new Function`这些方法都可能无法使用。 ```js // 方法一 (typeof window !== 'undefined' ? window : (typeof process === 'object' && typeof require === 'function' && typeof global === 'object') ? global : this); // 方法二 var getGlobal = function () { if (typeof self !== 'undefined') { return self; } if (typeof window !== 'undefined') { return window; } if (typeof global !== 'undefined') { return global; } throw new Error('unable to locate global object'); }; ``` ## 3. 变量的结构赋值 ### 3.1 数组的结构赋值 #### 基本用法 ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。 ```js let [a, b, c] = [1, 2, 3]; ``` 本质上,这种写法属于`模式匹配`,只要等号两边的模式相同,左边的变量就会被赋予对应的值。 ```js let [foo, [[bar], baz]] = [1, [[2], 3]]; foo // 1 bar // 2 baz // 3 let [ , , third] = ["foo", "bar", "baz"]; third // "baz" let [x, , y] = [1, 2, 3]; x // 1 y // 3 let [head, ...tail] = [1, 2, 3, 4]; head // 1 tail // [2, 3, 4] let [x, y, ...z] = ['a']; x // "a" y // undefined z // [] ``` 如果等号的右边没有继承`Iterator`,那么将会报错 ```js // 报错 let [foo] = 1; let [foo] = false; let [foo] = NaN; let [foo] = undefined; let [foo] = null; let [foo] = {}; ``` 对于Set结构,可以使用数组的结构赋值 ```js let [x, y, z] = new Set(['a', 'b', 'c']); x // "a" ``` > 只要某种数据结构具有`Iterator`接口,都可以采用数组的形式结构赋值 > > ```js > function* fibs(){ > let a=0; > let b=1; > while(true){ > yield a; > [a,b]=[b,a+b]; > } > } > let [first, second, third, fourth, fifth, sixth] = fibs(); > sixth //5 > ``` #### 默认值 结构赋值允许指定默认值 ```js let [foo=true]=[]; foo//true let [x, y = 'b'] = ['a']; // x='a', y='b' let [x, y = 'b'] = ['a', undefined]; // x='a', y='b' ``` 注意,ES6 内部使用严格相等运算符(`===`),判断一个位置是否有值。所以,只有当一个数组成员严格等于`undefined`,默认值才会生效。 ```js let [x = 1] = [undefined]; x // 1 let [x = 1] = [null]; x // null ``` 如果默认值是一个表达式,那么这个表达式是惰性求值的,即只有在用到的时候,才会求值。 ```js let x; if ([1][0] === undefined) { x = f(); } else { x = [1][0]; } ``` 默认值可以引用解构赋值的其他变量,但该变量必须已经声明。 ```js let [x = 1, y = x] = []; // x=1; y=1 let [x = 1, y = x] = [2]; // x=2; y=2 let [x = 1, y = x] = [1, 2]; // x=1; y=2 let [x = y, y = 1] = []; // ReferenceError: y is not defined ``` ### 3.2 对象的结构赋值 对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。 ```js let { bar, foo } = { foo: 'aaa', bar: 'bbb' }; foo // "aaa" bar // "bbb" let { baz } = { foo: 'aaa', bar: 'bbb' }; baz // undefined ``` 如果解构失败,变量的值等于`undefined`。 ```js let {foo} = {bar: 'baz'}; foo // undefined ``` 对象的解构赋值,可以很方便地将现有对象的方法,赋值到某个变量。 ```js // 例一 let { log, sin, cos } = Math; // 例二 const { log } = console; log('hello') // hello ``` 如果变量名与属性名不一致,必须写成下面这样。 ```js let { foo: baz } = { foo: 'aaa', bar: 'bbb' }; baz // "aaa" let obj = { first: 'hello', last: 'world' }; let { first: f, last: l } = obj; f // 'hello' l // 'world' ``` 也就是说,对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。 ```js let { foo: baz } = { foo: 'aaa', bar: 'bbb' }; baz // "aaa" foo // error: foo is not defined ``` 与数组一样,对象的结构赋值也可以用于嵌套结构中 ```js let obj = { p: [ 'Hello', { y: 'World' } ] }; let { p: [x, { y }] } = obj; x // "Hello" y // "World" ``` > 注意,这时`p`是模式,不是变量,因此不会被赋值。如果`p`也要作为变量赋值,可以写成下面这样。 > > ```js > let obj = { > p: [ > 'Hello', > { y: 'World' } > ] > }; > let { p, p: [x, { y }] } = obj; > x // "Hello" > y // "World" > p // ["Hello", {y: "World"}] > ``` > > - 如果要将一个已经声明的变量用于解构赋值,必须非常小心。 > > ```js > // 错误的写法 > let x; > {x} = {x: 1}; > // SyntaxError: syntax error > ``` > > 上面代码的写法会报错,因为 JavaScript 引擎会将`{x}`理解成一个代码块,从而发生语法错误。只有不将大括号写在行首,避免 JavaScript 将其解释为代码块,才能解决这个问题。 > > ```js > // 正确的写法 > let x; > ({x} = {x: 1}); > ``` > > - 解构赋值允许等号左边的模式之中,不放置任何变量名。因此,可以写出非常古怪的赋值表达式。 > > ```js > ({} = [true, false]); > ({} = 'abc'); > ({} = []); > ``` > > - 由于数组本质是特殊的对象,因此可以对数组进行对象属性的解构。 > > ```js > let arr = [1, 2, 3]; > let {0 : first, [arr.length - 1] : last} = arr; > first // 1 > last // 3 > ``` > ### 3.3 字符串的结构赋值 字符串也可以进行结构赋值,此时——字符串被转换成一个类似于数组的对象。 ```js const [a,b,c,d,e]="hello"; undefined a //'h' b //'e' c //'l' d //'l' e //'o' const {length:len}="hello" len //5 ``` ### 3.4 数值和布尔值的结构赋值 结构赋值时,如果等号右边是数值和布尔值,则会先转化为对象 ```js let {toString:s}=123 s===Number.prototype.toString //true let {toString:s}=true s===Boolean.prototype.toString //true ``` > 结构赋值的规则是,只要等号右边的值不是对象或数值,就先将其转化为对象。由于`undefined`和`null`无法转化为对象,所以对它们进行解构赋值,都会报错。 > > ```js > let {prop:x}=undefined; > let {prop:y}=null; > //Error: > //Uncaught TypeError > ``` ### 3.5 函数参数的结构赋值 函数的参数也可以使用解构赋值。 ``` function add([x, y]){ return x + y;}add([1, 2]); // 3 ``` 上面代码中,函数`add`的参数表面上是一个数组,但在传入参数的那一刻,数组参数就被解构成变量`x`和`y`。对于函数内部的代码来说,它们能感受到的参数就是`x`和`y`。 下面是另一个例子。 ```js [[1, 2], [3, 4]].map(([a, b]) => a + b);// [ 3, 7 ] ``` 函数参数的解构也可以使用默认值。 ```js function move({x = 0, y = 0} = {}) { return [x, y];}move({x: 3, y: 8}); // [3, 8]move({x: 3}); // [3, 0]move({}); // [0, 0]move(); // [0, 0] ``` 上面代码中,函数`move`的参数是一个对象,通过对这个对象进行解构,得到变量`x`和`y`的值。如果解构失败,`x`和`y`等于默认值。 注意,下面的写法会得到不一样的结果。 ```js function move({x, y} = { x: 0, y: 0 }) { return [x, y];}move({x: 3, y: 8}); // [3, 8]move({x: 3}); // [3, undefined]move({}); // [undefined, undefined]move(); // [0, 0] ``` 上面代码是为函数`move`的参数指定默认值,而不是为变量`x`和`y`指定默认值,所以会得到与前一种写法不同的结果。 `undefined`就会触发函数参数的默认值。 ```js [1, undefined, 3].map((x = 'yes') => x);// [ 1, 'yes', 3 ] ``` ### 3.6 用途 - 1.交换变量的值 ```js let x=1; let y=2; [x,y]=[y,x]; ``` - 2.从函数返回多个值 函数只能返回一个值,如果要返回多个值,只能将它们放在数组或对象里返回。有了解构赋值,取出这些值就非常方便。 ```js // 返回一个数组 function example() { return [1, 2, 3]; } let [a, b, c] = example(); // 返回一个对象 function example() { return { foo: 1, bar: 2 }; } let { foo, bar } = example(); ``` - 3.函数参数的定义 解构赋值可以方便地将一组参数与变量名对应起来。 ```js // 参数是一组有次序的值 function f([x, y, z]) { ... } f([1, 2, 3]); // 参数是一组无次序的值 function f({x, y, z}) { ... } f({z: 3, y: 2, x: 1}); ``` - 4.提取JSON数据 解构赋值对提取 JSON 对象中的数据,尤其有用。 ```js let jsonData = { id: 42, status: "OK", data: [867, 5309] }; let { id, status, data: number } = jsonData; console.log(id, status, number); // 42, "OK", [867, 5309] ``` - 5.函数参数的默认值 ```js jQuery.ajax = function (url, { async = true, beforeSend = function () {}, cache = true, complete = function () {}, crossDomain = false, global = true, // ... more config } = {}) { // ... do stuff }; ``` - 6.遍历Map结构 任何部署了 Iterator 接口的对象,都可以用`for...of`循环遍历。Map 结构原生支持 Iterator 接口,配合变量的解构赋值,获取键名和键值就非常方便。 ```js const map = new Map(); map.set('first', 'hello'); map.set('second', 'world'); for (let [key, value] of map) { console.log(key + " is " + value); } // first is hello // second is world ``` 如果只想获取键名,或者只想获取键值,可以写成下面这样。 ```js // 获取键名 for (let [key] of map) { // ... } // 获取键值 for (let [,value] of map) { // ... } ``` - 7.输入模块的指定方法 加载模块时,往往需要指定输入哪些方法。解构赋值使得输入语句非常清晰。 ```js const { SourceMapConsumer, SourceNode } = require("source-map"); ``` ## 4. 字符串的扩展 ### 4.1 字符串的Unicode表示法 `ES6`加强了对`Unicode`的支持,允许采用`\uxxxx`形式表示一个字符。 ```js "\u0061" ``` 但是,这种表示法只限于码点在`\u0000`~`\uFFFF`之间的字符。超出这个范围的字符,必须用两个双字节的形式表示。 ```js "\uD842\uDFB7" // "𠮷" "\u20BB7" // " 7" ``` 上面代码表示,如果直接在`\u`后面跟上超过`0xFFFF`的数值(比如`\u20BB7`),JavaScript 会理解成`\u20BB+7`。由于`\u20BB`是一个不可打印字符,所以只会显示一个空格,后面跟着一个`7`。 ES6 对这一点做出了改进,只要将码点放入大括号,就能正确解读该字符。 ```js "\u{20BB7}"// "𠮷" "\u{41}\u{42}\u{43}"// "ABC" let hello = 123;hell\u{6F} // 123 '\u{1F680}' === '\uD83D\uDE80'// true ``` 上面代码中,最后一个例子表明,大括号表示法与四字节的 UTF-16 编码是等价的。 有了这种表示法之后,JavaScript 共有 6 种方法可以表示一个字符。 ```js '\z' === 'z' // true '\172' === 'z' // true '\x7A' === 'z' // true '\u007A' === 'z' // true '\u{7A}' === 'z' // true ``` ### 4.2 字符串的遍历器接口 `ES6`为字符串添加了遍历器接口,使得字符串可以被`for...of`循环解析。 ```js for (let codePoint of 'foo') console.log(codePoint); //"f" //"o" //"o" ``` 这个遍历器最大的优点是可以识别大于`0xFFFF`的码点,传统的`for`循环无法识别这样的码点。 ```js let text=String.fromCodePoint(0x20BB7); for(let i=0;i>text.length;i++) console.log(text[i]); //undefined for(let i of text) console.log(i) //𠮷 ``` ### 4.3 直接输入 U+2028和U+2029 JavaScript 字符串允许直接输入字符,以及输入字符的转义形式。 ```js '\u4e2d\u56fd'==='中国' // true ``` > `JavaScript` 规定有5个字符串,不能在字符串中直接使用,只能使用转义形式。 > > - U+005C:反斜杠(reverse solidus) > - U+000D:回车(carriage return) > - U+2028:行分隔符(line separator) > - U+2029:段分隔符(paragraph separator) > - U+000A:换行符(line feed) ### 4.4 JSON.stringify() 的改造 **UTF-8** 标准规定,`0xD800`到`0xDFFF`之间的码点,不能单独使用,必须配对使用。比如,`\uD834\uDF06`是两个码点,但是必须放在一起配对使用,代表字符`𝌆`。这是为了表示码点大于`0xFFFF`的字符的一种变通方法。单独使用`\uD834`和`\uDFO6`这两个码点是不合法的,或者颠倒顺序也不行,因为`\uDF06\uD834`并没有对应的字符。 `JSON.stringify()`的问题在于,他可能返回`0xD800`到`0xDFFF`之间的单个码点 ```js JSON.stringify('\u{D834}') // "\u{D834}" ``` 为了确保返回的是合法的 UTF-8 字符,[ES2019](https://github.com/tc39/proposal-well-formed-stringify) 改变了`JSON.stringify()`的行为。如果遇到`0xD800`到`0xDFFF`之间的单个码点,或者不存在的配对形式,它会返回转义字符串,留给应用自己决定下一步的处理。 ```js JSON.stringify('\u{D834}') // ""\\uD834"" JSON.stringify('\uDF06\uD834') // ""\\udf06\\ud834"" ``` ### 4.5 模板字符串 `字符串拼接方法` ```js $('#result').append( 'There are ' + basket.count + ' ' + 'items in your basket, ' + '' + basket.onSale + ' are on sale!' ); ``` `模板字符串` ```js $('#result').append(` There are ${basket.count} items in your basket,${basket.onSale} are on sale! `); ``` 模板字符串使用反引号表示。如果在模板字符串中使用反引号,则前面要用反斜杠转义 ```js let greeting=`\`Yo\` World!`; ``` 模板字符串的空格和换行,都是被保留的,如果不想要换行,可以使用`trim`方法取消它。 ```js $('#list').html(` `.trim()); ``` 模板字符串可以嵌套 ```js const tmpl=addrs=>` ${addrs.map(addr=>` `).join('')}
${addr.first}
${addr.last}
`; const data=[ {first:'',last:'Bond'}, {first:'Lars',last:''} ]; console.log(tmpl(data)); ``` ### 4.6 模板编译 ```js function compile(template){ const evalExpr = /<%=(.+?)%>/g; const expr = /<%([\s\S]+?)%>/g; template = template .replace(evalExpr, '`); \n echo( $1 ); \n echo(`') .replace(expr, '`); \n $1 \n echo(`'); template = 'echo(`' + template + '`);'; let script = `(function parse(data){ let output = ""; function echo(html){ output += html; } ${ template } return output; })`; return script; } let template=` `; let parse = eval(compile(template)); let div = parse({ supplies: [ "broom", "mop", "cleaner" ] }); console.log(div); /*output: * */ ``` ### 4.7 标签模板 模板字符串后面可以紧跟在一个函数名后面,该函数将被用来处理这个模板字符串。这被称为"标签模板"功能(tagged template)。 ```js alert`hello` // 等同于 alert(['hello']) ``` 如果模板字符串中有变量,会将模板字符串先处理成多个参数,再调用函数。 ```js let a=5; let b=10; tag`Hello ${a+b} world ${a*b}`; //等同于 tag(['Hello ', ' world ', ''], 15, 50); ``` tag函数的第一个参数是一个数组,该数组的成员是模板字符串中那些没有变量替换的部分,也就是说,变量替换只发生在数组的第一个成员与第二个成员之间、第二个成员与第三个成员之间,以此类推。 tag函数的其他参数,都是模板字符串各个变量被替换后的值。由于本例中,模板字符串含有两个变量,因此tag会接受到value1和value2两个参数。 tag函数所有参数的实际值如下。 - 第一个参数:['Hello ', ' world ', ''] - 第二个参数: 15 - 第三个参数:50 ```js function tag(stringArr, value1, value2){ // ... } // 等同于 function tag(stringArr, ...values){ // ... } ``` **标签模板**的重要应用——过滤`HTML`字符串,防止用户输入恶意内容 ```js let sender=''; let message=SaferHTML`

${sender} has sent you a message.

>`; /** * @description 转化恶意字符 * @type {string} */ function SaferHTML(templateDate){ let s=templateDate[0]; for (let i = 1; i < arguments.length; i++) { let arg=String(arguments[i]); s+=arg.replace(/&/g,"&") .replace(//g,">"); s+=templateDate[i]; } return s; } ``` **模板标签**的重要应用——国际化处理 ```js i18n`Welcome to ${siteName}, you are visitor number ${visitorNumber}!` // "欢迎访问xxx,您是第xxxx位访问者!" ``` **模板处理**函数的第一个参数(模板字符串数组),还有一个`raw`属性。 ```js tag`First line\nSecond line` function tag(strings) { console.log(strings.raw[0]); // strings.raw[0] 为 "First line\\nSecond line" // 打印输出 "First line\nSecond line" } ``` ## 5. 字符串新增方法 ### 5.1 String.fromCodePoint() ES5 提供`String.fromCharCode()`方法,用于从 Unicode 码点返回对应字符,但是这个方法不能识别码点大于`0xFFFF`的字符。 ```js String.fromCharCode(0x20BB7); //'ஷ' ``` 上面代码中,`String.fromCharCode()`不能识别大于`0xFFFF`的码点,所以`0x20BB7`就发生了溢出,最高位`2`被舍弃了,最后返回码点`U+0BB7`对应的字符,而不是码点`U+20BB7`对应的字符。 ES6 提供了`String.fromCodePoint()`方法,可以识别大于`0xFFFF`的字符,弥补了`String.fromCharCode()`方法的不足。在作用上,正好与下面的`codePointAt()`方法相反。 ```js String.fromCodePoint(0x20BB7) // "𠮷" String.fromCodePoint(0x78, 0x1f680, 0x79) === 'x\uD83D\uDE80y' // true ``` 上面代码中,如果`String.fromCodePoint`方法有多个参数,则它们会被合并成一个字符串返回。 注意,`fromCodePoint`方法定义在`String`对象上,而`codePointAt`方法定义在字符串的实例对象上。 ### 5.2 String.raw() ES6 还为原生的 String 对象,提供了一个`raw()`方法。该方法返回一个斜杠都被转义(即斜杠前面再加一个斜杠)的字符串,往往用于模板字符串的处理方法。 ```js String.raw`Hi\n${2+3}!` // "Hi\\n5!" String.raw`Hi\u000A!`; //"Hi\\u000A!" ``` 如果原字符串的斜杠已经转义,那么`String.raw()`会进行再次转义。 ```js String.raw`Hi\\n` // 返回 "Hi\\\\n" String.raw`Hi\\n` === "Hi\\\\n" // true ``` `String.raw()`本质上是一个正常的函数,只是专用于模板字符串的标签函数。如果写成正常函数的形式,它的第一个参数,应该是一个具有`raw`属性的对象,且`raw`属性的值应该是一个数组,对应模板字符串解析后的值。 ```js // `foo${1 + 2}bar` // 等同于 String.raw({ raw: ['foo', 'bar'] }, 1 + 2) // "foo3bar" ``` 上面代码中,`String.raw()`方法的第一个参数是一个对象,它的`raw`属性等同于原始的模板字符串解析后得到的数组。 作为函数,`String.raw()`的代码实现基本如下。 ```js String.raw = function (strings, ...values) { let output = ''; let index; for (index = 0; index < values.length; index++) { output += strings.raw[index] + values[index]; } output += strings.raw[index] return output; } ``` ### 5.3 实例方法::codePointAt() JavaScript 内部,字符以 UTF-16 的格式储存,每个字符固定为`2`个字节。对于那些需要`4`个字节储存的字符(Unicode 码点大于`0xFFFF`的字符),JavaScript 会认为它们是两个字符。 ```js var s = "𠮷"; s.length // 2 s.charAt(0) // '' s.charAt(1) // '' s.charCodeAt(0) // 55362 s.charCodeAt(1) // 57271 ``` 汉字“𠮷”的码点是`0x20BB7`,UTF-16 编码为`0xD842 0xDFB7`(十进制为`55362 57271`),需要`4`个字节储存。对于这种`4`个字节的字符,JavaScript 不能正确处理,字符串长度会误判为`2`,而且`charAt()`方法无法读取整个字符,`charCodeAt()`方法只能分别返回前两个字节和后两个字节的值。 ES6 提供了`codePointAt()`方法,能够正确处理 4 个字节储存的字符,返回一个字符的码点。 ```js let s = '𠮷a'; s.codePointAt(0) // 134071 s.codePointAt(1) // 57271 s.codePointAt(2) // 97 ``` `codePointAt()`方法的参数,是字符在字符串中的位置(从 0 开始)。上面代码中,JavaScript 将“𠮷a”视为三个字符,codePointAt 方法在第一个字符上,正确地识别了“𠮷”,返回了它的十进制码点 134071(即十六进制的`20BB7`)。在第二个字符(即“𠮷”的后两个字节)和第三个字符“a”上,`codePointAt()`方法的结果与`charCodeAt()`方法相同。 总之,`codePointAt()`方法会正确返回 32 位的 UTF-16 字符的码点。对于那些两个字节储存的常规字符,它的返回结果与`charCodeAt()`方法相同。 `codePointAt()`方法返回的是码点的十进制值,如果想要十六进制的值,可以使用`toString()`方法转换一下。 ```js let s = '𠮷a'; s.codePointAt(0).toString(16) // "20bb7" s.codePointAt(2).toString(16) // "61" ``` `codePointAt()`方法的参数,仍然是不正确的。比如,上面代码中,字符`a`在字符串`s`的正确位置序号应该是 1,但是必须向`codePointAt()`方法传入 2。解决这个问题的一个办法是使用`for...of`循环,因为它会正确识别 32 位的 UTF-16 字符。 ``` let s = '𠮷a';for (let ch of s) { console.log(ch.codePointAt(0).toString(16));}// 20bb7// 61 ``` 另一种方法也可以,使用扩展运算符(`...`)进行展开运算。 ```js let arr = [...'𠮷a']; // arr.length === 2 arr.forEach( ch => console.log(ch.codePointAt(0).toString(16)) ); // 20bb7 // 61 ``` `codePointAt()`方法是测试一个字符由两个字节还是由四个字节组成的最简单方法。 ```js function is32Bit(c) { return c.codePointAt(0) > 0xFFFF; } is32Bit("𠮷") // true is32Bit("a") // false ``` ### 5.4 实例方法:normalize() `normalize`方法可以接受一个参数来指定`normalize`的方式,参数的四个可选值如下。 - `NFC`,默认参数,表示“标准等价合成”(Normalization Form Canonical Composition),返回多个简单字符的合成字符。所谓“标准等价”指的是视觉和语义上的等价。 - `NFD`,表示“标准等价分解”(Normalization Form Canonical Decomposition),即在标准等价的前提下,返回合成字符分解的多个简单字符。 - `NFKC`,表示“兼容等价合成”(Normalization Form Compatibility Composition),返回合成字符。所谓“兼容等价”指的是语义上存在等价,但视觉上不等价,比如“囍”和“喜喜”。(这只是用来举例,`normalize`方法不能识别中文。) - `NFKD`,表示“兼容等价分解”(Normalization Form Compatibility Decomposition),即在兼容等价的前提下,返回合成字符分解的多个简单字符。 ```js '\u004F\u030C'.normalize('NFC').length // 1 '\u004F\u030C'.normalize('NFD').length // 2 ``` 上面代码表示,`NFC`参数返回字符的合成形式,`NFD`参数返回字符的分解形式。 ```js '\u01d1' //'Ǒ' '\u004f\u030c' //'Ǒ' '\u01D1'==='\u004F\u030C' //false '\u01D1'.normalize() === '\u004F\u030C'.normalize() //true ``` ### 5.5 实例方法: includes()、startsWith()、endsWith() - **includes()**:返回布尔值,表示是否找到了参数字符串。 - **startsWith()**:返回布尔值,表示参数字符串是否在原字符串的头部。 - **endsWith()**:返回布尔值,表示参数字符串是否在原字符串的尾部。 ```js let s = 'Hello world!'; s.startsWith('Hello') // true s.endsWith('!') // true s.includes('o') // true ``` 这三个方法都支持第二个参数,表示开始搜索的位置。 ```js let s = 'Hello world!'; s.startsWith('world', 6) // true s.endsWith('Hello', 5) // true s.includes('Hello', 6) // false ``` 上面代码表示,使用第二个参数`n`时,`endsWith`的行为与其他两个方法有所不同。它针对前`n`个字符,而其他两个方法针对从第`n`个位置直到字符串结束。 ### 5.6 实例方法:repeat() `repeat`方法返回一个新字符串,表示将原字符串重复`n`次 ```js 'x'.repeat(3) //'xxx' 'hello'.repeat(2) //'hellohello' 'na'.repeat(0) // '' ``` 参数如果是小数,会被取整 `'na'.repeat(2.9)` `nana` 如果`repeat`的参数是负数或者`Infinity`,会报错。 ```js 'na'.repeat(Infinity) // RangeError 'na'.repeat(-1) // RangeError ``` 但是,如果参数是 0 到-1 之间的小数,则等同于 0,这是因为会先进行取整运算。0 到-1 之间的小数,取整以后等于`-0`,`repeat`视同为 0。 ```js 'na'.repeat(-0.9); //"" ``` 参数`NaN`等同于 0。 ```js 'na'.repeat(NaN) // "" ``` 如果`repeat`的参数是字符串,则会先转换成数字。 ```js 'na'.repeat('na') // "" 'na'.repeat('3') // "nanana" ``` ### 5.7 实例方法:padStart()、padEnd() `ES2017`引入字符串补全长度的功能。如果某个字符串不够指定长度。会在头部或尾部补全。`padStart()`用于头部补全,`padEnd()`用于尾部补全 ```js 'x'.padStart(5,'ab') //'ababx' 'x'.padStart(4,'ab') //'abax' 'x'.padEnd(5,'ab') //'xabab' 'x'.padEnd(4,'ab') //'xaba' ``` `padStart()` `padEnd()` 一共接受两个参数,第一个参数是字符串补全会生效的最大长度,第二参数是用来补全的字符串。 - 如果原字符串的长度,等于或大于最大长度,则字符串补全不生效,返回原字符串 - 如果原字符串的长度,等于或者大于最大长度,则字符串补全不生效,返回原字符串 - 如果用来补全的字符串与原字符串,两者的长度之和超过了最大长度,则会截去超出位数的补全字符串 - 如果省略第二个参数,默认使用空格补全长度 `padStart()`的常见用途是为数值补全指定位数。下面代码生成 10 位的数值字符串。 ```js '1'.padStart(10, '0') // "0000000001" '12'.padStart(10, '0') // "0000000012" '123456'.padStart(10, '0') // "0000123456" ``` 另一个用途是提示字符串格式。 ```js '12'.padStart(10, 'YYYY-MM-DD') // "YYYY-MM-12" '09-12'.padStart(10, 'YYYY-MM-DD') // "YYYY-09-12" ``` ### 5.8 实例方法:trimStart()、trimEnd() `ES2019`对字符串实例新增了`trimStart()`和`trimEnd()`两个实例方法。他们的行为与`trim()`一致,`trimStart()`消除字符串头部的空格,`trimEnd()`消除尾部的空格。它们返回的都是新字符串,不会修改原始字符串。 ```js const s=' abc'; s.trim(); 'abc' s.trimStart() 'abc' s.trimEnd(); ' abc' ``` 上面代码中,`trimStart()`只消除头部的空格,保留尾部的空格。`trimEnd()`也是类似行为。 除了空格键,这两个方法对字符串头部(或尾部)的 tab 键、换行符等不可见的空白符号也有效。 浏览器还部署了额外的两个方法,`trimLeft()`是`trimStart()`的别名,`trimRight()`是`trimEnd()`的别名。 ### 5.9 实例方法:matchAll() `matchAll()`方法返回一个正则表达式在当前字符串的所有匹配 ## 6. 正则的扩展 ### 6.1 RegExp构造函数 在 ES5 中,`RegExp`构造函数的参数有两种情况。 第一种情况是,参数是字符串,这时第二个参数表示正则表达式的修饰符(flag)。 ```js var regex = new RegExp('xyz', 'i'); // 等价于 var regex = /xyz/i; ``` 第二种情况是,参数是一个正则表示式,这时会返回一个原有正则表达式的拷贝。 ```js var regex = new RegExp(/xyz/i); // 等价于 var regex = /xyz/i; ``` 但是,ES5 不允许此时使用第二个参数添加修饰符,否则会报错。 ```js var regex = new RegExp(/xyz/, 'i'); // Uncaught TypeError: Cannot supply flags when constructing one RegExp from another ``` ES6 改变了这种行为。如果`RegExp`构造函数第一个参数是一个正则对象,那么可以使用第二个参数指定修饰符。而且,返回的正则表达式会忽略原有的正则表达式的修饰符,只使用新指定的修饰符。 ```js new RegExp(/abc/ig, 'i').flags // "i" ``` 上面代码中,原有正则对象的修饰符是`ig`,它会被第二个参数`i`覆盖。 ### 6.2 Unicode 属性类 `ES2018`引入了一种新的类的写法`\p{...}`和`\P{...}`,允许正则表达式匹配符合`Unicode`某种属性的所有字符。 ```js const regexGreekSymbol=/\p{Script=Greek}/u; // \p{Script=Greek} 指匹配一个希腊字母 regexGreekSymbol.test('π'); //true ``` Unicode 属性类要指定属性名和属性值。 ```js \p{UnicodePropertyName=UnicodePropertyValue} //对于某些属性,可以只写属性名,或者只写属性值 \p{UnicodePropertyName} \p{UnicodePropertyValue} ``` `\P{…}`是`\p{…}`的反向匹配,即匹配不满足条件的字符。 注意,这两种类只对 Unicode 有效,所以使用的时候一定要加上`u`修饰符。如果不加`u`修饰符,正则表达式使用`\p`和`\P`会报错,ECMAScript 预留了这两个类。 ```js const regex = /^\p{Decimal_Number}+$/u; regex.test('𝟏𝟐𝟑𝟜𝟝𝟞𝟩𝟪𝟫𝟬𝟭𝟮𝟯𝟺𝟻𝟼') // true ``` `\p{Number}`可以匹配罗马数字 ```js const regex=/^\p{Number}+$/u; regex.test('²³¹¼½¾'); //true regex.test('㉛㉜㉝'); //true regex.test('ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩⅪⅫ') //true ``` **其他例子** ```js // 匹配所有空格 \p{White_Space} // 匹配各种文字的所有字母,等同于 Unicode 版的 \w [\p{Alphabetic}\p{Mark}\p{Decimal_Number}\p{Connector_Punctuation}\p{Join_Control}] // 匹配各种文字的所有非字母的字符,等同于 Unicode 版的 \W [^\p{Alphabetic}\p{Mark}\p{Decimal_Number}\p{Connector_Punctuation}\p{Join_Control}] // 匹配 Emoji /\p{Emoji_Modifier_Base}\p{Emoji_Modifier}?|\p{Emoji_Presentation}|\p{Emoji}\uFE0F/gu // 匹配所有的箭头字符 const regexArrows = /^\p{Block=Arrows}+$/u; regexArrows.test('←↑→↓↔↕↖↗↘↙⇏⇐⇑⇒⇓⇔⇕⇖⇗⇘⇙⇧⇩') // true ``` ### 6.3 具名组匹配 正则表达式使用圆括号进行组匹配 ```js const RE_DATE = /(\d{4})-(\d{2})-(\d{2})/; const matchObj = RE_DATE.exec('1999-12-31'); //['1999-12-31', '1999', '12', '31', index: 0, input: '1999-12-31', groups: undefined] const year = matchObj[1]; // 1999 const month = matchObj[2]; // 12 const day = matchObj[3]; // 31 ``` > 组匹配的一个问题是,每一组的匹配含义不容易看出来,而且只能用数字序号(比如`matchObj[1]`)引用,要是组的顺序变了,引用的时候就必须修改序号。 `ES2018`引入具名组匹配(Named Capture Groups),允许为每一个组匹配制定一个名字。模式的头部添加`?` ```js const RE_DATE = /(?\d{4})-(?\d{2})-(?\d{2})/; const matchObj = RE_DATE.exec('1999-12-31'); //['1999-12-31', '1999', '12', '31', index: 0, input: '1999-12-31', groups: {…}] //groups: {year: '1999', month: '12', day: '31'} const year = matchObj.groups.year; // 1999 const month = matchObj.groups.month; // 12 const day = matchObj.groups.day; // 31 ``` 如果具名组没有找到匹配,那么`matchObj.groups.as`属性值就是undefined。 **结构赋值和替换** 有了具名组匹配以后,可以使用解构赋值直接从匹配结果上为变量赋值 ```js let {groups:{one,two}}=/^(?.*):(?.*)$/u.exec('foo:bar'); one;//foo two;//bar ``` 字符串替换时,使用`$<组名>`引用具名组 ```js let re = /(?\d{4})-(?\d{2})-(?\d{2})/u; '2022-04-10'.replace(re,'$/$/$'); //'10/04/2022' ``` `replace`方法的第二个参数也可以是函数 ```js '2018-06-15'.replace(re,(matched,capture1,capture2,capture3,position,s,groups)=>{ let {day,month,year}=groups; return `${day}/${month}/${year}`; }); //'15/06/2018' ``` **引用** 如果要在正则表达式内部引用某个“具名组匹配”,可以使用`\k<组名>`的写法(k小写); ```js const RE_TWICE=/^(?[a-z]+)!\k$/; RE_TWICE.test('abc!abc'); //true RE_TWICE.test('abc!ab'); //false ``` 数字引用(`\1`)依然有效 ```js const RE_TWICE=/^(?[a-z]+)!\1$/; RE_TWICE.test('abc!abc'); //true RE_TWICE.test('abc!ab'); //false ``` 同时使用两种写法 ```js const RE_TWICE=/^(?[a-z]+)!\k!\1$/; RE_TWICE.test('abc!abc'); //false RE_TWICE.test('abc!abc!abc'); //true RE_TWICE.test('abc?ac?abc'); //false ``` ### 6.4 正则匹配索引 如果正则表达式包含具名组匹配,`indices`属性数据还有一个`groups`属性。该属性是一个对象,可以从该对象获取具名组匹配的开始位置和结束位置。 ```js const text='zabbcdef'; const re=/ab+(?cd)/; const result=re.exec(text); result.groups //{Z: 'cd'}Z: "cd" result /*  ['abbcd', 'cd', index: 1, input: 'zabbcdef', groups: {Z: 'cd'}] */ ``` ### 6.5 String.prototype.matchAll() 如果一个正则表达式在字符串里面有多个匹配,现在一般使用`g`修饰符或`y`修饰符,在循环里面逐一取出。 ```js let regex=/t(e)(st(\d?))/g; let string = 'test1test2test3'; let matches=[]; let match; while(match=regex.exec(string)){ matches.push(match); } /*[ ['test1', 'e', 'st1', '1', index: 0, input: 'test1test2test3', groups: undefined] ['test2', 'e', 'st2', '2', index: 5, input: 'test1test2test3', groups: undefined] ['test3', 'e', 'st3', '3', index: 10, input: 'test1test2test3', groups: undefined] ]*/ ``` `ES2020`增加了`String.prototype.matchAll()`方法,可以一次性取出所有匹配。它返回的是一个遍历器`(Iterator)` ```js const string='test1test2test3'; const regex=/t(e)(st(\d?))/g; for(const match of string.matchAll(regex)){ console.log(match); } /* ['test1', 'e', 'st1', '1', index: 0, input: 'test1test2test3', groups: undefined] ['test2', 'e', 'st2', '2', index: 5, input: 'test1test2test3', groups: undefined] ['test3', 'e', 'st3', '3', index: 10, input: 'test1test2test3', groups: undefined] */ ``` 遍历器转化为数组,使用`...`运算符和`Array.from()`方法 ```js // 方法一 let result=[...string.matchAll(regex)]; // 方法二 let resultArray=Array.from(string.matchAll(regex)); /*[ ['test1', 'e', 'st1', '1', index: 0, input: 'test1test2test3', groups: undefined], ['test2', 'e', 'st2', '2', index: 5, input: 'test1test2test3', groups: undefined], ['test3', 'e', 'st3', '3', index: 10, input: 'test1test2test3', groups: undefined] ]*/ ``` ### 6.6 字符串的正则方法 字符串对象共有 4 个方法,可以使用正则表达式:`match()`、`replace()`、`search()`和`split()`。 ES6 将这 4 个方法,在语言内部全部调用`RegExp`的实例方法,从而做到所有与正则相关的方法,全都定义在`RegExp`对象上。 - `String.prototype.match` 调用 `RegExp.prototype[Symbol.match]` - `String.prototype.replace` 调用 `RegExp.prototype[Symbol.replace]` - `String.prototype.search` 调用 `RegExp.prototype[Symbol.search]` - `String.prototype.split` 调用 `RegExp.prototype[Symbol.split]` ### 6.7 $u$修饰符 ES6 对正则表达式添加了`u`修饰符,含义为“Unicode 模式”,用来正确处理大于`\uFFFF`的 Unicode 字符。也就是说,会正确处理四个字节的 UTF-16 编码。 ```js /^\uD83D/u.test('\uD83D\uDC2A') // false /^\uD83D/.test('\uD83D\uDC2A') // true ``` > - 点符号 > > 点(`.`)字符在正则表达式中,含义是除了换行符以外的任意单个字符。对于码点大于`0xFFFF`的Unicode字符,电子符不能识别,必须加上$u$修饰符。 > > ```js > var s = '𠮷'; > /^.$/.test(s) // false > /^.$/u.test(s) // true > ``` > > 上面代码表示,如果不添加`u`修饰符,正则表达式就会认为字符串为两个字符,从而匹配失败。 > > - Unicode 字符表示法 > > ES6 新增了使用大括号表示 Unicode 字符,这种表示法在正则表达式中必须加上`u`修饰符,才能识别当中的大括号,否则会被解读为量词。 > > ```js > /\u{61}/.test('a') // false > /\u{61}/u.test('a') // true > /\u{20BB7}/u.test('𠮷') // true > ``` > > - 量词 > > 使用`u`修饰符后,所有量词都会正确识别码点大于`0xFFFF`的 Unicode 字符。 > > ```js > /a{2}/.test('aa') // true > /a{2}/u.test('aa') // true > /𠮷{2}/.test('𠮷𠮷') // false > /𠮷{2}/u.test('𠮷𠮷') // true > ``` > > - 预定义模式 > > `u`修饰符也影响到预定义模式,能否正确识别码点大于`0xFFFF`的 Unicode 字符。 > > ```js > /^\S$/.test('𠮷') // false > /^\S$/u.test('𠮷') // true > ``` > > `\S`是预定义模式,匹配所有非空白字符。只有加了`u`修饰符,他才能正确✔匹配码点大于`0xFFFF`的Unicode字符。 > > ```js > function codePointLength(text) { > var result = text.match(/[\s\S]/gu); > return result ? result.length : 0; > } > var s = '𠮷𠮷'; > s.length // 4 > codePointLength(s) // 2 > ``` > > - i 修饰符 > > 有些 Unicode 字符的编码不同,但是字型很相近,比如,`\u004B`与`\u212A`都是大写的`K`。 > > ```js > /[a-z]/i.test('\u212A') // false > /[a-z]/iu.test('\u212A') // true > ``` > > - 转义 > > 没有`u`修饰符的情况下,正则中没有定义的转义(如逗号的转义`\,`)无效,而在`u`模式会报错。 > > ```js > /\,/ // /\,/ > /\,/u // 报错 > ``` > > 上面代码中,没有`u`修饰符时,逗号前面的反斜杠是无效的,加了`u`修饰符就报错。 ### 6.8 RegExp.prototype.unicode 属性 正则示例对象新增`unicode`属性,表示是否设置了`u`修饰符 ```js const r1=/hello/; const r2=/hello/u; r1.unicode //false r2.unicode //true ``` ### 6.9 y 修饰符 除了`u`修饰符,ES6 还为正则表达式添加了`y`修饰符,叫做“粘连”(sticky)修饰符。 `y`修饰符的作用与`g`修饰符类似,也是全局匹配,后一次匹配都从上一次匹配成功的下一个位置开始。不同之处在于,`g`修饰符只要剩余位置中存在匹配即可,而`y`修饰符确保匹配必须从剩余的第一个位置开始,这就是“粘连”的涵义。 ```js let s='aaa_aa_a'; let r1=/a+/g; let r2=/a+/y; r1.exec(s); //["aaa"] r2.exec(s); //["aaa"] r1.exec(s); //["aa"] r2.exec(s); //null ``` 上面代码有两个正则表达式,一个使用`g`修饰符,另一个使用`y`修饰符。这两个正则表达式各执行了两次,第一次执行的时候,两者行为相同,剩余字符串都是`_aa_a`。由于`g`修饰没有位置要求,所以第二次执行会返回结果,而`y`修饰符要求匹配必须从头部开始,所以返回`null`。 修改正则表达式,保证每次都能头部匹配,`y`修饰符就会返回结果 ```js let s='aaa_aa_a'; let r=/a+_/y; r.exec(s);//["aaa_"] r.exec(s);//["aa_"] ``` 使用`lastIndex`属性,可以更好地说明`y`修饰符。 ```js const REGEX = /a/g; // 指定从2号位置(y)开始匹配 REGEX.lastIndex = 2; // 匹配成功 const match = REGEX.exec('xaya'); // 在3号位置匹配成功 match.index // 3 // 下一次匹配从4号位开始 REGEX.lastIndex // 4 // 4号位开始匹配失败 REGEX.exec('xaya') // null ``` `lastIndex`属性指定每次搜索的开始位置,`g`修饰符从这个位置开始向后搜索,直到发现匹配为止。`y`修饰符同样遵守`lastIndex`属性,但是要求必须在`lastIndex`指定的位置发现匹配。 ```js const REGEX = /a/y; // 指定从2号位置开始匹配 REGEX.lastIndex = 2; // 不是粘连,匹配失败 REGEX.exec('xaya') // null // 指定从3号位置开始匹配 REGEX.lastIndex = 3; // 3号位置是粘连,匹配成功 const match = REGEX.exec('xaya'); match.index // 3 REGEX.lastIndex // 4 ``` `y`修饰符隐含了头部匹配的标志`^`,下面代码由于不能保证头部匹配,所以返回`null`。 ```js /b/y.exec('abc'); //null ``` 字符串对象的`replace`方法的例子。 ```js const REGEX = /a/gy;'aaxa'.replace(REGEX, '-') // '--xa' ``` 上面代码中,最后一个`a`因为不是出现在下一次匹配的头部,所以不会被替换。 单单一个`y`修饰符对`match`方法,只能返回第一个匹配,必须与`g`修饰符联用,才能返回所有匹配。 ```js 'a1a2a3'.match(/a\d/y) // ["a1"] 'a1a2a3'.match(/a\d/gy) // ["a1", "a2", "a3"] ``` `y`修饰符的一个应用,是从字符串提取 token(词元),`y`修饰符确保了匹配之间不会有漏掉的字符 ```js const TOKEN_Y = /\s*(\+|[0-9]+)\s*/y; const TOKEN_G = /\s*(\+|[0-9]+)\s*/g; function tokenize(TOKEN_REGEX, str) { let result = []; let match; while (match = TOKEN_REGEX.exec(str)) { result.push(match[1]); } return result; } tokenize(TOKEN_Y, '3 + 4') //['3', '+', '4'] tokenize(TOKEN_G, '3 + 4') //['3', '+', '4'] ``` 上面代码中,如果字符串里面没有非法字符,`y`修饰符与`g`修饰符的提取结果是一样的。但是,一旦出现非法字符,两者的行为就不一样了。 ```js tokenize(TOKEN_Y, '3x + 4') // [ '3' ] tokenize(TOKEN_G, '3x + 4') // [ '3', '+', '4' ] ``` ### 6.10 RegExp.protype.sticky属性 与`y`修饰符相匹配,ES6 的正则实例对象多了`sticky`属性,表示是否设置了`y`修饰符。 ```js let r=/hello\d/y; r.sticky //true ``` ### 6.11 RegExp.prototype.flags属性 `ES6`为正则表达式新增了`flags`属性,会返回正则表达式的修饰符。 ``` // ES5 的 source 属性 // 返回正则表达式的正文 /abc/ig.source // "abc" // ES6 的 flags 属性 // 返回正则表达式的修饰符 /abc/ig.flags // 'gi' ``` ### 6.12 s修饰符:dotAll 模式 正则表达式中,点(`.`)是一个特殊字符,代表任意的单个字符,但是有两个例外。一个是四个字节的 UTF-16 字符,这个可以用`u`修饰符解决;另一个是行终止符(line terminator character)。 所谓行终止符,就是该字符表示一行的终结。以下四个字符属于“行终止符”。 - U+000A 换行符(`\n`) - U+000D 回车符(`\r`) - U+2028 行分隔符(line separator) - U+2029 段分隔符(paragraph separator) ```js /foo.bar/.test('foo\nbar'); // false ``` 上面代码中,因为`.`不匹配`\n`,所以正则表达式返回`false`。 但是,很多时候我们希望匹配的是任意单个字符,这时有一种变通的写法。 ```js /foo[^]bar/.test('foo\nbar'); // true ``` ES2018 [引入](https://github.com/tc39/proposal-regexp-dotall-flag)`s`修饰符,使得`.`可以匹配任意单个字符。 ```js /foo.bar/s.test('foo\nbar'); //true ``` 这被称为`dotAll`模式,即点(dot)代表一切字符。所以,正则表达式还引入了一个`dotAll`属性,返回一个布尔值,表示该正则表达式是否处在`dotAll`模式。 ```js const re = /foo.bar/s; // 另一种写法 // const re = new RegExp('foo.bar', 's'); re.test('foo\nbar') // true re.dotAll // true re.flags // 's' ``` ### 6.13 后行断言 JavaScript 语言的正则表达式,只支持先行断言(lookahead)和先行否定断言(negative lookahead),不支持后行断言(lookbehind)和后行否定断言(negative lookbehind)。 “先行断言”指的是,`x`只有在`y`前面才匹配,必须写成`/x(?=y)/`。如,只匹配百分号之前的数字,只写成`/\d+(?=%)/`。“先行否定断言”指的是,`x`只有不在`y`前面才匹配,必须写成`/x(?!y)/`。比如,只匹配不在百分号之前的数字,要写成`/\d+(?!%)/`。 ```js /\d+(?=%)/.exec('100% of US presidents have been male') // ["100"] /\d+(?!%)/.exec('that’s all 44 of them') // ["44"] ``` 上面两个字符串,如果互换正则表达式,就不会得到相同结果。另外,还可以看到,“先行断言”括号之中的部分(`(?=%)`),是不计入返回结果的。 “后行断言”正好与“先行断言”相反,`x`只有在`y`后面才匹配,必须写成`/(?<=y)x/`。比如,只匹配美元符号之后的数字,要写成`/(?<=\$)\d+/`。“后行否定断言”则与“先行否定断言”相反,`x`只有不在`y`后面才匹配,必须写成`/(? 注意,如果参数类型不是数值,`Number.isFinite`一律返回`false` `Number.isNaN()`用来检查一个值是否为`NaN`。 ```js Number.isNaN(NaN) // true Number.isNaN(15) // false Number.isNaN('15') // false Number.isNaN(true) // false Number.isNaN(9/NaN) // true Number.isNaN('true' / 0) // true Number.isNaN('true' / 'true') // true ``` > 如果参数类型不是`NaN`,`Number.isNaN`一律返回`false`。 它们与传统的全局方法`isFinite()`和`isNaN()`的区别在于,传统方法先调用`Number()`将非数值的值转为数值,再进行判断,而这两个新方法只对数值有效,`Number.isFinite()`对于非数值一律返回`false`, `Number.isNaN()`只有对于`NaN`才返回`true`,非`NaN`一律返回`false`。 ```js isFinite(25) // true isFinite("25") // true Number.isFinite(25) // true Number.isFinite("25") // false isNaN(NaN) // true isNaN("NaN") // true Number.isNaN(NaN) // true Number.isNaN("NaN") // false Number.isNaN(1) // false ``` ### 7.3 Number.parseInt(), Number.parseFloat() ES6 将全局方法`parseInt()`和`parseFloat()`,移植到`Number`对象上面,行为完全保持不变。 ```js // ES5的写法 parseInt('12.34') // 12 parseFloat('123.45#') // 123.45 // ES6的写法 Number.parseInt('12.34') // 12 Number.parseFloat('123.45#') // 123.45 ``` ### 7.4 Number.isInteger() `Number.isInteger()`用来判断一个数值是否为整数 ```js Number.isInteger(25)//true Number.isInteger(52.1)//false ``` JavaScript 内部,整数和浮点数采用的是同样的储存方法,所以 25 和 25.0 被视为同一个值 ```js Number.isInteger(25) // true Number.isInteger(25.0) // true ``` 如果参数不是数值,`Number.isInteger`返回`false` ```js Number.isInteger() // false Number.isInteger(null) // false Number.isInteger('15') // false Number.isInteger(true) // false ``` > 注意,由于 JavaScript 采用 IEEE 754 标准,数值存储为64位双精度格式,数值精度最多可以达到 53 个二进制位(1 个隐藏位与 52 个有效位)。如果数值的精度超过这个限度,第54位及后面的位就会被丢弃,这种情况下,`Number.isInteger`可能会误判。 > > ```js > Number.isInteger(3.0000000000000002) // true > ``` > > 这个小数的精度达到了小数点后16个十进制位,转成二进制位超过了53个二进制位,导致最后的那个`2`被丢弃了。 > > 类似的情况还有,如果一个数值的绝对值小于`Number.MIN_VALUE`(5E-324),即小于 JavaScript 能够分辨的最小值,会被自动转为 0。这时,`Number.isInteger`也会误判。 > > ```js > Number.isInteger(5E-324) // false > Number.isInteger(5E-325) // true > ``` > > 上面代码中,`5E-325`由于值太小,会被自动转为0,因此返回`true`。 ### 7.5 Number.EPSILON ES6在Number对象上新增一个极小的常量`Number.EPSILON` $\epsilon$。它表示1与大于1的最小浮点数之前的差 对于 64 位浮点数来说,大于 1 的最小浮点数相当于二进制的`1.00..001`,小数点后面有连续 51 个零。这个值减去 1 之后,就等于 2 的 -52 次方。 ```js Number.EPSILON === Math.pow(2, -52) // true Number.EPSILON // 2.220446049250313e-16 Number.EPSILON.toFixed(20) // "0.00000000000000022204" ``` `Number.EPSILON`实际上是 JavaScript 能够表示的最小精度。误差如果小于这个值,就可以认为已经没有意义了,即不存在误差了。 引入一个这么小的量的目的,在于为浮点数计算,设置一个误差范围。我们知道浮点数计算是不精确的。 ```js 0.1 + 0.2 // 0.30000000000000004 0.1 + 0.2 - 0.3 // 5.551115123125783e-17 5.551115123125783e-17.toFixed(20) // '0.00000000000000005551' ``` `Number.EPSILON`可以用来设置“能够接受的误差范围”.如。设置范围为2的-50此次方`Number.EPSILION*Math.pow(2,2)`,即如果两个浮点数的差效于这个值,那么我们就认为这两个浮点数相同。 ```js 5.551115123125783e-17 numbers.sort(); ``` `arguments`对象不是数组,而是一个类似数组的对象。所以为了使用数组的方法,必须使用`Array.prototype.slice.call`先将其转为数组。rest 参数就不存在这个问题,它就是一个真正的数组,数组特有的方法都可以使用。下面是一个利用 rest 参数改写数组`push`方法的例子。 ```js function push(array, ...items) { items.forEach(function(item) { array.push(item); console.log(item); }); } var a = []; push(a, 1, 2, 3) ``` > 注意,rest 参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。 > > ```js > // 报错 > function f(a, ...b, c) { > // ... > } > ``` > > 函数的`length`属性,不包括 rest 参数 > > ```js > (function(a) {}).length // 1 > (function(...a) {}).length // 0 > (function(a, ...b) {}).length // 1 > ``` ### 8.3 严格模式 从 ES5 开始,函数内部可以设定为严格模式;ES2016 做了一点修改,规定只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错。 ```js // 报错 function doSomething(a, b = a) { 'use strict'; // code } // 报错 const doSomething = function ({a, b}) { 'use strict'; // code }; // 报错 const doSomething = (...a) => { 'use strict'; // code }; const obj = { // 报错 doSomething({a, b}) { 'use strict'; // code } }; ``` 这样规定的原因是,函数内部的严格模式,同时适用于函数体和函数参数。但是,函数执行的时候,先执行函数参数,然后再执行函数体。这样就有一个不合理的地方,只有从函数体之中,才能知道参数是否应该以严格模式执行,但是参数却应该先于函数体执行。 ```js // 报错 function doSomething(value = 070) { 'use strict'; return value; } ``` > 第一种是设定全局性的严格模式 > > ```js > 'use strict'; > function doSomething(a, b = a) { > // code > } > ``` > > 第二种是把函数包在一个无参数的立即执行函数里面 > > ```js > const doSomething = (function () { > 'use strict'; > return function(value = 42) { > return value; > }; > }()); > ``` ### 8.4 name属性 函数的`name`属性,返回该函数的函数名 ```js function foo(){} foo.name//"foo" ``` 如果将一个具名函数赋值给一个变量,则 ES5 和 ES6 的`name`属性都返回这个具名函数原本的名字。 ```js const bar = function baz() {}; // ES5 bar.name // "baz" // ES6 bar.name // "baz" ``` `Function`构造函数返回的函数实例,`name`属性的值为`anonymous`。 ```js (new Function).name // "anonymous" ``` `bind`返回的函数,`name`属性值会加上`bound`前缀。 ```js function foo() {}; foo.bind({}).name // "bound foo" (function(){}).bind({}).name // "bound " ``` ### 8.5 箭头函数 ES6 允许使用“箭头”(`=>`)定义函数。 ```js var f = v => v; // 等同于 var f = function (v) { return v; }; ``` 如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。 ```js var f = () => 5; // 等同于 var f = function () { return 5 }; var sum = (num1, num2) => num1 + num2; // 等同于 var sum = function(num1, num2) { return num1 + num2; }; ``` 如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用`return`语句返回。 ```js var sum = (num1, num2) => { return num1 + num2; } ``` 由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错。 ```js // 报错 let getTempItem = id => { id: id, name: "Temp" }; // 不报错 let getTempItem = id => ({ id: id, name: "Temp" }); ``` 如果箭头函数只有一行语句,且不需要返回值,可以采用下面的写法,就不用写大括号了. ```js let fn = () => void doesNotReturn(); ``` 箭头函数的一个用处是简化回调函数 ```js // 正常函数写法 [1,2,3].map(function (x) { return x * x; }); // 箭头函数写法 [1,2,3].map(x => x * x); ``` ```js // 正常函数写法 var result = values.sort(function (a, b) { return a - b; }); // 箭头函数写法 var result = values.sort((a, b) => a - b); ``` 下面是 rest 参数与箭头函数结合的例子 ```js const numbers = (...nums) => nums; numbers(1, 2, 3, 4, 5) // [1,2,3,4,5] const headAndTail = (head, ...tail) => [head, tail]; headAndTail(1, 2, 3, 4, 5) // [1,[2,3,4,5]] ``` **使用注意点** 箭头函数有几个使用注意点。 (1)函数体内的`this`对象,就是定义时所在的对象,而不是使用时所在的对象。 (2)不可以当作构造函数,也就是说,不可以使用`new`命令,否则会抛出一个错误。 (3)不可以使用`arguments`对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。 (4)不可以使用`yield`命令,因此箭头函数不能用作 Generator 函数。 `this`对象的指向是可变的,但是在箭头函数中,它是固定的 ```js function foo() { setTimeout(() => { console.log('id:', this.id); }, 100); } var id = 21; foo.call({ id: 42 }); // id: 42 ``` 上面代码中,`setTimeout`的参数是一个箭头函数,这个箭头函数的定义生效是在`foo`函数生成时,而它的真正执行要等到 100 毫秒后。如果是普通函数,执行时`this`应该指向全局对象`window`,这时应该输出`21`。但是,箭头函数导致`this`总是指向函数定义生效时所在的对象(本例是`{id: 42}`),所以输出的是`42`。 箭头函数可以让`setTimeout`里面的`this`,绑定定义时所在的作用域,而不是指向运行时所在的作用域。下面是另一个例子. ```js function Timer() { this.s1 = 0; this.s2 = 0; // 箭头函数 setInterval(() => this.s1++, 1000); // 普通函数 setInterval(function () { this.s2++; }, 1000); } var timer = new Timer(); setTimeout(() => console.log('s1: ', timer.s1), 3100); setTimeout(() => console.log('s2: ', timer.s2), 3100); // s1: 3 // s2: 0 ``` 上面代码中,`Timer`函数内部设置了两个定时器,分别使用了箭头函数和普通函数。前者的`this`绑定定义时所在的作用域(即`Timer`函数),后者的`this`指向运行时所在的作用域(即全局对象)。所以,3100 毫秒之后,`timer.s1`被更新了 3 次,而`timer.s2`一次都没更新。 箭头函数可以让`this`指向固定化,这种特性很有利于封装回调函数。下面是一个例子,DOM 事件的回调函数封装在一个对象里面。 ```js var handler = { id: '123456', init: function() { document.addEventListener('click', event => this.doSomething(event.type), false); }, doSomething: function(type) { console.log('Handling ' + type + ' for ' + this.id); } }; ``` 上面代码的`init`方法中,使用了箭头函数,这导致这个箭头函数里面的`this`,总是指向`handler`对象。否则,回调函数运行时,`this.doSomething`这一行会报错,因为此时`this`指向`document`对象。 `this`指向的固定化,并不是因为箭头函数内部有绑定`this`的机制,实际原因是箭头函数根本没有自己的`this`,导致内部的`this`就是外层代码块的`this`。正是因为它没有`this`,所以也就不能用作构造函数。 箭头函数还有一个功能,就是可以很方便地改写 λ 演算。 ```js // λ演算的写法 fix = λf.(λx.f(λv.x(x)(v)))(λx.f(λv.x(x)(v))) // ES6的写法 var fix = f => (x => f(v => x(x)(v))) (x => f(v => x(x)(v))); ``` **箭头函数中不存在的变量** `this` `arguments` `super` `new.target` ```js function foo(){ setTimeout(()=>{ console.log("args",arguments) },100); } foo(2,4,5,6) //args Arguments(4) [2, 4, 5, 6, callee: ƒ, Symbol(Symbol.iterator): ƒ] ``` 上面代码中,箭头函数内部的变量`arguments`,其实是函数`foo`的`arguments`变量。 另外,由于箭头函数没有自己的`this`,所以当然也就不能用`call()`、`apply()`、`bind()`这些方法去改变`this`的指向。 ```js (function() { return [ (() => this.x).bind({ x: 'inner' })() ]; }).call({ x: 'outer' }); // ['outer'] ``` 上面代码中,箭头函数没有自己的`this`,所以`bind`方法无效,内部的`this`指向外部的`this`。 **不适合的场景** 由于箭头函数使得`this`从“动态”变成“静态”,下面两个场合不应该使用箭头函数。 第一个场合是定义对象的方法,且该方法内部包括`this` ```js const cat = { lives: 9, jumps: () => { this.lives--; } } ``` 第二个场合是需要动态`this`的时候,也不应使用箭头函数。 ```js var button = document.getElementById('press'); button.addEventListener('click', () => { this.classList.toggle('on'); }); ``` ### 8.6 尾调用优化 #### 8.6.1 什么是尾调用? 尾调用(Tail Call)是函数式编程的一个重要概念,本身非常简单,一句话就能说清楚,就是指某个函数的最后一步是调用另一个函数。 ```js function f(x){ return g(x); } //以下不属于尾调用 // 情况一 function f(x){ let y = g(x); return y; } // 情况二 function f(x){ return g(x) + 1; } // 情况三 function f(x){ g(x); return undefined; } //尾调用不一定出现在函数尾部,只要是最后一步操作即可 function f(x) { if (x > 0) { return m(x) } return n(x); } ``` #### 8.6.2 尾调用优化 尾调用之所以与其他调用不同,就在于它的特殊的调用位置。 函数调用会在内存形成一个“调用记录”,又称“调用帧”(call frame),保存调用位置和内部变量等信息。如果在函数`A`的内部调用函数`B`,那么在`A`的调用帧上方,还会形成一个`B`的调用帧。等到`B`运行结束,将结果返回到`A`,`B`的调用帧才会消失。如果函数`B`内部还调用函数`C`,那就还有一个`C`的调用帧,以此类推。所有的调用帧,就形成一个“调用栈”(call stack)。 尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了。 ```js function f() { let m = 1; let n = 2; return g(m + n); } f(); // 等同于 function f() { return g(3); } f(); // 等同于 g(3); ``` 上面代码中,如果函数`g`不是尾调用,函数`f`就需要保存内部变量`m`和`n`的值、`g`的调用位置等信息。但由于调用`g`之后,函数`f`就结束了,所以执行到最后一步,完全可以删除`f(x)`的调用帧,只保留`g(3)`的调用帧。 这就叫做“尾调用优化”(Tail call optimization),即只保留内层函数的调用帧。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用帧只有一项,这将大大节省内存。这就是“尾调用优化”的意义。 只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧,否则就无法进行“尾调用优化”。 ```js function addOne(a){ var one = 1; function inner(b){ return b + one; } return inner(a); } ``` 上面的函数不会进行尾调用优化,因为内层函数`inner`用到了外层函数`addOne`的内部变量`one`。 > 目前只有 Safari 浏览器支持尾调用优化,Chrome 和 Firefox 都不支持。 #### 8.6.3 尾递归 Recursion 函数调用自身,称为递归。如果尾调用自身,就称为尾递归。 递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)。但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。以下时间复杂度 $O(n)$ ```js function factorial(n){ if(n===1) return 1; return n*factorial(n-1); } factorial(5) //120 function factorial(n, total) { if (n === 1) return total; return factorial(n - 1, n * total); } factorial(5, 1) // 120 ``` 斐波那契数列 ```js function Fibonacci (n) { if ( n <= 1 ) {return 1}; return Fibonacci(n - 1) + Fibonacci(n - 2); } function Fibonacci2 (n , ac1 = 1 , ac2 = 1) { if( n <= 1 ) {return ac2}; return Fibonacci2 (n - 1, ac2, ac1 + ac2); } ``` #### 8.6.4 递归函数的改写 尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身。做到这一点的方法,就是把所有用到的内部变量改写成函数的参数。比如上面的例子,阶乘函数 factorial 需要用到一个中间变量`total`,那就把这个中间变量改写成函数的参数。这样做的缺点就是不太直观,第一眼很难看出来,为什么计算`5`的阶乘,需要传入两个参数`5`和`1`。 ```js function tailFactorial(n, total) { if (n === 1) return total; return tailFactorial(n - 1, n * total); } function factorial(n) { return tailFactorial(n, 1); } factorial(5) // 120 ``` 函数式编程有一个概念,叫做柯里化(currying),意思是将多参数的函数转换成单参数的形式。这里也可以使用柯里化。 ```js function currying(fn, n) { return function (m) { return fn.call(this, m, n); }; } function tailFactorial(n, total) { if (n === 1) return total; return tailFactorial(n - 1, n * total); } const factorial = currying(tailFactorial, 1); factorial(5) // 120 ``` #### 8.6.5 严格模式 ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。 - `func.arguments`:返回调用时函数的参数。 - `func.caller`:返回调用当前函数的那个函数。 ```js function restricted() { 'use strict'; restricted.caller; // 报错 restricted.arguments; // 报错 } restricted(); ``` #### 8.6.6 尾递归优化的实现 尾递归优化只在严格模式下生效,那么正常模式下,或者那些不支持该功能的环境中,有没有办法也使用尾递归优化呢?回答是可以的,就是自己实现尾递归优化。 ```js //蹦床函数(trampoline)可以将递归转化为循环执行 function trampoline(f){ while(f&&f instanceof Function){ f=f(); } return f; } function sum(x,y){ if(y>0) return sum.bind(null,x+1,y-1); else return x; } result=trampoline(sum(1,100000)); console.log(result); ``` **尾递归优化** ```js //尾递归优化 function tco(f){ let value, active=false, accumulated=[]; return function accumulator(){ accumulated.push(arguments); if(!active){ active=true; while(accumulated.length){ value=f.apply(this,accumulated.shift()); } active=factorial; return value; } } } let sumRecursion=tco(function(x,y){ if(y>0){ return sumRecursion(x+1,y-1); } return x; }) result=sumRecursion(1,100000); console.log(result); ``` #### 8.7 Function.prototype.toString() [ES2019](https://github.com/tc39/Function-prototype-toString-revision) 对函数实例的`toString()`方法做出了修改。 `toString()`方法返回函数代码本身,以前会省略注释和空格。 ```js function /* foo comment */ foo () {} foo.toString() // function foo() {} ``` 上面代码中,函数`foo`的原始代码包含注释,函数名`foo`和圆括号之间有空格,但是`toString()`方法都把它们省略了。 修改后的`toString()`方法,明确要求返回一模一样的原始代码。 ```js function /* foo comment */ foo () {} foo.toString() // "function /* foo comment */ foo () {}" ``` ## 9. 数组的扩展 ### 9.1 扩展运算符 **含义** 扩展运算符(spread)是三个点(`...`)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列。 ```js console.log(...[1, 2, 3]) // 1 2 3 console.log(1, ...[2, 3, 4], 5) // 1 2 3 4 5 [...document.querySelectorAll('div')] // [
,
,
] function push(array, ...items) { array.push(...items); } function add(x, y) { return x + y; } const numbers = [4, 38]; add(...numbers) // 42 ``` 扩展运算符后面还可以放置表达式 ```js const arr = [ ...(x > 0 ? ['a'] : []), 'b', ]; [...[], 1] // [1] //只有函数调用时,扩展运算符才可以放在圆括号中,否则会报错 (...[1, 2]) // Uncaught SyntaxError: Unexpected number console.log((...[1, 2])) // Uncaught SyntaxError: Unexpected number console.log(...[1, 2]) // 1 2 ``` #### 9.1.1 代替函数的apply方法 由于扩展运算符可以展开数组,所以不再需要`apply`方法,将数组转为函数的参数了 ```js // ES5 的写法 function f(x, y, z) { // ... } var args = [0, 1, 2]; f.apply(null, args); // ES6的写法 function f(x, y, z) { // ... } let args = [0, 1, 2]; f(...args); ``` 应用`Math.max` 方法,简化求出数组的最大元素的写法 ```js // ES5 的写法 Math.max.apply(null, [14, 3, 77]) // ES6 的写法 Math.max(...[14, 3, 77]) // 等同于 Math.max(14, 3, 77); ``` `push`函数,将一个数组添加到另一个数组的尾部 ```js // ES5的 写法 var arr1 = [0, 1, 2]; var arr2 = [3, 4, 5]; Array.prototype.push.apply(arr1, arr2); // ES6 的写法 let arr1 = [0, 1, 2]; let arr2 = [3, 4, 5]; arr1.push(...arr2); new (Date.bind.apply(Date,[null,2015,1,1])) //Sun Feb 01 2015 00:00:00 GMT+0800 (中国标准时间) new Date(...[2015,1,1]) //Sun Feb 01 2015 00:00:00 GMT+0800 (中国标准时间) ``` #### 9.1.2 扩展运算符的应用 **(1)赋值数组** 数组是复合的数据类型,直接复制的话,只是复制了指向底层数据结构的指针,而不是克隆一个全新的数组。 ```js const a1 = [1, 2]; const a2 = a1; a2[0] = 2; a1 // [2, 2] ``` 上面代码中,`a2`并不是`a1`的克隆,而是指向同一份数据的另一个指针。修改`a2`,会直接导致`a1`的变化。 ES5 只能用变通方法来复制数组。 ```js const a1 = [1, 2]; const a2 = a1.concat(); a2[0] = 2; a1 // [1, 2] ``` 扩展运算符提供了复制数组的简便写法 ```js const a1 = [1, 2]; // 写法一 const a2 = [...a1]; // 写法二 const [...a2] = a1; ``` **(2)合并数组** 扩展运算符提供了数组合并的新写法 ```js const arr1 = ['a', 'b']; const arr2 = ['c']; const arr3 = ['d', 'e']; // ES5 的合并数组 arr1.concat(arr2, arr3); // [ 'a', 'b', 'c', 'd', 'e' ] // ES6 的合并数组 [...arr1, ...arr2, ...arr3] // [ 'a', 'b', 'c', 'd', 'e' ] ``` 不过,这两种方法都是浅拷贝 ```js const a1 = [{ foo: 1 }]; const a2 = [{ bar: 2 }]; const a3 = a1.concat(a2); const a4 = [...a1, ...a2]; a3[0] === a1[0] // true a4[0] === a1[0] // true ``` **(3)与结构赋值结合** 扩展运算符可以与解构赋值结合起来,用于生成数组 ```js // ES5 a = list[0], rest = list.slice(1) // ES6 [a, ...rest] = list const [first, ...rest] = [1, 2, 3, 4, 5]; first // 1 rest // [2, 3, 4, 5] const [first, ...rest] = []; first // undefined rest // [] const [first, ...rest] = ["foo"]; first // "foo" rest // [] ``` **(4)**字符串 扩展运算符还可以将字符串转为真正的数组 ```js [...'hello'] // [ "h", "e", "l", "l", "o" ] //采用扩展运算符返回字符串长度的函数 function length(str) { return [...str].length; } length('x\uD83D\uDE80y') // 3 let str = 'x\uD83D\uDE80y'; str.split('').reverse().join('') // 'y\uDE80\uD83Dx' [...str].reverse().join('') // 'y\uD83D\uDE80x' ``` **(5)**实现Iterator接口的对象 任何定义了遍历器(Iterator)接口的对象(参阅 Iterator 一章),都可以用扩展运算符转为真正的数组 ```js let nodeList = document.querySelectorAll('div'); let array = [...nodeList]; Number.prototype[Symbol.iterator] = function*() { let i = 0; let num = this.valueOf(); while (i < num) { yield i++; } } console.log([...5]) // [0, 1, 2, 3, 4] ``` **(6)**Map 和 Set 结构,Generator 函数 扩展运算符内部调用的是数据结构的 Iterator 接口,因此只要具有 Iterator 接口的对象,都可以使用扩展运算符,比如 Map 结构。 ```js let map = new Map([ [1, 'one'], [2, 'two'], [3, 'three'], ]); let arr = [...map.keys()]; // [1, 2, 3] const go = function*(){ yield 1; yield 2; yield 3; }; [...go()] // [1, 2, 3] ``` ### 9.2 数组的空位 数组的空位指,数组的某一个位置没有任何值。`Array`构造函数返回的数组都是空位 ```js Array(3) //[,,,] ``` 空位不是`undefined`,一个位置的值等于`undefined`,依然是有值的。空位是没有任何值,`in`运算符可以说明这一点。 ```js 0 in [undefined, undefined, undefined] // true 0 in [, , ,] // false ``` ES5 对空位的处理,已经很不一致了,大多数情况下会忽略空位。 - `forEach()`, `filter()`, `reduce()`, `every()` 和`some()`都会跳过空位。 - `map()`会跳过空位,但会保留这个值 - `join()`和`toString()`会将空位视为`undefined`,而`undefined`和`null`会被处理成空字符串。 ```js // forEach方法 [,'a'].forEach((x,i) => console.log(i)); // 1 // filter方法 ['a',,'b'].filter(x => true) // ['a','b'] // every方法 [,'a'].every(x => x==='a') // true // reduce方法 [1,,2].reduce((x,y) => x+y) // 3 // some方法 [,'a'].some(x => x !== 'a') // false // map方法 [,'a'].map(x => 1) // [,1] // join方法 [,'a',undefined,null].join('#') // "#a##" // toString方法 [,'a',undefined,null].toString() // ",a,," ``` ES6 则是明确将空位转为`undefined`。 `Array.from`方法会将数组的空位,转为`undefined`,也就是说,这个方法不会忽略空位。 ```js Array.from(['a',,'b']) // [ "a", undefined, "b" ] //扩展运算符(...)也会将空位转为undefined [...['a',,'b']] // [ "a", undefined, "b" ] //copyWithin()会连空位一起拷贝 [,'a','b',,].copyWithin(2,0) //fill()会将空位视为正常的数组位置 new Array(3).fill('a') // ["a","a","a"] //for...of循环也会遍历空位 let arr = [, ,]; for (let i of arr) { console.log(1); } //entries()、keys()、values()、find()和findIndex()会将空位处理成undefined // entries() [...[,'a'].entries()] // [[0,undefined], [1,"a"]] // keys() [...[,'a'].keys()] // [0,1] // values() [...[,'a'].values()] // [undefined,"a"] // find() [,'a'].find(x => true) // undefined // findIndex() [,'a'].findIndex(x => true) // 0 ``` ### 9.3 Array.prototype.sort() 的排序稳定性 排序稳定性(stable sorting)是排序算法的重要属性,指的是排序关键字相同的项目,排序前后的顺序不变 ```js const arr = [ 'peach', 'straw', 'apple', 'spork' ]; const stableSorting=(s1,s2)=>{ if(s1[0]{ if(s1[0]<=s2[0]) return -1; return 1; } arr.sort(unstableSorting); //['apple', 'peach', 'spork', 'straw'] ``` 上面代码中,排序结果是`spork`在`straw`前面,跟原始顺序相反,所以排序算法`unstableSorting`是不稳定的。 ### 9.4 Array.from() `Array.from`方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map) 类似数组的对象,`Array.from`将它转化为真正的数组 ```js let arrayLike = { '0': 'a', '1': 'b', '2': 'c', length: 3 }; let arr1=[].slice.call(arrayLike); //['a', 'b', 'c'] let arr2=Array.from(arrayLike); //['a', 'b', 'c'] ``` 类似数组的对象是 DOM 操作返回的 NodeList 集合,以及函数内部的`arguments`对象。`Array.from`都可以将它们转为真正的数组 ```js // NodeList对象 let ps = document.querySelectorAll('p'); Array.from(ps).filter(p => { return p.textContent.length > 100; }); // arguments对象 function foo() { var args = Array.from(arguments); // ... } ``` 继承了`Iterator`接口的数据结构,`Array.from`都可以将其转化为数组 ```js Array.from('hello') // ['h', 'e', 'l', 'l', 'o'] let namesSet = new Set(['a', 'b']) Array.from(namesSet) // ['a', 'b'] ``` 字符串和 Set 结构都具有 Iterator 接口,因此可以被`Array.from`转为真正的数组。 扩展运算符背后调用的是遍历器接口(`Symbol.iterator`),如果一个对象没有部署这个接口,就无法转化。 对于还没有部署该方法的浏览器,可以用`Array.prototype.slice`方法替代 ```js const toArray = (() => Array.from ? Array.from : obj => [].slice.call(obj) )(); ``` `Array.from`还可以接受第二个参数,作用类似于数组的`map`方法,用来对每个元素进行处理,将处理后的值放入返回的数组 ```js Array.from(arrayLike, x => x * x); // 等同于 Array.from(arrayLike).map(x => x * x); Array.from([1, 2, 3], (x) => x * x) // [1, 4, 9] ``` 返回各种数据的类型 ```js function typesOf(...args){ return Array.from(args,value=>typeof value) } function typesOf () { return Array.from(arguments, value => typeof value) } typesOf(null,[],NaN) // ['object', 'object', 'number'] ``` ### 9.5 Array.of() `Array.of`方法用于将一组值,转换为数组 ```js Array.of(3, 11, 8) // [3,11,8] Array.of(3) // [3] Array.of(3).length // 1 ``` 这个方法的主要目的,是弥补数组构造函数`Array()`的不足。因为参数个数的不同,会导致`Array()`的行为有差异 ```js Array() // [] Array(3) // [, , ,] Array(3, 11, 8) // [3, 11, 8] ``` `Array`方法没有参数、一个参数、三个参数时,返回结果都不一样。只有当参数个数不少于 2 个时,`Array()`才会返回由参数组成的新数组。参数个数只有一个时,实际上是指定数组的长度。 `Array.of`基本上可以用来替代`Array()`或`new Array()`,并且不存在由于参数不同而导致的重载。它的行为非常统一。 ```js Array.of() // [] Array.of(undefined) // [undefined] Array.of(1) // [1] Array.of(1, 2) // [1, 2] ``` `Array.of`总是返回参数值组成的数组。如果没有参数,就返回一个空数组。 `Array.of`方法可以用下面的代码模拟实现。 ```js function ArrayOf(){ return [].slice.call(arguments); } ``` ### 9.6 数组实例的 copyWithin() 数组实例的`copyWithin()`方法,在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。也就是说,使用这个方法,会修改当前数组。 ```js Array.prototype.copyWithin(target, start = 0, end = this.length) ``` - target(必需):从该位置开始替换数据。如果为负值,表示倒数。 - start(可选):从该位置开始读取数据,默认为 0。如果为负值,表示从末尾开始计算。 - end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示从末尾开始计算。 ```js [1,2,3,4,5].copyWithin(0,3) //[4, 5, 3, 4, 5] //上面代码表示将从 3 号位直到数组结束的成员(4 和 5),复制到从 0 号位开始的位置,结果覆盖了原来的 1 和 2 // 将3号位复制到0号位 [1, 2, 3, 4, 5].copyWithin(0, 3, 4) // [4, 2, 3, 4, 5] // -2相当于3号位,-1相当于4号位 [1, 2, 3, 4, 5].copyWithin(0, -2, -1) // [4, 2, 3, 4, 5] // 将3号位复制到0号位 [].copyWithin.call({length: 5, 3: 1}, 0, 3) // {0: 1, 3: 1, length: 5} // 将2号位到数组结束,复制到0号位 let i32a = new Int32Array([1, 2, 3, 4, 5]); i32a.copyWithin(0, 2); // Int32Array [3, 4, 5, 4, 5] // 对于没有部署 TypedArray 的 copyWithin 方法的平台 // 需要采用下面的写法 [].copyWithin.call(new Int32Array([1, 2, 3, 4, 5]), 0, 3, 4); // Int32Array [4, 2, 3, 4, 5] ``` ### 9.7 数组实例的find()和findIndex() 数组实例的`find`方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为`true`的成员,然后返回该成员。如果没有符合条件的成员,则返回`undefined` ```js [1, 4, -5, 10].find((n) => n < 0) // -5 [1, 5, 10, 15].find(function(value, index, arr) { return value > 9; }) // 10 ``` 数组实例的`findIndex`方法的用法与`find`方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回`-1`。 ```js [1, 5, 10, 15].findIndex(function(value, index, arr) { return value > 9; }) // 2 function f(v){ return v > this.age; } let person = {name: 'John', age: 20}; [10, 12, 26, 15].find(f, person); // 26 ``` 上面的代码中,`find`函数接收了第二个参数`person`对象,回调函数中的`this`对象指向`person`对象。 另外,这两个方法都可以发现`NaN`,弥补了数组的`indexOf`方法的不足。 ```js [NaN].indexOf(NaN) // -1 [NaN].findIndex(y => Object.is(NaN, y)) // 0 ``` `indexOf`方法无法识别数组的`NaN`成员,但是`findIndex`方法可以借助`Object.is`方法做到 ### 9.8 数组实例的fill() **用法** `fill` 方法使用给定值,填充一个数组。 ```js ['a','b','c'].fill('石岩') //['石岩', '石岩', '石岩'] new Array(3).fill('夏心研') //['夏心研', '夏心研', '夏心研'] ``` 上面代码表明,`fill`方法用于空数组的初始化非常方便。数组中已有的元素,会被全部抹去。 `fill`方法还可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置。 ```js ['a', 'b', 'c'].fill(7, 1, 2) // ['a', 7, 'c'] ``` 如果填充的类型为对象,那么被赋值的是同一个内存地址的对象,而不是深拷贝对象。 ```js let arr = new Array(3).fill({name: "Mike"}); arr[0].name = "Ben"; arr // [{name: "Ben"}, {name: "Ben"}, {name: "Ben"}] let arr = new Array(3).fill([]); arr[0].push(5); arr // [[5], [5], [5]] ``` ### 9.9 数组实例的entries()、keys()、values() ES6 提供三个新的方法——`entries()`,`keys()`和`values()`——用于遍历数组。它们都返回一个遍历器对象,可以用`for...of`循环进行遍历,唯一的区别是`keys()`是对键名的遍历、`values()`是对键值的遍历,`entries()`是对键值对的遍历。 ```js for (let index of ['a', 'b'].keys()) { console.log(index); } // 0 // 1 for (let elem of ['a', 'b'].values()) { console.log(elem); } // 'a' // 'b' for (let [index, elem] of ['a', 'b'].entries()) { console.log(index, elem); } // 0 "a" // 1 "b" ``` 如果不使用`for...of`循环,可以手动调用遍历器对象的`next`方法,进行遍历 ```js let letter = ['a', 'b', 'c']; let entries = letter.entries(); console.log(entries.next().value); // [0, 'a'] console.log(entries.next().value); // [1, 'b'] console.log(entries.next().value); // [2, 'c'] ``` ### 9.10 数组实例的includes() `Array.prototype.includes`方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的`includes`方法类似。 ```js [1,2,3].includes(3) //true [1,2,3].includes(4) //false [1,2,NaN].includes(NaN) //true ``` 该方法的第二个参数表示搜索的起始位置,默认为`0`。如果第二个参数为负数,则表示倒数的位置,如果这时它大于数组长度(比如第二个参数为`-4`,但数组长度为`3`),则会重置为从`0`开始 ```js [1, 2, 3].includes(3, 3); // false [1, 2, 3].includes(3, -1); // true ``` `indexOf`方法有两个缺点,一是不够语义化,它的含义是找到参数值的第一个出现位置,所以要去比较是否不等于`-1`,表达起来不够直观。二是,它内部使用严格相等运算符(`===`)进行判断,这会导致对`NaN`的误判。 ```js const contains = (() => Array.prototype.includes ? (arr, value) => arr.includes(value) : (arr, value) => arr.some(el => el === value) )(); contains(['foo', 'bar'], 'baz'); // => false ``` Map 和 Set 数据结构有一个`has`方法,需要注意与`includes`区分。 - Map 结构的`has`方法,是用来查找键名的,比如`Map.prototype.has(key)`、`WeakMap.prototype.has(key)`、`Reflect.has(target, propertyKey)`。 - Set 结构的`has`方法,是用来查找值的,比如`Set.prototype.has(value)`、`WeakSet.prototype.has(value)`。 ### 9.11 数组实例的flat()、flatMap() 数组的成员有时还是数组,`Array.prototype.flat()`用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数据没有影响。 ```js [1,2,[3,4]].flat() // [1, 2, 3, 4] ``` `flat()`默认只会“拉平”一层,如果想要“拉平”多层的嵌套数组,可以将`flat()`方法的参数写成一个整数,表示想要拉平的层数,默认为1。 ```js [1, 2, [3, [4, 5]]].flat() // [1, 2, 3, [4, 5]] [1, 2, [3, [4, 5]]].flat(2) // [1, 2, 3, 4, 5] ``` 上面代码中,`flat()`的参数为2,表示要“拉平”两层的嵌套数组。 如果不管有多少层嵌套,都要转成一维数组,可以用`Infinity`关键字作为参数 ```js [1, [2, [3]]].flat(Infinity) // [1, 2, 3] ``` 如果原数组有空位,`flat()`方法会跳过空位 ```js [1, 2, , 4, 5].flat() // [1, 2, 4, 5] ``` `flatMap()`方法对原数组的每个成员执行一个函数(相当于执行`Array.prototype.map()`),然后对返回值组成的数组执行`flat()`方法。该方法返回一个新数组,不改变原数组 ```js // 相当于 [[2, 4], [3, 6], [4, 8]].flat() [2, 3, 4].flatMap((x) => [x, x * 2]) // [2, 4, 3, 6, 4, 8] ``` `flatMap()`只能展开一层数组 ```js // 相当于 [[[2]], [[4]], [[6]], [[8]]].flat() [1, 2, 3, 4].flatMap(x => [[x * 2]]) // [[2], [4], [6], [8]] ``` 上面代码中,遍历函数返回的是一个双层的数组,但是默认只能展开一层,因此`flatMap()`返回的还是一个嵌套数组。 `flatMap()`方法的参数是一个遍历函数,该函数可以接受三个参数,分别是当前数组成员、当前数组成员的位置(从零开始)、原数组。 ```js arr.flatMap(function callback(currentValue[, index[, array]]) { // ... }[, thisArg]) ``` flatMap()`方法还可以有第二个参数,用来绑定遍历函数里面的`this ## 10. 对象的扩展 ### 10.1 属性的简洁表示法 ES6 允许在大括号里面,直接写入变量和函数,作为对象的属性和方法。 ```javascript const foo = 'bar'; const baz = {foo}; baz // {foo: "bar"} // 等同于 const baz = {foo: foo}; function f(x, y) { return {x, y}; } // 等同于 function f(x, y) { return {x: x, y: y}; } f(1, 2) // Object {x: 1, y: 2} ``` **方法简写** ```js const o = { method() { return "Hello!"; } }; // 等同于 const o = { method: function() { return "Hello!"; } }; let birth = '2000/01/01'; const Person = { name: '张三', //等同于birth: birth birth, // 等同于hello: function ()... hello() { console.log('我的名字是', this.name); } }; function getPoint() { const x = 1; const y = 10; return {x, y}; } getPoint() // {x:1, y:10} ``` CommonJS 模块输出一组变量 ```js let ms = {}; function getItem (key) { return key in ms ? ms[key] : null; } function setItem (key, value) { ms[key] = value; } function clear () { ms = {}; } module.exports = { getItem, setItem, clear }; // 等同于 module.exports = { getItem: getItem, setItem: setItem, clear: clear }; ``` 属性的赋值器(setter)和取值器(getter) ```js const cart = { _wheels: 4, get wheels () { return this._wheels; }, set wheels (value) { if (value < this._wheels) { throw new Error('数值太小了!'); } this._wheels = value; } } let user = { name: 'test' }; let foo = { bar: 'baz' }; console.log(user, foo) // {name: "test"} {bar: "baz"} console.log({user, foo}) // {user: {name: "test"}, foo: {bar: "baz"}} ``` > 注意,简写的对象方法不能用作构造函数,会报错 > > ```js > const obj = { > f() { > this.foo = 'bar'; > } > }; > new obj.f() // 报错 > ``` ### 10.2 属性表达式 `JavaScript`定义对象的属性,有两种方法 ```js //方法一 obj.foo=true; //方法二 obj['a'+'bc']=123; ``` 上面代码的方法一是直接用标识符作为属性名,方法二是用表达式作为属性名,这时要将表达式放在方括号之内 如果使用字面量方式定义对象(使用大括号),在 ES5 中只能使用方法一(标识符)定义属性 ```js let obj={ foo:true, abc:123 } //ES6 允许字面量定义对象时,用方法二(表达式)作为对象的属性名,即把表达式放在方括号内 let propKey = 'foo'; let obj = { [propKey]: true, ['a' + 'bc']: 123 }; let lastWord = 'last word'; const a = { 'first word': 'hello', [lastWord]: 'world' }; a['first word'] // "hello" a[lastWord] // "world" a['last word'] // "world" ``` 表达式还可以用于定义方法名 ```js let obj = { ['h' + 'ello']() { return 'hi'; } }; obj.hello() // hi ``` > 注意,属性名表达式与简洁表示法,不能同时使用,会报错 > > ```js > // 报错 > const foo = 'bar'; > const bar = 'abc'; > const baz = { [foo] }; > // 正确 > const foo = 'bar'; > const baz = { [foo]: 'abc'}; > ``` > > 注意,属性名表达式如果是一个对象,默认情况下会自动将对象转为字符串`[object Object]` > > ```js > const keyA = {a: 1}; > const keyB = {b: 2}; > const myObject = { > [keyA]: 'valueA', > [keyB]: 'valueB' > }; > myObject // Object {[object Object]: "valueB"} > ``` ### 10.3 方法的name属性 函数的`name`属性,返回函数名。对象方法也是函数,因此也有`name`属性 ```js const person = { sayName() { console.log('hello!'); }, }; person.sayName.name // "sayName" //方法的name属性返回函数名(即方法名) ``` 如果对象的方法使用了取值函数(`getter`)和存值函数(`setter`),则`name`属性不是在该方法上面,而是该方法的属性的描述对象的`get`和`set`属性上面,返回值是方法名前加上`get`和`set` ```js const obj = { get foo() {}, set foo(x) {} }; obj.foo.name // TypeError: Cannot read property 'name' of undefined const descriptor = Object.getOwnPropertyDescriptor(obj, 'foo'); descriptor.get.name // "get foo" descriptor.set.name // "set foo" ``` `bind`方法创造的函数,`name`属性返回`bound`加上原函数的名字;`Function`构造函数创造的函数,`name`属性返回`anonymous`。 ```js (new Function()).name // "anonymous" var doSomething = function() { // ... }; doSomething.bind().name // "bound doSomething" ``` 如果对象的方法是一个 Symbol 值,那么`name`属性返回的是这个 Symbol 值的描述 ```js const key1 = Symbol('description'); const key2 = Symbol(); let obj = { [key1]() {}, [key2]() {}, }; obj[key1].name // "[description]" obj[key2].name // "" ``` ### 10.4 属性的可枚举性和遍历 #### 10.4.1 可枚举性 对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为。`Object.getOwnPropertyDescriptor`方法可以获取该属性的描述对象。 ```js let obj = { foo: 123 }; Object.getOwnPropertyDescriptor(obj, 'foo') /* { value: 123, writable: true, enumerable: true, configurable: true } */ ``` 描述对象的`enumerable`属性,称为“可枚举性”,如果该属性为`false`,就表示某些操作会忽略当前属性 目前,有四个操作会忽略`enumerable`为`false`的属性 - `for...in`循环:只遍历对象自身的和继承的可枚举的属性 - `Object.keys()`:返回对象自身的可枚举的属性的键名 - `JSON.stringify()`:只串行化对象自身的可枚举的属性 - `Object.assign()`:忽略`enumerable`为`false`的属性,只拷贝对象自身的可枚举的属性 引入“可枚举”(`enumerable`)这个概念的最初目的,就是让某些属性可以规避掉`for...in`操作,不然所有内部属性和方法都会被遍历到。比如,对象原型的`toString`方法,以及数组的`length`属性,就通过“可枚举性”,从而避免被`for...in`遍历到。 ```js Object.getOwnPropertyDescriptor(Object.prototype, 'toString').enumerable // false Object.getOwnPropertyDescriptor([], 'length').enumerable // false ``` 上面代码中,`toString`和`length`属性的`enumerable`都是`false`,因此`for...in`不会遍历到这两个继承自原型的属性 > ES6 规定,所有 Class 的原型的方法都是不可枚举的 > > ```js > Object.getOwnPropertyDescriptor(class {foo() {}}.prototype, 'foo').enumerable > // false > ``` #### 10.4.2 遍历 ES6 一共有 5 种方法可以遍历对象的属性 **(1)for ... in** `for...in`循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性) **(2)Object.keys(obj)** `Object.keys`返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名 **(3)Object.getOwnPropertyNames(obj)** `Object.getOwnPropertyNames`返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名 **(4)Object.getOwnPropertySymbols(obj)** `Object.getOwnPropertySymbols`返回一个数组,包含对象自身的所有 Symbol 属性的键名 **(5)Reflect.ownKeys(obj)** `Reflect.ownKeys`返回一个数组,包含对象自身的(不含继承的)所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举 以上的 5 种方法遍历对象的键名,都遵守同样的属性遍历的次序规则。 - 首先遍历所有数值键,按照数值升序排列。 - 其次遍历所有字符串键,按照加入时间升序排列。 - 最后遍历所有 Symbol 键,按照加入时间升序排列。 ```js Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 }) // ['2', '10', 'b', 'a', Symbol()] ``` ### 10.5 super 关键字 `this`关键字总是指向函数所在的当前对象,ES6 又新增了另一个类似的关键字`super`,指向当前对象的原型对象。 ```js const proto = { foo: 'hello' }; const obj = { foo: 'world', find() { return super.foo; } }; Object.setPrototypeOf(obj, proto); obj.find() // "hello" ``` 上面代码中,对象`obj.find()`方法之中,通过`super.foo`引用了原型对象`proto`的`foo`属性 > 注意,`super`关键字表示原型对象时,只能用在对象的方法之中,用在其他地方都会报错 ```js // 报错 const obj = { foo: super.foo } // 报错 const obj = { foo: () => super.foo } // 报错 const obj = { foo: function () { return super.foo } } ``` JavaScript 引擎内部,`super.foo`等同于`Object.getPrototypeOf(this).foo`(属性)或`Object.getPrototypeOf(this).foo.call(this)`(方法) ```js const proto = { x: 'hello', foo() { console.log(this.x); }, }; const obj = { x: 'world', foo() { super.foo(); } } Object.setPrototypeOf(obj, proto); obj.foo() // "world" ``` ### 10.6 对象的扩展运算符 #### 10.6.1 解构赋值 对象的解构赋值用于从一个对象取值,相当于将目标对象自身的可遍历的(enumerable、但尚未被读取的属性,分配到指定的对象上面。所有的键和它们的值,都会被拷贝到新对象上面。 ```js let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 }; x // 1 y // 2 z // { a: 3, b: 4 } ``` 由于解构赋值要求等号右边是一个对象,所以如果等号右边是`undefined`或`null`,就会报错,因为它们无法转为对象。 ```js let { ...z } = null; // 运行时错误 let { ...z } = undefined; // 运行时错误 ``` 解构赋值必须是最后一个参数,否则会报错 ```js let { ...x, y, z } = someObject; // 句法错误 let { x, ...y, ...z } = someObject; // 句法错误 //上面代码中,解构赋值不是最后一个参数,所以会报错 ``` > 注意,解构赋值的拷贝是浅拷贝,即如果一个键的值是复合类型的值(数组、对象、函数)、那么解构赋值拷贝的是这个值的引用,而不是这个值的副本 > > ```js > let obj = { a: { b: 1 } }; > let { ...x } = obj; > obj.a.b = 2; > x.a.b // 2 > ``` > > 扩展运算符的解构赋值,不能复制继承自原型对象的属性 > > ```js > let o1 = { a: 1 }; > let o2 = { b: 2 }; > o2.__proto__ = o1; > let { ...o3 } = o2; > o3 // { b: 2 } > o3.a // undefined > > const o = Object.create({ x: 1, y: 2 }); > o.z = 3; > let { x, ...newObj } = o; > let { y, z } = newObj; > x // 1 > y // undefined > z // 3 > ``` **解构赋值的一个用处,是扩展某个函数的参数,引入其他操作** ```js function baseFunction({ a, b }) { // ... } function wrapperFunction({ x, y, ...restConfig }) { // 使用 x 和 y 参数进行操作 // 其余参数传给原始函数 return baseFunction(restConfig); } //原始函数baseFunction接受a和b作为参数,函数wrapperFunction在baseFunction的基础上进行了扩展,能够接受多余的参数,并且保留原始函数的行为 ``` #### 10.6.2 扩展运算符 对象的扩展运算符(`...`)用于取出参数对象的所有可遍历属性,拷贝到当前对象之中 ```js let z = { a: 3, b: 4 }; let n = { ...z }; n // { a: 3, b: 4 } ``` 由于数组是特殊的对象,所以对象的扩展运算符也可以用于数组 ```js let foo = { ...['a', 'b', 'c'] }; foo // {0: "a", 1: "b", 2: "c"} ``` ```js ////如果扩展运算符后面是一个空对象,则没有任何效果 {...{}, a: 1} // { a: 1 } //如果扩展运算符后面不是对象,则会自动将其转为对象 // 等同于 {...Object(1)} {...1} // {} // 等同于 {...Object(true)} {...true} // {} // 等同于 {...Object(undefined)} {...undefined} // {} // 等同于 {...Object(null)} {...null} // {} //如果扩展运算符后面是字符串,它会自动转成一个类似数组的对象,因此返回的不是空对象 {...'hello'} // {0: "h", 1: "e", 2: "l", 3: "l", 4: "o"} ``` 对象的扩展运算符等同于使用`Object.assign()`方法 ```js let aClone = { ...a }; // 等同于 let aClone = Object.assign({}, a); ``` 完整克隆一个对象,还拷贝对象原型的属性的方法 ```js // 写法一 const clone1 = { __proto__: Object.getPrototypeOf(obj), ...obj }; // 写法二 const clone2 = Object.assign( Object.create(Object.getPrototypeOf(obj)), obj ); // 写法三 const clone3 = Object.create( Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj) ) ``` 扩展运算符可以用于合并两个对象 ```js let ab = { ...a, ...b }; // 等同于 let ab = Object.assign({}, a, b); ``` 如果用户自定义的属性,放在扩展运算符后面,则扩展运算符内部的同名属性会被覆盖掉 ```js let aWithOverrides = { ...a, x: 1, y: 2 }; // 等同于 let aWithOverrides = { ...a, ...{ x: 1, y: 2 } }; // 等同于 let x = 1, y = 2, aWithOverrides = { ...a, x, y }; // 等同于 let aWithOverrides = Object.assign({}, a, { x: 1, y: 2 }); let newVersion = { ...previousVersion, name: 'New Name' // Override the name property }; //如果把自定义属性放在扩展运算符前面,就变成了设置新对象的默认属性值 let aWithDefaults = { x: 1, y: 2, ...a }; // 等同于 let aWithDefaults = Object.assign({}, { x: 1, y: 2 }, a); // 等同于 let aWithDefaults = Object.assign({ x: 1, y: 2 }, a); //与数组的扩展运算符一样,对象的扩展运算符后面可以跟表达式 const obj = { ...(x > 1 ? {a: 1} : {}), b: 2, }; //扩展运算符的参数对象之中,如果有取值函数get,这个函数是会执行的 let a = { get x() { throw new Error('not throw yet'); } } let aWithXGetter = { ...a }; // 报错 ``` ### 10.7 链判断运算符 编程实践中,如果读取对象内部的某个属性,往往需要判断下改对象是否存在。比如要读取`message.body.user.firstName` ```js const firstName = (message && message.body && message.body.user && message.body.user.firstName) || 'default'; ``` 或者使用三元运算符`?:`,判断一个对象是否存在 ```js const fooInput = myForm.querySelector('input[name=foo]') const fooValue = fooInput ? fooInput.value : undefined ``` [ES2020](https://github.com/tc39/proposal-optional-chaining) 引入了“链判断运算符”(optional chaining operator)`?.`,简化上面的写法 ```js const firstName = message?.body?.user?.firstName || 'default'; const fooValue = myForm.querySelector('input[name=foo]')?.value ``` 上面代码使用了`?.`运算符,直接在链式调用的时候判断,左侧的对象是否为`null`或`undefined`。如果是的,就不再往下运算,而是返回`undefined`。 链判断运算符三种用法 - obj?.prop //对象属性 - obj?.[expr] //同上 - func?.(...agrs) //函数或对象方法的调用 下面是判断对象方法是否存在,如果存在就立即执行 ```js iterator.return?.() ``` 对于那些可能没有实现的方法,这个运算符尤其有用 ```js if (myForm.checkValidity?.() === false) { // 表单校验失败 return; } ``` 链判断运算符常见的使用形式 ```js a?.b // 等同于 a == null ? undefined : a.b a?.[x] // 等同于 a == null ? undefined : a[x] a?.b() // 等同于 a == null ? undefined : a.b() a?.() // 等同于 a == null ? undefined : a() ``` 特别注意后两种形式,如果`a?.b()`里面的`a.b`不是函数,不可调用,那么`a?.b()`是会报错的。`a?.()`也是如此,如果`a`不是`null`或`undefined`,但也不是函数,那么`a?.()`会报错。 > (1) 短路机制 > > ```js > a?.[++x] > // 等同于 > a == null ? undefined : a[++x] > ``` > > (2)delete运算符 > > ```js > delete a?.b > // 等同于 > a == null ? undefined : delete a.b > ``` > > (3)括号的影响 > > 如果属性链有圆括号,链判断运算符对圆括号外部没有影响,只对圆括号内部有影响 > > ```js > (a?.b).c > // 等价于 > (a == null ? undefined : a.b).c > ``` > > (4)报错场合 > > 以下写法是禁止的,会报错 > > ```js > // 构造函数 > new a?.() > new a?.b() > // 链判断运算符的右侧有模板字符串 > a?.`{b}` > a?.b`{c}` > // 链判断运算符的左侧是 super > super?.() > super?.foo > // 链运算符用于赋值运算符左侧 > a?.b = c > ``` > > (5) 右侧不得为十进制数值 > > 为了保证兼容以前的代码,允许`foo?.3:0`被解析成`foo ? .3 : 0`,因此规定如果`?.`后面紧跟一个十进制数字,那么`?.`不再被看成是一个完整的运算符,而会按照三元运算符进行处理,也就是说,那个小数点会归属于后面的十进制数字,形成一个小数。 ### 10.8 Null判断运算符 读取对象属性的时候,如果某个属性的值是`null`或`undefined`,有时候需要为它们指定默认值。常见做法是通过`||`运算符指定默认值。 ```js const headerText = response.settings.headerText || 'Hello, world!'; const animationDuration = response.settings.animationDuration || 300; const showSplashScreen = response.settings.showSplashScreen || true; ``` 上面的三行代码都通过`||`运算符指定默认值,但是这样写是错的。开发者的原意是,只要属性的值为`null`或`undefined`,默认值就会生效,但是属性的值如果为空字符串或`false`或`0`,默认值也会生效。 为了避免这种情况,[ES2020](https://github.com/tc39/proposal-nullish-coalescing) 引入了一个新的 Null 判断运算符`??`。它的行为类似`||`,但是只有运算符左侧的值为`null`或`undefined`时,才会返回右侧的值。 ```js const headerText = response.settings.headerText ?? 'Hello, world!'; const animationDuration = response.settings.animationDuration ?? 300; const showSplashScreen = response.settings.showSplashScreen ?? true; ``` 这个运算符的一个目的,就是跟链判断运算符`?.`配合使用,为`null`或`undefined`的值设置默认值。 ```js const animationDuration = response.settings?.animationDuration ?? 300; ``` 上面代码中,`response.settings`如果是`null`或`undefined`,就会返回默认值300。 这个运算符很适合判断函数参数是否赋值。 ```js function Component(props) { const enable = props.enabled ?? true; // … } ``` 上面代码判断`props`参数的`enabled`属性是否赋值,等同于下面的写法 ```js function Component(props) { const { enabled: enable = true, } = props; // … } ``` `??`有一个运算优先级问题,它与`&&`和`||`的优先级孰高孰低。现在的规则是,如果多个逻辑运算符一起使用,必须用括号表明优先级,否则会报错。 ```js // 报错 lhs && middle ?? rhs lhs ?? middle && rhs lhs || middle ?? rhs lhs ?? middle || rhs ``` 上面四个表达式都会报错,必须加入表明优先级的括号. ```js (lhs && middle) ?? rhs; lhs && (middle ?? rhs); (lhs ?? middle) && rhs; lhs ?? (middle && rhs); (lhs || middle) ?? rhs; lhs || (middle ?? rhs); (lhs ?? middle) || rhs; lhs ?? (middle || rhs); ``` ## 11. 对象的新增方法 ### 11.1 Object.is() ES5 比较两个值是否相等,只有两个运算符:相等运算符(`==`)和严格相等运算符(`===`)。它们都有缺点,前者会自动转换数据类型,后者的`NaN`不等于自身,以及`+0`等于`-0`。JavaScript 缺乏一种运算,在所有环境中,只要两个值是一样的,它们就应该相等。 ES6 提出“Same-value equality”(同值相等)算法,用来解决这个问题。`Object.is`就是部署这个算法的新方法。它用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。 ```js Object.is('foo', 'foo') // true Object.is({}, {}) // false ``` 不同之处只有两个:一是`+0`不等于`-0`,二是`NaN`等于自身。 ```js +0 === -0 //true NaN === NaN // false Object.is(+0, -0) // false Object.is(NaN, NaN) // true ``` ES5 可以通过下面的代码,部署`Object.is` ```js Object.defineProperty(Object, 'is', { value: function(x, y) { if (x === y) { // 针对+0 不等于 -0的情况 return x !== 0 || 1 / x === 1 / y; } // 针对NaN的情况 return x !== x && y !== y; }, configurable: true, enumerable: false, writable: true }); ``` ### 11.2 Object.assign() `Object.assign`方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。 ```js const target = { a: 1 }; const source1 = { b: 2 }; const source2 = { c: 3 }; Object.assign(target, source1, source2); target // {a:1, b:2, c:3} ``` `Object.assign`方法的第一个参数是目标对象,后面的参数都是源对象。 > 注意,如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性 ```js const target = { a: 1, b: 1 }; const source1 = { b: 2, c: 2 }; const source2 = { c: 3 }; Object.assign(target, source1, source2); target // {a:1, b:2, c:3} //如果只有一个参数,Object.assign会直接返回该参数 const obj = {a: 1}; Object.assign(obj) === obj // true //如果该函数不是对象,则会先转化成对象,然后返回 typeof Object.assign(2)//'object' //由于undefined和null无法转成对象,所以如果它们作为参数,就会报错。 Object.assign(undefined) // 报错 Object.assign(null) // 报错 //由于undefined和null无法转成对象,所以如果它们作为参数,就会报错 Object.assign(undefined) // 报错 Object.assign(null) // 报错 //如果非对象参数出现在源对象的位置(即非首参数),那么处理规则有所不同。首先,这些参数都会转成对象,如果无法转成对象,就会跳过。这意味着,如果undefined和null不在首参数,就不会报错 let obj = {a: 1}; Object.assign(obj, undefined) === obj // true Object.assign(obj, null) === obj // true //其他类型的值(即数值、字符串和布尔值)不在首参数,也不会报错。但是,除了字符串会以数组形式,拷贝入目标对象,其他值都不会产生效果 const v1 = 'abc'; const v2 = true; const v3 = 10; const obj = Object.assign({}, v1, v2, v3); console.log(obj); // { "0": "a", "1": "b", "2": "c" } ``` `Object.assign`拷贝的属性是有限制的,只拷贝源对象的自身属性(不拷贝继承属性),也不拷贝不可枚举的属性(`enumerable: false`)。 ```js Object.assign({b: 'c'}, Object.defineProperty({}, 'invisible', { enumerable: false, value: 'hello' }) ) // { b: 'c' } //属性名为 Symbol 值的属性,也会被Object.assign拷贝 Object.assign({ a: 'b' }, { [Symbol('c')]: 'd' }) // { a: 'b', Symbol(c): 'd' } ``` **注意点** (1) 浅拷贝 `Object.assign`方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。 ```js const obj1 = {a: {b: 1}}; const obj2 = Object.assign({}, obj1); obj1.a.b = 2; obj2.a.b // 2 ``` (2)同名属性的替换 对于这种嵌套的对象,一旦遇到同名属性,`Object.assign`的处理方法是替换,而不是添加 ```js const target = { a: { b: 'c', d: 'e' } } const source = { a: { b: 'hello' } } Object.assign(target, source) // { a: { b: 'hello' } } ``` (3)数组的处理 `Object.assign`可以用来处理数组,但是会把数组是为对象。 ```js Object.assign([1, 2, 3], [4, 5]) // [4, 5, 3] ``` (4)取值函数的处理 `Object.assign`只能进行值的复制,如果要复制的值是一个取值函数,那么将求值后再复制。 ```js const source = { get foo() { return 1 } }; const target = {}; Object.assign(target, source) // { foo: 1 } ``` **常见用途** (1) 为对象添加属性 ```js class Point{ constructor(x,y){ Object.assign(this,{x,y}); } } ``` (2) 为对象添加方法 ```js Object.assign(SomeClass.prototype, { someMethod(arg1, arg2) { ··· }, anotherMethod() { ··· } }); // 等同于下面的写法 SomeClass.prototype.someMethod = function (arg1, arg2) { ··· }; SomeClass.prototype.anotherMethod = function () { ··· }; ``` (3) 克隆对象 ```js function clone(origin){ return Object.assign({},origin); } //如果想要保持继承链,可以采用下面的代码 function clone(origin) { let originProto = Object.getPrototypeOf(origin); return Object.assign(Object.create(originProto), origin); } ``` (4) 合并多个对象 将多个对象合并到某个对象 ```js const merge=(target,...sources)=>Object.assign(target,...sources); //如果希望合并后返回一个新对象,可以改写上面函数,对一个空对象合并 const merge = (...sources) => Object.assign({}, ...sources); ``` (5)为属性指定默认值 ```js const DEFAULTS = { logLevel: 0, outputFormat: 'html' }; function processContent(options) { options = Object.assign({}, DEFAULTS, options); console.log(options); // ... } ``` 注意,由于存在浅拷贝的问题,`DEFAULTS`对象和`options`对象的所有属性的值,最好都是简单类型,不要指向另一个对象。 ```js const DEFAULTS = { url: { host: 'example.com', port: 7070 }, }; processContent({ url: {port: 8000} }) // { // url: {port: 8000} // } ``` ### 11.3 Object.getOwnPropertyDescriptors() ES5 的`Object.getOwnPropertyDescriptor()`方法会返回某个对象属性的描述对象(descriptor)。ES2017 引入了`Object.getOwnPropertyDescriptors()`方法,返回指定对象所有自身属性(非继承属性)的描述对象。 ```js const obj = { foo: 123, get bar() { return 'abc' } }; Object.getOwnPropertyDescriptors(obj) // { foo: // { value: 123, // writable: true, // enumerable: true, // configurable: true }, // bar: // { get: [Function: get bar], // set: undefined, // enumerable: true, // configurable: true } } //该方法实现 function getOwnPropertyDescriptors(obj) { const result = {}; for (let key of Reflect.ownKeys(obj)) { result[key] = Object.getOwnPropertyDescriptor(obj, key); } return result; } ``` > 该方法的引入目标,主要是解决`Object.assign()`无法正确拷贝`get`属性和`set`属性的问题 ```js const source = { set foo(value) { console.log(value); } }; const target1 = {}; Object.assign(target1, source); Object.getOwnPropertyDescriptor(target1, 'foo') // { value: undefined, // writable: true, // enumerable: true, // configurable: true } //正确拷贝方式 const source = { set foo(value) { console.log(value); } }; const target2 = {}; Object.defineProperties(target2, Object.getOwnPropertyDescriptors(source)); Object.getOwnPropertyDescriptor(target2, 'foo') // { get: undefined, // set: [Function: set foo], // enumerable: true, // configurable: true } const shallowMerge = (target, source) => Object.defineProperties( target, Object.getOwnPropertyDescriptors(source) ); ``` `Object.getOwnPropertyDescriptors()`方法的另一个用处,是配合`Object.create()`方法,将对象属性克隆到一个新对象。这属于浅拷贝。 ```js const clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj)); // 或者 const shallowClone = (obj) => Object.create( Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj) ); ``` `Object.getOwnPropertyDescriptors()`方法可以实现一个对象继承另一个对象。以前,继承另一个对象,常常写成下面这样。 ```js const obj={ __proto__:prot, foo:123 }; //ES6 规定__proto__只有浏览器要部署,其他环境不用部署。如果去除__proto__,上面代码就要改成下面这样 const obj = Object.create(prot); obj.foo = 123; // 或者 const obj = Object.assign( Object.create(prot), { foo: 123, } ); ``` `Object.getOwnPropertyDescriptors()`也可以用来实现 Mixin(混入)模式。 ```js let mix = (object) => ({ with: (...mixins) => mixins.reduce( (c, mixin) => Object.create( c, Object.getOwnPropertyDescriptors(mixin) ), object) }); // multiple mixins example let a = {a: 'a'}; let b = {b: 'b'}; let c = {c: 'c'}; let d = mix(c).with(a, b); d.c // "c" d.b // "b" d.a // "a" ``` 上面代码返回一个新的对象`d`,代表了对象`a`和`b`被混入了对象`c`的操作。 ### 10.4 `__proto__`属性,Object.setPrototypeOf(),Object.getPrototypeOf() `__proto__`属性 `__proto__`属性(前后各两个下划线),用来读取或设置当前对象的原型对象(prototype)。目前,所有浏览器(包括 IE11)都部署了这个属性。 ```js // es5 的写法 const obj = { method: function() { ... } }; obj.__proto__ = someOtherObj; // es6 的写法 var obj = Object.create(someOtherObj); obj.method = function() { ... }; ``` 使用`Object.setPrototypeOf()`(写操作)、`Object.getPrototypeOf()`(读操作)、`Object.create()`(生成操作) ```js Object.defineProperty(Object.prototype, '__proto__', { get() { let _thisObj = Object(this); return Object.getPrototypeOf(_thisObj); }, set(proto) { if (this === undefined || this === null) { throw new TypeError(); } if (!isObject(this)) { return undefined; } if (!isObject(proto)) { return undefined; } let status = Reflect.setPrototypeOf(this, proto); if (!status) { throw new TypeError(); } }, }); function isObject(value) { return Object(value) === value; } ``` **Object.setPrototypeOf()** `Object.setPrototypeOf`方法的作用与`__proto__`相同,用来设置一个对象的原型对象(prototype),返回参数对象本身。它是 ES6 正式推荐的设置原型对象的方法。 ```js // 格式 Object.setPrototypeOf(object, prototype) // 用法 const o = Object.setPrototypeOf({}, null); //等同于下面的函数 function setPrototypeOf(obj, proto) { obj.__proto__ = proto; return obj; } //实例 let proto = {}; let obj = { x: 10 }; Object.setPrototypeOf(obj, proto); proto.y = 20; proto.z = 40; obj.x // 10 obj.y // 20 obj.z // 40 //如果第一个参数不是对象,会自动转为对象。但是由于返回的还是第一个参数,所以这个操作不会产生任何效果 Object.setPrototypeOf(1, {}) === 1 // true Object.setPrototypeOf('foo', {}) === 'foo' // true Object.setPrototypeOf(true, {}) === true // true //由于undefined和null无法转为对象,所以如果第一个参数是undefined或null,就会报错 ``` **Object.getPrototypeOf()** 该方法与`Object.setPrototypeOf`方法配套,用于读取一个对象的原型对象。 ```js Object.getPrototypeOf(obj); function Rectangle() { // ... } const rec = new Rectangle(); Object.getPrototypeOf(rec) === Rectangle.prototype // true Object.setPrototypeOf(rec, Object.prototype); Object.getPrototypeOf(rec) === Rectangle.prototype // false //如果参数不是对象,会被自动转为对象 // 等同于 Object.getPrototypeOf(Number(1)) Object.getPrototypeOf(1) // Number {[[PrimitiveValue]]: 0} // 等同于 Object.getPrototypeOf(String('foo')) Object.getPrototypeOf('foo') // String {length: 0, [[PrimitiveValue]]: ""} // 等同于 Object.getPrototypeOf(Boolean(true)) Object.getPrototypeOf(true) // Boolean {[[PrimitiveValue]]: false} Object.getPrototypeOf(1) === Number.prototype // true Object.getPrototypeOf('foo') === String.prototype // true Object.getPrototypeOf(true) === Boolean.prototype // true //如果参数是undefined或null,它们无法转为对象,所以会报错 Object.getPrototypeOf(null) // TypeError: Cannot convert undefined or null to object Object.getPrototypeOf(undefined) // TypeError: Cannot convert undefined or null to object ``` ### 10.5 Object.keys(),Object.values(),Object.entries() **Object.keys()** ES5 引入了`Object.keys`方法,返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键名。 ```js var obj = { foo: 'bar', baz: 42 }; Object.keys(obj) // ["foo", "baz"] ``` ES2017 [引入](https://github.com/tc39/proposal-object-values-entries)了跟`Object.keys`配套的`Object.values`和`Object.entries`,作为遍历一个对象的补充手段,供`for...of`循环使用。 ```js let {keys, values, entries} = Object; let obj = { a: 1, b: 2, c: 3 }; for (let key of keys(obj)) { console.log(key); // 'a', 'b', 'c' } for (let value of values(obj)) { console.log(value); // 1, 2, 3 } for (let [key, value] of entries(obj)) { console.log([key, value]); // ['a', 1], ['b', 2], ['c', 3] } ``` **Object.values()** `Object.values`方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值。 ```js const obj = { foo: 'bar', baz: 42 }; Object.values(obj) // ["bar", 42] ``` `Object.create`方法的第二个参数添加的对象属性(属性`p`),如果不显式声明,默认是不可遍历的,因为`p`的属性描述对象的`enumerable`默认是`false`,`Object.values`不会返回这个属性。只要把`enumerable`改成`true`,`Object.values`就会返回属性`p`的值。 ```js const obj = Object.create({}, {p: {value: 42}}); Object.values(obj) // [] const obj = Object.create({}, {p: { value: 42, enumerable: true } }); Object.values(obj) // [42] //Object.values会过滤属性名为 Symbol 值的属性 Object.values({ [Symbol()]: 123, foo: 'abc' }); // ['abc'] //如果Object.values方法的参数是一个字符串,会返回各个字符组成的一个数组 Object.values('foo') // ['f', 'o', 'o'] //如果参数不是对象,Object.values会先将其转为对象。由于数值和布尔值的包装对象,都不会为实例添加非继承的属性 Object.values(42) // [] Object.values(true) // [] ``` **Object.entries()** `Object.entries()`方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值对数组。 ```js const obj = { foo: 'bar', baz: 42 }; Object.entries(obj) // [ ["foo", "bar"], ["baz", 42] ] //如果原对象的属性名是一个 Symbol 值,该属性会被忽略 Object.entries({ [Symbol()]: 123, foo: 'abc' }); // [ [ 'foo', 'abc' ] ] //Object.entries的基本用途是遍历对象的属性 let obj = { one: 1, two: 2 }; for (let [k, v] of Object.entries(obj)) { console.log( `${JSON.stringify(k)}: ${JSON.stringify(v)}` ); } // "one": 1 // "two": 2 //Object.entries方法将对象转为真正的Map结构 const obj = { foo: 'bar', baz: 42 }; const map = new Map(Object.entries(obj)); map // Map { foo: "bar", baz: 42 } // Generator函数的版本 function* entries(obj) { for (let key of Object.keys(obj)) { yield [key, obj[key]]; } } // 非Generator函数的版本 function entries(obj) { let arr = []; for (let key of Object.keys(obj)) { arr.push([key, obj[key]]); } return arr; } ``` ### 10.6 Object.fromEntries() `Object.fromEntries()`方法是`Object.entries()`的逆操作,用于将一个键值对数组转为对象。 ```JS Object.fromEntries([ ['foo', 'bar'], ['baz', 42] ]) // { foo: "bar", baz: 42 } ``` 该方法的主要目的,是将键值对的数据结构还原为对象,因此特别适合将 Map 结构转为对象。 ```js // 例一 const entries = new Map([ ['foo', 'bar'], ['baz', 42] ]); Object.fromEntries(entries) // { foo: "bar", baz: 42 } // 例二 const map = new Map().set('foo', true).set('bar', false); Object.fromEntries(map) // { foo: true, bar: false } ``` 该方法的一个用处是配合`URLSearchParams`对象,将查询字符串转为对象。 ```js Object.fromEntries(new URLSearchParams('foo=bar&baz=qux')) // { foo: "bar", baz: "qux" } ``` ## 12. Symbol ES6引入了一个新的原始数据类型`Symbol`,表示独一无二的值。 Symbol 值通过`Symbol`函数生成。凡是属性名属于 Symbol 类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。 ```js let s = Symbol(); typeof s // "symbol" ``` > 注意,`Symbol`函数前不能使用`new`命令,否则会报错。这是因为生成的 Symbol 是一个原始类型的值,不是对象。也就是说,由于 Symbol 值不是对象,所以不能添加属性。基本上,它是一种类似于字符串的数据类型。 `Symbol`函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。 ```js let s1 = Symbol('foo'); let s2 = Symbol('bar'); s1 // Symbol(foo) s2 // Symbol(bar) s1.toString() // "Symbol(foo)" s2.toString() // "Symbol(bar)" ``` 如果 Symbol 的参数是一个对象,就会调用该对象的`toString`方法,将其转为字符串,然后才生成一个 Symbol 值。 ```js const obj = { toString() { return 'abc'; } }; const sym = Symbol(obj); sym // Symbol(abc) ``` 注意,`Symbol`函数的参数只是表示对当前 Symbol 值的描述,因此相同参数的`Symbol`函数的返回值是不相等的。 ```js // 没有参数的情况 let s1 = Symbol(); let s2 = Symbol(); s1 === s2 // false // 有参数的情况 let s1 = Symbol('foo'); let s2 = Symbol('foo'); s1 === s2 // false ``` Symbol 值不能与其他类型的值进行运算,会报错。 ```js let sym = Symbol('My symbol'); "your symbol is " + sym // TypeError: can't convert symbol to string `your symbol is ${sym}` // TypeError: can't convert symbol to string ``` Symbol 值可以显式转为字符串。 ```js let sym = Symbol('My symbol'); String(sym) // 'Symbol(My symbol)' sym.toString() // 'Symbol(My symbol)' ``` Symbol 值也可以转为布尔值,但是不能转为数值。 ```js let sym = Symbol(); Boolean(sym) // true !sym // false if (sym) { // ... } Number(sym) // TypeError sym + 2 // TypeError ``` ### 12.1 Symbol.prototype.description 创建Symbol的时候,可以添加一个描述。 ```js const sym=Symbol('foo'); //读取描述可以将symbol现实转化为字符串 String(sym); sym.toString(); //ES2019提供了一个description,直接返回Symbol的描述 const sym = Symbol('foo'); sym.description // "foo" ``` ### 12.2 作为属性名的Symbol 由于每一个 Symbol 值都是不相等的,这意味着 Symbol 值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性。这对于一个对象由多个模块构成的情况非常有用,能防止某一个键被不小心改写或覆盖。 ```js let mySymbol = Symbol(); // 第一种写法 let a = {}; a[mySymbol] = 'Hello!'; // 第二种写法 let a = { [mySymbol]: 'Hello!' }; // 第三种写法 let a = {}; Object.defineProperty(a, mySymbol, { value: 'Hello!' }); // 以上写法都得到同样结果 a[mySymbol] // "Hello!" ``` > 注意,Symbol值作为对象属性名是,不能使用点运算符 ```js const mySymbol = Symbol(); const a = {}; a.mySymbol = 'Hello!'; a[mySymbol] // undefined a['mySymbol'] // "Hello!" //同理,在对象的内部,使用 Symbol 值定义属性时,Symbol 值必须放在方括号之中 let s = Symbol(); let obj = { [s]: function (arg) { ... } }; obj[s](123); ``` Symbol 类型还可以用于定义一组常量,保证这组常量的值都是不相等的 ```js const log = {}; log.levels = { DEBUG: Symbol('debug'), INFO: Symbol('info'), WARN: Symbol('warn') }; console.log(log.levels.DEBUG, 'debug message'); console.log(log.levels.INFO, 'info message'); const COLOR_RED = Symbol(); const COLOR_GREEN = Symbol(); function getComplement(color) { switch (color) { case COLOR_RED: return COLOR_GREEN; case COLOR_GREEN: return COLOR_RED; default: throw new Error('Undefined color'); } } ``` Symbol 值作为属性名时,该属性还是公开属性,不是私有属性。 ### 12.3 实例消除魔术字符串 魔术字符串指的是,在代码之中多次出现、与代码形成强耦合的某一个具体的字符串或者数值。风格良好的代码,应该尽量消除魔术字符串,改由含义清晰的变量代替。 ```js function getArea(shape, options) { let area = 0; switch (shape) { case 'Triangle': // 魔术字符串 area = .5 * options.width * options.height; break; /* ... more code ... */ } return area; } getArea('Triangle', { width: 100, height: 100 }); // 魔术字符串 ``` 字符串`Triangle`就是一个魔术字符串。它多次出现,与代码形成“强耦合”,不利于将来的修改和维护。 常用的消除魔术字符串的方法,就是把它写成一个变量 ```js const shapeType = { triangle: 'Triangle' }; function getArea(shape, options) { let area = 0; switch (shape) { case shapeType.triangle: area = .5 * options.width * options.height; break; } return area; } getArea(shapeType.triangle, { width: 100, height: 100 }); ``` 如果仔细分析,可以发现`shapeType.triangle`等于哪个值并不重要,只要确保不会跟其他`shapeType`属性的值冲突即可。因此,这里就很适合改用 Symbol 值。 ```js const shapeType = { triangle: Symbol() }; ``` ### 12.4 属性名的遍历 Symbol 作为属性名,遍历对象的时候,该属性不会出现在`for...in`、`for...of`循环中,也不会被`Object.keys()`、`Object.getOwnPropertyNames()`、`JSON.stringify()`返回。 但是,它也不是私有属性,有一个`Object.getOwnPropertySymbols()`方法,可以获取指定对象的所有 Symbol 属性名。该方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。 ```js const obj = {}; let a = Symbol('a'); let b = Symbol('b'); obj[a] = 'Hello'; obj[b] = 'World'; const objectSymbols = Object.getOwnPropertySymbols(obj); objectSymbols // [Symbol(a), Symbol(b)] ``` `Object.getOwnPropertySymbols()`方法与`for...in`循环、`Object.getOwnPropertyNames`方法进行对比的例子。 ```js const obj = {}; const foo = Symbol('foo'); obj[foo] = 'bar'; for (let i in obj) { console.log(i); // 无输出 } Object.getOwnPropertyNames(obj) // [] Object.getOwnPropertySymbols(obj) // [Symbol(foo)] ``` `Reflect.ownKeys()`方法可以返回所有类型的键名,包括常规键名和 Symbol 键名。 ```js let obj = { [Symbol('my_key')]: 1, enum: 2, nonEnum: 3 }; Reflect.ownKeys(obj) // ["enum", "nonEnum", Symbol(my_key)] ``` 由于以 Symbol 值作为键名,不会被常规方法遍历得到。我们可以利用这个特性,为对象定义一些非私有的、但又希望只用于内部的方法。 ```js let size = Symbol('size'); class Collection { constructor() { this[size] = 0; } add(item) { this[this[size]] = item; this[size]++; } static sizeOf(instance) { return instance[size]; } } let x = new Collection(); Collection.sizeOf(x) // 0 x.add('foo'); Collection.sizeOf(x) // 1 Object.keys(x) // ['0'] Object.getOwnPropertyNames(x) // ['0'] Object.getOwnPropertySymbols(x) // [Symbol(size)] ``` ### 12.5 Symbol.for()、Symbol.keyFor() `Symbol.for()`方法接受一个字符串作为参数,然后搜索有没有以该参数作为名称的Symbol值。如果有,就返回这个 Symbol 值,否则就新建一个以该字符串为名称的 Symbol 值,并将其注册到全局。 ```js let s1=Symbol.for('foo'); let s2=Symbol.for('foo'); s1===s2 //true ``` `Symbol.for()`与`Symbol()`这两种写法,都会生成新的 Symbol。它们的区别是,前者会被登记在全局环境中供搜索,后者不会。`Symbol.for()`不会每次调用就返回一个新的 Symbol 类型的值,而是会先检查给定的`key`是否已经存在,如果不存在才会新建一个值。 ```js Symbol.for("bar") === Symbol.for("bar") // true Symbol("bar") === Symbol("bar") // false ``` `Symbol.keyFor()`方法返回一个已登记的Symbol类型值的`key` ```js let s1 = Symbol.for("foo"); Symbol.keyFor(s1) // "foo" let s2 = Symbol("foo"); Symbol.keyFor(s2) // undefined ``` > 注意,`Symbol.for()`为 Symbol 值登记的名字,是全局环境的,不管有没有在全局环境运行。 ```js function foo() { return Symbol.for('bar'); } const x = foo(); const y = Symbol.for('bar'); console.log(x === y); // true ``` `Symbol.for()`的这个全局登记特性,可以用在不同的 iframe 或 service worker 中取到同一个值 ```js iframe = document.createElement('iframe'); iframe.src = String(window.location); document.body.appendChild(iframe); iframe.contentWindow.Symbol.for('foo') === Symbol.for('foo') // true ``` ### 12.6 实例:模块的Singleton模式 `Singleton`模式值的是调用一个类,任何时候返回的都是同一个实例。 ```js //mod.js function A(){ this.foo='hello'; } if(!global._foo){ global._foo=new A(); } module.exports=global._foo; //加载模块 const a=require('./mod.js'); console.log(a.foo); ``` > 全局变量`global._foo`是可写的,任何文件都可以修改。 ```js global._foo = { foo: 'world' }; const a = require('./mod.js'); console.log(a.foo); ``` 上面的代码,会使得加载`mod.js`的脚本都失真。 可以使用Symbol防止这种情况 ```js //mod.js const FOO_KEY=Symbol.for('foo'); function A(){ globalThis.foo='hello'; } if(!globalThis[FOO_KEY]){ globalThis[FOO_KEY]=new A(); } module.exports=globalThis[FOO_KEY]; //上面代码中,可以保证global[FOO_KEY]不会被无意间覆盖,但还是可以被改写。 global[Symbol.for('foo')] = { foo: 'world' }; const a = require('./mod.js'); ``` 如果键名使用`Symbol`方法生成,那么外部将无法引用这个值,当然也就无法改写。 ### 12.7 内置的Symbol #### 12.7.1 Symbol.hasInstance 对象的`Symbol.hasInstance`属性,指向一个内部方法。当其他对象使用`instanceof`运算符,判断是否为该对象的实例时,会调用这个方法。比如,`foo instanceof Foo`在语言内部,实际调用的是`Foo[Symbol.hasInstance](foo)`。 ```js class MyClass { [Symbol.hasInstance](foo) { return foo instanceof Array; } } [1, 2, 3] instanceof new MyClass() // true class Even { static [Symbol.hasInstance](obj) { return Number(obj) % 2 === 0; } } // 等同于 const Even = { [Symbol.hasInstance](obj) { return Number(obj) % 2 === 0; } }; 1 instanceof Even // false 2 instanceof Even // true 12345 instanceof Even // false ``` #### 12.7.2 Symbol.isConcatSpreadable 对象的`Symbol.isConcatSpreadable`属性等于一个布尔值,表示该对象用于`Array.prototype.concat()`时,是否可以展开。 ```js let arr1 = ['c', 'd']; ['a', 'b'].concat(arr1, 'e') // ['a', 'b', 'c', 'd', 'e'] arr1[Symbol.isConcatSpreadable] // undefined let arr2 = ['c', 'd']; arr2[Symbol.isConcatSpreadable] = false; ['a', 'b'].concat(arr2, 'e') // ['a', 'b', ['c','d'], 'e'] ``` 上面代码说明,数组的默认行为是可以展开,`Symbol.isConcatSpreadable`默认等于`undefined`。该属性等于`true`时,也有展开的效果。 类似数组的对象正好相反,默认不展开。它的`Symbol.isConcatSpreadable`属性设为`true`,才可以展开。 ```js let obj = {length: 2, 0: 'c', 1: 'd'}; ['a', 'b'].concat(obj, 'e') // ['a', 'b', obj, 'e'] obj[Symbol.isConcatSpreadable] = true; ['a', 'b'].concat(obj, 'e') // ['a', 'b', 'c', 'd', 'e'] ``` `Symbol.isConcatSpreadable`属性也可以定义在类里面。 ```js class A1 extends Array { constructor(args) { super(args); this[Symbol.isConcatSpreadable] = true; } } class A2 extends Array { constructor(args) { super(args); } get [Symbol.isConcatSpreadable] () { return false; } } let a1 = new A1(); a1[0] = 3; a1[1] = 4; let a2 = new A2(); a2[0] = 5; a2[1] = 6; [1, 2].concat(a1).concat(a2) // [1, 2, 3, 4, [5, 6]] ``` #### 12.7.3 Symbol.species 对象的`Symbol.species`属性,指向一个构造函数。创建衍生对象时,会使用该属性。 ```js class MyArray extends Array { } const a = new MyArray(1, 2, 3); const b = a.map(x => x); const c = a.filter(x => x > 1); b instanceof MyArray // true c instanceof MyArray // true ``` 上面代码中,子类`MyArray`继承了父类`Array`,`a`是`MyArray`的实例,`b`和`c`是`a`的衍生对象。你可能会认为,`b`和`c`都是调用数组方法生成的,所以应该是数组(`Array`的实例),但实际上它们也是`MyArray`的实例。 `Symbol.species`属性就是为了解决这个问题而提供的。现在,我们可以为`MyArray`设置`Symbol.species`属性。 ```js class MyArray extends Array { static get [Symbol.species]() { return Array; } } class MyArray extends Array { static get [Symbol.species]() { return Array; } } const a = new MyArray(); const b = a.map(x => x); b instanceof MyArray // false b instanceof Array // true class T1 extends Promise { } class T2 extends Promise { static get [Symbol.species]() { return Promise; } } new T1(r => r()).then(v => v) instanceof T1 // true new T2(r => r()).then(v => v) instanceof T2 // false ``` #### 12.7.4 Symbol.match 对象的`Symbol.match`属性,指向一个函数。当执行`str.match(myObject)`时,如果该属性存在,会调用它,返回该方法的返回值。 ```js String.prototype.match(regexp) // 等同于 regexp[Symbol.match](this) class MyMatcher { [Symbol.match](string) { return 'hello world'.indexOf(string); } } 'e'.match(new MyMatcher()) // 1 ``` #### 12.7.5 Symbol.replace 对象的`Symbol.replace`属性,指向一个方法,当该对象被`String.prototype.replace`方法调用时,会返回该方法的返回值。 ```js String.prototype.replace(searchValue, replaceValue) // 等同于 searchValue[Symbol.replace](this, replaceValue) const x = {}; x[Symbol.replace] = (...s) => console.log(s); 'Hello'.replace(x, 'World') // ["Hello", "World"] ``` #### 12.7.6 Symbol.search 对象的`Symbol.search`属性,指向一个方法,当该对象被`String.prototype.search`方法调用时,会返回该方法的返回值。 ```js String.prototype.search(regexp) // 等同于 regexp[Symbol.search](this) class MySearch { constructor(value) { this.value = value; } [Symbol.search](string) { return string.indexOf(this.value); } } 'foobar'.search(new MySearch('foo')) // 0 ``` #### 12.7.7 Symbol.split 对象的`Symbol.split`属性,指向一个方法,当该对象被`String.prototype.split`方法调用时,会返回该方法的返回值。 ```js String.prototype.split(separator, limit) // 等同于 separator[Symbol.split](this, limit) class MySplitter { constructor(value) { this.value = value; } [Symbol.split](string) { let index = string.indexOf(this.value); if (index === -1) { return string; } return [ string.substr(0, index), //加上分割符的长度 string.substr(index + this.value.length) ]; } } 'foobar'.split(new MySplitter('foo')) // ['', 'bar'] 'foobar'.split(new MySplitter('bar')) // ['foo', ''] 'foobar'.split(new MySplitter('baz')) // 'foobar' ``` #### 12.7.8 Symbol.iterator 对象的`Symbol.iterator`属性,指向该对象的默认遍历器方法。 ```js const myIterable = {}; myIterable[Symbol.iterator] = function* () { yield 1; yield 2; yield 3; }; [...myIterable] // [1, 2, 3] class Collection { *[Symbol.iterator]() { let i = 0; while(this[i] !== undefined) { yield this[i]; ++i; } } } let myCollection = new Collection(); myCollection[0] = 1; myCollection[1] = 2; for(let value of myCollection) { console.log(value); } // 1 // 2 ``` #### 12.7.9 Symbol.toPrimitive 对象的`Symbol.toPrimitive`属性,指向一个方法。该对象被转为原始类型的值时,会调用这个方法,返回该对象对应的原始类型值。 `Symbol.toPrimitive`被调用时,会接受一个字符串参数,表示当前运算的模式,一共有三种模式。 - Number:该场合需要转成数值 - String:该场合需要转成字符串 - Default:该场合可以转成数值,也可以转成字符串 ```js let obj = { [Symbol.toPrimitive](hint) { switch (hint) { case 'number': return 123; case 'string': return 'str'; case 'default': return 'default'; default: throw new Error(); } } }; 2 * obj // 246 3 + obj // '3default' obj == 'default' // true String(obj) // 'str' ``` #### 12.7.10 Symbol.toStringTag 对象的`Symbol.toStringTag`属性,指向一个方法。在该对象上面调用`Object.prototype.toString`方法时,如果这个属性存在,它的返回值会出现在`toString`方法返回的字符串之中,表示对象的类型。也就是说,这个属性可以用来定制`[object Object]`或`[object Array]`中`object`后面的那个字符串。 ```js // 例一 ({[Symbol.toStringTag]: 'Foo'}.toString()) // "[object Foo]" // 例二 class Collection { get [Symbol.toStringTag]() { return 'xxx'; } } let x = new Collection(); Object.prototype.toString.call(x) // "[object xxx]" ``` ES6 新增内置对象的`Symbol.toStringTag`属性值如下。 - `JSON[Symbol.toStringTag]`:’JSON’ - `Math[Symbol.toStringTag]`:’Math’ - Module 对象`M[Symbol.toStringTag]`:’Module’ - `ArrayBuffer.prototype[Symbol.toStringTag]`:’ArrayBuffer’ - `DataView.prototype[Symbol.toStringTag]`:’DataView’ - `Map.prototype[Symbol.toStringTag]`:’Map’ - `Promise.prototype[Symbol.toStringTag]`:’Promise’ - `Set.prototype[Symbol.toStringTag]`:’Set’ - `%TypedArray%.prototype[Symbol.toStringTag]`:’Uint8Array’等 - `WeakMap.prototype[Symbol.toStringTag]`:’WeakMap’ - `WeakSet.prototype[Symbol.toStringTag]`:’WeakSet’ - `%MapIteratorPrototype%[Symbol.toStringTag]`:’Map Iterator’ - `%SetIteratorPrototype%[Symbol.toStringTag]`:’Set Iterator’ - `%StringIteratorPrototype%[Symbol.toStringTag]`:’String Iterator’ - `Symbol.prototype[Symbol.toStringTag]`:’Symbol’ - `Generator.prototype[Symbol.toStringTag]`:’Generator’ - `GeneratorFunction.prototype[Symbol.toStringTag]`:’GeneratorFunction’ #### 12.7.11 Symbol.unscopables 对象的`Symbol.unscopables`属性,指向一个对象。该对象指定了使用`with`关键字时,哪些属性会被`with`环境排除。 ```js Array.prototype[Symbol.unscopables] // { // copyWithin: true, // entries: true, // fill: true, // find: true, // findIndex: true, // includes: true, // keys: true // } Object.keys(Array.prototype[Symbol.unscopables]) // ['copyWithin', 'entries', 'fill', 'find', 'findIndex', 'includes', 'keys'] // 没有 unscopables 时 class MyClass { foo() { return 1; } } var foo = function () { return 2; }; with (MyClass.prototype) { foo(); // 1 } // 有 unscopables 时 class MyClass { foo() { return 1; } get [Symbol.unscopables]() { return { foo: true }; } } var foo = function () { return 2; }; with (MyClass.prototype) { foo(); // 2 } ``` ## 13. Set和Map数据结构 ### 13.1 Set #### 13.1.1 基本用法 ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。 `Set`本身是一个构造函数,用来生成Set数据结构。 ```js const s = new Set(); [2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x)); for (let i of s) { console.log(i); } // 2 3 5 4 ``` 上面代码通过`add()`方法向 Set 结构加入成员,结果表明 Set 结构不会添加重复的值。 `Set`函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化。 ```js // 例一 const set = new Set([1, 2, 3, 4, 4]); [...set] // [1, 2, 3, 4] // 例二 const items = new Set([1, 2, 3, 4, 5, 5, 5, 5]); items.size // 5 // 例三 const set = new Set(document.querySelectorAll('div')); set.size // 56 // 类似于 const set = new Set(); document .querySelectorAll('div') .forEach(div => set.add(div)); set.size // 56 // 去除数组的重复成员 [...new Set(array)] [...new Set('ababbc')].join('') //"abc" ``` 向 Set 加入值的时候,不会发生类型转换,所以`5`和`"5"`是两个不同的值。Set 内部判断两个值是否不同,使用的算法叫做“Same-value-zero equality”,它类似于精确相等运算符(`===`),主要的区别是向 Set 加入值时认为`NaN`等于自身,而精确相等运算符认为`NaN`不等于自身。 ```js let set = new Set(); let a = NaN; let b = NaN; set.add(a); set.add(b); set // Set {NaN} ``` 另外,两个对象总是不相等的。 ```js let set = new Set(); set.add({}); set.size // 1 set.add({}); set.size // 2 //上面代码表示,由于两个空对象不相等,所以它们被视为两个值。 ``` #### 13.1.2 Set实例的属性和方法 Set结构的实例有以下属性 - `Set.prototype.constructor`:构造函数,默认就是`Set`函数。 - `Set.prototype.size`:返回`Set`实例的成员总数。 Set 实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员) - `Set.prototype.add(value)`:添加某个值,返回 Set 结构本身。 - `Set.prototype.delete(value)`:删除某个值,返回一个布尔值,表示删除是否成功。 - `Set.prototype.has(value)`:返回一个布尔值,表示该值是否为`Set`的成员。 - `Set.prototype.clear()`:清除所有成员,没有返回值。 ```js s.add(1).add(2).add(2); // 注意2被加入了两次 s.size // 2 s.has(1) // true s.has(2) // true s.has(3) // false s.delete(2); s.has(2) // false // 对象的写法 const properties = { 'width': 1, 'height': 1 }; if (properties[someName]) { // do something } // Set的写法 const properties = new Set(); properties.add('width'); properties.add('height'); if (properties.has(someName)) { // do something } ``` `Array.from`方法可以将 Set 结构转为数组。 ```js const items = new Set([1, 2, 3, 4, 5]); const array = Array.from(items); //去除数组重复成员的另一种方法 function dedupe(array) { return Array.from(new Set(array)); } dedupe([1, 1, 2, 3]) // [1, 2, 3] ``` #### 13.1.3 遍历操作 Set 结构的实例有四个遍历方法,可以用于遍历成员。 - `Set.prototype.keys()`:返回键名的遍历器 - `Set.prototype.values()`:返回键值的遍历器 - `Set.prototype.entries()`:返回键值对的遍历器 - `Set.prototype.forEach()`:使用回调函数遍历每个成员 需要特别指出的是,`Set`的遍历顺序就是插入顺序。这个特性有时非常有用,比如使用 Set 保存一个回调函数列表,调用时就能保证按照添加顺序调用。 **(1)`keys()`,`values()`,`entries()`** `keys`方法、`values`方法、`entries`方法返回的都是遍历器对象。由于 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以`keys`方法和`values`方法的行为完全一致。 ```js let set = new Set(['red', 'green', 'blue']); for (let item of set.keys()) { console.log(item); } // red // green // blue for (let item of set.values()) { console.log(item); } // red // green // blue for (let item of set.entries()) { console.log(item); } // ["red", "red"] // ["green", "green"] // ["blue", "blue"] ``` Set 结构的实例默认可遍历,它的默认遍历器生成函数就是它的`values`方法。 ```js Set.prototype[Symbol.iterator] === Set.prototype.values // true //直接用for...of循环遍历 Set let set = new Set(['red', 'green', 'blue']); for (let x of set) { console.log(x); } // red // green // blue ``` **(2)`forEach()`** Set 结构的实例与数组一样,也拥有`forEach`方法,用于对每个成员执行某种操作,没有返回值。 ```js let set = new Set([1, 4, 9]); set.forEach((value, key) => console.log(key + ' : ' + value)) // 1 : 1 // 4 : 4 // 9 : 9 ``` `forEach`方法的参数就是一个处理函数。该函数的参数与数组的`forEach`一致,依次为键值、键名、集合本身(上例省略了该参数)。 **(3)遍历的应用** 扩展运算符(`...`)内部使用`for...of`循环,所以也可以用于 Set 结构。 ```js let set = new Set(['red', 'green', 'blue']); let arr = [...set]; // ['red', 'green', 'blue'] ``` 扩展运算符和 Set 结构相结合,就可以去除数组的重复成员。 ```js let arr = [3, 5, 2, 2, 5, 5]; let unique = [...new Set(arr)]; // [3, 5, 2] ``` 而且,数组的`map`和`filter`方法也可以间接用于 Set 了。 ```js let set = new Set([1, 2, 3]); set = new Set([...set].map(x => x * 2)); // 返回Set结构:{2, 4, 6} let set = new Set([1, 2, 3, 4, 5]); set = new Set([...set].filter(x => (x % 2) == 0)); // 返回Set结构:{2, 4} ``` 因此使用 Set 可以很容易地实现并集(Union)、交集(Intersect)和差集(Difference)。 ```js let a = new Set([1, 2, 3]); let b = new Set([4, 3, 2]); // 并集 let union = new Set([...a, ...b]); // Set {1, 2, 3, 4} // 交集 let intersect = new Set([...a].filter(x => b.has(x))); // set {2, 3} // (a 相对于 b 的)差集 let difference = new Set([...a].filter(x => !b.has(x))); // Set {1} ``` 如果想在遍历操作中,同步改变原来的 Set 结构,目前没有直接的方法,但有两种变通方法。一种是利用原 Set 结构映射出一个新的结构,然后赋值给原来的 Set 结构;另一种是利用`Array.from`方法。 ```js // 方法一 let set = new Set([1, 2, 3]); set = new Set([...set].map(val => val * 2)); // set的值是2, 4, 6 // 方法二 let set = new Set([1, 2, 3]); set = new Set(Array.from(set, val => val * 2)); // set的值是2, 4, 6 ``` ### 13.2 WeakSet **含义** WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别。 首先,WeakSet 的成员只能是对象,而不能是其他类型的值。 ```js const ws = new WeakSet(); ws.add(1) // TypeError: Invalid value used in weak set ws.add(Symbol()) // TypeError: invalid value used in weak set ``` 上面代码试图向 WeakSet 添加一个数值和`Symbol`值,结果报错,因为 WeakSet 只能放置对象。 其次,WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。 这是因为垃圾回收机制依赖引用计数,如果一个值的引用次数不为`0`,垃圾回收机制就不会释放这块内存。结束使用该值之后,有时会忘记取消引用,导致内存无法释放,进而可能会引发内存泄漏。WeakSet 里面的引用,都不计入垃圾回收机制,所以就不存在这个问题。因此,WeakSet 适合临时存放一组对象,以及存放跟对象绑定的信息。只要这些对象在外部消失,它在 WeakSet 里面的引用就会自动消失。 由于上面这个特点,WeakSet 的成员是不适合引用的,因为它会随时消失。另外,由于 WeakSet 内部有多少个成员,取决于垃圾回收机制有没有运行,运行前后很可能成员个数是不一样的,而垃圾回收机制何时运行是不可预测的,因此 ES6 规定 WeakSet 不可遍历。 这些特点同样适用于本章后面要介绍的 WeakMap 结构。 语法 WeakSet 是一个构造函数,可以使用`new`命令,创建 WeakSet 数据结构。 ```js const ws = new WeakSet(); ``` 作为构造函数,WeakSet 可以接受一个数组或类似数组的对象作为参数。(实际上,任何具有 Iterable 接口的对象,都可以作为 WeakSet 的参数。)该数组的所有成员,都会自动成为 WeakSet 实例对象的成员。 ```js const a = [[1, 2], [3, 4]]; const ws = new WeakSet(a); // WeakSet {[1, 2], [3, 4]} ``` 注意,是`a`数组的成员成为 WeakSet 的成员,而不是`a`数组本身。这意味着,数组的成员只能是对象。 ```js const b = [3, 4]; const ws = new WeakSet(b); // Uncaught TypeError: Invalid value used in weak set(…) ``` WeakSet 结构有以下三个方法。 - **WeakSet.prototype.add(value)**:向 WeakSet 实例添加一个新成员。 - **WeakSet.prototype.delete(value)**:清除 WeakSet 实例的指定成员。 - **WeakSet.prototype.has(value)**:返回一个布尔值,表示某个值是否在 WeakSet 实例之中。 ```js const ws = new WeakSet(); const obj = {}; const foo = {}; ws.add(window); ws.add(obj); ws.has(window); // true ws.has(foo); // false ws.delete(window); ws.has(window); // false ``` WeakSet 没有`size`属性,没有办法遍历它的成员。 ```js ws.size // undefined ws.forEach // undefined ws.forEach(function(item){ console.log('WeakSet has ' + item)}) // TypeError: undefined is not a function ``` WeakSet 的一个用处,是储存 DOM 节点,而不用担心这些节点从文档移除时,会引发内存泄漏。 ```js const foos = new WeakSet() class Foo { constructor() { foos.add(this) } method () { if (!foos.has(this)) { throw new TypeError('Foo.prototype.method 只能在Foo的实例上调用!'); } } } ``` 上面代码保证了`Foo`的实例方法,只能在`Foo`的实例上调用。这里使用 WeakSet 的好处是,`foos`对实例的引用,不会被计入内存回收机制,所以删除实例的时候,不用考虑`foos`,也不会出现内存泄漏。 ### 13.3 Map #### 13.3.1 含义和基本用法 JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。 ```js const data = {}; const element = document.getElementById('myDiv'); data[element] = 'metadata'; data['[object HTMLDivElement]'] // "metadata" ``` 上面代码原意是将一个 DOM 节点作为对象`data`的键,但是由于对象只接受字符串作为键名,所以`element`被自动转为字符串`[object HTMLDivElement]`。 ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。 ```js const m = new Map(); const o = {p: 'Hello World'}; m.set(o, 'content') m.get(o) // "content" m.has(o) // true m.delete(o) // true m.has(o) // false ``` 上面代码使用 Map 结构的`set`方法,将对象`o`当作`m`的一个键,然后又使用`get`方法读取这个键,接着使用`delete`方法删除了这个键。 作为构造函数,Map 也可以接受一个数组作为参数。该数组的成员是一个个表示键值对的数组。 ```js const map = new Map([ ['name', '张三'], ['title', 'Author'] ]); map.size // 2 map.has('name') // true map.get('name') // "张三" map.has('title') // true map.get('title') // "Author" ``` `Map`构造函数接受数组作为参数,实际上执行的是下面的算法。 ```js const items = [ ['name', '张三'], ['title', 'Author'] ]; const map = new Map(); items.forEach( ([key, value]) => map.set(key, value) ); ``` 事实上,不仅仅是数组,任何具有 Iterator 接口、且每个成员都是一个双元素的数组的数据结构都可以当作`Map`构造函数的参数。这就是说,`Set`和`Map`都可以用来生成新的 Map。 ```js const set = new Set([ ['foo', 1], ['bar', 2] ]); const m1 = new Map(set); m1.get('foo') // 1 const m2 = new Map([['baz', 3]]); const m3 = new Map(m2); m3.get('baz') // 3 ``` 如果对同一个键多次赋值,后面的值将覆盖前面的值。 ```js const map = new Map(); map .set(1, 'aaa') .set(1, 'bbb'); map.get(1) // "bbb" //如果读取一个未知的键,则返回undefined。 ``` > 注意,只有对同一个对象的引用,Map 结构才将其视为同一个键。这一点要非常小心。 > > ```js > const map = new Map(); > map.set(['a'], 555); > map.get(['a']) // undefined > ``` > > 上面代码的`set`和`get`方法,表面是针对同一个键,但实际上这是两个不同的数组实例,内存地址是不一样的,因此`get`方法无法读取该键,返回`undefined`。 同理,同样的值的两个实例,在 Map 结构中被视为两个键。 ```js const map = new Map(); const k1 = ['a']; const k2 = ['a']; map .set(k1, 111) .set(k2, 222); map.get(k1) // 111 map.get(k2) // 222 ``` 上面代码中,变量`k1`和`k2`的值是一样的,但是它们在 Map 结构中被视为两个键。 由上可知,Map 的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键。这就解决了同名属性碰撞(clash)的问题,我们扩展别人的库的时候,如果使用对象作为键名,就不用担心自己的属性与原作者的属性同名。 如果 Map 的键是一个简单类型的值(数字、字符串、布尔值),则只要两个值严格相等,Map 将其视为一个键,比如`0`和`-0`就是一个键,布尔值`true`和字符串`true`则是两个不同的键。另外,`undefined`和`null`也是两个不同的键。虽然`NaN`不严格相等于自身,但 Map 将其视为同一个键。 ```js let map = new Map(); map.set(-0, 123); map.get(+0) // 123 map.set(true, 1); map.set('true', 2); map.get(true) // 1 map.set(undefined, 3); map.set(null, 4); map.get(undefined) // 3 map.set(NaN, 123); map.get(NaN) // 123 ``` #### 13.3.2 实例的属性和操作方法 **(1)size属性** `size`属性返回 Map 结构的成员总数。 ```js const map = new Map(); map.set('foo', true); map.set('bar', false); map.size // 2 ``` **(2)Map.prototype.set(key, value)** `set`方法设置键名`key`对应的键值为`value`,然后返回整个 Map 结构。如果`key`已经有值,则键值会被更新,否则就新生成该键。 ```js const m = new Map(); m.set('edition', 6) // 键是字符串 m.set(262, 'standard') // 键是数值 m.set(undefined, 'nah') // 键是 undefined //set方法返回的是当前的Map对象,因此可以采用链式写法。 let map = new Map() .set(1, 'a') .set(2, 'b') .set(3, 'c'); ``` **(3)Map.prototype.get(key)** `get`方法读取`key`对应的键值,如果找不到`key`,返回`undefined`。 ```js const m = new Map(); const hello = function() {console.log('hello');}; m.set(hello, 'Hello ES6!') // 键是函数 m.get(hello) // Hello ES6! ``` **(4)Map.prototype.has(key)** `has`方法返回一个布尔值,表示某个键是否在当前 Map 对象之中。 ```js const m = new Map(); m.set('edition', 6); m.set(262, 'standard'); m.set(undefined, 'nah'); m.has('edition') // true m.has('years') // false m.has(262) // true m.has(undefined) // true ``` **(5)Map.prototype.delete(key)** `delete`方法删除某个键,返回`true`。如果删除失败,返回`false`。 ```js const m = new Map(); m.set(undefined, 'nah'); m.has(undefined) // true m.delete(undefined) m.has(undefined) // false ``` **(6)Map.prototype.clear()** `clear`方法清除所有成员,没有返回值。 ```js let map = new Map(); map.set('foo', true); map.set('bar', false); map.size // 2 map.clear() map.size // 0 ``` #### 13.3.3 遍历方法 Map 结构原生提供三个遍历器生成函数和一个遍历方法。 - `Map.prototype.keys()`:返回键名的遍历器。 - `Map.prototype.values()`:返回键值的遍历器。 - `Map.prototype.entries()`:返回所有成员的遍历器。 - `Map.prototype.forEach()`:遍历 Map 的所有成员。 需要特别注意的是,Map 的遍历顺序就是插入顺序。 ```js const map = new Map([ ['F', 'no'], ['T', 'yes'], ]); for (let key of map.keys()) { console.log(key); } // "F" // "T" for (let value of map.values()) { console.log(value); } // "no" // "yes" for (let item of map.entries()) { console.log(item[0], item[1]); } // "F" "no" // "T" "yes" // 或者 for (let [key, value] of map.entries()) { console.log(key, value); } // "F" "no" // "T" "yes" // 等同于使用map.entries() for (let [key, value] of map) { console.log(key, value); } // "F" "no" // "T" "yes" //表示 Map 结构的默认遍历器接口(Symbol.iterator属性),就是entries方法。 map[Symbol.iterator] === map.entries // true ``` Map 结构转为数组结构,比较快速的方法是使用扩展运算符(`...`)。 ```js const map = new Map([ [1, 'one'], [2, 'two'], [3, 'three'], ]); [...map.keys()] // [1, 2, 3] [...map.values()] // ['one', 'two', 'three'] [...map.entries()] // [[1,'one'], [2, 'two'], [3, 'three']] [...map] // [[1,'one'], [2, 'two'], [3, 'three']] ``` 结合数组的`map`方法、`filter`方法,可以实现 Map 的遍历和过滤(Map 本身没有`map`和`filter`方法)。 ```js const map0 = new Map() .set(1, 'a') .set(2, 'b') .set(3, 'c'); const map1 = new Map( [...map0].filter(([k, v]) => k < 3) ); // 产生 Map 结构 {1 => 'a', 2 => 'b'} const map2 = new Map( [...map0].map(([k, v]) => [k * 2, '_' + v]) ); // 产生 Map 结构 {2 => '_a', 4 => '_b', 6 => '_c'} ``` 此外,Map 还有一个`forEach`方法,与数组的`forEach`方法类似,也可以实现遍历。 ```js map.forEach(function(value, key, map) { console.log("Key: %s, Value: %s", key, value); }); ``` `forEach`方法还可以接受第二个参数,用来绑定`this`。 ```js const reporter = { report: function(key, value) { console.log("Key: %s, Value: %s", key, value); } }; map.forEach(function(value, key, map) { this.report(key, value); }, reporter); ``` #### 13.3.4 与其他数组结构的相互转化 **(1)Map 转为数组** 前面已经提过,Map 转为数组最方便的方法,就是使用扩展运算符(`...`)。 ```js const myMap = new Map() .set(true, 7) .set({foo: 3}, ['abc']); [...myMap] // [ [ true, 7 ], [ { foo: 3 }, [ 'abc' ] ] ] ``` **(2)数组 转为 Map** 将数组传入 Map 构造函数,就可以转为 Map。 ```js new Map([ [true, 7], [{foo: 3}, ['abc']] ]) // Map { // true => 7, // Object {foo: 3} => ['abc'] // } ``` **(3)Map 转为对象** 如果所有 Map 的键都是字符串,它可以无损地转为对象。 ```js function strMapToObj(strMap) { let obj = Object.create(null); for (let [k,v] of strMap) { obj[k] = v; } return obj; } const myMap = new Map() .set('yes', true) .set('no', false); strMapToObj(myMap) // { yes: true, no: false } ``` 如果有非字符串的键名,那么这个键名会被转成字符串,再作为对象的键名。 **(4)对象转为 Map** 对象转为 Map 可以通过`Object.entries()`。 ```js let obj = {"a":1, "b":2}; let map = new Map(Object.entries(obj)); ``` 可以自己实现一个转换函数。 ```js function objToStrMap(obj) { let strMap = new Map(); for (let k of Object.keys(obj)) { strMap.set(k, obj[k]); } return strMap; } objToStrMap({yes: true, no: false}) // Map {"yes" => true, "no" => false} ``` **(5)Map 转为 JSON** Map 转为 JSON 要区分两种情况。一种情况是,Map 的键名都是字符串,这时可以选择转为对象 JSON。 ```js function strMapToJson(strMap) { return JSON.stringify(strMapToObj(strMap)); } let myMap = new Map().set('yes', true).set('no', false); strMapToJson(myMap) // '{"yes":true,"no":false}' ``` 另一种情况是,Map 的键名有非字符串,这时可以选择转为数组 JSON。 ```js function mapToArrayJson(map) { return JSON.stringify([...map]); } let myMap = new Map().set(true, 7).set({foo: 3}, ['abc']); mapToArrayJson(myMap) // '[[true,7],[{"foo":3},["abc"]]]' ``` **(6)JSON 转为 Map** JSON 转为 Map,正常情况下,所有键名都是字符串。 ```js function jsonToStrMap(jsonStr) { return objToStrMap(JSON.parse(jsonStr)); } jsonToStrMap('{"yes": true, "no": false}') // Map {'yes' => true, 'no' => false} ``` 但是,有一种特殊情况,整个 JSON 就是一个数组,且每个数组成员本身,又是一个有两个成员的数组。这时,它可以一一对应地转为 Map。这往往是 Map 转为数组 JSON 的逆操作。 ```js function jsonToMap(jsonStr) { return new Map(JSON.parse(jsonStr)); } jsonToMap('[[true,7],[{"foo":3},["abc"]]]') // Map {true => 7, Object {foo: 3} => ['abc']} ``` ### 13.4 WeakMap `WeakMap`结构与`Map`结构类似,也是用于生成键值对的集合。 ```js // WeakMap 可以使用 set 方法添加成员 const wm1 = new WeakMap(); const key = {foo: 1}; wm1.set(key, 2); wm1.get(key) // 2 // WeakMap 也可以接受一个数组, // 作为构造函数的参数 const k1 = [1, 2, 3]; const k2 = [4, 5, 6]; const wm2 = new WeakMap([[k1, 'foo'], [k2, 'bar']]); wm2.get(k2) // "bar" ``` `WeakMap`与`Map`的区别有两点。 `WeakMap`只接受对象作为键名(`null`除外),不接受其他类型的值作为键名。 ```js 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 ``` 上面代码中,如果将数值`1`和`Symbol`值作为 WeakMap 的键名,都会报错。 其次,`WeakMap`的键名所指向的对象,不计入垃圾回收机制。 `WeakMap`的设计目的在于,有时我们想在某个对象上面存放一些数据,但是这会形成对于这个对象的引用。请看下面的例子。 ```js const e1 = document.getElementById('foo'); const e2 = document.getElementById('bar'); const arr = [ [e1, 'foo 元素'], [e2, 'bar 元素'], ]; ``` 上面代码中,`e1`和`e2`是两个对象,我们通过`arr`数组对这两个对象添加一些文字说明。这就形成了`arr`对`e1`和`e2`的引用。 一旦不再需要这两个对象,我们就必须手动删除这个引用,否则垃圾回收机制就不会释放`e1`和`e2`占用的内存。 ```js // 不需要 e1 和 e2 的时候 // 必须手动删除引用 arr [0] = null; arr [1] = null; ``` WeakMap 就是为了解决这个问题而诞生的,它的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。 基本上,如果你要往对象上添加数据,又不想干扰垃圾回收机制,就可以使用 WeakMap。一个典型应用场景是,在网页的 DOM 元素上添加数据,就可以使用`WeakMap`结构。当该 DOM 元素被清除,其所对应的`WeakMap`记录就会自动被移除。 ```js const wm = new WeakMap(); const element = document.getElementById('example'); wm.set(element, 'some information'); wm.get(element) // "some information" ``` 注意,WeakMap 弱引用的只是键名,而不是键值。键值依然是正常引用。 ```js const wm = new WeakMap(); let key = {}; let obj = {foo: 1}; wm.set(key, obj); obj = null; wm.get(key) // Object {foo: 1} ``` ## 14.Proxy ### 14.1 概述 Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。 Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。 ```js var obj = new Proxy({}, { get: function (target, propKey, receiver) { console.log(`getting ${propKey}!`); return Reflect.get(target, propKey, receiver); }, set: function (target, propKey, value, receiver) { console.log(`setting ${propKey}!`); return Reflect.set(target, propKey, value, receiver); } }); obj.count=1 //setting count! ++obj.count //getting count! //setting count! //2 ``` > Proxy实际上重载(overload)了点运算符,即用自己的定义覆盖了语言的原始定义。 ES6原生提供`Proxy`构造函数,用来生成`Proxy`实例。 ```js let proxy=new Proxy(target,handler); ``` Proxy 对象的所有用法,都是上面这种形式,不同的只是`handler`参数的写法。其中,`new Proxy()`表示生成一个`Proxy`实例,`target`参数表示所要拦截的目标对象,`handler`参数也是一个对象,用来定制拦截行为。 拦截读取属性行为的例子。 ```js let proxy = new Proxy({}, { get: function(target, propKey) { return 35; } }); proxy.time // 35 proxy.name // 35 proxy.title // 35 ``` 上面代码中,作为构造函数,`Proxy`接受两个参数。第一个参数是所要代理的目标对象(上例是一个空对象),即如果没有`Proxy`的介入,操作原来要访问的就是这个对象;第二个参数是一个配置对象,对于每一个被代理的操作,需要提供一个对应的处理函数,该函数将拦截对应的操作。 配置对象有一个`get`方法,用来拦截对目标对象属性的访问请求。`get`方法的两个参数分别是目标对象和所要访问的属性。可以看到,由于拦截函数总是返回`35`,所以访问任何属性都得到`35`。 注意,要使得`Proxy`起作用,必须针对`Proxy`实例(上例是`proxy`对象)进行操作,而不是针对目标对象(上例是空对象)进行操作。 如果`handler`没有设置任何拦截,那就等同于直接通向原对象。 ```js let target={}, handler={}, proxy=new Proxy(target,handler); proxy.a='b'; target.a;//"b" ``` 上面代码中,`handler`是一个空对象,没有任何拦截效果,访问`proxy`就等同于访问`target`。 一个技巧是将 Proxy 对象,设置到`object.proxy`属性,从而可以在`object`对象上调用。 ```js var object = { proxy: new Proxy(target, handler) }; ``` Proxy 实例也可以作为其他对象的原型对象。 ```js var proxy = new Proxy({}, { get: function(target, propKey) { return 35; } }); let obj = Object.create(proxy); obj.time // 35 ``` 上面代码中,`proxy`对象是`obj`对象的原型,`obj`对象本身并没有`time`属性,所以根据原型链,会在`proxy`对象上读取该属性,导致被拦截。 同一个拦截器函数,可以设置拦截多个操作。 ```js let handler={ get:function(target, name){ if(name==='prototype'){ return Object.prototype; } return 'Hello, '+name; }, apply:function(target,thisBinding,args){ return args[0]; }, construct:function(target,args){ return {value:args[1]}; } } let fproxy=new Proxy(function(x,y){ return x+y; },handler); fproxy(1,2) //1 new fproxy(1,2) //{value: 2} fproxy.prototype===Object.prototype //true fproxy.foo==="Hello, foo"//true ``` 对于可以设置、但没有设置拦截的操作,则直接落在目标对象上,按照原先的方式产生结果。 Proxy 支持的拦截操作一览,一共 13 种。 - **get(target, propKey, receiver)**:拦截对象属性的读取,比如`proxy.foo`和`proxy['foo']`。 - **set(target, propKey, value, receiver)**:拦截对象属性的设置,比如`proxy.foo = v`或`proxy['foo'] = v`,返回一个布尔值。 - **has(target, propKey)**:拦截`propKey in proxy`的操作,返回一个布尔值。 - **deleteProperty(target, propKey)**:拦截`delete proxy[propKey]`的操作,返回一个布尔值。 - **ownKeys(target)**:拦截`Object.getOwnPropertyNames(proxy)`、`Object.getOwnPropertySymbols(proxy)`、`Object.keys(proxy)`、`for...in`循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而`Object.keys()`的返回结果仅包括目标对象自身的可遍历属性。 - **getOwnPropertyDescriptor(target, propKey)**:拦截`Object.getOwnPropertyDescriptor(proxy, propKey)`,返回属性的描述对象。 - **defineProperty(target, propKey, propDesc)**:拦截`Object.defineProperty(proxy, propKey, propDesc)`、`Object.defineProperties(proxy, propDescs)`,返回一个布尔值。 - **preventExtensions(target)**:拦截`Object.preventExtensions(proxy)`,返回一个布尔值。 - **getPrototypeOf(target)**:拦截`Object.getPrototypeOf(proxy)`,返回一个对象。 - **isExtensible(target)**:拦截`Object.isExtensible(proxy)`,返回一个布尔值。 - **setPrototypeOf(target, proto)**:拦截`Object.setPrototypeOf(proxy, proto)`,返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。 - **apply(target, object, args)**:拦截 Proxy 实例作为函数调用的操作,比如`proxy(...args)`、`proxy.call(object, ...args)`、`proxy.apply(...)`。 - **construct(target, args)**:拦截 Proxy 实例作为构造函数调用的操作,比如`new proxy(...args)`。 ### 14.2 Proxy实例的方法 #### 14.2.1 get() `get`方法用于拦截某个属性的读取操作,可以接受三个参数,依次为目标对象、属性名和 proxy 实例本身(严格地说,是操作行为所针对的对象),其中最后一个参数可选。 ```js let person={ name:"张三" }; let proxy=new Proxy(person,{ get:function(target,propKey){ if(propKey in target){ return target[propKey]; }else{ throw new ReferenceError("Prop name \"" + propKey + "\" does not exist."); } } }); proxy.name // "张三" proxy.age // 抛出一个错误 ``` 上面代码表示,如果访问目标对象不存在的属性,会抛出一个错误。如果没有这个拦截函数,访问不存在的属性,只会返回`undefined`。 `get`方法可以继承。 ```js let proto = new Proxy({}, { get(target, propertyKey, receiver) { console.log('GET ' + propertyKey); return target[propertyKey]; } }); let obj = Object.create(proto); obj.foo // "GET foo" ``` 上面代码中,拦截操作定义在`Prototype`对象上面,所以如果读取`obj`对象继承的属性时,拦截会生效。 使用`get`拦截,实现数组读取负数的索引。 ```js function createArray(...elements) { let handler = { get(target, propKey, receiver) { let index = Number(propKey); if (index < 0) { propKey = String(target.length + index); } return Reflect.get(target, propKey, receiver); } }; let target = []; target.push(...elements); return new Proxy(target, handler); } let arr = createArray('a', 'b', 'c'); arr[-1] // c arr[0] //a ``` 利用 Proxy,可以将读取属性的操作(`get`),转变为执行某个函数,从而实现属性的链式操作。 ```js //链式操作 let pipe = function (value) { let funcStack = []; let oproxy = new Proxy({} , { get : function (pipeObject, fnName) { if (fnName === 'get') { return funcStack.reduce(function (val, fn) { return fn(val); },value); } funcStack.push(window[fnName]); return oproxy; } }); return oproxy; } let double = n => n * 2; let pow = n => n * n; let reverseInt = n => n.toString().split("").reverse().join("") | 0; pipe(3).double.pow.reverseInt.get; // 63 ``` 利用`get`拦截,实现一个生成各种 DOM 节点的通用函数`dom`。 ```js const dom = new Proxy({}, { get(target, property) { return function(attrs = {}, ...children) { const el = document.createElement(property); for (let prop of Object.keys(attrs)) { el.setAttribute(prop, attrs[prop]); } for (let child of children) { if (typeof child === 'string') { child = document.createTextNode(child); } el.appendChild(child); } return el; } } }); const el = dom.div({}, 'Hello, my name is ', dom.a({href: '//example.com'}, 'Mark'), '. I like:', dom.ul({}, dom.li({}, 'The web'), dom.li({}, 'Food'), dom.li({}, '…actually that\'s it') ) ); document.body.appendChild(el); ``` `get`方法的第三个参数的例子,它总是指向原始的读操作所在的那个对象,一般情况下就是 Proxy 实例。 ```js const proxy = new Proxy({}, { get: function(target, key, receiver) { return receiver; } }); proxy.getReceiver === proxy // true] const proxy = new Proxy({}, { get: function(target, key, receiver) { return receiver; } }); const d = Object.create(proxy); d.a === d // true ``` 如果一个属性不可配置(configurable)且不可写(writable),则 Proxy 不能修改该属性,否则通过 Proxy 对象访问该属性会报错。 ```js const target = Object.defineProperties({}, { foo: { value: 123, writable: false, configurable: false }, }); const handler = { get(target, propKey) { return 'abc'; } }; const proxy = new Proxy(target, handler); proxy.foo // TypeError: Invariant check failed ``` #### 14.2.1 set() `set`方法用来拦截某个属性的赋值操作,可以接受四个参数,依次为目标对象、属性名、属性值和 Proxy 实例本身,其中最后一个参数可选。 ```js let validator={ set:function(obj,prop,value){ if(prop==='age'){ if(!Number.isInteger(value)){ throw new TypeError("The age is not an integer"); } if(value<0||value>200){ throw new TypeError("The age seems invalid"); } } obj[prop]=value; } } let person=new Proxy({},validator); person.age=100; person.age="young"; person.age=0; ```