ES6学习笔记 • 上
前言
本读书笔记节是针对阮一峰编写的 《ECMAScript 6 入门教程》的个人解读。适合已经掌握
ES5
的读者,用来了解这门语言的最新发展。笔记内容仅包括日常开发中可能会用到的概念,剩余部分请自行阅读。
1. let 和 const 命令
1.1 let 命令
let
的用法类似于var
,但所声明的变量只在let
命令所在的代码块(即 {}
包裹起来的内容)内有效。而var
所声明的变量在全局范围内都有效。
{ |
(1)不存在变量提升
var
命令会发生“变量提升”现象,即变量可以在声明之前使用,值为undefined
。为了纠正这种现象,let
命令改变了语法行为,它所声明的变量一定要在声明后使用。
// var 的情况 |
(2)暂时性死区
只要块级作用域内存在let
和const
命令,它所声明的变量就“绑定”(binding
)这个区域,不再受外部的影响。凡是在声明之前就使用这些变量,就会报错。
var tmp = 123; |
1.2 块级作用域
ES5
只有全局作用域和函数作用域,没有块级作用域,这带来很多不合理的情况:
(1)内层变量覆盖外层变量
var tmp = new Date(); |
上面代码的原意是,if
代码块的外部使用外层的tmp
变量,内部使用内层的tmp
变量。但是函数f
执行后,输出结果却为undefined
。
原因其实就是变量提升,导致内层的tmp
变量覆盖了外层的tmp
变量。实际中的代码如下:
var tmp = new Date(); |
(2)用来计数的循环变量泄露为全局变量
下面代码中,变量i
只用来控制循环,但是循环结束后,它并没有消失,泄露成了全局变量。
var s = "hello"; |
为了应对上面这些情况,ES6
新增了块级作用域,外层作用域无法获取到内层作用域。即使外层和内层都使用相同变量名,也都互不干扰。
function f1() { |
上面的函数有两个代码块,都声明了变量n
,运行后输出 5
。这表示外层代码块不受内层代码块的影响。如果两次都使用var
定义变量n
,最后输出的值就是 10
。
1.3 const 命令
const
声明一个只读常量。一旦声明,常量的值就不能改变,且必须立即初始化。(与 let
一样,const
不存在变量提升,存在暂时性死区)
const PI = 3.1415; |
const
保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。
- 对于简单类型的数据(比如数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。
- 但对于复合类型的数据(比如对象、数组),变量指向的内存地址,保存的只是一个指向实际数据的指针。
const
只能保证这个指针是固定的,至于它指向的数据结构是不是可变的,就完全不能控制了。
const foo = {}; |
2. 变量的解构赋值
2.1 数组的解构赋值
从数组或对象中提取值,按照对应位置, 对变量进行赋值,这被称为解构。
let [a, b, c] = [1, 2, 3]; |
这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。
let [foo, [[bar], baz]] = [1, [[2], 3]]; |
数组的解构赋值有两种特殊情况:
- 解构不成功,那么对应变量的值就等于
undefined
。 - 不完全解构,等号左边的模式,只会匹配一部分的等号右边的数组。
let [foo] = []; // foo == undefined |
事实上,只要某种数据结构具有 Iterator
接口,都可以采用数组形式的解构赋值。
let [x, y, z] = new Set(["a", "b", "c"]); // Set 原生带有 Iterator 接口 |
数组的解构赋值可以指定默认值,并且必须严格等于undefined
才会生效。
let [foo = true] = []; |
2.2 对象的解构赋值
对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定。而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。
let { bar, foo } = { foo: "aaa", bar: "bbb" }; |
如果变量名与属性名不一致,必须写成下面这样。
let obj = { first: "hello", last: "world" }; |
上面的代码可以看出,对象的解构赋值的机制,是先找到同名属性,然后再赋给对应的变量,真正被赋值的是后者,而不是前者 。
let { foo: foo, bar: bar } = { foo: "aaa", bar: "bbb" }; |
解构也可以用于嵌套结构的对象。下面代码分别对p
,x
,y
三个属性进行解构赋值。注意,在对x
,y
属性的解构赋值之中,x
,y
是变量,p
是模式。
let obj = { |
对象的解构也可以指定默认值,并且必须严格等于undefined
才会生效。
let { x = 3 } = {}; |
2.3 函数参数的解构赋值
函数的参数也可以使用解构赋值。
function move({ x = 0, y = 0 } = {}) { |
上面代码中,函数move
的参数是一个对象。通过对这个对象进行解构,得到变量x
和y
的值。如果解构失败,x
和y
等于默认值。
注意,下面的写法会得到与上面不一样的结果。
function move({ x, y } = { x: 0, y: 0 }) { |
上面代码为函数move
的参数指定默认值,而不是为变量x
和y
指定默认值。当参数为undefined
时,参数默认值启用。
2.4 解构赋值的用途
(1)交换变量的值
下面代码交换变量x
和y
的值,这样的写法简洁易读,语义清晰。
let x = 1; |
(2)从函数返回多个值
函数只能返回一个值,如果要返回多个值,只能将它们放在数组或对象里返回。有了解构赋值,取出这些值就非常方便。
// 返回一个数组 |
(3)提取 JSON 数据
解构赋值对提取 JSON
对象中的数据,尤其有用。
let jsonData = { |
(4)函数参数的定义
解构赋值可以方便地将一组参数与变量名对应起来。
// 参数是一组有次序的值 |
(5)遍历 Map 结构
前面提到,任何部署了 Iterator
接口的对象,都可以用for...of
循环遍历。Map
结构原生支持 Iterator
接口,配合变量的解构赋值,获取键名和键值就非常方便。
const map = new Map(); |
3. 字符串的扩展
本章内容复杂,故先跳过一部分。
3.1 模板字符串
$("#result").append(` |
模板字符串(template string
)是增强版的字符串。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。
// 字符串中嵌入变量 |
注意,如果使用模板字符串表示多行字符串,所有的空格,换行,缩进都会被保留在输出之中。
let str1 = "Hello" + "World"; |
3.2 标签模板
模板字符串可以紧跟在一个函数名后面,该函数将被调用来处理这个模板字符串。这被称为“标签模板”功能(tagged template
)。
- 标签模板不是模板,而是函数调用的一种特殊形式。“标签”指的是函数,它的参数是紧跟在后面的模板字符串。
alert`hello`; |
如果模板字符里面有变量,就不是直接调用了。会先将模板字符串先处理成多个参数,再调用函数。
let a = 5; |
4. 字符串的新增方法
4.1 新增方法
实例方法 1:ES5
中只有indexOf()
方法可以用来确定一个字符串是否包含在另一个字符串中。ES6
又提供了三种新方法:
includes()
:返回布尔值,表示是否找到了参数字符串。startsWith()
:返回布尔值,表示参数字符串是否在原字符串的头部。endsWith()
:返回布尔值,表示参数字符串是否在原字符串的尾部。
let s = "Hello world!"; |
这三个方法都支持第二个参数,表示开始搜索的位置。
let s = "Hello world!"; |
上面代码中,使用第二个参数n
时,endsWith()
的行为与其他两个方法不同。它针对前n
个字符,而其他两个方法针对从第n
个位置直到字符串结束。
实例方法 2:repeat()
方法返回一个新字符串,将原字符串重复n
次。
"na".repeat(3); // "nana" |
实例方法 3:ES6
引入字符串补全长度功能,能在某个字符串不够指定长度时,往头部或尾部补全。padStart()
用于头部补全,padEnd()
用于尾部补全。
"x".padStart(5, "ab"); // 'ababx' 补到length == 5 |
实例方法 4:ES6
新增了trimStart()
和trimEnd()
两个方法。它们的行为与trim()
一致,trimStart()
消除字符串头部的空格,trimEnd()
消除尾部的空格。它们返回的都是新字符串,不会修改原始字符串。
const s = " abc "; |
实例方法 5:ES5
中,replace()
只能替换第一个匹配。如果要替换所有的匹配,需要使用正则表达式的g
修饰符。ES6
引入了replaceAll()
方法,可以一次性替换所有匹配。它返回的是新字符串,不会修改原始字符串。
"aabbcc".replace("b", "_"); |
实例方法 6:ES6
增加了matchAll()
方法,可以一次性取出字符串里面所有匹配。但是,它返回的是一个遍历器,而不是数组。
const string = "test1test2test3"; |
遍历器转为数组是非常简单的,使用...
运算符和Array.from()
方法就可以。
// 转为数组的方法一 |
5. 正则的扩展
本章内容复杂,故先跳过一部分。
5.1 具名组匹配
正则表达式使用圆括号进行组匹配。使用exec
方法,就可以将这匹配结果提取出来。
const RE_DATE = /(\d{4})-(\d{2})-(\d{2})/; |
组匹配的一个问题是,每一组的匹配含义不容易看出来,而且只能用数字序号XXX[1]
引用,要是组的顺序变了,引用的时候就必须修改序号。
ES6
引入了具名组匹配(Named Capture Groups
),允许为每一个组匹配指定一个名字,既便于阅读代码,又便于引用。
const RE_DATE = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/; |
6. 数值的扩展
本章内容复杂,故先跳过一部分。
6.1 BigInt 数据类型
ES5
中所有数字都保存成 64
位浮点数,这给数值的表示带来了两大限制:
- 数值的精度只能到
53
个二进制位,相当于16
个十进制位,大于这个范围的整数,无法精确表示。 (不适合进行科学和金融方面的精确计算) - 大于或等于
2
的1024
次方的数值无法表示,会返回Infinity
。
// 超过 53 个二进制位的数值,无法保持精度 |
ES6
引入了一种新的数据类型 BigInt
,来解决这个问题,这是 ECMAScript
的第八种数据类型。BigInt
只用来表示整数,没有位数的限制,任何位数的整数都可以精确表示。
const a = 2172141653n; |
BigInt
与普通整数是两种值,它们之间并不相等,且不能进行混合运算。为了与 Number
类型区别,BigInt
类型的数据必须添加后缀n
。
42n === 42; // false |
7. 函数的扩展
7.1 函数参数的默认值
ES6
允许为函数的参数设置默认值,可以直接写在参数定义的后面(通常是函数的尾参数)。
function Point(x = 0, y = 0) { |
函数参数的默认值有两个好处:
- 阅读代码的人,可以立刻意识到哪些参数是可以省略的,不用查看函数体或文档。
- 有利于将来的代码优化,即使未来的版本在对外接口中,彻底拿掉这个参数,也不会导致以前的代码无法运行。
参数默认值可以与解构赋值的默认值结合使用。
function fetch(url, { body = "", method = "GET", headers = {} }) { |
上面代码中,函数fetch
的第二个参数是一个对象,它的三个属性都有默认值。这种写法不能省略第二个参数,如果结合函数参数的默认值,就可以省略第二个参数。
function fetch(url, { body = "", method = "GET", headers = {} } = {}) { |
上面代码中,当函数fetch
没有第二个参数时,函数参数的默认值就会生效,然后才是解构赋值的默认值生效,变量method
才会取到默认值GET
。
注意,函数参数的默认值生效以后,参数解构赋值依然会进行。
function f({ a, b = "world" } = { a: "hello" }) { |
7.2 rest 参数
rest
参数可以代替arguments
对象,去获取函数的多余参数。rest
参数之后不能再有其他参数,它只能是最后一个参数。
// arguments变量的写法 |
上面代码的两种写法,比较后可以发现,rest
参数的写法更自然也更简洁。
7.3 箭头函数
ES6
允许使用“箭头” =>
定义函数。
- 如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。
- 如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用
return
语句返回。
let f = () => 5; |
箭头函数可以与变量解构结合使用。
const full = ({ first, last }) => first + " " + last; |
箭头函数还可以和上面的 rest
参数结合使用。
const numbers = (...nums) => nums; |
箭头函数的重要用处是简化回调函数。
// 普通函数写法 |
箭头函数有几个使用注意点,其中最重要的是,箭头函数没有自己的this
对象。
- 对于普通函数来说,内部的
this
指向函数运行时所在的对象,但是这一点对箭头函数不成立。它没有自己的this
对象,内部的this
就是定义时上层作用域中的this
。也就是说,箭头函数内部的this
指向是固定的,相比之下,普通函数的this
指向是可变的。
function Timer() { |
上面代码中,Timer
函数内部设置了两个定时器,分别使用了箭头函数和普通函数。前者的this
绑定定义时所在的作用域,即Timer
函数,后者的this
指向运行时所在的作用域,即全局对象。所以,3100
毫秒之后,timer.s1
被更新了 3
次,而timer.s2
一次都没更新。
8. 数组的扩展
8.1 扩展运算符
扩展运算符 ...
,将一个数组转为用逗号分隔的参数序列。类似 rest
参数的逆运算。
console.log(...[1, 2, 3]) |
扩展运算符主要用于函数调用,可以代替apply()
方法将数组转为函数的参数。
// ES5 的写法 |
8.2 扩展运算符的应用
(1)复制数组
数组是复合的数据类型,直接复制的话,只是复制了指向底层数据结构的指针,而不是克隆一个全新的数组。扩展运算符提供了复制数组的简便写法。
const a1 = [1, 2]; |
(2)合并数组
扩展运算符提供了数组合并的新写法。
const arr1 = ["a", "b"]; |
不过两种方法都是浅拷贝,使用的时候需要特别小心。
const a1 = [{ foo: 1 }]; |
上面代码中,a3
和a4
是用两种不同方法合并而成的新数组,但是它们的成员都是对原数组成员的引用,这就是浅拷贝。如果修改了引用指向的值,会同步反映到新数组。
(3)与解构赋值结合
扩展运算符可以与解构赋值结合起来,用于生成数组。
const [first, ...rest] = [1, 2, 3, 4, 5]; |
(4)具有 Iterator 接口的对象
扩展运算符调用的是数据结构的 Iterator
接口,因此只要具有 Iterator
接口的对象,都可以使用扩展运算符。比如字符串,Map
和Set
,Generator
函数返回的遍历器对象。
[..."hello"]; |
8.3 新增方法
静态方法 1:Array.from()
方法用于将两类对象转为真正的数组,类似数组的对象和可遍历的对象。(包括 ES6
新增的数据结构 Set
和 Map
,和部署了 Iterator
接口的数据结构)
let arrayLike = { |
Array.from()
还可以接受一个函数作为第二个参数,作用类似于数组的map()
方法,用来对每个元素进行处理,将处理后的值放入返回的数组。
Array.from(arrayLike, (x) => x * x); |
静态方法 2:Array.of()
方法用于将一组值,转换为数组。它可以替代Array()
或new Array()
,并且不存在由于参数不同而导致的重载。
Array.of(3, 11, 8); // [3,11,8] |
实例方法 1:find()
方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true
的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined
。
findIndex()
方法的用法与find()
方法类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1
。find()
和findIndex()
都是从数组的0
号位,依次向后检查。
[1, 4, -5, 10] |
与之相反,findLast()
和findLastIndex()
是从数组的最后一个成员开始,依次向前检查,其他都保持不变。
const array = [{ value: 1 }, { value: 2 }, { value: 3 }, { value: 4 }]; |
实例方法 2:fill()
方法使用给定值,填充一个数组。它还可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置。
["a", "b", "c"].fill(7); |
实例方法 3:ES6
提供三个新的方法用于遍历数组。keys()
是对键名的遍历,values()
是对键值的遍历,entries()
是对键值对的遍历。它们都可以用for...of
循环进行遍历。
for (let index of ["a", "b"].keys()) { |
实例方法 4:includes()
方法返回一个布尔值,表示某个数组是否包含给定的值。该方法的第二个参数表示搜索的起始位置,默认为0
。如果第二个参数为负数,则表示倒数的位置。
[1, 2, 3] |
实例方法 5:数组的成员有时还是数组,flat()
方法用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数据没有影响。
[1, 2, [3, 4]].flat(); |
flat()
方法默认只会“拉平”一层,如果想要“拉平”多层的嵌套数组,可以指定flat()
方法的参数,表示想要拉平的层数,默认为1
。
[1, 2, [3, [4, 5]]] |
flatMap()
方法对原数组的每个成员执行一个函数(相当于执行map()
), 然后对返回值组成的数组执行flat()
方法。注意,flatMap()
只能展开一层数组。该方法返回一个新数组,不改变原数组。
// 相当于 [[2, 4], [3, 6], [4, 8]].flat() |
实例方法 6:ES5
不支持数组的负索引,如果要引用数组的最后一个成员,不能写成arr[-1]
,只能使用arr[arr.length - 1]
。ES6
为数组实例增加了at()
方法,接受一个整数作为参数,返回对应位置的成员,并支持负索引。
const arr = [5, 12, 8, 130, 44]; |
9. 对象的扩展
本章内容复杂,故先跳过一部分。
9.1 属性的简洁表示法
ES6
允许在大括号里面,直接写入变量和函数,作为对象的属性和方法。
let birth = "2000/01/01"; |
9.2 super 关键字
众所周知,this
关键字总是指向函数所在的当前对象,ES6
又新增了另一个类似的关键字super
,指向当前对象的原型对象。
const proto = { |
上面代码中,对象obj.foo()
方法之中,通过super.foo
引用了原型对象proto
的foo
方法。但是绑定的this
却还是当前对象obj
,因此输出的就是world
。
注意,super
关键字表示原型对象时,只能用在对象的方法之中,用在其他地方都会报错。
/ 报错 |
9.3 对象的扩展运算符
(1)解构赋值
对象的解构赋值用于从一个对象取值,将目标对象所有可遍历的、但尚未被读取的属性,分配到指定的对象上面。(注意,解构赋值必须是最后一个参数,且等号右边不能是undefined
或null
)
let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 }; |
解构赋值的拷贝是浅拷贝,即如果一个键的值是复合类型的值(如数组、对象、函数),那么解构赋值拷贝的是这个值的引用,而不是这个值的副本。
let obj = { a: { b: 1 } }; |
上面代码中,x
是解构赋值所在的对象,拷贝了对象obj
的a
属性。a
属性引用了一个对象,修改这个对象的值,会影响到解构赋值对它的引用。
解构赋值的一个用处,是扩展某个函数的参数,引入其他操作。
function baseFunction({ a, b }) { |
上面代码中,原始函数baseFunction
接受a
和b
作为参数,函数wrapperFunction
在baseFunction
的基础上进行了扩展,能够接受多余的参数,并且保留原始函数的行为。
(2)扩展运算符
对象的扩展运算符 ...
用于取出参数对象的所有可遍历属性,拷贝到当前对象之中。(扩展运算符不需要是最后一个参数)
let z = { a: 3, b: 4 }; |
扩展运算符还可以用于合并两个对象。
let ab = { ...a, ...b }; |
如果用户自定义的属性,放在扩展运算符后面,则扩展运算符内部的同名属性会被覆盖掉。
let aWithOverrides = { ...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"); |
静态方法 2:Object.assign()
方法用于对象的合并,将源对象source
的所有可枚举属性,复制到目标对象target
。它的第一个参数是目标对象,后面的参数都是源对象。
const target = { a: 1 }; |
Object.assign()
有以下几个使用注意点。
(1)浅拷贝
Object.assign()
方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用(指针)。
const obj1 = {a: {b: 1}}; |
上面代码中,源对象obj1
的a
属性的值是一个对象,Object.assign()
拷贝得到的是这个对象的引用。这个对象的任何变化,都会反映到目标对象上面。
(2)同名属性的替换
对于嵌套的对象,一旦遇到同名属性,Object.assign()
的处理方法是替换,而不是添加。这通常不是我们想要的,需要特别小心。
const target = { a: { b: "c", d: "e" } }; |
静态方法 3:对于普通对象,我们可以用下面的方法对对象进行遍历。
Object.keys()
返回一个数组,成员是参数对象自身的所有可遍历属性的键名。Object.values()
返回一个数组,成员是参数对象自身的所有可遍历属性的键值。Object.entries()
返回一个数组,成员是参数对象自身的所有可遍历属性的键值对数组。
const obj = { foo: "bar", baz: 42 }; |
11. 运算符的扩展
11.1 链判断运算符
当我们打算读取对象内部的某个属性时,往往需要判断一下属性的上层对象是否存在。
// 错误的写法 |
上面代码中,firstName
属性在对象的第四层,所以需要判断四次,每一层是否有值。这样的层层判断非常麻烦,因此 ES6
引入了“链判断运算符” ?.
,用来简化上面的写法。
const firstName = message?.body?.user?.firstName || "default"; |
上面代码使用了?.
运算符,直接在链式调用的时候判断,左侧的对象是否为null
或undefined
。如果是的,就不再往下运算,而是返回undefined
。
下面是 ?.
运算符常见形式,以及不使用该运算符时的等价形式。
a?.b; |
11.2 Null 判断运算符
读取对象属性的时候,如果某个属性的值是null
或undefined
,有时候需要为它们指定默认值。常见做法是通过||
运算符指定默认值。
const headerText = response.settings.headerText || "Hello, world!"; |
但是属性的值如果为空字符串或false
或0
,默认值也会生效。为此,ES6
引入了一个新的 Null
判断运算符??
。它的行为类似||
,但是只有运算符左侧的值为null
或undefined
时,才会返回右侧的值。
const animationDuration = response.settings?.animationDuration ?? 300; |
上面代码中,如果response.settings
是null
或undefined
,或者response.settings.animationDuration
是null
或undefined
,就会返回默认值300
。
12. Symbol 数据类型
本章内容复杂,故先跳过一部分。
12.1 Symbol 概述
ES6
引入了一种新的原始数据类型 Symbol
,表示独一无二的值。
Symbol
通过 Symbol()
函数生成,可以用于为对象添加新的方法,且不存在属性名冲突。
let s = Symbol(); // Symbol() 函数前不能使用 new 命令 |
ES6
提供了一个 Symbol
值的实例属性description
,直接返回 Symbol
值的描述。
const sym = Symbol("foo"); // 字符串作为参数 |
因为 Symbol
值都是不相等的(就算是相同描述,除非使用 Symbol.for
),使用 Symbol
值作为标识符,用于对象的属性名,就能保证不会出现同名的属性。
let mySymbol = Symbol(); |
13. Set 和 Map 数据结构
本章跳过 WeakSet
和 WeakMap
部分。
13.1 Set 基本用法
ES6
提供了新的数据结构 Set
。它类似于数组,但是成员的值都是唯一的,没有重复的值。它可以接受一个数组,或者具有 iterable
接口的其他数据结构,作为参数初始化。
const s = new Set(); |
下面介绍 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(); |
Set
结构的实例默认可遍历, 可以直接用for...of
循环遍历 Set
。也可以用forEach
方法,用于对每个成员执行某种操作,没有返回值。
let set = new Set(["red", "green", "blue"]); |
上面代码中forEach
方法的参数是一个处理函数。但是因为 Set
结构的键名就是键值,因此第一个参数与第二个参数的值永远都是一样的。
13.2 Set 遍历的应用
扩展运算符和 Set
结构相结合,可以去除数组的重复成员。
// 去除数组的重复成员 |
数组的map
和filter
方法也可以间接用于 Set
。
let set = new Set([1, 2, 3]); |
13.3 Map 基本用法
Map
数据结构类似于对象,是键值对的集合,但是各种类型的值(包括对象)都可以当作键。
const map = new Map([ |
下面介绍 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
结构原生提供三个遍历器生成函数和一个遍历方法:
Map.prototype.keys()
:返回键名的遍历器。Map.prototype.values()
:返回键值的遍历器。Map.prototype.entries()
:返回所有成员的遍历器。Map.prototype.forEach()
:遍历Map
的所有成员。
const map = new Map([ |
需要特别注意的是,Map
的遍历顺序就是插入顺序。
13.4 Map 与其他数据结构转换
(1)Map 转为数组
Map
转为数组最方便的方法,就是使用扩展运算符...
。
const myMap = new Map().set(true, 7).set({ foo: 3 }, ["abc"]); |
结合数组的map
方法、filter
方法,就可以实现 Map
的遍历和过滤。
const map0 = new Map().set(1, "a").set(2, "b").set(3, "c"); |
(2)数组 转为 Map
将数组传入 Map
构造函数,就可以转为 Map
。
new Map([ |
(3)Map 转为对象
如果所有 Map
的键都是字符串,它可以无损地转为对象。如果有非字符串的键名,那么这个键名会被转成字符串,再作为对象的键名。
function strMapToObj(strMap) { |
(4)对象转为 Map
对象转为 Map
可以通过 Object.entries()
。
let obj = { a: 1, b: 2 }; |
(5)Map 转为 JSON
Map
转为 JSON
要区分两种情况。
一种情况是,Map
的键名都是字符串,这时可以选择转为对象 JSON
。
function strMapToJson(strMap) { |
另一种情况是,Map
的键名有非字符串,这时可以选择转为数组 JSON
。
function mapToArrayJson(map) { |
(6)JSON 转为 Map
JSON
转为 Map
,正常情况下,所有键名都是字符串。
function objToStrMap(obj) { |
如果整个 JSON
就是一个数组,且每个数组成员本身,又是一个有两个成员的数组。这时,它可以直接转为 Map
。(类似 Map
转为数组 JSON
的逆操作)
function jsonToMap(jsonStr) { |
14. Proxy
本章内容复杂,故先跳过一部分。
14.1 Proxy 概述
Proxy
可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。
let obj = new Proxy( |
上面代码对一个空对象架设了一层拦截,重定义了属性的读取get
和设置set
行为。对设置了拦截行为的对象obj
读写它的属性,就会得到下面的结果。
obj.count = 1; |
这里可以看出,Proxy
实际上重载overload
了点运算符,即用自己的定义覆盖了语言的原始定义。(类似C++
的重载)
ES6
原生提供 Proxy
构造函数,用来生成 Proxy
实例。target
参数表示所要拦截的目标对象,handler
参数也是一个对象,用来定制拦截行为。
let proxy = new Proxy(target, handler); |
下面是另一个拦截读取属性行为的例子。
let proxy = new Proxy( |
上面代码中,作为构造函数,Proxy
接受两个参数。第一个参数是所要代理的目标对象。第二个参数是一个配置对象,对于每一个被代理的操作,需要提供一个对应的处理函数,该函数将拦截对应的操作。
注意,要使得Proxy
起作用,必须针对Proxy
实例进行操作,而不是针对目标对象进行操作。
14.2 Proxy 的实例方法
Proxy
支持的拦截操作一共 13
种。(剩下的可以在这里找到)
实例方法 1:get()
方法用于拦截某个属性的读取操作,可以接受三个参数,依次为目标对象、属性名和 proxy
实例本身。
let person = { |
实例方法 2: set()
方法用来拦截某个属性的赋值操作,可以接受四个参数,依次为目标对象、属性名、属性值和 Proxy
实例本身。
let validator = { |
15. Reflect
15.1 Reflect 概述
Reflect
对象与Proxy
对象一样,也是 ES6
为了操作对象而提供的新 API
。Reflect
对象的设计目的有这样几个。
(1)将Object
对象的一些属于语言内部的方法(比如Object.defineProperty
),放到Reflect
对象上。也就是说,未来可以从Reflect
对象上拿到语言内部的方法。
(2)修改某些Object
方法的返回结果,让其变得更合理。
// 老写法 |
(3)让Object
操作都变成函数行为。比如name in obj
和delete obj[name]
,而Reflect.has(obj, name)
和Reflect.deleteProperty(obj, name)
让它们变成了函数行为。
// 老写法 |
(4)让Reflect
对象的方法与Proxy
对象的方法对应,只要是Proxy
对象的方法,就能在Reflect
对象上找到对应的方法。这就让Proxy
对象可以方便地调用对应的Reflect
方法,完成默认行为,作为修改行为的基础。
let loggedObj = new Proxy(obj, { |
上面代码中,每一个Proxy
对象的拦截操作get
、delete
、has
,内部都调用对应的Reflect
方法,保证原生行为能够正常执行。
简单来说,有了Reflect
对象以后,很多操作会更易读。
// 老写法 |
15.2 Reflect 静态方法
Reflect
对象也有 13
个静态方法。(剩下的可以在这里找到)
静态方法 1:Reflect.get
方法查找并返回target
对象的name
属性。
let myObject = { |
静态方法 2:Reflect.set
方法设置target
对象的name
属性等于value
。
let myObject = { |