ZhangYang's Blog

ES6核心

ES6的简介

ECMAScript 6(以下简称ES6)是JavaScript语言的下一代标准。因为当前版本的ES6是在2015年发布的,所以又称ECMAScript 2015

Babel转码器

Babel 是一个广泛使用的 ES6 转码器,可以将 ES6 代码转为 ES5 代码

1
2
3
4
5
6
7
// 转码前
input.map(item => item + 1);
// 转码后
input.map(function (item) {
return item + 1;
});

let和const命令

let命令

基本用法

ES6 新增了let命令,用来声明变量。它的用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效

1
2
3
4
5
6
7
{
let a = 10;
var b = 1;
}
a // ReferenceError: a is not defined.
b // 1

使用let,声明的变量仅在块级作用域内有效,解决如下代码问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 10
// 用let
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 6

let命令改变了语法行为,它所声明的变量一定要在声明后使用,否则报错

1
2
3
4
5
6
7
// var 的情况
console.log(foo); // 输出undefined
var foo = 2;
// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;

let不允许在相同作用域内,重复声明同一个变量

1
2
3
4
5
6
7
8
9
10
11
// 报错
function func() {
let a = 10;
var a = 1;
}
// 报错
function func() {
let a = 10;
let a = 1;
}

const 命令

const声明一个只读的常量。一旦声明,常量的值就不能改变

1
2
3
4
5
const PI = 3.1415;
PI // 3.1415
PI = 3;
// TypeError: Assignment to constant variable.

const的作用域与let命令相同:只在声明所在的块级作用域内有效

1
2
3
4
5
if (true) {
const MAX = 5;
}
MAX // Uncaught ReferenceError: MAX is not defined

变量的解构赋值

数组的解构赋值

ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构

1
2
3
4
5
6
7
// 为变量赋值,直接指定值
let a = 1;
let b = 2;
let c = 3;
// 从数组中提取值,按照对应位置,对变量赋值
let [a, b, c] = [1, 2, 3];

下面是一些使用嵌套数组进行解构的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let [foo, [[bar], baz]] = [1, [[2], 3]];
foo // 1
bar // 2
baz // 3
let [ , , third] = ["foo", "bar", "baz"];
third // "baz"
let [x, , y] = [1, 2, 3];
x // 1
y // 3
let [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4]
let [x, y, ...z] = ['a'];
x // "a"
y // undefined
z // []

解构赋值允许指定默认值

1
2
3
4
5
let [foo = true] = [];
foo // true
let [x, y = 'b'] = ['a']; // x='a', y='b'
let [x, y = 'b'] = ['a', undefined]; // x='a', y='b'

对象的解构赋值

解构不仅可以用于数组,还可以用于对象

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

对象的属性没有次序,变量必须与属性同名,才能取到正确的值

1
2
3
4
5
6
let { bar, foo } = { foo: "aaa", bar: "bbb" };
foo // "aaa"
bar // "bbb"
let { baz } = { foo: "aaa", bar: "bbb" };
baz // undefined

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

1
2
3
4
5
6
7
let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"
let obj = { first: 'hello', last: 'world' };
let { first: f, last: l } = obj;
f // 'hello'
l // 'world'

字符串的解构赋值

字符串被转换成了一个类似数组的对象

1
2
3
4
5
6
const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"

数组的对象都有一个length属性,因此还可以对这个属性解构赋值

1
2
let {length : len} = 'hello';
len // 5

数值和布尔值的解构赋值

解构赋值时,如果等号右边是数值和布尔值,则会先转为对象

1
2
3
4
5
let {toString: s} = 123;
s === Number.prototype.toString // true
let {toString: s} = true;
s === Boolean.prototype.toString // true

解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象.由于undefined和null无法转为对象,所以对它们进行解构赋值,都会报错

1
2
let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError

函数参数的解构赋值

函数add的参数表面上是一个数组,但在传入参数的那一刻,数组参数就被解构成变量x和y

1
2
3
4
function add([x, y]){
return x + y;
}
add([1, 2]); // 3

函数参数的解构也可以使用默认值

1
2
3
4
5
6
7
8
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]

圆括号问题

ES6 的规则是,只要有可能导致解构的歧义,就不得使用圆括号

解构赋值用途

交换变量的值

交换变量x和y的值,这样的写法不仅简洁,而且易读,语义非常清晰

1
2
3
4
let x = 1;
let y = 2;
[x, y] = [y, x];

从函数返回多个值

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 返回一个数组
function example() {
return [1, 2, 3];
}
let [a, b, c] = example();
// 返回一个对象
function example() {
return {
foo: 1,
bar: 2
};
}
let { foo, bar } = example();

函数参数的定义

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

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

提取 JSON 数据

快速提取 JSON 数据的值

1
2
3
4
5
6
7
8
9
10
let jsonData = {
id: 42,
status: "OK",
data: [867, 5309]
};
let { id, status, data: number } = jsonData;
console.log(id, status, number);
// 42, "OK", [867, 5309]

函数参数的默认值

指定参数的默认值,就避免了在函数体内部再写var foo = config.foo || ‘default foo’;这样的语句

1
2
3
4
5
6
7
8
9
10
11
jQuery.ajax = function (url, {
async = true,
beforeSend = function () {},
cache = true,
complete = function () {},
crossDomain = false,
global = true,
// ... more config
} = {}) {
// ... do stuff
};

遍历Map结构

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

1
2
3
4
5
6
7
8
9
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

如果只想获取键名,或者只想获取键值,可以写成下面这样

1
2
3
4
5
6
7
8
9
// 获取键名
for (let [key] of map) {
// ...
}
// 获取键值
for (let [,value] of map) {
// ...
}

输入模块的指定方法

加载模块时,往往需要指定输入哪些方法。解构赋值使得输入语句非常清晰

1
const { SourceMapConsumer, SourceNode } = require("source-map");

字符串的扩展

includes, startsWith, endsWith

includes():返回布尔值,表示是否找到了参数字符串

startsWith():返回布尔值,表示参数字符串是否在原字符串的头部

endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部

1
2
3
4
5
6
7
8
9
10
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

repeat

repeat方法返回一个新字符串,表示将原字符串重复n次

1
2
3
'x'.repeat(3) // "xxx"
'hello'.repeat(2) // "hellohello"
'na'.repeat(0) // ""

padStart,padEnd

padStart()用于头部补全,padEnd()用于尾部补全

1
2
3
4
5
6
7
8
'x'.padStart(5, 'ab') // 'ababx'
'x'.padStart(4, 'ab') // 'abax'
'x'.padEnd(5, 'ab') // 'xabab'
'x'.padEnd(4, 'ab') // 'xaba'
// 如果原字符串的长度,等于或大于指定的最小长度,则返回原字符串
'xxx'.padStart(2, 'ab') // 'xxx'
'xxx'.padEnd(2, 'ab') // 'xxx'

matchAll

matchAll方法返回一个正则表达式在当前字符串的所有匹配

模板字符串

传统的 JavaScript 语言,输出模板

1
2
3
4
5
6
$('#result').append(
'There are <b>' + basket.count + '</b> ' +
'items in your basket, ' +
'<em>' + basket.onSale +
'</em> are on sale!'
);

ES6 引入了模板字符串

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

模板字符串用反引号(`)标识,可以用来定义多行字符串,或者在字符串中嵌入变量

1
2
3
4
5
6
7
8
9
10
11
12
13
// 普通字符串
`In JavaScript '\n' is a line-feed.`
// 多行字符串
`In JavaScript this is
not legal.`
console.log(`string text line 1
string text line 2`);
// 字符串中嵌入变量
let name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`

正则的扩展

数值的扩展

Number.isFinite, Number.isNaN

Number.isFinite()用来检查一个数值是否为有限的(finite),即不是Infinity

1
2
3
4
5
6
7
8
Number.isFinite(15); // true
Number.isFinite(0.8); // true
Number.isFinite(NaN); // false
Number.isFinite(Infinity); // false
Number.isFinite(-Infinity); // false
Number.isFinite('foo'); // false
Number.isFinite('15'); // false
Number.isFinite(true); // false

Number.isNaN()用来检查一个值是否为NaN

1
2
3
4
5
6
7
Number.isNaN(NaN) // true
Number.isNaN(15) // false
Number.isNaN('15') // false
Number.isNaN(true) // false
Number.isNaN(9/NaN) // true
Number.isNaN('true' / 0) // true
Number.isNaN('true' / 'true') // true

Number.isInteger()

Number.isInteger()用来判断一个数值是否为整数

1
2
Number.isInteger(25) // true
Number.isInteger(25.1) // false

函数的扩展

函数参数的默认值

ES6 之前,不能直接为函数的参数指定默认值,只能采用变通的方法

1
2
3
4
5
6
7
function log(x, y) {
y = y || 'World';
console.log(x, y);
}
log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello World

ES6 允许为函数的参数设置默认值,即直接写在参数定义的后面

1
2
3
4
5
6
function log(x, y = 'World') {
console.log(x, y);
}
log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello

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

1
2
3
4
5
6
7
function foo({x, y = 5}) {
console.log(x, y);
}
foo({}) // undefined 5
foo({x: 1}) // 1 5
foo({x: 1, y: 2}) // 1 2
foo() // TypeError: Cannot read property 'x' of undefined

rest参数

ES6 引入 rest 参数(形式为…变量名),用于获取函数的多余参数

1
2
3
4
5
6
7
8
function add(...values) {
let sum = 0;
for (var val of values) {
sum += val;
}
return sum;
}
add(2, 5, 3) // 10

name属性

函数的name属性,返回该函数的函数名

1
2
3
4
5
6
7
8
9
function foo() {}
foo.name // "foo"
// ES6 对这个属性的行为做出了一些修改
var f = function () {};
// ES5
f.name // ""
// ES6
f.name // "f"

箭头函数

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

1
2
3
4
5
6
var f = v => v;
// 等同于
var f = function (v) {
return v;
};

如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分

1
2
3
4
5
6
7
8
9
var f = () => 5;
// 等同于
var f = function () { return 5 };
var sum = (num1, num2) => num1 + num2;
// 等同于
var sum = function(num1, num2) {
return num1 + num2;
};

如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回

1
var sum = (num1, num2) => { return num1 + num2; }

箭头函数的一个用处是简化回调函数

1
2
3
4
5
6
7
8
9
10
11
12
13
// 正常函数写法
[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);

rest 参数与箭头函数结合的例子

1
2
3
4
5
6
7
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]]

箭头函数有几个使用注意点

  • 函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
  • 不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
  • 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
  • 不可以使用yield命令,因此箭头函数不能用作 Generator 函数
1
2
3
4
5
6
7
8
9
10
// this对象的指向是可变的,但是在箭头函数中,它是固定的
function foo() {
setTimeout(() => {
console.log('id:', this.id);
}, 100);
}
var id = 21;
foo.call({ id: 42 });
// id: 42

箭头函数可以让setTimeout里面的this,绑定定义时所在的作用域,而不是指向运行时所在的作用域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Timer() {
this.s1 = 0;
this.s2 = 0;
// 箭头函数
setInterval(() => this.s1++, 1000);
// 普通函数
setInterval(function () {
this.s2++;
}, 1000);
}
var timer = new Timer();
setTimeout(() => console.log('s1: ', timer.s1), 3100);
setTimeout(() => console.log('s2: ', timer.s2), 3100);
// s1: 3
// s2: 0

数组的扩展

扩展运算符

扩展运算符(spread)是三个点(…)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列

1
2
3
4
5
6
7
8
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>]

Array.from()

Array.from方法用于将两类对象转为真正的数组:类似数组的对象和可遍历的对象

1
2
3
4
5
6
7
8
9
10
11
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.of()

Array.of方法用于将一组值,转换为数组

1
2
3
4
5
6
7
8
Array() // []
Array(3) // [, , ,]
Array(3, 11, 8) // [3, 11, 8]
// 这个方法的主要目的,是弥补数组构造函数Array()的不足
Array.of(3, 11, 8) // [3,11,8]
Array.of(3) // [3]
Array.of(3).length // 1

copyWithin()

在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组

接受三个参数

  • target(必需):从该位置开始替换数据。如果为负值,表示倒数
  • start(可选):从该位置开始读取数据,默认为 0。如果为负值,表示倒数
  • end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示倒数
1
2
3
4
5
6
7
8
9
10
11
// 将从 3 号位直到数组结束的成员(4 和 5),复制到从 0 号位开始的位置,结果覆盖了原来的 1 和 2
[1, 2, 3, 4, 5].copyWithin(0, 3)
// [4, 5, 3, 4, 5]
// 将3号位复制到0号位
[1, 2, 3, 4, 5].copyWithin(0, 3, 4)
// [4, 2, 3, 4, 5]
// -2相当于3号位,-1相当于4号位
[1, 2, 3, 4, 5].copyWithin(0, -2, -1)
// [4, 2, 3, 4, 5]

find() 和 findIndex()

find方法,用于找出第一个符合条件的数组成员,如果没有符合条件的成员,则返回undefined

1
2
3
4
5
6
7
[1, 4, -5, 10].find((n) => n < 0)
// -5
// 找出数组中第一个小于 0 的成员
[1, 5, 10, 15].find(function(value, index, arr) {
return value > 9;
}) // 10

findIndex方法的用法与find方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1

1
2
3
[1, 5, 10, 15].findIndex(function(value, index, arr) {
return value > 9;
}) // 2

fill()

fill方法使用给定值,填充一个数组

1
2
3
4
5
['a', 'b', 'c'].fill(7)
// [7, 7, 7]
new Array(3).fill(7)
// [7, 7, 7]

fill方法还可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置

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

entries(),keys() 和 values()

entries(),keys()和values()——用于遍历数组,可以用for…of循环进行遍历

keys()是对键名的遍历、values()是对键值的遍历,entries()是对键值对的遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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"

includes()

Array.prototype.includes方法返回一个布尔值,表示某个数组是否包含给定的值

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

对象的扩展

属性的简介表示法

ES6 允许直接写入变量和函数,作为对象的属性和方法

1
2
3
4
5
6
const foo = 'bar';
const baz = {foo};
baz // {foo: "bar"}
// 等同于
const baz = {foo: foo};

ES6 允许在对象之中,直接写变量。这时,属性名为变量名, 属性值为变量的值

1
2
3
4
5
6
7
8
9
10
11
function f(x, y) {
return {x, y};
}
// 等同于
function f(x, y) {
return {x: x, y: y};
}
f(1, 2) // Object {x: 1, y: 2}

属性名表达式

ES6 允许字面量定义对象时,把表达式放在方括号内,作为对象的属性名

1
2
3
4
5
6
let propKey = 'foo';
let obj = {
[propKey]: true,
['a' + 'bc']: 123
};

表达式还可以用于定义方法名

1
2
3
4
5
6
7
let obj = {
['h' + 'ello']() {
return 'hi';
}
};
obj.hello() // hi

方法的 name 属性

对象方法也是函数,因此也有name属性

1
2
3
4
5
6
7
const person = {
sayName() {
console.log('hello!');
},
};
person.sayName.name // "sayName"

Object.is()

Object.is就是比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致

1
2
3
4
Object.is('foo', 'foo')
// true
Object.is({}, {})
// false

Object.assign()

Object.assign方法用于对象的合并,将源对象的所有可枚举属性,复制到目标对象

1
2
3
4
5
6
7
const target = { a: 1 };
const source1 = { b: 2 };
const source2 = { c: 3 };
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}

如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性

1
2
3
4
5
6
7
const target = { a: 1, b: 1 };
const source1 = { b: 2, c: 2 };
const source2 = { c: 3 };
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}

Object.assign方法实行的是浅拷贝,而不是深拷贝,即源对象跟着目标对象的属性变化而变化

1
2
3
4
5
const obj1 = {a: {b: 1}};
const obj2 = Object.assign({}, obj1);
obj1.a.b = 2;
obj2.a.b // 2

对于这种嵌套的对象,一旦遇到同名属性,Object.assign的处理方法是替换

1
2
3
4
const target = { a: { b: 'c', d: 'e' } }
const source = { a: { b: 'hello' } }
Object.assign(target, source)
// { a: { b: 'hello' } }

Object.assign可以用来处理数组,但是会把数组视为对象

1
2
Object.assign([1, 2, 3], [4, 5])
// [4, 5, 3]

Object.assign只能进行值的复制,如果要复制的值是一个取值函数,那么将求值后再复制

1
2
3
4
5
6
7
const source = {
get foo() { return 1 }
};
const target = {};
Object.assign(target, source)
// { foo: 1 }

Object.keys()

ES5 引入了Object.keys方法,返回一个数组,成员是参数对象自身的所有可遍历属性的键名

1
2
3
var obj = { foo: 'bar', baz: 42 };
Object.keys(obj)
// ["foo", "baz"]

Object.values()

Object.values方法返回一个数组,成员是参数对象自身的所有可遍历属性的键值

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

Object.entries()

Object.entries方法返回一个数组,成员是参数对象自身的所有可遍历属性的键值对数组

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

作用

ES6 引入了跟Object.keys配套的Object.values和Object.entries,作为遍历一个对象的补充手段,供for…of循环使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let {keys, values, entries} = Object;
let obj = { a: 1, b: 2, c: 3 };
for (let key of keys(obj)) {
console.log(key); // 'a', 'b', 'c'
}
for (let value of values(obj)) {
console.log(value); // 1, 2, 3
}
for (let [key, value] of entries(obj)) {
console.log([key, value]); // ['a', 1], ['b', 2], ['c', 3]
}

Symbol

概述

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

1
2
3
4
let s = Symbol();
typeof s
// "symbol"

Symbol函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述

1
2
3
4
5
6
7
8
9
// s1和s2是两个 Symbol 值
let s1 = Symbol('foo');
let s2 = Symbol('bar');
s1 // Symbol(foo)
s2 // Symbol(bar)
s1.toString() // "Symbol(foo)"
s2.toString() // "Symbol(bar)"

Symbol 的参数是一个对象,就会调用该对象的toString方法,将其转为字符串,然后才生成一个 Symbol 值

1
2
3
4
5
6
7
const obj = {
toString() {
return 'abc';
}
};
const sym = Symbol(obj);
sym // Symbol(abc)

Symbol函数的参数只是表示对当前 Symbol 值的描述,因此相同参数的Symbol函数的返回值是不相等的

1
2
3
4
5
6
7
8
9
10
11
12
// s1和s2都是Symbol函数的返回值,而且参数相同,但是它们是不相等的
// 没有参数的情况
let s1 = Symbol();
let s2 = Symbol();
s1 === s2 // false
// 有参数的情况
let s1 = Symbol('foo');
let s2 = Symbol('foo');
s1 === s2 // false

作为属性名的 Symbol

Symbol 值都是不相等的,Symbol 值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 通过方括号结构和Object.defineProperty,将对象的属性名指定为一个 Symbol 值
let mySymbol = Symbol();
// 第一种写法
let a = {};
a[mySymbol] = 'Hello!';
// 第二种写法
let a = {
[mySymbol]: 'Hello!'
};
// 第三种写法
let a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });
// 以上写法都得到同样结果
a[mySymbol] // "Hello!"

Promise 对象

基本用法

Promise对象是一个构造函数,用来生成Promise实例

1
2
3
4
5
6
7
8
9
10
11
// Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject
//
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});

Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数

1
2
3
4
5
promise.then(function(value) {
// success
}, function(error) {
// failure
});

setTimeout的例子

1
2
3
4
5
6
7
8
9
10
11
// timeout方法返回一个Promise实例,表示一段时间以后才会发生的结果
// 过了指定的时间(ms参数)以后,Promise实例的状态变为resolved,就会触发then方法绑定的回调函数
function timeout(ms) {
return new Promise((resolve, reject) => {
setTimeout(resolve, ms, 'done');
});
}
timeout(100).then((value) => {
console.log(value);
});

异步加载图片的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function loadImageAsync(url) {
return new Promise(function(resolve, reject) {
const image = new Image();
image.onload = function() {
resolve(image);
};
image.onerror = function() {
reject(new Error('Could not load image at ' + url));
};
image.src = url;
});
}

ajax操作的例子

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
27
28
29
30
31
// getJSON是对 XMLHttpRequest 对象的封装,用于发出一个针对 JSON 数据的 HTTP 请求,并且返回一个Promise对象
// 需要注意的是,在getJSON内部,resolve函数和reject函数调用时,都带有参数
const getJSON = function(url) {
const promise = new Promise(function(resolve, reject){
const handler = function() {
if (this.readyState !== 4) {
return;
}
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
const client = new XMLHttpRequest();
client.open("GET", url);
client.onreadystatechange = handler;
client.responseType = "json";
client.setRequestHeader("Accept", "application/json");
client.send();
});
return promise;
};
getJSON("/posts.json").then(function(json) {
console.log('Contents: ' + json);
}, function(error) {
console.error('出错了', error);
});

resolve函数的参数除了正常的值以外,还可能是另一个 Promise 实例

1
2
3
4
5
6
7
8
const p1 = new Promise(function (resolve, reject) {
// ...
});
const p2 = new Promise(function (resolve, reject) {
// ...
resolve(p1);
})

Promise.prototype.then()

then方法的第一个参数是resolved状态的回调函数,第二个参数(可选)是rejected状态的回调函数

then方法返回的是一个新的Promise实例

采用链式写法,即then方法后面再调用另一个then方法

1
2
3
4
5
6
// 第一个回调函数完成以后,会将返回结果作为参数,传入第二个回调函数
getJSON("/posts.json").then(function(json) {
return json.post;
}).then(function(post) {
// ...
});

Module 的语法

概述

历史上,JavaScript 一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件

在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器

ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案

1
2
3
4
5
6
7
8
9
10
11
// CommonJS模块
let { stat, exists, readFile } = require('fs');
// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;
// ES6模块
import { stat, exists, readFile } from 'fs';

严格模式

ES6 的模块自动采用严格模式,不管你有没有在模块头部加上”use strict”

严格模式主要有以下限制

  • 变量必须声明后再使用
  • 函数的参数不能有同名属性,否则报错
  • 不能使用with语句
  • 不能对只读属性赋值,否则报错
  • 不能使用前缀 0 表示八进制数,否则报错
  • 不能删除不可删除的属性,否则报错
  • 不能删除变量delete prop,会报错,只能删除属性delete global[prop]
  • eval不会在它的外层作用域引入变量
  • eval和arguments不能被重新赋值
  • arguments不会自动反映函数参数的变化
  • 不能使用arguments.callee
  • 不能使用arguments.caller
  • 禁止this指向全局对象
  • 不能使用fn.caller和fn.arguments获取函数调用的堆栈
  • 增加了保留字(比如protected、static和interface)

export 命令

模块功能主要由两个命令构成:export和import

export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能

一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取

外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量

1
2
3
4
5
6
7
8
9
10
11
12
13
// profile.js文件,保存了用户信息。ES6 将其视为一个模块,里面用export命令对外部输出了三个变量
// profile.js
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;
// export的写法,除了像上面这样,还有另外一种
// profile.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;
export {firstName, lastName, year};

export命令除了输出变量,还可以输出函数或类(class)

1
2
3
4
// 代码对外输出一个函数multiply
export function multiply(x, y) {
return x * y;
};

export输出的变量就是本来的名字,但是可以使用as关键字重命名

1
2
3
4
5
6
7
8
9
// 使用as关键字,重命名了函数v1v2的对外接口。重命名后,v2可以用不同的名字输出两次
function v1() { ... }
function v2() { ... }
export {
v1 as streamV1,
v2 as streamV2,
v2 as streamLatestVersion
};

export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 报错
export 1;
// 报错
var m = 1;
export m;
// 正确写法
// 写法一
export var m = 1;
// 写法二
var m = 1;
export {m};
// 写法三
var n = 1;
export {n as m};

function和class的输出,也必须遵守这样的写法

1
2
3
4
5
6
7
8
9
10
// 报错
function f() {}
export f;
// 正确
export function f() {};
// 正确
function f() {}
export {f};

export命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错

1
2
3
4
5
// export语句放在函数之中,结果报错
function foo() {
export default 'bar' // SyntaxError
}
foo()

import 命令

使用export命令定义了模块的对外接口以后,其他 JS 文件就可以通过import命令加载这个模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// import命令,用于加载profile.js文件,并从中输入变量
// import命令接受一对大括号,里面指定要从其他模块导入的变量名
// 大括号里面的变量名,必须与被导入模块(profile.js)对外接口的名称相同
// main.js
import {firstName, lastName, year} from './profile.js';
function setName(element) {
element.textContent = firstName + ' ' + lastName;
}
// 如果想为输入的变量重新取一个名字,import命令要使用as关键字,将输入的变量重命名
import { lastName as surname } from './profile.js';

import语句会执行所加载的模块

1
2
// 仅仅执行lodash模块,但是不输入任何值
import 'lodash';

模块的整体加载

除了指定加载某个输出值,还可以使用整体加载,即用星号(*)指定一个对象,所有输出值都加载在这个对象上面

1
2
3
4
5
6
7
8
9
10
// 一个circle.js文件,它输出两个方法area和circumference
// circle.js
export function area(radius) {
return Math.PI * radius * radius;
}
export function circumference(radius) {
return 2 * Math.PI * radius;
}

加载这个模块

1
2
3
4
5
6
// main.js
import { area, circumference } from './circle';
console.log('圆面积:' + area(4));
console.log('圆周长:' + circumference(14));

上面写法是逐一指定要加载的方法,整体加载的写法如下

1
2
3
4
import * as circle from './circle';
console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circumference(14));

export default 命令

使用import命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载

用户肯定希望快速上手,未必愿意阅读文档,去了解模块有哪些属性和方法

为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出

1
2
3
4
5
// 一个模块文件export-default.js,它的默认输出是一个函数
// export-default.js
export default function () {
console.log('foo');
}

其他模块加载该模块时,import命令可以为该匿名函数指定任意名字

1
2
3
4
// 可以用任意名称指向export-default.js输出的方法,这时就不需要知道原模块输出的函数名
// import-default.js
import customName from './export-default';
customName(); // 'foo'

export default命令用在非匿名函数前,也是可以的

1
2
3
4
5
6
7
8
9
10
11
12
13
// foo函数的函数名foo,在模块外部是无效的。加载的时候,视同匿名函数加载
// export-default.js
export default function foo() {
console.log('foo');
}
// 或者写成
function foo() {
console.log('foo');
}
export default foo;

下面比较一下默认输出和正常输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 第一组是使用export default时,对应的import语句不需要使用大括号
// 第二组是不使用export default时,对应的import语句需要使用大括
// 第一组
export default function crc32() { // 输出
// ...
}
import crc32 from 'crc32'; // 输入
// 第二组
export function crc32() { // 输出
// ...
};
import {crc32} from 'crc32'; // 输入

有了export default命令,输入模块时就非常直观了,以输入 lodash 模块为例

1
2
// 如果想在一条import语句中,同时输入默认方法和其他接口,可以写成下面这样
import _, { each, each as forEach } from 'lodash';

对应上面代码的export语句如下

1
2
3
4
5
6
7
8
9
10
// 最后一行的意思是,暴露出forEach接口,默认指向each接口,即forEach和each指向同一个方法
export default function (obj) {
// ···
}
export function each(obj, iterator, context) {
// ···
}
export { each as forEach };

export default也可以用来输出类

1
2
3
4
5
6
// MyClass.js
export default class { ... }
// main.js
import MyClass from 'MyClass';
let o = new MyClass();