👨‍💻
Hong's 前端笔记
  • 🌈About
  • JavaScript
    • 作用域
    • 闭包
    • this
      • bind()
    • 原型
      • Object.create()
      • 模拟“类”
      • ES6 Class
      • Function
    • 对象
    • 类型 & 值
      • 原生函数
      • 数组
      • 字符串
    • 异步
    • JavaScript 相关概念
      • Event Loop 事件循环
      • Prototype 原型
      • Context 执行上下文
      • this
      • Promise
    • JavaScript 常见问题
      • 手写实现
      • 看代码输出
  • React
    • 核心概念
    • 深入理解
      • State
      • 如何管理 State
      • React 渲染过程
      • 处理 DOM 事件
      • Pure 组件
      • Context
      • Key 属性
      • React.lazy()
      • Render Props
    • Hooks
      • useState
      • useRef
      • useEffect
        • Some details
      • 相关概念
      • 自定义 Hook
      • useEffect vs useLayoutEffect
    • React 相关问题
      • Class vs Function
  • React Libraries
    • Redux
      • Quick Start
      • 如何持久化 Redux 数据
      • 异步操作
      • 性能优化
      • Dive into immutability
    • Route v6
      • Quick Start
  • Reactivity
    • Immer
  • Vue
    • 深入理解
      • Composition vs Options
      • Reactivity
  • Network
    • Cookies
    • HTTP
    • HTTPS
    • CORS 跨域资源共享
    • 认证手段
    • 安全相关
    • 网络相关知识
  • Browser
    • DOM 操作
    • Events 事件
    • XHR & Fetch
    • 性能优化
    • HTML 相关概念
    • 浏览器相关概念
  • CSS
    • 盒模型
    • Layout 布局
    • Styles 样式
    • CSS 相关概念
    • CSS 相关技巧
      • 移动端适配
      • Flex
      • 动画
  • TypeScript
    • Quick Peek
    • Types in TypeScript
    • Narrowing
    • Functions in TypeScript
  • Workflow
    • Webpack
      • Webpack 概念
      • 资源管理
      • 管理输出
      • 开发环境配置
      • 生产环境配置
      • 优化代码运行性能
  • Others
    • 小程序与原生 Web 的区别
  • SSO
    • About
    • API Doc
    • 接入指南
Powered by GitBook
On this page
  • 什么是闭包
  • 闭包解决循环问题
  • 利用 let 创建块级作用域
  • 用闭包封装模块
  • 模块机制
Edit on GitHub
  1. JavaScript

闭包

什么是闭包

当一个函数可以记住所在的词法作用域,就产生了闭包。即使该函数在词法作用域以外被调用。

function foo() {
    var a = 0

    function bar() {
        console.log(a)
    }

    return bar()
}

var baz = foo

// bar() 在词法作用域之外被调用,但是可以访问到他所在的词法作用域内的变量
baz()   // 0

上述例子中,bar() 在其定义的词法作用域以外被执行(虽然通过 baz 调用,但实际上就是 bar)。但是他可以访问到自己词法作用域内的变量 a

bar 在全局变量调用的时候,他其实是从自己的词法作用域内进行查找,顺着 foo() 函数作用域向上

而不是在全局作用域内查找,若在全局作用域中查找,应该会抛出一个 ReferenceError ( RHS 失败)

函数 foo() 在执行后内部的作用域并没有被销毁,而是被 bar() 所持有。

bar() 依旧持有对该作用域的引用,这个引用就叫闭包。

也就是说:一个函数在定义的词法作用域以外被调用,闭包使得这个函数在调用时仍然可以访问定义时的词法作用域。

闭包解决循环问题

下列代码并不会按照顺序依次输出 0, 1, 2 ...,而是输出 5 次 5

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

造成上述问题的本质是:我们会误以为 setTimeout 在每次循环时都会捕获时下变量 i 的值,但实际上只存在一个在全局作用域内的 i 。

一种解决方法,就是利用闭包特性,给每次 setTimeout 创建一个闭包,让回调函数可以捕获所对应的词法作用域:

在不利用 let 关键词的情况下,可以利用 IIFE 来创建函数作用域,再使用闭包来捕获他

for (var i = 0; i < 5; i++) {
    // 利用 IIFE 创建一个函数作用域
    (function() {
        // 用一个变量把时下的 i 保存在函数作用域内
        var j = i
        setTimeout(function() {
            console.log(j)
        }, 0)
    })()
}

利用 let 创建块级作用域

上述的方法是在不使用 let 关键词的情况下,通过创建一个函数作用域来将每一次的变量 i 捕获下来。

有了 let 关键词,可以免去使用 IIFE 创建函数作用域的过程:

for (var i = 0; i < 5; i++) {
    // let 创建了一个块级作用域,用于回调函数捕获
    let j = i
    setTimeout(function() {
        console.log(j)
    }, 0)
}

let 关键词将变量 j 捕获在了一个块级作用域内部。

但是,上述的写法还可以继续简化成:

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

这种写法就能得到正确的输出结果,他的原理是:

for 循环头部的 let 声明有一个特殊行为:变量在循环过程中不止会被声明一次,而是每次迭代都会声明一次。每一次声明,都是用上次循环结束的值进行赋值。

用闭包封装模块

下面就是一种模块封装的基本形式:

function FooModule() {
    var name = 'bar'

    function sayHello() {
        console.log(`Hello, ${name}`)
    }

    return {
        sayHello
    }
}

var foo = FooModule()

foo.sayHello()  // Hello, bar

这种模块封装的方式就利用到了闭包的特性。他具备以下两个必要条件:

  • 必须有一个外部封装函数,并且需要被调用一次(每调用一次就创建一个新的模块实例)

  • 返回的内容中必须持有一个内部函数,这样才能保证对封装函数内部的作用域引用(形成闭包)

模块往往只需要被调用一次,因此可以采用 IIFE 的写法:

var fooModule = (function() {
    var name = 'bar'

    function sayHello() {
        console.log(`Hello, ${name}`)
    }

    return {
        sayHello
    }
})()

fooModule.sayHello()  // Hello, bar

模块机制

现代的模块机制使用类似于下面的结构:

var MyModules = (function () {
    var modules = {}

    function define(name, deps, impl) {
        for (var i = 0; i < deps.length; i++) {
            deps[i] = modules[deps[i]]
        }
        modules[name] = impl.apply(impl, deps)
    }

    function get(name) {
        return modules[name]
    }

    return {
        define,
        get
    }
})()

MyModules.define('foo', [], function() {
    function hello(name) {
        console.log(`Hello, ${name}`)
    }

    return {
        hello
    }
})

MyModules.define('bar', ['foo'], function(foo) {
    function helloFromBar() {
        foo.hello('bar')
    }

    return {
        helloFromBar
    }
})

var bar = MyModules.get('bar')

bar.helloFromBar()  // Hello, bar

这里最核心的就是:modules[name] = impl.apply(impl, deps)

这边 apply 传入的 deps 就是可以被内部函数( return 出来的 helloFromBar)利用闭包来捕获。

也就是说,对于模块 bar,传入了一个依赖模块 foo。那么模块 foo 就存在 bar 模块的词法作用域中。

当你在外面调用 bar 模块暴露的内部函数时,内部函数仍然可以访问到模块 foo。

即:bar 成功依赖于 foo,foo 存在于 bar 的闭包里。

Last updated 2 years ago