前言

本读书笔记节是针对阮一峰编写的 《ECMAScript 6 入门教程》的个人解读。适合已经掌握 ES5 的读者,用来了解这门语言的最新发展。笔记内容仅包括日常开发中可能会用到的概念,剩余部分请自行阅读。


1. let 和 const 命令

1.1 let 命令

let的用法类似于var,但所声明的变量只在let命令所在的代码块(即 {} 包裹起来的内容)内有效。而var所声明的变量在全局范围内都有效。

{
let a = 10;
var b = 1;
}
a; // ReferenceError: a is not defined.
b; // 1

(1)不存在变量提升

var命令会发生“变量提升”现象,即变量可以在声明之前使用,值为undefined。为了纠正这种现象,let命令改变了语法行为,它所声明的变量一定要在声明后使用。

// var 的情况
console.log(foo); // 输出 undefined
var foo = 2;

// let 的情况
console.log(bar); // 报错 ReferenceError
let bar = 2;

(2)暂时性死区

只要块级作用域内存在letconst命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。凡是在声明之前就使用这些变量,就会报错。

var tmp = 123;

if (true) {
// TDZ 开始
tmp = "abc"; // ReferenceError
console.log(tmp); // ReferenceError

let tmp; // TDZ 结束
console.log(tmp); // undefined

tmp = 123;
console.log(tmp); // 123
}

1.2 块级作用域

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

(1)内层变量覆盖外层变量

var tmp = new Date();

function f() {
console.log(tmp);
if (false) {
var tmp = "hello world";
}
}

f(); // undefined

上面代码的原意是,if代码块的外部使用外层的tmp变量,内部使用内层的tmp变量。但是函数f执行后,输出结果却为undefined

原因其实就是变量提升,导致内层的tmp变量覆盖了外层的tmp变量。实际中的代码如下:

var tmp = new Date();

function f() {
var tmp; // 声明但未赋值 undefined
console.log(tmp); // 输出 undefined
if (false) {
tmp = "hello world";
}
}

f(); // undefined

(2)用来计数的循环变量泄露为全局变量

下面代码中,变量i只用来控制循环,但是循环结束后,它并没有消失,泄露成了全局变量。

var s = "hello";

for (var i = 0; i < s.length; i++) {
console.log(s[i]);
}

console.log(i); // 5

为了应对上面这些情况,ES6 新增了块级作用域,外层作用域无法获取到内层作用域即使外层和内层都使用相同变量名,也都互不干扰

function f1() {
let n = 5;
if (true) {
let n = 10;
}
console.log(n); // 5
}

上面的函数有两个代码块,都声明了变量n,运行后输出 5。这表示外层代码块不受内层代码块的影响。如果两次都使用var定义变量n,最后输出的值就是 10


1.3 const 命令

const声明一个只读常量。一旦声明,常量的值就不能改变,且必须立即初始化。(与 let 一样,const 不存在变量提升,存在暂时性死区)

const PI = 3.1415;
PI; // 3.1415
PI = 3;
// TypeError: Assignment to constant variable.

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

  • 对于简单类型的数据(比如数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。
  • 但对于复合类型的数据(比如对象、数组),变量指向的内存地址,保存的只是一个指向实际数据的指针。const只能保证这个指针是固定的,至于它指向的数据结构是不是可变的,就完全不能控制了。
const foo = {};

// 为 foo 添加一个属性
foo.prop = 123;
foo.prop; // 123

// 将 foo 指向另一个对象,报错
foo = {}; // TypeError: "foo" is read-only

2. 变量的解构赋值

2.1 数组的解构赋值

从数组或对象中提取值,按照对应位置, 对变量进行赋值,这被称为解构。

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 [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] = []; // foo == undefined
let [bar, foo] = [1]; // foo == undefined

let [a, [b], d] = [1, [2, 3], 4];
a; // 1
b; // 2
d; // 4

事实上,只要某种数据结构具有 Iterator 接口,都可以采用数组形式的解构赋值。

let [x, y, z] = new Set(["a", "b", "c"]); // Set 原生带有 Iterator 接口
x; // "a"

数组的解构赋值可以指定默认值,并且必须严格等于undefined才会生效。

let [foo = true] = [];
foo; // true

let [x, y = "b"] = ["a"]; // x='a', y='b'
let [x, y = "b"] = ["a", undefined]; // x='a', y='b'

let [x = 1] = [undefined];
x; // 1

let [x = 1] = [null];
x; // null

2.2 对象的解构赋值

对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定而对象的属性没有次序,变量必须与属性同名,才能取到正确的值

let { bar, foo } = { foo: "aaa", bar: "bbb" };
foo; // "aaa"
bar; // "bbb"

let { baz } = { foo: "aaa", bar: "bbb" };
baz; // undefined

如果变量名与属性名不一致,必须写成下面这样。

let obj = { first: "hello", last: "world" };
let { first: f, last: l } = obj;
f; // 'hello'
l; // 'world'

上面的代码可以看出,对象的解构赋值的机制,是先找到同名属性,然后再赋给对应的变量真正被赋值的是后者,而不是前者

let { foo: foo, bar: bar } = { foo: "aaa", bar: "bbb" };
// let {foo, bar} = { foo: 'aaa', bar: 'bbb' };

let { foo: baz } = { foo: "aaa", bar: "bbb" };
baz; // "aaa"
foo; // error: foo is not defined

解构也可以用于嵌套结构的对象。下面代码分别对pxy三个属性进行解构赋值。注意,在对xy属性的解构赋值之中,xy是变量,p是模式。

let obj = {
p: ["Hello", { y: "World" }],
};

let {
p,
p: [x, { y }],
} = obj;
p; // ["Hello", {y: "World"}]
x; // "Hello"
y; // "World"

对象的解构也可以指定默认值,并且必须严格等于undefined才会生效。

let { x = 3 } = {};
x; // 3

let { x, y = 5 } = { x: 1, y: undefined };
x; // 1
y; // 5

let { x: y = 3 } = {};
y; // 3

let { x: y = 3 } = { x: 5 };
y; // 5

2.3 函数参数的解构赋值

函数的参数也可以使用解构赋值。

function move({ x = 0, y = 0 } = {}) {
return [x, y];
}

move({ x: 3, y: 8 }); // [3, 8]
move({ x: 3 }); // [3, 0]
move({}); // [0, 0]
move(); // [0, 0]

上面代码中,函数move的参数是一个对象。通过对这个对象进行解构,得到变量xy的值。如果解构失败,xy等于默认值。

注意,下面的写法会得到与上面不一样的结果。

function move({ x, y } = { x: 0, y: 0 }) {
return [x, y];
}

move({ x: 3, y: 8 }); // [3, 8]
move({ x: 3 }); // [3, undefined]
move({}); // [undefined, undefined]
move(); // [0, 0]

上面代码为函数move的参数指定默认值,而不是为变量xy指定默认值。当参数为undefined时,参数默认值启用。


2.4 解构赋值的用途

(1)交换变量的值

下面代码交换变量xy的值,这样的写法简洁易读,语义清晰。

let x = 1;
let y = 2;

[x, y] = [y, x]; // x = 2, y = 1

(2)从函数返回多个值

函数只能返回一个值,如果要返回多个值,只能将它们放在数组或对象里返回。有了解构赋值,取出这些值就非常方便。

// 返回一个数组
function example() {
return [1, 2, 3];
}
let [a, b, c] = example();

// 返回一个对象
function example() {
return {
foo: 1,
bar: 2,
};
}
let { foo, bar } = example();

(3)提取 JSON 数据

解构赋值对提取 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]

(4)函数参数的定义

解构赋值可以方便地将一组参数与变量名对应起来。

// 参数是一组有次序的值
function f([x, y, z]) { ... }
f([1, 2, 3]);

// 参数是一组无次序的值
function f({x, y, z}) { ... }
f({z: 3, y: 2, x: 1});

(5)遍历 Map 结构

前面提到,任何部署了 Iterator 接口的对象,都可以用for...of循环遍历。Map 结构原生支持 Iterator 接口,配合变量的解构赋值,获取键名和键值就非常方便。

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

3. 字符串的扩展

本章内容复杂,故先跳过一部分。

3.1 模板字符串

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

模板字符串(template string)是增强版的字符串。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。

// 字符串中嵌入变量
let name = "Bob",
time = "today";
`Hello ${name}, how are you ${time}?`;

注意,如果使用模板字符串表示多行字符串,所有的空格,换行,缩进都会被保留在输出之中。

let str1 = "Hello" + "World";

let str2 = `Hello
World`;

console.log(str1); // HelloWorld
console.log(str2); // Hello
// World 换行被保留

3.2 标签模板

模板字符串可以紧跟在一个函数名后面,该函数将被调用来处理这个模板字符串。这被称为“标签模板”功能(tagged template)。

  • 标签模板不是模板,而是函数调用的一种特殊形式。“标签”指的是函数,它的参数是紧跟在后面的模板字符串。
alert`hello`;
// 等同于
alert(["hello"]);

如果模板字符里面有变量,就不是直接调用了。会先将模板字符串先处理成多个参数,再调用函数。

let a = 5;
let b = 10;

function tag(stringArr, ...values){ ... }
// 等同于
function tag(stringArr, value1, value2){ ... }

tag`Hello ${ a + b } world ${ a * b }`;
// 等同于
tag(['Hello ', ' world ', ''], 15, 50);

4. 字符串的新增方法

4.1 新增方法

实例方法 1:ES5 中只有indexOf()方法可以用来确定一个字符串是否包含在另一个字符串中。ES6 又提供了三种新方法:

  • includes():返回布尔值,表示是否找到了参数字符串。
  • startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。
  • endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部。
let s = "Hello world!";

s.startsWith("Hello"); // true
s.endsWith("!"); // true
s.includes("o"); // true

这三个方法都支持第二个参数,表示开始搜索的位置。

let s = "Hello world!";

s.startsWith("world", 6); // true
s.endsWith("Hello", 5); // true
s.includes("Hello", 6); // false

上面代码中,使用第二个参数n时,endsWith()的行为与其他两个方法不同。它针对前n个字符,而其他两个方法针对从第n个位置直到字符串结束。

实例方法 2:repeat()方法返回一个新字符串,将原字符串重复n次。

"na".repeat(3); // "nana"
"na".repeat("2"); // "nana" 转换成数字
"na".repeat(0); // ""
"na".repeat(2.9); // "nana" 取整
"na".repeat(-1); // RangeError

实例方法 3:ES6 引入字符串补全长度功能,能在某个字符串不够指定长度时,往头部或尾部补全。padStart()用于头部补全,padEnd()用于尾部补全。

"x".padStart(5, "ab"); // 'ababx' 补到length == 5
"abc".padStart(10, "0123456789");
// '0123456abc' 如果两者的长度之和超过了最大长度,则会截去超出的位数

"x".padEnd(5, "ab"); // 'xabab'
"x".padEnd(4, "ab"); // 'xaba'

实例方法 4:ES6 新增了trimStart()trimEnd()两个方法。它们的行为与trim()一致,trimStart()消除字符串头部的空格,trimEnd()消除尾部的空格。它们返回的都是新字符串,不会修改原始字符串。

const s = "  abc  ";

s.trim(); // "abc"
s.trimStart(); // "abc "
s.trimEnd(); // " abc"

实例方法 5:ES5中,replace()只能替换第一个匹配。如果要替换所有的匹配,需要使用正则表达式的g修饰符。ES6引入了replaceAll()方法,可以一次性替换所有匹配。它返回的是新字符串,不会修改原始字符串。

"aabbcc".replace("b", "_");
// 'aa_bcc'

"aabbcc".replace(/b/g, "_");
// 'aa__cc'

"aabbcc".replaceAll("b", "_");
// 'aa__cc'

实例方法 6:ES6 增加了matchAll()方法,可以一次性取出字符串里面所有匹配。但是,它返回的是一个遍历器,而不是数组。

const string = "test1test2test3";
const regex = /t(e)(st(\d?))/g;

for (const match of string.matchAll(regex)) {
console.log(match);
}
// ["test1", "e", "st1", "1", index: 0, input: "test1test2test3"]
// ["test2", "e", "st2", "2", index: 5, input: "test1test2test3"]
// ["test3", "e", "st3", "3", index: 10, input: "test1test2test3"]

遍历器转为数组是非常简单的,使用...运算符和Array.from()方法就可以。

// 转为数组的方法一
[...string.matchAll(regex)];

// 转为数组的方法二
Array.from(string.matchAll(regex));

5. 正则的扩展

本章内容复杂,故先跳过一部分。

5.1 具名组匹配

正则表达式使用圆括号进行组匹配。使用exec方法,就可以将这匹配结果提取出来。

const RE_DATE = /(\d{4})-(\d{2})-(\d{2})/;

const matchObj = RE_DATE.exec("1999-12-31");
const year = matchObj[1]; // 1999
const month = matchObj[2]; // 12
const day = matchObj[3]; // 31

组匹配的一个问题是,每一组的匹配含义不容易看出来,而且只能用数字序号XXX[1]引用,要是组的顺序变了,引用的时候就必须修改序号。

ES6 引入了具名组匹配(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"

6. 数值的扩展

本章内容复杂,故先跳过一部分。

6.1 BigInt 数据类型

ES5 中所有数字都保存成 64 位浮点数,这给数值的表示带来了两大限制:

  • 数值的精度只能到 53 个二进制位,相当于 16 个十进制位,大于这个范围的整数,无法精确表示。 (不适合进行科学和金融方面的精确计算)
  • 大于或等于21024次方的数值无法表示,会返回Infinity
// 超过 53 个二进制位的数值,无法保持精度
Math.pow(2, 53) === Math.pow(2, 53) + 1; // true

// 超过 2 的 1024 次方的数值,无法表示
Math.pow(2, 1024); // Infinity

ES6 引入了一种新的数据类型 BigInt,来解决这个问题,这是 ECMAScript第八种数据类型BigInt 只用来表示整数,没有位数的限制,任何位数的整数都可以精确表示。

const a = 2172141653n;
const b = 15346349309n;

// BigInt 可以保持精度
a * b; // 33334444555566667777n

// 普通整数无法保持精度
Number(a) * Number(b); // 33334444555566670000

BigInt 与普通整数是两种值,它们之间并不相等,且不能进行混合运算。为了与 Number 类型区别,BigInt 类型的数据必须添加后缀n

42n === 42; // false

1n + 1.3; // 报错

7. 函数的扩展

7.1 函数参数的默认值

ES6 允许为函数的参数设置默认值,可以直接写在参数定义的后面(通常是函数的尾参数)。

function Point(x = 0, y = 0) {
this.x = x; // this.x = x || 0 (ES5 写法)
this.y = y; // this.y = y || 0
}

const p = new Point();
p; // { x: 0, y: 0 }

函数参数的默认值有两个好处:

  • 阅读代码的人,可以立刻意识到哪些参数是可以省略的,不用查看函数体或文档。
  • 有利于将来的代码优化,即使未来的版本在对外接口中,彻底拿掉这个参数,也不会导致以前的代码无法运行。

参数默认值可以与解构赋值的默认值结合使用。

function fetch(url, { body = "", method = "GET", headers = {} }) {
console.log(method);
}

fetch("http://example.com", {});
// "GET"

fetch("http://example.com");
// 报错

上面代码中,函数fetch的第二个参数是一个对象,它的三个属性都有默认值。这种写法不能省略第二个参数,如果结合函数参数的默认值,就可以省略第二个参数。

function fetch(url, { body = "", method = "GET", headers = {} } = {}) {
console.log(method);
}

fetch("http://example.com");
// "GET"

上面代码中,当函数fetch没有第二个参数时,函数参数的默认值就会生效,然后才是解构赋值的默认值生效,变量method才会取到默认值GET

注意,函数参数的默认值生效以后,参数解构赋值依然会进行。

function f({ a, b = "world" } = { a: "hello" }) {
console.log(b);
}

f(); // world

7.2 rest 参数

rest 参数可以代替arguments对象,去获取函数的多余参数。rest 参数之后不能再有其他参数,它只能是最后一个参数。

// arguments变量的写法
function sortNumbers() {
return Array.from(arguments).sort();
}
console.log(sortNumbers(1, 2, 5, 3));
// [1,2,3,5]

// rest参数的写法
const sortNumbers = (...numbers) => numbers.sort();
console.log(sortNumbers(1, 2, 5, 3));
// [1,2,3,5]

上面代码的两种写法,比较后可以发现,rest 参数的写法更自然也更简洁。


7.3 箭头函数

ES6 允许使用“箭头” => 定义函数。

  • 如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。
  • 如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回。
let f = () => 5;
// 等同于
let f = function () {
return 5;
};

let sum = (num1, num2) => num1 + num2;
// 等同于
let sum = (num1, num2) => {
return num1 + num2;
};
// 等同于
let sum = function (num1, num2) {
return num1 + num2;
};

箭头函数可以与变量解构结合使用。

const full = ({ first, last }) => first + " " + last;

// 等同于
function full(person) {
return person.first + " " + person.last;
}

箭头函数还可以和上面的 rest 参数结合使用。

const numbers = (...nums) => nums;

numbers(1, 2, 3, 4, 5);
// [1,2,3,4,5]

const headAndTail = (head, ...tail) => [head, tail];

headAndTail(1, 2, 3, 4, 5);
// [1,[2,3,4,5]]

箭头函数的重要用处是简化回调函数

// 普通函数写法
[1, 2, 3].map(function (x) {
return x * x;
});

// 箭头函数写法
[1, 2, 3].map((x) => x * x);

// 普通函数写法
let result = values.sort(function (a, b) {
return a - b;
});

// 箭头函数写法
let result = values.sort((a, b) => a - b);

箭头函数有几个使用注意点,其中最重要的是,箭头函数没有自己的this对象。

  • 对于普通函数来说,内部的this指向函数运行时所在的对象,但是这一点对箭头函数不成立。它没有自己的this对象,内部的this就是定义时上层作用域中的this。也就是说,箭头函数内部的this指向是固定的,相比之下,普通函数的this指向是可变的
function Timer() {
this.s1 = 0;
this.s2 = 0;
// 箭头函数
setInterval(() => this.s1++, 1000);
// 普通函数
setInterval(function () {
this.s2++;
}, 1000);
}

let timer = new Timer(); // 注意

setTimeout(() => console.log("s1: ", timer.s1), 3100);
setTimeout(() => console.log("s2: ", timer.s2), 3100);
// s1: 3
// s2: 0

上面代码中,Timer函数内部设置了两个定时器,分别使用了箭头函数和普通函数。前者的this绑定定义时所在的作用域,即Timer函数,后者的this指向运行时所在的作用域,即全局对象。所以,3100 毫秒之后,timer.s1被更新了 3 次,而timer.s2一次都没更新。


8. 数组的扩展

8.1 扩展运算符

扩展运算符 ...,将一个数组转为用逗号分隔的参数序列。类似 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>]

扩展运算符主要用于函数调用,可以代替apply()方法将数组转为函数的参数。

// ES5 的写法
function f(x, y, z) {
// ...
}
let args = [0, 1, 2];
f.apply(null, args);

// ES6 的写法
function f(x, y, z) {
// ...
}
let args = [0, 1, 2];
f(...args);

8.2 扩展运算符的应用

(1)复制数组

数组是复合的数据类型,直接复制的话,只是复制了指向底层数据结构的指针,而不是克隆一个全新的数组。扩展运算符提供了复制数组的简便写法。

const a1 = [1, 2];
const a2 = a1;

a2[0] = 2;
a1; // [2, 2],浅拷贝

// 写法一
const a2 = [...a1];
// 写法二
const [...a2] = a1;

(2)合并数组

扩展运算符提供了数组合并的新写法。

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' ]

不过两种方法都是浅拷贝,使用的时候需要特别小心。

const a1 = [{ foo: 1 }];
const a2 = [{ bar: 2 }];

const a3 = a1.concat(a2);
const a4 = [...a1, ...a2];

a3[0] === a1[0]; // true
a4[0] === a1[0]; // true

上面代码中,a3a4是用两种不同方法合并而成的新数组,但是它们的成员都是对原数组成员的引用,这就是浅拷贝。如果修改了引用指向的值,会同步反映到新数组

(3)与解构赋值结合

扩展运算符可以与解构赋值结合起来,用于生成数组。

const [first, ...rest] = [1, 2, 3, 4, 5];
first; // 1
rest; // [2, 3, 4, 5]

const [first, ...rest] = [];
first; // undefined
rest; // []

(4)具有 Iterator 接口的对象

扩展运算符调用的是数据结构的 Iterator 接口,因此只要具有 Iterator 接口的对象,都可以使用扩展运算符。比如字符串,MapSetGenerator函数返回的遍历器对象。

[..."hello"];
// [ "h", "e", "l", "l", "o" ]

let map = new Map([
[1, "one"],
[2, "two"],
[3, "three"],
]);

let arr = [...map.keys()]; // [1, 2, 3]

const go = function* () {
yield 1;
yield 2;
yield 3;
};

[...go()]; // [1, 2, 3]

8.3 新增方法

静态方法 1:Array.from()方法用于将两类对象转为真正的数组,类似数组的对象和可遍历的对象。(包括 ES6 新增的数据结构 SetMap,和部署了 Iterator 接口的数据结构)

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']

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]

静态方法 2:Array.of()方法用于将一组值,转换为数组。它可以替代Array()new Array(),并且不存在由于参数不同而导致的重载。

Array.of(3, 11, 8); // [3,11,8]
Array.of(3); // [3]
Array.of(3).length; // 1

// 重载
Array(); // []
Array(3); // [, , ,]
Array(3, 11, 8); // [3, 11, 8]

实例方法 1:find()方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined

findIndex()方法的用法与find()方法类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1find()findIndex()都是从数组的0号位,依次向后检查。

[1, 4, -5, 10]
.find((n) => n < 0)
[
// -5

(1, 5, 10, 15)
].findIndex(function (value, index, arr) {
// 当前的值、当前的位置和原数组
return value > 9;
}); // 2

与之相反,findLast()findLastIndex()是从数组的最后一个成员开始,依次向前检查,其他都保持不变。

const array = [{ value: 1 }, { value: 2 }, { value: 3 }, { value: 4 }];

array.findLast((n) => n.value % 2 === 1); // { value: 3 }
array.findLastIndex((n) => n.value % 2 === 1); // 2

实例方法 2:fill()方法使用给定值,填充一个数组。它还可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置。

["a", "b", "c"].fill(7);
// [7, 7, 7]

new Array(3).fill(7)[
// [7, 7, 7]

("a", "b", "c")
].fill(7, 1, 2);
// ['a', 7, 'c']

实例方法 3:ES6 提供三个新的方法用于遍历数组。keys()是对键名的遍历,values()是对键值的遍历,entries()是对键值对的遍历。它们都可以用for...of循环进行遍历。

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"

实例方法 4:includes()方法返回一个布尔值,表示某个数组是否包含给定的值。该方法的第二个参数表示搜索的起始位置,默认为0。如果第二个参数为负数,则表示倒数的位置。

[1, 2, 3]
.includes(2) // true
[(1, 2, 3)].includes(4) // false
[(1, 2, NaN)].includes(NaN) // true
[(1, 2, 3)].includes(3, -1); // true

实例方法 5:数组的成员有时还是数组,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]

(1, [2, [3]])
].flat(Infinity);
// [1, 2, 3]

flatMap()方法对原数组的每个成员执行一个函数(相当于执行map()), 然后对返回值组成的数组执行flat()方法。注意,flatMap()只能展开一层数组。该方法返回一个新数组,不改变原数组。

// 相当于 [[2, 4], [3, 6], [4, 8]].flat()
[2, 3, 4]
.flatMap((x) => [x, x * 2])
[
// [2, 4, 3, 6, 4, 8]

// 相当于 [[[2]], [[4]], [[6]], [[8]]].flat()
(1, 2, 3, 4)
].flatMap((x) => [[x * 2]]);
// [[2], [4], [6], [8]]

实例方法 6:ES5 不支持数组的负索引,如果要引用数组的最后一个成员,不能写成arr[-1],只能使用arr[arr.length - 1]ES6 为数组实例增加了at()方法,接受一个整数作为参数,返回对应位置的成员,并支持负索引。

const arr = [5, 12, 8, 130, 44];

arr[-2]; // Error
arr.at(2); // 8
arr.at(-2); // 130

9. 对象的扩展

本章内容复杂,故先跳过一部分。

9.1 属性的简洁表示法

ES6 允许在大括号里面,直接写入变量和函数,作为对象的属性和方法。

let birth = "2000/01/01";

const Person = {
name: "张三",
birth, // 等同于 birth: birth
hello() {
console.log("我的名字是", this.name);
},
// 等同于 hello: function ()...
};

9.2 super 关键字

众所周知,this关键字总是指向函数所在的当前对象ES6 又新增了另一个类似的关键字super,指向当前对象的原型对象

const proto = {
x: "hello",
foo() {
console.log(this.x);
},
};

const obj = {
x: "world",
foo() {
super.foo();
},
};

Object.setPrototypeOf(obj, proto);
obj.foo(); // "world"

上面代码中,对象obj.foo()方法之中,通过super.foo引用了原型对象protofoo方法。但是绑定的this却还是当前对象obj,因此输出的就是world

注意,super关键字表示原型对象时,只能用在对象的方法之中,用在其他地方都会报错。

/ 报错
const obj = {
foo: super.foo // 对象属性
}

// 报错
const obj = {
foo: () => super.foo // 对象属性
}

// 报错
const obj = {
foo: function () { // 对象属性
return super.foo
}
}

9.3 对象的扩展运算符

(1)解构赋值

对象的解构赋值用于从一个对象取值,将目标对象所有可遍历的、但尚未被读取的属性,分配到指定的对象上面。(注意,解构赋值必须是最后一个参数,且等号右边不能是undefinednull

let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
x // 1
y // 2
z // { a: 3, b: 4 }

let z = { a: 3, b: 4 };
let n = { ...z };
n // { a: 3, b: 4 }

let { ...x, y, z } = someObject; // 句法错误

解构赋值的拷贝是浅拷贝,即如果一个键的值是复合类型的值(如数组、对象、函数),那么解构赋值拷贝的是这个值的引用,而不是这个值的副本

let obj = { a: { b: 1 } };
let { ...x } = obj;
obj.a.b = 2;
x.a.b; // 2

上面代码中,x是解构赋值所在的对象,拷贝了对象obja属性。a属性引用了一个对象,修改这个对象的值,会影响到解构赋值对它的引用。

解构赋值的一个用处,是扩展某个函数的参数,引入其他操作。

function baseFunction({ a, b }) {
// ...
}
function wrapperFunction({ x, y, ...restConfig }) {
// 使用 x 和 y 参数进行操作
// 其余参数传给原始函数
return baseFunction(restConfig);
}

上面代码中,原始函数baseFunction接受ab作为参数,函数wrapperFunctionbaseFunction的基础上进行了扩展,能够接受多余的参数,并且保留原始函数的行为。

(2)扩展运算符

对象的扩展运算符 ... 用于取出参数对象的所有可遍历属性,拷贝到当前对象之中。(扩展运算符不需要是最后一个参数)

let z = { a: 3, b: 4 };
let n = { ...z };
n // { a: 3, b: 4 }

{...{}, a: 1}
// { a: 1 }

扩展运算符还可以用于合并两个对象。

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 });

上面代码中,a对象的x属性和y属性,拷贝到新对象后会被覆盖掉。


10. 对象的新增方法

本章内容复杂,故先跳过一部分。

10.1 静态方法

ES5 中比较两个值是否相等,只有两个运算符,相等运算符==和严格相等运算符===。它们都有缺点,前者会自动转换数据类型,后者的NaN不等于自身,以及+0等于-0

静态方法 1:Object.is()方法用来比较两个值是否严格相等,与严格比较运算符===的行为基本一致。不同之处只有两个,一是+0不等于-0,二是NaN等于自身。

Object.is("foo", "foo");
// true

Object.is({}, {});
// false

静态方法 2: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()有以下几个使用注意点。

(1)浅拷贝

Object.assign()方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用(指针)。

const obj1 = {a: {b: 1}};
const obj2 = Object.assign({}, obj1);

obj1.a.b = 2;
obj2.a.b // 2

上面代码中,源对象obj1a属性的值是一个对象,Object.assign()拷贝得到的是这个对象的引用。这个对象的任何变化,都会反映到目标对象上面。

(2)同名属性的替换

对于嵌套的对象,一旦遇到同名属性,Object.assign()的处理方法是替换,而不是添加。这通常不是我们想要的,需要特别小心。

const target = { a: { b: "c", d: "e" } };
const source = { a: { b: "hello" } };
Object.assign(target, source);
// { a: { b: 'hello' } }

静态方法 3:对于普通对象,我们可以用下面的方法对对象进行遍历。

  • Object.keys()返回一个数组,成员是参数对象自身的所有可遍历属性的键名。
  • Object.values()返回一个数组,成员是参数对象自身的所有可遍历属性的键值。
  • Object.entries()返回一个数组,成员是参数对象自身的所有可遍历属性的键值对数组。
const obj = { foo: "bar", baz: 42 };
Object.keys(obj);
// ["foo", "baz"]

const obj = { foo: "bar", baz: 42 };
Object.values(obj);
// ["bar", 42]

const obj = { foo: "bar", baz: 42 };
Object.entries(obj);
// [ ["foo", "bar"], ["baz", 42] ]

11. 运算符的扩展

11.1 链判断运算符

当我们打算读取对象内部的某个属性时,往往需要判断一下属性的上层对象是否存在。

// 错误的写法
const firstName = message.body.user.firstName || "default"; // 无法保证 message.body 存在...

// 正确的写法
const firstName =
(message &&
message.body &&
message.body.user &&
message.body.user.firstName) ||
"default";

上面代码中,firstName属性在对象的第四层,所以需要判断四次,每一层是否有值。这样的层层判断非常麻烦,因此 ES6 引入了“链判断运算符” ?.,用来简化上面的写法。

const firstName = message?.body?.user?.firstName || "default";

上面代码使用了?.运算符,直接在链式调用的时候判断,左侧的对象是否为nullundefined。如果是的,就不再往下运算,而是返回undefined

下面是 ?. 运算符常见形式,以及不使用该运算符时的等价形式。

a?.b;
// 等同于
a == null ? undefined : a.b;

a?.[x];
// 等同于
a == null ? undefined : a[x];

a?.b();
// 等同于
a == null ? undefined : a.b();

a?.();
// 等同于
a == null ? undefined : a();

11.2 Null 判断运算符

读取对象属性的时候,如果某个属性的值是nullundefined,有时候需要为它们指定默认值。常见做法是通过||运算符指定默认值。

const headerText = response.settings.headerText || "Hello, world!";
const animationDuration = response.settings.animationDuration || 300;
const showSplashScreen = response.settings.showSplashScreen || true;

但是属性的值如果为空字符串或false0,默认值也会生效。为此,ES6 引入了一个新的 Null 判断运算符??。它的行为类似||,但是只有运算符左侧的值为nullundefined时,才会返回右侧的值。

const animationDuration = response.settings?.animationDuration ?? 300;

上面代码中,如果response.settingsnullundefined,或者response.settings.animationDurationnullundefined,就会返回默认值300


12. Symbol 数据类型

本章内容复杂,故先跳过一部分。

12.1 Symbol 概述

ES6 引入了一种新的原始数据类型 Symbol,表示独一无二的值。

Symbol 通过 Symbol() 函数生成,可以用于为对象添加新的方法,且不存在属性名冲突。

let s = Symbol(); // Symbol() 函数前不能使用 new 命令

typeof s;
// "symbol"

ES6 提供了一个 Symbol 值的实例属性description,直接返回 Symbol 值的描述。

const sym = Symbol("foo"); // 字符串作为参数

sym; // [object Symbol] { ... }
sym.toString(); // "Symbol(foo)"

sym.description; // "foo"

因为 Symbol 值都是不相等的(就算是相同描述,除非使用 Symbol.for),使用 Symbol 值作为标识符,用于对象的属性名,就能保证不会出现同名的属性。

let mySymbol = Symbol();

// 第一种写法
let a = {};
a[mySymbol] = "Hello!";

// 第二种写法
let a = {
[mySymbol]: "Hello!",
};

// 以上写法都得到同样结果
a[mySymbol]; // "Hello!"

13. Set 和 Map 数据结构

本章跳过 WeakSetWeakMap 部分。

13.1 Set 基本用法

ES6提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。它可以接受一个数组,或者具有 iterable 接口的其他数据结构,作为参数初始化。

const s = new Set();

[2, 3, 5, 4, 5, 2, 2].forEach((x) => s.add(x));

for (let i of s) {
console.log(i);
}
// 2 3 5 4

下面介绍 Set 实例的属性和方法:

  • Set.prototype.size:返回Set实例的成员总数。
  • Set.prototype.add(value):添加某个值,返回 Set 结构本身。
  • Set.prototype.delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
  • Set.prototype.has(value):返回一个布尔值,表示该值是否为Set的成员。
  • Set.prototype.clear():清除所有成员,没有返回值。
const s = new Set();
s.add(1).add(2).add(2);
// 注意2被加入了两次

s.size; // 2

s.has(1); // true
s.has(2); // true
s.has(3); // false

s.delete(2);
s.has(2); // false

Set 结构的实例默认可遍历, 可以直接用for...of循环遍历 Set。也可以用forEach方法,用于对每个成员执行某种操作,没有返回值。

let set = new Set(["red", "green", "blue"]);

for (let x of set) {
console.log(x);
}
// red
// green
// blue

let set = new Set([1, 4, 9]);
set.forEach((value, key) => console.log(key + " : " + value));
// 1 : 1
// 4 : 4
// 9 : 9

上面代码中forEach方法的参数是一个处理函数。但是因为 Set 结构的键名就是键值,因此第一个参数与第二个参数的值永远都是一样的。


13.2 Set 遍历的应用

扩展运算符和 Set 结构相结合,可以去除数组的重复成员。

// 去除数组的重复成员
[...new Set(array)]

// 去除字符串里面的重复字符
[...new Set('ababbc')].join('')
// "abc"

// 用Array.from方法可以将 Set 结构转为数组。
function dedupe(array) {
return Array.from(new Set(array));
}

dedupe([1, 1, 2, 3]) // [1, 2, 3]

数组的mapfilter方法也可以间接用于 Set

let set = new Set([1, 2, 3]);
set = new Set([...set].map((x) => x * 2));
// 返回Set结构:{2, 4, 6}

let set = new Set([1, 2, 3, 4, 5]);
set = new Set([...set].filter((x) => x % 2 == 0));
// 返回Set结构:{2, 4}

13.3 Map 基本用法

Map 数据结构类似于对象,是键值对的集合,但是各种类型的值(包括对象)都可以当作键。

const map = new Map([
["name", "张三"],
["title", "Author"],
]);

map.size; // 2
map.has("name"); // true
map.get("name"); // "张三"
map.has("title"); // true
map.get("title"); // "Author"

下面介绍 Map 实例的属性和方法:

  • Map.prototype.size:返回 Map 结构的成员总数。
  • Map.prototype.set(key, value):设置键名key对应的键值为value,然后返回整个 Map 结构。如果key已经有值,则键值会被更新,否则就新生成该键。
  • Map.prototype.get(key):读取key对应的键值,如果找不到key,返回undefined
  • Map.prototype.has(key):返回一个布尔值,表示某个键是否在当前 Map 对象之中。
  • Map.prototype.delete(key):删除某个键,返回true。如果删除失败,返回false
  • Map.prototype.clear():清除所有成员,没有返回值。
const map = new Map();
map.set("foo", "hello");
map.set("bar", false);

map.size; // 2

map.get("foo"); // hello
map.has("foo"); // true

map.delete("foo");
map.has("foo"); // false

map.clear();
map.size; // 0

下面介绍 Map 结构原生提供三个遍历器生成函数和一个遍历方法:

  • Map.prototype.keys():返回键名的遍历器。
  • Map.prototype.values():返回键值的遍历器。
  • Map.prototype.entries():返回所有成员的遍历器。
  • Map.prototype.forEach():遍历 Map 的所有成员。
const map = new Map([
["F", "no"],
["T", "yes"],
]);

for (let key of map.keys()) {
console.log(key);
}
// "F"
// "T"

for (let value of map.values()) {
console.log(value);
}
// "no"
// "yes"

for (let item of map.entries()) {
console.log(item[0], item[1]);
}
// "F" "no"
// "T" "yes"

// 或者
for (let [key, value] of map.entries()) {
console.log(key, value);
}
// "F" "no"
// "T" "yes"

// 等同于使用map.entries()
for (let [key, value] of map) {
console.log(key, value);
}
// "F" "no"
// "T" "yes"

map.forEach(function (value, key, map) {
console.log("Key: %s, Value: %s", key, value);
});

需要特别注意的是,Map 的遍历顺序就是插入顺序。


13.4 Map 与其他数据结构转换

(1)Map 转为数组

Map 转为数组最方便的方法,就是使用扩展运算符...

const myMap = new Map().set(true, 7).set({ foo: 3 }, ["abc"]);
[...myMap];
// [ [ true, 7 ], [ { foo: 3 }, [ 'abc' ] ] ]

结合数组的map方法、filter方法,就可以实现 Map 的遍历和过滤。

const map0 = new Map().set(1, "a").set(2, "b").set(3, "c");

const map1 = new Map([...map0].filter(([k, v]) => k < 3));
// 产生 Map 结构 {1 => 'a', 2 => 'b'}

const map2 = new Map([...map0].map(([k, v]) => [k * 2, "_" + v]));
// 产生 Map 结构 {2 => '_a', 4 => '_b', 6 => '_c'}

(2)数组 转为 Map

将数组传入 Map 构造函数,就可以转为 Map

new Map([
[true, 7],
[{ foo: 3 }, ["abc"]],
]);
// Map {
// true => 7,
// Object {foo: 3} => ['abc']
// }

(3)Map 转为对象

如果所有 Map 的键都是字符串,它可以无损地转为对象。如果有非字符串的键名,那么这个键名会被转成字符串,再作为对象的键名。

function strMapToObj(strMap) {
let obj = Object.create(null);
for (let [k, v] of strMap) {
obj[k] = v;
}
return obj;
}

const myMap = new Map().set("yes", true).set("no", false);
strMapToObj(myMap);
// { yes: true, no: false }

(4)对象转为 Map

对象转为 Map 可以通过 Object.entries()

let obj = { a: 1, b: 2 };
let map = new Map(Object.entries(obj));

(5)Map 转为 JSON

Map 转为 JSON 要区分两种情况。

一种情况是,Map 的键名都是字符串,这时可以选择转为对象 JSON

function strMapToJson(strMap) {
return JSON.stringify(strMapToObj(strMap));
}

let myMap = new Map().set("yes", true).set("no", false);
strMapToJson(myMap);
// '{"yes":true,"no":false}'

另一种情况是,Map 的键名有非字符串,这时可以选择转为数组 JSON

function mapToArrayJson(map) {
return JSON.stringify([...map]);
}

let myMap = new Map().set(true, 7).set({ foo: 3 }, ["abc"]);
mapToArrayJson(myMap);
// '[[true,7],[{"foo":3},["abc"]]]'

(6)JSON 转为 Map

JSON 转为 Map,正常情况下,所有键名都是字符串。

function objToStrMap(obj) {
let strMap = new Map();
for (let k of Object.keys(obj)) {
strMap.set(k, obj[k]);
}
return strMap;
}

function jsonToStrMap(jsonStr) {
return objToStrMap(JSON.parse(jsonStr));
}

jsonToStrMap('{"yes": true, "no": false}');
// Map {'yes' => true, 'no' => false}

如果整个 JSON 就是一个数组,且每个数组成员本身,又是一个有两个成员的数组。这时,它可以直接转为 Map。(类似 Map 转为数组 JSON 的逆操作)

function jsonToMap(jsonStr) {
return new Map(JSON.parse(jsonStr));
}

jsonToMap('[[true,7],[{"foo":3},["abc"]]]');
// Map {true => 7, Object {foo: 3} => ['abc']}

14. Proxy

本章内容复杂,故先跳过一部分。

14.1 Proxy 概述

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写

let obj = new Proxy(
{},
{
get: function (target, propKey, receiver) {
console.log(`getting ${propKey}!`);
return Reflect.get(target, propKey, receiver);
},
set: function (target, propKey, value, receiver) {
console.log(`setting ${propKey}!`);
return Reflect.set(target, propKey, value, receiver);
},
}
);

上面代码对一个空对象架设了一层拦截,重定义了属性的读取get和设置set行为。对设置了拦截行为的对象obj读写它的属性,就会得到下面的结果。

obj.count = 1;
// setting count!
++obj.count;
// getting count!
// setting count!
// 2

这里可以看出,Proxy 实际上重载overload了点运算符,即用自己的定义覆盖了语言的原始定义。(类似C++的重载)

ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为。

let proxy = new Proxy(target, handler);

下面是另一个拦截读取属性行为的例子。

let proxy = new Proxy(
{},
{
get: function (target, propKey) {
return 35;
},
}
);

proxy.time; // 35
proxy.name; // 35
proxy.title; // 35

上面代码中,作为构造函数,Proxy接受两个参数。第一个参数是所要代理的目标对象。第二个参数是一个配置对象,对于每一个被代理的操作,需要提供一个对应的处理函数,该函数将拦截对应的操作。

注意,要使得Proxy起作用,必须针对Proxy实例进行操作,而不是针对目标对象进行操作。


14.2 Proxy 的实例方法

Proxy 支持的拦截操作一共 13 种。(剩下的可以在这里找到)

实例方法 1:get()方法用于拦截某个属性的读取操作,可以接受三个参数,依次为目标对象、属性名和 proxy 实例本身。

let person = {
name: "张三",
};

let proxy = new Proxy(person, {
get: function (target, propKey) {
if (propKey in target) {
return target[propKey];
} else {
throw new ReferenceError('Prop name "' + propKey + '" does not exist.');
}
},
});

proxy.name; // "张三"
proxy.age; // 抛出一个错误

实例方法 2: set()方法用来拦截某个属性的赋值操作,可以接受四个参数,依次为目标对象、属性名、属性值和 Proxy 实例本身。

let validator = {
set: function (obj, prop, value) {
if (prop === "age") {
if (!Number.isInteger(value)) {
throw new TypeError("The age is not an integer");
}
if (value > 200) {
throw new RangeError("The age seems invalid");
}
}

// 对于满足条件的 age 属性以及其他属性,直接保存
obj[prop] = value;
return true;
},
};

let person = new Proxy({}, validator);
person.age = 100;

person.age; // 100
person.age = "young"; // 报错
person.age = 300; // 报错

15. Reflect

15.1 Reflect 概述

Reflect对象与Proxy对象一样,也是 ES6 为了操作对象而提供的新 APIReflect对象的设计目的有这样几个。

(1)将Object对象的一些属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。也就是说,未来可以从Reflect对象上拿到语言内部的方法。

(2)修改某些Object方法的返回结果,让其变得更合理。

// 老写法
try {
Object.defineProperty(target, property, attributes);
// success
} catch (e) {
// 报错
// failure
}

// 新写法
if (Reflect.defineProperty(target, property, attributes)) {
// success
} else {
// 直接返回 false
// failure
}

(3)让Object操作都变成函数行为。比如name in objdelete obj[name],而Reflect.has(obj, name)Reflect.deleteProperty(obj, name)让它们变成了函数行为。

// 老写法
"assign" in Object; // true

// 新写法
Reflect.has(Object, "assign"); // true

(4)让Reflect对象的方法与Proxy对象的方法对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。

let loggedObj = new Proxy(obj, {
get(target, name) {
console.log("get", target, name);
return Reflect.get(target, name);
},
deleteProperty(target, name) {
console.log("delete" + name);
return Reflect.deleteProperty(target, name);
},
has(target, name) {
console.log("has" + name);
return Reflect.has(target, name);
},
});

上面代码中,每一个Proxy对象的拦截操作getdeletehas,内部都调用对应的Reflect方法,保证原生行为能够正常执行。

简单来说,有了Reflect对象以后,很多操作会更易读。

// 老写法
Function.prototype.apply.call(Math.floor, undefined, [1.75]); // 1

// 新写法
Reflect.apply(Math.floor, undefined, [1.75]); // 1

15.2 Reflect 静态方法

Reflect对象也有 13 个静态方法。(剩下的可以在这里找到)

静态方法 1:Reflect.get方法查找并返回target对象的name属性。

let myObject = {
foo: 1,
bar: 2,
get baz() {
return this.foo + this.bar;
},
};

Reflect.get(myObject, "foo"); // 1
Reflect.get(myObject, "bar"); // 2
Reflect.get(myObject, "baz"); // 3

静态方法 2:Reflect.set方法设置target对象的name属性等于value

let myObject = {
foo: 1,
set bar(value) {
return (this.foo = value);
},
};

myObject.foo; // 1

Reflect.set(myObject, "foo", 2);
myObject.foo; // 2

Reflect.set(myObject, "bar", 3);
myObject.foo; // 3

附录

该学习笔记的所有章节均保存在Github中,如有需要,可下载并在Typora中阅读。