# 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单线程的本质。
### 什么是`异步`
所谓`异步`,简单说就是一个任务分成两段,先执行第一段,然后转而执行其他任务,等做好了准备,再回过头执行第二段。比如,有一个任务是读取文件进行处理,异步的执行过程就是下面这样。
![异步](vscode-webview-resource://6f0f06f2-19fc-435d-90f8-d810823e6ed0/file///Users/magicdata/Documents/share/ES/img/%E5%BC%82%E6%AD%A5.png)上图中,任务的第一段是向操作系统发出请求,要求读取文件。然后,程序执行其他任务,等到操作系统返回文件,再接着执行任务的第二段(处理文件)。
**这种不连续的执行,就叫做异步**。相应地,连续的执行,就叫做同步。
![同步](vscode-webview-resource://6f0f06f2-19fc-435d-90f8-d810823e6ed0/file///Users/magicdata/Documents/share/ES/img/%E5%90%8C%E6%AD%A5.png)上图就是同步的执行方式。由于是连续执行,不能插入其他任务,所以操作系统从硬盘读取文件的这段时间,程序只能干等着。
### `回调函数`
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