本文仅用于笔者的学习记录。水平还不够教学(手动狗头)。如果发现错误和有疑惑的地方,欢迎交流学习!!!
在了解闭包之前,我觉得很有必要弄清楚js代码的执行过程和变量存储。
执行上下文和变量存储
浏览器打开一个页面,首先会从计算机的虚拟内存中分配两块内存出来,分别是栈内存
和堆内存
。栈内存(ECStack)
主要供代码执行,存储声明的变量和原始值类型的值。
堆内存(heap)
存储对象类型的值。在堆内存中,还有一个Global Object的全局对象,存储浏览器为js提供的内置api
模型简化如下图所示
ECStack执行栈
当我们执行一段代码的过程中
var x = [12, 23] function fn(y) { y[0] = 100; y = [100] y[1] = [200] } fn(x) console.log(x)
这段代码执行的过程中会创建一个全局的执行上下文(EC(G))
,全局执行上下文是全局代码执行的一个环境,并供其他上下文中的代码执行。上下文在其所有代码执行完毕后销毁,包括定义在它上面的所有变量和函数,但是全局执行上下文在应用退出或者页面关闭的时候才会被销毁。所以,我们可以知道执行上下文
其实就是栈内存
中的一段空间。
在代码执行的过程当中,会出现变量的声明。所以在每个上下文都有一个关联的变量对象(variable object)
,这个上下文中定义的所有变量和函数都存在这个对象上。全局执行上下文则存储全局的变量对象。
上下文中的代码在执行的过程当中,如果上下文是函数,则其活动对象(activation object)
用作变量对象。简单理解就是活动对象中存储了这个函数执行需要的变量,包括传入的形参和函数中的私有变量。
附:ECMAScript规范指出独立作用域只能通过“函数(function)”代码类型的执行上下文创建,ECMAScript里的for循环并不能创建一个局部的上下文。(想起来一道经典的题目,就是在for循环中把var改成let)
回归正题,简单总结一下上述的名词含义
- 执行环境栈(ECStack):专门用来供代码执行的 栈内存
- 全局对象(GO):存放内置的属性方法,window 指向
- 全局执行上下文( EC(G) ):页面加载后进栈、销毁后出栈
- 变量对象VO(Variable Object) : 存放当前执行上下文中创建的变量和值
- 活动对象AO(Activation Object) : 函数私有上下文中的变量对象
现在模型变成下图所示
作用域链
上下文中的代码在执行过程中,会创建变量对象的一个作用域链
。作用域链主要是提供一个查找机制。比如私有上下文中遇到一个变量,他会看是否是自己私有的,如果是:接下来就会私有处理,和外界毫无关系。如果不是私有的,则基于作用域链,去上级查找,直到找到全局作用域为止。
上述代码具体的执行过程,大家可以去看这一篇博客,我觉得做得非常详细。通过动图了解JS中的ECStack、EC、VO 和 AO - 知乎 (zhihu.com)
闭包
看一下Javascript高级程序设计对闭包的解释。闭包
是指那些引用了另一函数作用域中的变量的函数,通常是在嵌套函数中实现的。
let x = 5; const fn = function fn(x){ return function(y){ console.log(y + (++x)); } } let f = fn(6) f(7) fn(8)(9) f(10) console.log(x);
接下来,我们一步步画图来理解这个给代码
先创建一个执行栈和堆,然后执行全局代码。执行全局代码时,会创建全局的执行上下文,基本数据类型会存在全局变量对象(VO)
里面,函数创建会在堆内存中开辟一段新的空间,将地址赋给全局的变量。
let x = 5
会在VO中把x赋值为5
const fn = function fn(x){ return function(y){ console.log(y + (++x)); } }
然后会在堆内存中创建一个fn函数堆,初始化它的作用域为全局作用域,存储的代码为
return function(y){ console.log(y + (++x)); }
然后fn的值是fn函数堆的地址
let f = fn(6)
先把fn执行,fn执行又会在堆内存开辟一段空间。然后再初始化作用域,因为我是向fn函数传入参数执行,即相当于向0x001(fn的地址)传参执行。初始化作用域是fn函数堆。如上文所说:上下文中的代码在执行的过程当中,如果上下文是函数,则其活动对象(activation object)
用作变量对象。所以AO中x赋值为传入的参数6。重点来了,由于返回的是一个函数,所以又会开辟一个小函数堆,初始化作用域为fn1,存储的上下文代码是console.log(y + (++x));
然后返回小函数的地址,所以f指向的是最后创建的小函数的地址。在全局对象中新添加f指向最后创建的小函数地址。
f(7)
相当于小函数堆0x100(7),在小函数堆的AO中创建变量y,赋值为传入的形参7。然后作用域中产找x,发现没有找到,则向上层作用域查找,上层作用域是0x001,查找到x = 6,++x时x的值变成了7,所以输出14
fn(8)(9)
先执行fn(8),又会重新创建一个堆内存,x的变量值为8,和上次一样,返回值是一个新函数的地址。再执行fn(8)(9),相当于将形参y赋值为9, y + (++x)
执行,发现作用域中没有x,向上层查找,将x从8变成了9,然后9+9输出18
f(10)
++x时由之前的7变成了8,10 + 8输出18
console.log(x);
直接输出全局作用域的5