这是 ES6 学习笔记记录 Part1-3
学习并节选自 阮一峰 - ECMAScript 6 入门
仅供个人学习记录,原著请访问链接
ECMAScript 6.0(以下简称 ES6)是 JavaScript 语言的下一代标准,已经在 2015 年 6 月正式发布了。它的目标,是使得 JavaScript 语言可以用来编写复杂的大型应用程序,成为企业级开发语言。
ECMAScript 和 JavaScript 的关系是,前者是后者的规格,后者是前者的一种实现。日常场合,这两个词是可以互换的。
ES6 既是一个历史名词,也是一个泛指,含义是 5.1 版以后的 JavaScript 的下一代标准,涵盖了 ES2015、ES2016、ES2017 等等,而 ES2015 则是正式名称,特指该年发布的正式版本的语言标准。
ES6 从开始制定到最后发布,整整用了 15 年。
Babel 是一个广泛使用的 ES6 转码器,可以将 ES6 代码转为 ES5 代码,从而在现有环境执行。例如:
// 转码前
input.map(item => item + 1);
// 转码后
input.map(function (item) {
return item + 1;
});
Babel 的配置文件是.babelrc,存放在项目的根目录下。使用 Babel 的第一步,就是配置这个文件。
该文件用来设置转码规则和插件,基本格式为:
{
"presets": [],
"plugins": []
}
ES6 新增了let
命令,用来声明变量。它的用法类似于var
,但是所声明的变量,只在let
命令所在的代码块内有效。
{
let a = 10;
var b = 1;
}
a // ReferenceError: a is not defined.
b // 1
所以在 for
循环中,最好使用 let
而不是 var
:
for (let i = 0; i < 10; i++) {
}
for (var j = 0; j < 10; j++) {
}
i // ReferenceError: i is not defined
j // 10
例子:
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 6
变量i
是let
声明的,当前的i
只在本轮循环有效,所以每一次循环的i
其实都是一个新的变量,所以最后输出的是6
。
因为 JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i
时,就在上一轮循环的基础上进行计算。
另外,for
循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域:
for (let i = 0; i < 3; i++) {
let i = 'abc';
console.log(i);
}
// abc
// abc
// abc
var
命令会发生”变量提升“现象,即变量可以在声明之前使用,值为undefined
。
为了纠正这种现象,let
命令改变了语法行为,它所声明的变量一定要在声明后使用,否则报错。
// var 的情况
console.log(foo); // 输出undefined
var foo = 2;
// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;
只要块级作用域内存在let
命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。
var tmp = 123;
if (true) {
tmp = 'abc'; // ReferenceError
let tmp;
}
// 存在全局变量tmp,但是块级作用域内
// let又声明了一个局部变量tmp
// 导致后者绑定这个块级作用域
// 所以在let声明变量前,对tmp赋值会报错。
ES6 明确规定,如果区块中存在let
和const
命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。
“暂时性死区”也意味着typeof
不再是一个百分之百安全的操作:
typeof x; // ReferenceError
let x;
// ----------
// 作为比较,如果一个变量根本没有被声明
// 使用typeof反而不会报错。
typeof undeclared_variable // "undefined"
// ----------
function bar(x = y, y = 2) {
return [x, y];
}
bar(); // 报错
// 参数x默认值等于另一个参数y
// 而此时y还没有声明,属于"死区"
// ----------
function bar(x = 2, y = x) {
return [x, y];
}
bar(); // [2, 2]
// ----------
// 不报错
var x = x;
// 报错
let x = x;
// ReferenceError: x is not defined
// 代码报错,也是因为暂时性死区
暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。
let
不允许在相同作用域内,重复声明同一个变量。
也不可以在函数内部重新声明参数。
// 报错
function func() {
let a = 10;
var a = 1;
}
// 报错
function func() {
let a = 10;
let a = 1;
}
function func(arg) {
let arg; // 报错
}
function func(arg) {
{
let arg; // 不报错
}
}
ES5 只有全局作用域和函数作用域,没有块级作用域,这带来很多不合理的场景:
// 变量提升导致了覆盖
var tmp = new Date();
function f() {
console.log(tmp);
if (false) {
var tmp = 'hello world';
}
}
f(); // undefined
// 计数变量泄露
var s = 'hello';
for (var i = 0; i < s.length; i++) {
console.log(s[i]);
}
console.log(i); // 5
let
实际上为 JavaScript 新增了块级作用域,允许块级作用域的任意嵌套:
function f1() {
let n = 5;
if (true) {
let n = 10;
}
console.log(n); // 5
}
ES5 规定,函数只能在顶层作用域和函数作用域之中声明,不能在块级作用域声明(但是,浏览器没有遵守这个规定)。
ES6 引入了块级作用域,明确允许在块级作用域之中声明函数。ES6 规定,块级作用域之中,函数声明语句的行为类似于let
,在块级作用域之外不可引用。
但是在浏览器的 ES6 环境中,块级作用域内声明的函数,行为类似于var声明的变量。
考虑到环境导致的行为差异太大,应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句:
// 函数声明语句
{
let a = 'secret';
function f() {
return a;
}
}
// 函数表达式
{
let a = 'secret';
let f = function () {
return a;
};
}
// ES6 的块级作用域允许声明函数的规则
// 只在使用大括号的情况下成立
// 如果没有使用大括号,就会报错
const
声明一个只读的常量。一旦声明,常量的值就不能改变。const
一旦声明变量,就必须立即初始化,不能留到以后赋值,否则会报错。const
的作用域与let
命令相同:只在声明所在的块级作用域内有效。const
命令声明的常量也是不提升,同样存在暂时性死区,只能在声明的位置后面使用。const
声明的常量,也与let
一样不可重复声明。const
实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。
对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。
对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const
只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。
var
(ES5)function
(ES5)let
(ES6)const
(ES6)import
(ES6)class
(ES6)顶层对象,在浏览器环境指的是window
对象,在 Node 指的是global
对象。ES5 之中,顶层对象的属性与全局变量是等价的。
ES6 为了改变这一点,一方面规定,为了保持兼容性,var
命令和function
命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let
命令、const
命令、class
命令声明的全局变量,不属于顶层对象的属性。
ES5 的顶层对象,本身也是一个问题,因为它在各种实现里面是不统一的:
window
,但 Node 和 Web Worker 没有window
。self
也指向顶层对象,但是 Node 没有self
。global
,但其他环境都不支持。现在有一个提案,在语言标准的层面,引入global
作为顶层对象。也就是说,在所有环境下,global
都是存在的,都可以从它拿到顶层对象。
ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。
// 可以从数组中提取值,按照对应位置,对变量赋值
let [a, b, c] = [1, 2, 3];
本质上,这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值:
let [foo, [[bar], baz]] = [1, [[2], 3]];
foo // 1
bar // 2
baz // 3
let [ , , third] = ["foo", "bar", "baz"];
third // "baz"
let [x, , y] = [1, 2, 3];
x // 1
y // 3
let [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4]
let [x, y, ...z] = ['a'];
x // "a"
y // undefined
z // []
如果解构不成功,变量的值就等于undefined
:
let [foo] = [];
let [bar, foo] = [1];
// 以上两种情况都属于解构不成功
// foo的值都会等于undefined
另一种情况是不完全解构,即等号左边的模式,只匹配一部分的等号右边的数组。这种情况下,解构依然可以成功:
let [x, y] = [1, 2, 3];
x // 1
y // 2
let [a, [b], d] = [1, [2, 3], 4];
a // 1
b // 2
d // 4
如果等号的右边不是数组(严格地说,不是可遍历的结构),那么将会报错:
// 报错
let [foo] = 1;
let [foo] = false;
let [foo] = NaN;
let [foo] = undefined;
let [foo] = null;
let [foo] = {};
事实上,只要某种数据结构具有 Iterator 接口,都可以采用数组形式的解构赋值。
解构赋值允许指定默认值:
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
,默认值才会生效:
let [x = 1] = [undefined];
x // 1
let [x = 1] = [null];
x // null, 因为null不严格等于undefined
如果默认值是一个表达式,那么这个表达式是惰性求值的,即只有在用到的时候,才会求值。
默认值可以引用解构赋值的其他变量,但该变量必须已经声明:
let [x = 1, y = x] = []; // x=1; y=1
let [x = 1, y = x] = [2]; // x=2; y=2
let [x = 1, y = x] = [1, 2]; // x=1; y=2
let [x = y, y = 1] = []; // ReferenceError: y is not defined
解构不仅可以用于数组,还可以用于对象:
let { foo, bar } = { foo: "aaa", bar: "bbb" };
foo // "aaa"
bar // "bbb"
对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值:
let { bar, foo } = { foo: "aaa", bar: "bbb" };
foo // "aaa"
bar // "bbb"
let { baz } = { foo: "aaa", bar: "bbb" };
baz // undefined
对象的解构赋值是下面形式的简写:
let { foo: foo, bar: bar } = { foo: "aaa", bar: "bbb" };
// 对象的解构赋值的内部机制
// 是先找到同名属性,然后再赋给对应的变量
// 真正被赋值的是后者,而不是前者
与数组一样,解构也可以用于嵌套结构的对象:
et obj = {
p: [
'Hello',
{ y: 'World' }
]
};
let { p: [x, { y }] } = obj;
x // "Hello"
y // "World"
// 注意,这时p是模式,不是变量,因此不会被赋值
// 如果p也要作为变量赋值,可以写成下面这样
let { p, p: [x, { y }] } = obj;
x // "Hello"
y // "World"
p // ["Hello", {y: "World"}]
对象的解构也可以指定默认值,默认值生效的条件是,对象的属性值严格等于undefined
,如果解构失败,变量的值等于undefined
:
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
var { message: msg = 'Something went wrong' } = {};
msg // "Something went wrong"
var {x = 3} = {x: undefined};
x // 3
var {x = 3} = {x: null};
x // null
let {foo} = {bar: 'baz'};
foo // undefined
如果要将一个已经声明的变量用于解构赋值,必须非常小心:
// 错误的写法
let x;
{x} = {x: 1};
// SyntaxError: syntax error
// 因为 JavaScript 引擎会将{x}理解成一个代码块
// 从而发生语法错误
// 正确的写法
let x;
({x} = {x: 1});
解构赋值允许等号左边的模式之中,不放置任何变量名:
({} = [true, false]);
({} = 'abc');
({} = []);
对象的解构赋值,可以很方便地将现有对象的方法,赋值到某个变量:
let { log, sin, cos } = Math;
// 代码将Math对象的对数、正弦、余弦三个方法,赋值到对应的变量上
字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象:
const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"
类似数组的对象都有一个length属性,因此还可以对这个属性解构赋值:
let {length : len} = 'hello';
len // 5
解构赋值时,如果等号右边是数值和布尔值,则会先转为对象:
let {toString: s} = 123;
s === Number.prototype.toString // true
let {toString: s} = true;
s === Boolean.prototype.toString // true
解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于undefined
和null
无法转为对象,所以对它们进行解构赋值,都会报错。
function add([x, y]){
return x + y;
}
add([1, 2]); // 3
// 函数add的参数表面上是一个数组
// 但在传入参数的那一刻,数组参数就被解构成变量x和y
// 对于函数内部的代码来说,它们能感受到的参数就是x和y
解构赋值虽然很方便,但是解析起来并不容易。ES6 的规则是,只要有可能导致解构的歧义,就不得使用圆括号。
但是,这条规则实际上不那么容易辨别,处理起来相当麻烦。因此,建议只要有可能,就不要在模式中放置圆括号。
// 1)变量声明语句
// 全部报错
let [(a)] = [1];
let {x: (c)} = {};
let ({x: c}) = {};
let {(x: c)} = {};
let {(x): c} = {};
let { o: ({ p: p }) } = { o: { p: 2 } };
// 2)函数参数
// 函数参数也属于变量声明,因此不能带有圆括号。
// 全部报错
function f([(z)]) { return z; }
function f([z,(x)]) { return x; }
// 3)赋值语句的模式
// 全部报错: 将整个模式放在圆括号之中,导致报错
({ p: a }) = { p: 42 };
([a]) = [5];
// 报错: 将一部分模式放在圆括号之中,导致报错
[({ p: a }), { x: c }] = [{}, {}];
// 赋值语句的非模式部分,可以使用圆括号
[(b)] = [3]; // 正确
({ p: (d) } = {}); // 正确
[(parseInt.prop)] = [3]; // 正确
// 1)交换变量的值
let x = 1;
let y = 2;
[x, y] = [y, x];
// 2)从函数返回多个值
// 返回一个数组
function example() {
return [1, 2, 3];
}
let [a, b, c] = example();
// 返回一个对象
function example() {
return {
foo: 1,
bar: 2
};
}
let { foo, bar } = example();
// 3)函数参数的定义
// 参数是一组有次序的值
function f([x, y, z]) { ... }
f([1, 2, 3]);
// 参数是一组无次序的值
function f({x, y, z}) { ... }
f({z: 3, y: 2, x: 1});
// 4)提取 JSON 数据
let jsonData = {
id: 42,
status: "OK",
data: [867, 5309]
};
let { id, status, data: number } = jsonData;
console.log(id, status, number); // 42, "OK", [867, 5309]
// 5)函数参数的默认值
jQuery.ajax = function (url, {
async = true,
beforeSend = function () {},
cache = true,
complete = function () {},
crossDomain = false,
global = true,
// ... more config
} = {}) {
// ... do stuff
};
// 6)遍历 Map 结构
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
// 获取键名
for (let [key] of map) {
// ...
}
// 获取键值
for (let [,value] of map) {
// ...
}
// 7)输入模块的指定方法
const { SourceMapConsumer, SourceNode } = require("source-map");
ES6 加强了对 Unicode 的支持,并且扩展了字符串对象。
JavaScript 允许采用\uxxxx
形式表示一个字符,其中xxxx
表示字符的 Unicode 码点。但是,这种表示法只限于码点在\u0000
~\uFFFF
之间的字符。超出这个范围的字符,必须用两个双字节的形式表示:
"\u20BB7"
// " 7"
"\uD842\uDFB7"
// "?"
ES6 对这一点做出了改进,只要将码点放入大括号,就能正确解读该字符:
"\u{20BB7}"
// "?"
"\u{41}\u{42}\u{43}"
// "ABC"
'\u{1F680}' === '\uD83D\uDE80'
// true
// 大括号表示法与四字节的 UTF-16 编码是等价的
JavaScript 共有 6 种方法可以表示一个字符:
'\z' === 'z' // true
'\172' === 'z' // true
'\x7A' === 'z' // true
'\u007A' === 'z' // true
'\u{7A}' === 'z' // true
JavaScript 内部,字符以 UTF-16 的格式储存,每个字符固定为2个字节。对于那些需要4个字节储存的字符(Unicode 码点大于0xFFFF的字符),JavaScript 会认为它们是两个字符。
对于这种4个字节的字符,JavaScript 不能正确处理,字符串长度会误判为2,而且charAt
方法无法读取整个字符,charCodeAt
方法只能分别返回前两个字节和后两个字节的值。
ES6 提供了codePointAt
方法,能够正确处理 4 个字节储存的字符,返回一个字符的码点。
codePointAt
方法会正确返回 32 位的 UTF-16 字符的码点。对于那些两个字节储存的常规字符,它的返回结果与charCodeAt
方法相同。
codePointAt
方法返回的是码点的十进制值,如果想要十六进制的值,可以使用toString
方法转换一下。
var s1 = "?";
s1.length // 2
s1.charAt(0) // ''
s1.charAt(1) // ''
s1.charCodeAt(0) // 55362
s1.charCodeAt(1) // 57271
let s = '?a';
s.codePointAt(0) // 134071
s.codePointAt(1) // 57271
s.codePointAt(2) // 97
s.codePointAt(0).toString(16) // "20bb7"
s.codePointAt(2).toString(16) // "61"
// for...of循环会正确识别 32 位的 UTF-16 字符
for (let ch of s) {
console.log(ch.codePointAt(0).toString(16));
}
// 20bb7
// 61
ES5 提供String.fromCharCode
方法,用于从码点返回对应字符,但是这个方法不能识别 32 位的 UTF-16 字符(Unicode 编号大于0xFFFF
)。
ES6 提供了String.fromCodePoint
方法,可以识别大于0xFFFF
的字符,弥补了String.fromCharCode
方法的不足。在作用上,正好与codePointAt
方法相反。
注意,fromCodePoint
方法定义在String
对象上,而codePointAt
方法定义在字符串的实例对象上。
ES6 为字符串添加了遍历器接口,使得字符串可以被for...of
循环遍历。
// for..of遍历器最大的优点是
// 可以识别大于0xFFFF的码点
// 传统的for循环无法识别这样的码点。
for (let codePoint of 'foo') {
console.log(codePoint)
}
// "f"
// "o"
// "o"
let text = String.fromCodePoint(0x20BB7);
for (let i = 0; i < text.length; i++) {
console.log(text[i]);
}
// " "
// " "
for (let i of text) {
console.log(i);
}
// "?"
许多欧洲语言有语调符号和重音符号。为了表示它们,Unicode 提供了两种方法:
遗憾的是,这两种表示方法, JavaScript 不能识别:
'\u01D1'==='\u004F\u030C' //false
'\u01D1'.length // 1
'\u004F\u030C'.length // 2
ES6 提供字符串实例的normalize()
方法,用来将字符的不同表示方法统一为同样的形式,这称为 Unicode 正规化:
'\u01D1'.normalize() === '\u004F\u030C'.normalize()
// true
不过,normalize
方法目前不能识别三个或三个以上字符的合成。这种情况下,还是只能使用正则表达式,通过 Unicode 编号区间判断。
传统上,JavaScript 只有indexOf
方法,可以用来确定一个字符串是否包含在另一个字符串中。ES6 又提供了三种新方法。
includes()
:返回布尔值,表示是否找到了参数字符串。startsWith()
:返回布尔值,表示参数字符串是否在原字符串的头部。endsWith()
:返回布尔值,表示参数字符串是否在原字符串的尾部。let s = 'Hello world!';
s.startsWith('Hello') // true
s.endsWith('!') // true
s.includes('o') // true
// 这三个方法都支持第二个参数,表示开始搜索的位置。
s.startsWith('world', 6) // true
s.endsWith('Hello', 5) // true
s.includes('Hello', 6) // false
// 使用第二个参数n时
// endsWith的行为与其他两个方法有所不同
// 它针对前n个字符
// 而其他两个方法针对从第n个位置直到字符串结束。
repeat
方法返回一个新字符串,表示将原字符串重复n次。
'x'.repeat(3) // "xxx"
'hello'.repeat(2) // "hellohello"
'na'.repeat(0) // ""
// 参数如果是小数,会被取整。
'na'.repeat(2.9) // "nana"
// 如果repeat的参数是负数或者Infinity,会报错。
'na'.repeat(Infinity) // RangeError
'na'.repeat(-1) // RangeError
// 如果参数是 0 到-1 之间的小数,则等同于 0
// 因为会先进行取整运算
// 0 到-1 之间的小数,取整以后等于-0,repeat视同为 0。
'na'.repeat(-0.9) // ""
// 参数NaN等同于 0
'na'.repeat(NaN) // ""
// 如果repeat的参数是字符串,则会先转换成数字。
'na'.repeat('na') // ""
'na'.repeat('3') // "nanana"
ES2017 引入了字符串补全长度的功能。如果某个字符串不够指定长度,会在头部或尾部补全。padStart()
用于头部补全,padEnd()
用于尾部补全。
// padStart()和padEnd()一共接受两个参数
// 第一个参数是字符串补全生效的最大长度
// 第二个参数是用来补全的字符串
'x'.padStart(5, 'ab') // 'ababx'
'x'.padStart(4, 'ab') // 'abax'
'x'.padEnd(5, 'ab') // 'xabab'
'x'.padEnd(4, 'ab') // 'xaba'
// 如果原字符串的长度,等于或大于最大长度
// 则字符串补全不生效,返回原字符串
'xxx'.padStart(2, 'ab') // 'xxx'
'xxx'.padEnd(2, 'ab') // 'xxx'
// 如果用来补全的字符串与原字符串
// 两者的长度之和超过了最大长度
// 则会截去超出位数的补全字符串
'abc'.padStart(10, '0123456789') // '0123456abc'
// 如果省略第二个参数,默认使用空格补全长度
'x'.padStart(4) // ' x'
'x'.padEnd(4) // 'x '
matchAll
方法返回一个正则表达式在当前字符串的所有匹配。
传统的 JavaScript 语言,输出模板通常是这样写的(下面使用了 jQuery 的方法)。
$('#result').append(
'There are ' + basket.count + ' ' +
'items in your basket, ' +
'' + basket.onSale +
' are on sale!'
);
上面这种写法相当繁琐不方便,ES6 引入了模板字符串解决这个问题:
// 使用模板字符串表示多行字符串
// 所有的空格和缩进都会被保留在输出之中
//
// 模板字符串中嵌入变量,需要将变量名写在${}之中
// * 大括号内部可以放入任意的 JavaScript 表达式
// * 可以进行运算,以及引用对象属性,还能调用函数
// * 如果大括号中的值不是字符串,将按照一般的规则转为字符串
// 默认会调用对象的toString方法
$('#result').append(`
There are ${basket.count} items
in your basket, ${basket.onSale}
are on sale!
`);
模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。如果在模板字符串中需要使用反引号,则前面要用反斜杠转义。
// `...`字符串中,所有模板字符串的空格和换行,都是被保留的
// 比如标签前面会有一个换行
// 如果你不想要这个换行,可以使用trim方法消除它。
$('#list').html(`
- first
- second
`.trim());
模板字符串甚至还能嵌套:
const tmpl = addrs => `
${addrs.map(addr => `
<tr><td>${addr.first}
${addr.last}
`).join('')}
</table>
`;
const data = [
{ first: '' , last: 'Bond' },
{ first: 'Lars', last: '' },
];
console.log(tmpl(data));
//
//
//
// Bond
//
// Lars
//
//
//
4.11 标签模板
模板字符串可以紧跟在一个函数名后面,该函数将被调用来处理这个模板字符串。这被称为“标签模板”功能(tagged template):
alert`123`
// 等同于
alert(123)
// 如果模板字符里面有变量,就不是简单的调用了
// 而是会将模板字符串先处理成多个参数,再调用函数
let a = 5;
let b = 10;
tag`Hello ${ a + b } world ${ a * b }`;
// 等同于
tag(['Hello ', ' world ', ''], 15, 50);
4.12 String.raw()
ES6 还为原生的 String 对象,提供了一个raw
方法。
String.raw
方法,往往用来充当模板字符串的处理函数,返回一个斜杠都被转义(即斜杠前面再加一个斜杠)的字符串,对应于替换变量后的模板字符串:
String.raw`Hi\n${2+3}!`;
// 返回 "Hi\\n5!"
String.raw
方法可以作为处理模板字符串的基本方法,它会将所有变量替换,而且对斜杠进行转义,方便下一步作为字符串来使用。
String.raw
方法也可以作为正常的函数使用。这时,它的第一个参数,应该是一个具有raw
属性的对象,且raw
属性的值应该是一个数组。
String.raw({ raw: 'test' }, 0, 1, 2);
// 't0e1s2t'
// 等同于
String.raw({ raw: ['t','e','s','t'] }, 0, 1, 2);
5. 正则的扩展
5.1 RegExp 构造函数
在 ES5 中,RegExp构造函数的参数有两种情况。
第一种情况是,参数是字符串,这时第二个参数表示正则表达式的修饰符(flag)。
var regex = new RegExp('xyz', 'i');
// 等价于
var regex = /xyz/i;
第二种情况是,参数是一个正则表示式,这时会返回一个原有正则表达式的拷贝。
var regex = new RegExp(/xyz/i);
// 等价于
var regex = /xyz/i;
但是,ES5 不允许此时使用第二个参数添加修饰符,否则会报错。
var regex = new RegExp(/xyz/, 'i');
// Uncaught TypeError: Cannot supply flags when constructing one RegExp from another
ES6 改变了这种行为。如果RegExp构造函数第一个参数是一个正则对象,那么可以使用第二个参数指定修饰符。而且,返回的正则表达式会忽略原有的正则表达式的修饰符,只使用新指定的修饰符。
new RegExp(/abc/ig, 'i').flags
// "i"
5.2 字符串的正则方法
字符串对象共有 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]
5.3 u 修饰符
ES6 对正则表达式添加了u
修饰符,含义为“Unicode 模式”,用来正确处理大于\uFFFF
的 Unicode 字符。也就是说,会正确处理四个字节的 UTF-16 编码:
/^\uD83D/u.test('\uD83D\uDC2A') // false
/^\uD83D/.test('\uD83D\uDC2A') // true
5.4 RegExp.prototype.unicode 属性
正则实例对象新增unicode属性,表示是否设置了u修饰符:
const r1 = /hello/;
const r2 = /hello/u;
r1.unicode // false
r2.unicode // true
5.5 y 修饰符
y
修饰符,叫做“粘连”(sticky)修饰符。
y
修饰符的作用与g
修饰符类似,也是全局匹配,后一次匹配都从上一次匹配成功的下一个位置开始。不同之处在于,g
修饰符只要剩余位置中存在匹配就可,而y
修饰符确保匹配必须从剩余的第一个位置开始,这也就是“粘连”的涵义。
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
lastIndex
属性指定每次搜索的开始位置:
const REGEX = /a/g;
// 指定从2号位置(y)开始匹配
REGEX.lastIndex = 2;
5.6 RegExp.prototype.sticky 属性
与y
修饰符相匹配,ES6 的正则实例对象多了sticky
属性,表示是否设置了y
修饰符。
var r = /hello\d/y;
r.sticky // true
5.7 RegExp.prototype.flags 属性
ES6 为正则表达式新增了flags属性,会返回正则表达式的修饰符。
// ES5 的 source 属性
// 返回正则表达式的正文
/abc/ig.source
// "abc"
// ES6 的 flags 属性
// 返回正则表达式的修饰符
/abc/ig.flags
// 'gi'
5.8 s 修饰符:dotAll 模式
ES2018 引入s
修饰符,使得.
可以匹配任意单个字符。
/foo.bar/s.test('foo\nbar') // true
这被称为dotAll模式,即点(dot)代表一切字符。所以,正则表达式还引入了一个dotAll
属性,返回一个布尔值,表示该正则表达式是否处在dotAll模式。
const re = /foo.bar/s;
// 另一种写法
// const re = new RegExp('foo.bar', 's');
re.test('foo\nbar') // true
re.dotAll // true
re.flags // 's'
/s
修饰符和多行修饰符/m
不冲突,两者一起使用的情况下,.
匹配所有字符,而^
和$
匹配每一行的行首和行尾。
5.9 后行断言
JavaScript 语言的正则表达式,只支持先行断言(lookahead)和先行否定断言(negative lookahead),不支持后行断言(lookbehind)和后行否定断言(negative lookbehind)。ES2018 引入了后行断言。
5.10 Unicode 属性类
ES2018 引入了一种新的类的写法\p{…}和\P{…},允许正则表达式匹配符合 Unicode 某种属性的所有字符。
const regexGreekSymbol = /\p{Script=Greek}/u;
regexGreekSymbol.test('π') // true
上面代码中,\p{Script=Greek}
指定匹配一个希腊文字母,所以匹配π
成功。
5.11 具名组匹配
正则表达式使用圆括号进行组匹配。
ES2018 引入了具名组匹配(Named Capture Groups),允许为每一个组匹配指定一个名字,既便于阅读代码,又便于引用。
const RE_DATE = /(?\d{4})-(?\d{2})-(?\d{2})/ ;
const matchObj = RE_DATE.exec('1999-12-31');
const year = matchObj.groups.year; // 1999
const month = matchObj.groups.month; // 12
const day = matchObj.groups.day; // 31
具名组匹配等于为每一组匹配加上了 ID,便于描述匹配的目的。如果组的顺序变了,也不用改变匹配后的处理代码。
如果具名组没有匹配,那么对应的groups
对象属性会是undefined
。
如果要在正则表达式内部引用某个“具名组匹配”,可以使用\k<组名>
的写法。
6. 数值的扩展
6.1 二进制与八进制
ES6 提供了二进制和八进制数值的新的写法,分别用前缀0b
(或0B
)和0o
(或0O
)表示。
0b111110111 === 503 // true
0o767 === 503 // true
从 ES5 开始,在严格模式之中,八进制就不再允许使用前缀0
表示,ES6 进一步明确,要使用前缀0o
表示。
如果要将0b
和0o
前缀的字符串数值转为十进制,要使用Number
方法。
Number('0b111') // 7
Number('0o10') // 8
6.2 Number.isFinite(), Number.isNaN()
ES6 在Number
对象上,新提供了Number.isFinite()
和Number.isNaN()
两个方法。
Number.isFinite()
用来检查一个数值是否为有限的(finite),即不是Infinity
。
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.isFinite
一律返回false
。
Number.isNaN()
用来检查一个值是否为NaN
。
Number.isNaN(NaN) // true
Number.isNaN(15) // false
Number.isNaN('15') // false
Number.isNaN(true) // false
Number.isNaN(9/NaN) // true
Number.isNaN('true' / 0) // true
Number.isNaN('true' / 'true') // true
如果参数类型不是NaN
,Number.isNaN
一律返回false
。
它们与传统的全局方法isFinite()
和isNaN()
的区别在于,传统方法先调用Number()
将非数值的值转为数值,再进行判断。而这两个新方法只对数值有效。
isFinite(25) // true
isFinite("25") // true
Number.isFinite(25) // true
Number.isFinite("25") // false
isNaN(NaN) // true
isNaN("NaN") // true
Number.isNaN(NaN) // true
Number.isNaN("NaN") // false
Number.isNaN(1) // false
6.3 Number.parseInt(), Number.parseFloat()
ES6 将全局方法parseInt()
和parseFloat()
,移植到Number
对象上面,行为完全保持不变。
// ES5的写法
parseInt('12.34') // 12
parseFloat('123.45#') // 123.45
// ES6的写法
Number.parseInt('12.34') // 12
Number.parseFloat('123.45#') // 123.45
// 这样做的目的,是逐步减少全局性方法,使得语言逐步模块化。
Number.parseInt === parseInt // true
Number.parseFloat === parseFloat // true
6.4 Number.isInteger()
Number.isInteger()
用来判断一个数值是否为整数。
Number.isInteger(25) // true
Number.isInteger(25.1) // false
// JavaScript 内部,整数和浮点数采用的是同样的储存方法
// 所以 25 和 25.0 被视为同一个值。
Number.isInteger(25) // true
Number.isInteger(25.0) // true
// 如果参数不是数值,Number.isInteger返回false。
Number.isInteger() // false
Number.isInteger(null) // false
Number.isInteger('15') // false
Number.isInteger(true) // false
注意,由于 JavaScript 采用 IEEE 754 标准,数值精度最多可以达到 53 个二进制位(1 个隐藏位与 52 个有效位)。如果数值的精度超过这个限度,第54位及后面的位就会被丢弃,这种情况下,Number.isInteger
可能会误判:
Number.isInteger(3.0000000000000002) // true
类似的情况还有,如果一个数值的绝对值小于Number.MIN_VALUE(5E-324)
,即小于 JavaScript 能够分辨的最小值,会被自动转为 0。这时,Number.isInteger
也会误判:
Number.isInteger(5E-324) // false
Number.isInteger(5E-325) // true
如果对数据精度的要求较高,不建议使用Number.isInteger()
判断一个数值是否为整数。
6.5 Number.EPSILON
ES6 在Number
对象上面,新增一个极小的常量Number.EPSILON
。根据规格,它表示 1 与大于 1 的最小浮点数之间的差。
Number.EPSILON
实际上是 JavaScript 能够表示的最小精度。误差如果小于这个值,就可以认为已经没有意义了,即不存在误差了。
6.6 安全整数和 Number.isSafeInteger()
JavaScript 能够准确表示的整数范围在-2^53
到2^53
之间(不含两个端点),超过这个范围,无法精确表示这个值。
ES6 引入了Number.MAX_SAFE_INTEGER
和Number.MIN_SAFE_INTEGER
这两个常量,用来表示这个范围的上下限。Number.isSafeInteger()
则是用来判断一个整数是否落在这个范围之内。
Number.isSafeInteger('a') // false
Number.isSafeInteger(null) // false
Number.isSafeInteger(NaN) // false
Number.isSafeInteger(Infinity) // false
Number.isSafeInteger(-Infinity) // false
Number.isSafeInteger(3) // true
Number.isSafeInteger(1.2) // false
Number.isSafeInteger(9007199254740990) // true
Number.isSafeInteger(9007199254740992) // false
Number.isSafeInteger(Number.MIN_SAFE_INTEGER - 1) // false
Number.isSafeInteger(Number.MIN_SAFE_INTEGER) // true
Number.isSafeInteger(Number.MAX_SAFE_INTEGER) // true
Number.isSafeInteger(Number.MAX_SAFE_INTEGER + 1) // false
实际使用这个函数时,需要注意。验证运算结果是否落在安全整数的范围内,不要只验证运算结果,而要同时验证参与运算的每个值。
6.7 Math 对象的扩展
ES6 在 Math 对象上新增了 17 个与数学相关的方法。所有这些方法都是静态方法,只能在 Math 对象上调用。
Math.trunc()
Math.trunc
方法用于去除一个数的小数部分,返回整数部分。
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.trunc
内部使用Number
方法将其先转为数值。
对于空值和无法截取整数的值,返回NaN
。
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 位的带符号整数。该方法等同于(a * b)|0
的效果(超过 32 位的部分溢出)。
Math.fround()
Math.fround
方法返回一个数的32位单精度浮点数形式。
Math.fround
方法的主要作用,是将64位双精度浮点数转为32位单精度浮点数。
Math.hypot()
Math.hypot
方法返回所有参数的平方和的平方根。
Math.expm1()
Math.expm1(x)
返回Math.exp(x) - 1
。
Math.log1p()
Math.log1p(x)
方法返回1 + x
的自然对数,即Math.log(1 + x)
。如果x小于-1
,返回NaN
。
Math.log10()
Math.log10(x)
返回以 10 为底的x
的对数。如果x
小于 0,则返回 NaN
。
Math.log2()
Math.log2(x)
返回以 2 为底的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)
6.8 指数运算符
ES2016 新增了一个指数运算符(**
)。
2 ** 2 // 4
2 ** 3 // 8
这个运算符的一个特点是右结合,而不是常见的左结合。多个指数运算符连用时,是从最右边开始计算的。
// 相当于 2 ** (3 ** 2)
2 ** 3 ** 2
// 512
上面代码中,首先计算的是第二个指数运算符,而不是第一个。
指数运算符可以与等号结合,形成一个新的赋值运算符(**=
)。
let a = 1.5;
a **= 2;
// 等同于 a = a * a;
let b = 4;
b **= 3;
// 等同于 b = b * b * b;
注意,V8 引擎的指数运算符与Math.pow的实现不相同,对于特别大的运算结果,两者会有细微的差异。
7. 函数的扩展
7.1 函数参数的默认值
基本用法
ES6 之前,不能直接为函数的参数指定默认值,只能采用变通的方法。
ES6 允许为函数的参数设置默认值,即直接写在参数定义的后面:
function log(x, y = 'World') {
console.log(x, y);
}
log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello
function Point(x = 0, y = 0) {
this.x = x;
this.y = y;
}
const p = new Point();
p // { x: 0, y: 0 }
参数变量是默认声明的,所以不能用let
或const
再次声明。
使用参数默认值时,函数不能有同名参数。
参数默认值不是传值的,而是每次都重新计算默认值表达式的值。也就是说,参数默认值是惰性求值的:
let x = 99;
function foo(p = x + 1) {
console.log(p);
}
foo() // 100
x = 100;
foo() // 101
与解构赋值默认值结合使用
参数默认值可以与解构赋值的默认值,结合起来使用:
function foo({x, y = 5}) {
console.log(x, y);
}
foo({}) // undefined 5
foo({x: 1}) // 1 5
foo({x: 1, y: 2}) // 1 2
foo() // TypeError: Cannot read property 'x' of undefined
// ---------------------------------------
// 如果函数foo调用时没提供参数
// 变量x和y就不会生成,从而报错
// 通过提供函数参数的默认值,就可以避免这种情况
function foo({x, y = 5} = {}) {
console.log(x, y);
}
foo() // undefined 5
function fetch(url, { body = '', method = 'GET', headers = {} } = {}) {
console.log(method);
}
fetch('http://example.com')
参数默认值的位置
通常情况下,定义了默认值的参数,应该是函数的尾参数。如果非尾部的参数设置默认值,实际上这个参数是没法省略的,除非显式输入undefined
。
如果传入undefined
,将触发该参数等于默认值,null
则没有这个效果:
function foo(x = 5, y = 6) {
console.log(x, y);
}
foo(undefined, null)
// 5 null
函数的 length 属性
指定了默认值以后,函数的length
属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length
属性将失真。
如果设置了默认值的参数不是尾参数,那么length
属性也不再计入后面的参数了。
作用域
一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的。
7.2 rest 参数
ES6 引入 rest
参数(形式为...变量名
),用于获取函数的多余参数,这样就不需要使用arguments
对象了。rest
参数搭配的变量是一个数组,该变量将多余的参数放入数组中。
function add(...values) {
let sum = 0;
for (var val of values) {
sum += val;
}
return sum;
}
add(2, 5, 3) // 10
// rest参数的写法
const sortNumbers = (...numbers) => numbers.sort();
注意,rest
参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。
函数的length
属性,不包括 rest
参数。
7.3 严格模式
从 ES5 开始,函数内部可以设定为严格模式:
function doSomething(a, b) {
'use strict';
// code
}
ES2016 做了一点修改,规定只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错。
两种方法可以规避这种限制。第一种是设定全局性的严格模式,这是合法的:
'use strict';
function doSomething(a, b = a) {
// code
}
第二种是把函数包在一个无参数的立即执行函数里面:
const doSomething = (function () {
'use strict';
return function(value = 42) {
return value;
};
}());
7.4 name 属性
函数的name
属性,返回该函数的函数名:
function foo() {}
foo.name // "foo"
如果将一个匿名函数赋值给一个变量,ES5 的name
属性,会返回空字符串,而 ES6 的name
属性会返回实际的函数名:
var f = function () {};
// ES5
f.name // ""
// ES6
f.name // "f"
如果将一个具名函数赋值给一个变量,则 ES5 和 ES6 的name
属性都返回这个具名函数原本的名字:
const bar = function baz() {};
// ES5
bar.name // "baz"
// ES6
bar.name // "baz"
Function
构造函数返回的函数实例,name
属性的值为anonymous
:
(new Function).name // "anonymous"
bind返回的函数,name属性值会加上bound前缀。
function foo() {};
foo.bind({}).name // "bound foo"
(function(){}).bind({}).name // "bound "
7.5 箭头函数
基本用法
ES6 允许使用“箭头”(=>
)定义函数。
var f = v => v;
// 等同于
var f = function (v) {
return v;
};
如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分:
var f = () => 5;
// 等同于
var f = function () { return 5 };
var sum = (num1, num2) => num1 + num2;
// 等同于
var sum = function(num1, num2) {
return num1 + num2;
};
如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return
语句返回:
var sum = (num1, num2) => { return num1 + num2; }
由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错:
// 报错
let getTempItem = id => { id: id, name: "Temp" };
// 不报错
let getTempItem = id => ({ id: id, name: "Temp" });
下面是一种特殊情况,虽然可以运行,但会得到错误的结果:
let foo = () => { a: 1 };
foo() // undefined
如果箭头函数只有一行语句,且不需要返回值,可以采用下面的写法,就不用写大括号了:
let fn = () => void doesNotReturn();
箭头函数可以与变量解构结合使用:
const full = ({ first, last }) => first + ' ' + last;
// 等同于
function full(person) {
return person.first + ' ' + person.last;
}
箭头函数使得表达更加简洁:
const isEven = n => n % 2 === 0;
const square = n => n * n;
箭头函数的一个用处是简化回调函数:
// 正常函数写法
[1,2,3].map(function (x) {
return x * x;
});
// 箭头函数写法
[1,2,3].map(x => x * x);
使用注意点
箭头函数有几个使用注意点:
- 函数体内的
this
对象,就是定义时所在的对象,而不是使用时所在的对象。
- 不可以当作构造函数,也就是说,不可以使用
new
命令,否则会抛出一个错误。
- 不可以使用
arguments
对象,该对象在函数体内不存在。如果要用,可以用 rest
参数代替。
- 不可以使用
yield
命令,因此箭头函数不能用作 Generator
函数。
箭头函数可以让this
指向固定化,这种特性很有利于封装回调函数:
var handler = {
id: '123456',
init: function() {
document.addEventListener('click',
event => this.doSomething(event.type), false);
},
doSomething: function(type) {
console.log('Handling ' + type + ' for ' + this.id);
}
};
// 上面代码的init方法中,使用了箭头函数
// 这导致这个箭头函数里面的this,总是指向handler对象
// 否则,回调函数运行时,this.doSomething这一行会报错
// 因为此时this指向document对象。
this
指向的固定化,并不是因为箭头函数内部有绑定this
的机制,实际原因是箭头函数根本没有自己的this
,导致内部的this
就是外层代码块的this
。正是因为它没有this
,所以也就不能用作构造函数。
长期以来,JavaScript 语言的this对象一直是一个令人头痛的问题,在对象方法中使用this
,必须非常小心。箭头函数”绑定”this
,很大程度上解决了这个困扰。
不适用场合
由于箭头函数使得this
从“动态”变成“静态”,下面两个场合不应该使用箭头函数。
第一个场合是定义函数的方法,且该方法内部包括this
:
const cat = {
lives: 9,
jumps: () => {
this.lives--;
}
}
// cat.jumps()方法是一个箭头函数,这是错误的
// 调用cat.jumps()时,如果是普通函数,该方法内部的this指向cat
// 如果写成上面那样的箭头函数,使得this指向全局对象
// 因此不会得到预期结果
第二个场合是需要动态this的时候,也不应使用箭头函数:
var button = document.getElementById('press');
button.addEventListener('click', () => {
this.classList.toggle('on');
});
// 上面代码运行时,点击按钮会报错
// 因为button的监听函数是一个箭头函数
// 导致里面的this就是全局对象
// 如果改成普通函数,this就会动态指向被点击的按钮对象
另外,如果函数体很复杂,有许多行,或者函数内部有大量的读写操作,不单纯是为了计算值,这时也不应该使用箭头函数,而是要使用普通函数,这样可以提高代码可读性。
嵌套的箭头函数
箭头函数内部,还可以再使用箭头函数。
7.6 双冒号运算符
箭头函数可以绑定this
对象,大大减少了显式绑定this
对象的写法(call
、apply
、bind
)。但是,箭头函数并不适用于所有场合,所以现在有一个提案,提出了“函数绑定”(function bind)运算符,用来取代call
、apply
、bind
调用。
函数绑定运算符是并排的两个冒号(::
),双冒号左边是一个对象,右边是一个函数。该运算符会自动将左边的对象,作为上下文环境(即this
对象),绑定到右边的函数上面:
foo::bar;
// 等同于
bar.bind(foo);
foo::bar(...arguments);
// 等同于
bar.apply(foo, arguments);
const hasOwnProperty = Object.prototype.hasOwnProperty;
function hasOwn(obj, key) {
return obj::hasOwnProperty(key);
}
如果双冒号左边为空,右边是一个对象的方法,则等于将该方法绑定在该对象上面:
var method = obj::obj.foo;
// 等同于
var method = ::obj.foo;
let log = ::console.log;
// 等同于
var log = console.log.bind(console);
如果双冒号运算符的运算结果,还是一个对象,就可以采用链式写法:
import { map, takeWhile, forEach } from "iterlib";
getPlayers()
::map(x => x.character())
::takeWhile(x => x.strength > 100)
::forEach(x => console.log(x));
7.7 尾调用优化
什么是尾调用
指某个函数的最后一步是调用另一个函数:
function f(x){
return g(x);
}
以下三种情况,都不属于尾调用:
// 情况一
function f(x){
let y = g(x);
return y;
}
// 情况二
function f(x){
return g(x) + 1;
}
// 情况三
function f(x){
g(x);
}
尾调用不一定出现在函数尾部,只要是最后一步操作即可:
function f(x) {
if (x > 0) {
return m(x)
}
return n(x);
}
尾调用优化
“尾调用优化”(Tail call optimization),即只保留内层函数的调用帧。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用帧只有一项,这将大大节省内存。这就是“尾调用优化”的意义。
注意,只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧,否则就无法进行“尾调用优化”。
尾递归
递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)。但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。
// 非尾递归
function Fibonacci (n) {
if ( n <= 1 ) {return 1};
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
Fibonacci(10) // 89
Fibonacci(100) // 堆栈溢出
Fibonacci(500) // 堆栈溢出
// 尾递归优化过的 Fibonacci 数列实现如下。
function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
if( n <= 1 ) {return ac2};
return Fibonacci2 (n - 1, ac2, ac1 + ac2);
}
Fibonacci2(100) // 573147844013817200000
Fibonacci2(1000) // 7.0330367711422765e+208
Fibonacci2(10000) // Infinity
ES6 中只要使用尾递归,就不会发生栈溢出,相对节省内存。
严格模式
ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。
这是因为在正常模式下,函数内部有两个变量,可以跟踪函数的调用栈:
func.arguments
:返回调用时函数的参数。
func.caller
:返回调用当前函数的那个函数。
尾调用优化发生时,函数的调用栈会改写,因此上面两个变量就会失真。严格模式禁用这两个变量,所以尾调用模式仅在严格模式下生效。
7.8 函数参数的尾逗号
ES2017 允许函数的最后一个参数有尾逗号(trailing comma)。
这样的规定也使得,函数参数与数组和对象的尾逗号规则,保持一致了。
8. 数组的扩展
8.1 扩展运算符
含义
扩展运算符(spread)是三个点(...
)。它好比 rest
参数的逆运算,将一个数组转为用逗号分隔的参数序列。
console.log(...[1, 2, 3])
// 1 2 3
console.log(1, ...[2, 3, 4], 5)
// 1 2 3 4 5
[...document.querySelectorAll('div')]
// [, , ]
function f(v, w, x, y, z) { }
const args = [0, 1];
f(-1, ...args, 2, ...[3]);
如果扩展运算符后面是一个空数组,则不产生任何效果:
[...[], 1]
// [1]
注意,扩展运算符如果放在括号中,JavaScript 引擎就会认为这是函数调用,否则就会报错:
(...[1,2])
// Uncaught SyntaxError: Unexpected number
console.log((...[1,2]))
// Uncaught SyntaxError: Unexpected number
代替函数的 apply 方法
由于扩展运算符可以展开数组,所以不需要 apply
方法,将数组转为函数的参数了
// ES5 的写法
Math.max.apply(null, [14, 3, 77])
// ES6 的写法
Math.max(...[14, 3, 77])
// 等同于
Math.max(14, 3, 77);
扩展运算符的应用
复制数组:
// 数组是复合的数据类型,直接复制的话
// 只是复制了指向底层数据结构的指针,而不是克隆一个全新的数组
// ES5 只能用变通方法来复制数组。
const a1 = [1, 2];
const a2 = a1.concat();
a2[0] = 2;
a1 // [1, 2]
// ES6
const a1 = [1, 2];
const a2 = [...a1]; // 写法一
const [...a2] = a1; // 写法二
合并数组:
const arr1 = ['a', 'b'];
const arr2 = ['c'];
const arr3 = ['d', 'e'];
// ES5 的合并数组
arr1.concat(arr2, arr3);
// [ 'a', 'b', 'c', 'd', 'e' ]
// ES6 的合并数组
[...arr1, ...arr2, ...arr3]
// [ 'a', 'b', 'c', 'd', 'e' ]
// 这两种方法都是浅拷贝,使用的时候需要注意。
结合解构赋值:
// ES5
a = list[0], rest = list.slice(1)
// ES6
[a, ...rest] = list
// 如果将扩展运算符用于数组赋值
// 只能放在参数的最后一位,否则会报错
const [...butLast, last] = [1, 2, 3, 4, 5]; // 报错
const [first, ...middle, last] = [1, 2, 3, 4, 5]; // 报错
字符串:
// 扩展运算符还可以将字符串转为真正的数组。
[...'hello']
// [ "h", "e", "l", "l", "o" ]
// 上面的写法能够正确识别四个字节的 Unicode 字符。
'x\uD83D\uDE80y'.length // 4
[...'x\uD83D\uDE80y'].length // 3
实现了 Iterator 接口的对象:
// 任何 Iterator 接口的对象
// 都可以用扩展运算符转为真正的数组。
let nodeList = document.querySelectorAll('div');
let array = [...nodeList];
// 没有部署 Iterator 接口的类似数组的对象
// 扩展运算符就无法将其转为真正的数组。
let arrayLike = {
'0': 'a',
'1': 'b',
'2': 'c',
length: 3
};
// TypeError: Cannot spread non-iterable object.
let arr = [...arrayLike];
Map 和 Set 结构,Generator 函数:
// 扩展运算符内部调用的是数据结构的 Iterator 接口
// 因此只要具有 Iterator 接口的对象,都可以使用扩展运算符
// 比如 Map 结构:
let map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
]);
let arr = [...map.keys()]; // [1, 2, 3]
// Generator 函数运行后,返回一个遍历器对象
// 因此也可以使用扩展运算符。
const go = function*(){
yield 1;
yield 2;
yield 3;
};
[...go()] // [1, 2, 3]
// 如果对没有 Iterator 接口的对象
// 使用扩展运算符,将会报错
const obj = {a: 1, b: 2};
let arr = [...obj]; // TypeError: Cannot spread non-iterable object
8.2 Array.from()
Array.from
方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set
和 Map
)。
下面是一个类似数组的对象,Array.from
将它转为真正的数组:
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
都可以将它们转为真正的数组:
// NodeList对象
let ps = document.querySelectorAll('p');
Array.from(ps).filter(p => {
return p.textContent.length > 100;
});
// arguments对象
function foo() {
var args = Array.from(arguments);
// ...
}
上面代码中,querySelectorAll
方法返回的是一个类似数组的对象,可以将这个对象转为真正的数组,再使用filter
方法。
只要是部署了 Iterator
接口的数据结构,Array.from
都能将其转为数组:
Array.from('hello')
// ['h', 'e', 'l', 'l', 'o']
let namesSet = new Set(['a', 'b'])
Array.from(namesSet) // ['a', 'b']
上面代码中,字符串和 Set
结构都具有 Iterator
接口,因此可以被Array.from
转为真正的数组。
如果参数是一个真正的数组,Array.from会返回一个一模一样的新数组。
值得提醒的是,扩展运算符(…)也可以将某些数据结构转为数组:
// arguments对象
function foo() {
const args = [...arguments];
}
// NodeList对象
[...document.querySelectorAll('div')]
Array.from
方法还支持类似数组的对象。所谓类似数组的对象,本质特征只有一点,即必须有length
属性。因此,任何有length
属性的对象,都可以通过Array.from
方法转为数组,而此时扩展运算符就无法转换:
Array.from({ length: 3 });
// [ undefined, undefined, undefined ]
Array.from
还可以接受第二个参数,作用类似于数组的map
方法,用来对每个元素进行处理,将处理后的值放入返回的数组:
Array.from(arrayLike, x => x * x);
// 等同于
Array.from(arrayLike).map(x => x * x);
Array.from([1, 2, 3], (x) => x * x)
// [1, 4, 9]
如果map
函数里面用到了this
关键字,还可以传入Array.from
的第三个参数,用来绑定this
。
Array.from()
的另一个应用是,将字符串转为数组,然后返回字符串的长度。因为它能正确处理各种 Unicode 字符,可以避免 JavaScript 将大于\uFFFF
的 Unicode 字符,算作两个字符的 bug:
function countSymbols(string) {
return Array.from(string).length;
}
8.3 Array.of()
Array.of
方法用于将一组值,转换为数组:
Array.of(3, 11, 8) // [3,11,8]
Array.of(3) // [3]
Array.of(3).length // 1
// 这个方法的主要目的,是弥补数组构造函数Array()的不足
// 因为参数个数的不同,会导致Array()的行为有差异。
Array() // []
Array(3) // [, , ,]
Array(3, 11, 8) // [3, 11, 8]
Array.of
基本上可以用来替代Array()
或new Array()
,并且不存在由于参数不同而导致的重载。它的行为非常统一:
Array.of() // []
Array.of(undefined) // [undefined]
Array.of(1) // [1]
Array.of(1, 2) // [1, 2]
Array.of总是返回参数值组成的数组。如果没有参数,就返回一个空数组。
8.4 数组实例的 copyWithin()
数组实例的copyWithin
方法,在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。使用这个方法,会修改当前数组。
Array.prototype.copyWithin(target, start = 0, end = this.length)
// 它接受三个参数:
// target(必需):从该位置开始替换数据。如果为负值,表示倒数。
// start(可选):从该位置开始读取数据,默认为 0。如果为负值,表示倒数。
// end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示倒数。
// 这三个参数都应该是数值,如果不是,会自动转为数值。
[1, 2, 3, 4, 5].copyWithin(0, 3)
// [4, 5, 3, 4, 5]
// 将3号位复制到0号位
[1, 2, 3, 4, 5].copyWithin(0, 3, 4)
// [4, 2, 3, 4, 5]
// -2相当于3号位,-1相当于4号位
[1, 2, 3, 4, 5].copyWithin(0, -2, -1)
// [4, 2, 3, 4, 5]
// 将3号位复制到0号位
[].copyWithin.call({length: 5, 3: 1}, 0, 3)
// {0: 1, 3: 1, length: 5}
// 将2号位到数组结束,复制到0号位
let i32a = new Int32Array([1, 2, 3, 4, 5]);
i32a.copyWithin(0, 2);
// Int32Array [3, 4, 5, 4, 5]
8.5 数组实例的 find() 和 findIndex()
数组实例的find
方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true
的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined
。
[1, 4, -5, 10].find((n) => n < 0)
// -5
[1, 5, 10, 15].find(function(value, index, arr) {
return value > 9;
}) // 10
// find方法的回调函数可以接受三个参数
// 依次为当前的值、当前的位置和原数组
数组实例的findIndex
方法的用法与find
方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1
。
[1, 5, 10, 15].findIndex(function(value, index, arr) {
return value > 9;
}) // 2
function f(v){
return v > this.age;
}
let person = {name: 'John', age: 20};
[10, 12, 26, 15].find(f, person); // 26
这两个方法都可以发现NaN
,弥补了数组的indexOf
方法的不足:
[NaN].indexOf(NaN)
// -1
[NaN].findIndex(y => Object.is(NaN, y))
// 0
8.6 数组实例的 fill()
fill
方法使用给定值,填充一个数组:
['a', 'b', 'c'].fill(7)
// [7, 7, 7]
new Array(3).fill(7)
// [7, 7, 7]
fill
方法用于空数组的初始化非常方便。数组中已有的元素,会被全部抹去。
fill
方法还可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置:
['a', 'b', 'c'].fill(7, 1, 2)
// ['a', 7, 'c']
注意,如果填充的类型为对象,那么被赋值的是同一个内存地址的对象,而不是深拷贝对象:
let arr = new Array(3).fill({name: "Mike"});
arr[0].name = "Ben";
arr
// [{name: "Ben"}, {name: "Ben"}, {name: "Ben"}]
let arr = new Array(3).fill([]);
arr[0].push(5);
arr
// [[5], [5], [5]]
8.7 数组实例的 entries(), keys(), values()
ES6 提供三个新的方法——entries()
,keys()
和values()
——用于遍历数组。它们都返回一个遍历器对象,可以用for...of
循环进行遍历,唯一的区别是keys()
是对键名的遍历、values()
是对键值的遍历,entries()
是对键值对的遍历:
for (let index of ['a', 'b'].keys()) {
console.log(index);
}
// 0
// 1
for (let elem of ['a', 'b'].values()) {
console.log(elem);
}
// 'a'
// 'b'
for (let [index, elem] of ['a', 'b'].entries()) {
console.log(index, elem);
}
// 0 "a"
// 1 "b"
如果不使用for...of
循环,可以手动调用遍历器对象的next
方法,进行遍历:
let letter = ['a', 'b', 'c'];
let entries = letter.entries();
console.log(entries.next().value); // [0, 'a']
console.log(entries.next().value); // [1, 'b']
console.log(entries.next().value); // [2, 'c']
8.8 数组实例的 includes()
Array.prototype.includes
方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的includes
方法类似。ES2016 引入了该方法:
[1, 2, 3].includes(2) // true
[1, 2, 3].includes(4) // false
[1, 2, NaN].includes(NaN) // true
该方法的第二个参数表示搜索的起始位置,默认为0
如果第二个参数为负数,则表示倒数的位置
如果这时它大于数组长度,则会重置为从0开始:
[1, 2, 3].includes(3, 3); // false
[1, 2, 3].includes(3, -1); // true
Map
和 Set
数据结构有一个has
方法,需要注意与includes
区分。
// Map 结构的has方法,是用来查找键名的
Map.prototype.has(key)
WeakMap.prototype.has(key)
Reflect.has(target, propertyKey)
// Set 结构的has方法,是用来查找值的
Set.prototype.has(value)
WeakSet.prototype.has(value)
8.9 数组实例的 flat(), flatMap()
数组的成员有时还是数组,Array.prototype.flat()
用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数据没有影响:
[1, 2, [3, 4]].flat()
// [1, 2, 3, 4]
// flat()默认只会“拉平”一层,如果想要“拉平”多层的嵌套数组
// 可以将flat()方法的参数写成一个整数,表示想要拉平的层数
// 默认为1
[1, 2, [3, [4, 5]]].flat()
// [1, 2, 3, [4, 5]]
[1, 2, [3, [4, 5]]].flat(2)
// [1, 2, 3, 4, 5]
// 如果不管有多少层嵌套,都要转成一维数组
// 可以用Infinity关键字作为参数
[1, [2, [3]]].flat(Infinity)
// [1, 2, 3]
// 如果原数组有空位,flat()方法会跳过空位
[1, 2, , 4, 5].flat()
// [1, 2, 4, 5]
flatMap()
方法对原数组的每个成员执行一个函数(相当于执行Array.prototype.map()
),然后对返回值组成的数组执行flat()
方法。该方法返回一个新数组,不改变原数组:
// 相当于 [[2, 4], [3, 6], [4, 8]].flat()
[2, 3, 4].flatMap((x) => [x, x * 2])
// [2, 4, 3, 6, 4, 8]
flatMap()只能展开一层数组。
// 相当于 [[[2]], [[4]], [[6]], [[8]]].flat()
[1, 2, 3, 4].flatMap(x => [[x * 2]])
// [[2], [4], [6], [8]]
flatMap()
方法的参数是一个遍历函数,该函数可以接受三个参数,分别是当前数组成员、当前数组成员的位置(从零开始)、原数组:
arr.flatMap(function callback(currentValue[, index[, array]]) {
// ...
}[, thisArg])
8.10 数组的空位
数组的空位指,数组的某一个位置没有任何值。比如,Array
构造函数返回的数组都是空位:
Array(3) // [, , ,]
注意,空位不是undefined
,一个位置的值等于undefined
,依然是有值的。空位是没有任何值,in
运算符可以说明这一点:
0 in [undefined, undefined, undefined] // true
0 in [, , ,] // false
上面代码说明,第一个数组的 0
号位置是有值的,第二个数组的 0
号位置没有值。
ES5 对空位的处理,已经很不一致了,大多数情况下会忽略空位。ES6 则是明确将空位转为undefined
。
// Array.from方法会将数组的空位,转为undefined
Array.from(['a',,'b'])
// [ "a", undefined, "b" ]
// 扩展运算符(...)也会将空位转为undefined
[...['a',,'b']]
// [ "a", undefined, "b" ]
// copyWithin()会连空位一起拷贝
[,'a','b',,].copyWithin(2,0) // [,"a",,"a"]
// fill()会将空位视为正常的数组位置
new Array(3).fill('a') // ["a","a","a"]
// for...of循环也会遍历空位
let arr = [, ,];
for (let i of arr) {
console.log(1);
}
// 1
// 1
// entries()、keys()、values()、find()和findIndex()
// 会将空位处理成undefined。
// entries()
[...[,'a'].entries()] // [[0,undefined], [1,"a"]]
// keys()
[...[,'a'].keys()] // [0,1]
// values()
[...[,'a'].values()] // [undefined,"a"]
// find()
[,'a'].find(x => true) // undefined
// findIndex()
[,'a'].findIndex(x => true) // 0
由于空位的处理规则非常不统一,所以建议避免出现空位。
9. 对象的扩展
9.1 属性的简洁表示法
ES6 允许直接写入变量和函数,作为对象的属性和方法。书写更加简洁:
const foo = 'bar';
const baz = {foo};
baz // {foo: "bar"}
// 等同于
const baz = {foo: foo};
function f(x, y) {
return {x, y};
}
// 等同于
function f(x, y) {
return {x: x, y: y};
}
f(1, 2) // Object {x: 1, y: 2}
除了属性简写,方法也可以简写:
const o = {
method() {
return "Hello!";
}
};
// 等同于
const o = {
method: function() {
return "Hello!";
}
};
let birth = '2000/01/01';
const Person = {
name: '张三',
//等同于birth: birth
birth,
// 等同于hello: function ()...
hello() { console.log('我的名字是', this.name); }
};
这种写法用于函数的返回值,会非常方便:
function getPoint() {
const x = 1;
const y = 10;
return {x, y};
}
getPoint()
// {x:1, y:10}
属性的赋值器(setter)和取值器(getter),事实上也是采用这种写法:
const cart = {
_wheels: 4,
get wheels () {
return this._wheels;
},
set wheels (value) {
if (value < this._wheels) {
throw new Error('数值太小了!');
}
this._wheels = value;
}
}
注意,简洁写法的属性名总是字符串,这会导致一些看上去比较奇怪的结果:
const obj = {
class () {}
};
// 等同于
var obj = {
'class': function() {}
};
// 上面代码中,class是字符串
// 所以不会因为它属于关键字,而导致语法解析报错
如果某个方法的值是一个 Generator 函数,前面需要加上星号:
const obj = {
* m() {
yield 'hello world';
}
};
9.2 属性名表达式
JavaScript 定义对象的属性,有两种方法。
// 方法一: 直接用标识符作为属性名
obj.foo = true;
// 方法二: 用表达式作为属性名, 要将表达式放在方括号之内
obj['a' + 'bc'] = 123;
如果使用字面量方式定义对象(使用大括号),在 ES5 中只能使用方法一(标识符)定义属性:
var obj = {
foo: true,
abc: 123
};
ES6 允许字面量定义对象时,用方法二(表达式)作为对象的属性名,即把表达式放在方括号内:
let propKey = 'foo';
let obj = {
[propKey]: true,
['a' + 'bc']: 123
};
let lastWord = 'last word';
const a = {
'first word': 'hello',
[lastWord]: 'world'
};
a['first word'] // "hello"
a[lastWord] // "world"
a['last word'] // "world"
表达式还可以用于定义方法名:
let obj = {
['h' + 'ello']() {
return 'hi';
}
};
obj.hello() // hi
注意,属性名表达式与简洁表示法,不能同时使用,会报错:
// 报错
const foo = 'bar';
const bar = 'abc';
const baz = { [foo] };
// 正确
const foo = 'bar';
const baz = { [foo]: 'abc'};
注意,属性名表达式如果是一个对象,默认情况下会自动将对象转为字符串[object Object],这一点要特别小心:
const keyA = {a: 1};
const keyB = {b: 2};
const myObject = {
[keyA]: 'valueA',
[keyB]: 'valueB'
};
myObject // Object {[object Object]: "valueB"}
// [keyA]和[keyB]得到的都是[object Object]
// 所以[keyB]会把[keyA]覆盖掉
// 而myObject最后只有一个[object Object]属性
9.3 方法的 name 属性
函数的name
属性,返回函数名。对象方法也是函数,因此也有name
属性:
const person = {
sayName() {
console.log('hello!');
},
};
person.sayName.name // "sayName"
如果对象的方法使用了取值函数(getter)和存值函数(setter),则name
属性不是在该方法上面,而是该方法的属性的描述对象的get
和set
属性上面,返回值是方法名前加上get
和set
:
const obj = {
get foo() {},
set foo(x) {}
};
obj.foo.name
// TypeError: Cannot read property 'name' of undefined
const descriptor = Object.getOwnPropertyDescriptor(obj, 'foo');
descriptor.get.name // "get foo"
descriptor.set.name // "set foo"
有两种特殊情况:
bind
方法创造的函数,name
属性返回bound
加上原函数的名字
Function
构造函数创造的函数,name
属性返回anonymous
:Function
构造函数创造的函数,name
属性返回anonymous
(new Function()).name // "anonymous"
var doSomething = function() {
// ...
};
doSomething.bind().name // "bound doSomething"
如果对象的方法是一个 Symbol
值,那么name
属性返回的是这个 Symbol
值的描述:
const key1 = Symbol('description');
const key2 = Symbol();
let obj = {
[key1]() {},
[key2]() {},
};
obj[key1].name // "[description]"
obj[key2].name // ""
9.4 属性的可枚举性和遍历
可枚举性
对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为。Object.getOwnPropertyDescriptor
方法可以获取该属性的描述对象:
let obj = { foo: 123 };
Object.getOwnPropertyDescriptor(obj, 'foo')
// {
// value: 123,
// writable: true,
// enumerable: true,
// configurable: true
// }
描述对象的enumerable
属性,称为“可枚举性”,如果该属性为false
,就表示某些操作会忽略当前属性。
目前,有四个操作会忽略enumerable
为false
的属性:
for...in
循环:只遍历对象自身的和继承的可枚举的属性。
Object.keys()
:返回对象自身的所有可枚举的属性的键名。
JSON.stringify()
:只串行化对象自身的可枚举的属性。
Object.assign()
: 忽略enumerable
为false
的属性,只拷贝对象自身的可枚举的属性。
这四个操作之中,只有for...in
会返回继承的属性,其他三个方法都会忽略继承的属性,只处理对象自身的属性。实际上,引入“可枚举”(enumerable
)这个概念的最初目的,就是让某些属性可以规避掉for...in
操作,不然所有内部属性和方法都会被遍历到。比如,对象原型的toString
方法,以及数组的length
属性,就通过“可枚举性”,从而避免被for...in
遍历到:
Object.getOwnPropertyDescriptor(Object.prototype, 'toString').enumerable
// false
Object.getOwnPropertyDescriptor([], 'length').enumerable
// false
另外,ES6 规定,所有 Class
的原型的方法都是不可枚举的:
Object.getOwnPropertyDescriptor(class {foo() {}}.prototype, 'foo').enumerable
// false
总的来说,操作中引入继承的属性会让问题复杂化。所以,尽量不要用for...in
循环,而用Object.keys()
代替。
属性的遍历
ES6 一共有 5 种方法可以遍历对象的属性:
for...in
:
for...in
循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)。
Object.keys(obj)
:
Object.keys
返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol
属性)的键名。
Object.getOwnPropertyNames(obj)
Object.getOwnPropertyNames
返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名。
Object.getOwnPropertySymbols(obj)
Object.getOwnPropertySymbols
返回一个数组,包含对象自身的所有 Symbol
属性的键名。
Reflect.ownKeys(obj)
Reflect.ownKeys
返回一个数组,包含对象自身的所有键名,不管键名是 Symbol
或字符串,也不管是否可枚举。
以上的 5 种方法遍历对象的键名,都遵守同样的属性遍历的次序规则:
- 首先遍历所有数值键,按照数值升序排列
- 其次遍历所有字符串键,按照加入时间升序排列
- 最后遍历所有 Symbol 键,按照加入时间升序排列
Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 })
// ['2', '10', 'b', 'a', Symbol()]
9.5 super 关键字
我们知道,this
关键字总是指向函数所在的当前对象,ES6 又新增了另一个类似的关键字super
,指向当前对象的原型对象:
const proto = {
foo: 'hello'
};
const obj = {
foo: 'world',
find() {
return super.foo;
}
};
Object.setPrototypeOf(obj, proto);
obj.find() // "hello"
注意,super
关键字表示原型对象时,只能用在对象的方法之中,用在其他地方都会报错:
// 报错
const obj = {
foo: super.foo
}
// 报错
const obj = {
foo: () => super.foo
}
// 报错
const obj = {
foo: function () {
return super.foo
}
}
上面三种super
的用法都会报错,因为对于 JavaScript 引擎来说,这里的super
都没有用在对象的方法之中。
9.6 对象的扩展运算符
扩展运算符为(...
),ES2018 将这个运算符引入了对象。
解构赋值
对象的解构赋值用于从一个对象取值,相当于将目标对象自身的所有可遍历的(enumerable)、但尚未被读取的属性,分配到指定的对象上面。所有的键和它们的值,都会拷贝到新对象上面。
let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
x // 1
y // 2
z // { a: 3, b: 4 }
上面代码中,变量z
是解构赋值所在的对象。它获取等号右边的所有尚未读取的键(a
和b
),将它们连同值一起拷贝过来。
由于解构赋值要求等号右边是一个对象,所以如果等号右边是undefined
或null
,就会报错,因为它们无法转为对象:
let { x, y, ...z } = null; // 运行时错误
let { x, y, ...z } = undefined; // 运行时错误
解构赋值必须是最后一个参数,否则会报错:
let { ...x, y, z } = obj; // 句法错误
let { x, ...y, ...z } = obj; // 句法错误
注意,解构赋值的拷贝是浅拷贝,即如果一个键的值是复合类型的值(数组、对象、函数)、那么解构赋值拷贝的是这个值的引用,而不是这个值的副本:
let obj = { a: { b: 1 } };
let { ...x } = obj;
obj.a.b = 2;
x.a.b // 2
扩展运算符的解构赋值,不能复制继承自原型对象的属性:
let o1 = { a: 1 };
let o2 = { b: 2 };
o2.__proto__ = o1;
let { ...o3 } = o2;
o3 // { b: 2 }
o3.a // undefined
解构赋值的一个用处,是扩展某个函数的参数,引入其他操作:
function baseFunction({ a, b }) {
// ...
}
function wrapperFunction({ x, y, ...restConfig }) {
// 使用 x 和 y 参数进行操作
// 其余参数传给原始函数
return baseFunction(restConfig);
}
扩展运算符
对象的扩展运算符(...
)用于取出参数对象的所有可遍历属性,拷贝到当前对象之中:
let z = { a: 3, b: 4 };
let n = { ...z };
n // { a: 3, b: 4 }
由于数组是特殊的对象,所以对象的扩展运算符也可以用于数组:
let foo = { ...['a', 'b', 'c'] };
foo
// {0: "a", 1: "b", 2: "c"}
对象的扩展运算符等同于使用Object.assign()
方法:
let aClone = { ...a };
// 等同于
let aClone = Object.assign({}, a);
扩展运算符可以用于合并两个对象:
let ab = { ...a, ...b };
// 等同于
let ab = Object.assign({}, a, b);
如果用户自定义的属性,放在扩展运算符后面,则扩展运算符内部的同名属性会被覆盖掉:
let aWithOverrides = { ...a, x: 1, y: 2 };
// 等同于
let aWithOverrides = { ...a, ...{ x: 1, y: 2 } };
// 等同于
let x = 1, y = 2, aWithOverrides = { ...a, x, y };
// 等同于
let aWithOverrides = Object.assign({}, a, { x: 1, y: 2 });
如果把自定义属性放在扩展运算符前面,就变成了设置新对象的默认属性值:
let aWithDefaults = { x: 1, y: 2, ...a };
// 等同于
let aWithDefaults = Object.assign({}, { x: 1, y: 2 }, a);
// 等同于
let aWithDefaults = Object.assign({ x: 1, y: 2 }, a);
与数组的扩展运算符一样,对象的扩展运算符后面可以跟表达式:
const obj = {
...(x > 1 ? {a: 1} : {}),
b: 2,
};
如果扩展运算符后面是一个空对象,则没有任何效果:
{...{}, a: 1}
// { a: 1 }
如果扩展运算符的参数是null或undefined,这两个值会被忽略,不会报错:
let emptyObject = { ...null, ...undefined }; // 不报错
扩展运算符的参数对象之中,如果有取值函数get
,这个函数是会执行的:
// 并不会抛出错误,因为 x 属性只是被定义,但没执行
let aWithXGetter = {
...a,
get x() {
throw new Error('not throw yet');
}
};
// 会抛出错误,因为 x 属性被执行了
let runtimeError = {
...a,
...{
get x() {
throw new Error('throw now');
}
}
};
10. 对象的新增方法
10.1 Object.is()
ES5 比较两个值是否相等,只有两个运算符:相等运算符(==
)和严格相等运算符(===
)。它们都有缺点,前者会自动转换数据类型,后者的NaN
不等于自身,以及+0
等于-0
。
JavaScript 缺乏一种运算,在所有环境中,只要两个值是一样的,它们就应该相等。
ES6 提出“Same-value equality”(同值相等)算法,用来解决这个问题。Object.is
就是部署这个算法的新方法。它用来比较两个值是否严格相等,与严格比较运算符(===
)的行为基本一致:
Object.is('foo', 'foo')
// true
Object.is({}, {})
// false
不同之处只有两个:一是+0不等于-0,二是NaN等于自身:
+0 === -0 //true
NaN === NaN // false
Object.is(+0, -0) // false
Object.is(NaN, NaN) // true
10.2 Object.assign()
基本用法
Object.assign
方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。
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方法的第一个参数是目标对象
// 后面的参数都是源对象。
注意,如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性:
const target = { a: 1, b: 1 };
const source1 = { b: 2, c: 2 };
const source2 = { c: 3 };
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}
如果只有一个参数,Object.assign
会直接返回该参数:
const obj = {a: 1};
Object.assign(obj) === obj // true
如果该参数不是对象,则会先转成对象,然后返回:
typeof Object.assign(2) // "object"
// 由于undefined和null无法转成对象
// 所以如果它们作为参数,就会报错。
Object.assign(undefined) // 报错
Object.assign(null) // 报错
// 非对象参数出现在源对象的位置(即非首参数)
// 那么处理规则有所不同
// 首先,这些参数都会转成对象
// 如果无法转成对象,就会跳过
// 这意味着,如果undefined和null不在首参数
// 就不会报错。
let obj = {a: 1};
Object.assign(obj, undefined) === obj // true
Object.assign(obj, null) === obj // true
// 其他类型的值(即数值、字符串和布尔值)不在首参数也不会报错
// 但是,除了字符串会以数组形式,拷贝入目标对象,其他值都不会产生效果
const v1 = 'abc';
const v2 = true;
const v3 = 10;
const obj = Object.assign({}, v1, v2, v3);
console.log(obj); // { "0": "a", "1": "b", "2": "c" }
Object.assign
拷贝的属性是有限制的,只拷贝源对象的自身属性(不拷贝继承属性),也不拷贝不可枚举的属性(enumerable: false
):
Object.assign({b: 'c'},
Object.defineProperty({}, 'invisible', {
enumerable: false,
value: 'hello'
})
)
// { b: 'c' }
// 属性名为 Symbol 值的属性,也会被Object.assign拷贝。
Object.assign({ a: 'b' }, { [Symbol('c')]: 'd' })
// { a: 'b', Symbol(c): 'd' }
注意点
浅拷贝:
Object.assign
方法实行的是浅拷贝,而不是深拷贝。
同名属性的替换:
对于这种嵌套的对象,一旦遇到同名属性,Object.assign
的处理方法是替换,而不是添加。
数组的处理:
Object.assign
可以用来处理数组,但是会把数组视为对象。
取值函数的处理:
Object.assign
只能进行值的复制,如果要复制的值是一个取值函数,那么将求值后再复制。
常见用途
为对象添加属性:
class Point {
constructor(x, y) {
Object.assign(this, {x, y});
}
}
为对象添加方法:
Object.assign(SomeClass.prototype, {
someMethod(arg1, arg2) {
···
},
anotherMethod() {
···
}
});
// 等同于下面的写法
SomeClass.prototype.someMethod = function (arg1, arg2) {
···
};
SomeClass.prototype.anotherMethod = function () {
···
};
克隆对象:
function clone(origin) {
return Object.assign({}, origin);
}
上面代码将原始对象拷贝到一个空对象,就得到了原始对象的克隆。不过,采用这种方法克隆,只能克隆原始对象自身的值,不能克隆它继承的值。如果想要保持继承链,可以采用下面的代码。
function clone(origin) {
let originProto = Object.getPrototypeOf(origin);
return Object.assign(Object.create(originProto), origin);
}
合并多个对象:
将多个对象合并到某个对象:
const merge =
(target, ...sources) => Object.assign(target, ...sources);
为属性指定默认值:
const DEFAULTS = {
logLevel: 0,
outputFormat: 'html'
};
function processContent(options) {
options = Object.assign({}, DEFAULTS, options);
console.log(options);
// ...
}
注意,由于存在浅拷贝的问题,DEFAULTS对象和options对象的所有属性的值,最好都是简单类型,不要指向另一个对象。否则,DEFAULTS对象的该属性很可能不起作用。
10.3 Object.getOwnPropertyDescriptors()
ES2017 引入了Object.getOwnPropertyDescriptors()
方法,返回指定对象所有自身属性(非继承属性)的描述对象:
const obj = {
foo: 123,
get bar() { return 'abc' }
};
Object.getOwnPropertyDescriptors(obj)
// { foo:
// { value: 123,
// writable: true,
// enumerable: true,
// configurable: true },
// bar:
// { get: [Function: get bar],
// set: undefined,
// enumerable: true,
// configurable: true } }
该方法的引入目的,主要是为了解决Object.assign()
无法正确拷贝get
属性和set
属性的问题。
10.4 __proto__相关
__proto__属性
__proto__
属性(前后各两个下划线),用来读取或设置当前对象的prototype
对象。目前,所有浏览器(包括 IE11)都部署了这个属性。
// es5 的写法
const obj = {
method: function() { ... }
};
obj.__proto__ = someOtherObj;
// es6 的写法
var obj = Object.create(someOtherObj);
obj.method = function() { ... };
Object.setPrototypeOf()
Object.setPrototypeOf
方法的作用与__proto__
相同,用来设置一个对象的prototype
对象,返回参数对象本身。它是 ES6 正式推荐的设置原型对象的方法。
// 格式
Object.setPrototypeOf(object, prototype)
// 用法
const o = Object.setPrototypeOf({}, null);
Object.getPrototypeOf()
该方法与Object.setPrototypeOf方法配套,用于读取一个对象的原型对象。
Object.getPrototypeOf(obj);
10.5 Object.keys(),Object.values(),Object.entries()
Object.keys()
ES5 引入了Object.keys
方法,返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable
)属性的键名。
var obj = { foo: 'bar', baz: 42 };
Object.keys(obj)
// ["foo", "baz"]
Object.values()
Object.values
方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable
)属性的键值。
const obj = { foo: 'bar', baz: 42 };
Object.values(obj)
// ["bar", 42]
Object.entries()
Object.entries()
方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable
)属性的键值对数组。
const obj = { foo: 'bar', baz: 42 };
Object.entries(obj)
// [ ["foo", "bar"], ["baz", 42] ]
除了返回值不一样,该方法的行为与Object.values
基本一致。
10.6 Object.fromEntries()
Object.fromEntries()
方法是Object.entries()
的逆操作,用于将一个键值对数组转为对象。
Object.fromEntries([
['foo', 'bar'],
['baz', 42]
])
// { foo: "bar", baz: 42 }
你可能感兴趣的:(Web,JavaScript)