Tip
最近读完了 Nicholas C. Zakas 的新书 深入理解ES6,根据之前的经验和一些文档梳理的知识脉络,总结一些关于 ES6 的常用方法和技巧。
1. let/const 特性
在 ES6 标准发布之前,JS 一般都是通过关键字var
声明变量,与此同时,并不存在明显的代码块声明,想要形成代码块,一般都是采用闭包的方式,举个十分常见的例子:
var arr = []
for(var i = 0; i < 5; i++) {
arr.push(function() {
console.log(i)
})
}
arr.forEach(function(item) {
item()
})
// 输出5次数字5
关于为什么输出的全是数字5,涉及到了JS的事件循环机制,异步队列里的函数执行的时候,由于关键字var
不会生成代码块,所以参数i = 5
,最后也就全输出了数字5。用之前的方法我们可以这样修改:
var arr = []
for(var i = 0; i < 5; i++) {
(function(i) {
arr.push(function() {
console.log(i)
})
})(i)
}
arr.forEach(function(item) {
item()
})
// 输出 0 1 2 3 4
而在引入let
和const
之后,这两个关键字会自动生成代码块,并且不存在变量提升,因此只需要把var
关键字换成let
就可以输出数字0到4了:
var arr = []
for(let i = 0; i < 5; i++) {
arr.push(function() {
console.log(i)
})
}
arr.forEach(function(item) {
item()
})
// 输出 0 1 2 3 4
关键字let
和const
的区别在于,使用const
声明的值类型变量不能被重新赋值,而引用类型变量是可以的。
变量提升是因为浏览器在编译执行JS代码的时候,会先对变量和函数进行声明,
var
关键字声明的变量也会默认为undefined
,在声明语句执行时再对该变量进行赋值。
值得注意的是,在重构原先代码的过程中,要十分注意,盲目地使用let
来替换var
可能会出现出乎意料的情况:
var snack = 'Meow Mix'
function getFood(food) {
if (food) {
var snack = 'Friskies'
return snack
}
return snack
}
getFood(false)
// undefined
替换之后会出现与原先输出不匹配的情况,至于原因,就是上面提到的let
不存在变量提升。
使用var
虽然没有执行if
内的语句,但是在声明变量的时候已经声明了var snack = undefined
的局部变量,最后输出的是局部变量里的undefined
。
let snack = 'Meow Mix'
function getFood(food) {
if (food) {
let snack = 'Friskies'
return snack
}
return snack
}
getFood(false)
// 'Meow Mix'
而使用let
则在不执行if
语句时拿不到代码块中局部的snack
变量(在临时死区中),最后输出了全局变量中的snack
。
当前使用块级绑定的最佳实践是:默认使用
const
,只在确实需要改变变量的值时使用let
。这样就可以在某种程度上实现代码的不可变,从而防止某些错误的产生。
2. 箭头函数
在 ES6 中箭头函数是其最有趣的新增特性,它是一种使用箭头=>
定义函数的新语法,和传统函数的不同主要集中在:
- 没有
this
、super
、arguments
和new.target
绑定 - 不能通过
new
关键字调用 - 没有原型
- 不可以改变
this
的绑定 - 不支持
arguments
对象 - 不支持重复的命名参数
this
的绑定是JS程序中一个常见的错误来源,尤其是在函数内就很容易对this
的值是去控制,经常会导致意想不到的行为。
在出现箭头函数之前,在声明构造函数并且修改原型的时候,经常会需要对this
的值进行很多处理:
function Phone() {
this.type = type
}
Phone.prototype.fixTips = function(tips) {
// var that = this
return tips.map(function(tip) {
// return that.type + tip
return this.type + tip
// })
},this)
}
要输出正确的fixTips
,必须要对this
的指向存在变量中或者给它找个上下文绑定,而如果使用箭头函数的话,则很容易实现:
function Phone() {
this.type = type
}
Phone.prototype.fixTips = function(tips) {
return tips.map(tip => this.type + tip)
}
就像上面的例子一样,在我们写一个函数的时候,箭头函数更加简洁并且可以简单地返回一个值。
当我们需要维护一个
this
上下文的时候,就可以使用箭头函数。
3. 字符串
我认为 ES6 在对字符串处理这一块,新增的特性是最多的,本文只总结常用的方法,但还是推荐大家有时间去仔细了解一下。
.includes()
之前在需要判断字符串中是否包含某些字符串的时候,基本都是通过indexOf()
的返回值来判断的:
var str = 'superman'
var subStr = 'super'
console.log(str.indexOf(subStr) > -1)
// true
而现在可以简单地使用includes()
来进行判断,会返回一个布尔值:
const str = 'superman'
const subStr = 'super'
console.log(str.includes(subStr))
// true
当然除此之外还有两个特殊的方法,它们的用法和includes()
一样:
- startWith():如果在字符串的起始部分检测到指定文本则返回
true
- endsWith():如果在字符串的结束部分检测到指定文本则返回
true
.repeat()
在此之前,需要重复字符串,我们需要自己封装一个函数:
function repeat(str, count) {
var strs = []
while(str.length < count) {
strs.push(str)
}
return strs.join('')
}
现在则只需要调用repeat()
就可以了:
'superman'.repeat(2)
// supermansuperman
模板字符串
我觉得模板字符串也是ES6最牛逼的特性之一,因为它极大地简化了我们对于字符串的处理,开发过程中也是用得特别爽。
首先它让我们不用进行转义处理了:
var text = 'my name is \'Branson\'.'
const newText = `my name is 'Branson'.`
然后它还支持插入、换行和表达式:
const name = 'Branson'
console.log(`my name is ${name}.`)
// my name is Branson.
const text = (`
what's
wrong
?
`)
console.log(text)
// what's
// wrong
// ?
const today = new Date()
const anotherText = `The time and date is ${today.toLocaleString()}.`
console.log(anotherText)
// The time and date is 2017-10-23 14:52:00
值得注意的是,之前在改离职同学留下的bug时,也发现了要推荐使用模板字符串的一个点:
formatDate(date) {
if(!date) return ""
if(!(date instanceof Date)) date = new Date(date)
const y = date.getFullYear()
let m = date.getMonth() + 1
m = m < 10 ? ('0' + m) : m
let d = date.getDate()
d = d < 10 ? ('0' + d) : d
// return y + m + d
return `${y}${m}${d}`
}
之前使用的是注释掉的输出方式,由于这位同学在判断m
和d
的值的时候,没有把大于10情况下的值类型转换成字符串,所以在我们放了个“十一”假回来输出的值就出现问题了。
而使用模板字符串有一个好处就是,我不管你m
和d
是不是类型转换了,我最后都输出一个字符串,算是容错率更高了吧。
4. 解构
解构可以让我们用一个更简便的语法从一个数组或者对象(即使是深层的)中分离出来值,并存储他们。
这一块没什么可说的,直接放代码了:
// 数组解构
// ES5
var arr = [1, 2, 3, 4]
var a = arr[0]
var b = arr[1]
var c = arr[2]
var d = arr[3]
// ES6
let [a, b, c, d] = [1, 2, 3, 4]
// 对象解构
// ES5
var luke = {occupation: 'jedi', father: 'anakin'}
var occupation = luke.occupation
// 'jedi'
var father = luke.father
// 'anakin'
// ES6
let luke = {occupation: 'jedi', father: 'anakin'}
let {occupation, father} = luke
console.log(occupation)
// 'jedi'
console.log(father)
// 'anakin'
5. 模块
在 ES6 之前,我们使用Browserify
这样的库来创建客户端的模块化,在node.js
中使用require
。在 ES6 中,我们可以直接使用所有类型的模块化(AMD 和 CommonJS)。
CommonJS 模块的出口定义:
module.exports = 1
module.exports = { foo: 'bar' }
module.exports = ['foo', 'bar']
module.exports = function bar () {}
ES6 模块的出口定义:
// 暴露单个对象
export let type = 'ios'
// 暴露多个对象
function deDuplication(arr) {
return [...(new Set(arr))]
}
function fix(item) {
return `${item} ok!`
}
export {deDuplication, fix}
// 暴露函数
export function sumThree(a, b, c) {
return a + b + c
}
// 绑定默认输出
let api = {
deDuplication,
fix
}
export default api
// export { api as default }
模块出口最佳实践:总是在模块的最后面使用
export default
方法,这可以让暴露的东西更加清晰并且可以节省时间去找出暴露出来值的名字。尤其如此,在 CommonJS 中通常的实践就是暴露一个简单的值或者对象。坚持这种模式,可以让我们的代码更加可读,并且在 ES6 和 CommonJS 模块之间更好地兼容。
ES6 模块导入:
// 导入整个文件
import 'test'
// 整体加载
import * as test from 'test'
// 按需导入
import { deDuplication, fix } from 'test'
// 遇到出口为 export { foo as default, foo1, foo2 }
import foo, { foo1, foo2 } from 'foos'
6. 参数
参数这一块儿在这之前,无论是默认参数、不定参数还是重命名参数都需要我们做很多处理,有了ES6之后相对来说就简洁多了:
默认参数:
// ES5
function add(x, y) {
x = x || 0
y = y || 0
return x + y
}
// ES6
function add(x=0, y=0) {
return x + y
}
add(3, 6) // 9
add(3) // 3
add() // 0
不定参数:
// ES5
function logArgs() {
for(var i = 0; i < arguments.length; i++) {
console.log(arguments[i])
}
}
// ES6
function logArgs(...args) {
for(let arg of args) {
console.log(arg)
}
}
命名参数:
// ES5
function Phone(options) {
var type = options.type || 'ios'
var height = options.height || 667
var width = options.width || 375
}
// ES6
function Phone(
{type='ios', height=667, width=375}) {
console.log(height)
}
展开操作:
求一个数组的最大值:
// ES5
Math.max.apply(null, [-1, 100, 9001, -32])
// ES6
Math.max(...[-1, 100, 9001, -32])
当然这个特性还可以用来进行数组的合并:
const player = ['Bryant', 'Durant']
const team = ['Wade', ...player, 'Paul']
console.log(team)
// ['Wade', 'Bryant', 'Durant', 'Paul']
7. 类 class
关于面向对象这个词,大家都不陌生,在这之前,JS要实现面向对象编程都是基于原型链,ES6提供了很多类的语法糖,我们可以通过这些语法糖,在代码上简化很多对prototype
的操作:
// ES5
// 创造一个类
function Animal(name, age) {
this.name = name
this.age = age
}
Animal.prototype.incrementAge = function() {
this.age += 1
}
// 类继承
function Human(name, age, hobby, occupation) {
Animal.call(this, name, age)
this. hobby = hobby
this.occupation = occupation
}
Human.prototype = Object.create(Animal.prototype)
Human.prototype.constructor = Human
Human.prototype.incrementAge = function() {
Animal.prototype.incrementAge.call(this)
console.log(this.age)
}
在ES6中使用语法糖简化:
// ES6
// 创建一个类
class Animal {
constructor(name, age) {
this.name = name
this.age = age
}
incrementAge() {
this.age += 1
}
}
// 类继承
class Human extends Animal {
constructor(name, age, hobby, occupation) {
super(name, age)
this.hobby = hobby
this.occupation = occupation
}
incrementAge() {
super.incrementAge()
console.log(this.age)
}
}
注意:尽管类与自定义类型之间有诸多相似之处,我们仍然需要牢记它们之间的这些差异:
- 函数声明可以被提升,而类声明与
let
声明类似,不能被提升;真正执行声明语句之前,它们会一直存在于临时死区中。 - 类声明中的所有代码将自动运行在严格模式下,而且无法强行让代码脱离严格模式进行。
- 在自定义类型中,需要通过
Object.defineProperty()
方法手动指定某个方法不可枚举;而在类中,所有方法都是不可枚举的。 - 每个类都有一个
constructor
方法,通过关键字new
调用那些不包含constructor
的方法会导致程序抛出错误。 - 使用除关键字
new
以外的方式调用类的构造函数会导致程序抛出错误。 - 在类中修改类名会导致程序报错。
8. Symbols
Symbols
在 ES6 之前就已经存在,我们现在可以直接使用一个开发的接口了。
Symbols
是不可改变并且是独一无二的,可以在任意哈希中作一个key。
Symbol():
调用Symbol()
或者Symbol(description)
可以创造一个独一无二的符号,但是在全局是看不到的。Symbol()
的一个使用情况是给一个类或者命名空间打上补丁,但是可以确定的是你不会去更新它。比如,你想给React.Component
类添加一个refreshComponent
方法,但是可以确定的是你不会在之后更新这个方法:
const refreshComponent = Symbol()
React.Component.prototype[refreshComponent] = () => {
// do something
}
Symbol.for(key):
Symbol.for(key)
同样会创造一个独一无二并且不可改变的 Symbol,但是它可以全局看到,两个相同的调用Symbol.for(key)
会返回同一个Symbol
类:
Symbol('foo') === Symbol('foo')
// false
Symbol.for('foo') === Symbol('foo')
// false
Symbol.for('foo') === Symbol.for('foo')
// true
对于 Symbols 的普遍用法(尤其是Symbol.for(key)
)是为了协同性。它可以通过在一个第三方插件中已知的接口中对象中的参数中寻找用 Symbol 成员来实现,比如:
function reader(obj) {
const specialRead = Symbol.for('specialRead')
if (obj[specialRead]) {
const reader = obj[specialRead]()
// do something with reader
} else {
throw new TypeError('object cannot be read')
}
}
在另一个库中:
const specialRead = Symbol.for('specialRead')
class SomeReadableType {
[specialRead]() {
const reader = createSomeReaderFrom(this)
return reader
}
}
9. Set/Map
在此之前,开发者都是用对象属性来模拟set
和map
两种集合:
// set
var set = Object.create(null)
set.foo = true
if(set.foo) {
// do something
}
// map
var map = Object.create(null)
map.foo = 'bar'
var value = map.foo
console.log(value)
// 'bar'
由于在 ES6 中set
和map
的操作与其它语言类似,本文就不过多介绍这些,主要通过几个例子来说说它们的应用。
在 ES6 中新增了有序列表set
,其中含有一些相互独立的非重复值,通过set
集合可以快速访问其中的数据,更有效地追踪各种离散值。
关于set
运用得最多的应该就是去重了:
const arr = [1, 1, 2, 11, 32, 1, 2, 3, 11]
const deDuplication = function(arr) {
return [...(new Set(arr))]
}
console.log(deDuplication(arr))
// [1, 2, 11, 32, 3]
map
是一个非常必需的数据结构,在 ES6 之前,我们通过对象来实现哈希表:
var map = new Object()
map[key1] = 'value1'
map[key2] = 'value2'
但是它并不能防止我们偶然地用一些特殊的属性名重写函数:
getOwnProperty({ hasOwnProperty: 'Hah, overwritten'}, 'Pwned')
// TypeError: Property 'hasOwnProperty' is not a function
在 ES6 中map
允许我们队值进行get
、set
和search
操作:
let map = new Map()
map.set('name', 'david')
map.get('name')
// david
map.has('name')
// true
而map
更令人惊奇的部分就是它不仅限于使用字符串作为 key,还可以用其他任何类型的数据作为 key:
let map = new Map([
['name', 'david'],
[true, 'false'],
[1, 'one'],
[{}, 'object'],
[function () {}, 'function']
])
for(let key of map.keys()) {
console.log(typeof key)
}
// string, boolean, number, object, function
注意:我们使用map.get()
方法去测试相等时,如果在map
中使用函数或者对象等非原始类型值的时候测试将不起作用,所以我们应该使用 Strings, Booleans 和 Numbers 这样的原始类型的值。
我们还可以使用 .entries()
来遍历迭代:
for(let [key, value] of map.entries()) {
console.log(key, value);
}
10. Weak Set/Weak Map
对于set
和WeakSet
来说,它们之间最大的区别就是,WeakSet
保存的是对象值得弱引用,下面这个实例会展示它们的差异:
let set = new WeakSet(),
key = {}
set.add(key)
console.log(set.has(key))
// true
// 移除对象key的最后一个强引用( WeakSet 中的引用也自动移除 )
key = null
这段代码执行过后,就无法访问WeakSet
中 key 的引用了。除了这个,它们还有以下几个差别:
- 在
WeakSet
的实例中,如果向add()
、has()
和delete()
这三个方法传入非对象参数都会导致程序报错。 -
WeakSet
集合不可迭代,所以不能被用于for-of
循环。 -
WeakSet
集合不暴露任何迭代器(例如keys()
和values()
方法),所以无法通过程序本身来检测其中的内容。 -
WeakSet
集合不支持forEach()
方法。 -
WeakSet
集合不支持size
属性。
总之,如果你只需要跟踪对象引用,你更应该使用WeakSet
集合而不是普通的set
集合。
在 ES6 之前,为了存储私有变量,我们有各种各样的方法去实现,其中一种方法就是用命名约定:
class Person {
constructor(age) {
this._age = age
}
_incrementAge() {
this._age += 1
}
}
但是命名约定在代码中仍然会令人混淆并且并不会真正的保持私有变量不被访问。现在,我们可以使用WeakMap
来存储变量:
let _age = new WeakMap()
class Person {
constructor(age) {
_age.set(this, age)
}
incrementAge() {
let age = _age.get(this) + 1
_age.set(this, age)
if (age > 50) {
console.log('Midlife crisis')
}
}
}
在WeakMap
存储变量很酷的一件事是它的 key 他不需要属性名称,可以使用Reflect.ownKeys()
来查看这一点:
const person = new Person(50)
person.incrementAge()
// 'Midlife crisis'
Reflect.ownKeys(person)
// []
一个更实际的实践就是可以WeakMap
储存 DOM 元素,而不会污染元素本身:
let map = new WeakMap()
let el = document.getElementById('someElement');
// Store a weak reference to the element with a key
map.set(el, 'reference')
// Access the value of the element
let value = map.get(el)
// 'reference'
// Remove the reference
el.parentNode.removeChild(el)
el = null
如上所示,当一个对象被垃圾回收机制销毁的时候,WeakMap
将会自动地一处关于这个对象地键值对。
注意:为了进一步说明这个例子的实用性,可以考虑 jQuery 是如何实现缓存一个对象相关于对引用地 DOM 元素对象。使用 jQuery ,当一个特定地元素一旦在 document 中移除的时候,jQuery 会自动地释放内存。总体来说,jQuery 在任何 dom 库中都是很有用的。
11. Promise
在 ES6 出现之前,处理异步函数主要是通过回调函数,虽然看起来也挺不错,但是用多之后就会发现嵌套太多回调函数回引起回调地狱:
func1(function (value1) {
func2(value1, function (value2) {
func3(value2, function (value3) {
func4(value3, function (value4) {
func5(value4, function (value5) {
// Do something with value 5
})
})
})
})
})
当我们有了 Promise 之后,就可以将这些转化成垂直代码:
func1(value1)
.then(func2)
.then(func3)
.then(func4)
.then(func5, value5 => {
// Do something with value 5
})
原生的 Promise 有两个处理器:resolve
(当 Promise 是fulfilled
时的回调)和reject
(当 Promise 是rejected
时的回调):
new Promise((resolve, reject) =>
reject(new Error('Failed to fulfill Promise')))
.catch(reason => console.log(reason))
Promise的好处:对错误的处理使用一些列回调会使代码很混乱,使用 Promise,我看可以清晰的让错误冒泡并且在合适的时候处理它,甚至,在 Promise 确定了resolved/rejected
之后,他的值是不可改变的——它从来不会变化。
这是使用 Promise 的一个实际的例子:
const request = require('request')
return new Promise((resolve, reject) => {
request.get(url, (error, response, body) => {
if (body) {
resolve(JSON.parse(body))
} else {
resolve({})
}
})
})
我们还可以使用 Promise.all()
来并行处理多个异步函数:
let urls = [
'/api/commits',
'/api/issues/opened',
'/api/issues/assigned',
'/api/issues/completed',
'/api/issues/comments',
'/api/pullrequests'
]
let promises = urls.map((url) => {
return new Promise((resolve, reject) => {
$.ajax({ url: url })
.done((data) => {
resolve(data);
})
})
})
Promise.all(promises)
.then((results) => {
// Do something with results of all our promises
})
12. Generators 生成器
就像 Promise 可以帮我们避免回调地狱,Generator 可以帮助我们让代码风格更整洁——用同步的代码风格来写异步代码,它本质上是一个可以暂停计算并且可以随后返回表达式的值的函数:
function* sillyGenerator() {
yield 1
yield 2
yield 3
yield 4
}
var generator = sillyGenerator();
console.log(generator.next())
// { value: 1, done: false }
console.log(generator.next())
// { value: 2, done: false }
console.log(generator.next())
// { value: 3, done: false }
console.log(generator.next())
// { value: 4, done: false }
next
可以回去到下一个yield
返回的值,当然上面的代码是非常不自然的,我们可以利用 Generator 来用同步的方式来写异步操作:
function request(url) {
getJSON(url, function(response) {
generator.next(response)
})
}
这里的 generator 函数将会返回需要的数据:
function* getData() {
var entry1 = yield request('http://some_api/item1')
var data1 = JSON.parse(entry1)
var entry2 = yield request('http://some_api/item2')
var data2 = JSON.parse(entry2)
}
通过yield
,我们可以保证entry1
有data1
中我们需要解析并储存的数据。
虽然我们可以利用 Generator 来用同步的方式来写异步操作,但是确认错误的传播变得不再清晰,我们可以在 Generator 中加上 Promise:
function request(url) {
return new Promise((resolve, reject) => {
getJSON(url, resolve)
})
}
然后我们写一个函数逐步调用next
并且利用 request 方法产生一个 Promise:
function iterateGenerator(gen) {
var generator = gen()
(function iterate(val) {
var ret = generator.next()
if(!ret.done) {
ret.value.then(iterate)
}
})()
}
在 Generators 中加上 Promise 之后我们可以更清晰的使用 Promise 中的.catch
和reject
来捕捉错误,让我们使用新的 Generator,和之前的还是蛮相似的:
iterateGenerator(function* getData() {
var entry1 = yield request('http://some_api/item1')
var data1 = JSON.parse(entry1)
var entry2 = yield request('http://some_api/item2')
var data2 = JSON.parse(entry2)
})
13. Async Await
当 ES7 真正到来的时候,async await
可以用更少的处理实现 Promise 和 Generators 所实现的异步处理:
var request = require('request')
function getJSON(url) {
return new Promise(function(resolve, reject) {
request(url, function(error, response, body) {
resolve(body)
})
})
}
async function main() {
var data = await getJSON()
console.log(data)
// NOT undefined!
}
main()
14. Getter/Setter 函数
ES6 已经开始实现了 getter
和 setter
函数:
class Employee {
constructor(name) {
this._name = name
}
get name() {
if(this._name) {
return 'Mr. ' + this._name.toUpperCase()
} else {
return undefined
}
}
set name(newName) {
if (newName == this._name) {
console.log('I already have this name.')
} else if (newName) {
this._name = newName
} else {
return false
}
}
}
var emp = new Employee("James Bond")
// uses the get method in the background
if (emp.name) {
console.log(emp.name)
// Mr. JAMES BOND
}
// uses the setter in the background
emp.name = "Bond 007"
console.log(emp.name)
// Mr. BOND 007
最新版本的浏览器也在对象中实现了getter
和setter
函数,我们可以使用它们来实现 计算属性,在设置和获取一个属性之前加上监听器和处理。
var person = {
firstName: 'James',
lastName: 'Bond',
get fullName() {
console.log('Getting FullName')
return this.firstName + ' ' + this.lastName
},
set fullName (name) {
console.log('Setting FullName')
var words = name.toString().split(' ')
this.firstName = words[0] || ''
this.lastName = words[1] || ''
}
}
person.fullName
// James Bond
person.fullName = 'Bond 007'
person.fullName
// Bond 007
总结
本文部分代码参考自:https://github.com/DrkSephy/es6-cheatsheet
主要是为了分享一些 ES6 的用法技巧以及最佳实践,欢迎交流!