ECMAScript6 之常用技巧

主要列举了工作开发中一些高频使用的 ECMAScript 6 的实用内容。
参考:《ECMAScript 6 入门》

1. let 和 const

letconst 声明的变量只在它所在的代码块有效。

1.1. var 存在的问题

  • var 有作用域问题(会污染全局作用域)
  • var 可以重复声明
  • var 会变量提升预解释
  • var 不能定义常量

1.2. letconst 特性

  • letconst 不可以重复声明
  • letconst 不存在变量提升
  • letconst 不会声明到全局作用域上
  • letconst 存在暂时性死区(temporal dead zone,简称 TDZ)。letconst 声明的变量不会被提升到作用域顶部,如果在声明之前访问这些变量,会导致报错。
  • const 做常量声明(一般常量名用大写),const 实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址不得改动。

1.3. 代码说明

1.3.1. 作用域

var 有作用域问题(会污染全局作用域),而 letconst 不会;var 在顶层作用域声明的变量,会直接挂在全局 window 对象下,成为 window 对象的一个属性。

1
2
3
4
5
6
7
// var
var a = 1;
console.log(window.a);

// let、const
let a = 1;
console.log(window.a); // undefined

1.3.2. 重复声明

var 可以重复声明同一个变量,但 letconst 不行;letconst 不允许在相同作用域内,重复声明同一个变量。

1
2
3
4
5
6
7
// var
var a = 1;
var a = 2;

// let、const
let a = 1;
let a = 2; // Uncaught SyntaxError: Identifier 'a' has already been declared

1.3.3. 变量提升、暂时性死区

var 存在变量提升、而 letconst 不存在,且 letconst 存在“暂时性死区”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// var
console.log(a); // undefined
var a = 5;

// let、const
console.log(a); // Uncaught ReferenceError: a is not defined
let a = 5;

// 暂时性死区
var a = 5;
{
console.log(a); // Uncaught ReferenceError: a is not defined
let a = 4;
}

1.3.4. 常量

var 不能直接定义常量,而 const 可以定义常量。

1
2
const a = 5;
a = 6; // Uncaught TypeError: Assignment to constant variable

2. 解构赋值

ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。本质上,只要等号两边的模式相同,左边的变量就会被赋予对应的值。
数组,对象,字符串,数字和布尔值都能进行结构,但常用的还是对数组和对象进行结构。

2.1. 数组的解构赋值

事实上,只要某种数据结构具有 Iterator 接口,都可以采用数组形式的解构赋值。如果等号的右边不是数组(或者严格地说,不是可遍历的结构),那么将会报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 1. 通常情况
let [a, b, c] = [1, 2, 3];
// a: 1, b: 2, c: 3
let [ , , c] = [1, 2, 3];
// c: 3

// 2. 嵌套数组
let [a, [[b], c]] = [1, [[2], 3]];
// a: 1, b: 2, c: 3

// 3. 解构不成功,为 undefined
let [a] = [];
// a: undefined

// 4. 不完全解构
let [a, [b], d] = [1, [2, 3], 4];
// a: 1, b: 2, d: 4

// 5. 默认值,变量严格等于 undefined,默认值才会生效
let [x = 1] = [undefined];
x // 1
let [x = 1] = [null];
x // null

2.2. 对象的解构赋值

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 1. 通常情况
let { foo, bar } = { foo: "aaa", bar: "bbb" };
// foo: "aaa" , bar: "bbb"

// 2. 变量名与属性名不一致
let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
// baz: "aaa"

// 3. 解构不成功,为 undefined
let { foo } = {bar: 'baz'};
// foo: undefined

// 4. 默认值,变量严格等于 undefined,默认值才会生效
var { x: y = 3 } = {};
// y: 3

// 5. 复杂结构
let obj = {
p: [
'Hello',
{ y: 'World' }
]
};

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

2.3. 字符串的解构赋值

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

1
2
const [a, b, c, d, e, length] = 'hello';
// a: "h" .... length: 5

3. 箭头函数

3.1. 什么是箭头函数

3.1.1. 箭头函数简介

箭头函数的语法非常简单 (parameters) => { statements },箭头函数只能用赋值式写法,不能用声明式写法。看一下最简单的箭头函数表示法:

1
2
let sayHello = () => { console.log('Hello World'); }
sayHello(); // Hello World

3.1.2. 箭头函数的优点

和普通函数相比,箭头函数主要就是以下两个方面的特点:

  • 更简化的代码语法
  • 没有局部 this 的绑定
  • 不绑定 arguments

3.2. 更简化的代码语法

3.2.1. 一般写法

(parameters) => { statements }

1
2
3
4
5
6
7
8
9
10
11
12
// 求两个数的和并在控制台输出
// ES6
let sum = (a, b) => {
console.log(a + b);
return a + b;
}

// ES5
var sum = function(a, b) {
console.log(a + b);
return a + b;
};

3.2.2. 省略 () 的情况

如果只有一个参数,可以省略()括号。
parameters => { statements }

1
2
3
4
5
6
7
8
9
10
11
12
// 求一个数的两倍,并输出
// ES6
let double = num => {
console.log(num * 2);
return num * 2;
}

// ES5
var double = function(num) {
console.log(num * 2);
return num * 2;
};

3.2.3. 省略 {} 的情况

如果返回值仅仅只有一个表达式(expression), 还可以省略大括号{}
parameters => statements

1
2
3
4
5
6
7
8
// 求一个数的两倍
// ES6
let double = num => num * 2;

// ES5
var double = function(num) {
return num * 2;
};

3.2.4. 特殊情况

如果箭头函数直接返回一个对象,必须在对象外面加上括号。

1
2
3
4
5
6
7
8
9
10
// ES6
let returnObj = () => ({name: "xiguapi", age: 23});

// ES5
var returnObj = function() {
return {
name: "xiguapi",
age: 23
}
};

3.3. 没有局部 this 的绑定

和一般的函数不同,箭头函数不会绑定 this。 或则说箭头函数不会改变 this 本来的绑定。
我们用一个例子来说明:

1
2
3
4
function Counter() {
this.num = 0;
}
var a = new Counter(); // a.num 0

因为使用了关键字 new 构造,Count() 函数中的 this绑定到一个新的对象,并且赋值给 a。通过 console.log 打印 a.num,会输出 0。

如果我们想每过一秒将 a.num 的值加 1,该如何实现呢?可以使用 setInterval() 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
function Counter() {
this.num = 0;
this.timer = setInterval(function add() {
this.num++;
console.log(this.num);
}, 1000);
}

var b = new Counter();
// NaN
// NaN
// NaN
// ...

首先函数 setInterval 没有被某个声明的对象调用,也没有使用 new 关键字,再之没有使用 bind, callapplysetInterval 只是一个普通的函数。实际上 setInterval 里面的 this 绑定到全局对象 window 上了。

使用箭头函数!使用箭头函数就不会导致 this 被绑定到全局对象。

1
2
3
4
5
6
7
8
9
10
11
12
function Counter() {
this.num = 0;
this.timer = setInterval(() => {
this.num++;
console.log(this.num);
}, 1000);
}
var b = new Counter();
// 1
// 2
// 3
// ...

通过 Counter 构造函数绑定的 this 将会被保留。在 setInterval 函数中,this 依然指向我们新创建的 b 对象。

3.3. 不绑定 arguments

箭头函数还有一个比较有特点的地方就是其不绑定 arguments,即如果你在箭头函数中使用 arguments 参数不能得到想要的内容。

1
2
3
let arrowfunc = () => console.log(arguments.length)
arrowfunc()
// arguments is not defined

3.4. 适用场景及注意点

  • 箭头函数适合于无复杂逻辑或者无副作用的纯函数场景下,例如:用在 map、reduce、filter 的回调函数定义中
  • 箭头函数的亮点是简洁,但在有多层函数嵌套的情况下,箭头函数反而影响了函数的作用范围的识别度,这种情况不建议使用箭头函数
  • 箭头函数要实现类似纯函数的效果,必须剔除外部状态。所以箭头函数不具备普通函数里常见的 this、arguments 等,当然也就不能用 call()、apply()、bind() 去改变 this 的指向
  • 箭头函数不适合定义对象的方法(对象字面量方法、对象原型方法、构造器方法),因为箭头函数没有自己的 this,其内部的 this 指向的是外层作用域的 this
  • 箭头函数不适合定义结合动态上下文的回调函数(事件绑定函数),因为箭头函数在声明的时候会绑定静态上下文
1
2
const json = {bar: 1, fn: () => console.log(this.bar)};
json.fn(); //-> undefined
1
2
3
4
5
6
7
function Foo() {
this.bar = 1;
}

Foo.prototype.fn = () => console.log(this.foo);
const foo = new Foo();
foo.fn(); //-> undefined // this 并不是指向 Foo,根据变量查找规则,回溯到了全局作用域
1
2
3
4
5
const Message = (text) => {
this.text = text;
};
var helloMessage = new Message('Hello World!');
console.log(helloMessage.text); //-> Message is not a constructor // 不可以当作构造函数,也就是说,不可以使用 new 命令
1
2
3
4
const button = document.querySelector('button');
button.addEventListener('click', () => {
this.textContent = 'Loading...';
}); // this 并不是指向预期的 button 元素,而是 window

4. 字符串

4.1. includes 方法

1
2
3
// 判断字符串中是否包含某个字符串
let str = 'javascript';
str.includes('sc'); // true

4.2. endsWith、startsWith 方法

1
2
3
4
// 判断字符串是否以某一个字符串开始或结束
var a = '1AB2345CD';
console.log(a.startsWith('1A')); // true
a.endsWith('cD') // false

4.3. 字符串内容重复输出

1
'a'.repeat(5);//aaaaa 重复输出5遍

4.4. 字符串补全

1
2
3
4
// 参数1:[number] 目标字符串长度
// 参数2:[string] 进行补全的字符串
'12345'.padStart(7, '0') // 0012345 - 字符串不足7位,在头部补充不足长度的目标字符串
'12345'.padEnd(7, '0') // 1234500 - 在尾部进行字符串补全

4.5. 原生支持模板语言

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// es5
$('#result').append(
'There are <b>' + basket.count + '</b> ' +
'items in your basket, ' +
'<em>' + basket.onSale +
'</em> are on sale!'
);
// es6
// 在es6中,内容模板,可以定义在 `` 包起来的字符串中,其中的内容会保持原有格式
// 另外可以在字符串中直接使用模板语言进行变量填充,优雅而简洁
$('#result').append(`
There are <b>${basket.count}</b> items
in your basket, <em>${basket.onSale}</em>
are on sale!
`);