每日读《你不知道的JavaScript(上)》| 作用域+闭包知识串讲

124 阅读3分钟

前言

其实这章我本来主要想写的是关于闭包的实际运用,但我发现能够和前面章节的知识点都关联起来,帮助大家更深入地理解闭包,当然,是在深刻理解作用域的基础之上。

我想,大概真正掌握一门技术,是能够将所有知识点都串联起来吧。

就像脑子里面自动构建一个很完整的知识体系。

还木有那么深的功力啦...QAQ

so今天就来做一个简单的知识串讲吧。

从闭包回归块级作用域

来看一段代码:

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

预期是,每秒从开始依次递增打印出1~5。

但实际上:

image.png

每秒每次打印的都是6,打印了5个6。

乍一看,这不合理呀。

但是,前一章说过的吧。setTimeout 是个异步任务。

那么 setTimeout 的回调函数 timer 就会延迟执行。

也就是说,等到循环已经走完了,i这时候都已经等于6了,才会执行每一轮的 timer。

因为 i 在这里,是全局共享变量。

var 定义的 i 会导致变量提升

所以才变成全局变量。

此时循环中的5个函数都在共用这个i。

然而想要达到预期效果,我们肯定需要每一轮都有自己的作用域。

那这个时候就能运用闭包的原理来解决问题了。

回顾且牢记:

当函数可以记住并访问所在的词法作用域时,就产生了闭包。

用IIFE来创建闭包:

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

这样每次循环的时候,依托IIFE都会立即创建并执行里面的匿名函数。

每个迭代都会生产一个新的作用域,相当于创建一个新的闭包。

然后我们用一个变量 j 将每次迭代的 i 存储起来,再在 timer 中打印出来。

这样能保证 timer 记住并访问它所在的作用域,由此形成一个闭包,而闭包会保持对这个变量 j 的引用。

利用IIFE的特性优化一下:

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

我们知道,用IIFE的目的是为了创建新的作用域。

我们也知道,IIFE自身存在很多缺陷。

那有没有什么替代IIFE的好的方案呢?

get!

块级作用域,对吧。

前面几章我们提到的块作用域,能够很好地解决这个问题。

本质上这是将一个块转换成一个可以被关闭的作用域。

for(var i=1; i<=5; i++) {
    let j = i; // 用 let 声明就行啦!
    setTimeout(function timer() {
        console.log(j);
    }, j*1000);
}

那不妨这样:

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

简直绝了。

第一次看完的时候,我直呼牛皮🥹🥹🥹

改动的最终结果很简单,但是推理过程耐人寻味呀。

从闭包到模块

你一定很好奇这里为什么会提到模块

闭包实际上可多地方用到了呢!

举个例子:

function person() {
    var age = 18;
    var sex = '女';
    
    function printAge() {
        console.log(age);
    }
    
    function printSex() {
        console.log(sex);
    }
}

定义一个 person 函数,里面有 age 和 sex 这两个私有属性,还有 printAge 和 printSex 俩函数。

我们让这个模块暴露出去:

function person() {
    var age = 18;
    var sex = '女';
    
    function printAge() {
        console.log(age);
    }
    
    function printSex() {
        console.log(sex);
    }
    
    return {
        printAge,
        printSex
    };
}

var p = person();
p.printAge();
p.printSex();

我们调用内部函数,看打印效果: image.png

可以正常打印出来!

在上面这段代码中,printAge 和 printSex 函数具有涵盖模块实例内部作用域的闭包。

小结

闭包到这里就算结束啦。

到此,《你不知道的JavaScript》(上)就已经过半了。

日后也还要继续努力~