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以外的方式调用类的构造函数会导致程序抛出错误。
- 在类中修改类名会导致程序报错。