# ES
## 全局作用域中,用 const 和 let 聲明的變量不在 window 上,那到底在哪里~ 如何去獲取~
~~~js
var a = 1
let b = 2
const c = 3
console.dir(new Function())
~~~
* 在全局作用域中,用 let 和 const 聲明的全局變量并沒有在全局對象中,只是一個塊級作用域(Script)中
* 在定義變量的塊級作用域中獲取
~~~js
let b = 2
const c = 3
// like
(function() {
var b = 2
var c = 3
})()
~~~
## var、let 和 const 區別的實現原理是什么~
①、var 聲明變量會掛在window, let const 不會 ②、let, const 聲明形成 作用域 ③、同一作用域下 let const 不能聲明 同名變量, 而var 可以 ④、暫存死區 ⑤、const 聲明后不得修改
* 聲明過程
* var:遇到有var的作用域,在任何語句執行前都已經完成了聲明和初始化,也就是變量提升而且拿到undefined的原因由來~
* function: 聲明、初始化、賦值一開始就全部完成,所以函數的變量提升優先級更高
* let:解析器進入一個塊級作用域,發現let關鍵字,變量只是先完成聲明,并沒有到初始化那一步。 此時如果在此作用域提前訪問,則報錯xx is not defined,這就是暫時性死區的由來。 等到解析到有let那一行的時候,才會進入初始化階段。如果let的那一行是賦值操作,則初始化和賦值同時進行
* 內存分配
* var 的話會直接在棧內存里預分配內存空間,然后等到實際語句執行的時候,再存儲對應的變量;
* let 是不會在棧內存里預分配內存空間,而且在棧內存分配變量時,做一個檢查,如果已經有相同變量名存在就會報錯
* const 也不會預分配內存空間,在棧內存分配變量時也會做同樣的檢查。不過const存儲的變量是不可修改的; 對于基本類型來說你無法修改定義的值 對于引用類型來說你無法修改棧內存里分配的指針,但是你可以修改指針指向的對象里面的屬性
## ES5/ES6 的繼承除了寫法以外還有什么區別~
* class 聲明內部會啟用嚴格模式。
~~~js
// 引用一個未聲明的變量
function Bar() {
baz = 42; // it's ok
}
const bar = new Bar()
class Foo {
constructor() {
fol = 42 // ReferenceError: fol is not defined
}
}
const foo = new Foo()
~~~
* class 的所有方法(包括靜態方法和實例方法)都是不可枚舉的。
~~~js
function Bar() {
this.bar = 42
}
Bar.answer = function() {
return 42
}
Bar.prototype.print = function() {
console.log(this.bar)
}
const barKeys = Object.keys(Bar) // ['answer']
const barProtoKeys = Object.keys(Bar.prototype) // ['print']
class Foo {
constructor() {
this.foo = 42
}
static answer() {
return 42
}
print() {
console.log(this.foo)
}
}
const fooKeys = Object.keys(Foo); // []
const fooProtoKeys = Object.keys(Foo.prototype); // []
~~~
* class 的所有方法(包括靜態方法和實例方法)都沒有原型對象 prototype,也沒有 construct,不能使用 new 來調用。
~~~js
function Bar() {
this.bar = 42
}
Bar.prototype.print = function() {
console.log(this.bar)
}
const bar = new Bar()
const barPrint = new bar.print() // it's ok
class Foo {
constructor() {
this.foo = 42
}
print() {
console.log(this.foo)
}
}
const foo = new Foo()
const fooPrint = new foo.print() // TypeError: foo.print is not a constructor
~~~
* 必須使用 new 調用 class。
~~~js
function Bar() {
this.bar = 42
}
const bar = Bar() // it's ok
class Foo {
constructor() {
this.foo = 42
}
}
const foo = Foo() // TypeError: Class constructor Foo cannot be invoked without 'new'
~~~
* class 內部無法重寫類名。
~~~js
function Bar() {
Bar = 'Baz' // it's ok
this.bar = 42;
}
const bar = new Bar() // bar: Bar {bar: 42}
class Foo {
constructor() {
this.foo = 42
Foo = 'Fol' // TypeError: Assignment to constant variable
}
}
const foo = new Foo()
Foo = 'Fol' // it's ok
~~~
## 箭頭函數與普通函數(function)的區別是什么?構造函數(function)可以使用 new 生成實例,那么箭頭函數可以嗎~ 為什么~
* 箭頭函數體內的 this 對象,就是定義時所在的對象,而不是使用時所在的對象。
* 不可以使用 arguments 對象,該對象在函數體內不存在。如果要用,可以用 rest 參數代替。
* 不可以使用 new 生成實例:
* 沒有自己的 this,無法調用 call,apply。
* 沒有 prototype 屬性 ,而 new 命令在執行時需要將構造函數的 prototype 賦值給新的對象的**proto**
* 不可以使用 yield 命令,因此箭頭函數不能用作 Generator 函數。
~~~js
// new的過程
var objectFactory = function() {
// 從Object.prototype上克隆一個空對象
var obj = new Object()
// 取得外部傳入的構造器,在此是Person
var Constructor = [].shift.call( arguments )
// 指向正確的原型
obj.__proto__ = Constructor.prototype
// 借用構造函數給obj設置屬性
var ret = Constructor.apply(obj, arguments)
return typeof ret === 'object' ? ret : obj
}
~~~
## 使用 JavaScript Proxy 實現簡單的數據綁定
~~~html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
hello,world
<input type="text" id="model">
<p id="word"></p>
<script>
const model = document.getElementById('model')
const word = document.getElementById('word')
var obj= {}
const newObj = new Proxy(obj, {
get: function(target, key, receiver) {
console.log(`getting ${key}!`)
return Reflect.get(target, key, receiver)
},
set: function(target, key, value, receiver) {
console.log('setting',target, key, value, receiver)
if (key === 'text') {
model.value = value
word.innerHTML = value
}
return Reflect.set(target, key, value, receiver)
}
})
model.addEventListener('keyup',function(e){
newObj.text = e.target.value
})
</script>
</body>
</html>
~~~
## 介紹模塊化發展歷程
> 可從IIFE、AMD、CMD、CommonJS、UMD、webpack(require.ensure)、ES Module、`<`script type="module"`>`這幾個角度考慮
**模塊化主要是用來抽離公共代碼,隔離作用域,避免變量沖突等。**
`IIFE`: 使用自執行函數來編寫模塊化,特點:在一個單獨的函數作用域中執行代碼,避免變量沖突。
~~~js
(function(){
return {
data:[]
}
})()
~~~
`AMD`: 使用requireJS 來編寫模塊化,特點:依賴必須提前聲明好。
~~~js
define('./index.js', function( code ){
// code 就是index.js 返回的內容
})
~~~
`CMD`: 使用seaJS 來編寫模塊化,特點:支持動態引入依賴文件。
~~~js
define(function(require, exports, module) {
var indexCode = require('./index.js')
})
~~~
`CommonJS`: nodejs 中自帶的模塊化。
~~~js
var fs = require('fs')
~~~
`UMD`:兼容AMD,CommonJS 模塊化語法。 webpack(require.ensure):webpack 2.x 版本中的代碼分割。
`ES Modules`: ES6 引入的模塊化,支持import 來引入另一個 js
~~~js
import a from 'a'
~~~
## 介紹下 Set、Map、WeakSet 和 WeakMap 的區別
*Set 是一種叫做`集合`的數據結構,Map 是一種叫做`字典`的數據結構*
* 集合 是以`[value, value]`的形式儲存元素,字典 是以`[key, value]`的形式儲存
~~~js
var a = [1, 2, 3, 4]
var b = { name: 'zhangsan', age: 15}
~~~
### Set
> ES6 提供了新的數據結構`Set`。它類似于數組,但是成員的值都是`唯一`的,沒有重復的值。`Set`本身是一個構造函數,用來生成`Set數據結構`。
* 基礎語法
* `new Set([iterable])`
* 參數: iterable傳遞一個可迭代對象,它的所有元素將不重復地被添加到新的`Set`, 返回一個新的`Set`對象。
* 可迭代對象(需要遵守[可迭代協議](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Iteration_protocols "https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Iteration_protocols"))
* 內置的[可迭代對象](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Iteration_protocols#%E5%86%85%E7%BD%AE%E5%8F%AF%E8%BF%AD%E4%BB%A3%E5%AF%B9%E8%B1%A1 "https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Iteration_protocols#%E5%86%85%E7%BD%AE%E5%8F%AF%E8%BF%AD%E4%BB%A3%E5%AF%B9%E8%B1%A1")`String`、`Array`、`TypedArray`、`Map`和`Set`.
* 實例的屬性
* `Set.prototype.constructor`: Set 的構造函數
* `Set.prototype.size`:返回 Set 實例的成員數量
~~~js
let set = new Set([1, 2, 3, 2, 1])
console.info(set.size) // 3
~~~
* 實例的方法
* `add(value)`: 添加某個值,返回 Set 結構本身。
* `delete(value)`:刪除某個值,返回一個布爾值,表示刪除是否成功。
* `has(value)`: 返回一個布爾值,表示該值是否為Set的成員。
* `clear()`: 清除所有成員,沒有返回值。
* 遍歷的方法
* `keys()`:返回鍵名的遍歷器
* `values()`: 返回鍵值得遍歷器
* `entries()`: 返回鍵值對的遍歷器
* `forEach()`: 使用回調函數遍歷每個成員
~~~js
// Set的基礎操作
let set = new Set()
set.add(1).add(2).add(3)
set.has(1) // true
set.has(3) // true
set.delete(1)
set.has(1) // false
// 結合Array.from
const items = new Set([1, 2, 3, 2, 1])
const array = Array.from(items)
console.info(array) // [1, 2, 3]
// 支持解構
const arr = [...set]
console.info(arr) // [1, 2, 3]
// 遍歷
let set = new Set([1, 2, 3])
console.log(set.keys()) // SetIterator {1, 2, 3}
console.log(set.values()) // SetIterator {1, 2, 3}
console.log(set.entries()) // SetIterator {1, 2, 3}
for (let item of set.keys()) {
console.log(item) // 1 2 3
}
for (let item of set.entries()) {
console.log(item) // [1, 1] [2, 2] [3, 3]
}
set.forEach((value, key) => {
console.log(key + ' : ' + value) // 1 : 1 2 : 2 3 : 3
})
console.log([...set]) // [1, 2, 3]
// Set 和容易實現 交集(Intersect)、并集(Union)、差集(Difference)
let set1 = new Set([1, 2, 3])
let set2 = new Set([4, 3, 2])
// 交集
const intersect = [...set1].filter(item => set2.has(item))
// 并集
const union = new Set([...set1, ...set2])
// 差集
const difference = [...set1].filter(item => !set2.has(item))
console.info(intersect, union, difference) // [2, 3] Set{1, 2, 3, 4} [1]
~~~
### Map
> JavaScript 的對象(Object),本質上是鍵值對的集合(Hash 結構),但是傳統上只能用字符串當作鍵。這給它的使用帶來了很大的限制。
~~~js
const data = {}
const element = document.getElementById('myDiv')
data[element] = 'metadata'
data['[object HTMLDivElement]'] // 'metadata'
~~~
上面代碼原意是將一個 DOM 節點作為對象data的鍵,但是由于對象只接受字符串作為鍵名,所以element被自動轉為字符串`[object HTMLDivElement]`。
為了解決這個問題,ES6 提供了`Map`數據結構。它類似于`對象`,也是鍵值對的集合,但是`鍵`的范圍不限于字符串,各種類型的值(包括對象)都可以當作鍵。也就是說,Object 結構提供了`字符串—值`的對應,`Map`結構提供了`值—值`的對應,是一種更完善的`Hash`結構實現。如果你需要`鍵值對`的數據結構,Map 比 Object 更合適。
**Map 的鍵實際上是跟`內存地址`綁定的,只要內存地址不一樣,就視為兩個鍵。**
* 基礎語法
* `new Map([iterable])`
* 參數: iterable接受一個數組作為參數,該數組的成員是一個個表示鍵值對的數組。
* 實例的屬性
* `Map.prototype.constructor`: Map 的構造函數
* `Map.prototype.size`:返回 Map 實例的成員數量
~~~js
const map = new Map([
['name', 'An'],
['des', 'JS']
])
console.info(map.size) // 2
~~~
* 實例的方法
* `set(key, value)`: 設置Map對象中鍵的值。返回該Map對象。
* `get(key)`: 返回鍵對應的值,如果不存在,則返回undefined。
* `delete(value)`:如果 Map 對象中存在該元素,則移除它并返回 true;否則如果該元素不存在則返回 false。
* `has(key)`: 返回一個布爾值,表示Map實例是否包含鍵對應的值。
* `clear()`: 移除Map對象的所有鍵/值對, 沒有返回值。
* 遍歷的方法
* `keys()`:返回鍵名的遍歷器
* `values()`: 返回鍵值得遍歷器
* `entries()`: 返回鍵值對的遍歷器
* `forEach()`: 使用回調函數遍歷每個成員
### WeakSet
WeakSet 對象允許你將弱引用對象儲存在一個集合中 與`Set`的區別
* WeakSet 只能儲存對象引用,不能存放值,而 Set 對象都可以
* WeakSet 對象中儲存的對象值都是被弱引用的,即垃圾回收機制不考慮 WeakSet 對該對象的應用,如果沒有其他的變量或屬性引用這個對象值,則這個對象將會被垃圾回收掉(不考慮該對象還存在于 WeakSet 中),所以,WeakSet 對象里有多少個成員元素,取決于垃圾回收機制有沒有運行,運行前后成員個數可能不一致,遍歷結束之后,有的成員可能取不到了(被垃圾回收了),WeakSet 對象是無法被遍歷的(ES6 規定 WeakSet 不可遍歷),也沒有辦法拿到它包含的所有元素
* 基礎語法
* `new WeakSet([iterable])`
* 參數: iterable傳遞一個可迭代對象,它的所有元素將不重復地被添加到新的`WeakSet`, 返回一個新的`WeakSet`對象。
* 實例的屬性
* `Set.prototype.constructor`: Set 的構造函數
* 實例的方法
* `add(value)`: 添加某個值,返回 Set 結構本身。
* `delete(value)`:刪除某個值,返回一個布爾值,表示刪除是否成功。
* `has(value)`: 返回一個布爾值,表示該值是否為Set的成員。
* 弱引用
> JavaScript 語言中,內存的回收并不是斷開引用后即時觸發的,而是根據運行環境的不同、在不同的運行環境下根據不同瀏覽器的回收機制而異的。比如在 Chrome 中,我們可以在控制臺里點擊 CollectGarbage 按鈕來進行內存回收
~~~js
var test = {
name : 'test',
content : {
name : 'content',
will : 'be clean'
}
};
var ws = new WeakSet()
ws.add(test.content)
console.log('清理前', ws)
test.content = null
console.log('清理后', ws)
~~~
### WeakMap
WeakMap 對象是一組鍵值對的集合,其中的鍵是弱引用對象,而值可以是任意。
WeakMap 中,每個鍵對自己所引用對象的引用都是弱引用,在沒有其他引用和該鍵引用同一對象,這個對象將會被垃圾回收(相應的key則變成無效的),所以,WeakMap 的 key 是不可枚舉的。
* 基礎語法
* `new WeakMap([iterable])`
* 參數: iterable接受一個數組作為參數,該數組的成員是一個個表示鍵值對的數組。
* 實例的屬性
* `Map.prototype.constructor`: Map 的構造函數
* 實例的方法
* `set(key, value)`: 設置Map對象中鍵的值。返回該Map對象。
* `get(key)`: 返回鍵對應的值,如果不存在,則返回undefined。
* `delete(value)`:如果 Map 對象中存在該元素,則移除它并返回 true;否則如果該元素不存在則返回 false。
* `has(key)`: 返回一個布爾值,表示Map實例是否包含鍵對應的值。
~~~js
let myElement = document.getElementById('logo')
let myWeakmap = new WeakMap()
myWeakmap.set(myElement, {timesClicked: 0})
myElement.addEventListener('click', function() {
let logoData = myWeakmap.get(myElement)
logoData.timesClicked++
}, false)
~~~
> 上面代碼中,myElement是一個 DOM 節點,每當發生click事件,就更新一下狀態。我們將這個狀態作為鍵值放在 WeakMap 里,對應的鍵名就是myElement。一旦這個 DOM 節點刪除,該狀態就會自動消失,不存在內存泄漏風險。
### 總結
* Set
* 成員唯一、無序且不重復
* \[value, value\],鍵值與鍵名是一致的(或者說只有鍵值,沒有鍵名)
* 可以遍歷,方法有:add、delete、has
* WeakSet
* 成員都是對象
* 成員都是弱引用,可以被垃圾回收機制回收,可以用來保存DOM節點,不容易造成內存泄漏
* 不能遍歷,方法有add、delete、has
* Map
* 本質上是鍵值對的集合,類似集合
* 可以遍歷,方法很多可以跟各種數據格式轉換 WeakMap
* 只接受對象作為鍵名(null除外),不接受其他類型的值作為鍵名
* 鍵名是弱引用,鍵值可以是任意的,鍵名所指向的對象可以被垃圾回收,此時鍵名是無效的
* 不能遍歷,方法有get、set、has、delete
* Set 與 WeakSet 的區別
* WeakSet只能存放對象
* WeakSet不支持遍歷, 沒有size屬性
* WeakSet存放的對象不會計入到對象的引用技術, 因此不會影響GC的回收
* WeakSet存在的對象如果在外界消失了, 那么在WeakSet里面也會不存在
* Map 與 WeakMap 的區別
* WeakMap只能接受對象作為鍵名字(null除外),不接受其他類型的值作為鍵名
* WeakMap不支持遍歷, 沒有size屬
* WeakMap鍵名指向對象不會計入到對象的引用技術, 因此不會影響GC的回收
## 垃圾回收機制文章
[JavaScript垃圾回收機制](https://www.jianshu.com/p/c99dd69a8f2c "https://www.jianshu.com/p/c99dd69a8f2c")[JavaScript 內存泄漏教程](http://www.ruanyifeng.com/blog/2017/04/memory-leak.html "http://www.ruanyifeng.com/blog/2017/04/memory-leak.html")
## 認識一下遍歷器
> 存在的意義
JavaScript 原有的表示`集合`的數據結構,主要是數組(`Array`)和對象(`Object`),ES6 又添加了`Map`和`Set`。這樣就有了四種數據集合,用戶還可以組合使用它們,定義自己的數據結構,比如數組的成員是Map,Map的成員是對象。這樣就需要一種`統一`的接口機制,來處理所有不同的數據結構。
遍歷器(Iterator)就是這樣一種機制。它是一種接口,為各種不同的數據結構提供統一的訪問機制。任何數據結構只要部署`Iterator`接口,就可以完成遍歷操作(即依次處理該數據結構的所有成員)。
> 作用
* 一是為各種數據結構,提供一個統一的、簡便的訪問接口
* 二是使得數據結構的成員能夠按某種次序排列;
* 三是 ES6 創造了一種新的遍歷命令`for...of`循環,Iterator 接口主要供`for...of`使用。
> 過程或使用
* 創建一個指針對象,指向當前數據結構的起始位置。(也就是說,遍歷器對象本質上,就是一個指針對象)
* 第一次調用指針對象的next方法,可以將指針指向數據結構的第一個成員。
* 第二次調用指針對象的next方法,指針就指向數據結構的第二個成員。
* 不斷調用指針對象的next方法,直到它指向數據結構的結束位置。
> 每一次調用next方法,都會返回數據結構的當前成員的信息。具體來說,就是返回一個包含value和done兩個屬性的對象。其中,value屬性是當前成員的值,done屬性是一個布爾值,表示遍歷是否結束。
~~~js
var it = makeIterator(['a', 'b']);
it.next() // { value: "a", done: false }
it.next() // { value: "b", done: false }
it.next() // { value: undefined, done: true }
function makeIterator(array) {
var nextIndex = 0
return {
next() {
return nextIndex < array.length ?
{value: array[nextIndex++], done: false} :
{value: undefined, done: true}
}
}
}
~~~
對于遍歷器對象來說,`done: false`和`value: undefined`屬性都是可以省略的。
一種數據結構只要部署了`Iterator`接口,我們就稱這種數據結構是`可遍歷的`(iterable)。
ES6 規定,默認的`Iterator`接口部署在數據結構的`Symbol.iterator`屬性,或者說,一個數據結構只要具有`Symbol.iterator`屬性,就可以認為是`可遍歷的`(iterable)。`Symbol.iterator`屬性本身是一個函數,就是當前數據結構默認的遍歷器生成函數。執行這個函數,就會返回一個遍歷器。
內置的[可迭代對象](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Iteration_protocols#%E5%86%85%E7%BD%AE%E5%8F%AF%E8%BF%AD%E4%BB%A3%E5%AF%B9%E8%B1%A1 "https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Iteration_protocols#%E5%86%85%E7%BD%AE%E5%8F%AF%E8%BF%AD%E4%BB%A3%E5%AF%B9%E8%B1%A1")
* `String`
* `Array`
* `TypedArray`
* `Map`
* `Set`
~~~js
let arr = ['a', 'b', 'c']
let iter = arr[Symbol.iterator]()
iter.next() // { value: 'a', done: false }
iter.next() // { value: 'b', done: false }
iter.next() // { value: 'c', done: false }
iter.next() // { value: undefined, done: true }
~~~
對于原生部署`Iterator`接口的數據結構,不用自己寫遍歷器生成函數,`for...of`循環會自動遍歷它們。除此之外,其他數據結構(主要是對象)的`Iterator`接口,都需要自己在`Symbol.iterator`屬性上面部署,這樣才會被`for...of`循環遍歷。
對象(Object)之所以沒有默認部署 Iterator 接口,是因為對象的哪個屬性先遍歷,哪個屬性后遍歷是不確定的,需要開發者手動指定。本質上,遍歷器是一種線性處理,對于任何非線性的數據結構,部署遍歷器接口,就等于部署一種線性轉換。不過,嚴格地說,對象部署遍歷器接口并不是很必要,因為這時對象實際上被當作 Map 結構使用,ES5 沒有 Map 結構,而 ES6 原生提供了。
~~~js
class objIterator {
constructor(obj) {
this.values = Object.keys(obj).map(key => obj[key])
this.length = this.values.length
}
[Symbol.iterator]() {
let index = 0
return {
next: () => {
console.info(index, this.length)
return {
value: this.values[index++],
done: index > this.length
}
}
}
}
}
let newObj = new objIterator({ id: 12, name: '張三'})
for(let item of newObj) {
console.info(item)
}
~~~
重寫數組的`Symbol.iterator`的方法
~~~js
let arr = ['zhangsan', 12, 'hello']
arr[Symbol.iterator] = function() {
let index = 0
return {
next: () => {
if (index < this.length) {
let value = this[index]
if (typeof value == 'string') {
value = value + '加點啥'
}
index++
return {
value,
done: false
}
}
return { done: true, value: '就要輸出值'}
}
}
}
for(let a of arr) {
console.info(a)
}
~~~
一個類似數組的對象調用數組的`Symbol.iterator`方法的例子。
~~~js
let iterable = {
0: 'a',
1: 'b',
2: 'c',
length: 3,
[Symbol.iterator]: Array.prototype[Symbol.iterator]
}
for (let item of iterable) {
console.log(item) // 'a', 'b', 'c'
}
~~~
普通對象部署數組的`Symbol.iterator`方法,并無效果。
~~~js
let iterable = {
a: 'a',
b: 'b',
c: 'c',
length: 3,
[Symbol.iterator]: Array.prototype[Symbol.iterator]
}
for (let item of iterable) {
console.log(item) // undefined, undefined, undefined
}
~~~
javaScript 原有的`for...in`循環,只能獲得對象的鍵名,不能直接獲取鍵值。ES6 提供`for...of`循環,允許遍歷獲得鍵值。
~~~js
// array
var arr = ['a', 'b', 'c', 'd']
for (let a in arr) {
console.log(a) // 0 1 2 3
}
for (let a of arr) {
console.log(a) // a b c d
}
// string
var str = 'abc'
for (let a in str) {
console.log(a) // 0 1 2
}
for (let a of str) {
console.log(a) // a b c
}
~~~
## JS 異步解決方案的發展歷程以及優缺點 - 滴滴、挖財、微醫、海康
**異步編程的語法目標,就是怎樣讓它更像同步編程。**
### 為什么JavaScript是`單線程`
JavaScript語言的一大特點就是單線程,也就是說,同一個時間只能做一件事。那么,為什么JavaScript不能有多個線程呢?這樣能提高效率啊。
JavaScript的單線程,與它的用途有關。作為瀏覽器腳本語言,JavaScript的主要用途是與用戶互動,以及操作DOM。這決定了它只能是單線程,否則會帶來很復雜的同步問題。
> 假定JavaScript同時有兩個線程,一個線程在某個DOM節點上添加內容,另一個線程刪除了這個節點,這時瀏覽器應該以哪個線程為準?
所以,為了避免復雜性,從一誕生,JavaScript就是單線程,這已經成了這門語言的核心特征,將來也不會改變。
為了利用多核CPU的計算能力,HTML5提出Web Worker標準,允許JavaScript腳本創建多個線程,但是子線程完全受主線程控制,且不得操作DOM。所以,這個新標準并沒有改變JavaScript單線程的本質。
### 什么是`異步`
所謂`異步`,簡單說就是一個任務分成兩段,先執行第一段,然后轉而執行其他任務,等做好了準備,再回過頭執行第二段。比如,有一個任務是讀取文件進行處理,異步的執行過程就是下面這樣。
上圖中,任務的第一段是向操作系統發出請求,要求讀取文件。然后,程序執行其他任務,等到操作系統返回文件,再接著執行任務的第二段(處理文件)。
**這種不連續的執行,就叫做異步**。相應地,連續的執行,就叫做同步。
上圖就是同步的執行方式。由于是連續執行,不能插入其他任務,所以操作系統從硬盤讀取文件的這段時間,程序只能干等著。
### `回調函數`
JavaScript 語言對異步編程的實現,就是回調函數。所謂回調函數,就是把任務的第二段單獨寫在一個函數里面,等到重新執行這個任務的時候,就直接調用這個函數。它的英語名字 callback,直譯過來就是"重新調用"。
~~~js
setTimeout(() => {
// callback 函數體
}, 1000)
~~~
### `Promise`
回調函數本身并沒有問題,它的問題出現在多個回調函數嵌套。不難想象,如果依次讀取多個文件,就會出現多重嵌套。這種情況就稱為[回調函數噩夢](http://callbackhell.com/ "http://callbackhell.com/")(callback hell)。
~~~js
ajax('XXX1', () => {
// callback 函數體
ajax('XXX2', () => {
// callback 函數體
ajax('XXX3', () => {
// callback 函數體
...
})
})
})
~~~
`Promise`就是為了解決這個問題而提出的。它不是新的語法功能,而是一種新的寫法。
~~~js
ajax('XXX1').
then(res => {
// 操作邏輯
return ajax('XXX2')
})
.then(res => {
// 操作邏輯
return ajax('XXX3')
})
.then(res => {
// 操作邏輯
})
.catch(function(error) {
// 處理錯誤
})
~~~
Promise 實現了`鏈式`調用,也就是說每次`then`后返回的都是一個全新`Promise`,如果我們在`then`中`return`,`return`的結果會被`Promise.resolve()`包裝。
`Promise`的最大問題是代碼冗余,原來的任務被`Promise`包裝了一下,不管什么操作,一眼看去都是一堆`then`,原來的語義變得很不清楚。
`變相中止`Promise 與[取消狀態](https://github.com/tc39/proposal-cancelable-promises/issues/70 "https://github.com/tc39/proposal-cancelable-promises/issues/70")
~~~js
Promise.resolve().then(function() { return new Promise(function() {}) })
~~~
> 跟傳統的`try/catch`代碼塊不同的是,如果沒有使用`catch()`方法指定錯誤處理的回調函數,`Promise`對象拋出的錯誤不會傳遞到外層代碼,即不會有任何反應。
~~~js
// normal
function someAsyncThing() {
console.info(x + 2)
}
someAsyncThing()
setTimeout(() => { console.log(123) }, 2000)
// Uncaught (in promise) ReferenceError: x is not defined
// 中止運行
// promise
const someAsyncThing = function() {
return new Promise(function(resolve, reject) {
// 下面一行會報錯,因為x沒有聲明
resolve(x + 2)
})
}
someAsyncThing().then(function() {
console.log('everything is great')
})
setTimeout(() => { console.log(123) }, 2000)
// Uncaught (in promise) ReferenceError: x is not defined
// 123
~~~
上面代碼中,`someAsyncThing()`函數產生的`Promise`對象,內部有語法錯誤。瀏覽器運行到這一行,會打印出錯誤提示`ReferenceError: x is not defined`,但是不會退出進程、終止腳本執行,`2`秒之后還是會輸出`123`。這就是說,Promise 內部的錯誤不會影響到 Promise 外部的代碼,通俗的說法就是`Promise 會吃掉錯誤`。
### `Generator`
`Generator`函數是協程在 ES6 的實現,最大特點就是`可以交出函數的執行權`(即**暫停執行**)。
~~~js
function fetch(url, fn) {
return fn()
}
function *action() {
yield fetch('XXX1', () => { return 'zhangsan' })
yield fetch('XXX2', () => { return 'lisi' })
yield fetch('XXX3', () => { return 'wangwu' })
}
// 非鏈式
let it = action()
let result1 = it.next() // zhangsan
let result2 = it.next() // lisi
let result3 = it.next() // wangwu
// 鏈式
var g = action()
var result1 = g.next()
result1.value.then(function(data){
return data
})
.then(function(data){
return g.next(data).value
})
.then(function(data){
return data.json()
})
~~~
* 它不同于普通函數,是可以暫停執行的,所以函數名之前要加星號,以示區別。整個`Generator`函數就是一個封裝的異步任務,或者說是異步任務的容器。異步操作需要暫停的地方,都用`yield`語句注明。
* `Generator`函數不同于普通函數的另一個地方是調用`Generator`函數,會返回一個內部指針(即遍歷器 )`it`,即執行它不會返回結果,返回的是指針對象。調用指針`it`的 next 方法,會移動內部指針。
> `next`方法的作用是分階段執行`Generator`函數。每次調用`next`方法,會返回一個對象,表示當前階段的信息(`value`屬性和`done`屬性)。`value`屬性是`yield`語句后面表達式的值,表示當前階段的值;`done`屬性是一個布爾值,表示`Generator`函數是否執行完畢,即是否還有下一個階段。
**雖然`Generator`函數將異步操作表示得很簡潔,但是流程管理卻不方便(即何時執行第一階段、何時執行第二階段)**
~~~js
function* gen(){
var url = 'https://api.github.com/users/github'
var result = yield fetch(url)
console.log(result.bio)
}
// 執行
var g = gen()
var result = g.next()
result.value.then(function(data){
return data.json()
}).then(function(data){
g.next(data)
})
~~~
`簡易自執行函數`
~~~js
function run(gen){
var g = gen()
function next(data){
var result = g.next(data)
if (result.done) {
return result.value
}
// 使用then執行next,把上一個結果data傳入
result.value.then(function(data){
next(data)
})
}
// 執行next
next()
}
// 自動運行gen函數
run(gen)
~~~
### `async/await`
Generator 函數,依次讀取兩個文件
~~~js
const gen = function* () {
const f1 = yield readFile('/etc/fstab')
const f2 = yield readFile('/etc/shells')
console.log(f1.toString())
console.log(f2.toString())
}
~~~
上面代碼的函數`gen`可以寫成`async`函數,就是下面這樣。
~~~js
const asyncReadFile = async function () {
const f1 = await readFile('/etc/fstab')
const f2 = await readFile('/etc/shells')
console.log(f1.toString())
console.log(f2.toString())
}
~~~
一比較就會發現,async函數就是將`Generator`函數的星號(`*`)替換成`async`,將`yield`替換成`await`。
* `async`函數自帶執行器。`async`函數的執行,與普通函數一模一樣,只要一行。不像 Generator 函數,需要調用next方法,才能真正執行,得到最后結果。
* `async`和`await`,比起`星號`和`yield`,語義更清楚了。`async`表示函數里有異步操作,`await`表示緊跟在后面的表達式需要等待結果。
* 返回值是`Promise`,這比`Generator`函數的返回值是`Iterator`對象方便多了。你可以用`then`方法指定下一步的操作。
**`async`函數的實現原理,就是將`Generator`函數和自動執行器,包裝在一個函數里。**
### 與其他異步處理方法的比較
我們通過一個例子,來看`async`函數與`Promise`、`Generator`函數的比較。
**假定某個`DOM`元素上面,部署了一系列的動畫,前一個動畫結束,才能開始后一個。如果當中有一個動畫出錯,就不再往下執行,返回上一個成功執行的動畫的返回值。**
首先是`Promise`的寫法。
~~~js
function chainAnimationsPromise(elem, animations) {
// 變量ret用來保存上一個動畫的返回值
let ret = null
// 新建一個空的Promise
let p = Promise.resolve()
// 使用then方法,添加所有動畫
for(let anim of animations) {
p = p.then(function(val) {
ret = val
return anim(elem)
})
}
// 返回一個部署了錯誤捕捉機制的Promise
return p.catch(function(e) {
/* 忽略錯誤,繼續執行 */
}).then(function() {
return ret
})
}
~~~
雖然`Promise`的寫法比回調函數的寫法大大改進,但是一眼看上去,代碼完全都是`Promise`的`API`(`then`、`catch`等等),操作本身的語義反而不容易看出來。
接著是`Generator`函數的寫法。
~~~js
function chainAnimationsGenerator(elem, animations) {
return spawn(function*() {
let ret = null
try {
for(let anim of animations) {
ret = yield anim(elem)
}
} catch(e) {
/* 忽略錯誤,繼續執行 */
}
return ret
})
}
~~~
自執行函數`spawn`
~~~js
function spawn(genF) {
return new Promise(function(resolve, reject) {
const gen = genF()
function step(nextF) {
let next
try {
next = nextF()
} catch(e) {
return reject(e)
}
if(next.done) {
return resolve(next.value);
}
Promise.resolve(next.value).then(function(v) {
step(function() {
return gen.next(v)
})
}, function(e) {
step(function() {
return gen.throw(e)
})
})
}
step(function() {
return gen.next(undefined)
})
})
}
~~~
上面代碼使用`Generator`函數遍歷了每個動畫,語義比`Promise`寫法更清晰,用戶定義的操作全部都出現在spawn函數的內部。這個寫法的問題在于,必須有一個任務運行器,自動執行`Generator`函數,上面代碼的`spawn`函數就是自動執行器,它返回一個`Promise`對象,而且必須保證`yield`語句后面的表達式,必須返回一個`Promise`。
最后是`async`函數的寫法。
~~~js
async function chainAnimationsAsync(elem, animations) {
let ret = null
try {
for(let anim of animations) {
ret = await anim(elem)
}
} catch(e) {
/* 忽略錯誤,繼續執行 */
}
return ret
}
~~~
可以看到`Async`函數的實現最簡潔,最符合語義,幾乎沒有語義不相關的代碼。它將`Generator`寫法中的自動執行器,改在語言層面提供,不暴露給用戶,因此代碼量最少。如果使用`Generator`寫法,自動執行器需要用戶自己提供。
### 異步總結
* `回調函數`
* 優點
* **解決了同步的問題**(只要有一個任務耗時很長,后面的任務都必須排隊等著,會拖延整個程序的執行。)
* 缺點(**回調地獄**)
* `缺乏順序性`:回調地獄導致的調試困難。
* 嵌套函數存在耦合性,一旦有所改動,就會牽一發而動全身,即(`控制反轉`)。
* 嵌套函數過多的多話,很難處理錯誤。
* `Promise`
* 特點
* 對象的狀態不受外界影響。
* 一旦狀態改變,就不會再變,任何時候都可以得到這個結果。
* 優點
* 解決了`回調地獄`的問題
* 回調函數變成了鏈式寫法,程序的流程可以看得很清楚,而且有一整套的配套方法,可以實現許多強大的功能。
* 缺點
* 無法取消Promise,一旦新建它就會立即執行,無法中途取消。
* 如果不設置回調函數,Promise內部拋出的錯誤,不會反應到外部。
* 當處于pending狀態時,無法得知目前進展到哪一個階段(剛剛開始還是即將完成,要在用戶界面展示進度條)。
* `Generator`
* 特點
* 可以交出函數的執行權(即暫停執行)
* 優點
* 將異步操作表示得很簡潔
* 缺點
* 流程管理卻不方便
* 需要手動執行next
* `async/await`
* 優點
* 代碼清晰,語義化更強,不用像`Promise`寫一大堆`then`鏈
* 返回值是`Promise`
* `async`函數自帶執行器
* 缺點
* await 將異步代碼改造成同步代碼,如果多個異步操作沒有依賴性而使用`await`會導致性能上的降低。
### 引用
[Javascript異步編程的4種方法](http://www.ruanyifeng.com/blog/2012/12/asynchronous%EF%BC%BFjavascript.html "http://www.ruanyifeng.com/blog/2012/12/asynchronous%EF%BC%BFjavascript.html")
[再談Event Loop](http://www.ruanyifeng.com/blog/2014/10/event-loop.html "http://www.ruanyifeng.com/blog/2014/10/event-loop.html")
[Generator 函數的含義與用法](http://www.ruanyifeng.com/blog/2015/04/generator.html "http://www.ruanyifeng.com/blog/2015/04/generator.html")
[Generator](https://www.liaoxuefeng.com/wiki/1022910821149312/1023024381818112 "https://www.liaoxuefeng.com/wiki/1022910821149312/1023024381818112")
[Generator-ES6入門](https://es6.ruanyifeng.com/#docs/generator "https://es6.ruanyifeng.com/#docs/generator")
[深入理解 Generators](http://www.alloyteam.com/2016/02/generators-in-depth/ "http://www.alloyteam.com/2016/02/generators-in-depth/")
[JS 異步解決方案的發展歷程以及優缺點](https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/11 "https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/11")
[co函數庫](http://www.ruanyifeng.com/blog/2015/05/co.html "http://www.ruanyifeng.com/blog/2015/05/co.html")
- 版本控制之Git簡介
- Git工作流程
- Git工作區、暫存區、版本庫
- Git 指令匯總
- Git 忽略文件規則 .gitignore
- pull request
- HTTP簡介
- HTTP - Keep-Alive
- HTTP緩存
- XMLHttpRequest
- Fetch
- 跨域
- HTTP 消息頭
- TCP/IP
- TCP首部
- IP首部
- IP 協議
- TCP/IP漫畫
- 前端開發規范
- 前端開發規范整理
- 前端未來規劃
- HTML思維導圖
- CSS思維導圖
- 布局
- position,float,display的關系和優先級
- line-height、height、font-size
- 移動端適配
- JS 對象
- JS 原型模式 - 創建對象
- JS 預編譯
- 探索JS引擎
- ES