这是笔者读完阮一峰的《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
。