• 夯实基础中篇-图解作用域链和闭包


    前言

    本文承接上篇 夯实基础上篇-图解 JavaScript 执行机制,请先阅读上篇~

    讲基础不容易,本文通过 7个demo和6张图,和大家一起学习温故作用域链和闭包,本文大纲:

    1. 什么是作用域链
    2. 什么是词法作用域
    3. 什么是闭包
    4. 闭包的实际使用案例

    什么是作用域链

    正文开始~

    请思考下面 demo 的 name 打印什么

        function test() {
          console.log(name)
        }
        function test1() {
          const name = 'test1的name'
          test()
        }
        const name = 'global的name'
        test1()
    

    通过执行上下文来分析代码的执行流程,执行到 test 函数时:

    image.png

    那 test 函数里的 name 是哪个呢?这就涉及到了作用域链的定义:变量和函数的查找链条就是作用域链。它决定了各级上下文中的代码在访问变量和函数时的顺序:查找变量和函数时,先在当前执行上下文找,当前没有,到下一个执行上下文找,没有再到下一个,直到全局执行上下文,都没有就报错使用未定义的变量或函数

    而在每个执行上下文的变量环境中,都包含了一个外部引用 outer,用来指向外部的执行上下文,链条结构是当前执行上下文 > 包含当前上下文的上下文1 > 包含上下文1的上下文2 ...

    而这个 demo 会打印global的name,原因是 test 执行上下文的 outer 指向全局执行上下文,包括 test1 的 outer 也是指向全局执行上下文:

    image.png

    也许会有同学疑惑,为什么 test 的 outer 指向全局执行上下文,而不是 test1,这是因为在 JavaScript 执行过程中,其作用域链是由词法作用域决定的。

    什么是词法作用域

    词法作用域就是作用域是由代码中函数声明的位置来决定的,它是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符,它与函数是怎样调用的没有关系。所以刚才的例子打印的是global的name

    看个具体例子:

        const count = 0
        function test() {
          const count = 1
    
          function test1() {
            const count = 2
    
            function test2() {
              const count = 3
            }
          }
        }
    

    其包含关系和作用域链:

    image.png

    image.png

    事实上在 Global Scope 全局作用域(Window)之前,还有一个 Script Scope 脚本作用域,它存放的是当前 Script 内可访问的 let 变量和 const 变量,而 var 变量存放在 Global 上的就不在 Script Scope,它类似于是脚本范围内的全局作用域。在下面的 demo 中再举例。

    什么是闭包

    闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的。

    比如这个例子:

        var globalVariable = 1
        const scriptVariable = 2
        
        function test() {
          let name = 'Jaychou'
    
          return {
            getName() {
              const count = 1
              return name
            },
            setName(newValue) {
              name = newValue
            }
          }
        }
    
        const testFun = test()
        console.log(testFun.getName()) // Jaychou
        testFun.setName('小明')
        console.log(testFun.getName()) // 小明
    

    大家可以根据作用域链的知识,思考一下执行到console.log(testFun.getName())的 getName 里面的时候作用域链是怎样的~

    我们用浏览器的开发者工具看一下:

    image.png
    作用域链是当前作用域 》test 函数的闭包 》Script 作用域 》Global 作用域

    1. 为什么叫 test 函数的闭包?因为当const testFun = test()的 test 函数执行完之后,test 的函数执行上下文已经被销毁了,但它返回的{ getName(){}, setName(){} }对象被 testFun 引用着,而 getName 和 setName 引用着 test 函数内定义的 name 变量,所以这些被引用的变量依然需要被保存在内存中,而这些变量的集合称为闭包 Closure;
    2. 目前闭包内的 name 变量就只能通过 getName 和 setName 去访问和设置,而这也是闭包的作用之一:封装私有变量;
    3. 刚才说的 Script Scope 中保存着 scriptVariable 变量,globalVariable 变量是 var 声明的,所以在 Global Scope(Window)中。

    再看1个具体案例理解闭包:

        const globalCount = 0
    
        function test() {
          const count = 0
          return test1
    
          function test1() {
            const count1 = 1
            return test2
    
            function test2() {
              const count2 = 2
              console.log('test2', globalCount + count + count1 + count2)
            }
          }
        }
        test()()()
    

    image.png
    执行到 test2 内部的 console.log 那一行时,其作用域链是当前作用域 》test1 的闭包 》test 的闭包 》Script Scope 》Global Scope

    闭包使用建议:当不需要使用了之后,注意要解除引用着闭包的变量,这样闭包才会被释放。比如第1个案例的 testFun 如果不需要用了,就把它释放 testFun = null。

    闭包的实际使用案例

    封装私有变量

    就是刚才的 getName、setName 案例,通过 getName 获取 name,通过 setName 设置 name

    封装单例

        const Single = (function () {
          let instance = null
    
          return function () {
            if (!instance) {
              instance = {
                name: 'jaychou',
                age: 40
              }
            }
            return instance
          }
        })()
        const obj1 = new Single()
        const obj2 = new Single()
        console.log(obj1 === obj2) // true
    

    这里只是举个例子,具体的 instance 是什么类型,支持什么功能要看实际项目。

    防抖和节流

    防抖:

        function debounce(fn, delay) {
          let timer = null;
    
          return function () {
            let context = this;
            let args = arguments;
    
            timer && clearTimeout(timer);
    
            timer = setTimeout(function () {
              fn.apply(context, args);
            }, delay);
          }
        }
    

    节流:

        function throttle(fn, interval) {
          let last = 0;
    
          return function () {
            let now = +new Date()
            if (now - last >= interval) {
              fn.apply(this, arguments);
              last = now;
            }
          }
        }
    

    更完整的防抖和节流的实现可参考 Lodash ,这里主要是演示闭包的使用

    总结

    闭包的使用场景很多,功能很强大,可以说在前端项目中经常可见例如 React Hooks 等等,这里只列举了几个很简单的很实用的应用场景。

    总结

    本文主要介绍了作用域链和闭包,沿着 夯实基础上篇-图解 JavaScript 执行机制 来一起看的话应该比较容易理解,若对大家有所帮助,请不吝点赞关注~

  • 相关阅读:
    互联网那些技术 | 高可用三大利器 — 熔断、限流和降级
    Unity C# 网络学习(十)——UnityWebRequest(一)
    初识生成对抗网络(11)——利用Pytorch搭建WGAN生成手写数字
    【树莓派】项目中找不到第三方库的问题
    构建自定义ChatGPT,微软推出Copilot Studio
    原理:用UE5制作一个2D游戏
    Dart语言基础
    针对上两篇微信同声传译语音播报功能,又出现了坑
    VUE-CLI/VUE-ROUTER
    js的变量及运算符
  • 原文地址:https://www.cnblogs.com/qcjay/p/16159562.html