Skip to content
大纲

闭包相关问题

闭包是一个函数,其可以记住并访问外部变量。

在函数被创建时,函数的隐藏属性 [[Environment]] 会记住函数被创建时的位置,即当时的词法环境 Lexical Environment

这样,无论在哪里调用函数,都会去到 [[Environment]] 所引用的词法环境

当查找变量时,先在词法环境内部查找,当没有找到局部变量时,前往当前词法环境所记录的外部词法环境查找

来源为 JavaScript.info/Closure

应用场景

  1. 封装私有变量

手写一个闭包

js
function outer() {
  let i = 0
  return function inner() {
    console.log(i++)
  }
}

const add = outer()
add()
add()

闭包经典问题

改造下面的代码,使之输出 0-9,写出你能想到的所有解法

js
// 解决问题
for (var i = 0; i < 10; i++) {
  setTimeout(() => {
    console.log(i)
  }, 1000)
}

解决办法

js
// 方法一
// 1. 利用 setTimeout 函数的第三个参数,会作为回调函数的第一个参数传入
// 2. 利用 bind 函数的部分执行的特性

// code1
for (var i = 0; i < 10; i++) {
  setTimeout((i) => {
    console.log(i)
  }, i)
}

// code2
for (var i = 0; i < 10; i++) {
  setTimeout(console.log, 1000, i)
}

// code3
for (var i = 0; i < 10; i++) {
  setTimeout(console.log.bind(Object.create(null), i), 1000)
}
js
// 方法二
// 1. 利用 let 变量块级作用域特性

// code1
for (let i = 0; i < 10; i++) {
  setTimeout((i) => {
    console.log(i)
  }, 1000)
}
js
// 方法三
// 1. 利用函数自执行的方式,将当前 for 循环过程中的 i 传递进去,构建出块级作用域。
//   IIFE 其实并不属于闭包的范畴
//   参考:
//   - [difference-between-closures-and-iifes-in-javascript](https://stackoverflow.com/questions/41228824/difference-between-closures-and-iifes-in-javascript)
//   - [IIFE 是闭包?](https://bit.ly/2NXNT56)
// 2. 利用其他方式构建出块级作用域

// code1
for (var i = 0; i < 10; i++) {
  ;((i) => {
    setTimeout(() => {
      console.log(i)
    }, 1000)
  })(i)
}

// code2
for (var i = 0; i < 10; i++) {
  try {
    throw new Error(i)
  } catch ({ message: i }) {
    setTimeout(() => {
      console.log(i)
    }, 1000)
  }
}

// code3
for (var i = 0; i < 10; i++) {
  try {
    throw i
  } catch (i) {
    setTimeout(() => {
      console.log(i)
    }, 1000)
  }
}

// code4
for (var i = 0; i < 10; i++) {
  customFn(i)
}
function customFn(i) {
  setTimeout(() => {
    console.log(i)
  }, 1000)
}
js
// 方法四
// 很多其他方案只是把 console.log(i) 放到一个函数里面
// 因为 setTimeout 函数的第一个参数只接收函数及字符串
// 如果是 js 语句的话,js 引擎应该会自动在该语句外面包裹一层函数

// code1
for (var i = 0; i < 10; i++) {
  setTimeout(console.log(i), 1000)
}

// code2
for (var i = 0; i < 10; i++) {
  setTimeout(
    (() => {
      console.log(i)
    })(),
    1000
  )
}

// code3
for (var i = 0; i < 10; i++) {
  setTimeout(
    ((i) => {
      console.log(i)
    })(i),
    1000
  )
}

// code4
for (var i = 0; i < 10; i++) {
  setTimeout(
    ((i) => {
      console.log(i)
    }).call(Object.create(null), i),
    1000
  )
}

// code5
for (var i = 0; i < 10; i++) {
  setTimeout(
    ((i) => {
      console.log(i)
    }).apply(Object.create(null), [i]),
    1000
  )
}

// code6
for (var i = 0; i < 10; i++) {
  setTimeout(
    ((i) => {
      console.log(i)
    }).apply(Object.create(null), { lenght: 1, 0: i }),
    1000
  )
}
js
// 方法五
// 利用 eval 或 new Function 执行字符串,然后执行过程同方法 四

// code1
for (var i0; i < 10; i++) {
  setTimeout(eval('console.log(i)'), 1000)
}

// code2
for (var i0; i < 10; i++) {
  setTimeout(new Function('i', 'console.log(i)')(i), 1000)
}

// code3
for (var i0; i < 10; i++) {
  setTimeout(new Function('console.log(i)')(), 1000)
}

扩展阅读

知识点

  • 什么是闭包?
    • MDN 定义
      • 闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment,词法环境)的引用的组合。
      • 换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。
      • 在 JavaScript 中,闭包会随着函数的创建而被同时创建。
    • 定义一:闭包是指可以访问其所在作用域的函数
    • 定义二:闭包是指有权访问另一个函数作用域中的变量的函数
    • 定义三:闭包是指在函数声明时的作用域以外的地方被调用的函数
    • 严格来说,闭包需要满足三个条件:【1】访问所在作用域;【2】函数嵌套;【3】在所在作用域外被调用
  • 什么是 IIFE?
    • 立即调用函数表达式,Immediately-Invoked Function Expression,简写为 IIFE。顾名思义,这个函数定义之后马上被执行。
    • IFE 最大的好处在创建出一个新的作用域
    • IIFE 有助于避免多个函数访问全局变量时的全局变量污染
  • 闭包和 IIFE 的区别?
    • IIFE 是创建闭包的一种特定方式
  • IIFE 是闭包吗?
    • 严格来讲 IIFE 并不算闭包,因为函数并没用在本身的词法作用域以外执行。

思考题

TODO: 参考文档,后续整理

参考