堆栈内存、作用域及作用域链

980 阅读10分钟

堆/栈/ECStack/EC/GO/VO/AO的理解

heap(堆)

在js中,堆存储引用数据类型(对象,函数代码字符串),返回十六进制地址值.

栈(Stack)

在js中,栈

  • 执行js
  • 存储基本数据类型的值.

ECStack

ECStack(Execution Context Stack): js执行环境栈 => js想要执行代码,就一定会创建一个执行栈(栈内存).

EC - 执行上下文

EC(Execution Context): 执行上下文 => 某个域下的代码执行都有自己的执行上下文.

  • 全局执行上下文 EC(G) golbol
  • 某个函数执行上下文 EC(Function) 函数执行上下文

把创建的上下文(EC(...))压缩到栈中执行 => 进栈

  • 执行完有的上下文就没有用了 => 出栈
  • 有的还有用,会把当前上下文压到栈底 => 形成闭包

GO - 全局对象

GO(golbal object): 全局对象-- 在浏览器端,会把全局对象赋值给window

GO:{
    setTimeout: fn,
    Math: {},
    Date:{},
    ...
}

VO - 变量对象

VO(Variable Object): 全局变量对象 => VO不仅包含了全局对象的原有属性,还包括了在全局定义的变量与函数.

VO(G):{
    window: GO(G),
    全局定义的变量
    全局定义的函数
}

AO - 活动变量对象

AO(activation object): 只有函数执行时,会创建AO. AO相当于是VO的分支. AO中包含了

  • 函数的实参(arguments)集合对象
  • this对象
  • 函数的形参
  • 局部函数的定义
  • 内部函数的定义

作用域与作用域链

作用域

  • 1.作用域就是代码的执行环境,全局执行环境就是全局作用域,函数的执行环境就是私有作用域,他们都是栈内存.
    • 全局作用域: 在全局代码执行的时候,会形成全局作用域.
    • 私有作用域: 在函数创建的时候,会形成私有作用域.
  • 2.在Web浏览器中,全局执行环境被认为是window对象,因此所有全局变量和函数都是作为window对象的属性和方法创建.(ES6用let和const创建的变量,不挂载到window上)
  • 3.在Node环境中,全局执行环境就是global.

函数创建和执行分别做了哪些事

函数创建做了哪些事

  • 1.创建了一个堆内存对着(堆中存放着函数代码块和函数的键值对)
    • 代码字符串
          alert('...')......
      
    • 键值对
          name: '函数名',
          length: 形参的个数
      
  • 2.初始化当前函数的作用域
    • [[Scope]] = 所在上下文EC中的变量对象VO/AO
    • 函数的作用域是谁,和他在哪执行没有关系,和他在哪(定义)创建有关系,在哪创建的,它的上级作用域就是谁
      • 函数在全局创建的 [[Scope]] == (VO(G))
      • 函数在函数(FN)中创建的 [[Scope]] == (AO(当前函数),AO(FN)) AO(FN)就是他的上级作用域
          function FN(){
              return function f(){
                  alert('...')
              }
          }
          // FN的作用域就是 全局(,FN(VO-FN),VO(G)) 
          // f的作用域就是 (f(VO-f),FN(VO-FN))  
      

函数执行做了哪些事

  • a.函数执行,会把存储的函数代码块压到ECStack(代码执行环境)栈中
  • b.创建一个新的执行上下文EC (每一个函数执行都会形成一个全新的执行上下文)
    • 初始化this执行
    • 初始化作用域链[[Scope Chain]]
    • 创建AO(活动变量对象)
      • 1.初始化实参集合(arguments)
        • 严格模式下(实参集合和形参没有映射关系)
        • 非严格模式下(有映射关系)
          • 实参和形参如果length不相同,相对应的映射(位置),不对应的不映射
          • 实参和形参如果length相同,一一映射
      • 2.声明形参变量
      • 3.变量提升 (在function里,用var,let等定义的变量是局部变量;不用var,let定义的变量为隐式全局变量)
      • 4代码执行

作用域链

我们在局部作用域中,访问一个变量时,系统首先会在当前作用域中寻找(变量声明or函数形参声明),如果找到则使用. 如果没有找到,则继续向上级作用域中查找(变量声明or函数形参声明),如果找到,就使用. 如果没有找到,则继续上级作用域中查找,找到就使用,没有找到就继续向上级作用域查找,一直找到全局作用域.这就是作用域链机制.

let x = 5;
function fn(){
    var y = 1
    return function f(z){
        // 这里z,z是形参变量,属于私有的, z = 10
        // 这里y,不属于私有的,向上级作用域查找,上级作用域为FN,找到了y,y=1
        /*
            这里x,不属于私有的,向上级作用域查找,
                上级作用域为FN,没有找到.
                继续向上查找,FN的上级作用域为VO(G).
                在VO(G)中找到了x=5
        */ 
        // 10+1+5 = 16
        console.log(z+y+x)
    }
}
fn()(10)

js引擎工作原理简单流程

var a = 10;
function Fn(b){
    var a = 20;
    function f(c){
        console.log(a+b+c)
    }
    return f;
}
var f = Fn(10);
f(20)

全局初始化

创建ECStack/EC

JS代码执行时,js引擎会创建js代码执行环境栈(ECStack),与此同时创建一个全局的执行上希望(EC(G)),并将这个全局执行上下文(EC(G))压入执行环境栈中.

  • 执行环境栈(ECStack)的作用就是为了给js提供正确的顺序的执行环境.

在javascript中,每个函数都有自己的执行上下文(EC(函数)),当函数执行时,就会把压到执行环境栈(ECStack)的栈顶并获取执行.当这个函数执行完毕后:

  • 如果函数内部创建的变量(指return一个函数的地址值)没有被外部所引用,这个函数就会被销毁,把执行权交给原来的执行环境.
  • 如果函数内部创建的变量(指return一个函数的地址值)被外部所引用,这个函数就会被压到栈底(形成闭包),并且把执行权交给原来的执行环境.
var ECStack = [];       // 创建执行环境栈 ECStack
var EC = {};            // 创建执行上下文(EC),执行空间

ECStack.push(EC)        // 入栈
ECStack.pop(EC)         // 出栈

创建全局对象GO(Global Object)

创建一个全局对象(GO Object),这个对象全局只存在一份,它的属性在任何地方都可以访问,它的存在伴随着应用程序的整个生命周期. 如Math,Date,setTimeout等常用的js对象作为它的属性. 在Web浏览器端,全局对象会多一个window属性,并将window指向了自身.这样就可以通过window访问这个全局对象了

// 创建全局对象
var GO = {
    Date: {},
    Math: {},
    setTimeout: fn,
    setInterval: fn
}

创建VO

js引擎还会创建一个全局变量对象(Varibale Object) VO,并把VO指向全局对象, VO中不仅仅包含了全局对象的原有属性,还包括了全局定义的变量a和函数Fn.

  • Fn在创建函数的时候,会创建一个堆内存地址AAAFFF000,存储函数的代码字符串与函数的对象键值对({name:'Fn',length:1}),并把这个堆内存地址AAAFFF000赋值给Fn.
  • 以此同时,还会给函数FN创建一个内部属性(作用域[[Scope]]),并把这个**[[Scope]]**指向了当前创建的执行上下文EC(G)下的VO. Fn[[Scope]]==VO(G)
// 此时ECStack结构
ECStack = [     // 全局执行上下文(ECStack)
    EC(G) : {   // 全局执行上下文(EC(G))
        VO(G): {    // 定义全局变量对象(VO)
            ...全局变量对象原有属性
            变量提升:  Fn = AAAFFF000 // 定义函数
                        Fn[[Scope]] = VO; 定义Fn[[Scope]],并赋值为VO(G)本身
                    var a;
            a = 1;  // 定义变量a
            
            f = Fn()执行后返回的结果,
        }
    }
]

执行函数Fn

当执行Fn(10)时,js会

创建EC

  • js引擎会创建函数A的执行上下文EC(Fn),把创建的EC(Fn)压入执行环境栈(ECStack)顶.
  • 初始化this指向,此时Fn中的this->window
  • 初始化作用域链[[ScopeChain]]=> (AO(ECFn),VO(G));
  • 创建一个当前函数的活动变量对象AO(Activatiob Object),
    • AO中包含了函数的实参对象,
    • 形参变量,
    • this对象,
    • 以及局部变量和
    • 内部的函数定义
  • 代码执行

返回一个堆内存地址给变量f = AAAFFF111;

ECStack:[
    EC(Fn):{
        // 初始化this执行 -> window
        this: window,
        // 初始化作用域链
        [[ScopeChain]]: (EC(Fn),VO(G)),
        // 创建AO
        AO:{
            初始化实参集合: arguments: { 0:10 },
            形参定义: b = 10
            变量提升 f = AAAFFF222(函数在变量提升的阶段声明加定义)
                        f[[Scope]] = AO(Fn)
                    var a;
            代码执行 a = 20;
            return AAAFFF222;
        }
    }
    EC(G): {
        ...GO(global Object )
        a = 1;
        Fn = AAAFFF000,
            Fn[[Scope]] = VO;
        f = AAAFFF111;
        f();    // 紧接着f();
    },
]

执行函数f

执行函数f相当于执行Fn返回的堆内存函数.

执行函数f,因为函数f的地址值在A中创建的,而这个地址值(AAAFFF222)被f占用,所以不能销毁,形成了一个暂时不销毁的私有作用域,这种机制叫闭包.

创建EC(f)

创建函数f执行上下文(EC(f)),并把EC(f)压入到执行环境栈ECStack的栈顶.步骤和Fn函数执行一样

  • 初始化this执行; this->window
  • 初始化函数的作用域链 [[ScopeChain]] = (EC(f),VO(Fn))
  • 创建VO
    • 初始化实参结合arguments: { 0: 20 }
    • 形参变量定义 c=20;
    • 变量提升(没有)
    • 代码执行 console.log(a+b+c)
ECStack:[
    EC(f):{
        // 初始化this执行 -> window
        this: window,
        // 初始化作用域链
        [[ScopeChain]]: (EC(f),VO(Fn)),
        // 创建AO
        AO:{
            初始化实参集合: arguments: { 0:20 },
            形参定义: c = 10,
            变量提升: 没有,
            代码执行 console.log(a+b+c)  => 50
        }
    },
    EC(Fn):{  // 此时EC(FN)中创建的AAAFFF222被外面所占用,不能销毁.
        // 初始化this执行 -> window
        this: window,
        // 初始化作用域链
        [[ScopeChain]]: (EC(A),VO(G)),
        // 创建AO
        AO:{
            初始化实参集合: arguments: { 0:10 },
            形参定义: b = 10
            变量提升 f = AAAFFF222(函数在变量提升的阶段声明加定义)
                        f[[Scope]] = AO(Fn)
                    var a;
            代码执行 a = 20;
            return AAAFFF222;
        }
    },
    EC(G): {
        ...GO(global Object )
        a = 1;
        Fn = AAAFFF000,
            Fn[[Scope]] = VO;
        f = AAAFFF111;
        f();    // 紧接着f();
    },
]

作用域链查找机制:

我们在局部作用域访问一个变量时,系统首先会在局部作用域中查找,如果找到则使用,停止查找;如果没有找到,则会向上级作用域查找,如果找到则使用,停止查找;如果没有找到,继续向上级作用域查找,如果一直没有找到,继续向上,知道windon为止.

执行代码console.log(a+b+c);

根据作用域链查找机制

  • 从当前作用域找变量a或者形参变量b,a不是私有的,向EC(f)上级作用域VO(Fn)中找,VO(Fn)中有a,所以 a=>20;
  • 从当前作用域找变量b或者形参变量b,b不是私有的,向EC(f)上级作用域VO(Fn)中找,VO(Fn)中有形参变量b,所以 b=> 10;
  • 从当前作用域找变量c或者形参变量c,c是私有的,停止查找,c=>20

所以最后的执行结果为 50

总结与后语


多多沉淀技术,吸取经验.

不要吹灭你的灵感和你的想象力; 不要成为你的模型的奴隶。

欢迎大佬随时指点与批评.谢谢.