闭包
问题?
- 什么是闭包?
- 闭包有哪些实际运用场景?
- 闭包是如何产生的?
- 闭包产生的变量如何被回收?
闭包的底层实现原理
JS 执行上下文
js 代码是要经过浏览器 V8 进行预编译后才能真正的被执行
js 执行上下文分三种:
- 全局执行上下文: 代码开始执行时首先进入的环境
- 函数执行上下文:函数调用时,会开始执行函数中的代码
- eval 执行上下文:不建议使用,可忽略
那么,执行上下文的周期,分为两个阶段:
- 创建阶段
- 创建词法环境
- 生成变量对象(VO),建立作用域链、作用域链、作用域链(重要的事说三遍)
- 确认 this 指向,并绑定 this
- 执行阶段。这个阶段进行变量赋值,函数引用及执行代码
预编译发生在函数执行之前。预编译四部曲为:
- 创建 AO 对象
- 找形参和变量声明,将变量和形参作为 AO 属性名,值为 undefined
- 将实参和形参相统一
- 在函数体里找到函数声明,值赋予函数体。最后程序输出变量值的时候,就是从 AO 对象中拿
- 什么叫变量对象?
变量对象是 js 代码在进入执行上下文时,js 引擎在内存中建立的一个对象,用来存放当前执行环境中的变量。
- 变量对象(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
- 变量对象变为活动对象
执行上下文的第二个阶段,称为执行阶段,在此时,会进行变量赋值,函数引用并执行其他代码,此时,变量对象变为活动对象
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)
分析过程如下:
- 在全局上下文中声明变量 top 并赋值为 0.
- 2 - 8 行。在全局执行上下文中声明了一个名为 createWarp 的变量,并为其分配了一个函数定义。其中第 3-7 行描述了其函数定义,并将函数定义存储到那个变量(createWarp)中
- 第 9 行。我们在全局执行上下文中声明了一个名为 sum 的新变量,暂时,值为 undefined
- 第 9 行。遇到(),表明需要执行或调用一个函数。那么查找全局执行上下文的内存并查找名为 createWarp 的变量。 明显,已经在步骤 2 中创建完毕。接着,调用它
- 调用函数时,回到第 2 行。创建一个新的 createWarp 执行上下文。我们可以在 createWarp 的执行上下文中创建自有变量。js 引擎 createWarp 的上下文添加到调用堆栈(call stack)。因为这个函数没有参数,直接跳到它的主体部分
- 3 - 6 行。我们有一个新的函数声明,在 createWarp 执行上下文中创建一个变量 add。add 只存在于 createWarp 执行上下文中, 其函数定义存储在名为 add 的自有变量中
- 第 7 行,我们返回变量 add 的内容。js 引擎查找一个名为 add 的变量并找到它. 第 4 行和第 5 行括号之间的内容构成该函数定义
- createWarp 调用完毕,createWarp 执行上下文将被销毁。add 变量也跟着被销毁。 但 add 函数定义仍然存在,因为它返回并赋值给了 sum 变量。 (ps: 这才是闭包产生的变量存于内存当中的真相)
- 接下来就是简单的执行过程,不再赘述 ...
- 代码执行完毕,全局执行上下文被销毁。sum 和 result 也跟着被销毁
所以,什么是闭包?
- 解释一下作用域链是如何产生的
- 解释一下 js 执行上下文的创建、执行过程
- 解释一下闭包所产生的变量放在哪了 最后请把以上 3 点结合起来说给面试官听