ES6 学习笔记 Part1-3

ES6 学习笔记 Part1-3

这是 ES6 学习笔记记录 Part1-3

学习并节选自 阮一峰 - ECMAScript 6 入门

仅供个人学习记录,原著请访问链接


文章目录


1. ECMAScript 6 简介

ECMAScript 6.0(以下简称 ES6)是 JavaScript 语言的下一代标准,已经在 2015 年 6 月正式发布了。它的目标,是使得 JavaScript 语言可以用来编写复杂的大型应用程序,成为企业级开发语言。

1.1 ES 与 JS 之间的关系

ECMAScript 和 JavaScript 的关系是,前者是后者的规格,后者是前者的一种实现。日常场合,这两个词是可以互换的。

1.2 ES6 与 ECMAScript 2015 的关系

ES6 既是一个历史名词,也是一个泛指,含义是 5.1 版以后的 JavaScript 的下一代标准,涵盖了 ES2015、ES2016、ES2017 等等,而 ES2015 则是正式名称,特指该年发布的正式版本的语言标准。

ES6 从开始制定到最后发布,整整用了 15 年。

1.3 Babel 转码器

Babel 是一个广泛使用的 ES6 转码器,可以将 ES6 代码转为 ES5 代码,从而在现有环境执行。例如:

// 转码前
input.map(item => item + 1);

// 转码后
input.map(function (item) {
  return item + 1;
});

Babel 的配置文件是.babelrc,存放在项目的根目录下。使用 Babel 的第一步,就是配置这个文件。

该文件用来设置转码规则和插件,基本格式为:

{
  "presets": [],
  "plugins": []
}

2. let 和 const 命令

2.1 let 命令

基本用法

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

变量ilet声明的,当前的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;
暂时性死区(TDZ, temporal dead zone)

只要块级作用域内存在let命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。

var tmp = 123;

if (true) {
  tmp = 'abc'; // ReferenceError
  let tmp;
}
// 存在全局变量tmp,但是块级作用域内
// let又声明了一个局部变量tmp
// 导致后者绑定这个块级作用域
// 所以在let声明变量前,对tmp赋值会报错。

ES6 明确规定,如果区块中存在letconst命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。

“暂时性死区”也意味着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; // 不报错
  }
}

2.2 块级作用域

为何需要块级作用域

ES5 只有全局作用域和函数作用域,没有块级作用域,这带来很多不合理的场景:

  1. 内层变量可能会覆盖外层变量
  2. 用来计数的循环变量泄露为全局变量
// 变量提升导致了覆盖
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
ES6 的块级作用域

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 的块级作用域允许声明函数的规则
// 只在使用大括号的情况下成立
// 如果没有使用大括号,就会报错

2.3 const 命令

基本用法
  • const声明一个只读的常量。一旦声明,常量的值就不能改变。
  • const一旦声明变量,就必须立即初始化,不能留到以后赋值,否则会报错。
  • const的作用域与let命令相同:只在声明所在的块级作用域内有效。
  • const命令声明的常量也是不提升,同样存在暂时性死区,只能在声明的位置后面使用。
  • const声明的常量,也与let一样不可重复声明。
本质

const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。

对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。

对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。

ES6 声明变量的六种方式
  • var (ES5)
  • function (ES5)
  • let (ES6)
  • const (ES6)
  • import (ES6)
  • class (ES6)

2.4 顶层对象的属性

顶层对象,在浏览器环境指的是window对象,在 Node 指的是global对象。ES5 之中,顶层对象的属性与全局变量是等价的。

ES6 为了改变这一点,一方面规定,为了保持兼容性,var命令和function命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。

2.5 global 对象

ES5 的顶层对象,本身也是一个问题,因为它在各种实现里面是不统一的:

  • 浏览器里面,顶层对象是window,但 Node 和 Web Worker 没有window
  • 浏览器和 Web Worker 里面,self也指向顶层对象,但是 Node 没有self
  • Node 里面,顶层对象是global,但其他环境都不支持。

现在有一个提案,在语言标准的层面,引入global作为顶层对象。也就是说,在所有环境下,global都是存在的,都可以从它拿到顶层对象。


3. 变量的解构赋值

3.1 数组的解构赋值

基本用法

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

3.2 对象的解构赋值

解构不仅可以用于数组,还可以用于对象:

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对象的对数、正弦、余弦三个方法,赋值到对应的变量上

3.3 字符串的解构赋值

字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象:

const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"

类似数组的对象都有一个length属性,因此还可以对这个属性解构赋值:

let {length : len} = 'hello';
len // 5

3.4 数值和布尔值的解构赋值

解构赋值时,如果等号右边是数值和布尔值,则会先转为对象:

let {toString: s} = 123;
s === Number.prototype.toString // true

let {toString: s} = true;
s === Boolean.prototype.toString // true

解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于undefinednull无法转为对象,所以对它们进行解构赋值,都会报错。

3.5 函数参数的解构赋值

function add([x, y]){
  return x + y;
}

add([1, 2]); // 3
// 函数add的参数表面上是一个数组
// 但在传入参数的那一刻,数组参数就被解构成变量x和y
// 对于函数内部的代码来说,它们能感受到的参数就是x和y

3.6 圆括号问题

解构赋值虽然很方便,但是解析起来并不容易。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]; // 正确

3.7 解构赋值的用途

// 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");

4. 字符串的扩展

ES6 加强了对 Unicode 的支持,并且扩展了字符串对象。

4.1 字符的 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

4.2 codePointAt()

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

4.3 String.fromCodePoint()

ES5 提供String.fromCharCode方法,用于从码点返回对应字符,但是这个方法不能识别 32 位的 UTF-16 字符(Unicode 编号大于0xFFFF)。

ES6 提供了String.fromCodePoint方法,可以识别大于0xFFFF的字符,弥补了String.fromCharCode方法的不足。在作用上,正好与codePointAt方法相反。

注意,fromCodePoint方法定义在String对象上,而codePointAt方法定义在字符串的实例对象上。

4.4 字符串的遍历器接口

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);
}
// "?"

4.5 normalize()

许多欧洲语言有语调符号和重音符号。为了表示它们,Unicode 提供了两种方法:

  • 直接提供带重音符号的字符
  • 提供合成符号(combining character),即原字符与重音符号的合成,两个字符合成一个字符

遗憾的是,这两种表示方法, JavaScript 不能识别:

'\u01D1'==='\u004F\u030C' //false

'\u01D1'.length // 1
'\u004F\u030C'.length // 2

ES6 提供字符串实例的normalize()方法,用来将字符的不同表示方法统一为同样的形式,这称为 Unicode 正规化:

'\u01D1'.normalize() === '\u004F\u030C'.normalize()
// true

不过,normalize方法目前不能识别三个或三个以上字符的合成。这种情况下,还是只能使用正则表达式,通过 Unicode 编号区间判断。

4.6 includes(), startsWith(), endsWith()

传统上,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个位置直到字符串结束。

4.7 repeat()

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"

4.8 padStart(), padEnd()

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   '

4.9 matchAll()

matchAll方法返回一个正则表达式在当前字符串的所有匹配。

4.10 模板字符串

传统的 JavaScript 语言,输出模板通常是这样写的(下面使用了 jQuery 的方法)。

$('#result').append(
  'There are <b>' + basket.count + '</b> ' +
  'items in your basket, ' +
  '<em>' + basket.onSale +
  '</em> are on sale!'
);

上面这种写法相当繁琐不方便,ES6 引入了模板字符串解决这个问题:

// 使用模板字符串表示多行字符串
// 所有的空格和缩进都会被保留在输出之中
// 
// 模板字符串中嵌入变量,需要将变量名写在${}之中
// * 大括号内部可以放入任意的 JavaScript 表达式
// * 可以进行运算,以及引用对象属性,还能调用函数
// * 如果大括号中的值不是字符串,将按照一般的规则转为字符串
//   默认会调用对象的toString方法
$('#result').append(`
  There are <b>${basket.count}</b> items
   in your basket, <em>${basket.onSale}</em>
  are on sale!
`);

模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。如果在模板字符串中需要使用反引号,则前面要用反斜杠转义。

// `...`字符串中,所有模板字符串的空格和换行,都是被保留的
// 比如<ul>标签前面会有一个换行
// 如果你不想要这个换行,可以使用trim方法消除它。
$('#list').html(`
<ul>
  <li>first</li>
  <li>second</li>
</ul>
`.trim());

模板字符串甚至还能嵌套:

const tmpl = addrs => `
  <table>
  ${addrs.map(addr => `
    <tr><td>${addr.first}</td></tr>
    <tr><td>${addr.last}</td></tr>
  `).join('')}
  </table>
`;
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>

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 = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\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表示。

如果要将0b0o前缀的字符串数值转为十进制,要使用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

如果参数类型不是NaNNumber.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^532^53之间(不含两个端点),超过这个范围,无法精确表示这个值。

ES6 引入了Number.MAX_SAFE_INTEGERNumber.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 }

参数变量是默认声明的,所以不能用letconst再次声明。

使用参数默认值时,函数不能有同名参数。

参数默认值不是传值的,而是每次都重新计算默认值表达式的值。也就是说,参数默认值是惰性求值的:

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对象的写法(callapplybind)。但是,箭头函数并不适用于所有场合,所以现在有一个提案,提出了“函数绑定”(function bind)运算符,用来取代callapplybind调用。

函数绑定运算符是并排的两个冒号(::),双冒号左边是一个对象,右边是一个函数。该运算符会自动将左边的对象,作为上下文环境(即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')]
// [<div>, <div>, <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 新增的数据结构 SetMap)。

下面是一个类似数组的对象,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

MapSet 数据结构有一个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属性不是在该方法上面,而是该方法的属性的描述对象的getset属性上面,返回值是方法名前加上getset:

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"

有两种特殊情况:

  1. bind方法创造的函数,name属性返回bound加上原函数的名字
  2. 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,就表示某些操作会忽略当前属性。

目前,有四个操作会忽略enumerablefalse的属性:

  • for...in循环:只遍历对象自身的和继承的可枚举的属性。
  • Object.keys():返回对象自身的所有可枚举的属性的键名。
  • JSON.stringify():只串行化对象自身的可枚举的属性。
  • Object.assign(): 忽略enumerablefalse的属性,只拷贝对象自身的可枚举的属性。

这四个操作之中,只有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是解构赋值所在的对象。它获取等号右边的所有尚未读取的键(ab),将它们连同值一起拷贝过来。

由于解构赋值要求等号右边是一个对象,所以如果等号右边是undefinednull,就会报错,因为它们无法转为对象:

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 }
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值