🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
# 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")