闭包的底层实现原理
JS 的执行上下文
JS的代码在之前之前,会被浏览的JS引擎进行一次预编译以后,才能被真正执行。而在预编译的过程中,引擎做的事情有:变量提升,函数提升。
var d = 'abd';function a(){console.log('函数a')}console.log(a)a();var c = 123;// 在函数进行预编译以后相当于function a(){console.log('函数a')}var d;console.log(a) // function(){}a()a = '变量a' // 变量a赋值,函数声明被覆盖console.log(a)a(); // a is not a function
预编译的这个过程是在一个运行环境中执行的,而这个运行环境就是JS的执行期上下文。
执行上下文分为三种:
- 全局执行期上下文:代码开始执行时首先进入的环境
- 函数执行期上下文:函数调用时,会开始执行函数中的代码
- eval执行期上下文:解析js字符串时确定的上下文
执行上下文的周期分为两个阶段:
创建阶段- 创建词法环境
- 生成变量对象,建立作用域链
- 确定
this指向,并同时绑定this
执行阶段- 在这个阶段进行变量赋值。函数引用及执行代码
如图:

预编译发生在函数执行之前,预编译四部曲为:
- 创建
AO对象 - 找形参和变量声明,将变量和形参作为
AO属性名,值为undefined - 将形参和实参统一(形参赋值)
- 在函数体里找到函数声明,值赋予函数体。最后程序输出变量值的时候,就是从
AO对象中拿。
什么叫变量对象?
变量对象是js代码在进入执行期上下文时,JS引擎在内存中简历的一个对象,用来存放当前执行环境中的变量。变量对对象的创建过程
变量对象的创建,是在执行期上下文创建阶段,依次经过一下三个过程:- 创建
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。当遇到同名的函数声明,则会忽略,变量和函数都一样。重复的声明不会导致报错,在执行阶段会被忽略。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';
- 执行期上下文的第二个阶段,成为执行阶段,在此时,会进行变量赋值,函数引用并执行其他代码,此时变量对象变为活动对象。
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`)。
1: let top = 0; //
2: function createWarp() {
3: function add(a, b) {
4: let ret = a + b
5: return ret
6: }
7: return add
8: }
9: let sum = createWarp()
10: let result = sum(top, 8)
11: 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 也跟着被销毁。
