ECMAScript6简明学习笔记(待完善)
本文由 小茗同学 发表于 2016-10-31 浏览(2480)
最后修改 2018-06-18 标签:es6

前言

阮一峰的《ECMAScript 6入门》太过详细了,全部看完需要花很长时间,所以,本文只是前者的一个超级精简版。

ES6简介

ECMAScript6.0,简称ES6,又叫ES2015,是JavaScript语言的下一代标准,对JavaScript语法进行了比较大的修改。

对于不支持ES6的浏览器可以将ES6代码用转换工具转换成ES5语法。

ECMAScriptJavaScript的标准,JavaScriptECMAScript的一种实现。

let和const

let

letvar类似,也是用来声明一个变量,不同的是它支持块级作用域。

let相对于var的区别:

  • let支持块级作用域
  • let不会发生变量提升
  • 不允许重复声明(即使之前是用var声明的也不行);
  • let声明的全局变量不属于window的子属性(也就是let a = 1;之后再调用window.a依旧是undefined);
  • 使用let或const声明变量之前该变量都是不可用的,否则报错。

补充:

如果块级作用域中存在letconst命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。总之,在同一代码块内,使用let声明变量之前该变量都是不可用的,语法上称为暂时性死区temporal dead zone,简称TDZ);

const

const用来声明一个只读的常量,一旦声明,常量的值就不能改变,所以const一旦声明变量,就必须立即初始化,否则报错。constlet非常类似,具备上面提到的let具备的所有特点,比如块级作用域、不存在变量提升、暂时性死区、不能重复声明。

但是,const命令只是保证变量名指向的地址不变,并不保证该地址的数据不变,所以下面的代码是没问题的:

const obj = {aa: 1};
obj.bb = 2;
console.log(obj.bb); // 输出 2

所以实际使用中应当尽量避免这种情况,非要将对象声明成常量可以这样:

const obj = Object.freeze({aa: 1});
// 常规模式时,下面一行不起作用;
// 严格模式时,该行会报错
obj.bb = 2;

ES6声明变量的6种方法

ES5只有两种声明变量的方法:var命令和function命令。ES6除了添加letconst命令之外,还有2个:import命令和class命令。所以,ES6一共有6种声明变量的方法。

关于顶级对象

顶级对象,在浏览器环境指的是window对象,在Node指的是global对象。ES5之中,顶层对象的属性与全局变量是等价的。也就是用var声明的全局变量就是window的一个属性(有一点不同,就是var声明的变量无法使用delete删除,window声明的变量可以删除)。

顶层对象的属性与全局变量挂钩,被认为是JavaScript语言最大的设计败笔之一。

ES6为了改变这一点,一方面规定,为了保持兼容性,var命令和function命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。

var a = 1;
console.log(window.a); // 1

let b = 1;
console.log(window.b); // undefined

变量的解构赋值

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

各种类型的解构赋值示例

var [a, b, c] = [1, 2, 3]; // 相当于分别给a、b、c赋值
console.log(a, b, c); // 1, 2, 3

var [x, , y] = [1, 2, 3];
console.log(x, y); // 1,3

var [x, y = 'b'] = ['a']; // 赋默认值,结果:x='a', y='b'
console.log(x, y); // a, b

var [x, y, z] = new Set(["a", "b", "c"]);
console.log(x, y, z); // a,b,c

var {a, b} = {a: "aaa", b: "bbb"}; // 对象解构
console.log(a, b); // 输出 aaa 和 bbb

var [a, b, c, d, e] = 'hello'; // 字符串解构
console.log(a, b, c, d, e); // 分别输出 h、e、l、l、o

// 函数的解构
function add([x, y])
{
	return x + y;
}
add([1, 2]); // 3
  • 如果解构不成功,就会赋予默认值undefined
  • 解构赋值不仅适用于var命令,也适用于let和const命令。
  • 只要某种数据结构具有Iterator接口,都可以采用数组形式的解构赋值,如Set

解构的用途

  • 交换变量的值,如[x, y] = [y, x];
  • 从函数返回多个值,如:function foo(){ return [1, 2, 3];} var [a, b, c] = foo();
  • 提取JSON数据
  • 给函数参数设置默认值
  • 遍历map:
var 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

字符串的扩展

传统的\uxxxx字符表示法只能表示\u0000~\uFFFF之间的字符,超出这个范围的字符会显示异常。ES6可以采用\u{xxxx}的方式来表示超出范围的字符,如\u{20BB7}

本小节有待补充。

正则扩展

新增3个修饰符

在原有gim基础上再增加3个修饰符:

  • u:Unicode 模式,用来正确处理大于\uFFFF的 Unicode 字符;
  • y:“粘连”(sticky)修饰符,与g类似,唯一区别:y确保匹配必须从剩余的第一个位置开始,而g只要剩余位置中存在匹配就可;
  • s:传统情况下,.匹配除换行符以为任意字符。如果加了s修饰符,则匹配包括换行符在内的任意字符。

支持后行断言

另外,JS中一直不支持的后行断言也在ES6中被支持。

具名组匹配

同时增加了具名组匹配,在此之前只能通过索引匹配组,圆括号里面的内容通过索引获取,例如:

/^(\d+?)-(\d+?)-(\d+)$/g.exec('2018-06-05'); // ["2018-06-05", "2018", "06", "05"]

es6新增?<name>的方式来给组命名,例如:

var result = /^(?<year>\d+?)-(?<month>\d+?)-(?<day>\d+)$/.exec('2018-06-05');
console.log(result.groups.year);
console.log(result.groups.month);
console.log(result.groups.day);

数值的扩展

函数的扩展

  • 新增函数默认值,例如:function log(x, y = 'World') {}
  • 新增可变参数列表,形式为...变量名,又叫rest参数,和Java非常类似,只能作为最后一个参数,与arguments不同的是它是一个真正数组而不是伪数组;
  • 箭头函数,例如: var fn = a => a*2,主要优点是绑定this。

下面2个变化仅作为了解,不算特性:

  • 函数的 length 属性变化,不计入默认参数和rest参数的个数;
  • 严格模式变化,只要函数参数使用了默认值、解构赋值、或者扩展运算符就不能主动设置'use strict'

关于箭头函数:

  • 箭头函数会绑定其所在作用域的this,其父作用域的this指向哪里,它本身的this就指向哪里。。
  • 不可以当作构造函数,也就是说不能拿来new,否则报错;
  • 不可以使用arguments对象,该对象在函数体内直接不存在,如果非要使用,可以用rest参数代替。
  • 不可以使用yield命令,因此箭头函数不能用作Generator函数。

数组的扩展

新增了一些方法,个人觉得比较常见的是...扩展运算符以及Array.from

...扩展运算符可以看成是rest参数的逆运算,前者将数组拆散,后者将分散的变量合并为一个数组:

function fn(a, b, ...c) {console.log(a, b, c);}
fn(...[1, 2, 3, 4]); // 1, 2, [3, 4]

最常见用途是替代apply方法,ES6以前要将数组拆散传给函数执行只能使用fn.apply(this, [arg1, arg2]),ES6直接可以fn(...[arg1, arg2])

常见示例:

  • 求最大值,ES5:Math.max.apply(null, [1,4,2]),ES6:Math.max(...[1,4,2])
  • 复制数组,ES5:var b = [1,2,3].concat(),ES6:var b = [...[1,2,3]]
  • 合并数组,ES5:var b = [1,2,3].concat([4,5]),ES6:var b = [1,2,3].push(...[4,5]),或者var b = [...[1,2,3], ...[4,5]]

对象的扩展

属性简写

  • 普通变量简写:var a = 1; var obj = {a};
  • 函数简写:var obj = { fn(){} };
  • get和set简写:
var obj = {
	_test: 123,
	get test() {return this._test},
	set test(value) {this._test = value;}
}

属性名表达式

例如:var a = 'test'; var obj = {[a+'_abc']: 123};

新增的几个方法

  • Object.is(a, b),比较2个值是否相等,和===的区别只有2个:+0-0不相等,NaNNaN相等;
  • Object.assign(target, ...source),作用:将source的所有可枚举属性(包括Symbol属性)复制到target并返回target,缺点是不能深拷贝;
  • Object.setPrototypeOf(obj, prototype) 等价于obj.__proto__ = prototype
  • Object.getPrototypeOf(obj) 等价于return obj.__proto__
  • Object.values,同ES5的Object.keys类似,也是遍历自身可枚举属性,只不过返回的是value数组;
  • Object.entries,返回一个二维数组,形如[[key1, value1], [key2, value2]]

除此之外,ES6中,__proto__被写入标准,浏览器环境必须部署这个属性,其它环境非必须,但是一般不推荐直接操作这个属性。

遍历和可枚举性

普通方式定义的属性都是可枚举的,要将某个属性设置成不可枚举必须使用Object.definePropertyenumerable设置为false

5种遍历对象属性的方法:

  1. for in:遍历自身和原型上的可枚举属性(不含Symbol属性);
  2. Object.keys(obj):遍历自身可枚举属性(不含原型上的,以及Symbol属性);
  3. Object.getOwnPropertyNames(obj):遍历自身所有字符串属性,包括不可枚举(不含原型上的,以及Symbol属性);
  4. Object.getOwnPropertySymbols(obj):遍历自身所有Symbol属性,包括不可枚举(不含原型上的,以及字符串属性);
  5. Reflect.ownKeys(obj):遍历自身所有属性,包括Sybol属性,包括不可枚举(不含原型上的);

可见,以上5种方法除了for in会遍历原型上的方法需要特别记忆,其它方法都只遍历自身属性。

测试:

var obj = {a: 1, [Symbol('b')]: 2};
Object.prototype.c = 3; // 这里偷懒,直接修改Object的原型
Object.defineProperty(obj, 'd', {
	value: 4,
	enumerable: false
})
Object.defineProperty(obj, Symbol('e'), {
	value: 5,
	enumerable: false
})
for(var i in obj) console.log(i); // a c
Object.keys(obj); // ['a']
Object.getOwnPropertyNames(obj); // ['a', 'd']
Object.getOwnPropertySymbols(obj); // [Symbol(b), Symbol(e)]
Reflect.ownKeys(obj); // ['a', 'd', Symbol(b), Symbol(e)]

运行结果如下:

除此之外:

  • JSON.stringify():只串行化对象自身的可枚举的属性;
  • Object.assign():只遍历自身可枚举属性;

super关键字

this代表当前对象,super表示对象的原型,等价于obj.__proto__,目前只能用在对象的简写方法中var obj = { fn(){return super.xxx;} };,直接写对象里面、普通函数写法、或者箭头函数都报错:

// 报错
const obj = {
	foo: super.foo
}

// 报错
const obj = {
	foo: () => super.foo
}

// 报错
const obj = {
	foo: function () {
		return super.foo
	}
}

对象的扩展运算符

我们已经知道数组有扩展运算符,ES2018又将扩展运算符引入了对象,语法基本类似:

对象属性合并:

var { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
console.log(x, y, z); // 1, 2, {a: 3, b: 4}

对象属性拆解:

let a = {a: 3, b: 4}, b = {x:1, y: 2};
let target = {...a, ...b};
console.log(target); // {a: 3, b: 4, x: 1, y: 2},等价于 Object.assign({}, a, b)

Symbol

ES6新增一种原始数据类型Symbol,表示独一无二的值。它是JS的第七种数据类型,前六种是:undefinednullBooleanStringNumberObject。我们可以把它大致看成是一种特殊的字符串,这种字符串是唯一的,没发重复生成的。

  • 它由Symbol函数生成,接收一个可省的key参数,这个参数仅仅是用来标识,没其它用途。
  • 每次都会生成一个唯一的值;
  • for infor of等常规方法无法遍历Symbol属性,必须通过Object.getOwnPropertySymbols或者Reflect.ownKeys才能获取到。
var a = Symbol('a');
console.log(a); // Symbol(a)
typeof a; // 'symbol'
Symbol('a') === Symbol('a'); // false
Symbol('a').toString(); // 'Symbol(a)'

Symbol.for():首先会看全局环境有没有注册相同描述的Symbol值,如果有直接返回,否则创建一个新的Symbol并在全局环境注册,这个全局环境包括iframe。

大致原理如下(仅仅是模拟类似效果):

Sumbol.for = function(key) {
	var result = window.symbolKeys[key] || Symbol(key);
	window.symbolKeys[key] = result;
	return result;
}

Symbol.keyFor(symbol):返回一个由Symbol.for生成的Symbol的key值,如果是由Symbol()生成的则返回undefined

除此之外,ES6 还提供了 11 个内置的 Symbol 值,指向语言内部使用的方法,这里只介绍Symbol.hasInstanceSymbol.iterator

ES6以前,A instanceof B的判断逻辑我们一般认为是闲着A的原型链往上找,如果匹配到了B就返回true,但是ES6新增一条规则:如果B定义了名为Symbol.hasInstance的Symbol方法,就以这个方法返回的结果为准:

var Test = {
	[Symbol.hasInstance](obj) {
		return true; // 任何遍历和Test做instanceof比较都返回true
	}
};
123 instanceof Test; // true
'abc' instanceof Test; // true
/a/g instanceof Test; // true

一个对象只要按照要求部署了名为Symbol.iterator的方法就可以被for of遍历,详见后文关于Iterator部分。

Set和Map

Set

Set是一种类似数组的新的数据结构,叫集合,最大特点是内容必须是唯一的。形式上Set是一个构造函数,接收一个可选的数组参数初始化,通过add方法添加内容,delete方法删除内容。

属性:

  • size:返回成员的个数,注意set获取长度是size而不是length,而且size是一个属性而不是一个方法;

方法:

  • set.add(value):添加值,如果已存在则不添加,返回自身,所以add()可以链式调用,Set判断重复的逻辑和===基本类似,唯一区别是2NaN相等(注意和Object.is(a, b)也有差别,+0-0会被认为是同一个值);
  • set.delete(value):删除某个内容,返回是否删除成功;
  • set.has(value):判断set中是否存在某个value;
  • set.clear():清空set;

示例:

var set = new Set([1, 2, 3]); // 参数为可选
set.add(4).add(5); // 可以链式调用
set.add(4); // 故意添加一个重复元素
set.size; // 4,

遍历(顺序就是插入顺序):

  • set.keys():返回键名的遍历器,Set的键值比较特殊,它的key其实就是value,所以set.keys()set.values()效果完全相同。
  • set.values():返回键值的遍历器;
  • set.entries():返回所有成员的遍历器,内容为[value, value](因为key和value相同)。
  • set.forEach()Set也可以像数组一样用forEach遍历set.forEach((value, key) => {});
  • for ofSet内部已经实现了迭代器接口,所以可以使用for of遍历,也可以用...进行扩展,[...new Set(array)]是最简单的数组去重方法。

一般用for of遍历就足够了,其实for of内部调用的就是set.values()

Map

ES6新增一种名叫Map的数据结构,类似于对象,对象为字符串->值的映射,而Map为值->值的映射,也就是Map的key可以是任意对象,而不仅仅是字符串。Map通过set添加值,get取值,可以接收一个形如[[key1, value1], [key2, value2]]的二维数组来初始化。

var map = new Map([
	['name', 'tom'],
	['age', 18]
]);
var key = {a: 1};
map.set(key, 'aaa'); // 对象做键值
map.get(key); // 'aaa'

属性:

  • size:返回成员的个数;

方法:

  • map.set(key, value):设置值,如果已经存在则覆盖;
  • map.get(key):取值,不存在则返回undefined
  • map.delete(key):删除某个key,返回是否删除成功;
  • map.has(key):判断map中是否存在某个key;
  • map.clear():清空map;

遍历(顺序为插入顺序):

  • map.keys():返回键名的遍历器。
  • map.values():返回键值的遍历器。
  • map.entries():返回所有成员的遍历器。
  • map.forEach():遍历Map的所有成员。
  • for of:一般格式为:for (let [key, value] of map) {}

与数组互转:

  • 二维数组转Map:new Map([二维数组)
  • Map转二维数组:[...map.entries()]

WeakSet和WeakMap

WeakSet根据字面意思理解为弱集合WeakSetSet基本类似,只有2个区别:

  1. 只能存放对象,存放其它类似会报错;
  2. WeakSet对它里面的对象都是弱引用,GC回收时不考虑 WeakSet对它的引用,也就是,只要这个对象没有被其它对象引用GC就会回收它。

所以它里面的内容随时可能消失,也因此它没有size属性,而且它不能遍历。

WeakMapWeakSet类似,也是键值只能是对象,弱引用,这里不再详述。

Proxy

Proxy代理,作用是给目标对象设置拦截器,从语言层面拦截一些默认行为。

语法:

// target:要拦截的对象
// handler:拦截器,也是一个对象,用来定制拦截行为
var obj = new Proxy(target, handler);

下面的实例为拦截对象的get方法:

var obj = new Proxy({}, {
	get: function(target, property) {
		return '小茗同学很帅';
	}
});
obj.time; // '小茗同学很帅'
obj.name; // '小茗同学很帅'

Proxy一共可以拦截13种默认行为:

  • get(target, propKey, receiver):拦截对象属性的读取,比如proxy.fooproxy['foo']
  • set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = vproxy['foo'] = v,返回一个布尔值。
  • has(target, propKey):拦截propKey in proxy的操作,返回一个布尔值。
  • deleteProperty(target, propKey):拦截delete proxy[propKey]的操作,返回一个布尔值。
  • ownKeys(target):拦截Object.getOwnPropertyNames(proxy)Object.getOwnPropertySymbols(proxy)Object.keys(proxy)for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
  • getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
  • defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)Object.defineProperties(proxy, propDescs),返回一个布尔值。
  • preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。
  • getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象。
  • isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。
  • setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
  • apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)proxy.call(object, ...args)proxy.apply(...)
  • construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)

Reflect

Reflect,中文含义是反射,熟悉Java的一听到这个词应该知道这个大概是个什么东西了。它是一个普通对象,下面放置了一些和语言层面关联较大的静态方法,其设计目的主要是:

  • 将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上;
  • 让Object操作都变成函数行为;
  • Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。前面说了,Proxy一共可以拦截13种默认行为,所以Reflect下面也有13个静态方法。

这13个方法是:

  • Reflect.apply(target, thisArg, args)
  • Reflect.construct(target, args)
  • Reflect.get(target, name, receiver)
  • Reflect.set(target, name, value, receiver)
  • Reflect.defineProperty(target, name, desc)
  • Reflect.deleteProperty(target, name)
  • Reflect.has(target, name)
  • Reflect.ownKeys(target)
  • Reflect.isExtensible(target)
  • Reflect.preventExtensions(target)
  • Reflect.getOwnPropertyDescriptor(target, name)
  • Reflect.getPrototypeOf(target)
  • Reflect.setPrototypeOf(target, prototype)

Promise

Promise是异步编程的一种解决方案,其本质可以看成是一个状态机,一个容器,里面保存着某个未来才会执行的操作。Promise实例有3种状态:pendingresolvedrejected,状态不可逆。

Promise是一个构造函数,接收一个函数作为参数:new Promise(function(resolve, reject){});,这个函数有2个参数,resolve()方法将状态从pending变为resolvedreject()方法将pending变为rejectednewPromise之后这个函数会立即执行。

下文中,小写的promise代表Promise实例,大写的Promise表示原始对象。

  • promise.then:为promise实例添加状态改变的回调函数promise.then(success, failed),第二个失败回调可以省略,返回一个新的promise实例;
  • promise.catch:它是.then(null, failed)的别名,promise的错误会被最近的一个catch捕获;
  • Promise.all([promiseList]):将多个promise对象包装成一个新的promise对象,只有所有promise实例状态都变成resolved才会触发成功回调(参数是所有实例结果组成的一个新数组),只要有一个实例状态变成rejected就会触发失败回调。
  • Promise.race([promiseList]):哪个实例对象状态最先发生改变就以哪个为准,即使状态是变成rejected
  • Promise.resolve():将某个对象转化成promise实例,立即返回一个状态为resolved的promise对象。特别注意,Promise.resolve()在本轮“事件循环”结束时执行,而setTimeout(fn, 0)在下一轮“事件循环”开始时执行。
  • Promise.reject():立即返回一个状态为rejected的promise对象,与Promise.resolve不同的是,其参数会原封不动地作为reject的理由,变成后续方法的参数。

可以发现,除了promise.thenPromise.allPromise.race这3个方法之外,其余的都是语法糖,可以通过其它方式变相实现。

特别注意:通过new Promise()返回的promise实例,其回调函数里面必须调用resolve或者reject方法,否则then永远不会被触发,但是,通过then返回的promise实例,即使回调函数不返回任何内容,新实例的then也会被触发,因为其状态默认就是resolved。

new Promise(function(resolve, reject) {}).then(data => console.log(data)); // then方法永远不会执行
new Promise(function(resolve, reject) {
	resolve(123);
}).then(data => console.log('第一个then', data))
.then(data => console.log('第二个then', data)); // 2个then都会被触发

Iterator

迭代器Iterator(也叫遍历器)是一种接口,为各种不同的数据结构提供统一的访问机制,主要是供ES6新增的for of使用。迭代器本质上可以看成是一个指针对象,指向当前迭代的索引。

一个数据结构只要具有Symbol.iterator属性(原型上有也可以),就可以认为是可遍历的(iterable)。Symbol.iterator必须是一个函数,返回一个对象(也就是前面说的迭代器),这个对象必须要有一个next方法,返回类似{value:1, done: false}的内容(done为true表示遍历完毕)。

数组、类数组(如argumentsNodeList等)、MapSet等自带Symbol.iterator属性,所以这些数据结构默认就可以使用for of遍历。对象默认不能使用for of遍历,因为它没有部署Symbol.iterator属性,之所以没有部署主要有2个原因,一是因为对象属性的顺序是不确定的,需要开发者自行指定,二是因为对于需要遍历的场景,Map完全可以替代Object,所以,为Object部署迭代器属性不是很必要。

让普通对象可以遍历:

var obj = {a:1, b:2, c: 3};
obj[Symbol.iterator] = function() {
	var keys = Object.keys(this), idx = 0;
	return {
		next: () => {
			return idx < keys.length ? {value: [keys[idx], this[keys[idx++]]]} : {done: true};
		}
	}
}
// 测试
for(let [key, value] of obj) console.log(key, value);

遍历器对象除了具有next方法,还可以具有return方法和throw方法。return方法主要在breakcontinuethrow这几种场合被调用。throw方法主要是配合Generator函数使用,详见后文。

调用Iterator的场合

除了for of之外,还有一些地方会用到迭代器,如:

  • for of
  • 扩展运算符...,如[...array]
  • 解构赋值:对数组和 Set 结构进行解构赋值,如var [a, b, c] = new Set([1, 2, 3])
  • yield*:如yield* [2,3,4]
  • 使用数组做参数的场合,如:Array.from()Promise.all()new Set()等;

Generator语法

Generator函数是ES6提供的又一种异步编程解决方案(前一种是Promise)。