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,涉及到了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) //此处对this做处理 或者用注释掉的方法that对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
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 模块之间更好地兼容。// 导入整个文件 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,但是它可以全局看到,两个相同的调用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
浙公网安备 33010602011771号