2015年6月,ECMAScript 6正式通过,成为国际标准。尽管在目前的工作中还没有使用ES6,但是每项新技术出来总是忍不住想尝尝鲜,想知道ES6能为前端开发带来哪些变化?对自己的工作有哪些方面可以提升。刚好看到阮一峰的《ES6标准入门》,便顺着这本书尝试着ES6的各种新特性。
ES6的各种新特性的兼容性查询http://kangax.github.io/compat-table/es6/
尽管我们的浏览器还不一定完全支持ES6代码,我们可以使用Babel转码器,在这里我们使用命令行转码babel-cli,命令行$ npm install --global babel-cli
安装babel-cli
let和const命令
let命令
ES6新增了 let 命令,用来声明变量。它的用法类似于 var ,但是所声明的变量,只在 let 命令所在的代码块内有效。let不像var那样会发生“变量提升”现象。所以,变量一定要在声明后使用,否则报错。
{
let a = 10;
var b = 1;
}
a // ReferenceError: a is not defined.
b // 1
ES6明确规定,如果区块中存在 let 和 const 命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。
总之,在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称TDZ)。
let不允许在相同作用域内,重复声明同一个变量。let 实际上为JavaScript新增了块级作用域。 ES6引入了块级作用域,明确允许在块级作用域之中声明函数。
const命令
const 声明一个只读的常量。一旦声明,常量的值就不能改变。
const 命令声明的常量也是不提升,同样存在暂时性死区,只能在声明的位置后面使用。
if (true) {
console.log(MAX); // ReferenceError
const MAX = 5;
}
ES6规定var 命令和 function 命令声明的全局变量,依旧是全局对象的属性;let 命令、 const 命令、 class 命令声明的全局变量,不属于全局对象的属性。
变量的解构赋值
数组的解构赋值
let [foo, [[bar], baz]] = [1, [[2], 3]];
foo // 1
bar // 2
baz // 3
解构赋值允许指定默认值。
[x, y = 'b'] = ['a']; // x='a', y='b'
[x, y = 'b'] = ['a', undefined]; // x='a', y='b'
注意,ES6内部使用严格相等运算符( === ),判断一个位置是否有值。所以,如果一个数组成员不严格等于 undefined ,默认值是不会生效的。
对象的解构赋值
var { foo, bar } = { foo: "aaa", bar: "bbb" };
foo // "aaa"
bar // "bbb"
var { foo: baz } = { foo: "aaa", bar: "bbb" };
baz // "aaa"
foo // error: foo is not defined
对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。
函数参数的解构赋值
[[1, 2], [3, 4]].map(function([a,b]){
return a + b;
})
//[3,7]
变量解构赋值用途
- 交换变量的值
[x, y] = [y, x];
-
提取JSON数据
var jsonData = {
id: 42,
status: "OK",
data: [867, 5309]
};
let { id, status, data: number } = jsonData;
console.log(id, status, number);// 42, "OK", [867, 5309] -
函数参数的默认值
jQuery.ajax = function (url, {
async = true,
beforeSend = function () {},
cache = true,
complete = function () {},
crossDomain = false,
global = true
}) {
// ... do stuff
};
字符串的扩展
includes(), startsWith(), endsWith()
includes():返回布尔值,表示是否找到了参数字符串。
startsWith():返回布尔值,表示参数字符串是否在源字符串的头部。
endsWith():返回布尔值,表示参数字符串是否在源字符串的尾部。
var s = 'Hello world!';
s.startsWith('world', 6); // true
s.endsWith('Hello', 5); // true
s.includes('Hello', 6); // false
使用第二个参数 n 时, endsWith 的行为与其他两个方法有所不同。它针对前 n 个字符,而其他两个方法针对从第 n 个位置直到字符串结束。
repeat()
返回一个新字符串,表示将原字符串重复 n 次。
'hello'.repeat(2) // "hellohello"
padStart(),padEnd()
padStart 用于头部补全, padEnd 用于尾部补全。
'x'.padStart(5, 'ab') // 'ababx'
'x'.padStart(4, 'ab') // 'abax'
'x'.padEnd(5, 'ab') // 'xabab'
'x'.padEnd(4, 'ab') // 'xaba'
padStart 和 padEnd 一共接受两个参数,第一个参数用来指定字符串的最小长度,第二个参数是用来补全的字符串。
模板字符串
模板字符串(template string)是增强版的字符串,用反引号标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。
var name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`
数值的扩展
从ES5开始,在严格模式之中,八进制就不再允许使用前缀 0 表示,ES6进一步明确,要使用前缀 0o 表示。
Number.isFinite()
Number.isFinite() 用来检查一个数值是否非无穷(infinity)。
Number.isFinite(0.8); // true
Number.isFinite(NaN); // false
Number.isFinite(Infinity); // false
Number.isNaN()
Number.isNaN() 用来检查一个值是否为 NaN 。
Number.isNaN(NaN) // true
Number.isNaN(15) // false
它们与传统的全局方法 isFinite() 和 isNaN() 的区别在于,传统方法先调用 Number() 将非数值的值转为数值,再进行判断,而这两个新方法只对数值有效,非数值一律返回 false 。
Number.parseInt(), Number.parseFloat()
ES6将全局方法 parseInt() 和 parseFloat() ,移植到Number对象上面,行为完全保持不变。
Number.isInteger()
Number.isInteger() 用来判断一个值是否为整数。需要注意的是,在JavaScript内部,整数和浮点数是同样的储存方法,所以3和3.0被视为同一个值。
安全整数和Number.isSafeInteger()
JavaScript能够准确表示的整数范围在 -2^53 到 2^53 之间(不含两个端点),超过这个范围,无法精确表示这个值。
ES6引入了 Number.MAX_SAFE_INTEGER 和 Number.MIN_SAFE_INTEGER 这两个
常量,用来表示这个范围的上下限。
Number.isSafeInteger() 则是用来判断一个整数是否落在这个范围之内。
Math对象的扩展
Math.trunc 方法用于去除一个数的小数部分,返回整数部分。
Math.sign 方法用来判断一个数到底是正数、负数、还是零。
Math.cbrt 方法用于计算一个数的立方根。
Math.fround方法返回一个数的单精度浮点数形式。
Math.hypot 方法返回所有参数的平方和的平方根。
数组的扩展
Array.from()
Array.from 方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括ES6新增的数据结构Set和Map)。
实际应用中,常见的类似数组的对象是DOM操作返回的NodeList集合,以及函数内部的 arguments 对象。 Array.from 都可以将它们转为真正的数组。
// NodeList对象
let ps = document.querySelectorAll('p');
Array.from(ps).forEach(function (p) {
console.log(p);
});
// arguments对象
function foo() {
var args = Array.from(arguments);
// ...
}
Array.of()
Array.of 方法用于将一组值,转换为数组。
Array.of(3, 11, 8) // [3,11,8]
数组实例的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]
数组实例的find()和findIndex()
数组实例的 find 方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为 true 的成员,然后返回该成员。如果没有符合条件的成员,则返回 undefined 。
[1, 4, -5, 10].find((n) => n < 0)
// -5
数组实例的 findIndex 方法的用法与 find 方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回 -1 。
数组实例的fill()
fill 方法使用给定值,填充一个数组。
['a', 'b', 'c'].fill(7)// [7, 7, 7]
数组实例的entries(),keys()和values()
ES6提供三个新的方法—— entries() , keys() 和 values() ——用于遍历数组。唯一的区别是 keys() 是对键名的遍历、 values() 是对键值的遍历, entries() 是对键值对的遍历。
函数的扩展
函数参数的默认值
ES6允许为函数的参数设置默认值,即直接写在参数定义的后面。
function log(x, y = 'World') {
console.log(x, y);
}
函数的length属性
指定了默认值以后,函数的 length 属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后, length 属性将失真。
(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2
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参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。
扩展运算符
扩展运算符(spread)是三个点( ... )。它好比rest参数的逆运算,将一个数组转为用逗号分隔的参数序列。
console.log(1, ...[2, 3, 4], 5)// 1 2 3 4 5
// ES5的写法
Math.max.apply(null, [14, 3, 77])
// ES6的写法
Math.max(...[14, 3, 77])
扩展运算符的应用
-
合并数组
var arr1 = ['a', 'b'];
var arr2 = ['c'];
var arr3 = ['d', 'e'];
// ES5的合并数组
arr1.concat(arr2, arr3);// [ 'a', 'b', 'c', 'd', 'e' ]
// ES6的合并数组
[...arr1, ...arr2, ...arr3]// [ 'a', 'b', 'c', 'd', 'e' ] -
扩展运算符还可以将字符串转为真正的数组。
[...'hello']// [ "h", "e", "l", "l", "o" ]
-
实现了Iterator接口的对象
var nodeList = document.querySelectorAll('div');
var array = [...nodeList];
箭头函数
ES6允许使用“箭头”( => )定义函数。
var f = v => v;
//等同于
var f = function(v) {
return v;
};
箭头函数有几个使用注意点
- 函数体内的 this 对象,就是定义时所在的对象,而不是使用时所在的对象。
- 不可以当作构造函数,也就是说,不可以使用 new 命令,否则会抛出一个错误。
- 不可以使用 arguments 对象,该对象在函数体内不存在。如果要用,可以用Rest参数代替。
- 不可以使用 yield 命令,因此箭头函数不能用作Generator函数。
对象的扩展
属性的简洁表示法
ES6允许在对象之中,只写属性名,不写属性值。这时,属性值等
于属性名所代表的变量。
var Person = {
name: '张三',
//等同于birth: birth
birth,
// 等同于hello: function ()...
hello() { console.log('我的名字是', this.name); }
};
Object.assign()
Object.assign 方法用于对象的合并,将源对象(source)的所有可枚举属性,
复制到目标对象(target)。
var target = { a: 1 };
var source1 = { b: 2 };
var source2 = { c: 3 };
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}
如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性
Object.assign 方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。
Object.assign 方法有很多用处。
-
为对象添加属性
class Point {
constructor(x, y) {
Object.assign(this, {x, y});
}
} -
为对象添加方法
Object.assign(SomeClass.prototype, {
someMethod(arg1, arg2) {
···
},
anotherMethod() {
···
}
}); -
克隆对象
function clone(origin) {
return Object.assign({}, origin);
} -
合并多个对象
const merge = (target, ...sources) => Object.assign(target, ...sources);
-
为属性指定默认值
const DEFAULTS = {
logLevel: 0,
outputFormat: 'html'
};
function processContent(options) {
let options = Object.assign({}, DEFAULTS, options);
}
ES6属性的遍历5种方法
- for...in 循环遍历对象自身的和继承的可枚举属性(不含Symbol属性)。
- Object.keys(obj)返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含Symbol属性)。
- Object.getOwnPropertyNames(obj)返回一个数组,包含对象自身的所有属性(不含Symbol属性,但是包括不可枚举属性)。
- Object.getOwnPropertySymbols(obj)返回一个数组,包含对象自身的所有Symbol属性。
- Reflect.ownKeys(obj)返回一个数组,包含对象自身的所有属性,不管是属性名是Symbol或字符串,也不管是否可枚举。
Symbol
ES5的对象属性名都是字符串,这容易造成属性名的冲突。如果有一种机制,保证每个属性的名字都是独一无二的就好了,这样就从根本上防止属性名的冲突。这就是ES6引入Symbol的原因。
注意,Symbol值作为对象属性名时,不能用点运算符。
var mySymbol = Symbol();
var a = {};
a.mySymbol = 'Hello!';
a[mySymbol] // undefined
a['mySymbol'] // "Hello!"
因为点运算符后面总是字符串,所以不会读取 mySymbol 作为标识名所指代的那个值,导致 a 的属性名实际上是一个字符串,而不是一个Symbol值。
Symbol作为属性名,该属性不会出现在 for...in 、 for...of 循环中,也不会被 Object.keys() 、 Object.getOwnPropertyNames() 返回。但是,它也不是私有属性,有一个Object.getOwnPropertySymbols方法,可以获取指定对象的所有Symbol属性名。
Reflect.ownKeys 方法可以返回所有类型的键名,包括常规键名和Symbol键名。
let obj = {
[Symbol('my_key')]: 1,
enum: 2,
nonEnum: 3
};
Reflect.ownKeys(obj)// [Symbol(my_key), 'enum', 'nonEnum']
Symbol.for(),Symbol.keyFor()
有时,我们希望重新使用同一个Symbol值, Symbol.for 方法可以做到这一点。它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的Symbol值。如果有,就返回这个Symbol值,否则就新建并返回一个以该字符串为名称的Symbol值。
Symbol.for("bar") === Symbol.for("bar")// true
Symbol("bar") === Symbol("bar")// false
var s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo"
var s2 = Symbol("foo");
Symbol.keyFor(s2) // undefined
Set和Map数据结构
ES6提供了新的数据结构Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。Set本身是一个构造函数,用来生成Set数据结构。
var s = new Set();
[2, 3, 5, 4, 5, 2, 2].map(x => s.add(x));
for (let i of s) {
console.log(i);
}
// 2 3 5 4
Set实例的属性和方法
- Set.prototype.constructor :构造函数,默认就是 Set 函数。
- Set.prototype.size :返回 Set 实例的成员总数。
- add(value) :添加某个值,返回Set结构本身。
- delete(value) :删除某个值,返回一个布尔值,表示删除是否成功。
- has(value) :返回一个布尔值,表示该值是否为 Set 的成员。
- clear() :清除所有成员,没有返回值。
Map结构的目的和基本用法
JavaScript的对象(Object),本质上是键值对的集合(Hash结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。为了解决这个问题,ES6提供了Map数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键
Map原生提供三个遍历器生成函数和一个遍历方法。
- keys() :返回键名的遍历器。
- values() :返回键值的遍历器。
- entries() :返回所有成员的遍历器。
- forEach() :遍历Map的所有成员。
Generator 函数
Generator函数有多种理解角度。从语法上,首先可以把它理解成,Generator函数是一个状态机,封装了多个内部状态。
执行Generator函数会返回一个遍历器对象,也就是说,Generator函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历Generator函数内部的每一个状态。
形式上,Generator函数是一个普通函数,但是有两个特征。一是, function关键字与函数名之间有一个星号;二是,函数体内部使用 yield语句,定义不同的内部状态(yield语句在英语里的意思就是“产出”)。
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
hw.next()// { value: 'hello', done: false }
hw.next()// { value: 'world', done: false }
hw.next()// { value: 'ending', done: true }
hw.next()// { value: undefined, done: true }
Promise对象
基本用法
var promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
Promise实例生成以后,可以用 then 方法分别指定 Resolved 状态和 Reject 状态的回调函数。
var getJSON = function(url) {
var promise = new Promise(function(resolve, reject){
var client = new XMLHttpRequest();
client.open("GET", url);
client.onreadystatechange = handler;
client.responseType = "json";
client.setRequestHeader("Accept", "application/json");
client.send();
function handler() {
if (this.readyState !== 4) {
return;
}
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
});
return promise;
};
getJSON("/posts.json").then(function(json) {
console.log('Contents: ' + json);
}, function(error) {
console.error('出错了', error);
});
Promise.prototype.then()
then 方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为Resolved时调用,第二个回调函数是Promise对象的状态变为Reject时调用。其中,第二个函数是可选的,不一定要提供。这两个函数都接受Promise对象传出的值作为参数。
then 方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即 then 方法后面再调用另一个 then 方法。
Promise.prototype.catch()
Promise.prototype.catch 方法是 .then(null, rejection) 的别名,用于指定发生错误时的回调函数。
一般来说,不要在 then 方法里面定义Reject状态的回调函数(即 then 的第二个参数),总是使用 catch 方法。
Class
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
上面代码定义了一个“类”,可以看到里面有一个 constructor 方法,这就是构造方法,而 this 关键字则代表实例对象。也就是说,ES5的构造函数 Point ,对应ES6的 Point 类的构造方法。
由于类的方法都定义在 prototype 对象上面,所以类的新方法可以添加在 prototype 对象上面。 Object.assign 方法可以很方便地一次向类添加多个方法。
class Point {
constructor(){
// ...
}
}
Object.assign(Point.prototype, {
toString(){},
toValue(){}
});
另外,类的内部所有定义的方法,都是不可枚举的(non-enumerable)。
constructor方法
constructor 方法是类的默认方法,通过 new 命令生成对象实例时,自动调用该方法。一个类必须有 constructor 方法,如果没有显式定义,一个空的 constructor 方法会被默认添加。constructor 方法默认返回实例对象(即 this ),完全可以指定返回另外一个
对象。
Class的继承
Class之间可以通过 extends 关键字实现继承
class ColorPoint extends Point {}
另一个需要注意的地方是,在子类的构造函数中,只有调用 super 之后,才可以
使用 this 关键字,否则会报错。
Class不存在变量提升(hoist),这一点与ES5完全不同。
new Foo(); // ReferenceError
class Foo {}
Object.getPrototypeOf()
Object.getPrototypeOf 方法可以用来从子类上获取父类。
因此,可以使用这个方法判断,一个类是否继承了另一个类。
super关键字
super 这个关键字,有两种用法,含义不同。
- 作为函数调用时(即 super(...args) ), super 代表父类的构造函数。
- 作为对象调用时(即 super.prop 或 super.method() ), super 代表父
类。注意,此时 super 即可以引用父类实例的属性和方法,也可以引用父类的静
态方法。
Class的静态方法
类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上 static 关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。
编程风格
- let取代var
- 静态字符串一律使用单引号或反引号,不使用双引号。动态字符串使用反引号。
- 函数的参数如果是对象的成员,优先使用解构赋值。
-
单行定义的对象,最后一个成员不以逗号结尾。多行定义的对象,最后一个成员以
逗号结尾。const a = { k1: v1, k2: v2 };
const b = {
k1: v1,
k2: v2,
}; -
数组
使用扩展运算符(...)拷贝数组。const itemsCopy = [...items];
使用Array.from方法,将类似数组的对象转为数组。const foo = document.querySelectorAll('.foo');
const nodes = Array.from(foo); -
函数
立即执行函数可以写成箭头函数的形式。(() => {
console.log('Welcome to the Internet.');
})();
总是用Class,取代需要prototype的操作。因为Class的写法更简洁,更易于理解。
这些笔记只是看第一遍消化的,还有更多的内容需要对研究几遍才行。