【面试篇】寒冬求职季之你必须要懂的原生JS(中)

互联网寒冬之际,各大公司都缩减了HC,甚至是采取了“裁员”措施,在这样的大环境之下,想要获得一份更好的工作,必然需要付出更多的努力。

一年前,也许你搞清楚闭包,this,原型链,就能获得认可。但是现在,很显然是不行了。本文梳理出了一些面试中有一定难度的高频原生JS问题,部分知识点可能你之前从未关注过,或者看到了,却没有仔细研究,但是它们却非常重要。本文将以真实的面试题的形式来呈现知识点,大家在阅读时,建议不要先看我的答案,而是自己先思考一番。尽管,本文所有的答案,都是我在翻阅各种资料,思考并验证之后,才给出的(绝非复制粘贴而来)。但因水平有限,本人的答案未必是最优的,如果您有更好的答案,欢迎给我留言。

本文篇幅较长,但是满满的都是干货!并且还埋伏了可爱的表情包,希望小伙伴们能够坚持读完。

写文超级真诚的小姐姐祝愿大家都能找到心仪的工作。

如果你还没读过上篇【上篇和中篇并无依赖关系,您可以读过本文之后再阅读上篇】,可戳【面试篇】寒冬求职季之你必须要懂的原生JS(上)

小姐姐花了近百个小时才完成这篇文章,篇幅较长,希望大家阅读时多花点耐心,力求真正的掌握相关知识点。

1.说一说JS异步发展史

异步最早的解决方案是回调函数,如事件的回调,setInterval/setTimeout中的回调。但是回调函数有一个很常见的问题,就是回调地狱的问题(稍后会举例说明);

为了解决回调地狱的问题,社区提出了Promise解决方案,ES6将其写进了语言标准。Promise解决了回调地狱的问题,但是Promise也存在一些问题,如错误不能被try catch,而且使用Promise的链式调用,其实并没有从根本上解决回调地狱的问题,只是换了一种写法。

ES6中引入 Generator 函数,Generator是一种异步编程解决方案,Generator 函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权,Generator 函数可以看出是异步任务的容器,需要暂停的地方,都用yield语句注明。但是 Generator 使用起来较为复杂。

ES7又提出了新的异步解决方案:async/await,async是 Generator 函数的语法糖,async/await 使得异步代码看起来像同步代码,异步编程发展的目标就是让异步逻辑的代码看起来像同步一样。

1.回调函数: callback

//node读取文件
fs.readFile(xxx, 'utf-8', function(err, data) {
   
    //code
});

回调函数的使用场景(包括但不限于):

  1. 事件回调
  2. Node API
  3. setTimeout/setInterval中的回调函数

异步回调嵌套会导致代码难以维护,并且不方便统一处理错误,不能try catch 和 回调地狱(如先读取A文本内容,再根据A文本内容读取B再根据B的内容读取C…)。

fs.readFile(A, 'utf-8', function(err, data) {
   
    fs.readFile(B, 'utf-8', function(err, data) {
   
        fs.readFile(C, 'utf-8', function(err, data) {
   
            fs.readFile(D, 'utf-8', function(err, data) {
   
                //....
            });
        });
    });
});

2.Promise

Promise 主要解决了回调地狱的问题,Promise 最早由社区提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。

那么我们看看Promise是如何解决回调地狱问题的,仍然以上文的readFile为例。

function read(url) {
   
    return new Promise((resolve, reject) => {
   
        fs.readFile(url, 'utf8', (err, data) => {
   
            if(err) reject(err);
            resolve(data);
        });
    });
}
read(A).then(data => {
   
    return read(B);
}).then(data => {
   
    return read(C);
}).then(data => {
   
    return read(D);
}).catch(reason => {
   
    console.log(reason);
});

想要运行代码看效果,请戳(小姐姐使用的是VS的 Code Runner 执行代码): https://github.com/YvetteLau/Blog/blob/master/JS/Async/promise.js

思考一下在Promise之前,你是如何处理异步并发问题的,假设有这样一个需求:读取三个文件内容,都读取成功后,输出最终的结果。有了Promise之后,又如何处理呢?代码可戳: https://github.com/YvetteLau/Blog/blob/master/JS/Async/index.js

注: 可以使用 bluebird 将接口 promise化;

引申: Promise有哪些优点和问题呢?

3.Generator

Generator 函数是 ES6 提供的一种异步编程解决方案,整个 Generator 函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用 yield 语句注明。

Generator 函数一般配合 yield 或 Promise 使用。Generator函数返回的是迭代器。对生成器和迭代器不了解的同学,请自行补习下基础。下面我们看一下 Generator 的简单使用:

function* gen() {
   
    let a = yield 111;
    console.log(a);
    let b = yield 222;
    console.log(b);
    let c = yield 333;
    console.log(c);
    let d = yield 444;
    console.log(d);
}
let t = gen();
//next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值
t.next(1); //第一次调用next函数时,传递的参数无效
t.next(2); //a输出2;
t.next(3); //b输出3; 
t.next(4); //c输出4;
t.next(5); //d输出5;

为了让大家更好的理解上面代码是如何执行的,我画了一张图,分别对应每一次的next方法调用:

仍然以上文的readFile为例,使用 Generator + co库来实现:

const fs = require('fs');
const co = require('co');
const bluebird = require('bluebird');
const readFile = bluebird.promisify(fs.readFile);

function* read() {
   
    yield readFile(A, 'utf-8');
    yield readFile(B, 'utf-8');
    yield readFile(C, 'utf-8');
    //....
}
co(read()).then(data => {
   
    //code
}).catch(err => {
   
    //code
});

不使用co库,如何实现?能否自己写一个最简的my_co?请戳: https://github.com/YvetteLau/Blog/blob/master/JS/Async/generator.js

PS: 如果你还不太了解 Generator/yield,建议阅读ES6相关文档。

4.async/await

ES7中引入了 async/await 概念。async其实是一个语法糖,它的实现就是将Generator函数和自动执行器(co),包装在一个函数中。

async/await 的优点是代码清晰,不用像 Promise 写很多 then 链,就可以处理回调地狱的问题。错误可以被try catch。

仍然以上文的readFile为例,使用 Generator + co库来实现:

const fs = require('fs');
const bluebird = require('bluebird');
const readFile = bluebird.promisify(fs.readFile);


async function read() {
   
    await readFile(A, 'utf-8');
    await readFile(B, 'utf-8');
    await readFile(C, 'utf-8');
    //code
}

read().then((data) => {
   
    //code
}).catch(err => {
   
    //code
});

可执行代码,请戳:https://github.com/YvetteLau/Blog/blob/master/JS/Async/async.js

思考一下 async/await 如何处理异步并发问题的? https://github.com/YvetteLau/Blog/blob/master/JS/Async/index.js

如果你有更好的答案或想法,欢迎在这题目对应的github下留言:说一说JS异步发展史


2.谈谈对 async/await 的理解,async/await 的实现原理是什么?

async/await 就是 Generator 的语法糖,使得异步操作变得更加方便。来张图对比一下:

async 函数就是将 Generator 函数的星号(*)替换成 async,将 yield 替换成await。

我们说 async 是 Generator 的语法糖,那么这个糖究竟甜在哪呢?

1)async函数内置执行器,函数调用之后,会自动执行,输出最后结果。而Generator需要调用next或者配合co模块使用。

2)更好的语义,async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。

3)更广的适用性。co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async 函数的 await 命令后面,可以是 Promise 对象和原始类型的值。

4)返回值是Promise,async函数的返回值是 Promise 对象,Generator的返回值是 Iterator,Promise 对象使用起来更加方便。

async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。

具体代码试下如下(和spawn的实现略有差异,个人觉得这样写更容易理解),如果你想知道如何一步步写出 my_co ,可戳: https://github.com/YvetteLau/Blog/blob/master/JS/Async/my_async.js

function my_co(it) {
   
    return new Promise((resolve, reject) => {
   
        function next(data) {
   
            try {
   
                var {
    value, done } = it.next(data);
            }catch(e){
   
                return reject(e);
            }
            if (!done) {
    
                //done为true,表示迭代完成
                //value 不一定是 Promise,可能是一个普通值。使用 Promise.resolve 进行包装。
                Promise.resolve(value).then(val => {
   
                    next(val);
                }, reject);
            } else {
   
                resolve(value);
            }
        }
        next(); //执行一次next
    });
}
function* test() {
   
    yield new Promise((resolve, reject) => {
   
        setTimeout(resolve, 100);
    });
    yield new Promise((resolve, reject) => {
   
        // throw Error(1);
        resolve(10)
    });
    yield 10;
    return 1000;
}

my_co(test()).then(data => {
   
    console.log(data); //输出1000
}).catch((err) => {
   
    console.log('err: ', err);
});

如果你有更好的答案或想法,欢迎在这题目对应的github下留言:谈谈对 async/await 的理解,async/await 的实现原理是什么?


3.使用 async/await 需要注意什么?

  1. await 命令后面的Promise对象,运行结果可能是 rejected,此时等同于 async 函数返回的 Promise 对象被reject。因此需要加上错误处理,可以给每个 await 后的 Promise 增加 catch 方法;也可以将 await 的代码放在 try...catch 中。
  2. 多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。
//下面两种写法都可以同时触发
//法一
async function f1() {
   
    await Promise.all([
        new Promise((resolve) => {
   
            setTimeout(resolve, 600);
        }),
        new Promise((resolve) => {
   
            setTimeout(resolve, 600);
        })
    ])
}
//法二
async function f2() {
   
    let fn1 = new Promise((resolve) => {
   
            setTimeout(resolve, 800);
        });
    
    let fn2 = new Promise((resolve) => {
   
            setTimeout(resolve, 800);
        })
    await fn1;
    await fn2;
}
  1. await命令只能用在async函数之中,如果用在普通函数,会报错。
  2. async 函数可以保留运行堆栈。
/**
* 函数a内部运行了一个异步任务b()。当b()运行的时候,函数a()不会中断,而是继续执行。
* 等到b()运行结束,可能a()早就* 运行结束了,b()所在的上下文环境已经消失了。
* 如果b()或c()报错,错误堆栈将不包括a()。
*/
function 
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值