闭包

问题?

  • 什么是闭包?
  • 闭包有哪些实际运用场景?
  • 闭包是如何产生的?
  • 闭包产生的变量如何被回收?

闭包的底层实现原理

JS 执行上下文

js 代码是要经过浏览器 V8 进行预编译后才能真正的被执行

js 执行上下文分三种:

  1. 全局执行上下文: 代码开始执行时首先进入的环境
  2. 函数执行上下文:函数调用时,会开始执行函数中的代码
  3. eval 执行上下文:不建议使用,可忽略

那么,执行上下文的周期,分为两个阶段:

  • 创建阶段
    • 创建词法环境
    • 生成变量对象(VO),建立作用域链、作用域链、作用域链(重要的事说三遍)
    • 确认 this 指向,并绑定 this
  • 执行阶段。这个阶段进行变量赋值,函数引用及执行代码

预编译发生在函数执行之前。预编译四部曲为:

  1. 创建 AO 对象
  2. 找形参和变量声明,将变量和形参作为 AO 属性名,值为 undefined
  3. 将实参和形参相统一
  4. 在函数体里找到函数声明,值赋予函数体。最后程序输出变量值的时候,就是从 AO 对象中拿
  1. 什么叫变量对象?

变量对象是 js 代码在进入执行上下文时,js 引擎在内存中建立的一个对象,用来存放当前执行环境中的变量。

  1. 变量对象(VO)的创建过程

变量对象的创建,是在执行上下文创建阶段,依次经过以下三个过程:

  • 创建 arguments 对象

    对于函数执行环境,首先查询是否有传入的实参,如果有,则会将参数名是实参值组成的键值对放入 arguments 对象中。否则,将参数名和 undefined 组成的键值对放入 arguments 对象中

function bar(a, b, c) {
  console.log(arguments) // [1, 2]
  console.log(arguments[2]) // undefined
}
bar(1, 2)
  • 当遇到同名的函数时,后面的会覆盖前面的
console.log(a) // function a() {console.log('Is a ?') }
function a() {
  console.log('Is a')
}
function a() {
  console.log('Is a ?')
}
// 在执行第一行代码之前,函数声明已经创建完成.后面的对之前的声明进行了覆盖
  • 检查当前环境中的变量声明并赋值为 undefined。当遇到同名的函数声明,为了避免函数被赋值为 undefined ,会忽略此声明
console.log(a) // function a() {console.log('Is a ?') }
console.log(b) // undefined
function a() {
  console.log('Is a ')
}
function a() {
  console.log('Is a ?')
}
var b = 'Is b'
var a = 10086
// 这段代码执行一下,你会发现 a 打印结果仍旧是一个函数,而 b 则是 undefined
  1. 变量对象变为活动对象

执行上下文的第二个阶段,称为执行阶段,在此时,会进行变量赋值,函数引用并执行其他代码,此时,变量对象变为活动对象

console.log(a) // function a() {console.log('fjdsfs') }
console.log(b) // undefined
function a() {
  console.log('Is a')
}
function a() {
  console.log('Is a?')
}
var b = 'Is b'
console.log(b) // 'Is b'
var a = 10086
console.log(a) // 10086
var b = 'Is b?'
console.log(b) // 'Is b?'

在上面的代码中,代码真正开始执行是从第一行 console.log() 开始的,自这之前,执行上下文是这样的:

// 创建过程
EC= {
  VO{}; // 创建变量对象
  scopeChain: {}; // 作用域链
}
VO = {
  argument: {...}; // 当前为全局上下文,所以这个属性值是空的
  a: <a reference> // 函数 a  的引用地址
  b: undefiend  // 见上文创建变量对象的第三步
}
  • 词法作用域(Lexical scope)

这里想说明,我们在函数执行上下文中有变量,在全局执行上下文中有变量。JavaScript 的一个复杂之处在于它如何查找变量,如果在函数执行上下文中找不到变量,它将在调用上下文中寻找它,如果在它的调用上下文中没有找到,就一直往上一级,直到它在全局执行上下文中查找为止。(如果最后找不到,它就是 undefined)

let top = 0 //
function createWarp() {
  function add(a, b) {
    let ret = a + b
    return ret
  }
  return add
}
let sum = createWarp()
let result = sum(top, 8)
console.log('result:', result)

分析过程如下:

  1. 在全局上下文中声明变量 top 并赋值为 0.
  2. 2 - 8 行。在全局执行上下文中声明了一个名为 createWarp 的变量,并为其分配了一个函数定义。其中第 3-7 行描述了其函数定义,并将函数定义存储到那个变量(createWarp)中
  3. 第 9 行。我们在全局执行上下文中声明了一个名为 sum 的新变量,暂时,值为 undefined
  4. 第 9 行。遇到(),表明需要执行或调用一个函数。那么查找全局执行上下文的内存并查找名为 createWarp 的变量。 明显,已经在步骤 2 中创建完毕。接着,调用它
  5. 调用函数时,回到第 2 行。创建一个新的 createWarp 执行上下文。我们可以在 createWarp 的执行上下文中创建自有变量。js 引擎 createWarp 的上下文添加到调用堆栈(call stack)。因为这个函数没有参数,直接跳到它的主体部分
  6. 3 - 6 行。我们有一个新的函数声明,在 createWarp 执行上下文中创建一个变量 add。add 只存在于 createWarp 执行上下文中, 其函数定义存储在名为 add 的自有变量中
  7. 第 7 行,我们返回变量 add 的内容。js 引擎查找一个名为 add 的变量并找到它. 第 4 行和第 5 行括号之间的内容构成该函数定义
  8. createWarp 调用完毕,createWarp 执行上下文将被销毁。add 变量也跟着被销毁。 但 add 函数定义仍然存在,因为它返回并赋值给了 sum 变量。 (ps: 这才是闭包产生的变量存于内存当中的真相)
  9. 接下来就是简单的执行过程,不再赘述 ...
  10. 代码执行完毕,全局执行上下文被销毁。sum 和 result 也跟着被销毁

所以,什么是闭包?

  • 解释一下作用域链是如何产生的
  • 解释一下 js 执行上下文的创建、执行过程
  • 解释一下闭包所产生的变量放在哪了 最后请把以上 3 点结合起来说给面试官听