大白话解释作用域和闭包是个啥

作用域的分类

常见的变量作用域就是 静态作用域(词法作用域)动态作用域 。词法作用域注重的是 write-time ,即 编程时的上下文 ,而 动态作用域 则注重的是 run-time ,即 运行时上下文 。词法作用域中我们需要知道一个函数 在什么地方被定义 ,而动态作用域中我们需要关心的是函数 在什么地方被调用

而 javascript 使用的则是词法作用域

 1let value = 1
 2
 3function foo() {
 4    console.log(value)
 5}
 6
 7function bar() {
 8    let value = 2
 9    foo()
10}
11
12bar() // 1

在 javascript 解析模式中,当 foo 被调用的时候:

  • 检查 foo 函数内是否存在 value
  • 存在则使用这个 value
  • 不存在则根据书写代码的位置查找上一层代码(这里的 window),找到 value 为 1

在动态作用域的解析模式中,当 foo 被调用的时候:

  • 检查 foo 函数内是否存在 value
  • 存在则使用这个 value
  • 不存在则根据调用该函数的作用域中去寻找也就是这里的 bar 函数,找到 value 为 2

在从内层到外层的变量搜索过程中,当前作用域到外层作用域再到更外层作用域直到最外层的全局作用域,整个搜寻轨迹就是 作用域链

20190307092333.png

变量的两种查找类型

一种是 rhs 一种是 lhs

假设有这么一段代码:

1console.log(a) // 输出 undefined
2console.log(a2) // 报错 a2 is not defined
3var a = 1

上述代码实际上在变量提升的作用下应该是下面这个顺序:

1var a
2console.log(a) // 输出 undefined
3console.log(a2) // 报错 a2 is not defined
4a = 1
  • 第一个 console 输出 undefined 因为还未执行赋值操作,查询过程是 rhs 也就是 right-hand-side
  • 第二个 console 报错,是因为 rhs 查询 a2 变量不存在因此报错
  • a = 1 则是赋值操作,也就是 lhs,英文 left-hand-side

20190307093837.png

闭包

闭包是啥?闭包就是从函数外部访问函数内部的变量,函数内部的变量可以持续存在的一种实现。

在了解了词法作用域和变量的查询方式之后,我们看看一个简单的闭包的实现逻辑:

 1function f() {
 2    num = 1 // 里面的变量
 3    function add() {
 4        num += 1
 5    }
 6    function log() {
 7        console.log(num)
 8    }
 9    return { add, log } // 我要到外面去了
10}
11
12add = f().add
13log = f().log
14
15log() // 1 我从里面来,我在外面被调用,还是可以获得里面的变量
16add()
17log() // 2
  • 首先定义一个 f 函数,函数内部维护一个变量 num,然后定义两个函数 add 和 log
  • add 函数每次调用会增加 num 的值
  • log 函数每次调用会打印 num 的值
  • 然后我们将两个函数通过 return 方法返回
  • 紧接着先调用外部的 log 方法打印 f 方法维护的 num,此时为 1
  • 然后调用外部的 add 方法增加 num 的值
  • 最后再次调用 log 方法打印 num,此时则为 2

为什么外部定义的 add 函数可以访问 f 函数内部的变量呢。正常情况下外部作用域不可访问内部作用域的变量,但我们将内部访问其内部变量的方法“导出”出去,以至于可以从外部直接调用函数内部的方法,这样我们就可以从函数的外部访问函数内部的变量了。

经典的 for 循环问题

1arr = []
2for (var i = 0; i < 10; i ++) {
3    arr[i] = function() {
4        console.log(i)
5    }
6}
7arr[2]() // 10

首先我们知道 for 循环体内的 i 实际上会被定义在全局作用域中

每次循环我们都将 function 推送到一个 arr 中,for 循环执行完毕后,arr 中张这样:

20190307101411.png

随后我们执行代码 arr[2]() 此时 arr[2] 对应的函数 function(){ console.log(i) } 会被触发

函数尝试搜索函数局部作用域中的 i 变量,搜索不到则会继续向外层搜索,i 被定义到了外层,因此会直接采用外层的 i,就是这里的全局作用域中的 i,等到这个时候调用这个函数,i 早已变成 10 了

那么有什么方法能够避免出现这种情况吗?

ES6 之前的解决方案:

了解了闭包我们就知道了闭包内的变量可以持续存在,所以修改代码将 arr 中的每一项改为指向一个闭包:

1arr = []
2for (var i = 0; i < 10; i ++) {
3    arr[i] = (function() { // 这是一个闭包
4        var temp = i // 闭包内部维护一个变量,这个变量可以持续存在
5        return function() {
6            console.log(temp)
7        }
8    })()
9}

这样程序就能按照我们的想法运行了

20190307102109.png

ES6 之后的解决方案:

ES6 之后我们就有了块级作用域因此代码可以改为这样:

1arr = []
2for (let i = 0; i < 10; i ++) { // 使用 let
3    arr[i] = function() {
4        console.log(i)
5    }
6}

在使用 let 之后,我们每次定义 i 都是通过 let i 的方法定义的,这个时候 i 不再是被定义到全局作用域中了,而是被绑定在了 for 循环的块级作用域中

因为是块级作用域所以对应 i 的 arr 每一项都变成了一个闭包,arr 每一项都在不同的块级作用域中因此不会相互影响

20190307102815.png

参考:

  • https://github.com/mqyqingfeng/blog/issues/3
  • https://www.datchley.name/basic-scope/
  • https://segmentfault.com/a/1190000006671020

原文发布于微信公众号 - JS菌(awesomefe)

原文发表时间:2019-03-07

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券