这是笔者读完阮一峰的《ES6标准入门》的总结,可称为《ES6标准入门的入门》,总结的知识点都比较通俗易懂,可为想大概了解ES6但没有时间阅读全书的人做一个参考。
1.let 和 const
暂时性死区 (Temporal Dead Zone)
let和const命令声明的变量无变量提升,且都会被锁定在声明的代码块中,在let和const命令执行前,使用该变量都将报错,这一部分称为“暂时性死区”。
var tmp = 123;
if (true) {
tmp = 'abc'; // ReferenceError
let tmp;
}
let tmp将tmp变量绑定至{}代码块之内,外部的tmp声明无效,tmp = 'abc'就处在死区,所以报错。同理在以前没有let和const命令的时候,typeof是一个安全的运算符,即使变量没有被声明,也会正常返回undefined,但如果typeof处在死区中,处理了在后文被let和const的变量,将会报错。
typeof undeclared_variable; // undefined 未被let和const声明反而没事
if (true) {
// TDZ开始 (Temporal Dead Zone: 暂时性死区)
typeof tmp // ReferenceError
let tmp; // TDZ结束
console.log(tmp); // undefined
}
顶层对象
var和function的全局声明会自动绑定到window或global对象,这是ES5全局变量的一个缺陷,let和const不会。
var a = 1;
// 如果在 Node 的 REPL 环境,可以写成 global.a
// 或者采用通用方法,写成 this.a
window.a // 1
let b = 1;
window.b // undefined
const命令
const声明的变量只是引用无法修改,对象的内部结构可以改变,使用Object.freeze()可以彻底锁定某对象,需递归锁定多层级对象。
var constantize = (obj) => {
Object.freeze(obj);
Object.keys(obj).forEach( (key, i) => {
if ( typeof obj[key] === 'object' ) {
constantize( obj[key] );
}
});
};
2.变量的解构赋值
解构时分为匹配模式和被赋值的变量,若相同可简写,注意区分
// 被解构的对象的key,和后边被赋值的变量同名,可以简写。
let { matchValue } = { matchValue: 123 };
console.log(matchValue); //123
等价于
let { matchValue: matchValue } = { matchValue: 123 }
console.log(matchValue); //123
通过代码的高亮可以看出相互之间的对应关系。所以
let { matchValue: value } = { matchValue: 123 }
console.log(matchValue); //报错,未定义,这个只是匹配模式,不会被赋值
console.log(value); //123
函数参数
首先解构赋值允许指定默认值,这为函数参数设置默认值提供基础。
// 数组解构赋值的默认值
let [x, y = 'b'] = ['a']; // x='a', y='b'
let [x = 'a', y = 'b'] = ['aa', undefined]; // x='aa', y='b'
// 对象解构赋值的默认值
let {x, y = 5} = {x: 1};
x // 1
y // 5
这里只讨论一下参数为Object类型时,该如何设置默认值,比如一些options的设置,通过设置默认值,可有效避免var foo = options.foo || 'default foo';。有三种形式,注意这三种的区别:
const ajax1 = function (url, { type = 'GET', dataType = 'json'} = {}) {
// TODO
}
const ajax2 = function (url, {} = { type = 'GET', dataType = 'json' }) {
// TODO
}
const ajax3 = function (url, { type = 'GET', dataType = 'json'} ) {
// TODO
}
ajax1的默认参数表示,如果没有传入options,则用一个没有键值对的对象{}作为默认值,但也正是因此,传入的options没有options.type 和 options.dataType这两个属性,则赋予默认值type = 'GET', dataType = 'json',这是针对键值对某一个key设默认值。
ajax2的默认参数表示,如果没有传入options对象,则用一个{ type = 'GET', dataType = 'json' }这样的options对象作为默认值,这是针对这一整个options设默认值。弊端就是如果开发者在使用时这样写
ajax2(url, {type = 'POST'})
那么dataType参数将要丢失,因为{type = 'POST'}代替了默认参数{ type = 'GET', dataType = 'json' },所以一般通过形如ajax1的方式定义默认参数。
ajax3的默认参数有一个问题,就是当没有传入options的时候,相当于从undefined中取值type,dataType来解构,所以会报错,所以ajax1会通过= {}的方式,把不传入options的情况过滤掉。
3.各种数据结构的扩展
字符串
``表示模板字符串,就不多介绍了,功能强大好用。
--------------------
codePointAt可作为charCodeAt的替代品,必要时使用for...of遍历字符串,他们都是为了处理32 位的 UTF-16 字符。
var s = "𠮷a";
s.length // 3 无法正确识别字符串长度,会把‘𠮷’识别为2个字符
s.charAt(0) // '' charAt无法处理这个字
s.charAt(1) // ''
s.charCodeAt(0) // 55362 charCodeAt只能两字节两字节的分开返回
s.charCodeAt(1) // 57271
下面看一下ES6的codePointAt和for...of
let s = '𠮷a';
s.codePointAt(0) // 134071 可以识别一整个字
s.codePointAt(1) // 57271 第三,四字节会被返回
s.codePointAt(2) // 97 字符串长度仍有问题
// 使用for...of循环正确处理
for (let ch of s) {
console.log(ch.codePointAt(0).toString(16));
}
// 20bb7 134071是10进制,20bb7为16进制表示
// 61 字符串长度也没问题,循环执行了2次
--------------------
还引入了includes,startWith,endWith,padStart,padEnd等方法,具体使用方法可以通过API手册了解一下。
Number
parseInt等全局方法挂在到Number上,如Number.parseInt,Number.parseFloat等,增加了一些高阶计算函数。
函数
箭头函数,this的指向在函数生成时固定,说白了就是this指向与外部一致。
--------------------
函数参数设置默认值,前文已经说明。补充一点,设置某参数必须可以:
function throwNeedThisParamException(param) {
throw new Error(`Missing parameter: ${param}`);
}
function demo (x = throwNeedThisParamException('x')) {
}
参数的默认值是在取不到值的情况下才会执行,所以正常情况不会抛出这个错误。
--------------------
参数的rest形式,例子如下:
function demo (...values) {
console.log(values)
console.log('-----------------------')
console.log(arguments)
}
demo(1,2,3,4)
//[1,2,3,4]
-----------------------
//[1, 2, 3, 4, callee: (...), Symbol(Symbol.iterator): ƒ]
内置的arguments为类数组结构,可以看见有一个Symbol类型的字段Symbol.iterator,这是它的迭代器,使其可以向数组一样遍历。但如果展开看其__proto__,值为Object。而使用rest形式的参数,可以直接将参数转为数组。注意rest形式的参数只能用作最后一个参数。
--------------------
函数的length属性返回函数参数的个数,name属性返回声明的函数名称,ES6的变量式声明返回变量名。
function f1 () {}
f1.name // f1
const f2 = function (x,y) {}
f2.name // f2
f2.length // 2
--------------------
双冒号运算符,代替bind,call,apply绑定this对象指向。foo::bar(arguments)相当于bar.apply(foo, arguments)
--------------------
尾调用,说白了就是最后返回值为执行另一个函数return anotherFunction(),而return anotherFunction() + 1不属于尾调用,因为在执行完anotherFunction后还需要+1。尾调用的优势就是在return后,可以释放当前函数执行所需要的一切资源空间。对比如下两个例子,是做斐波那契数列求值的:
function Fibonacci (n) {
if ( n <= 1 ) {return 1};
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
Fibonacci(10) // 89
Fibonacci(100) // 堆栈溢出
Fibonacci(500) // 堆栈溢出
这是最简单的写法,清晰明了,第n项就是前两项的和。但是,为了计算加号两边的值,必须要保存函数执行的全部资源,递归后造成堆栈溢出。这不属于尾调用。
function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
if( n <= 1 ) {return ac2};
return Fibonacci2 (n - 1, ac2, ac1 + ac2);
}
Fibonacci2(100) // 573147844013817200000
Fibonacci2(1000) // 7.0330367711422765e+208
Fibonacci2(10000) // Infinity
这是优化过的递归调用,return之后无需保存函数所需要的资源,所以不会堆栈溢出,只是在逻辑上不好理解。这种写法Fibonacci2 (n - 1, ac2, ac1 + ac2)可以看成一个从前到后推导过程,n相当于一个计数器,每次值的增长是通过两个数求和ac1 + ac2作为第二个数,ac2作为第一个数。
数组
扩展运算符...,与上文的rest参数是相反的用法,rest参数是把一个个的参数总和到数组rest参数中,而扩展运算符是把数组中的元素一个个提取出来。
扩展运算符可以用来方便的复制一个数组。
let arr = [1,2,3]
console.log(...arr) // 等价于console.log(1,2,3)
let arr2 = [...arr] // 等价于let arr2 = [1,2,3],新建一个数组
arr.push(4)
console.log(arr2) // [1,2,3]
--------------------
数组可以通过Array.from,Array.of生成,可以通过keys(),values(),entries()遍历。
Array.from可以从具有iterator的数据结构生成数组,比如arguments对象,document.querySelectorAll()获得的DOM对象,这些都是类数组,或者Map,Set等新增的数据结构。
Array.of可以代替new Array(),因为new Array()的参数与行为不统一,当传入一个参数且为数字时,表示数组长度,Array.of不会有这个问题,会通过参数创建数组。
Array还新增了一些工具方法,如find,findIndex,includes等可以参考其他API手册。
对象
Object.assign是合并对象,把多个对象合并到第一个对象上。
Object.create是以某原型,生成一个新对象。可选第二个参数,为属性描述符,使用方式参见下方代码。
Object.getPrototypeOf,Object.setPrototypeOf是获取和设置对象的原型属性__proto__,不应显式使用__proto__这个属性。
Object.getOwnPropertyDescriptors是获取对象的属性信息,包括value,writable,enumerable,configurable。
const target = { a: 1 };
const source1 = { b: 2 };
const source2 = { c: 3 };
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}
-------------------
Object.setPrototypeOf(target, { myProto: 'PROTO'})
Object.getPrototypeOf(target) //{ myProto: 'PROTO', __proto__: Object}
let newObj = Object.create(Object.getPrototypeOf(target))
newObj // 无显式属性{ __proto__:{ myProto: 'PROTO', __proto__: Object} }
-------------------
const descriptors = Object.getOwnPropertyDescriptors(target)
console.log(descriptors)
// {
// a: {value: 1, writable: true, enumerable: true, configurable: true},
// b: {value: 2, writable: true, enumerable: true, configurable: true},
// c: {value: 3, writable: true, enumerable: true, configurable: true}
// }
newObj = Object.create(Object.getPrototypeOf(target), descriptors)
newObj // { a:1, b:2, c:3, __proto__:{ myProto: 'PROTO', __proto__: Object} }
--------------------
ES6允许字面量定义对象时,用表达式作为属性名,把表达式放在方括号内。
const propKey = 'foo';
const obj = {
[propKey]: true,
['a' + 'bc']: 123
};
obj // { foo: true, abc: 123 }
--------------------
Object.is优化了===运算符,处理了===的两个问题。
NaN === NaN // false
Object.is(NaN, NaN) // true
--------------
+0 === -0 // true
Object.is(+0, -0) // false
4.Symbol
Symbol为不会重复的值,第七种基本数据类型,类似字符串,可以作为对象的key,但不会被for...of,for...in,Object.getOwnPropertyNames(),Object.keys()返回,如需要遍历,需使用Object.getOwnPropertySymbols(),或者Reflect.ownKeys()返回全部key。
let foo = Symbol('foo');
const obj = { [foo]: 'foobar' }
for (let i in obj) {
console.log(i); // 无输出
}
Object.getOwnPropertyNames(obj)
// []
Object.getOwnPropertySymbols(obj)
// [Symbol(foo)]
Reflect.ownKeys(obj)
// [Symbol(foo)]
Symbol.for() 和 Symbol.keyFor()
Symbol可以去确保生成的值不同,但有时需要保存下来以便再次使用,类似于单例,如果存在就不会重新创建。这个时候就需要使用Symbol.for()。
let s1 = Symbol('foo');
let s2 = Symbol.for('foo');
let s3 = Symbol.for('foo');
s1 === s2 // false
s2 === s3 // true
从上例可以看出,Symbol.for类似于将这个Symbol登记,所以s1这个未登记的Symbol不会等于其他Symbol。
Symbol.keyFor会返回已登记的Symbol的key,一定是登记过的才会返回。接上例:
Symbol.keyFor(s1) // undefiend
Symbol.keyFor(s2) // "foo"
5.Proxy和Reflect
Proxy代理对象的各种内置方法,get set construct等,类似于拦截器。
Reflect则作为Object的替代者,Object上的一些静态方法被移植到了Reflect上。
Reflect对象一共有 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)
通过Proxy和Reflect可以实现观察者模式,说白了就是:监听set方法,执行相应操作。
const person = { name: 'Li', age: 18}
const personObserved = observe(person)
function observe(obj) {
return new Proxy(obj, {
set: function (target, key, value, receiver) {
console.log(`setting ${key} to ${value}!`);
return Reflect.set(target, key, value, receiver);
}
})
}
personObserved.name = 'zhang'
// setting name to zhang!
6.Promise
Promise用来处理异步操作,是构造函数,参数为then和catch后需要执行的方法。下面是使用Promise封装的ajax:
const getJSON = function(url) {
const promise = new Promise((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);
});
7. Iterator 和 for...of循环
Iterator被挂载在对象的Symbol.iterator属性下,Symbol.iterator不是一个Iterator,而是会返回Iterator的函数。
const arr = [1,2,3,4,5]
let iterator = arr[Symbol.iterator]();
iterator // Array Iterator {}
iterator.next() // {value: 1, done: false}
......
iterator.next() // {value: 5, done: false}
iterator.next() // {value: undefined, done: true}
8. Generator 和 yield
Generator会生成一个Iterator,每次iterator.next()返回yield的产出值,且中断程序执行。yield*表示产出的值是另外一个generator的结果。代码如下:
function* demo(){
console.log(`${yield 1}`);
console.log(`${yield 2}`);
yield* demo2(); //返回另一个generator的结果
}
function* demo2(){
yield 3;
}
let ite = demo();
ite.next() // 返回值:{value: 1, done: false}
ite.next() // console:undefined, 返回值:{value: 2, done: false}
ite.next(123456789) // console: 123456789, 返回值:{value: 3, done: false}
解释一下运行结果:第一次ite.next()时,程序执行到yield 1被终止,故没有打印日志,再次执行ite.next()时,代码继续,开始执行console.log(`${yield 1}`);,但输出不是1而是undefiend,因为ite.next()的参数值会被当做上次yield语句的执行结果,所以下面的ite.next(123456789)会输出数字123456789。