ECMAScript 2015版本开始,JavaScript开始变得好玩起来。
本文学习阮一峰老师的《ECMAScript 6 入门》的笔记,有兴趣可以直接查看他的原书,感谢阮老师开源。内容主要为ES2015部分。
1、let和const关键字
let声明的变量在块级作用域内有效,且不允许在相同作用域重复声明同一个变量。不存在变量提升,存在暂时性死区,须先声明再使用。如果未声明就使用,直接报错,包括使用typeof进行判断。
// 块作用域
{
let x = y; // Error
let y = 10;
var y = 20; // Error
var z = 30;
}
y // Error
z // 30
let可以避免变量从未声明就使用导致的全局污染情况,也给闭包内循环时变量使用同一块内存提供了一种解决方案。
var arr = []
for(var i = 0; i < 10; i++) {
arr[i] = function() { console.log(i) }
}
i // 10 泄漏成全局变量
arr[6] // 10 使用匿名IIFE可解决
let arr2 = []
for(let i = 0; i < 10; i++) {
arr2[i] = () => console.log(i)
}
i // Error
arr2[6] // 6
块级作用域对变量有效,对函数也是有效的。在JS里,函数也是特殊的变量,ES6对函数使用块级作用域才显得正常。但是ES5块级作用域是不允许声明函数的,ES6规定允许在块级作用内声明函数,声明类似var的作用域,还会提升到所在块级作用域的头部。因此,不建议在块级作用域内声明函数,即便声明也应该写函数表达式,而不是函数声明语句。
{
function f1() {} // 不建议
let f2 = () => {} // 建议
}
ES6的块级作用域必须有大括号。
if(true) let x = 1; // error
if(true) { let x = 1; }
const声明一个只读常量,声明之后不能更改。其他特性与let关键字基本相同。
const常量声明是不能保证的。简单数据的指针所指向的内存比较单一,因此等同于常量。而复合类型的数据(主要是对象和数组)所指的内存只是针对这个类型,里面的数据结构的可变性是不能控制的。如果要冻结对象,应该使用object.freeze。
2、变量的解构赋值
解构赋值本质上就是“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋值,允许不完全解构。
let [a, [b], c] = [1, [2, 3], 4]
数组的解构赋值允许有缺省值,缺省值可以是变量、表达式。缺省值是变量须注意是否已声明。缺省值是表达式是惰性求值,只有在用到的时候才会计算值。
// 缺省值
let [x, y = 'b'] = ['a']
// 缺省值是变量
let [x = 1, y = x] = [] // x = 1; y = 1
let [x = y, y = 1] = [] // error
// 缺省值是表达式
let f = () => 'b'
let [x = f()] = [1]
对象的解构赋值与数组的本质是一样的,可以有缺省值。ES6对对象进行了扩展,在不需要改变接收变量的时候,通常都可以简写。
// 完整写法
let { log: log, sin: sin, cos: cos } = Math
// 简写,通常这样写
let { log, sin, cos } = Math
// 假如将console解构,变量冲突
// key保持与右边一直,接收变量自定义
let { log: print } = console
// 缺省值
let {x: y = 3} = {}
解构赋值只要按着规矩来,一般不会出问题。如果要整些骚操作,要作死,那么谁也拦不住。使用已经声明的变量解构,必须小心。
let x
{x} = {x: 1} // error 大括号在行首,左边的x被引擎解释为在代码块内
let y
({y} = {y: 1})
除字符串(类数组)之外其他数据类型解构赋值时会先转为对象再解构,如果无法转换就会直接报错。解构赋值应用到函数参数上,提取数据尤为有用,加上缺省参数,代码会变得更加清晰。
3、字符串的扩展
ES6加强了对unicode的支持,只要将码点放入大括号就能正确结对该字符。
'\u{1F680}' === '\uD83D\uDE80'
ES6为字符串添加了遍历器接口,可以使用for...of循环遍历,遍历器可识别大于0xFFFF的码点。
模版字符串是增强版的字符串,用反引号(`)标识,可以是普通字符串,可以定义多行字符串,可以使用(${})嵌入变量、任意的JavaScript表达式包括函数进行运算以及对象属性。它能替代字符串使用+号来组合字符串。
标签模版,模版字符串紧跟在一个函数名后面,该函数将被调用来处理这个模版字符串。它不是模版,而是函数调用的一种特殊形式。
4、正则的扩展
ES6添加u修饰符,用来处理大于\uFFFF的Unicode字符。
添加y修饰符(粘连修饰符),与g修饰符类似,y修饰符是从匹配必须从生效与的第一个位置开始,g只要剩余位置中存在匹配即可。
5、数值的扩展
ES6提供了二进制和八进制的新写法,分别用前缀0b/0B和0o/0O表示。
在Number对象上新增一些方法,包括一些全局方法,如isNaN()可用Number.isNaN(),parseInt()可用Number.parseInt()。
Math对象上新增了了些方法,如Math.trunc()去除一个小数的小数部分返回整数,和parseInt()类似。
新增指数运算符(**),和等号结合(**=)。
6、函数的扩展
ES6允许函数的参数设置默认值,通常会与解构赋值结合起来。缺省参数一般放在参数尾部,如果不这样做,在某些时候,这个参数是没法省略的。这是ES6语法,也就意味着,在作用域内它不能再被let声明。
ES6引入rest参数(...变量名),用于获取函数的多余参数,相当于arguments对象的作用。rest参数之后不能再有其他参数,否则会报错。函数的length属性不包括rest参数。
ES5规定函数内部可设定为严格模式,ES6规定函数参数使用默认值、解构赋值或者扩展运算符,内部不能设定严格模式,否则会报错。
箭头函数,使用=>定义,不需要参数或多个参数就使用圆括号代表参数部分,如果箭头函数多余一条语句就需要用大括号括起来,使用return语句返回值。当返回体是单语句且是对象,必须在对象外部套一个括号,否则会得到错误的反馈。
TIPS:箭头函数本身没有this,它的this是定义时所在的上下文,而不是使用时所在的上下文;不是构造函数不能使用new;不存在arguments,但可以使用rest参数;不能使用yield命令,不能作为Generator函数。
7、数组的扩展
扩展运算符(...),好比是rest参数的逆运算,将数组转为用逗号分隔的参数序列,可用在具有Iterator接口的对象上。
// ES5
function f(x, y, z) {}
var args = [0, 1, 2]
f.apply(null, args)
// ES6
let f = (x, y, z) => {}
let args = [0, 1, 2]
f(...args)
// ES5
var arr1 = [0, 1, 2]
var arr2 = [3, 4, 5]
Array.prototype.push.apply(arr1, arr2)
// ES6
let arr1 = [0, 1, 2]
let arr2 = [3, 4, 5]
arr1.push(...arr2)
数组还扩展了一些实用的方法,如Array.from(),将类数组转为真数组
let arrayLike = { '0': 'a', '1': 'b', length: 2 }
// ES5
var arr1 = [].slice.call(arrayLike)
// ES6
let arr2 = Array.from(arrayLike)
8、对象的扩展
属性简洁表示,属性和值用同一个变量名时可只写属性名。
const foo = 'bar'
// 全写
const baz = {foo:foo}
// 简写
const baz = {foo}
方法名可以省略function关键字
// 全写
const o = {
method: function() {}
}
// 简写
const o = {
method() {}
}
JS定义属性的两种方法:obj.key或obj[key],后一种常用在变量的情况,在定义对象时只能用前一种。ES6允许属性名可以用字面量定义对象,变量写入中括号,当变量的值与属性对应的值相同时不能简写。
let propKey = 'foo'
let obj = {
[propKey]: propKey,
[propKey] // error
}
this关键字指向函数所在的对象,ES6新增super关键字指向当前对象的原型对象。super关键字表示原型对象时只能用在对象的方法之中。
Object对象新增了一些实用方法:Object.is()、Object.assign()等等。
9、Symbol类型
Symbol值通过Symbol函数生成,表示独一无二的值,现在对象的属性名有两种类型:字符串、Symbol。Symbol不能与其他类型的值进行运算,但可以显示转为字符串。Symbol可以转为布尔值,但是不能转为数值。Symbol的每一个值是不相等的,意味着可以作为标识符用于对象的属性名,保证不会出现相同的属性名。
在很多博客中都在COPY阮老师的一个用Symbol生成唯一常量的例子。这其实挺悲哀的,以前我查资料的时候发现,很多人直接COPY别人的博客,翻来翻来翻去就是一篇文章,完全不带思考能力的。
// 阮老师的例子
const COLOR_RED = Symbol()
const COLOR_GREEN = Symbol()
let getComplement = color => {
switch(color) {
case COLOR_RED:
return COLOR_RED;
case COLOR_GREEN:
return COLOR_GREEN;
default:
throw new Error('Undefined color');
}
}
假如这个方法定义在a文件,调用在b文件,就不能用了。Symbol()似乎更适合在私有属性中使用,它是不能共享的,Symbol.for()和没用Symbol之前没太大差。我想如果分模块的话,可以将这个局部常量抛出去或在全局定义这个常量,这样或许可行。
10、Set和Map数据结构
Set本身是一个构造函数,类似于数组,但成员的值是惟一的。
// 数组去重
const set = new Set([1, 2, 3, 4, 4])
[...set] // [1, 2, 3, 4]
// 去重复字符
[...new Set('abbabc')].join('') // 'abc'
WeakSet与Set类似,但WeakSet的成员只能是对象,且都是弱引用,垃圾回收机制不考虑WeakSet对该对象的引用,不能被遍历。通常用它来临时存放一组对象,以及存放与对象绑定的信息。
Map也是一个构造函数,类似于对象,但Map的Key不局限于字符串,对同一个Key多次赋值,后面的值会覆盖前面的值。当Key是一个对象时,只有对同一个对象的引用才会被视为同一个键。
WeakMap与Map类似,但WeakMap的Key只接受对象,所指向的对象不计入垃圾回收机制。
11、Proxy
Proxy用于修改某些默认行为,等同于在语言层面做修改,属于一种“元编程”。Proxy相当于在对操作目标做了一层拦截。
12、Reflect
Reflect对象可以拿到语言内部的方法,修改某些object方法的返回结果让其变得合理,让object操作变成函数行为,与Proxy对象的方法对应。
13、Promise
Promise是异步编程的一种解决方案,比传统的回调、事件、发布订阅更为合理,避免了回调地狱的问题。Promise对象是一个构造函数,用来生成实例。
// 定义Promise实例
const getJSON = url => {
return new Promise((resolve, reject) => {
const handler = () => {
if(this.readState !== 4) return
this.status == 200 ? resolve(this.response) : 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()
})
}
// 生成实例后then方法进行处理
getJSON ('/posts.jsoin').then(json => {
// success
}, error => {
// failure
// 一般不需要这个回调函数,该用catch来处理
}).catch(error => {
// failure
}).finally(() => {
// 一定会执行
})
Promise.all()/Promise.race()将多个Promise实例包装成一个新的Promise实例。
Promise.resolve()/Promise.reject()返回一个新的Promise实例。
14、Generator
Generator函数是另一种异步编程解决方案,是一个内部封装了很多状态的状态机,function和函数名之间有一个星号,内部使用yield表达式定义不同的内部状态。
function* generator() {
yield 'hello';
yield 'world';
return 'ending';
}
let g = generator();
g.next() // {value:'hello', done:false}
g.next() // {value:'world', done:false}
g.next() // {value:'ending', done:true}
g.next() // {value:undefined, done:true}
Generator函数通过next方法将指针移到下一个状态,next传参表示的是上一个yeild表达式的返回值。
function* foo(x) {
let y = 2 * (yield (x + 1)
let z = yield (y / 3)
return (x + y + z)
}
let a = foo(5)
a.next() // {value: 6, done: false} x = 5
a.next() // {value: NaN, done: false} y = 2 * undefined
a.next() // {value: NaN, done: true} 5 + NaN + undefined
let b = foo(5)
b.next() // {value: 6, done: false} x = 5
b.next(12) // {value: 8, done: false} y = 2 * 12
b.next(13) // {value: 42, done: true} 5 + 24 + 13
通过throw方法在函数题外抛出错误,在Generator函数体内捕获。
通过return方法返回给定的值,并终结遍历Generator函数。
yield*表达式,用来在一个Generator函数里执行另一个Generator函数。
作为对象属性的Generator函数,可以简写成下面形式。
let obj = {
*generator() {
// ...
}
}
Generator函数返回一个遍历器,这个遍历器是Generator函数的实例,也继承了Generator函数的prototype对象上的方法,也不能跟new关键字一起用。
// 将Generator函数编程一个正常的实例对象
function* Generator() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
// let obj = {};
// let f = Generator.call(obj);
// let f = Generator.call(Generator.prototype);
let F = () => Generator.call(Generator.prototype);
let f = new F();
f.next(); // {value: 2, done: false}
obj.a // 1
Generator函数应用:异步操作的同步化表达,Generator的暂停执行效果,减少回调处理;控制流管理,避免回调地狱和大量Promise的语法;在任意对象上部署Iterator接口;当作一个数组结构。
15、async
async函数就是Generator函数的语法糖,将(*)替换成了关键字async,yield替换成了await。当然,async做了优化,内置执行器,返回值是Promise。async函数须等到await异步操作执行完成才返回结果,如果await后的Promise对象抛错,后面的操作就会终止。如果不希望终止,可以在awai后面的Promise对象后做catch拦截,或将语句放入try...catch中。如果不存在继发关系,可以用Promise.all同时触发。
16、Iterator和for...of
Iterator为各种数据结构提供统一简单的访问接口,使得其成员能够按某种次序排列,主要供for...of消费。Iterator部署在数据结构的Symbol.iterator属性,只要具有Symbol.iterator属性就认为是可遍历。Symbol.iterator本身是一个函数,即当前数据结构默认的遍历器生成函数。执行这个属性会返回一个遍历器对象,根本特征是具有next方法。
遍历器对象除了具有next方法,还可以具有return方法和throw方法。如果for...of循环提前退出就会调用return方法,必须返回一个对象。throw方法主要是配合Generator函数,一般遍历器对象用不到这个方法。
for...of循环可以替代数组的forEach方法,forEach不能中断。for...of不能遍历没有部署Iterator的对象,可以利用entries()/keys()/values()达到遍历的目的。
17、Class
JS传统的类的定义方法是通过构造函数,方法封装在构造函数内部或prototype原型链上。
function Point(x, y) {
this.x = x;
this.y = y;
this.distance = function() {
return Math.sqrt(Math.pow(this.x, 2) + Math.pow(this.y, 2));
}
}
Point.prototype.toString = function() {
return '(' + this.x + ', ' + this.y + ')';
}
ES6提供了class关键字,可以理解为一个语法糖,生成实例的时候都需要用new,所有方法都在prototype上面,原型链上都有constructor。ES如果没有显式添加constructor,默认会添加一个空的constructor。class定义的方法不可枚举,ES5写法可以。
在类内部可以使用get和set关键字,对某个属性设置存取值函数,它们都在属性的Descriptor对象上。实例属性可以定义在构造函数的this上面,也可以定义在类的最顶层,此时可以省略this。
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
this.distance = () => {
return this.sqrt(Math.pow(this.x, 2) + Math.pow(this.y, 2));
}
}
toString() {
return `(${this.x}, ${this.y})`;
}
}
typeof Point // "function"
Point === Point.prototype.constructor // true
let p1 = new Point(2, 4);
p1.constructor === Point.prototype.constructor // true
let p2 = new Point(6, 8);
p1.__proto__ === p2.__proto__ // true
在方法前加上static关键字表示静态方法,该方法不会被实例继承,通过类直接调用。静态方法可以被子类继承,也可以从super对象上调用。
ES6在类的定义上还存在缺陷,缺少私有属性和私有方法的概念,尽管其他方式来达到私有的目的。
Class可以通过extends关键字来实现继承,子类必须在constructor方法中调用super方法才能使用this关键字。super关键字既可以作为函数,也可以当做对象。
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y);
this.color = color;
}
toString() {
return `${this.color} ${super.toString()}`;
}
}
Object.getPrototypeOf(ColorPoint) === Point // true
Class同时拥有prototype和__proto__,子类的__proto__表示构造函数的继承,总是指向父类,子类的prototype的__proto__表示方法的继承,总是指向父类的prototype。
ColorPoint.__proto__ === Point // true
ColorPoint.prototype.__proto__ === Point.prototype // true
子类实例的__proto__的__proto__指向父类实例的__proto__,也就是子类的原型的原型是父类的原型。
let p = new Point(2, 3);
let cp = new ColorPoint(2, 3, 'red');
cp.__proto__ === p.__proto__ // false
cp.__proto__.__proto__ === p.__proto__ // true
Mixin指的是多个对象合成一个新的对象,新对象具有各个组成成员的接口。
18、Module
模块功能主要有两个关键字构成:export和import,export用于模块的接口导出,import用于输入模块提供的功能。
// profile.js
export let firstName = 'Michael';
export let lastName = 'Jackson';
export let year = 1958;
// 与上面写法等价
let firstName = 'Michael';
let lastName = 'Jackson';
let year = 1958;
export {firstName, lastName, year};
import可以使用as关键字将输入的变量重命名,也可以通过解构赋值来达到目的。import模块可以省略.js后缀。除了指定加载某个输出值,还可以使用整体加载,即使用星号(*)指定一个对象。import()加载成功后会作为一个对象当作then方法的参数,可以使用对象结构赋值的语法输出接口,default可以用参数直接获得。同时加载多个模块可以采用Promise.all()。
import {firstName: surname} from './profile'
import {firstName as surname} from './profile'
// 整体加载
import * as profile from './profile'
import 'loadsh'
为了用户可以不用阅读文档就能加载模块,可以使用export default为模块指定默认输出。
// 默认模块
import _ from 'loadsh'
// 默认模块和其他模块
import _, {each, forEach} from 'lodash'
在一个模块中先输入后输出一个模块,import语句可以和export语句写在一起。
export {foo, bar} from 'my_module'
// 可以理解为
import {foo, bar} from 'my_module'
export {foo, bar}
// 具名接口改为默认接口
export {es6 as default } from './someModule'
// 默认接口改为具名接口
export {default as es6 } from './someModule'
// export * 会忽略default
export * from 'circle'
export let e = 2.71828182846
export default x => Math.exp(x)
跨模块常量,可以专门建一个或者多个文件模块来共享这些值。
TIPS:ES2015及之后的新版本在浏览器的部署上不是同步的,如果要使用就需要通过转码器编译成ES5的通行标准。大家普遍都是通过babel来完成转换,编译后的代码在大多数浏览器时可以执行的。如果还需要兼容像IE这种货色,这还需要polyfill等方式来解决。
最后关于ES6版本的说法,我更倾向阮一峰老师##传送门##:
ES6 既是一个历史名词,也是一个泛指,含义是 5.1 版以后的 JavaScript 的下一代标准,涵盖了 ES2015、ES2016、ES2017 等等,而 ES2015 则是正式名称,特指该年发布的正式版本的语言标准。