详解Js的closure(闭包)和异步编程

闭包

使用闭包主要是为了设计私有的方法和变量。闭包的优点是可以避免全局变量的污染,缺点是闭包会常驻内存,会增大内存使用量,使用不当很容易造成内存泄露。
闭包有三个特性:
1.函数嵌套函数 2.函数内部可以引用外部的参数和变量 3.参数和变量不会被垃圾回收机制回收

招聘前端工程师,尤其是中高级前端工程师,扎实的 JS 基础绝对是必要条件,基础不扎实的工程师在面对前端开发中的各种问题时大概率会束手无策。现在拿出一道基本每次面试都会被问到的代码:

for (var i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log(i);
  }, 1000 * i);
}

看到这里,你不禁会说:“这不是老子背了很多遍的闭包题吗?开始输出一个 5,然后每隔一秒再输出一个 5,一共 5 个 5。”

如果这道题仅仅是考察候选人对 JS 异步代码、变量作用域的理解,局限性未免太大,接下来我会追问,如果期望代码的输出变成: 0,1,2,3,4,该怎么改造代码?熟悉闭包的同学很快能给出下面的解决办法:

for (var i = 0; i < 5; i++) {
    (function(j) {  // j = i
        setTimeout(function() {
            console.log(new Date, j);
        }, 1000);
    })(i);
}

巧妙的利用 IIFE**(Immediately Invoked Function Expression:声明即执行的函数表达式)来解决闭包造成的问题,确实是不错的思路,但是初学者可能并不觉得这样的代码很好懂,至少笔者初入门的时候这里琢磨了一会儿才真正理解。

有没有更符合直觉的做法?答案是有,我们只需要对循环体稍做手脚,让负责输出的那段代码能拿到每次循环的 i 值即可。该怎么做呢?利用 JS 中基本类型(Primitive Type)的参数传递是按值传递**(Pass by Value)的特征,不难改造出下面的代码:

for (var i=1; i<=5; i++) {
     (function() {
         var j = i;
         setTimeout( function timer() {
             console.log( j );
         }, j*1000 );
})(); }

但是,如果代码改成下面这样,又会怎么样呢?

for (var i = 0; i < 5; i++) {
  setTimeout((function(i) {
    console.log(i);
  })(i), i * 1000);
}

这里给 setTimeout 传递了一个立即执行函数。额,setTimeout 可以接受函数或者字符串作为参数,那么这里立即执行函数是个啥呢,应该是个 undefined ,也就是说等价于:
setTimeout(undefined, ...);
而立即执行函数会立即执行,那么应该是立马输出的。
多以还是立马输出0-4;

ES 6

for (let i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log(i);
  }, 1000 * i);
}

ES6 的let将变量绑定在了块级作用域,所以,每次执行时i都存在于一个新的块级作用域当中,这看上去很不错。
顺着下来,不难给出基于 Promise 的解决方案(既然 Promise 是 ES6 中的新特性,我们的新代码使用 ES6 编写是不是会更好?如果你这么写了,大概率会让面试官心生好感):

const tasks = [];
for (var i = 0; i < 5; i++) {   // 这里 i 的声明不能改成 let,如果要改该怎么做?
    ((j) => {
        tasks.push(new Promise((resolve) => {
            setTimeout(() => {
                console.log(new Date, j);
                resolve();  // 这里一定要 resolve,否则代码不会按预期 work
            }, 1000 * j);   // 定时器的超时时间逐步增加
        }));
    })(i);
}

ES7 ansyc awit

const sleep = (timeountMS) => new Promise((resolve) => {
    setTimeout(resolve, timeountMS);
});
(async () => {  // 声明即执行的 async 函数表达式
    for (var i = 0; i < 5; i++) {
        await sleep(1000);
        console.log(new Date, i);
    }
})();

异步编程方法

js中的异步编程共有下面这些方法,好吧以前我就知道前两个,其中事件监听和观察者有些类似,下面我们将分别介绍

  • 回调函数
  • 事件监听
  • 观察者
  • Promise
  • Generator
  • async/await

回调函数
callback是我们最熟悉的方式,上面提到的动画的例子,用callback实现代码如下
moveTo(100, 0, function () { moveTo(200, 0, function () { moveTo(300, 0, function () { moveTo(400, 0, function () { // 无限不循环 }) }) })})

有同学可能会问moveTo函数是如何实现的?答案如下
function moveTo(x = 0, y = 0, cb = function () {}) { move('.box').x(x).y(y).end(cb);}

下面来列举回调的N大罪状

  1. 违反直觉
  2. 错误追踪
  3. 模拟同步
  4. 回调地狱
  5. 并发执行
  6. 信任问题(多次调用)

违反直觉,并不是说缩进,缩进其实可以通过拆分函数来解决,而是对人友好的顺序执行,现在要跳来跳去
错误追踪,异步让try catch直接跪了,为了能够捕获到异步的错误,有两种方案,分离回调和first error
jquery的ajax就是典型的分离回调

function success(data) { 
    console.log(data);
}
function error(err) {
     console.error(err);
}
$.ajax({}, success, error);

Node采用的是first error,所有系统异步接口第一个参数都是error对象

function callback(err, data) { if (err) { // 出错 return; } // 成功 console.log(data);}async("url", callback);

模拟同步,node的io相关的api都是异步的,我只是想要python那种同步的个脚本,险些精神分裂,o(╯□╰)o
一段很容易理解的同步代码
for (let i = 0; i < arr.length; ++i) { arr[i] = sync(arr[i]);}

然而如果是异步的话要这么写,/(ㄒoㄒ)/~~
(function next(i, len, callback) { if (i < len) { async(arr[i], function (value) { arr[i] = value; next(i + 1, len, callback); }); } else { callback(); }}(0, arr.length, function () { // All array items have processed.}));

关于回调地狱,并发执行和信任问题,我建议你阅读文章结尾处推荐的资料,因为我实在才疏学浅,o(╯□╰)o
观察者
观察者模式需要一个pub和sub函数,或者其他类似工具,一定有同学说,这不还是回调吗?请看清楚,回调是因为观察者模式,而不是异步。其实回调的问题,观察者模式并没有解决
sub('1', function () { moveTo(100, 0, function () { pub('2') })})sub('2', function () { moveTo(200, 0, function () { pub('3') })})sub('3', function () { moveTo(300, 0, function () { pub('4') })})// 无限不循环

pub和sub函数的是怎么实现的呢?最简单的实现如下
let eventMap = {};function pub(msg, ...rest) { eventMap[msg] && eventMap[msg].forEach((cb) => { cb(...rest); });}function sub(msg, cb) { eventMap[msg] = eventMap[msg] || []; eventMap[msg].push(cb);}

Promise
如果你还不了解Promise,那么建议先阅读这篇文章**,下面是Promise实现上面的例子
moveTo(100, 0).then(function () { return moveTo(200, 0)}).then(function () { return moveTo(300, 0)}).then(function () { return moveTo(400, 0)}).then(function () { // 无限不循环})

再来看一下,moveTo函数有何不同,看出来没?
function moveTo(x = 0, y = 0) { return new Promise(function (resolve, reject) { move('.box').x(x).y(y).end(resolve); })}

其实Promise开始我是拒绝的,这不还是回调吗!!!请再次注意,这里的回调不是为了异步,而是Promise协议
Promise其实是一种控制反转,举个例子,就是原来我们要给异步函数传入一个回调函数,现在变成了异步函数返回一个Promise对象,堪称神来之笔,而Promise就是实现这种反转的工具,Promise是一个双方约定的契约(规范)
其实Promise还有很多优点,要知道Promise是后面新技术的基础,堪称一切异步方案的粘合剂,没有Promise,可能就不会有generator,那么为什么说是可能呢?请看Generator一节
Promise解决了回调的一些问题,但并没有全部解决,比如Promise有很好的错误追踪,避免了回调地狱,对并发执行很友好,因为Promise只决议一次,就很好的解决了信任问题
但Promise对违反直觉并不友好,回调变成了长长的Promise链
看一下模拟同步的代码,貌似成功解决了
var chain = Promise.resolve();for (let i = 0; i < arr.length; ++i) { chain = chain.then(() => async(arr[i]).then((x) => arr[i] = x))}

Generator
Generator是一个革命性特性,es2015(es6)中引入,让原本必须一次执行完毕函数,现在可以在中间暂停,并在下次继续执行,这就让js可以模拟协程的概念
其实关于Generator我开始也是拒绝的,这东西本来不是为异步而生,是为了产生持续生成的数据,和迭代器是天生一对, 而把他用在异步上优点类似那float做布局(float本来只是为了实现文字环绕的效果),有点绕,不是很好理解
动画的例子用generator实现如下,就好像是同步的一样
function* gen() { yield moveTo(100, 0) yield moveTo(200, 0) yield moveTo(300, 0) yield moveTo(400, 0) // 无限不循环}run(gen)

moveTo函数如下
function moveTo(x = 0, y = 0) { return new Promise(function (resolve, reject) { move('.box').x(x).y(y).end(resolve); })}

看吧,多么简洁,简直完美,但我发誓当你看到run函数后你就疯了
function run(fn) { let gen = fn() function next(data) { var res = gen.next(data) if (res.done) return res.value; res.value.then(function (data) { next(data) }) } next()}

生成器要实现异步,必须得有启动函数,也就是run,好在社区有封装好的run函数,比如co这个库
生成器能很好的解决Promise不能解决的问题,如违反直觉,繁琐的代码,但也有其自身的问题,比如不太好理解,蹩脚,需要启动器
Promise那一节,我说如果没有Promise就可能没有generator,而不是必须呢?因为generator实现异步除了基于Promise,还可以基于thunk函数,感兴趣的你自行查阅吧,祝好
async/await
那么有没有一种完美方案呢?还真有啊,es2017(es8)给我们带来了异步函数,这家伙简直就是自带启动器的生成器函数,好吧他就是给予generator的
动画的例子用异步函数来实现
async function run() { await moveTo(100, 0) await moveTo(200, 0) await moveTo(300, 0) await moveTo(400, 0) // 无限不循环}run()

moveTo函数如下
async function moveTo(x = 0, y = 0) { await new Promise(function (resolve, reject) { move('.box').x(x).y(y).end(resolve); })}

完美同步般的代码,自带启动器,世界终于重归美好,终极异步方案,还学什么generator啊
总结
上面只是介绍了js中异步编程的全部方法,如果你想深入了解异步编程,那么我推荐一本书《你不知道的JavaScript**》中卷,这本书能解答你全部的疑惑

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容