本文的目的是让了解ES5的人能快速上手ES6开发。
原文链接: 阮一峰的ES6入门教程
let和const命令
let命令
let
命令用来声明变量,但它的作用范围只在let
命令所在的代码块内有效。
{
let a = 10;
var b = 1;
}
a // ReferenceError: a is not defined.
b // 1
下述代码写了两种不同的循环体,他们区别在于:
方式一声明全局变量i
,每一次循环i的值都进行变化。
方式二每一次循环都重新声明一个新变量i
, JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。
for(var i = 0; i < 10; i++){
...
}
for(let i = 0; i < 10; i++){
...
}
特点
1.不存在变量提升
var
命令允许现使用后声明,而let
命名不允许这种愚蠢的行为。
// var 的情况
console.log(foo); // 输出undefined
var foo = 2;
// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;
2.暂时性死区
由于let
命令不存在变量提升,因此如果我们现使用变量,后声明,从使用变量开始到声明的这段语句就叫做暂时性死区。
if (true) {
// TDZ开始
tmp = 'abc'; // ReferenceError
console.log(tmp); // ReferenceError
let tmp; // TDZ结束
console.log(tmp); // undefined
tmp = 123;
console.log(tmp); // 123
}
暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。
3.不允许重复声明
let
不允许在相同作用域内,重复声明同一个变量。
// 报错
function func() {
let a = 10;
var a = 1;
}
// 报错
function func() {
let a = 10;
let a = 1;
}
4.块级作用域
let实际上为 JavaScript 新增了块级作用域。
下述代码想要在某种情况下让n为10,否则为默认的5。假如变量n是var则会导致变量提升到if前,无论什么情况都是输出10.
function f1() {
let n = 5;
if (true) {
let n = 10;
}
console.log(n); // 5
}
需要注意的是块级作用域必须要要有{}
// 第一种写法,报错
if (true) let x = 1;
// 第二种写法,不报错
if (true) {
let x = 1;
}
5.顶层对象的属性
ES6 为了改变顶层对象的属性与全局变量挂钩,一方面规定,为了保持兼容性,var命令和function命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。
var a = 1;
// 如果在 Node 的 REPL 环境,可以写成 global.a
// 或者采用通用方法,写成 this.a
window.a // 1
let b = 1;
window.b // undefined
const命令
const
声明一个只读的常量。一旦声明,常量的值就不能改变。
const PI = 3.1415;
PI // 3.1415
PI = 3;
// TypeError: Assignment to constant variable.
const
实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据来说(数值,字符串,布尔值)等同于常量,而对于复合类型的变量(对象、数组)则不一定。
比如对象指向的只是一个地址,const
只是保证这个地址不改变,而地址中存储的对象是可以改变的。(有点类似于java里的final
关键字,final
声明的对象,也可以之后赋值改变)
const foo = {};
// 为 foo 添加一个属性,可以成功
foo.prop = 123;
foo.prop // 123
// 将 foo 指向另一个对象,就会报错
foo = {}; // TypeError: "foo" is read-only
变量的解构赋值
数组的解构赋值
ES6允许按照一定模式,从数组和对象中提取值,对变量进行赋值。
//例子1
let [a, b, c] = [1, 2, 3];
//a 1
//b 2
//c 3
这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值,接下来的例子就是嵌套数组的写法。
//例子2
let [foo, [[bar], baz]] = [1, [[2], 3]];
foo // 1
bar // 2
baz // 3
值得注意的是
(1)...
相当于把head
后面的全部赋值给tail
。
let [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4]
(2)可以不完全解构,不完全解构也属于解构成功。
let [x, y] = [1, 2, 3];
x // 1
y // 2
(3)想要解构的前提右侧必须是可遍历结构(包含Iterator 接口),比如Set也可以被解构。
// 报错
let [foo] = 1;
//正确
let [x, y, z] = new Set(['a', 'b', 'c']);
x // "a"
(4)可以为变量赋默认值,不过要切记ES6中使用的是严格相等于,只有当一个数组成员严格等于undefined
,默认值才会生效。
let [foo = true] = [];
foo // true
let [x = 1] = [undefined];
x // 1
let [x = 1] = [null];
x // null
对象的解构赋值
对象的解构赋值与数组类似,但区别在于不会根据顺序来解构,而是根据属性/键值来解构。
let { bar, foo } = { foo: 'aaa', bar: 'bbb' };
foo // "aaa"
bar // "bbb"
值得注意的是
(1)可以通过对象的解构赋值将对象的方法赋值给变量。
const { log } = console;
log('hello') // hello
(2)匹配模式与对象的关系
下述代码中,foo
是模式,baz
才是真正的变量。
let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"
foo // error: foo is not defined
在ES6中是使用模式:变量形式来进行解构的,而默认的相当于以下代码。
let { foo: foo, bar: bar } = { foo: 'aaa', bar: 'bbb' };
(3)多层嵌套
再多层嵌套中,如果想取到值就不光需要模式匹配,同时也需要多个模式(比如最后去取line
时的loc
和start
)
const node = {
loc: {
start: {
line: 1,
column: 5
}
}
};
let { loc, loc: { start }, loc: { start: { line }} } = node;
line // 1
loc // Object {start: Object}
start // Object {line: 1, column: 5}
(4)如果要将一个已经声明的变量用于解构赋值,必须非常小心
JavaScript 引擎会将{x}
理解成一个代码块,从而发生语法错误。
// 错误的写法
let x;
{x} = {x: 1};
// SyntaxError: syntax error
//正确的写法
// 正确的写法
let x;
({x} = {x: 1});
函数参数的解构赋值
其实这也是一个解构赋值,因为传入的是一个数组,数组参数会在传入时被结构出x
和y
。
function add([x, y]){
return x + y;
}
add([1, 2]); // 3
函数参数同样允许有默认值,函数move
的参数是一个对象,通过对这个对象进行解构,得到变量x
和y
的值。如果解构失败,x
和y
等于默认值。
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]
用途
(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)函数参数的定义
简单来说,如果传入的数据是按照形参顺序的,直接就扔进去个数组就完事了。如果传入的数据不是按照形参顺序传入,也可以传进去个对象,然后根据key去对应形参。
总之,这样就使得传入的数据要求降低了。
// 参数是一组有次序的值
function f([x, y, z]) { ... }
f([1, 2, 3]);
// 参数是一组无次序的值
function f({x, y, z}) { ... }
f({z: 3, y: 2, x: 1});
(4)提取 JSON 数据
我学的时候就觉得可以提取JSON数据,这个果然是最重要的一个应用。我们可以从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]
(5)遍历 Map 结构
这个我感觉也很有用,任何带有了 Iterator
接口的对象,都可以用for...of
循环遍历。我们可以轻松拿到key
和value
。
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) {
// ...
}
(6)输入指定模块
这个超级常用,比如一个库里我们只想拿几个函数/类之类的就可以使用。
const { SourceMapConsumer, SourceNode } = require("source-map");
函数的扩展
函数参数的默认值
基本定义
当参数是基本类型时。
function log(x,y='world'){
console.log(x,y);
}
log('Hello') // Hello World
当参数是对象时,就要记得不光要赋默认值,同时也要与对象的解构相结合。
下面的例子就没有为参数赋默认值,就会导致foo
只有传入对象才可以,否则就会报错。
//错误形式
function foo({x, y = 5}) {
console.log(x, y);
}
foo({x: 1}) // 1 5
foo({x: 1, y: 2}) // 1 2
foo() // TypeError: Cannot read property 'x' of undefined
//正确形式
function foo({x, y = 5}=={}) {
console.log(x, y);
}
foo()// undefined 5
下面再来一个例子来介绍下如何正确的与解构赋值结合。
第一种形式是把函数的参数默认值为空对象然后在对象解构时x与y有默认值,而第二种形式时把函数的参数默认值变为x为0,y为0的对象。
// 写法一
function m1({x = 0, y = 0} = {}) {
return [x, y];
}
// 写法二
function m2({x, y} = { x: 0, y: 0 }) {
return [x, y];
}
它们在不传入参数时,没有任何区别,但当传入的对象不包含x
或y
属性时就会体现出区别。因此我们建议使用第一种方式这样不会出现对象解构失败而出现undefined
情况。
// 函数没有参数的情况
m1() // [0, 0]
m2() // [0, 0]
// x 和 y 都有值的情况
m1({x: 3, y: 8}) // [3, 8]
m2({x: 3, y: 8}) // [3, 8]
// x 有值,y 无值的情况
m1({x: 3}) // [3, 0]
m2({x: 3}) // [3, undefined]
// x 和 y 都无值的情况
m1({}) // [0, 0];
m2({}) // [undefined, undefined]
m1({z: 3}) // [0, 0]
m2({z: 3}) // [undefined, undefined]
参数的默认值位置
我们要切记参数的默认值应该是函数的尾参数,否则无法省略。
可以看到,除非我们传入undefined否则都会报错,(,1)
也不行。
// 例一
function f(x = 1, y) {
return [x, y];
}
f() // [1, undefined]
f(2) // [2, undefined]
f(, 1) // 报错
f(undefined, 1) // [1, 1]
指定了默认值以后,函数的length
属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length
属性将失真。
如果设置了默认值的参数不是尾参数,那么length属性也不再计入后面的参数了。
(function (a = 0, b, c) {}).length // 0
(function (a, b = 1, c) {}).length // 1
参数的作用域
一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的。
利用参数默认值,可以指定某一个参数不得省略,如果省略就抛出一个错误。
下面的例子中参数是单独的作用域,和函数内部的不是同一作用域,而f()
没有传入参数,因此会去外部找x
,因此最后y
就是1了。
let x = 1;
function f(y = x) {
let x = 2;
console.log(y);
}
f() // 1
再来一个复杂的例子:
上面代码中,函数foo
的参数形成一个单独作用域。这个作用域里面,首先声明了变量x,然后声明了变量y
,y
的默认值是一个匿名函数。这个匿名函数内部的变量x
,指向同一个作用域的第一个参数x
。函数foo
内部又声明了一个内部变量x
,该变量与第一个参数x
由于不是同一个作用域,所以不是同一个变量,因此执行y
后,内部变量x
和外部全局变量x
的值都没变。
var x = 1;
function foo(x, y = function() { x = 2; }) {
var x = 3;
y();
console.log(x);
}
foo() // 3
x // 1
如果将var x = 3
的var
去除,函数foo
的内部变量x
就指向第一个参数x
,与匿名函数内部的x
是一致的,所以最后输出的就是2,而外层的全局变量x
依然不受影响。
var x = 1;
function foo(x, y = function() { x = 2; }) {
x = 3;
y();
console.log(x);
}
foo() // 2
x // 1
简单应用
值得注意的是:参数mustBeProvided
的默认值等于throwIfMissing
函数的运行结果,这表明参数的默认值不是在定义时执行,而是在运行时执行。如果参数已经赋值,默认值中的函数就不会运行。
function throwIfMissing() {
throw new Error('Missing parameter');
}
function foo(mustBeProvided = throwIfMissing()) {
return mustBeProvided;
}
foo()
// Error: Missing parameter
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
参数来代替arguments
的好处是rest
就是一个数组。可以直接使用数组的方法,而arguments是对象,还需要转换成数组。
// arguments变量的写法
function sortNumbers() {
return Array.prototype.slice.call(arguments).sort();
}
// rest参数的写法
const sortNumbers = (...numbers) => numbers.sort();
箭头函数
基础定义
ES6允许使用箭头来定义函数,形式如下。
var f = v => v;
// 等同于
var f = function (v) {
return v;
};
当函数没有参数时使用()
来表示,多个参数时用(arg1,arg2)
来表示,如果代码语句超过一句,需要用{}
,并且使用return
语句返回。
var f = () => 1;
//等同于
var f = function (){
return 1;
}
var f = (arg1,arg2) => arg1+arg2;
//等同于
var f = function (){
return arg1+arg2;
}
var sum = (num1, num2) => { return num1 + num2; }
由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错,或取得错误结果。
let foo = () => { a: 1 };
foo() // undefined
应用
1.与变量结构相结合
传入一个person对象,可以进行解构。
const full = ({ first, last }) => first + ' ' + last;
// 等同于
function full(person) {
return person.first + ' ' + person.last;
}
2.简化回调函数
也可以叫做简化匿名函数,可以直接用箭头函数来代替匿名函数。
// 正常函数写法
[1,2,3].map(function (x) {
return x * x;
});
// 箭头函数写法
[1,2,3].map(x => x * x);
另一个是在更改排序的内部函数,要从小到大排序。
// 正常函数写法
var result = values.sort(function (a, b) {
return a - b;
});
// 箭头函数写法
var result = values.sort((a, b) => a - b);
注意
1.函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
2.不可以当作构造函数
3.没有arguments对象,可以用rest参数代替。
其实第一个注意点是个好事,可以将this
绑定。DOM的回调函数封装在handler
对象中,添加的方法使用箭头函数,可以使this
绑定至handler
对象,否则的话this就会指向最外层,就会报错,因为外层没有dosomething
方法。
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);
}
};
this指向的固定化,并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。正是因为它没有this,所以也就不能用作构造函数。
// ES6
function foo() {
setTimeout(() => {
console.log('id:', this.id);
}, 100);
}
// ES5
function foo() {
var _this = this;
setTimeout(function () {
console.log('id:', _this.id);
}, 100);
}
可以看到上述代码将ES6转成ES5之后其实是在外边定义了一个对象,来指向外层的this,这就和ES5中为了让内部函数的this绑定在外部的对象使用了相同的方法。
var o = {
f1: function() {
console.log(this);
var that = this;
var f2 = function() {
console.log(that);
}();
}
}
o.f1()
// Object
// Object
4.不可以在定义对象的方法/属性里使用箭头函数,因为它没有this
globalThis.s = 21;
const obj = {
s: 42,
m: () => console.log(this.s)
};
obj.m() // 21
上面例子中,obj.m()
使用箭头函数定义。JavaScript 引擎的处理方法是,先在全局空间生成这个箭头函数,然后赋值给obj.m
,这导致箭头函数内部的this
指向全局对象,所以obj.m()
输出的是全局空间的21,而不是对象内部的42。上面的代码实际上等同于下面的代码。核心原因在于对象不具有单独的作用域。
globalThis.s = 21;
globalThis.m = () => console.log(this.s);
const obj = {
s: 42,
m: globalThis.m
};
obj.m() // 21
假如是普通的函数中使用this的话,就会指向使用它的对象,那么就会是正确的值而非全局空间了。
5.不可以在需要动态的时候使用箭头函数
var button = document.getElementById('press');
button.addEventListener('click', () => {
this.classList.toggle('on');
});
上面代码运行时,点击按钮会报错,因为button的监听函数是一个箭头函数,导致里面的this就是全局对象。如果改成普通函数,this就会动态指向被点击的按钮对象。
数组的扩展
扩展运算符
...
,它的作用是将一个数组转化为用逗号分割的参数序列,相当于rest的逆运算。
console.log(...[1, 2, 3])
// 1 2 3
console.log(1, ...[2, 3, 4], 5)
// 1 2 3 4 5
其应用如下:
1.替代apply函数,直接展开数组。
例如Math.max方法需要传入一组参数,而数组没有自带取最大值的方法,就可以使用扩展运算符。
// ES5 的写法
Math.max.apply(null, [14, 3, 77])
// ES6 的写法
Math.max(...[14, 3, 77])
// 等同于
Math.max(14, 3, 77);
2.数组的复制
如果想将数组a中的元素复制到数组b,在es5需要将数组a展开,这里可以借助扩展运算符。
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 [...butLast, last] = [1, 2, 3, 4, 5];
// 报错
const [first, ...middle, last] = [1, 2, 3, 4, 5];
// 报错
4.任何实现了Iterator 接口的对象都可以变成数组
let map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
]);
let arr = [...map.keys()]; // [1, 2, 3]
其他函数
Array.from
Array.from
方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。
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函数,允许传入第二参数,用来对每个元素进行处理,将处理后的值放入返回的数组。
Array.from(arrayLike, x => x * x);
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]
find和findIndex
数组实例的find
方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true
的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined
。
[1,2,3].find((x) => x >= 2);
//2
而findIndex
是找到第一个满足的元素的位置,否则返回-1。
[1,2,3].findIndex(function(x){
return x>=2;
});
//1
find
和findIndex
都允许传入第二个函数以绑定this
。
function f(v){
return v > this.age;
}
let person = {name: 'John', age: 20};
[10, 12, 26, 15].find(f, person); // 26
数组实例的 entries(),keys() 和 values()
这是ES6新提供的遍历数组的新方法,它们都返回一个遍历器对象(《Iterator》),可以用for…of循环进行遍历,唯一的区别是keys()
是对键名的遍历、values()
是对键值的遍历,entries()
是对键值对的遍历。
for(i of[1,2,3,4,5].keys()){
console.log(i);
}
for(v of[1,2,3,4,5].values()){
console.log(v);
}
for([key,value] of [1,2,3,4,5].entries()){
console.log(key+","+value);
}
数组实例的includes()
可以看到和indexof
的区别在于可以判断NaN
了。因为indexof
方法判断的是===
就会导致NaN无法满足条件。
[1, 2, 3].includes(2) // true
[1, 2, 3].includes(4) // false
[1, 2, NaN].includes(NaN) // true
[NaN].indexOf(NaN) // -1
第二个参数代表从哪里开始寻找。
[1, 2, 3].includes(3, 3); // false
[1, 2, 3].includes(3, -1); // true
对象的扩展
属性的简洁表达式
ES6 允许在大括号里面,直接写入变量和函数,作为对象的属性和方法。这样的书写更加简洁。
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!";
}
};
但切记,不要在构造函数中简写,否则会报错。
我们常见的例子就是get
和set
函数。
const cart = {
_wheels: 4,
get wheels () {
return this._wheels;
},
set wheels (value) {
if (value < this._wheels) {
throw new Error('数值太小了!');
}
this._wheels = value;
}
}
属性的可枚举性和遍历
对象的每一个属性都有一个描述对象(Descriptor),用来控制属性的行为。
let obj = { foo: 123 };
Object.getOwnPropertyDescriptor(obj, 'foo')
// {
// value: 123,
// writable: true,
// enumerable: true,
// configurable: true
// }
其中的enumerable是可枚举性,通常在遍历中,会根据该属性判断是否会规避该属性,在for...in
语句中,就会因为length属性的可枚举性为false,而不遍历。
而在正常使用中,我们更多的关注对象本身,而不关注其继承的对象,因此尽量使用Object.keys(obj)
而不是for...in
语句。
for…in循环:只遍历对象自身的和继承的可枚举的属性。
Object.keys():返回对象自身的所有可枚举的属性的键名。
super关键词
ES6 新增了另一个类似this
的关键字super
,指向当前对象的原型对象。但只能在对象方法的简略法中使用。
const proto = {
foo: 'hello'
};
const obj = {
foo: 'world',
find() {
return super.foo;
}
};
Object.setPrototypeOf(obj, proto);
obj.find() // "hello"
错误实例:
// 报错
const obj = {
foo: super.foo
}
// 报错
const obj = {
foo: () => super.foo
}
// 报错
const obj = {
foo: function () {
return super.foo
}
}
对象的扩展运算符
对象的解构赋值用于从一个对象取值,相当于将目标对象自身的所有可遍历的(enumerable)、但尚未被读取的属性,分配到指定的对象上面。所有的键和它们的值,都会拷贝到新对象上面。
let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
x // 1
y // 2
z // { a: 3, b: 4 }
上面代码中,变量z
是解构赋值所在的对象。它获取等号右边的所有尚未读取的键(a和b),将它们连同值一起拷贝过来。
链判断运算符
ES6中提出了链判断运算符,以防止出现以下情况,如果其中一个为null的话就会报错,因此常规会有很麻烦的判断。
// 错误的写法
const firstName = message.body.user.firstName;
// 正确的写法
const firstName = (message
&& message.body
&& message.body.user
&& message.body.user.firstName) || 'default';
而在ES6中简化了写法,使用?.
const firstName = message?.body?.user?.firstName || 'default';
该运算符具有中断特性,只要其为null
或undefined
就会返回undefined
。
链判断运算符有三种用法。
- obj?.prop // 对象属性
- obj?.[expr] // 同上
- func?.(…args) // 函数或对象方法的调用
例子如下:
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()
Null运算符
我们会选择在对象的属性为null
或undefined
的时候为其赋初始值,通常情况如下:
const headerText = response.settings.headerText || 'Hello, world!';
const animationDuration = response.settings.animationDuration || 300;
const showSplashScreen = response.settings.showSplashScreen || true;
可由于JS的自动转换,当值为0或者false都会使用初始值,因此我们这里引进新的Null判断符??
。它的行为类似||,但是只有运算符左侧的值为null或undefined时,才会返回右侧的值。
该运算符可以和?.
相结合,并赋值。
const animationDuration = response.settings?.animationDuration ?? 300;
上述代码代表当response.settings
为null
或undefined
时,就会返回300。
Set和Map数据结构
Set
基础
可以通过两种方式创建Set,普通创建和用数组(或者具有Iterable接口的其他数据结构)创建。
const s = new Set();
// 例一
const set = new Set([1, 2, 3, 4, 4]);
[...set]
// [1, 2, 3, 4]
值得一提的是Set中判断是否相等的机制类似于完全相等(===),区别是,这里认为NaN等于自身。
常用方法
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
遍历方式:
常规的就是keys(),values(),entries()三个方法,而Set没有value,所以和key是相同的值。
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"]
扩展运算符(…)内部使用for...of
循环,所以也可以用于 Set 结构。我们可以利用Set和扩展运算符去掉数组中重复的元素。
let arr = [3, 5, 2, 2, 5, 5];
let unique = [...new Set(arr)];
// [3, 5, 2]
Map
常用方法
- size属性
- set(key,value)
- get(key)
- has(key)
- delete(key)
- clear()
遍历与Set相似,顺序都是插入顺序。
数据结构互换
1.map转数组,使用扩展运算符
const myMap = new Map()
.set(true, 7)
.set({foo: 3}, ['abc']);
[...myMap]
// [ [ true, 7 ], [ { foo: 3 }, [ 'abc' ] ] ]
2.数组转map,直接传入
new Map([
[true, 7],
[{foo: 3}, ['abc']]
])
// Map {
// true => 7,
// Object {foo: 3} => ['abc']
// }
Iterator和for…of循环
基础
Iterator 的作用有三个:一是为各种数据结构,提供一个统一的、简便的访问接口;二是使得数据结构的成员能够按某种次序排列;三是 ES6 创造了一种新的遍历命令for...of
循环,Iterator 接口主要供for...of
消费。
Iterator的实质是通过调用next函数。每次next函数都会返回一个对象,包括value
和done
。当done
为true时,就会停止遍历。
Iterator 的遍历过程是这样的。
(1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
(2)第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。
(3)第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。
(4)不断调用指针对象的next方法,直到它指向数据结构的结束位置。
Iterator过程可以通过如下代码来理解,it
是一个可以遍历数组的遍历器,当调用其next
函数就可以进行遍历。
var it = makeIterator(['a', 'b']);
it.next() // { value: "a", done: false }
it.next() // { value: "b", done: false }
it.next() // { value: undefined, done: true }
function makeIterator(array) {
var nextIndex = 0;
return {
next: function() {
return nextIndex < array.length ?
{value: array[nextIndex++], done: false} :
{value: undefined, done: true};
}
};
}
ES6 规定,默认的 Iterator
接口部署在数据结构的Symbol.iterator
属性,或者说,一个数据结构只要具有Symbol.iterator
属性,就可以认为是“可遍历的”(iterable)。Symbol.iterator属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。
const obj = {
[Symbol.iterator] : function () {
return {
next: function () {
return {
value: 1,
done: true
};
}
};
}
};
ES6中实现iterator接口的有:
- Array
- Map
- Set
- String
- TypedArray
- 函数的 arguments 对象
- NodeList 对象
对象之所以不实现该接口的缘故是不知道遍历顺序,因此可以我们自己定义。下面就是个例子,我们使用了RangeIterator来实现输出范围内数字的功能。range函数生成了一个对象,该对象具有Symbol.Iterator属性,也就是本身对象,因为在构造函数中定义了next函数。在for…of过程中就会通过Symbol.Iterator属性,找到迭代器,并调用其next函数。
class RangeIterator {
constructor(start, stop) {
this.value = start;
this.stop = stop;
}
[Symbol.iterator]() { return this; }
next() {
var value = this.value;
if (value < this.stop) {
this.value++;
return {done: false, value: value};
}
return {done: true, value: undefined};
}
}
function range(start, stop) {
return new RangeIterator(start, stop);
}
for (var value of range(0, 3)) {
console.log(value); // 0, 1, 2
}
上面代码首先在构造函数的原型链上部署Symbol.iterator方法,调用该方法会返回遍历器对象iterator,调用该对象的next方法,在返回一个值的同时,自动将内部指针移到下一个实例。有点类似于链表。
function Obj(value) {
this.value = value;
this.next = null;
}
Obj.prototype[Symbol.iterator] = function() {
var iterator = { next: next };
var current = this;
function next() {
if (current) {
var value = current.value;
current = current.next;
return { done: false, value: value };
} else {
return { done: true };
}
}
return iterator;
}
var one = new Obj(1);
var two = new Obj(2);
var three = new Obj(3);
one.next = two;
two.next = three;
for (var i of one){
console.log(i); // 1, 2, 3
}
遍历器对象同时具有return
函数和throw
函数,return()
方法的使用场合是,如果for...of
循环提前退出(通常是因为出错,或者有break语句),就会调用return()
方法。throw()
方法主要是配合 Generator
函数使用,一般的遍历器对象用不到这个方法。
调用return函数的时机如下:情况一输出文件的第一行以后,就会执行return()方法,关闭这个文件;情况二会在执行return()方法关闭文件之后,再抛出错误。
function readLinesSync(file) {
return {
[Symbol.iterator]() {
return {
next() {
return { done: false };
},
return() {
file.close();
return { done: true };
}
};
},
};
// 情况一
for (let line of readLinesSync(fileName)) {
console.log(line);
break;
}
// 情况二
for (let line of readLinesSync(fileName)) {
console.log(line);
throw new Error();
}
实例
最好在工作中实际遇到这种问题关注下,看看自己实现的Iterator都注意了哪些东西。