从执行上下文和作用域链理解闭包

简介: 从执行上下文和作用域链理解闭包

本文仅用于笔者的学习记录。水平还不够教学(手动狗头)。如果发现错误和有疑惑的地方,欢迎交流学习!!!

在了解闭包之前,我觉得很有必要弄清楚js代码的执行过程和变量存储。


执行上下文和变量存储



浏览器打开一个页面,首先会从计算机的虚拟内存中分配两块内存出来,分别是栈内存堆内存栈内存(ECStack)主要供代码执行,存储声明的变量和原始值类型的值。

堆内存(heap)存储对象类型的值。在堆内存中,还有一个Global Object的全局对象,存储浏览器为js提供的内置api


模型简化如下图所示

image.png


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) : 函数私有上下文中的变量对象


现在模型变成下图所示image.png



作用域链



上下文中的代码在执行过程中,会创建变量对象的一个作用域链。作用域链主要是提供一个查找机制。比如私有上下文中遇到一个变量,他会看是否是自己私有的,如果是:接下来就会私有处理,和外界毫无关系。如果不是私有的,则基于作用域链,去上级查找,直到找到全局作用域为止。


上述代码具体的执行过程,大家可以去看这一篇博客,我觉得做得非常详细。通过动图了解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指向最后创建的小函数地址。

image.png

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


目录
相关文章
|
4月前
|
JavaScript 前端开发
作用域链的理解
作用域链的理解
53 0
|
14天前
|
移动开发
浅谈H5闭包
浅谈H5闭包
|
1月前
|
存储 自然语言处理 JavaScript
闭包
闭包
10 0
|
4月前
|
自然语言处理 JavaScript 前端开发
深入理解作用域、作用域链和闭包
在 JavaScript 中,作用域是指变量在代码中可访问的范围。理解 JavaScript 的作用域和作用域链对于编写高质量的代码至关重要。本文将详细介绍 JavaScript 中的词法作用域、作用域链和闭包的概念,并探讨它们在实际开发中的应用场景。
|
4月前
|
自然语言处理 JavaScript 前端开发
对作用域链的理解
对作用域链的理解
46 0
|
设计模式 自然语言处理 JavaScript
一篇文章帮你真正理解javascsript作用域闭包
一篇文章帮你真正理解javascsript作用域闭包
79 0
|
消息中间件 存储 自然语言处理
兄台: 作用域、执行上下文了解一下
• 作用域(Scopes) • 词法环境(Lexical environments) • 作用域链 • 执行上下文 • 调用栈
|
自然语言处理 前端开发 JavaScript
作用域闭包
作用域闭包
80 0
|
存储 缓存 JavaScript
深入理解作用域和闭包(下)
深入理解作用域和闭包(下)
深入理解作用域和闭包(下)
|
存储 JavaScript 前端开发
深入理解作用域和闭包(上)
深入理解作用域和闭包(上)
深入理解作用域和闭包(上)