首发地址:https://mp.weixin.qq.com/s/A-...
“ 用最精简的方式,抓住最核心的知识点,帮助你快读ES6。”
这几个知识点在【框架设计】以及【任务流控制】中起着重要作用,属于必备知识。本文部分内容需要你具备相关基本知识,帮你划出重点,结合笔者的一些思考,有查缺补漏之功效。
关键词:Proxy、Promise、Iterator、Generator、Async
Proxy
- 原理:重载了
点运算符
- 存在This指向问题
Proxy 针对目标对象的访问,不是透明代理,即不做任何拦截的情况下,也无法保证与目标对象的行为一致。主要原因就是在 Proxy 代理的情况下,目标对象内部的this关键字会指向 Proxy 代理
。 -
Proxy实例的方法
1)get(target, property, proxy)
- 作为原型对象
Object.create(new Proxy({}, handler))
, handler中的get可以被继承 - 实现属性链式操作(
内部DSL:**级联属性**
):pipe(3).double.pow.get;
-
dom.div | dom.h1 |...
:用作生成各种DOM节点的通用函数,避免穷举(内部DSL:**动态代理**
)备注:内部DSL相关介绍,可参考:内部DSL,你不可不知
- 作为原型对象
2)set(target, property, value, proxy)
- vue实现的数据双向绑定:数据初始化时拦截
getter
做依赖收集,当编译模板时创建watcher对象,触发数据的读操作,将watcher对象存入deps,当数据变更,触发setter
执行deps中的回调进行更新操作; - 内部会调用Proxy的
defineProperty()
Promise
异步编程(回调函数 -> 事件监听 -> 发布订阅 -> promise,Generator,async 对 流程、时间 控制的探索)
特点
- 链式调用的方式:适合
流程、时间
敏感的场景; - 状态不受外界影响;
- 回调函数注册时机不必须在异步执行完前。
不足
- 进度未知
- 无法中途取消
原型方法
- Promise.prototype.then()
-
Promise.prototype.**catch**()
是.then(undefined, rejection)
的语法糖 -
Promise.prototype.**finally**()
是.then(() => {}, () => {})
的语法糖
Promise私有方法
-
resolve():等价于
new Promise(resolve => resolve(参数))
,其中参数的形式如下:- promise:原样返回此实例
- thenable
- 原始值
- 无参数
-
reject()
- 其参数会
原封不动的作为reject的理由
,和resolve不同;
- 其参数会
-
all()
- 用于将多个 Promise 实例,包装成一个新的 Promise 实例;
- 如果数组中的值不是Promise实例,会
先调用Promise.resolve方法
,将参数转为 Promise 实例 - 参数可以不是数组,但
必须具有Iterator接口
- 所有参数中的Promise实例为fulfilled,才会为fulfilled
-
race()
- 基本同all;差异在于参数中的Promise实例最先改变的状态决定了包装实例的状态
-
allSettled() :该方法由 ES2020 引入
- 等到所有状态变更后,无论是fulfilled还是rejected,才会执行外层,且返回的Promise实例状态始终是fulfilled。
-
any()
- 只要参数实例有一个变成fulfilled状态,包装实例就会变成fulfilled状态
【注意】
- 如果没有对应状态的回调函数,会冒泡传递,直到被相应的回调函数处理
- 状态为reject且没有相应处理函数,会最终被catch捕获
- 返回的状态值,仅能被消费一次;之后的传递值由对应回调函数的return值决定
错误处理
1. Promise执行过程中抛出异常的处理
throw new Error('test');
//等价于
try {
throw new Error('test');
} catch(e) {
reject(e);
}
//或者
reject(new Error('test'));
2. 内部的promise的异常捕获
new Promise((r,j)=>{r()}).then(() => {
try{
new Promise(() => { throw 'error' })
return 'try';
}catch(e) {
console.log('inner', e);
return 'catch'
}
}).then(info => console.log(info)).catch(err=> {console.log('outer:', err)})
//执行结果:try
由上可知,promise会吃掉错误(即不会传递到外层代码
,不能被外层代码捕获
,不会影响外层代码的执行
)
Q:想要捕获内部错误怎么办?
- 对抛出异常的promise加上catch
- return掉promise,就可以统一使用最外层的catch捕获处理
promise的实现原理:
1. .then
返回新的上下文
2. status完成,记录value,等待回调到位执行;回调就位,等待status完成执行
.then的返回值为什么不使用同一个上下文?
1.状态不固定status在变动:pedding->fulfilled | rejected->pedding...
2..then返回promise时resolve(消费)多次问题
3.中间状态不能被记录
Iterator
Iterator:遍历器,本质为指针对象
- Iterator
- 集合:数组、对象、Map、Set。可以组合使用的前提是需要统一的接口机制,来处理所有不同数据结构的访问。
- 遍历器可以不依附于别的数据结构;
- 遍历器对象的特征是具有next方法,返回
{value:当前成员的值, done:是否结束标志}
对象:
{
next:()=> {
//...
return {
value: ,//任意类型值
done: ,//Boolean
}
}
}
-
Iterator接口:
遍历器生成函数
- 要部署到数据结构的
Symbol.iterator
属性。
- 要部署到数据结构的
{
[Symbol.iterator]() {
return { next: () => {}}
}
}
- 因此具有Symbol.iterator属性的数据结构,可以认为是可遍历的。
- 提供了统一的访问机制:
for...of
,会调用Symbol.iterator方法,返回遍历器对象。 - 原生具备Iterator接口的数据结构:
Array
、Map
、Set
、String
、Typedarray
、Arguments对象
、Nodelist对象
注意
-
对象(Object)之所以没有默认部署Iterator接口,是因为
属性遍历次序不确定
;对象部署遍历器接口并不是很必要,因为其等效于Map结构
; -
类数组的对象(
存在数值键名和length属性
),获取Iterator接口的方式:- 可以引用数组的Iterator接口
- Array.from()
- Array.prototype.slice.call(arguments, 0)
- ...
应用场景(默认调用symbol.iterator)
- 解构赋值
- 扩展运算符(
只要某个数据结构部署了iterator接口,就可以将其转为数组
) yield*
后是一个可遍历的结构-
for...of
等 - 相关Tips
- Symbol.iterator方法的
最简单实践是使用Generator函数
。
let myIterable = {
//简洁写法:* [Symbol.iterator]() {
[Symbol.iterator]: function* () {
yield 1;
yield 2;
yield 3;
}
}
[...myIterable] // [1, 2, 3]
- 遍历器对象的return():遍历时的提前退出「异常」或「break语句」都会调用return()
- forEach(无法中断);for...in 循环读取键名(包括原型链上的key);for...of 循环读取键值(只返回具有数字索引的属性;可以中断)
Generator
状态机 | 遍历器生成函数:返回的遍历器对象依次遍历内部的每个状态
使用
// 形式1
function* () {
yield ;
}
// 形式2:作为对象属性的写法
let obj = {
* myGeneratorMethod() {
···
}
};
特性
- 惰性求值的语法功能
-
分段执行
,yield表达式是暂停执行的标记,而next方法可以恢复执行,每次next返回对象中的value为yield后的表达式值
; - yield表达式的返回值:其本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield表示的返回值,因此第一次传参无效。
- yield 表达式如果用在另一个表达式中,
需放在括号中(表示其为表达式,和({})作用相似)
function* demo() {
console.log('Hello' + yield 123); // SyntaxError
console.log('Hello' + (yield)); // OK
console.log('Hello' + (yield 123)); // OK
}
注意return的使用
for...of循环
中,一旦done为true,就会中止,且不会包含该返回对象
yield*
yield*表达式:表明后面的表达式是一个带有[Symbol.iterator]
的数据结构。如:yield* [1,2,3,4]。
- 案例1:等同于在 Generator 函数内部,部署一个for...of循环:
let A = (function* () {
yield 'Hello!';
yield 'Bye!';
}());
let B = (function* () {
yield 'Greetings!';
//yield* A; 等价于
for (var value of A) {
yield value;
}
yield 'Ok, bye.';
}());
- 案例2:yield*的返回值
function* () {
Let res = yield* gen(); // res为generator中return的值
}
- 应用:取出嵌套数组的所有成员
function* iterTree(tree) {
if (Array.isArray(tree)) {
for(let i=0; i < tree.length; i++) {
yield* iterTree(tree[i]);
}
} else {
yield tree;
}
}
const tree = [ 'a', ['b', 'c'], ['d', 'e'] ];
next 、 throw、return
都是让 Generator 函数恢复执行,并且使用不同的语句替换yield表达式。
-
next():是将yield表达式替换成
一个值
,供下一段执行代码使用:
// 相当于将
let result = yield x + y
// 替换成
let result = 1;
-
throw():是将yield表达式替换成
一个throw语句
// 相当于将
let result = yield x + y
// 替换成
let result = throw(new Error('出错了'));
-
return():是将yield表达式替换成
一个return语句
。
// 相当于将
let result = yield x + y
// 替换成
let result = return 2; // {value:2,done:true}
注意点
- gen.return:只能执行一次
-
gen.throw(
async内部实现reject中用到
)- 可以外部捕获try...catch捕获
- 也可以内部
try...catch
就可以内部捕获(内部错误未捕获,就不会再执行下去,认为generator已结束);
var g = function* () {
try {
yield;
} catch (e) {
console.log('内部捕获', e);
}
};
【疑问】For...of
、扩展运算符(...
)、解构赋值、Array.from()
可以直接适用于Generator的原因?如下例:
function* numbers () {
yield 1
yield 2
return 3
yield 4
}
for (let n of numbers()) {
console.log(n)
} // 1, 2
已知for...of
需要处理的是一个带有[Symbol.iterator]
的对象,而生成器执行(numbers()
)返回的是遍历器对象
;那为什么可以如下使用:for...of
?
原因如下:
numbers() === numbers()[Symbol.iterator]
应用
- 状态机
-
控制流管理:函数嵌套 -> promise -> generator+自动执行器 (
异步操作的同步化表达
) - Iterator接口
异步编程的完整解决方案
- Generator 函数可以
暂停执行
和恢复执行
,这是它能封装异步任务的根本原因; - 函数体内外的数据交换(双向消息传递机制)
1) 通过next()的参数给下一个代码块传参
2) 通过yield返回上一个代码块执行的结果
- 错误处理机制。
自动执行
需要有一种机制,自动控制 Generator 函数的流程,接收和交还程序的执行权
。
- 回调函数【缓存数据延迟执行,如compose】
将异步操作包装成 Thunk 函数,在回调函数里面交回执行权。
函数传参的底层设计方式有如下2种方式:
1) 传名调用(thunk是"传名调用"的一种实现策略,用来替换某个表达式)
f(x + 5)
// 传名调用时,等同于
(x + 5) * 2
2) 传值调用
f(x + 5)
// 传值调用时,等同于
f(6)
js是传值调用
,而js中,Thunk 函数替换的不是表达式,而是多参数函数
,将其替换成单参数的版本,且只接受回调函数作为参数。
- Promise 对象
将异步操作包装成 Promise 对象,用then方法交回执行权。
Async
Generator的语法糖
- 内置执行器
- 语义清晰(async和await 对比 * yield)
- 更广的适用性(await后可以是「promise」、「thenable」和原始类型的值,
会被用promise.resove()包装成promise
)- 返回值是promise对象,Generator返回的是Iterator对象
错误处理
任何一个await语句后面的 Promise 对象变为reject状态,等同于「async函数返回的Promise对象」
被reject。那么整个async函数都会中断执行:
-
await后面的可以使用try...catch捕获:
await后面的表达式抛出异常,内部promise实例reject对应的回调函数为使用「gen.throw(e)」抛出异常,因此当执行下一个代码块时,遵循Generator的错误处理
,错误抛出,可以被外部捕获(tip:promise内部的错误不会被被外界感知
,注意不要混淆)。因此可以使用try...catch
捕获异常,不影响函数内之后代码的执行async function test() { try { await myPromise; } }
-
可以用返回的promise的catch捕获:
无论是否使用try...catch捕获异常,内部的错误都不会影响外层,是因为自执行器对next的执行做了try...catch处理
,异常时等同于try{gen.throw(e)}catch(e){return reject(e)}
,那么等同于async函数返回的 Promise 对象被reject。let promise = async function test() { await myPromise; } promise.catch(err => console.log(err));
-
await后的promise内部消化异常:
let myPromise = Promise().catch(err => console.log(err)); async function test() { await myPromise; }
简单实现原理如下:
function fn(args) {
return spawn(function* () {
//代码如下
});
}
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); });
});
}
递归的设计:每一步step负责执行yield后的表达式,并用promise包装好(根据状态决定下一个step是执行next还是throw)。
至此,ES6【代理】和【流程控制】相关的内容就介绍这么多~
更多技术分享及时获取,欢迎关注~
基础薄弱的的童鞋,可以自行品读:阮一峰的ECMAScript 6 入门.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。