ES6-1
let 命令
|
1 2 3 4 5 6 7 8 |
var a = []; for (var i = 0; i < 10; i++) { a[i] = function () { console.log(i); }; } a[6](); // 10 |
上面代码中,变量i是var命令声明的,在全局范围内都有效,所以全局只有一个变量i。每一次循环,变量i的值都会发生改变,而循环内被赋给数组a的函数内部的console.log(i),里面的i指向的就是全局的i。也就是说,所有数组a的成员里面的i,指向的都是同一个i,导致运行时输出的是最后一轮的i的值,也就是 10。
如果使用let,声明的变量仅在块级作用域内有效,最后输出的是 6。
|
1 2 3 4 5 6 7 8 |
var a = []; for (let i = 0; i < 10; i++) { a[i] = function () { console.log(i); }; } a[6](); // 6 |
上面代码中,变量i是let声明的,当前的i只在本轮循环有效,所以每一次循环的i其实都是一个新的变量,所以最后输出的是6。你可能会问,如果每一轮循环的变量i都是重新声明的,那它怎么知道上一轮循环的值,从而计算出本轮循环的值?这是因为 JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。
另外,for循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。
|
1 2 3 4 5 6 7 8 |
for (let i = 0; i < 3; i++) { let i = 'abc'; console.log(i); } // abc // abc // abc |
上面代码正确运行,输出了 3 次abc。这表明函数内部的变量i与循环变量i不在同一个作用域,有各自单独的作用域。【但是如果在循环体内修改i的数值,也会生效(如可以跳过几个数),很奇怪】
不存在变量提升
暂时性死区【重要】
只要块级作用域内存在let命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。
|
1 2 3 4 5 6 7 |
var tmp = 123; if (true) { tmp = 'abc'; // ReferenceError let tmp; } |
ES6 明确规定,如果区块中存在let和const命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。
总之,在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。
“暂时性死区”也意味着typeof不再是一个百分之百安全的操作。
|
1 2 3 |
typeof x; // ReferenceError let x; |
上面代码中,变量x使用let命令声明,所以在声明之前,都属于x的“死区”,只要用到该变量就会报错。因此,typeof运行时就会抛出一个ReferenceError。
作为比较,如果一个变量根本没有被声明,使用typeof反而不会报错。
|
1 2 |
typeof undeclared_variable // "undefined" |
ES6 规定暂时性死区和let、const语句不出现变量提升,主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。这样的错误在 ES5 是很常见的,现在有了这种规定,避免此类错误就很容易了。
总之,暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。
不允许重复声明
【同一层括号内】
块级作用域【和C一样】
为什么需要块级作用域?
ES5 只有全局作用域和函数作用域,没有块级作用域,这带来很多不合理的场景。
第一种场景,内层变量可能会覆盖外层变量。
|
1 2 3 4 5 6 7 8 9 10 11 |
var tmp = new Date(); function f() { console.log(tmp); if (false) { var tmp = 'hello world'; } } f(); // undefined |
上面代码的原意是,if代码块的外部使用外层的tmp变量,内部使用内层的tmp变量。但是,函数f执行后,输出结果为undefined,原因在于变量提升,导致内层的tmp变量覆盖了外层的tmp变量。【声明提升,定义没有提升】
考虑到环境导致的行为差异太大,应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句。【重要】
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// 函数声明语句 { let a = 'secret'; function f() { return a; } } // 函数表达式 { let a = 'secret'; let f = function () { return a; }; } |
const 命令
const的作用域与let命令相同:只在声明所在的块级作用域内有效。
const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址不得改动。
因此,将一个对象声明为常量必须非常小心。
|
1 2 3 4 5 6 7 8 9 |
const foo = {}; // 为 foo 添加一个属性,可以成功 foo.prop = 123; foo.prop // 123 // 将 foo 指向另一个对象,就会报错 foo = {}; // TypeError: "foo" is read-only |
|
1 2 3 4 5 |
const a = []; a.push('Hello'); // 可执行 a.length = 0; // 可执行 a = ['Dave']; // 报错 |
如果真的想将对象冻结,应该使用Object.freeze方法。
|
1 2 3 4 5 6 |
const foo = Object.freeze({}); // 常规模式时,下面一行不起作用; // 严格模式时,该行会报错 foo.prop = 123; |
上面代码中,常量foo指向一个冻结的对象,所以添加新属性不起作用,严格模式时还会报错。
除了将对象本身冻结,对象的属性也应该冻结。下面是一个将对象彻底冻结的函数。【⭐️】
|
1 2 3 4 5 6 7 8 9 |
var constantize = (obj) => { Object.freeze(obj); Object.keys(obj).forEach( (key, i) => { if ( typeof obj[key] === 'object' ) { constantize( obj[key] ); } }); }; |
ES6 声明变量的六种方法
顶层对象的属性
顶层对象,在浏览器环境指的是window对象,在 Node 指的是global对象。ES5 之中,顶层对象的属性与全局变量是等价的。
|
1 2 3 4 5 6 |
window.a = 1; a // 1 a = 2; window.a // 2 |
上面代码中,顶层对象的属性赋值与全局变量的赋值,是同一件事。
ES6 为了改变这一点,一方面规定,为了保持兼容性,var命令和function命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。也就是说,从 ES6 开始,全局变量将逐步与顶层对象的属性脱钩。
|
1 2 3 4 5 6 7 8 |
var a = 1; // 如果在 Node 的 REPL 环境,可以写成 global.a // 或者采用通用方法,写成 this.a window.a // 1 let b = 1; window.b // undefined |
很难找到一种方法,可以在所有情况下,都取到顶层对象。下面是两种勉强可以使用的方法。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<span class="hljs-comment">// 方法一</span> (<span class="hljs-keyword">typeof</span> <span class="hljs-built_in">window</span> !== <span class="hljs-string">'undefined'</span> ? <span class="hljs-built_in">window</span> : (<span class="hljs-keyword">typeof</span> process === <span class="hljs-string">'object'</span> && <span class="hljs-keyword">typeof</span> <span class="hljs-built_in">require</span> === <span class="hljs-string">'function'</span> && <span class="hljs-keyword">typeof</span> global === <span class="hljs-string">'object'</span>) ? global : <span class="hljs-keyword">this</span>); <span class="hljs-comment">// 方法二</span> <span class="hljs-keyword">var</span> getGlobal = <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-params">()</span> </span>{ <span class="hljs-keyword">if</span> (<span class="hljs-keyword">typeof</span> self !== <span class="hljs-string">'undefined'</span>) { <span class="hljs-keyword">return</span> self; } <span class="hljs-keyword">if</span> (<span class="hljs-keyword">typeof</span> <span class="hljs-built_in">window</span> !== <span class="hljs-string">'undefined'</span>) { <span class="hljs-keyword">return</span> <span class="hljs-built_in">window</span>; } <span class="hljs-keyword">if</span> (<span class="hljs-keyword">typeof</span> global !== <span class="hljs-string">'undefined'</span>) { <span class="hljs-keyword">return</span> global; } <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'unable to locate global object'</span>); }; |
现在有一个提案,在语言标准的层面,引入global作为顶层对象。也就是说,在所有环境下,global都是存在的,都可以从它拿到顶层对象。
垫片库system.global模拟了这个提案,可以在所有环境拿到global。
|
1 2 3 4 5 6 |
// CommonJS 的写法 require('system.global/shim')(); // ES6 模块的写法 import shim from 'system.global/shim'; shim(); |
上面代码可以保证各种环境里面,global对象都是存在的。
|
1 2 3 4 5 6 7 |
// CommonJS 的写法 var global = require('system.global')(); // ES6 模块的写法 import getGlobal from 'system.global'; const global = getGlobal(); |
上面代码将顶层对象放入变量global。
数组的解构赋值
ES6 允许写成下面这样。
|
1 2 |
let [a, b, c] = [1, 2, 3]; |
|
1 2 3 4 5 6 7 8 9 |
let [head, ...tail] = [1, 2, 3, 4]; head // 1 tail // [2, 3, 4] let [x, y, ...z] = ['a']; x // "a" y // undefined z // [] |
对于 Set 结构,也可以使用数组的解构赋值。
|
1 2 3 |
let [x, y, z] = new Set(['a', 'b', 'c']); x // "a" |
事实上,只要某种数据结构具有 Iterator 接口,都可以采用数组形式的解构赋值。
|
1 2 3 4 5 6 7 8 9 10 11 12 |
<span class="hljs-function"><span class="hljs-keyword">function</span>* <span class="hljs-title">fibs</span><span class="hljs-params">()</span> </span>{ <span class="hljs-keyword">let</span> a = <span class="hljs-number">0</span>; <span class="hljs-keyword">let</span> b = <span class="hljs-number">1</span>; <span class="hljs-keyword">while</span> (<span class="hljs-literal">true</span>) { <span class="hljs-keyword">yield</span> a; [a, b] = [b, a + b]; } } <span class="hljs-keyword">let</span> [first, second, third, fourth, fifth, sixth] = fibs(); sixth <span class="hljs-comment">// 5</span> |
上面代码中,fibs是一个 Generator 函数(参见《Generator 函数》一章),原生具有 Iterator 接口。解构赋值会依次从这个接口获取值。
默认值
解构赋值允许指定默认值。
|
1 2 3 4 5 6 |
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,默认值才会生效。
|
1 2 3 4 5 6 |
let [x = 1] = [undefined]; x // 1 let [x = 1] = [null]; x // null |
上面代码中,如果一个数组成员是null,默认值就不会生效,因为null不严格等于undefined。
|
1 2 3 4 5 6 |
function f() { return 'aaa'; } let [x = f()] = [1]; |
上面代码中,因为x能取到值,所以函数f根本不会执行。
对象的解构赋值
解构不仅可以用于数组,还可以用于对象。
|
1 2 3 4 |
let { foo, bar } = { foo: "aaa", bar: "bbb" }; foo // "aaa" bar // "bbb" |
对象的属性没有次序,变量必须与属性同名,才能取到正确的值。
|
1 2 3 4 5 6 7 |
<span class="hljs-keyword">let</span> { bar, foo } = { foo: <span class="hljs-string">"aaa"</span>, bar: <span class="hljs-string">"bbb"</span> }; foo <span class="hljs-comment">// "aaa"</span> bar <span class="hljs-comment">// "bbb"</span> <span class="hljs-keyword">let</span> { baz } = { foo: <span class="hljs-string">"aaa"</span>, bar: <span class="hljs-string">"bbb"</span> }; baz <span class="hljs-comment">// undefined</span> |
这实际上说明,对象的解构赋值是下面形式的简写(参见《对象的扩展》一章)。
|
1 2 |
let { foo: foo, bar: bar } = { foo: "aaa", bar: "bbb" }; |
也就是说,对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。
如果变量名与属性名不一致,必须写成下面这样。【也就是要额外声明变量和key对应关系】
|
1 2 3 4 5 6 7 8 |
<span class="hljs-keyword">let</span> { foo: baz } = { foo: <span class="hljs-string">'aaa'</span>, bar: <span class="hljs-string">'bbb'</span> }; baz <span class="hljs-comment">// "aaa"</span> <span class="hljs-keyword">let</span> obj = { first: <span class="hljs-string">'hello'</span>, last: <span class="hljs-string">'world'</span> }; <span class="hljs-keyword">let</span> { first: f, last: l } = obj; f <span class="hljs-comment">// 'hello'</span> l <span class="hljs-comment">// 'world'</span> |
对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。
【可以赋值多次】
|
1 2 3 4 5 6 7 8 9 10 11 12 |
<span class="hljs-keyword">let</span> obj = { p: [ <span class="hljs-string">'Hello'</span>, { y: <span class="hljs-string">'World'</span> } ] }; <span class="hljs-keyword">let</span> { p, p: [x, { y }] } = obj; x <span class="hljs-comment">// "Hello"</span> y <span class="hljs-comment">// "World"</span> p <span class="hljs-comment">// ["Hello", {y: "World"}]</span> |
|
1 2 3 4 5 6 7 8 |
<span class="hljs-keyword">let</span> obj = {}; <span class="hljs-keyword">let</span> arr = []; ({ foo: obj.prop, bar: arr[<span class="hljs-number">0</span>] } = { foo: <span class="hljs-number">123</span>, bar: <span class="hljs-literal">true</span> }); obj <span class="hljs-comment">// {prop:123}</span> arr <span class="hljs-comment">// [true]</span> |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
var {x = 3} = {}; x // 3 var {x, y = 5} = {x: 1}; x // 1 y // 5 var {x: y = 3} = {}; y // 3 var {x: y = 3} = {x: 5}; y // 5 y的默认值是3,如果右侧有x的key则用右侧的数据赋值 |
|
1 2 3 4 5 |
// 错误的写法 let x; {x} = {x: 1}; // SyntaxError: syntax error |
上面代码的写法会报错,因为 JavaScript 引擎会将{x}理解成一个代码块,从而发生语法错误。只有不将大括号写在行首,避免 JavaScript 将其解释为代码块,才能解决这个问题。
|
1 2 3 4 |
// 正确的写法 let x; ({x} = {x: 1}); |
字符串的解构赋值
字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象。
|
1 2 3 4 5 6 7 |
const [a, b, c, d, e] = 'hello'; a // "h" b // "e" c // "l" d // "l" e // "o" |
类似数组的对象都有一个length属性,因此还可以对这个属性解构赋值。
|
1 2 3 |
let {length : len} = 'hello'; len // 5 |
函数参数的解构也可以使用默认值。
|
1 2 3 4 5 6 7 8 9 |
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] |
注意,下面的写法会得到不一样的结果。
|
1 2 3 4 5 6 7 8 9 |
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] |
函数参数的定义
解构赋值可以方便地将一组参数与变量名对应起来。
|
1 2 3 4 5 6 7 8 |
// 参数是一组有次序的值 function f([x, y, z]) { ... } f([1, 2, 3]); // 参数是一组无次序的值 function f({x, y, z}) { ... } f({z: 3, y: 2, x: 1}); |
提取 JSON 数据
解构赋值对提取 JSON 对象中的数据,尤其有用。
|
1 2 3 4 5 6 7 8 9 10 |
let jsonData = { id: 42, status: "OK", data: [867, 5309] }; let { id, status, data: number } = jsonData; console.log(id, status, number); |
遍历 Map 结构
任何部署了 Iterator 接口的对象,都可以用for...of循环遍历。Map 结构原生支持 Iterator 接口,配合变量的解构赋值,获取键名和键值就非常方便。
|
1 2 3 4 5 6 7 8 9 10 |
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 |
如果只想获取键名,或者只想获取键值,可以写成下面这样。
|
1 2 3 4 5 6 7 8 9 10 |
// 获取键名 for (let [key] of map) { // ... } // 获取键值 for (let [,value] of map) { // ... } |
输入模块的指定方法
加载模块时,往往需要指定输入哪些方法。解构赋值使得输入语句非常清晰。
|
1 2 |
const { SourceMapConsumer, SourceNode } = require("source-map"); |
字符串的扩展
ES6 对这一点做出了改进,只要将码点放入大括号,就能正确解读该字符。
|
1 2 3 4 5 6 7 8 9 10 11 12 |
"\u{20BB7}" // "" "\u{41}\u{42}\u{43}" // "ABC" let hello = 123; hell\u{6F} // 123 '\u{1F680}' === '\uD83D\uDE80' // true |
JavaScript 共有 6 种方法可以表示一个字符。
|
1 2 3 4 5 6 |
'\z' === 'z' // true '\172' === 'z' // true '\x7A' === 'z' // true '\u007A' === 'z' // true '\u{7A}' === 'z' // true |
codePointAt()
ES6 提供了codePointAt方法,能够正确处理 4 个字节储存的字符,返回一个字符的码点。
|
1 2 3 4 5 6 7 |
let s = 'a'; s.codePointAt(0) // 134071 s.codePointAt(1) // 57271 s.codePointAt(2) // 97 |
你可能注意到了,codePointAt方法的参数,仍然是不正确的。比如,上面代码中,字符a在字符串s的正确位置序号应该是 1,但是必须向codePointAt方法传入 2。解决这个问题的一个办法是使用for...of循环,因为它会正确识别 32 位的 UTF-16 字符。【⭐️】
|
1 2 3 4 5 6 7 |
let s = 'a'; for (let ch of s) { console.log(ch.codePointAt(0).toString(16)); } // 20bb7 // 61 |
codePointAt方法是测试一个字符由两个字节还是由四个字节组成的最简单方法。
|
1 2 3 4 5 6 7 |
function is32Bit(c) { return c.codePointAt(0) > 0xFFFF; } is32Bit("") // true is32Bit("a") // false |
String.fromCodePoint()
ES6 提供了String.fromCodePoint方法,可以识别大于0xFFFF的字符,弥补了String.fromCharCode方法的不足。在作用上,正好与codePointAt方法相反。
|
1 2 3 4 5 |
String.fromCodePoint(0x20BB7) // "" String.fromCodePoint(0x78, 0x1f680, 0x79) === 'x\uD83D\uDE80y' // true |
normalize()【不重要】
许多欧洲语言有语调符号和重音符号。为了表示它们,Unicode 提供了两种方法。一种是直接提供带重音符号的字符,比如Ǒ(\u01D1)。另一种是提供合成符号(combining character),即原字符与重音符号的合成,两个字符合成一个字符,比如O(\u004F)和ˇ(\u030C)合成Ǒ(\u004F\u030C)。
ES6 提供字符串实例的normalize()方法,用来将字符的不同表示方法统一为同样的形式,这称为 Unicode 正规化。
|
1 2 3 |
'\u01D1'.normalize() === '\u004F\u030C'.normalize() // true |
normalize方法可以接受一个参数来指定normalize的方式,参数的四个可选值如下。
NFC,默认参数,表示“标准等价合成”(Normalization Form Canonical Composition),返回多个简单字符的合成字符。所谓“标准等价”指的是视觉和语义上的等价。NFD,表示“标准等价分解”(Normalization Form Canonical Decomposition),即在标准等价的前提下,返回合成字符分解的多个简单字符。NFKC,表示“兼容等价合成”(Normalization Form Compatibility Composition),返回合成字符。所谓“兼容等价”指的是语义上存在等价,但视觉上不等价,比如“囍”和“喜喜”。(这只是用来举例,normalize方法不能识别中文。)NFKD,表示“兼容等价分解”(Normalization Form Compatibility Decomposition),即在兼容等价的前提下,返回合成字符分解的多个简单字符。
includes(), startsWith(), endsWith()
传统上,JavaScript 只有indexOf方法,可以用来确定一个字符串是否包含在另一个字符串中。ES6 又提供了三种新方法。
- includes():返回布尔值,表示是否找到了参数字符串。
- startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。
- endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部。
repeat()
repeat方法返回一个新字符串,表示将原字符串重复n次。
|
1 2 3 4 |
'x'.repeat(3) // "xxx" 'hello'.repeat(2) // "hellohello" 'na'.repeat(0) // "" |
padStart(),padEnd()
ES2017 引入了字符串补全长度的功能。如果某个字符串不够指定长度,会在头部或尾部补全。padStart()用于头部补全,padEnd()用于尾部补全。【缺少的长度,就用后面的字符串参数去填补】
|
1 2 3 4 5 6 |
'x'.padStart(5, 'ab') // 'ababx' 'x'.padStart(4, 'ab') // 'abax' 'x'.padEnd(5, 'ab') // 'xabab' 'x'.padEnd(4, 'ab') // 'xaba' |
如果省略第二个参数,默认使用空格补全长度。
|
1 2 3 |
'x'.padStart(4) // ' x' 'x'.padEnd(4) // 'x ' |
padStart的常见用途是为数值补全指定位数。下面代码生成 10 位的数值字符串。
|
1 2 3 4 |
'1'.padStart(10, '0') // "0000000001" '12'.padStart(10, '0') // "0000000012" '123456'.padStart(10, '0') // "0000123456" |
另一个用途是提示字符串格式。
|
1 2 3 |
'12'.padStart(10, 'YYYY-MM-DD') // "YYYY-MM-12" '09-12'.padStart(10, 'YYYY-MM-DD') // "YYYY-09-12" |
模板字符串
传统的 JavaScript 语言,输出模板通常是这样写的。
|
1 2 3 4 5 6 7 |
$('#result').append( 'There are <b>' + basket.count + '</b> ' + 'items in your basket, ' + '<em>' + basket.onSale + '</em> are on sale!' ); |
上面这种写法相当繁琐不方便,ES6 引入了模板字符串解决这个问题。
|
1 2 3 4 5 6 |
$('#result').append(` There are <b>${basket.count}</b> items in your basket, <em>${basket.onSale}</em> are on sale! `); |
模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。
模板字符串中嵌入变量,需要将变量名写在${}之中。
模板字符串之中还能调用函数:
|
1 2 3 4 5 6 7 |
function fn() { return "Hello World"; } `foo ${fn()} bar` // foo Hello World bar |
如果大括号中的值不是字符串,将按照一般的规则转为字符串。比如,大括号中是一个对象,将默认调用对象的toString方法。
如果模板字符串中的变量没有声明,将报错。
|
1 2 3 4 |
// 变量place没有声明 let msg = `Hello, ${place}`; // 报错 |
由于模板字符串的大括号内部,就是执行 JavaScript 代码,因此如果大括号内部是一个字符串,将会原样输出。
|
1 2 3 |
`Hello ${'World'}` // "Hello World" |
模板字符串甚至还能嵌套。
|
1 2 3 4 5 6 7 8 9 |
<span class="hljs-keyword">const</span> tmpl = addrs => ` <table> ${addrs.map(addr => ` <tr><span class="xml"><span class="hljs-tag"><<span class="hljs-title">td</span>></span>${addr.first}<span class="hljs-tag"></<span class="hljs-title">td</span>></span><span class="hljs-tag"></<span class="hljs-title">tr</span>></span> <span class="hljs-tag"><<span class="hljs-title">tr</span>></span><span class="hljs-tag"><<span class="hljs-title">td</span>></span>${addr.last}<span class="hljs-tag"></<span class="hljs-title">td</span>></span><span class="hljs-tag"></<span class="hljs-title">tr</span>></span> `).join('')} <span class="hljs-tag"></<span class="hljs-title">table</span>></span> `;</span> |
上面代码中,模板字符串的变量之中,又嵌入了另一个模板字符串,使用方法如下。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
const data = [ { first: '<Jane>', last: 'Bond' }, { first: 'Lars', last: '<Croft>' }, ]; console.log(tmpl(data)); // <table> // // <tr><td><Jane></td></tr> // <tr><td>Bond</td></tr> // // <tr><td>Lars</td></tr> // <tr><td><Croft></td></tr> // // </table> |
如果需要引用模板字符串本身,在需要时执行,可以像下面这样写。
|
1 2 3 4 5 6 7 8 9 10 |
// 写法一 let str = 'return ' + '`Hello ${name}!`'; let func = new Function('name', str); func('Jack') // "Hello Jack!" // 写法二 let str = '(name) => `Hello ${name}!`'; let func = eval.call(null, str); func('Jack') // "Hello Jack!" |
实例:模板编译
标签模板
模板字符串的功能,不仅仅是上面这些。它可以紧跟在一个函数名后面,该函数将被调用来处理这个模板字符串。这被称为“标签模板”功能(tagged template)。
|
1 2 3 4 |
alert`123` // 等同于 alert(123) |
String.raw()
ES6 还为原生的 String 对象,提供了一个raw方法。
String.raw方法,往往用来充当模板字符串的处理函数,返回一个斜杠都被转义(即斜杠前面再加一个斜杠)的字符串,对应于替换变量后的模板字符串。
|
1 2 3 4 5 6 |
String.raw`Hi\n${2+3}!`; // 返回 "Hi\\n5!" String.raw`Hi\u000A!`; // 返回 "Hi\\u000A!" |
如果原字符串的斜杠已经转义,那么String.raw会进行再次转义。
|
1 2 3 |
String.raw`Hi\\n` // 返回 "Hi\\\\n" |
正则的扩展
RegExp 构造函数
在 ES5 中,RegExp构造函数的参数有两种情况。
第一种情况是,参数是字符串,这时第二个参数表示正则表达式的修饰符(flag)。【i 是一个修饰符 (搜索不区分大小写)。】
|
1 2 3 4 |
var regex = new RegExp('xyz', 'i'); // 等价于 var regex = /xyz/i; |
第二种情况是,参数是一个正则表示式,这时会返回一个原有正则表达式的拷贝。
|
1 2 3 4 |
var regex = new RegExp(/xyz/i); // 等价于 var regex = /xyz/i; |
但是,ES5 不允许此时使用第二个参数添加修饰符,否则会报错。
|
1 2 3 |
var regex = new RegExp(/xyz/, 'i'); // Uncaught TypeError: Cannot supply flags when constructing one RegExp from another |
ES6 改变了这种行为。如果RegExp构造函数第一个参数是一个正则对象,那么可以使用第二个参数指定修饰符。而且,返回的正则表达式会忽略原有的正则表达式的修饰符,只使用新指定的修饰符。
|
1 2 3 |
new RegExp(/abc/ig, 'i').flags // "i" |
上面代码中,原有正则对象的修饰符是ig,它会被第二个参数i覆盖。
字符串的正则方法
字符串对象共有 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]
u 修饰符
ES6 对正则表达式添加了u修饰符,含义为“Unicode 模式”,用来正确处理大于\uFFFF的 Unicode 字符。也就是说,会正确处理四个字节的 UTF-16 编码。
|
1 2 3 |
/^\uD83D/u.test('\uD83D\uDC2A') // false /^\uD83D/.test('\uD83D\uDC2A') // true |
上面代码中,\uD83D\uDC2A是一个四个字节的 UTF-16 编码,代表一个字符。但是,ES5 不支持四个字节的 UTF-16 编码,会将其识别为两个字符,导致第二行代码结果为true。加了u修饰符以后,ES6 就会识别其为一个字符,所以第一行代码结果为false。
一旦加上u修饰符号,就会修改下面这些正则表达式的行为。
(1)点字符
点(.)字符在正则表达式中,含义是除了换行符以外的任意单个字符。对于码点大于0xFFFF的 Unicode 字符,点字符不能识别,必须加上u修饰符。
|
1 2 3 4 5 |
var s = ''; /^.$/.test(s) // false /^.$/u.test(s) // true |
上面代码表示,如果不添加u修饰符,正则表达式就会认为字符串为两个字符,从而匹配失败。
(2)Unicode 字符表示法
ES6 新增了使用大括号表示 Unicode 字符,这种表示法在正则表达式中必须加上u修饰符,才能识别当中的大括号,否则会被解读为量词。
|
1 2 3 4 |
/\u{61}/.test('a') // false /\u{61}/u.test('a') // true /\u{20BB7}/u.test('') // true |
上面代码表示,如果不加u修饰符,正则表达式无法识别\u{61}这种表示法,只会认为这匹配 61 个连续的u。
(3)量词
使用u修饰符后,所有量词都会正确识别码点大于0xFFFF的 Unicode 字符。
|
1 2 3 4 5 |
/a{2}/.test('aa') // true /a{2}/u.test('aa') // true /{2}/.test('') // false /{2}/u.test('') // true |
y 修饰符
除了u修饰符,ES6 还为正则表达式添加了y修饰符,叫做“粘连”(sticky)修饰符。
y修饰符的作用与g修饰符类似,也是全局匹配,后一次匹配都从上一次匹配成功的下一个位置开始。不同之处在于,g修饰符只要剩余位置中存在匹配就可,而y修饰符确保匹配必须从剩余的第一个位置开始,这也就是“粘连”的涵义。
|
1 2 3 4 5 6 7 8 9 10 |
var s = 'aaa_aa_a'; var r1 = /a+/g; var 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。
flags 属性
ES6 为正则表达式新增了flags属性,会返回正则表达式的修饰符。
|
1 2 3 4 5 6 7 8 9 10 |
// ES5 的 source 属性 // 返回正则表达式的正文 /abc/ig.source // "abc" // ES6 的 flags 属性 // 返回正则表达式的修饰符 /abc/ig.flags // 'gi' |
数值的扩展
二进制和八进制表示法
ES6 提供了二进制和八进制数值的新的写法,分别用前缀0b(或0B)和0o(或0O)表示。
|
1 2 3 |
0b111110111 === 503 // true 0o767 === 503 // true |
Number.isFinite(), Number.isNaN()
Number.isFinite()用来检查一个数值是否为有限的(finite),即不是Infinity。
|
1 2 3 4 5 6 7 8 9 |
Number.isFinite(15); // true Number.isFinite(0.8); // true Number.isFinite(NaN); // false Number.isFinite(Infinity); // false Number.isFinite(-Infinity); // false Number.isFinite('foo'); // false Number.isFinite('15'); // false Number.isFinite(true); // false |
Number.isNaN()用来检查一个值是否为NaN。
|
1 2 3 4 5 6 7 8 |
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 |
Number.parseInt(), Number.parseFloat()
ES6 将全局方法parseInt()和parseFloat(),移植到Number对象上面,行为完全保持不变。
Number.isInteger()
Number.isInteger()用来判断一个数值是否为整数。
|
1 2 3 |
Number.isInteger(25) // true Number.isInteger(25.1) // false |
JavaScript 内部,整数和浮点数采用的是同样的储存方法,所以 25 和 25.0 被视为同一个值。
|
1 2 3 |
Number.isInteger(25) // true Number.isInteger(25.0) // true |
由于 JavaScript 采用 IEEE 754 标准,数值存储为64位双精度格式,数值精度最多可以达到 53 个二进制位(1 个隐藏位与 52 个有效位)。如果数值的精度超过这个限度,第54位及后面的位就会被丢弃,这种情况下,Number.isInteger可能会误判。
|
1 2 |
Number.isInteger(3.0000000000000002) // true |
Number.EPSILON
ES6 在Number对象上面,新增一个极小的常量Number.EPSILON。根据规格,它表示 1 与大于 1 的最小浮点数之间的差。
Number.EPSILON实际上是 JavaScript 能够表示的最小精度。误差如果小于这个值,就可以认为已经没有意义了,即不存在误差了。
Number.EPSILON可以用来设置“能够接受的误差范围”。比如,误差范围设为 2 的-50 次方(即Number.EPSILON * Math.pow(2, 2)),即如果两个浮点数的差小于这个值,我们就认为这两个浮点数相等。【注意还是要乘以一个数】
|
1 2 3 |
5.551115123125783e-17 < Number.EPSILON * Math.pow(2, 2) // true |
安全整数和 Number.isSafeInteger()
JavaScript 能够准确表示的整数范围在-2^53到2^53之间(不含两个端点),超过这个范围,无法精确表示这个值。
|
1 2 3 4 5 6 7 8 |
Math.pow(2, 53) // 9007199254740992 9007199254740992 // 9007199254740992 9007199254740993 // 9007199254740992 Math.pow(2, 53) === Math.pow(2, 53) + 1 // true |
ES6 引入了Number.MAX_SAFE_INTEGER和Number.MIN_SAFE_INTEGER这两个常量,用来表示这个范围的上下限。
Math 对象的扩展
ES6 在 Math 对象上新增了 17 个与数学相关的方法。所有这些方法都是静态方法,只能在 Math 对象上调用。
Math.trunc()
Math.trunc方法用于去除一个数的小数部分,返回整数部分。
|
1 2 3 4 5 6 |
Math.trunc(4.1) // 4 Math.trunc(4.9) // 4 Math.trunc(-4.1) // -4 Math.trunc(-4.9) // -4 Math.trunc(-0.1234) // -0 |
Math.sign()
Math.sign方法用来判断一个数到底是正数、负数、还是零。对于非数值,会先将其转换为数值。
它会返回五种值。
- 参数为正数,返回
+1; - 参数为负数,返回
-1; - 参数为 0,返回
0; - 参数为-0,返回
-0; - 其他值,返回
NaN。
Math.cbrt()
Math.cbrt方法用于计算一个数的立方根。
Math.clz32()
JavaScript 的整数使用 32 位二进制形式表示,Math.clz32方法返回一个数的 32 位无符号整数形式有多少个前导 0。
Math.imul() § ⇧
Math.imul方法返回两个数以 32 位带符号整数形式相乘的结果,返回的也是一个 32 位的带符号整数。
Math.fround()
Math.fround方法返回一个数的32位单精度浮点数形式。
Math.fround方法的主要作用,是将64位双精度浮点数转为32位单精度浮点数。如果小数的精度超过24个二进制位,返回值就会不同于原值,否则返回值不变(即与64位双精度值一致)。
Math.hypot()
Math.hypot方法返回所有参数的平方和的平方根。
对数方法
ES6 新增了 4 个对数相关方法。
(1) Math.expm1()
Math.expm1(x)返回 ex – 1,即Math.exp(x) - 1。
(2)Math.log1p()
Math.log1p(x)方法返回1 + x的自然对数,即Math.log(1 + x)。如果x小于-1,返回NaN。
(3)Math.log10()
Math.log10(x)返回以 10 为底的x的对数。如果x小于 0,则返回 NaN。
(3)Math.log10()
Math.log10(x)返回以 10 为底的x的对数。如果x小于 0,则返回 NaN。
双曲函数方法
ES6 新增了 6 个双曲函数方法。
Math.sinh(x)返回x的双曲正弦(hyperbolic sine)Math.cosh(x)返回x的双曲余弦(hyperbolic cosine)Math.tanh(x)返回x的双曲正切(hyperbolic tangent)Math.asinh(x)返回x的反双曲正弦(inverse hyperbolic sine)Math.acosh(x)返回x的反双曲余弦(inverse hyperbolic cosine)Math.atanh(x)返回x的反双曲正切(inverse hyperbolic tangent)
指数运算符
ES2016 新增了一个指数运算符(**)。
函数的扩展
函数参数的默认值
ES6 允许为函数的参数设置默认值,即直接写在参数定义的后面。
函数的 length 属性
指定了默认值以后,函数的length属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length属性将失真。
应用
利用参数默认值,可以指定某一个参数不得省略,如果省略就抛出一个错误。
|
1 2 3 4 5 6 7 8 9 10 11 |
function throwIfMissing() { throw new Error('Missing parameter'); } function foo(mustBeProvided = throwIfMissing()) { return mustBeProvided; } foo() // Error: Missing parameter |
上面代码的foo函数,如果调用的时候没有参数,就会调用默认值throwIfMissing函数,从而抛出一个错误。
从上面代码还可以看到,参数mustBeProvided的默认值等于throwIfMissing函数的运行结果(注意函数名throwIfMissing之后有一对圆括号),这表明参数的默认值不是在定义时执行,而是在运行时执行。如果参数已经赋值,默认值中的函数就不会运行。
rest 参数
ES6 引入 rest 参数(形式为...变量名),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。
|
1 2 3 4 5 6 7 8 9 10 11 12 |
function add(...values) { let sum = 0; for (var val of values) { sum += val; } return sum; } add(2, 5, 3) // 10 |
下面是一个 rest 参数代替arguments变量的例子。
|
1 2 3 4 5 6 7 8 |
// arguments变量的写法 function sortNumbers() { return Array.prototype.slice.call(arguments).sort(); } // rest参数的写法 const sortNumbers = (...numbers) => numbers.sort(); |
注意,rest 参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。
|
1 2 3 4 5 |
// 报错 function f(a, ...b, c) { // ... } |
函数的length属性,不包括 rest 参数。
|
1 2 3 4 |
(function(a) {}).length // 1 (function(...a) {}).length // 0 (function(a, ...b) {}).length // 1 |
name 属性
函数的name属性,返回该函数的函数名。
|
1 2 3 |
function foo() {} foo.name // "foo" |
这个属性早就被浏览器广泛支持,但是直到 ES6,才将其写入了标准。
箭头函数【和C++的lambda一样】
基本用法
ES6 允许使用“箭头”(=>)定义函数。
|
1 2 3 4 5 6 7 |
var f = v => v; // 等同于 var f = function (v) { return v; }; |
如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。
|
1 2 3 4 5 6 7 8 9 10 |
var f = () => 5; // 等同于 var f = function () { return 5 }; var sum = (num1, num2) => num1 + num2; // 等同于 var sum = function(num1, num2) { return num1 + num2; }; |
如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回。
|
1 2 |
var sum = (num1, num2) => { return num1 + num2; } |
箭头函数可以与变量解构结合使用。
|
1 2 3 4 5 6 7 |
const full = ({ first, last }) => first + ' ' + last; // 等同于 function full(person) { return person.first + ' ' + person.last; } |
使用注意点
箭头函数有几个使用注意点。
(1)函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
(2)不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
(3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
(4)不可以使用yield命令,因此箭头函数不能用作 Generator 函数。
上面四点中,第一点尤其值得注意。this对象的指向是可变的,但是在箭头函数中,它是固定的。
|
1 2 3 4 5 6 7 8 9 10 11 |
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。
this指向的固定化,并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。正是因为它没有this,所以也就不能用作构造函数。
双冒号运算符
数组的扩展
扩展运算符
含义
扩展运算符(spread)是三个点(...)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列。
|
1 2 3 4 5 6 7 8 9 |
console.log(...[1, 2, 3]) // 1 2 3 console.log(1, ...[2, 3, 4], 5) // 1 2 3 4 5 [...document.querySelectorAll('div')] // [<div>, <div>, <div>] |
替代函数的 apply 方法
由于扩展运算符可以展开数组,所以不再需要apply方法,将数组转为函数的参数了。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// 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); |
另一个例子是通过push函数,将一个数组添加到另一个数组的尾部。
|
1 2 3 4 5 6 7 8 9 10 |
// 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); |
下面是另外一个例子。
|
1 2 3 4 5 |
// ES5 new (Date.bind.apply(Date, [null, 2015, 1, 1])) // ES6 new Date(...[2015, 1, 1]); |
扩展运算符的应用
(1)复制数组
扩展运算符提供了复制数组的简便写法。
|
1 2 3 4 5 6 |
const a1 = [1, 2]; // 写法一 const a2 = [...a1]; // 写法二 const [...a2] = a1; |
上面的两种写法,a2都是a1的克隆。
(2)合并数组
扩展运算符提供了数组合并的新写法。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// ES5 [1, 2].concat(more) // ES6 [1, 2, ...more] var arr1 = ['a', 'b']; var arr2 = ['c']; var arr3 = ['d', 'e']; // ES5的合并数组 arr1.concat(arr2, arr3); // [ 'a', 'b', 'c', 'd', 'e' ] // ES6的合并数组 [...arr1, ...arr2, ...arr3] // [ 'a', 'b', 'c', 'd', 'e' ] |
(3)与解构赋值结合
扩展运算符可以与解构赋值结合起来,用于生成数组。
|
1 2 3 4 5 |
// ES5 a = list[0], rest = list.slice(1) // ES6 [a, ...rest] = list |
如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错。
|
1 2 3 4 5 6 |
const [...butLast, last] = [1, 2, 3, 4, 5]; // 报错 const [first, ...middle, last] = [1, 2, 3, 4, 5]; // 报错 |
字符串
扩展运算符还可以将字符串转为真正的数组。
|
1 2 3 |
[...'hello'] // [ "h", "e", "l", "l", "o" ] |
上面的写法,有一个重要的好处,那就是能够正确识别四个字节的 Unicode 字符。
|
1 2 3 |
'x\uD83D\uDE80y'.length // 4 [...'x\uD83D\uDE80y'].length // 3 |
上面代码的第一种写法,JavaScript 会将四个字节的 Unicode 字符,识别为 2 个字符,采用扩展运算符就没有这个问题。因此,正确返回字符串长度的函数,可以像下面这样写。
|
1 2 3 4 5 6 |
function length(str) { return [...str].length; } length('x\uD83D\uDE80y') // 3 |
凡是涉及到操作四个字节的 Unicode 字符的函数,都有这个问题。因此,最好都用扩展运算符改写。
|
1 2 3 4 5 6 7 8 |
let str = 'x\uD83D\uDE80y'; str.split('').reverse().join('') // 'y\uDE80\uD83Dx' [...str].reverse().join('') // 'y\uD83D\uDE80x' |
上面代码中,如果不用扩展运算符,字符串的reverse操作就不正确。
6)Map 和 Set 结构,Generator 函数
扩展运算符内部调用的是数据结构的 Iterator 接口,因此只要具有 Iterator 接口的对象,都可以使用扩展运算符,比如 Map 结构。
|
1 2 3 4 5 6 7 8 |
let map = new Map([ [1, 'one'], [2, 'two'], [3, 'three'], ]); let arr = [...map.keys()]; // [1, 2, 3] |
Generator 函数运行后,返回一个遍历器对象,因此也可以使用扩展运算符。
|
1 2 3 4 5 6 7 8 |
const go = function*(){ yield 1; yield 2; yield 3; }; [...go()] // [1, 2, 3] |
上面代码中,变量go是一个 Generator 函数,执行后返回的是一个遍历器对象,对这个遍历器对象执行扩展运算符,就会将内部遍历得到的值,转为一个数组。
如果对没有 Iterator 接口的对象,使用扩展运算符,将会报错。
|
1 2 3 |
const obj = {a: 1, b: 2}; let arr = [...obj]; // TypeError: Cannot spread non-iterable object |
Array.from()
Array.from方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。
下面是一个类似数组的对象,Array.from将它转为真正的数组。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
let arrayLike = { '0': 'a', '1': 'b', '2': 'c', length: 3 }; // ES5的写法 var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c'] // ES6的写法 let arr2 = Array.from(arrayLike); // ['a', 'b', 'c'] |
实际应用中,常见的类似数组的对象是 DOM 操作返回的 NodeList 集合,以及函数内部的arguments对象。Array.from都可以将它们转为真正的数组。
Array.from还可以接受第二个参数,作用类似于数组的map方法,用来对每个元素进行处理,将处理后的值放入返回的数组。
|
1 2 3 4 5 6 7 |
Array.from(arrayLike, x => x * x); // 等同于 Array.from(arrayLike).map(x => x * x); Array.from([1, 2, 3], (x) => x * x) // [1, 4, 9] |
扩展运算符背后调用的是遍历器接口(Symbol.iterator),如果一个对象没有部署这个接口,就无法转换。Array.from方法还支持类似数组的对象。所谓类似数组的对象,本质特征只有一点,即必须有length属性。因此,任何有length属性的对象,都可以通过Array.from方法转为数组,而此时扩展运算符就无法转换。
|
1 2 3 |
Array.from({ length: 3 }); // [ undefined, undefined, undefined ] |
上面代码中,Array.from返回了一个具有三个成员的数组,每个位置的值都是undefined。扩展运算符转换不了这个对象。
Array.from()可以将各种值转为真正的数组,并且还提供map功能。这实际上意味着,只要有一个原始的数据结构,你就可以先对它的值进行处理,然后转成规范的数组结构,进而就可以使用数量众多的数组方法。
|
1 2 3 |
Array.from({ length: 2 }, () => 'jack') // ['jack', 'jack'] |
Array.of() 【代替了Array初始化】
Array.of方法用于将一组值,转换为数组。
|
1 2 3 4 |
Array.of(3, 11, 8) // [3,11,8] Array.of(3) // [3] Array.of(3).length // 1 |
这个方法的主要目的,是弥补数组构造函数Array()的不足。因为参数个数的不同,会导致Array()的行为有差异。
|
1 2 3 4 |
Array() // [] Array(3) // [, , ,] Array(3, 11, 8) // [3, 11, 8] |
上面代码中,Array方法没有参数、一个参数、三个参数时,返回结果都不一样。只有当参数个数不少于 2 个时,Array()才会返回由参数组成的新数组。参数个数只有一个时,实际上是指定数组的长度。
Array.of基本上可以用来替代Array()或new Array(),并且不存在由于参数不同而导致的重载。它的行为非常统一。
|
1 2 3 4 5 |
Array.of() // [] Array.of(undefined) // [undefined] Array.of(1) // [1] Array.of(1, 2) // [1, 2] |
数组实例的 copyWithin()
数组实例的copyWithin方法,在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。也就是说,使用这个方法,会修改当前数组。
|
1 2 |
Array.prototype.copyWithin(target, start = 0, end = this.length) |
它接受三个参数。
- target(必需):从该位置开始替换数据。如果为负值,表示倒数。
- start(可选):从该位置开始读取数据,默认为 0。如果为负值,表示倒数。
- end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示倒数。
这三个参数都应该是数值,如果不是,会自动转为数值。
|
1 2 3 |
[1, 2, 3, 4, 5].copyWithin(0, 3) // [4, 5, 3, 4, 5] |
数组实例的 find() 和 findIndex()
数组实例的find方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined。
|
1 2 3 |
[1, 4, -5, 10].find((n) => n < 0) // -5 |
数组实例的 fill()
fill方法使用给定值,填充一个数组。
|
1 2 3 4 5 6 |
['a', 'b', 'c'].fill(7) // [7, 7, 7] new Array(3).fill(7) // [7, 7, 7] |
fill方法还可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置。
|
1 2 3 |
['a', 'b', 'c'].fill(7, 1, 2) // ['a', 7, 'c'] |
数组实例的 entries(),keys() 和 values()
ES6 提供三个新的方法——entries(),keys()和values()——用于遍历数组。它们都返回一个遍历器对象(详见《Iterator》一章),可以用for...of循环进行遍历,唯一的区别是keys()是对键名的遍历、values()是对键值的遍历,entries()是对键值对的遍历。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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" |
数组实例的 includes()
Array.prototype.includes方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的includes方法类似。ES2016 引入了该方法。
|
1 2 3 4 |
[1, 2, 3].includes(2) // true [1, 2, 3].includes(4) // false [1, 2, NaN].includes(NaN) // true |
该方法的第二个参数表示搜索的起始位置,默认为0。如果第二个参数为负数,则表示倒数的位置,如果这时它大于数组长度(比如第二个参数为-4,但数组长度为3),则会重置为从0开始。
|
1 2 3 |
[1, 2, 3].includes(3, 3); // false [1, 2, 3].includes(3, -1); // true |
数组的空位
数组的空位指,数组的某一个位置没有任何值。比如,Array构造函数返回的数组都是空位。
|
1 2 |
Array(3) // [, , ,] |
上面代码中,Array(3)返回一个具有 3 个空位的数组。
注意,空位不是undefined,一个位置的值等于undefined,依然是有值的。空位是没有任何值,in运算符可以说明这一点。
|
1 2 3 |
0 in [undefined, undefined, undefined] // true 0 in [, , ,] // false |
【in 说的是存不存在这个key,而不是value !】
ES5 对空位的处理,已经很不一致了,大多数情况下会忽略空位。
ES6 则是明确将空位转为undefined。
Array.from方法会将数组的空位,转为undefined,也就是说,这个方法不会忽略空位。
|
1 2 3 |
Array.from(['a',,'b']) // [ "a", undefined, "b" ] |
扩展运算符(...)也会将空位转为undefined。
|
1 2 3 |
[...['a',,'b']] // [ "a", undefined, "b" ] |
对象的扩展
属性的简洁表示法
ES6 允许直接写入变量和函数,作为对象的属性和方法。这样的书写更加简洁。
|
1 2 3 4 5 6 7 |
const foo = 'bar'; const baz = {foo};//[会进行自动转换到key、value] baz // {foo: "bar"} // 等同于 const baz = {foo: foo}; |
属性名表达式
ES6 允许字面量定义对象时,用方法二(表达式)作为对象的属性名,即把表达式放在方括号内。
|
1 2 3 4 5 6 7 |
let propKey = 'foo'; let obj = { [propKey]: true, ['a' + 'bc']: 123 }; |
表达式还可以用于定义方法名。
|
1 2 3 4 5 6 7 8 |
let obj = { ['h' + 'ello']() { return 'hi'; } }; obj.hello() // hi |
注意,属性名表达式如果是一个对象,默认情况下会自动将对象转为字符串[object Object],这一点要特别小心。
|
1 2 3 4 5 6 7 8 9 10 |
const keyA = {a: 1}; const keyB = {b: 2}; const myObject = { [keyA]: 'valueA', [keyB]: 'valueB' }; myObject // Object {[object Object]: "valueB"} |
方法的 name 属性
函数的name属性,返回函数名。对象方法也是函数,因此也有name属性。
|
1 2 3 4 5 6 7 8 |
const person = { sayName() { console.log('hello!'); }, }; person.sayName.name // "sayName" |
上面代码中,方法的name属性返回函数名(即方法名)。
如果对象的方法使用了取值函数(getter)和存值函数(setter),则name属性不是在该方法上面,而是该方法的属性的描述对象的get和set属性上面,返回值是方法名前加上get和set。
Object.is() § ⇧
ES6 提出“Same-value equality”(同值相等)算法,用来解决这个问题。Object.is就是部署这个算法的新方法。它用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。
|
1 2 3 4 5 |
Object.is('foo', 'foo') // true Object.is({}, {}) // false |
不同之处只有两个:一是+0不等于-0,二是NaN等于自身。
|
1 2 3 4 5 6 |
+0 === -0 //true NaN === NaN // false Object.is(+0, -0) // false Object.is(NaN, NaN) // true |
Object.assign()
基本用法
Object.assign方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。
|
1 2 3 4 5 6 7 8 |
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方法的第一个参数是目标对象,后面的参数都是源对象。
注意,如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。
|
1 2 3 4 5 6 7 8 |
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会直接返回该参数。
|
1 2 3 |
const obj = {a: 1}; Object.assign(obj) === obj // true |
如果该参数不是对象,则会先转成对象,然后返回。
|
1 2 |
typeof Object.assign(2) // "object" |
由于undefined和null无法转成对象,所以如果它们作为参数,就会报错。
|
1 2 3 |
Object.assign(undefined) // 报错 Object.assign(null) // 报错 |
如果非对象参数出现在源对象的位置(即非首参数),那么处理规则有所不同。首先,这些参数都会转成对象,如果无法转成对象,就会跳过。这意味着,如果undefined和null不在首参数,就不会报错。
|
1 2 3 4 |
let obj = {a: 1}; Object.assign(obj, undefined) === obj // true Object.assign(obj, null) === obj // true |
|
1 2 3 4 5 6 7 |
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" } |
上面代码中,v1、v2、v3分别是字符串、布尔值和数值,结果只有字符串合入目标对象(以字符数组的形式),数值和布尔值都会被忽略。这是因为只有字符串的包装对象,会产生可枚举属性。
注意点
(1)浅拷贝【拷贝不了那么深层,内部对象数据还是拷贝地址】
Object.assign方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。
|
1 2 3 4 5 6 |
const obj1 = {a: {b: 1}}; const obj2 = Object.assign({}, obj1); obj1.a.b = 2; obj2.a.b // 2 |
上面代码中,源对象obj1的a属性的值是一个对象,Object.assign拷贝得到的是这个对象的引用。这个对象的任何变化,都会反映到目标对象上面。
(3)数组的处理
Object.assign可以用来处理数组,但是会把数组视为对象。
常见用途
Object.assign方法有很多用处。
(1)为对象添加属性
|
1 2 3 4 5 6 |
class Point { constructor(x, y) { Object.assign(this, {x, y}); } } |
上面方法通过Object.assign方法,将x属性和y属性添加到Point类的对象实例。
(2)为对象添加方法
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
Object.assign(SomeClass.prototype, { someMethod(arg1, arg2) { ··· }, anotherMethod() { ··· } }); // 等同于下面的写法 SomeClass.prototype.someMethod = function (arg1, arg2) { ··· }; SomeClass.prototype.anotherMethod = function () { ··· }; |
上面代码使用了对象属性的简洁表示法,直接将两个函数放在大括号中,再使用assign方法添加到SomeClass.prototype之中。
(3)克隆对象
|
1 2 3 4 |
function clone(origin) { return Object.assign({}, origin); } |
上面代码将原始对象拷贝到一个空对象,就得到了原始对象的克隆。
不过,采用这种方法克隆,只能克隆原始对象自身的值,不能克隆它继承的值。如果想要保持继承链,可以采用下面的代码。
|
1 2 3 4 5 |
function clone(origin) { let originProto = Object.getPrototypeOf(origin); return Object.assign(Object.create(originProto), origin); } |
属性的遍历
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 键,按照加入时间升序排列。
Object.setPrototypeOf()
Object.setPrototypeOf方法的作用与__proto__相同,用来设置一个对象的prototype对象,返回参数对象本身。它是 ES6 正式推荐的设置原型对象的方法。
|
1 2 3 4 5 6 |
// 格式 Object.setPrototypeOf(object, prototype) // 用法 const o = Object.setPrototypeOf({}, null); |
该方法等同于下面的函数。
|
1 2 3 4 5 |
function (obj, proto) { obj.__proto__ = proto; return obj; } |
下面是一个例子。
|
1 2 3 4 5 6 7 8 9 10 11 |
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 |
解构赋值
对象的解构赋值用于从一个对象取值,相当于将目标对象自身的所有可遍历的(enumerable)、但尚未被读取的属性,分配到指定的对象上面。所有的键和它们的值,都会拷贝到新对象上面。
|
1 2 3 4 5 |
let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 }; x // 1 y // 2 z // { a: 3, b: 4 } |
扩展运算符
对象的扩展运算符(...)用于取出参数对象的所有可遍历属性,拷贝到当前对象之中。
|
1 2 3 4 |
let z = { a: 3, b: 4 }; let n = { ...z }; n // { a: 3, b: 4 } |
扩展运算符可以用于合并两个对象。
|
1 2 3 4 |
let ab = { ...a, ...b }; // 等同于 let ab = Object.assign({}, a, b); |
Symbol
作为属性名的 Symbol
由于每一个 Symbol 值都是不相等的,这意味着 Symbol 值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性。这对于一个对象由多个模块构成的情况非常有用,能防止某一个键被不小心改写或覆盖。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
let mySymbol = Symbol(); // 第一种写法 let a = {}; a[mySymbol] = 'Hello!'; // 第二种写法 let a = { [mySymbol]: 'Hello!' }; // 第三种写法 let a = {}; Object.defineProperty(a, mySymbol, { value: 'Hello!' }); // 以上写法都得到同样结果 a[mySymbol] // "Hello!" |
另一个新的 API,Reflect.ownKeys方法可以返回所有类型的键名,包括常规键名和 Symbol 键名。
|
1 2 3 4 5 6 7 8 9 |
let obj = { [Symbol('my_key')]: 1, enum: 2, nonEnum: 3 }; Reflect.ownKeys(obj) // ["enum", "nonEnum", Symbol(my_key)] |
Symbol.for(),Symbol.keyFor()
有时,我们希望重新使用同一个 Symbol 值,Symbol.for方法可以做到这一点。它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建并返回一个以该字符串为名称的 Symbol 值。
|
1 2 3 4 5 |
let s1 = Symbol.for('foo'); let s2 = Symbol.for('foo'); s1 === s2 // true |
上面代码中,s1和s2都是 Symbol 值,但是它们都是同样参数的Symbol.for方法生成的,所以实际上是同一个值。
Symbol.for()与Symbol()这两种写法,都会生成新的 Symbol。它们的区别是,前者会被登记在全局环境中供搜索,后者不会。Symbol.for()不会每次调用就返回一个新的 Symbol 类型的值,而是会先检查给定的key是否已经存在,如果不存在才会新建一个值。
Symbol.keyFor方法返回一个已登记的 Symbol 类型值的key。
|
1 2 3 4 5 6 |
let s1 = Symbol.for("foo"); Symbol.keyFor(s1) // "foo" let s2 = Symbol("foo"); Symbol.keyFor(s2) // undefined |
Set 和 Map 数据结构
Set 函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化。
|
1 2 3 4 5 6 7 8 9 |
// 例一 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 |
Set 内部判断两个值是否不同,使用的算法叫做“Same-value-zero equality”,它类似于精确相等运算符(===),主要的区别是NaN等于自身,而精确相等运算符认为NaN不等于自身。
|
1 2 3 4 5 6 7 |
let set = new Set(); let a = NaN; let b = NaN; set.add(a); set.add(b); set // Set {NaN} |
上面代码向 Set 实例添加了两个NaN,但是只能加入一个。这表明,在 Set 内部,两个NaN是相等。
另外,两个对象总是不相等的。
|
1 2 3 4 5 6 7 8 |
let set = new Set(); set.add({}); set.size // 1 set.add({}); set.size // 2 |
上面代码表示,由于两个空对象不相等,所以它们被视为两个值。
Set 实例的属性和方法
add(value):添加某个值,返回 Set 结构本身。delete(value):删除某个值,返回一个布尔值,表示删除是否成功。has(value):返回一个布尔值,表示该值是否为Set的成员。clear():清除所有成员,没有返回值。
遍历操作
Set 结构的实例有四个遍历方法,可以用于遍历成员。
keys():返回键名的遍历器values():返回键值的遍历器entries():返回键值对的遍历器forEach():使用回调函数遍历每个成员
由于 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys方法和values方法的行为完全一致。
使用 Set 可以很容易地实现并集(Union)、交集(Intersect)和差集(Difference)。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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} // 差集 let difference = new Set([...a].filter(x => !b.has(x))); // Set {1} |
WeakSet
含义
WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别。
首先,WeakSet 的成员只能是对象,而不能是其他类型的值。
其次,WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。
Map
含义和基本用法
JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。
ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。
任何具有 Iterator 接口、且每个成员都是一个双元素的数组的数据结构(详见《Iterator》一章)都可以当作Map构造函数的参数。这就是说,Set和Map都可以用来生成新的 Map。
|
1 2 3 4 5 6 7 8 9 10 11 |
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 |
只有对同一个对象的引用,Map 结构才将其视为同一个键。这一点要非常小心。
|
1 2 3 4 5 |
const map = new Map(); map.set(['a'], 555); map.get(['a']) // undefined |
由上可知,Map 的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键。这就解决了同名属性碰撞(clash)的问题,我们扩展别人的库的时候,如果使用对象作为键名,就不用担心自己的属性与原作者的属性同名。
与其他数据结构的互相转换
(1)Map 转为数组
前面已经提过,Map 转为数组最方便的方法,就是使用扩展运算符(...)。
|
1 2 3 4 5 6 |
const myMap = new Map() .set(true, 7) .set({foo: 3}, ['abc']); [...myMap] // [ [ true, 7 ], [ { foo: 3 }, [ 'abc' ] ] ] |
(2)数组 转为 Map
将数组传入 Map 构造函数,就可以转为 Map。
|
1 2 3 4 5 6 7 8 9 |
new Map([ [true, 7], [{foo: 3}, ['abc']] ]) // Map { // true => 7, // Object {foo: 3} => ['abc'] // } |
(3)Map 转为对象
如果所有 Map 的键都是字符串,它可以无损地转为对象。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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
|
1 2 3 4 5 6 7 8 9 10 11 |
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。
|
1 2 3 4 5 6 7 8 |
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。
|
1 2 3 4 5 6 7 8 |
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,正常情况下,所有键名都是字符串。
|
1 2 3 4 5 6 7 |
function jsonToStrMap(jsonStr) { return objToStrMap(JSON.parse(jsonStr)); } jsonToStrMap('{"yes": true, "no": false}') // Map {'yes' => true, 'no' => false} |
但是,有一种特殊情况,整个 JSON 就是一个数组,且每个数组成员本身,又是一个有两个成员的数组。这时,它可以一一对应地转为 Map。这往往是 Map 转为数组 JSON 的逆操作。
|
1 2 3 4 5 6 7 |
function jsonToMap(jsonStr) { return new Map(JSON.parse(jsonStr)); } jsonToMap('[[true,7],[{"foo":3},["abc"]]]') // Map {true => 7, Object {foo: 3} => ['abc']} |
WeakMap
含义
WeakMap结构与Map结构类似,也是用于生成键值对的集合。
WeakMap与Map的区别有两点。
首先,WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。
其次,WeakMap的键名所指向的对象,不计入垃圾回收机制。
Proxy
Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。
Reflect
(1) 将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。现阶段,某些方法同时在Object和Reflect对象上部署,未来的新方法将只部署在Reflect对象上。也就是说,从Reflect对象上可以拿到语言内部的方法。
(2) 修改某些Object方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false。
(3) 让Object操作都变成函数行为。某些Object操作是命令式,比如name in obj和delete obj[name],而Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)让它们变成了函数行为。
|
1 2 3 4 5 6 |
// 老写法 'assign' in Object // true // 新写法 Reflect.has(Object, 'assign') // true |
(4)Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为。
Promise 对象
Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。
Promise对象有以下两个特点。
(1)对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。
2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
let promise = new Promise(function(resolve, reject) { console.log('Promise'); resolve(); }); promise.then(function() { console.log('resolved.'); }); console.log('Hi!'); // Promise // Hi! // resolved |
上面代码中,Promise 新建后立即执行,所以首先输出的是Promise。然后,then方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行,所以resolved最后输出。
不带有任何参数
Promise.resolve方法允许调用时不带参数,直接返回一个resolved状态的 Promise 对象。
所以,如果希望得到一个 Promise 对象,比较方便的方法就是直接调用Promise.resolve方法。
|
1 2 3 4 5 6 |
const p = Promise.resolve(); p.then(function () { // ... }); |
需要注意的是,立即resolve的 Promise 对象,是在本轮“事件循环”(event loop)的结束时,而不是在下一轮“事件循环”的开始时。
Iterator 和 for…of 循环
Iterator 的作用有三个:一是为各种数据结构,提供一个统一的、简便的访问接口;二是使得数据结构的成员能够按某种次序排列;三是 ES6 创造了一种新的遍历命令for...of循环,Iterator 接口主要供for...of消费。
原生具备 Iterator 接口的数据结构如下。
- Array
- Map
- Set
- String
- TypedArray
- 函数的 arguments 对象
- NodeList 对象
对象
对于普通的对象,for...of结构不能直接使用,会报错,必须部署了 Iterator 接口后才能使用。但是,这样情况下,for...in循环依然可以用来遍历键名。
|
1 2 3 4 5 6 7 8 9 10 |
let es6 = { edition: 6, committee: "TC39", standard: "ECMA-262" }; for (let e in es6) { console.log(e); } |
上面代码表示,对于普通的对象,for...in循环可以遍历键名,for...of循环会报错。
一种解决方法是,使用Object.keys方法将对象的键名生成一个数组,然后遍历这个数组。
|
1 2 3 4 |
for (var key of Object.keys(someObject)) { console.log(key + ': ' + someObject[key]); } |
与其他遍历语法的比较
以数组为例,JavaScript 提供多种遍历语法。最原始的写法就是for循环。
|
1 2 3 4 |
for (var index = 0; index < myArray.length; index++) { console.log(myArray[index]); } |
这种写法比较麻烦,因此数组提供内置的forEach方法。
|
1 2 3 4 |
myArray.forEach(function (value) { console.log(value); }); |
这种写法的问题在于,无法中途跳出forEach循环,break命令或return命令都不能奏效。【厉害】
for...in循环可以遍历数组的键名。
|
1 2 3 4 |
for (var index in myArray) { console.log(myArray[index]); } |
for...in循环有几个缺点。【但是可以break】
- 数组的键名是数字,但是
for...in循环是以字符串作为键名“0”、“1”、“2”等等。 for...in循环不仅遍历数字键名,还会遍历手动添加的其他键,甚至包括原型链上的键。- 某些情况下,
for...in循环会以任意顺序遍历键名。
总之,for...in循环主要是为遍历对象而设计的,不适用于遍历数组。
for...of循环相比上面几种做法,有一些显著的优点。【推荐用for of】
|
1 2 3 4 |
for (let value of myArray) { console.log(value); } |
- 有着同
for...in一样的简洁语法,但是没有for...in那些缺点。 - 不同于
forEach方法,它可以与break、continue和return配合使用。 - 提供了遍历所有数据结构的统一操作接口。
小明编程![[转]JavaScript 的 this 原理-小明编程](https://www.wangbase.com/blogimg/asset/201806/bg2018061801.png)
![[转]js 中的多个连续的箭头函数与柯里化-小明编程](https://pic2.zhimg.com/80/v2-ff4e407148a6e45da5da0b440a7e01d9_hd.jpg)