ECMAScript6

ECMAScript6.0(简称ES6)是JavaScript语言的下一代标准, 已经在2015年6月正式发布了. 它的目标是使JavaScript可以用来编写复杂的大型应用程序, 成为企业级开发语言. 各个浏览器对ECMAScript6的支持可以查看 http://kangax.github.io/compat-table/es6/.

Babel转码器

Babel是一个广泛的ECMAScript6转码器, 可以将ECMAScript6代码转为ECMAScript5代码. 通过这种方式就不用担心当前环境是否支持ECMAScript6.

Babel提供了一个REPL在线编译器, 可以在线将ECMAScript6代码转为ECMAScript5代码.

Babel还提供了Babel-cli工具, 可以使用命令行转码.

1
2
3
# npm是node.js包管理工具,使用npm命令必须先安装node.js
# 使用npm命令安装babel-cli工具
$ npm i -g babel-cli

基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
# 转码结果输出到标准输出
$ babel example.js

# 转码结果写入一个文件中
# -o(也可以使用--out-file参数)参数指定输出文件
$ babel example.js -o compiled.js

# 整个目录转码
# -d参数(也可以使用--out-dir参数)指定输出目录
$ babel src -d lib

# -s参数生成source map文件
$ babel src -d lib -s

let和const关键字

ECMAScript6新增了let关键字, 用来声明变量. 它的用法与var关键字类似, 但是用let关键字声明的变量, 只在let关键字所在的代码块内有效, 不会和var关键字一样发生变量提升.

1
2
3
4
5
6
7
{
let a = 1;
var b = 4;
}

console.log(a); // ReferenceError: a is not defined.
console.log(b); // 4

ECMAScript6明确规定, 区块中存在let和const关键字, 这个区块对这些声明的变量和常量, 从一开始就形成了封闭作用域. 只要在声明之前使用这些变量和常量, 就会报错.

在代码块中, 使用let关键字声明变量之前, 该变量是不可用的, 这在语法上称为TDZ(暂时性死区).

let关键字不允许在相同作用域中重复声明同一变量, let关键字为JavaScript新增了块级作用域, ECMAScript6引入了块级作用域, 明确允许在块级作用域中声明函数.

const关键字用来声明一个只读常量, 声明过后, 常量的值就不能改变. const关键字声明的常量也是不提升的, 也存在TDZ, 只能在声明之后使用常量.

1
2
3
4
if (true) {
console.log(a); // ReferenceError
const a = 4;
}

对于复合类型的常量, 常量名不指向数据, 而是指向数据所在的地址. const关键字只保证变量名指向地址不变, 并不保证该地址的数据不变.

1
2
3
4
5
6
7
const foo = {
bar: 'zero',
bingo: 'four'
};

foo.bingo = 'two';
console.log(foo.bingo); // two

ECMAScript6规定var关键字和function关键字声明的全局变量, 依旧是全局对象的属性; let关键字, const关键字和class关键字声明的全局变量和常量, 不属于全局对象的属性

变量的解构赋值

1
2
3
4
5
6
7
8
9
// 数组的解构赋值
let [foo, [[bar], bingo]] = [1, [[2], 3]];
console.log(foo); // 1
console.log(bar); // 2
console.log(bingo); // 3

// 解构数组允许指定默认值
[foo, bar = 1] = [3]; // foo = 3, bar = 1
[foo, bar = 1] = [3, undefined]; // foo = 3, bar = 1

ECMAScript6内部使用严格相等相等符(===)判断一个值. 如果一个数组成员不严格等于undefined,默认值不会生效.

1
2
3
4
5
6
7
8
// 对象的解构赋值
var { bar, foo } = { bar: 'one', foo: 'two' };
console.log(bar); // one
console.log(foo); // two

var { bar: bingo } = { bar: 'one', foo: 'two' };
console.log(bingo); // one
console.log(bar); // error: bar is not defined

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

1
2
3
4
5
6
7
8
9
// 函数参数的解构赋值
[[1, 5], [7, 2]].map(function([a, b]) {
return a + b;
}); // [6, 9]

// 函数的解构赋值用作:
// 交换变量的值
// 提交JSON的值
// 函数参数的默认值

字符串的扩展

1
2
3
4
5
6
7
8
9
// includes(): 返回布尔值, 表示是否找到参数字符串
// startWidth(): 返回布尔值, 表示参数字符串是否在源字符串的头部
// endsWidth(): 返回布尔值, 表示参数字符是否在源字符串的尾部
// 第二个参数n, endsWidth()方法与另两个方法不同, 它针对前n个字符
// 另两个方法第二个参数n针对从第n个位置直到字符串结束
let bar = 'hello world!';
bar.startWidth('world', 6); // true
bar.endsWidth('hello', 5); // true
bar.includes('hello', 6); // false
1
2
// repeat()方法返回一个新字符串, 表示将源字符串重复n次
// 'bingo'.repeat(2); // 'bingobingo'
1
2
3
4
5
6
7
8
// padStart()方法用于头部补全
// padEnd()方法用于尾部补全
// 第一个参数用来指定字符串的最小长度
// 第二个参数用来补全的字符串
'go'.padStart(5, 'a'); // 'aaago'
'go'.padStart(4, 'abc'); // 'abgo'
'go'.padEnd(5, 'ab'); // 'goaba'
'go'.padEnd(3,'abcd'); // 'goa'
1
2
3
4
// 模板字符串是增强版的字符串, 用反引号标识. 它可以当作普通字符串使用
// 也可以用来定义多行字符串或者字符串嵌入变量
const name = 'Mike', time = 'today';
console.log(`Hello ${name}, how are you ${time}?`); // Hello Mike, how are you today?

数值的扩展

在ECMAScript5开始, 严格模式中八进制不再允许使用前缀0表示, 要使用前缀0o表示

1
2
3
4
// Number.isFinite()方法: 检查一个数字是否无穷(infinity)
Number.isFinite(0.0008); // true
Number.isFinite(NaN); // false
Number.isFinite(Infinity); // false
1
2
3
// Number.isNaN()方法: 检查一个值是否为NaN(not a number)
Number.isNaN(NaN); // true
Number.isNaN(10); // false

它们与传统的全局方法isFinite()和isNaN()的区别, 传统方法先调用Number将非数值的值转为数值, 在进行判断, 这两个新方法只对数值有效, 非数值一律返回false.

Number.parseInt(), Number.parseFloat():
ECMAScript6将全局方法parseInt()和parseFloat()移植到了Number对象上, 作用完全保持不变.

Number.isInteger():
用来判断一个值是否为整数, 在JavaScript内部, 整数和浮点数是同样的存储方法, 4和4.0被视为同一个值.

安全整数和Number.isSafeInteger():
JavaScript能准确表示的整数范围在-2^532^53之间(不含两个端点), 超过范围, 无法精确表示.

ECMAScript6引入了Number.MAX_SAFE_INTEGERNumber.MIN_SAFE_INTEGER两个常量, 用来表示这个范围的上下限.
Number.isSafeInteger()方法用来判断一个整数是否在这个范围内.

Math对象的扩展

  • Math.trunc()方法用于去除一个数的小数部分, 返回整数部分.
  • Math.sign()方法用来判断一个数是正数, 负数还是零.
  • Math.cbrt()方法用来计算一个数的立方根.
  • Math.fround()方法返回一个数的单精度浮点数形式.
  • Math.hypot方法返回所有参数的平方和的平方根.

数组的扩展

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Array.from()方法用于将两类对象转为真正的数组:
// 类似数组的对象(array-like object)和可遍历(iterable)的对象(包括新增的数据结构Set和Map)
// 常见的类似数组的对象是DOM操作返回的NodeList集合, 以及函数内部的arguments对象

// NodeList对象
let foo = document.getElementById('bar');
Array.from(foo).forEach(function (bar) {
console.log(bar);
});

// arguments对象
let bingo = function () {
let args = Array.from(arguments);
}
1
2
// Array.of()方法: 将一组值,转为数组形式
Array.of(1, 5, 8); // [1, 5, 8]
1
2
3
4
5
6
// 数组实例的copyWithin()方法
// Array.prototype.copyWithin(target, start = 0, end = this.length)
// 第一个参数target(必需): 从该位置开始替换数据
// 第二个参数start(可选): 从该位置开始读取数据,默认为0.如果为负数,表示倒数
// 第三个参数end(可选): 到该位置停止读取数据,默认等于数组长度.如果为负数,表示倒数
[1, 2, 3, 4, 5].copyWithin(0, 3, 4); // [4, 2, 3, 4, 5]
1
2
3
4
5
6
7
8
9
// 数组实例的find()方法: 找出第一个符合条件的数组元素
// 参数为一个回调函数, 所有数组元素依次执行该回调函数, 直到找到第一个返回值为true的元素, 返回该元素
// 没有符合条件的元素, 返回undefined
[1, 3, -8, 6].find(n => n < 0); // -8

// 数组实例的findIndex()方法用法与find()方法类似
// 不过返回的是第一个符合条件的数组元素的索引, 没有符合田间的元素,返回-1
[1, 3, -8, 6].find(n => n < 0); // 2
[1, 3, 8, 6].find(n => n < 0); // -1
1
2
// fill()方法使用给定值, 填充一个数组
[1, 3, 5].fill(4); // [4, 4, 4]

ECMAScript6提供三个新方法–entries(), key()和values(), 用于遍历数组. 唯一区别是key()方法是对键名的遍历, values()方法是对键值的遍历,entries()方法是对键值对的遍历.

函数的扩展

1
2
3
4
5
6
7
// 箭头函数
// ECMAScript6允许使用"箭头"( => )定义函数
var bar = a => a;
// 等同于
var bar = function (a) {
return a;
}

箭头函数注意点:

  • 函数体内的this对象, 就是定义时所在的对象, 而不是使用时所在的对象
  • 不可以当作构造函数, 不可以使用new关键字创建实例对象, 否则抛出一个错误
  • 不可以使用arguments对象, 该对象在函数体中不存在. 如要使用Rest参数代替
  • 不可以使用yield关键字, 箭头函数不能用作Generator函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ECMAScript6允许给函数的参数设置默认值
// 函数参数的默认值直接写在参数定义的后面
var foo = function (a, b = 'one') {
console.log(a, b);
}

// 指定默认之后, 函数的length属性可以返回没有指定默认值的参数个数
(function (a) {}).length // 1
(function (a = 1) {}).length // 0
(function (a, b, c = 2) {}).length // 2

// ECMAScript6引入rest参数(形式为'...变量名')
// 用于获取函数的剩余参数
// rest参数搭配的变量是一个数组, 该变量将剩余的参数放入数组中
// rest参数之后不能再有参数, 否则报错
function bar(...values) {
let sum = 0;
for (var val of values) {
sum += val;
}
return sum;
}

bar(1, 2, 3); // 6

扩展运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 扩展运算符(spread)是三个点( ... )
// 它好比rest参数的逆运算, 将一个数组转为用逗号分隔的参数序列
console.log(1, ...[2, 3, 4], 5); // 1 2 3 4 5
// ECMAScript5写法
Math.max.apply(null,[1, 2, 3]);
// ECMAScript6写法
Math.max(...[1, 2, 3]);

// 用扩展运算符合并数组
var arrOne = ['a', 'b', 'c'];
var arrTwo = ['d', 'e'];
var arrThree = ['f'];
// ECMAScript5写法
arrOne.concat(arrTwo, arrThree); // ['a', 'b', 'c', 'd', 'e', 'f']
// ECMAScript6写法
[...arrOne, ...arrTwo, ...arrThree] // ['a', 'b', 'c', 'd', 'e', 'f']

// 用扩展字符串将字符串转为数组
[...'bingo'] // ['b', 'i', 'n', 'g', 'o']

// 实现Iterator接口的对象
var nodeList = document.getElementById('bar');
var arr = [...nodeList];

对象的扩展

1
2
3
4
5
6
7
8
9
10
11
// ECMAScript6允许在对象中, 可以不写属性值
// 这时属性值等于属性名所代表的变量
const person = {
name: 'Mary',
// 等同于birthday: birthday
birthday,
// 等同于say: function () ...
say() {
console.log('My name is' + this.name);
}
};
1
2
3
4
5
6
7
// Object.assign()方法用于对象的合并
// 将源对象(source)的所有可枚举属性, 复制到目标对象(target)
var target = { a: 1 };
var sourceOne = { b: 9 };
var sourceTwo = { c: 6 };
Object.assign(target, sourceOne, sourceTwo);
console.log(target); // { a: 1, b: 9, c: 6 }

如果目标对象与源对象有同名属性, 或多个源对象有同名属性, 则后面的属性会覆盖前面的属性. Object.assign()方法实现的是浅拷贝, 而不是深拷贝. 如果源对象某个属性值是对象, 那目标杜希昂拷贝得到的是这个对象的引用.

Object.assign()方法的用处:

  • 为对象添加属性
  • 为对象添加方法
  • 克隆对象
  • 合并多个对象
  • 为属性指定默认值

ECMAScript6属性的遍历方法:

  • for..in循环遍历对象自身和继承的可枚举属性(不含Symbol属性)
  • Object.keys(obj)方法返回一个数组, 包含对象自身的(不含继承的)所有可枚举属性(不含Symbol属性)
  • Object.getOwnPropertyNames(obj)方法返回一个数组, 包含对象自身的所有属性(包含不可枚举属性, 不含Symbol属性)
  • Object.getOwnPropertySymbols(obj)方法返回一个数组, 包含对象自身的所有Symbol属性
  • Reflect.ownKeys(obj)方法返回一个数组, 包含对象自身的所有属性, 不管是属性名是Symbol或字符串, 也不管是否可枚举
1
2
3
4
5
6
7
8
// ECMAScript5对象属性名都是字符串, 容易造成属性名的冲突
// 为了防止属性名的冲突, ECMAScript6引入了Symbol
// Symbol值作为对象属性名时, 不能用点运算符
var mySymbol = Symbol();
var bar = {};
bar.mySymbol = 'hello';
console.log(bar[mySymbol]); // undefined
console.log(bar['mySymbol']); // hello

点运算符后面总是字符串, 所以不会读取mySymbol作为标识符所指代的那个值, 导致bar的属性名实际上是一个字符串, 而不是一个Symbol的值.

Symbol作为属性名, 该属性不出现在for..in, for..of循环中, 也不会被Object.keys()方法和Object.getOwnPropertyNames()方法返回. 但是他也不是私有属性, Object.getOwnPropertySymbols()方法可以获取指定对象的所有Symbol属性名.

Reflect.ownKeys()方法可以返回所有类型的键名, 包括常规键名和Symbol键名.

1
2
3
4
5
6
const obj = {
[Symbol('obj_key')]: 1,
bar: 2,
foo: 3
}
Reflect.ownKeys(obj); // [Symbol(obj_key), 'bar', 'foo']
1
2
3
4
5
6
7
8
9
10
// Symbol.for()方法可以重新使用同一个Symbol的值
// 它接收一个字符串作为参数, 搜索有没有以该参数作为名称的Symbol值
// 如果搜索有这个值, 返回这个Symbol值
// 否则就新建并返回一个以该字符串为名称的Symbol值
Symbol.for("bar") === Symbol.for("bar"); // true
Symbol("bar") === Symbol("bar"); // false
var symOne = Symbol.for("foo");
Symbol.keyFor(symOne); // "foo"
var symTwo = Symbol("foo");
Symbol.keyFor(symTwo); // undefined

Set和Map数据结构

ECMAScript6提供了新的数据结构Set. 它类似数组, 但元素值都是唯一的, 无重复值. Set本身是一个构造函数, 可以用来生成Set数据结构

1
2
3
4
5
6
var bar = new Set();
[2, 5, 6, 1, 2, 1, 2].map(a => bar.add(a));
for (let i of bar) {
console.log(i);
}
// 输出 2 5 6 1

Set实例对象的属性和方法

  • Set.prototype.constructor属性: 构造函数, 默认是Set函数
  • Set.prototype.size属性: 返回Set实例对象的成员总数
  • add(value)方法: 添加某个值, 返回Set结构本身
  • delete(value)方法: 删除某个值, 返回一个布尔值, 表示删除是否成功
  • has(value)方法: 返回一个布尔值, 表示该值是否为Set的成员
  • clear()方法: 清除所有成员, 没有返回值

JavaScript对象本质上是键值对的集合(Hash结构), 传统上只能用字符串当作键, 给使用带来了限制. 为了解决这个问题, ECMAScript6提供了Map数据结构, 它类似对象, 也是键值对的集合, 但是”键”的范围不限于字符串, 各种类型的值(包括对象)都可以当作键.

原生Map提供三个遍历器生成函数和一个遍历方法

  • keys(): 返回键名的遍历器
  • values(): 返回键值的遍历器
  • entries(): 返回所有成员的遍历器
  • forEach(): 遍历Map的所有成员

Generator函数

Generator函数从语法上可以把它理解成是一个状态机, 封装了多个内部状态. 执行Generator函数会返回一个遍历器对象, 他除了状态机, 还是一个遍历器对象生成函数, 返回的遍历器对象, 可以一次遍历Generator函数内部的每一个状态.

Generator函数形式上是一个普通函数, 但有两个特点, function关键字与函数名之间有一个星号, 函数体内部使用yield语句定义不同的内部状态.

1
2
3
4
5
6
7
8
9
10
11
function* Gener() {
yield 'hello';
yield 'world';
return 'ending';
}

var foo = Gener();
foo.next(); // { value: 'hello', done: false }
foo.next(); // { value: 'world', done: fasle }
foo.next(); // { value: 'ending', done: true }
foo.next(); // { value: undefined, done: true }

Promise对象

1
2
3
4
5
6
7
8
9
// 基本使用
var promise = new Promise(function (resolve, reject) {
// ... some code
if (/* success */) {
resolve(value);
} else {
reject(error);
}
});

Promise实例对象可以使用then()方法分别指定Resolved状态和Reject状态的回调函数. then()方法可以接收两个回调函数作为参数. 第一个回调函数是Promise对象的状态变为Resolved时调用, 第二个回调函数是Promise对象的状态变为Reject时调用. 第二个回调函数是可选的, 不一定要提供, 这两个回调函数都可以接收Promise对象传出的值作为参数. then()方法返回的是一个新的Promise实例(不是原来的Promise实例), 我们可以使用链式写法, 即then()方法之后再调用另一个then()方法.

Promise实例对象的catch()方法, 也就是.then(null, rejection)的别名, 用于指定发生错误时的回调函数. 不要在then()方法里面定义Reject状态的回调函数(即then()方法的第二个参数), 而是使用catch()方法.

Class

1
2
3
4
5
6
7
8
9
class Person {
constructor(a, b) {
this.a = a;
this.b = b;
}
toString() {
return '(' + this.a + ', ' + this.b + ')';
}
}

上面定义了一个”类”, 里面有一个constructor()方法, 这就是构造函数, this关键字代表实例对象. 由于类的方法默认定义在prototype对象上面, 所以类的新方法可以添加在prototype对象上面. Object.assign()方法可以很方便一次向类添加多个方法.

1
2
3
4
5
6
7
8
9
10
class Person {
constructor() {
// ... some code
}
}

Object.assign(Person.prototype, {
toString() {},
toValue() {}
});

类内部所有定义的方法都是不可枚举的(non-enumerable).

constructor()方法是类的默认方法, 通过new关键字创建实例对象时, 自动调用该方法. 一个类必须有constructor()方法, 如果没有显示定义, 会默认添加一个空的constructor()方法. contructor()方法默认返回的实例对象(即this)可以指定返回另一个对象.

Class继承

1
2
3
4
5
6
7
// Class之间可以使用extends关键字实现继承
class Son extends Person {}

// 子类构造函数中, 只有调用super()方法之后, 才可以使用this关键字, 否则会报错
// Class不存在变量提升
new Person(); // ReferenceError
class Person {}

Object.getPrototypeOf()方法可以从子类上获取父类, 可以使用该方法判断一个类是否继承至另一个类

super关键字

  • 作为函数调用时(即super(…args)), super代表父类的构造函数
  • 作为对象调用时(即super.prop或者super.method()), super代表父类, 此时super既可以引用父类实例的属性和方法, 也可以引用父类的静态方法

Class静态方法

类相当于实例的原型, 所有在类中定义的方法, 都会被实例继承. 在一个方法前加上static关键字, 表示该方法不会被实例继承, 而是直接通过类来调用, 这被称为静态方法.