ECMAScript 6 (一)
一、ECMAScript 6 简介
ECMAScript和JavaScript
1996年11月,JavaScript的创造者Netscape公司,决定将JavaScript提交给国际标准化组织ECMA,希望这种语言能够成为国际标准。次年,ECMA发布262号标准文件(ECMA-262)的第一版,规定了浏览器脚本语言的标准,并将这种语言称为ECMAScript,这个版本就是1.0版。该标准从一开始就是针对JavaScript语言制定的。
因此,ECMAScript和JavaScript的关系是,前者是后者的规范,后者是前者的一种实现。由于商标权的问题,欧洲计算机协会制定的语言标准不能叫做JS,只能叫ES。
ECMAScript 6.0(以下简称 ES6)是 JavaScript 语言的下一代标准,已经在2015年6月正式发布了。目前,ECMAScript已经发展到ECMAScript2020,但习惯上我们将ES6及其以后的部分统称为ES6。它的目标,是使得 JavaScript 语言可以用来编写复杂的大型应用程序,成为企业级开发语言。而企业级开发语言就是:适合模块化开发,拥有良好的依赖管理。近几年几乎所有使用JavaScript这门语言开发的项目,都在使用ES6的新特性来开发。
官网:http://www.ecma-international.org/ecma-262/6.0/
二、ES6的新特性
1. 新的声明方式
let:ES6 新增了let命令,用来声明变量。
1) let 声明的全局变量不是全局window的属性
这意味着,不可以通过 window. 变量名的方式访问这些变量,而 var 声明的全局变量是 window 的属性,可以通过 window. 变量名的方式访问。
var a = 50;
console.log(window.a); // 50
let a = 50;
console.log(window.a) ;// undefined
2) 用let定义变量不允许重复声明
使用 var 可以重复定义,使用 let 却不可以。
var a = 5;
var a = 6;
console.log(a) ;// 5
如果是 let ,则会报错
let a = 5;
let a = 6;
// VM131:1 Uncaught SyntaxError: Identifier 'a' has already been declared
// at <anonymous>:1:1
3) let声明的变量不存在变量提升
function fun() {
console.log(a);
var a = 5;
}
fun() //undefined
上述代码中, a
的调用在声明之前,所以它的值是 undefined,而不是 Uncaught ReferenceError。实际上因为 var 会导致变量提升,上述代码和下面的代码等同:
function fun() {
var a;
console.log(a);
a = 5;
}
fun() ;//undefined
而对于 let 而言,变量的调用是不能先于声明的,看如下代码:
function fun() {
console.log(a);
let a = 5;
}
fun();//Uncaught ReferenceError: can't access lexical declaration 'a' before initialization
在这个代码中, a
的调用是在声明之前,因为 let 没有发生变量提升,所有读取 a 的时候,并没有找到,而在调用之后才找到 let 对 a
的定义,所以会报错。
4) let声明的变量具有暂时性死区
只要块级作用域内存在 let
命令,它所声明的变量就绑定在了这个区域,不再受外部的影响。
var a = 5;
if (true) {
a = 6;
let a;//在块级作用域内,let命令不能再声明前就使用这些变量
}
// Uncaught ReferenceError: Cannot access 'a' before initialization
上面代码中,存在全局变量 a
,但是块级作用域内 let
又声明了一个局部变量 a
,导致后者绑定这个块级作用域,所以在let声明变量前,对 a
赋值会报错。
ES6 明确规定,如果区块中存在 let
和 const
命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。
总之,在代码块内,使用 let
命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”
5) let 声明的变量拥有块级作用域
var s = 'hello';
for (var i = 0; i < s.length; i++) {
console.log(s[i]);
}
console.log(i); // 5
上面代码中,变量i
只用来控制循环,但是循环结束后,它并没有消失,泄露成了全局变量。
let
实际上为 JavaScript 新增了块级作用域。
function f1() {
let n = 5;
if (true) {
let n = 10;
}
console.log(n); // 5
}
上面的函数有两个代码块,都声明了变量n
,运行后输出 5。这表示外层代码块不受内层代码块的影响。如果两次都使用var
定义变量n
,最后输出的值才是 10。
const:ES6 新增了const
命令,用来声明一个只读的常量。
而const
除了具有 let
的上述的特性之外,还有一些其他特点:
(1)在用 const
定义变量后,就不能修改它了,对变量的修改会抛出异常。
const a = 'hello';
console.log(a);//hello
a = 'es6';
console.log(a);
// Uncaught TypeError: invalid assignment to const 'a'
(2)const声明的常量必须进行初始化,否则会报错。
const a;
a = 0.5;
// Uncaught SyntaxError: missing = in const declaration
(3)const定义对象和数组的值是可以改变的。
const obj = {
name: '张三',
age: 24
}
obj.sex = '男';
obj.age = 24;
console.log(obj); // {name: "张三", age: 24, sex: "男"}
const arr = ['es6','es7','es8'];
console.log(arr); //Array(3) [ "es6", "es7", "es8" ]
arr[0] = 'es2015';
console.log(arr); //Array(3) [ "es2015", "es7", "es8" ]
const
实际上保证的并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。
在js引擎中对变量的存储主要有两种位置,堆内存和栈内存。
和java中对内存的处理类似,栈内存主要用于存储各种基本类型的变量,包括Boolean、Number、String、Undefined、Null,以及对象变量的指针,这时候栈内存给人的感觉就像一个线性排列的空间,每个小单元大小基本相等。
而堆内存主要负责像对象Object这种变量类型的存储,如下图。栈内存中的变量一般都是已知大小或者有范围上限的,算作一种简单存储。而堆内存存储的对象类型数据对于大小这方面,一般都是未知的
对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const
只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。
如何让对象或者数组这种引用数据类型也不被改变呢?
Object.freeze(obj);
注意:Object.freeze()
只是浅层冻结,只会对最近一层的对象进行冻结,并不会对深层对象冻结。
其他
使用const声明的常量:
- 不属于顶层对象window
- 不允许重复声明
- 不存在变量提升
- 暂时性死区
- 块级作用域
2. 解构赋值
在 ES6 中新增了变量赋值的方式:解构赋值。ES6允许按照一定模式,从数组和对象中提取值,对变量进行赋值。在解构赋值里用的最多的就是 Object 和 Array ,我们可以分别来看下两者的解构赋值是如何操作的。
1)数组的解构赋值
1.数组的解构赋值
解构赋值遵循一个原则,只要左右两边的模式相同,就可以进行合法赋值。解构过程中,应该把每个解构的部分对应在一起,层层解构。下面代码表示,可以从数组中提取值,按照对应位置,对变量赋值。
let [a, b, c] = [1, 2, 3] // a = 1 b = 2 c = 3
let [a, [[b], c]] = [1, [[2], 3]]; // a = 1 b = 2 c = 3
let [a, , b] = [1, 2, 3]; // a = 1 ,b = 3
如果数组的内容少于变量的个数,并不会报错,但没有分配到内容的变量值为undefined。
let [firstName, lastName] = ['John']; //firstName:John,lastName:undefined
2.对象的解构赋值
基本的语法如下:
let {var1, var2} = {var1:…, var2…}
解构赋值可以把对象里面的属性分别拿出来,而无需通过调用属性的方式赋值给指定变量。具体做法是在赋值的左侧声明一个和Object结构等同的模板,然后把所需属性的value指定为新的变量即可。
let options = {
title: "Menu",
width: 100,
height: 50
}
let {title, width, height} = options
console.log(title) // Menu
console.log(width) // 100
console.log(height) // 50
TIP
在这个结构赋值的过程中,左侧的“模板”结构要与右侧的 Object 一致,但属性的顺序无需一致。
当然,这个赋值的过程中也可以指定默认值:
let options = {
title: "Menu"
}
let {width = 100, height = 50, title} = options
console.log(title) // Menu
console.log(width) // 100
console.log(height) // 50
如果Array或Object嵌套了多层,那只要被赋值的结构和右侧赋值的元素一致即可。
const options = {
size: {
width: 100,
height: 200
},
items: ["Copy", "Cut"],
}
const {
size: {
width,
height,
},
items: [item1, item2],
title = 'Menu', // 指定默认值
} = options
//title = 'Menu' width = 100 heigth = 200 item1 = 'Copy' item2 = 'Cut'
3.字符串的解构赋值
字符串也可以解构赋值。此时,字符串被转换成了一个类似数组的对象。
const [a, b, c] = 'ES6';
a // "E"
b // "S"
c // "6"
类似数组的对象都有一个length
属性,因此还可以对这个属性解构赋值。
let {length : len} = 'ES6';
len // 3
4.函数参数的解构赋值
函数的参数也可以使用解构赋值。
function add([x, y]){
return x + y;
}
add([1, 4]); // 5
上面代码中,函数add
的参数表面上是一个数组,但在传入参数的那一刻,数组参数就被解构成变量x
和y
。对于函数内部的代码来说,它们能感受到的参数就是x
和y
。
下面是另一个例子。
[[1, 2], [3, 4]].map(([a, b]) => a + b);
// [ 3, 7 ]
函数参数的解构也可以使用默认值。
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
的参数是一个对象,通过对这个对象进行解构,得到变量x
和y
的值。如果解构失败,x
和y
等于默认值。
注意,下面的写法会得到不一样的结果。
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
的参数指定默认值,而不是为变量x
和y
指定默认值,所以会得到与前一种写法不同的结果。
undefined
就会触发函数参数的默认值。
[1, undefined, 3].map((x = 'yes') => x);
// [ 1, 'yes', 3 ]
3. 数组
Spread Operator 和 Rest Parameter 是形似(...
)但相反意义的操作符:
- rest参数(Rest Parameter):用来解决函数参数不确定的场景,和一个变量名搭配使用,生成一个数组,用于获取函数多余的参数。
- 扩展运算符(Spread Operato):它好比rest参数的逆运算,将一个数组转为用逗号分隔的参数序列。
扩展运算符
扩展运算符(spread)是三个点(...
),将一个数组转为用逗号分隔的参数序列。
console.log(...[1, 2, 3])
// 1 2 3
console.log(1, ...[2, 3, 4], 5)
// 1 2 3 4 5
扩展运算符的应用
(1)复制数组
数组是复合的数据类型,直接复制的话,只是复制了指向底层数据结构的指针,而不是克隆一个全新的数组。
const a1 = [1, 2];
const a2 = a1;
a2[0] = 2;
a1 // [2, 2]
上面代码中,a2
并不是a1
的克隆,而是指向同一份数据的另一个指针。修改a2
,会直接导致a1
的变化。
扩展运算符提供了复制数组的简便写法。
const a1 = [1, 2];
const a2 = [...a1];
a2[0] = 3;
console.log(a1,a2);//[ 1, 2 ],[ 3, 2 ]
(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' ]
(3)与解构赋值结合
扩展运算符可以与解构赋值结合起来,用于生成数组。
const [first, ...rest] = [1, 2, 3, 4, 5];
first // 1
rest // [2, 3, 4, 5]
const [first, ...rest] = [];
first // undefined
rest // []
const [first, ...rest] = ["foo"];
first // "foo"
rest // []
如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错。
const [...butLast, last] = [1, 2, 3, 4, 5];
// 报错
const [first, ...middle, last] = [1, 2, 3, 4, 5];
// 报错
(4)字符串
扩展运算符还可以将字符串转为真正的数组。
[...'hello']
// [ "h", "e", "l", "l", "o" ]
数组遍历方式for…of
for…of 遍历的是一切可遍历的元素(数组、对象、集合)等。
for (variable of iterable) {
}
for (let val of [1, 2, 3]) {
console.log(val);
}
// 1,2,3
上述代码中轻松实现了数组的遍历,for…of是支持 break、continue、return的,所以在功能上非常贴近原生的 for。
Array.from()方法
Array.from
方法用于将两类对象转为真正的数组:类似数组的对象(ArrayLike Object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。
下面是一个类似数组的对象,Array.from
将它转为真正的数组。
ArrayLike Object这种数据结构使用数字作为属性名,并且具有长度属性
length
。伪数组具备两个特征,1. 按索引方式储存数据 2. 具有length属性;
let arrayLike = {
'0': 'a',
'1': 'b',
'2': 'c',
length: 3
};
let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']
只要是部署了 Iterato接口的数据结构,Array.from
都能将其转为数组。
Iterator(迭代器)是一个接口,它的作用就是遍历容器的所有元素。
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
会返回一个一模一样的新数组。
Array.from([1, 2, 3])
// [1, 2, 3]
Array.of()方法
Array.of() 方法创建一个具有可变数量参数的新数组实例,不考虑参数的数量或类型。
参数 | 含义 | 必选 |
---|---|---|
elementN | 任意个参数,将按顺序成为返回数组中的元素 | Y |
Array.of(7); // [7]
Array.of(1, 2, 3); // [1, 2, 3]
Array(7); // [ , , , , , , ]
Array(1, 2, 3); // [1, 2, 3]
Array.of() 和 Array 构造函数之间的区别在于处理整数参数:Array.of(7) 创建一个具有单个元素 7 的数组,而 Array(7) 创建一个长度为7的空数组(注意:这是指一个有7个空位(empty)的数组,而不是由7个undefined组成的数组)。
数组实例的 fill()
参数 | 含义 | 必选 |
---|---|---|
value | 用来填充数组元素的值 | Y |
start | 起始索引,默认值为0 | N |
end | 终止索引,默认值为 this.length | N |
fill
方法用于空数组的初始化非常方便。数组中已有的元素,会被全部抹去。
['a', 'b', 'c'].fill(7)
// [7, 7, 7]
new Array(3).fill(7)
// [7, 7, 7]
fill() 方法用一个固定值填充一个数组中从起始索引到终止索引内的全部元素。不包括终止索引。
let array = [1, 2, 3, 4]
array.fill(0, 1, 2)
// [1,0,3,4]
这个操作是将 array 数组的第二个元素(索引为1)到第三个元素(索引为2)内的数填充为 0,不包括第三个元素,所以结果是 [1, 0, 3, 4]。
数组实例的 find() 和 findIndex()
find() 方法返回数组中满足提供的测试函数的第一个元素的值,否则返回 undefined。
参数 | 含义 | 必选 |
---|---|---|
callback | 在数组每一项上执行的函数 | Y |
let array = [5, 12, 8, 130, 44];
let found = array.find(function(element) {
return element > 10;
});
console.log(found);
// 12
findIndex()方法返回数组中满足提供的测试函数的第一个元素的索引。否则返回-1。其实这个和 find() 是成对的,不同的是它返回的是索引而不是值。
参数 | 含义 | 必选 |
---|---|---|
callback | 在数组每一项上执行的函数 | Y |
let array = [5, 12, 8, 130, 44];
let found = array.findIndex(function(element) {
return element > 10;
});
console.log(found);
// 1
数组实例的 copyWithin()
在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。也就是说,使用这个方法,会修改当前数组。
语法:arr.copyWithin(target, start = 0, end = this.length)
参数 | 含义 | 必选 |
---|---|---|
target | 从该位置开始替换数据。如果为负值,表示倒数 | Y |
start | 从该位置开始读取数据,默认为 0。如果为负值,表示从末尾开始计算 | N |
end | 到该位置前停止读取数据,默认等于数组长度。如果为负值,表示从末尾开始计算 | N |
let arr = [1, 2, 3, 4, 5]
console.log(arr.copyWithin(1, 3))
// [1, 4, 5, 4, 5]
4. 函数
函数参数的扩展
ES6 之前,不能直接为函数的参数指定默认值,而ES6 允许为函数的参数设置默认值,即直接写在参数定义的后面。
function f(x, y = 'World') {
console.log(x, y);
}
参数变量是默认f声明的,所以不能用let
或const
再次声明。
function foo(x = 5) {
let x = 1; // error
const x = 2; // error
}
使用参数默认值时,函数不能有同名参数。
// 不报错
function foo(x, x, y) {
// ...
}
// 报错
function foo(x, x, y = 1) {
// ...
}
Rest 参数
ES6 引入 rest 参数(形式为...变量名
),用于获取函数的多余参数。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。
在写函数的时候,部分情况我们不是很确定参数有多少个,比如求和运算:
function add(...values) {
let sum = 0;
for (var val of values) {
sum += val;
}
return sum;
}
add(2, 5, 3) // 10
函数的 length 属性
指定了默认值以后,函数的length
属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length
属性将失真。
(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2
这是因为length
属性的含义是,该函数预期传入的参数个数。某个参数指定默认值以后,预期传入的参数个数就不包括这个参数了。
同理,后文的 rest 参数也不会计入length
属性。
(function(...args) {}).length // 0
如果设置了默认值的参数不是尾参数,那么length
属性也不再计入后面的参数了。
(function (a = 0, b, c) {}).length // 0
(function (a, b = 1, c) {}).length // 1
name属性
函数的name属性,返回该函数的函数名。
function foo() {}
foo.name // "foo"
箭头函数
箭头函数可以说是 ES6 很大的福利了,不管你是函数式爱好者还是面向对象开发者,函数是必须要用到的东西。
之前声明函数需要使用 function,如下:
function hello() {
console.log('say hello')
}
// 或
let hello = function() {
console.log('say hello')
}
箭头函数提供了一种更加简洁的函数书写方式。
- 基本语法
参数 => 函数体
let hello = () => {
console.log('say hello')
}
- 参数的处理
let hello = (name) => {
console.log('say hello', name)
}
// 或者
let hello = name => {
console.log('say hello', name)
}
TIP
如果只有一个参数,可以省略括号,如果大于一个参数一定要记得带括号
函数的声明和参数写的很清楚了,那么对于返回值有什么要注意的地方呢?
-
如果返回值是表达式
如果返回值是表达式可以省略 return 和 {}
let pow = x => x * x
- 关于this的处理
普通函数和箭头函数对 this 的处理方式是截然不同的。箭头函数体中的 this 对象,是定义函数时的对象,而不是使用函数时的对象。
let p= {
data:{
flag: true
},
init: function(){
console.log(this.data.flag)
}
}
p.init();//true
这是用普通函数的写法,init()在被调用之后,this 指向的是调用 init方法的对象,即p对象,所以 this.data.flag === p.data.flag 。
let p= {
data:{
flag: true
},
init: () => {
console.log(this.data.flag)
}
}
p.init(); // this.data is undefined
基本上对于参数的简单操作都可以使用箭头函数完成,这里可以告诉你不要试图滥用。
- 不要在最外层定义箭头函数,因为在函数内部操作this会很容易污染全局作用域。最起码在箭头函数外部包一层普通函数,将this控制在可见的范围内;
- 如开头所述,箭头函数最吸引人的地方是简洁。在有多层函数嵌套的情况下,箭头函数的简洁性并没有很大的提升,反而影响了函数的作用范围的识别度,这种情况不建议使用箭头函数。
5. Set 数据结构
在 JavaScript 里通常使用 Array 或 Object 来存储数据。但是在频繁操作数据的过程中查找或者统计并需要手动来实现,并不能简单的直接使用。 比如如何保证 Array 是去重的,如何统计 Object 的数据总数等,必须自己去手动实现类似的需求,不是很方便。 在 ES6 中为了解决上述痛点,新增了数据结构 Set 和 Map,它们分别对应传统数据结构的“集合”和“字典”。首先,我们先来学习下 Set 数据结构。
ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。
基本语法
生成 Set 实例
let s = new Set();
Set函数可以接受一个数组(或类似数组的对象)作为参数,用来初始化。
let s = new Set([1, 2, 3, 4]);
for (let i of s) {
console.log(i);
}
添加数据
add(value)
:添加某个值,返回Set结构本身。
s.add('hello')
s.add('goodbye')
或者
s.add('hello').add('goodbye')
注意
Set 数据结构不允许数据重复,所以添加重复的数据是无效的
删除数据
删除数据分两种,一种是删除指定的数据,一种是删除全部数据。
delete(value)
:删除某个值,返回一个布尔值,表示删除是否成功。
clear()
:清除所有成员,没有返回值。
// 删除指定数据
s.delete('hello') // true
// 删除全部数据
s.clear()
统计数据
Set 可以快速进行统计数据,如数据是否存在、数据的总数。
has(value)
:返回一个布尔值,表示该值是否为Set
的成员。
// 判断是否包含数据项,返回 true 或 false
s.has('hello') // true
// 计算数据项总数
s.size // 2
数组去重
let arr = [1, 2, 3, 4, 2, 3]
let s = new Set(arr)
console.log(s)
两个对象总是不相等的。
let set = new Set();
set.add({});
set.size // 1
set.add({});
set.size // 2
上面代码表示,两个空对象被视为两个不同的值。
合并去重
Array.from
方法可以将Set结构转为数组。
let arr1 = [1, 2, 3, 4]
let arr2 = [2, 3, 4, 5, 6]
let s = new Set([...arr1, ...arr2])
console.log(s)
console.log(Array.from(s))
交集
let s1 = new Set(arr1)
let s2 = new Set(arr2)
let result = new Set(arr1.filter(item => s2.has(item)))
console.log(Array.from(result))
差集
let arr3 = new Set(arr1.filter(item => !s2.has(item)))
let arr4 = new Set(arr2.filter(item => !s1.has(item)))
console.log(arr3)
console.log(arr4)
console.log([...arr3, ...arr4])
遍历操作
keys()
:返回键名的遍历器values()
:返回键值的遍历器entries()
:返回键值对的遍历器for...of
:可以直接遍历每个成员forEach()
:使用回调函数遍历每个成员
(1)keys()
,values()
,entries()
,for...of
由于Set结构没有键名,只有键值(或者说键名和键值是同一个值),所以key
方法和value
方法的行为完全一致。
let set = new Set(['red', 'green', 'blue']);
for (let item of set.keys()) {
console.log(item);
}
// red
// green
// blue
for (let item of set.values()) {
console.log(item);
}
// red
// green
// blue
for (let item of set.entries()) {
console.log(item);
}
// ["red", "red"]
// ["green", "green"]
// ["blue", "blue"]
上面代码中,entries
方法返回的同时包括键名和键值,所以每次输出一个数组,它的两个成员完全相等。
直接用for...of
循环遍历Set。
let set = new Set(['red', 'green', 'blue']);
for (let x of set) {
console.log(x);
}
// red
// green
// blue
(2)forEach()
Set结构的实例的forEach
方法,用于对每个成员执行某种操作,没有返回值。
let set = new Set([1, 2, 3]);
set.forEach(item => {
console.log(item);
})
上面代码说明,forEach
方法的参数就是一个处理函数。该函数的参数依次为键值、键名、集合本身(上例省略了该参数)。这里需要注意,Set 结构的键名就是键值(两者是同一个值),因此第一个参数与第二个参数的值永远都是一样的。
WeakSet
WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别。
首先,WeakSet 的成员只能是对象,而不能是其他类型的值。
const ws = new WeakSet()
ws.add(555)
// TypeError: WeakSet value must be an object
上面代码试图向WeakSet添加一个数值,结果报错,因为WeakSet只能放置对象。
let ws2 = new WeakSet();
const obj1 = {
name: '张三',
}
const obj2 = {
age: 25,
}
ws2.add(obj1);
ws2.add(obj2);
console.log(ws2);
console.log(ws2.has(obj2));
其次,WeakSet 没有size属性,没有办法遍历它的成员。
ws2.size // undefined
ws2.forEach(function(item){ console.log(item)})
// TypeError: ws2.forEach is not a function
WeakSet不能遍历,是因为成员都是弱引用,随时可能消失,遍历机制无法保证成员的存在,很可能刚刚遍历结束,成员就取不到了。
6. Map数据结构
ES6 提供了 Map 数据结构。JavaScript的对象,本质上是键值对的集合,但是传统上只能用字符串作为建,这有了很大的限制。但是Map中“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。
也就是说,Object结构提供了“字符串-值”的对应,Map结构提供了“值-值”的对应。
基本语法
生成Map实例
const map = new Map();
Map 作为构造函数也可以接受一个数组作为参数。该数组的成员是一个个表示键值对的数组(每个成员都是一个双元素的数组的数据结构)
const map = new Map([
['name', '张三'],
['age', '25']
]);
上面代码在新建 Map 实例时,就指定了两个键name
和age
。
添加数据
set
方法返回的是当前的Map
对象。
const map = new Map();
let keyObj = {};
let keyFunc = function() {};
let keyString = 'a string';
// 添加键
map.set(keyString, "和键'a string'关联的值");
map.set(keyObj, '和键keyObj关联的值');
map.set(keyFunc, '和键keyFunc关联的值');
如果对同一个键多次赋值,后面的值将覆盖前面的值。
let map = new Map();
map.set(1, 'aaa');
map.set(1, 'bbb');
map.get(1) // "bbb"
上面代码对键1
连续赋值两次,后一次的值覆盖前一次的值。
删除数据
delete
方法删除某个键,返回true
。如果删除失败,返回false
。
clear
方法清除所有成员,没有返回值。
// 删除指定的数据
map.delete(keyObj)
// 删除所有数据
map.clear()
统计数据
size
属性返回 Map 结构的成员总数。
has
方法返回一个布尔值,表示某个键是否在当前 Map 对象之中。
// 统计所有 key-value 的总数
console.log(map.size) //2
// 判断是否有 key-value
console.log(map.has(keyObj)) // true
查询数据
get
方法读取key
对应的键值,如果找不到key
,返回undefined
。
console.log(map.get(keyObj)); // 和键keyObj关联的值
如果读取一个未知的键,则返回undefined
。
const map = new Map();
console.log(map.get('abc')); // undefined
遍历操作
keys()
:返回 Map 对象中每个元素的 键名(即Key值)。values()
:返回Map对象中每个元素的键值(即 value 值)。entries()
:返回 [key, value] 。forEach()
:对 Map 对象中的每一个键值对执行一次参数中提供的回调函数。for...of
:可以直接遍历每个成员
需要特别注意的是,Map的遍历顺序就是插入顺序。
下面是使用实例。
let 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);
}
// 等同于使用map.entries()
for (let [key, value] of map) {
console.log(key, value);
}
与数组的互相转换
(1)Map转为数组
Map转为数组最方便的方法,就是使用扩展运算符(…)。
let myMap = new Map().set(true, 7).set({foo: 3}, ['abc']);
[...myMap]
// [ [ true, 7 ], [ { foo: 3 }, [ 'abc' ] ] ]//双元素数组
(2)数组转为Map
将数组转入Map构造函数,就可以转为Map。
new Map([[true, 7], [{foo: 3}, ['abc']]])
// Map {true => 7, Object {foo: 3} => ['abc']}
WeakMap
WeakMap
结构与Map
结构基本类似,唯一的区别是它只接受对象作为键名(null
除外),不接受其他类型的值作为键名。
var map = new WeakMap()
map.set(1, 2)
// TypeError: 1 is not an object!
上面代码中,如果将1
作为WeakMap的键名,都会报错。
WeakMap
的设计目的在于,键名是对象的弱引用(垃圾回收机制不将该引用考虑在内),所以其所对应的对象可能会被自动回收。当对象被回收后,WeakMap
自动移除对应的键值对。
典型应用是,一个对应DOM元素的WeakMap
结构,当某个DOM元素被清除,其所对应的WeakMap
记录就会自动被移除。
WeakMap的设计目的在于,有时我们想在某个对象上面存放一些数据,但是这会形成对于这个对象的引用。一旦不再需要这两个对象,我们就必须手动删除这个引用,否则垃圾回收机制就不会释放对象占用的内存。
WeakMap与Map在API上的区别主要是两个,一是没有遍历操作(即没有key()
、values()
和entries()
方法),也没有size
属性;二是无法清空,即不支持clear
方法。WeakMap
只有四个方法可用:get()
、set()
、has()
、delete()
。
var wm = new WeakMap();
wm.size
// undefined
wm.forEach
// undefined