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 |