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
配合使用。 - 提供了遍历所有数据结构的统一操作接口。