什么?ES6 中还有 Tail Calls!
栏目: JavaScript · 发布时间: 5年前
内容简介:先吐槽一件事,最近把原先的 TOP 域名更换到 CN 域名,并且用有人要问了,都 9102 年,ES10 都出来了,怎么还在讲 ES6,非也!本文针对 ES6 几个不为人知、和重要的特性做讲解,精彩的在后面!ES6 除了固有的函数作用域,还引入了
先吐槽一件事,最近把原先的 TOP 域名更换到 CN 域名,并且用 Gatsby
重建个人站点,之前是用采用 HTTPS 部署的方式绕过阿里云的域名备案系统。更换 CN 域名后,这招不管用了,:sob::sob: 域名必须要备案了,等待幕布邮寄中……
有人要问了,都 9102 年,ES10 都出来了,怎么还在讲 ES6,非也!本文针对 ES6 几个不为人知、和重要的特性做讲解,精彩的在后面!
基础篇
Let + Const
ES6 除了固有的函数作用域,还引入了 块级作用域({})
function f() { { let x; // ① { // 包含在当前块中,与 ① 中的 x 分属不同作用域 const x = "sneaky"; // 错误,const 定义的变量不可以重新赋值,如果 const 定义了一个对象,那么对象的属性是可以修改的 x = "foo"; } // let 定义的变量可以重新赋值 x = "bar"; // 错误,x 在 ① 块中已被定义 let x = "inner"; } } 复制代码
默认、剩余、展开参数(Default + Rest + Spread)
function f(x, y=12) { // y 等于 12 如果不传递 (或者传递 undefined) return x + y; } f(3); // 15 复制代码
function f(x, ...y) { // y 是一个数组 return x * y.length; } f(3, "hello", true); // 6 复制代码
function f(x, y, z) { return x + y + z; } // 将数组的每一项作为参数传递 f(...[1,2,3]); // 6 复制代码
解构(Destructuring)
var [a, ,b] = [1,2,3]; a === 1; // true b === 3; // true var { op: a, lhs: { op: b }, rhs: c } = getASTNode() // var {op: op, lhs: lhs, rhs: rhs} = getASTNode() var {op, lhs, rhs} = getASTNode() // 参数解构 function g({name: x}) { console.log(x); } g({name: 5}) var [a] = []; a === undefined; // true var [a = 1] = []; a === 1; // true // 解构 + 默认参数 function r({x, y, w = 10, h = 10}) { return x + y + w + h; } r({x:1, y:2}) === 23 // true 复制代码
箭头函数(Arrows and Lexical This)
// 除了支持返回语句,还可以将表达式作为返回主体 const foo = () => ({ name: 'es6' }); const bar = (num) => (num++, num ** 2); foo(); // 返回一个对象 { name: 'es6' } bar(3); // 执行多个表达式,并返回最后一个表达式的值 16 复制代码
JS 中 this
的指向问题一直都是面试高频考点,不少人在实战中也掉入坑中,总结起来就是一句话:“ this
永远指向调用它的那个对象 ”,而箭头函数则改写了这一规则,就是
箭头函数共享当前代码上下文的 this
什么意思呢?可以理解为
- 箭头函数不会创建自己的
this
, 它只会从自己的作用域链的上一层继承this
,如果上一层还是箭头函数,则继续向上查找,直至全局作用域,在浏览器环境下即window
。 - 函数具有作用域链,对象则不具有
因此,在下面的代码中,传递给 setInterval
的函数内的 this
与 sayHello
函数中的 this
一致:
const bob = { name: 'Bob', sayHello() { setTimeout(() => { console.log(`hello, I am ${this.name}`); }, 1000); } }; const hello = bob.sayHello; bob.sayHello(); // hello, I am Bob // 作为对象的方法调用,sayHello的this指向bob hello(); // hello, I am undefined // 作为普通函数调用,相当于window.hello(),this指向全局对象 hello.call({name:'Mike'}); // hello, I am Mike // call,apply调用,第一个参数为this指向的对象 复制代码
language = 'Python'; const obj = { language: 'TS', speak() { language = 'GO'; return function() { return () => { console.log(`I speak ${this.language}`); }; }; } }; obj.speak()()(); // 做个小测试,会打印什么呢? 复制代码
箭头函数还有以下特点
- 由于箭头函数没有自己的
this
指针,通过call
或apply
调用,第一个参数会被忽略 - 不绑定
Arguments
对象,其引用上一层作用域链的Arguments
对象 - 不能用作构造器,和
new
一起用会抛出错误。 - 没有
prototype
属性。
现在你应该明白为何 React 中的函数写法都为箭头函数,就是为了绑定 this
Symbols
ES6 引入了一种新的原始数据类型 Symbol
,表示独一无二的值,它的功能类似于一种标识唯一性的 ID
// 每个 Symbol 实例都是唯一的。因此,当你比较两个 Symbol 实例的时候,将总会返回 false const s1 = Symbol('macOS'); const s2 = Symbol('macOS'); // Symbol.for 机制有点类似于单例模式 const s3 = Symbol.for('windows'); // 注册一个全局 Symbol const s4 = Symbol.for('windows'); // 已存在相同名称的 Symbol,返回全局 Symbol s1 === s2; // false s3 === s4; // true 复制代码
let key = Symbol('key'); function MyClass(privateData) { // 注意,Symbol值作为对象属性名时,不能用点运算符 this[key] = privateData; } MyClass.prototype = { doStuff() { console.log(this[key]); } }; // Symbol的一些特性必须要浏览器的原生实现,不可被 transpiled 或 polyfilled typeof key // symbol let c = new MyClass('hello'); c.key; // undefined c[key]; // hello 复制代码
应用场景
- 更好的设计我们的数据对象,让“对内操作”和“对外选择性输出”变得更加优雅。
在实际应用中,我们经常会需要使用 Object.keys()
或者 for...in
来枚举对象的属性名,那在这方面, Symbol
类型的 key
表现的会有什么不同之处呢?来看以下示例代码:
let obj = { [Symbol('name')]: '一斤代码', age: 18, title: 'Engineer' } Object.keys(obj) // ['age', 'title'] for (let p in obj) { console.log(p) // 分别会输出:'age' 和 'title' } Object.getOwnPropertyNames(obj) // ['age', 'title'] 复制代码
也正因为这样一个特性,当使用 JSON.stringify()
将对象转换成 JSON
字符串的时候, Symbol
属性也会被排除在输出内容之外:
JSON.stringify(obj) // {"age":18,"title":"Engineer"} 复制代码
由上代码可知, Symbol
类型的 key
是不能通过 Object.keys()
或者 for...in
来枚举的,所以,利用该特性,我们可以把一些不需要对外操作和访问的属性使用 Symbol
来定义。
- 消除魔术字符串
const TYPE_AUDIO = 'AUDIO' const TYPE_VIDEO = 'VIDEO' const TYPE_IMAGE = 'IMAGE' function handleFileResource(resource) { switch(resource.type) { case TYPE_AUDIO: playAudio(resource) break case TYPE_VIDEO: playVideo(resource) break case TYPE_IMAGE: previewImage(resource) break default: throw new Error('Unknown type of resource') } } 复制代码
上面的代码中那样,我们需要为常量赋一个唯一的值(比如这里的 'AUDIO'
), 'AUDIO'
就是一个魔术字符串,它本身没意义,只是为了保证常量唯一的关系。常量一多,就变得十分臃肿且难以理解
现在有了 Symbol
,我们大可不必这么麻烦了:
// 保证了三个常量的值是唯一的 const TYPE_AUDIO = Symbol() const TYPE_VIDEO = Symbol() const TYPE_IMAGE = Symbol() 复制代码
增强的对象字面量(Enhanced Object Literals)
const obj = { // 允许设置原型 __proto__: theProtoObj, // 允许覆盖属性 ['__proto__']: somethingElse, // 属性简写,等于 ‘handler: handler’ handler, // 计算 (动态) 属性名 ['prop_' + (() => 42)()]: 42 }; obj.prop_42 // 42 obj.__proto__ // somethingElse 复制代码
__proto__
需要原生支持,它在之前的 ECMAScript
版本中被移除,但大多数浏览器都实现了这一特性,包括 Node
环境
Map + Set + WeakMap + WeakSet
Set
Set
是 ES6 中新增的数据结构,它允许创建唯一值的集合。集合中的值可以是简单的基本类型(如字符串或数值),但更复杂的对象类型(如对象或数组)也可以,亦或是一个新的 Set
let animals = new Set(); animals.add(':pig:'); animals.add(':panda_face:'); animals.add(':turtle:'); animals.add(' '); console.log(animals.size); // 4 animals.add(':panda_face:'); console.log(animals.size); // 4 console.log(animals.has(':pig:')); // true animals.delete(':pig:'); console.log(animals.has(':pig:')); // false animals.forEach(animal => { console.log(`Hey ${animal}!`); }); // Hey :panda_face:! // Hey :turtle:! // Hey ! animals.clear(); console.log(animals.size); // 0 复制代码
我们还可以传入一个数组来初始化集合
let myAnimals = new Set([':pig:', ':turtle:', ':pig:', ':pig:']); myAnimals.add([':koala:', ':sheep:']); myAnimals.add({ name: 'Rud', type: ':turtle:' }); console.log(myAnimals.size); // 4 // Set 内置了遍历器,可以调用 forEach, for…of myAnimals.forEach(animal => { console.log(animal); }); // :pig: // :turtle: // [":koala:", ":sheep:"] // Object { name: "Rud", type: ":turtle:" } 复制代码
Map
与普通对象( Object
)不同, Map
的键名( Key
)可以是任何类型,不再局限于字符串( String
),包括但不限于 objects
或 functions
let things = new Map(); const myFunc = () => ':pizza:'; things.set(':car:', 'Car'); things.set(':house:', 'House'); things.set(':airplane:', 'Airplane'); things.set(myFunc, ':smile: Key is a function!'); things.size; // 4 things.has(':car:'); // true things.has(myFunc) // true things.has(() => ':pizza:'); // false things.get(myFunc); // ':smile: Key is a function!' things.delete(':airplane:'); things.has(':airplane:'); // false things.clear(); things.size; // 0 // 链式设置键值对 things.set(':wrench:', 'Wrench') .set(':guitar:', 'Guitar') .set(' ', 'Joystick'); const myMap = new Map(); // 甚至键名可以是另一个 Map things.set(myMap, 'Oh gosh!'); things.size; // 4 things.get(myMap); // 'Oh gosh!' 复制代码
可以通过传入包含两个元素的数组来初始化 Map
const funArray = [ [' ', 'Champagne'], [':lollipop:', 'Lollipop'], [':confetti_ball:', 'Confetti'], ]; let funMap = new Map(funArray); funMap.get(' '); // Champagne 复制代码
WeakMap
WeakMap
对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。它最重要的特性是 WeakMap
保持了对键名所引用的对象的弱引用
我们可以通过 Node 来证明一下这个问题:
// 允许手动执行垃圾回收机制 node --expose-gc global.gc(); // 返回 Nodejs 的内存占用情况,单位是 bytes process.memoryUsage(); // heapUsed: 4640360 ≈ 4.4M let map = new Map(); let key = new Array(5 * 1024 * 1024); // new Array 当为 Obj map.set(key, 1); global.gc(); process.memoryUsage(); // heapUsed: 46751472 注意这里大约是 44.6M // 所以当你设置 key = null 时,只是去掉了 key 对 Obj 的强引用 // 并没有去除 arr 对 Obj 的强引用,所以 Obj 还是不会被回收掉 key = null; global.gc(); process.memoryUsage(); // heapUsed: 46754648 ≈ 44.6M // 这句话其实是无用的,因为 key 已经是 null 了 map.delete(key); global.gc(); process.memoryUsage(); // heapUsed: 46755856 ≈ 44.6M 复制代码
node --expose-gc global.gc(); process.memoryUsage(); // heapUsed: 4638992 ≈ 4.4M const wm = new WeakMap(); let key = new Array(5 * 1024 * 1024); wm.set(key, 1); global.gc(); process.memoryUsage(); // heapUsed: 46776176 ≈ 44.6M // 当我们设置 key = null 的时候,就只有 wm 对所引用对象的弱引用 // 下次垃圾回收机制执行的时候,该引用对象就会被回收掉。 key = null; global.gc(); process.memoryUsage(); // heapUsed: 4800792 ≈ 4.6M 复制代码
应用场景
传统使用 jQuery
的时候,我们会通过 $.data()
方法在 DOM
对象上储存相关信息(就比如在删除按钮元素上储存帖子的 ID 信息), jQuery
内部会使用一个对象管理 DOM
和对应的数据,当你将 DOM
元素删除, DOM
对象置为空的时候,相关联的数据并不会被删除,你必须手动执行 $.removeData()
方法才能删除掉相关联的数据, WeakMap
就可以简化这一操作:
let wm = new WeakMap(), element = document.querySelector(".element"); wm.set(element, "data"); let value = wm.get(elemet); console.log(value); // data element.parentNode.removeChild(element); element = null; 复制代码
WeakSet
特性与 WeakMap
相似
遍历器(Iterators + For..Of)
遍历器( Iterator
)它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator
接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。 Iterator
的作用有三个:
- 为各种数据结构,提供一个统一的、简便的访问接口;
- 使得数据结构的成员能够按某种次序排列;
- ES6 创造了一种新的遍历命令
for...of
循环,Iterator
接口主要供for...of
消费。
ES6 规定,默认的 Iterator
接口部署在数据结构的 Symbol.iterator
属性,或者说,一个数据结构只要具有 Symbol.iterator
属性,就可以认为是“可遍历的”( iterable
)
let fibonacci = { [Symbol.iterator]() { let pre = 0, cur = 1; return { next() { [pre, cur] = [cur, pre + cur]; // 数组解构 return { done: false, value: cur } } } } } for (var n of fibonacci) { // 当n超过1000时停止 if (n > 1000) break; console.log(n); } 复制代码
上面代码中,对象 fibonacci
是可遍历的( iterable
),因为具有 Symbol.iterator
属性。执行这个属性,会返回一个遍历器对象。该对象的根本特征就是具有 next
方法。每次调用 next
方法,都会返回一个代表当前成员的信息对象,具有 value
和 done
两个属性
原生具备 Iterator
接口的数据结构如下
- Array
- Map
- Set
- String
- TypedArray
- 函数的 arguments 对象
- NodeList 对象
for...in 和 for..of 的差别
- for...in 遍历键名(Key)并转化为字符串,for...of 遍历键值(Value)
- for...in 语句以任意顺序遍历一个对象自有的、继承的、可枚举的、非 Symbol 的属性
- for...in 更适合遍历对象,for...of 更适合遍历数组。
for (let i in [1, 2, 3]) { console.log(typeof i); // string 数组下标被转化字符串 console.log(i); // '1', '2', '3' } var triangle = { a: 1, b: 2, c: 3 }; function ColoredTriangle() { this.color = 'red'; } ColoredTriangle.prototype = triangle; var obj = new ColoredTriangle(); for (var prop in obj) { if (obj.hasOwnProperty(prop)) { // 如果去了 hasOwnProperty() 这个约束条件会怎么样? console.log(`obj.${prop} = ${obj[prop]}`); // obj.color = red } } 复制代码
新增 API(Math + Number + String + Object APIs)
我们先来看看新增的 Number.EPSILON
,不少人都是懵逼的状态,WTF?
先来看看的 JS 世界中的一道送命题
0.1 + 0.2 // 结果0.30000000000000004 而不是0.3 复制代码
事出必有因,这是因为 JS 的数值采用了 IEEE 754
标准,而且 JS 是弱类型语言,所以数字都是以64位双精度浮点数据类型储存。也就是说,JS 语言底层根本没有整数,所有数字都是小数!当我们以为在用整数进行计算时,都会被转换为小数
而浮点数都是以多位二进制的方式进行存储的
十进制的0.1用二进制表示为:0.0 0011 0011 0011 0011…,循环部分是0011
十进制0.2用二进制表示为:0.0011 0011 0011 0011…,循环部分是0011
由于存储空间有限,最后计算机会舍弃后面的数值,所以我们最后就只能得到一个近似值
JS中采用的 IEEE 754
的双精度标准也是一样的道理在存储空间有限的情况下,当出现这种无法整除的小数的时候就会取一个近似值,在 JS 中如果这个近似值足够近似,那么 JS 就会认为他就是那个值。
console.log(0.1000000000000001) // 0.1000000000000001 (中间14个0,不会被近似处理,输出本身) console.log(0.10000000000000001) // 0.1 (中间15个0,js会认为两个值足够近似,所以输出0.1) 复制代码
那么这个近似的界限如何判断呢?
ES6的 Number.EPSILON
就是一个界限,它表示 1 与大于 1 的最小浮点数之间的差。
对于 64 位浮点数来说,大于 1 的最小浮点数相当于二进制的1.00..001,小数点后面有连续 51 个零。这个值减去 1 之后,就等于 2 的 -52 次方
Number.EPSILON === Math.pow(2, -52) // true Number.EPSILON // 2.220446049250313e-16 Number.EPSILON.toFixed(20) // "0.00000000000000022204" 复制代码
Number.EPSILON
实际上是 JavaScript 能够表示的最小精度。误差如果小于这个值,就可以认为已经没有意义了,即不存在误差了。
0.1 + 0.2 - 0.3 // 5.551115123125783e-17 5.551115123125783e-17.toFixed(20) // '0.00000000000000005551' 复制代码
0.00000000000000005551 < 0.00000000000000022204 // true 复制代码
显然, 0.30000000000000004
不存在误差,不会被近似处理
我们可以通过以下手段来达到我们想要的效果
function withinErrorMargin (left, right) { return Math.abs(left - right) < Number.EPSILON * Math.pow(2, 2); } 0.1 + 0.2 === 0.3 // false withinErrorMargin(0.1 + 0.2, 0.3) // true 复制代码
其他一些新增的 API
Number.isInteger(Infinity) // false Number.isNaN("NaN") // false Math.sign(-5) // 判断一个数到底是正数、负数、还是零 -1 Math.hypot(3, 4) // 返回所有参数的平方和的平方根 5 Math.imul(-2, -2) // 返回两个数以 32 位带符号整数形式相乘的结果 4 "abcde".includes("cd") // true "abc".repeat(3) // "abcabcabc" Array.from(document.querySelectorAll("*")) // 返回一个真正的数组 Array.of(1, 2, 3) // [1,2,3] [0, 0, 0].fill(7, 1) // [0,7,7] [1,2,3].findIndex(x => x == 2) // 1 ["a", "b", "c"].entries() // [0, "a"], [1,"b"], [2,"c"] ["a", "b", "c"].keys() // 0, 1, 2 ["a", "b", "c"].values() // "a", "b", "c" Object.assign(Point, { origin: new Point(0,0) }) // 合并对象 复制代码
二进制和八进制字面量(Binary and Octal Literals)
0b111 === 7 // true 二进制 0o111 === 73 // true 八进制 0x111 === 273 // true 十六进制 复制代码
进阶篇
尾递归(Tail Calls)
假设现在要实现一个阶乘函数,即 5!= 120
,我们很容易想到递归实现
function factorial(n) { if (n === 1) return 1; return n * factorial(n - 1); } 复制代码
但递归非常耗费内存,因为需要同时保存成千上百个调用记录,很容易发生"栈溢出"错误( stack overflow
)。但对于尾递归来说,由于只存在一个调用记录,所以永远不会发生"栈溢出"错误。
何为调用记录,在示例代码中,由于最后一步返回了一个表达式,内存会保留 n
这个变量的信息和 factorial(n - 1)
调用下一次函数的位置,形成一层层的调用栈
尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身,返回函数本身。做到这一点的方法,就是把所有用到的内部变量改写成函数的参数。尾递归优化如下
function factorial(n, acc = 1) { "use strict"; if (n <= 1) return acc; return factorial(n - 1, n * acc); } factorial(100000) 复制代码
由此可见,"尾调用优化"对递归操作意义重大,所以一些函数式编程语言将其写入了语言规格。ES6 也是如此,第一次明确规定,所有 ECMAScript
的实现,都必须部署"尾调用优化"。这就是说,在 ES6 中,只要使用尾递归,就不会发生栈溢出,相对节省内存。
ES6的尾调用优化只在严格模式下开启,正常模式是无效的。
反射(Reflect)
Reflect
对象与 Proxy
对象一样,也是 ES6 为了操作对象而提供的新 API。 Reflect
对象的设计目的有这样几个。
- 将Object对象的一些明显属于语言内部的方法(比如
Object.defineProperty
),放到Reflect
对象上 - 修改某些
Object
方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)
在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)
则会返回false
。
var O = {a: 1}; Reflect.defineProperty(O, 'b', {value: 2}); O[Symbol('c')] = 3; Reflect.ownKeys(O); // ['a', 'b', Symbol(c)] Reflect.getOwnPropertyDescriptor(O, 'b'); // { value: 2, writable: false, enumerable: false, configurable: false } function C(a, b){ this.c = a + b; } var instance = Reflect.construct(C, [20, 22]); instance.c; // 42 复制代码
获取属性名的方法有很多,以上面的代码为例子,它们的区别如下
方法 | 结果 | 解释 |
---|---|---|
Object.getOwnPropertyNames(O) |
[ 'a', 'b' ] |
获取除 Symbol 外的所有属性 |
Object.getOwnPropertySymbols(O) |
[ Symbol(c) ] |
只获取 Symbol 属性 |
OReflect.ownKeys(O) |
[ 'a', 'b', Symbol(c) ] |
获取所有属性 |
for...in |
a |
获取除 Symbol 外的可枚举属性 |
代理(Proxy)
Proxy
可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。 Proxy
这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。
// 代理一个对象 var target = {}; var handler = { get: function (receiver, name) { return `Hello, ${name}!`; } }; var p = new Proxy(target, handler); p.world; // "Hello, world!" 复制代码
// 代理一个函数 var target = function () { return "I am the target"; }; var handler = { apply: function (receiver, ...args) { return "I am the proxy"; } }; var p = new Proxy(target, handler); p(); // "I am the proxy" 复制代码
// 代理会将所有应用到它的操作转发到这个对象上 let target = {}; let p = new Proxy(target, {}); p.a = 37; target.a; // 37 操作转发到目标 复制代码
// 如何实现 a == 1 && a == 2 && a == 3,利用Proxy的get劫持 const a = new Proxy( {}, { val: 1, get() { return () => this.val++; } } ); a == 1 && a == 2 && a == 3; // true 复制代码
由于 ES5 的限制,Proxy 不能被 transpiled or polyfilled,自己亲自入的坑,由于在项目中使用了 Mobx5.x,其内部是用 Proxy 写的,结果 IE11 不支持 ES6,只得回退版本 Mobx 到 4.x
生成器(Generators)
Generator
函数是 ES6 提供的一种异步编程解决方案。 Generator
函数有多种理解角度。语法上,首先可以把它理解成, Generator
函数是一个状态机,封装了多个内部状态。
形式上, Generator
函数是一个普通函数,但是有两个特征。一是, function
关键字与函数名之间有一个星号;二是,函数体内部使用 yield
表达式,定义不同的内部状态
function* helloWorldGenerator() { yield 'hello'; // yield使Generator函数暂停了执行,并将结果返回给调用者 yield 'world'; // 当下一次调用时,从它中断的地方恢复执行 return 'ending'; } var hw = helloWorldGenerator(); a = hw.next(); // { value: 'hello', done: false } b = hw.next(); // { value: 'world', done: false } c = hw.next(); // { value: 'ending', done: true } 复制代码
可以利用这种暂停执行的特性,来实现惰性求值
向Generator传递数据
function* sayFullName() { const firstName = yield; const secondName = yield; console.log(firstName + ' ' + secondName); } let fullName = sayFullName(); fullName.next(); // 第一次调用,代码暂停在 const firstName = yield,因为没有通过 yield 发送任何值,因此 next 将返回 undefined fullName.next('Handsome'); // 第二次调用,传入了值 Handsome,yield 被 Handsome 替代,因此 firstName 的值变为 Handsome,代码执行恢复 // 直到再次遇到 const secondName = yield 暂停执行 fullName.next('Jack'); // 第三次调用,传入了值 Jack,yield 被 Jack 替代,因此 secondName 的值变为 Jack,代码执行恢复 // 打印 Handsome Jack 复制代码
使用Generator处理异步调用
let generator; let getDataOne = () => { setTimeout(() => { generator.next('dummy data one'); }, 1000); }; let getDataTwo = () => { setTimeout(() => { generator.next('dummy data one'); }, 1000); }; function* main() { let dataOne = yield getDataOne(); let dataTwo = yield getDataTwo(); console.log(dataOne, dataTwo); } generator = main(); generator.next(); // 执行 getDataOne(),然后 yield 暂停 // 直至一秒后 generator.next('dummy data one') 恢复代码执行,并赋值 dataOne console.log('i am previous print'); // i am previous print // dummy data one dummy data one 复制代码
Promises
Promises
是一个异步编程的解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。
所谓 Promise
,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说, Promise
是一个对象,从它可以获取异步操作的消息。 Promise
提供统一的 API
,各种异步操作都可以用同样的方法进行处理。
function timeout(duration = 0) { return new Promise((resolve, reject) => { setTimeout(resolve, duration); }) } var p = timeout(1000).then(() => { return timeout(2000); }).then(() => { throw new Error("hmm"); }).catch(err => { return Promise.all([timeout(100), timeout(200)]); }) 复制代码
这里强调几点
- 不要剥夺函数
return
的能力,很多人写Promise
,照样有大量嵌套,掉进Promise
地狱,要记得及时return
,避免嵌套 - 当需要多个请求全部结束时,才更新数据,可以用
Promise.all(fetch1,fetch2)
- 当需要从多个请求中,接受最先返回数据的那个请求,可以用
Promise.race(fetch1,fetch2)
结尾
ES6 是 ECMAScript
一个非常重要的版本,我们必须深入理解,不仅能提高我们书写代码的能力,还能增强业务能力
附上一张我之前精心整理的思维导图
本文参考资料
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 聊聊 WebSocket,还有 HTTP
- 威胁建模还有多少用武之地
- “地震波还有61秒到达”
- Webpack模块引用中还有什么坑?
- Kubernetes的集群联邦之路还有多远?
- Cocos活久见!忽悠还有直男式的?
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
HTML 压缩/解压工具
在线压缩/解压 HTML 代码
MD5 加密
MD5 加密工具