JS之VO、GO、AO

130 阅读11分钟

程序的内存空间:

1、栈区(stack)—— 由编译器自动分配释放 ,存放为运行函数而分配的局部变量、函数参数、返回数据、返回地址等。其操作方式类似于数据结构中的栈。
2、堆区(heap) —— 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式类似于链表。
3、全局区(静态区)(static)—存放全局变量、静态数据、常量。程序结束后由系统释放。
4、文字常量区 —常量字符串就是放在这里的。 程序结束后由系统释放。
5、程序代码区—存放函数体(类成员函数和全局函数)的二进制代码。

int a = 0; //全局初始化区  
char *p1; //全局未初始化区  
int main() {  
  int b; //栈  
  char s[] = "abc"; //栈  
  char *p2; //栈  
  char *p3 = "123456"; //123456在常量区,p3在栈上。  
  static int c =0;//全局(静态)初始化区  
  p1 = new char[10];  
  p2 = new char[20];  
  //分配得来得和字节的区域就在堆区。   
  strcpy(p1, "123456"); //123456放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。  
}  

JS引擎核心组件

探索JS引擎工作原理

  • 执行环境栈 ECStack( Execution Context Stack ): 保证程序能够按照正确的顺序被执行
  • 全局对象 GO( Global Object ): 这个对象全局只存在一份,它的属性在任何地方都可以访问,它的存在伴随着应用程序的整个生命周期。全局对象在创建时,将Math,String,Date,document 等常用的JS对象作为其属性。
  • 执行环境 : 在javascript中,每个函数都有自己的执行环境,当执行一个函数时,该函数的执行环境就会被推入执行环境栈的顶部并获取执行权。当这个函数执行完毕,它的执行环境又从这个栈的顶部被删除,并把执行权并还给之前执行环境。
  • 变量对象 VO( Varibale Object ):全局VO中不仅包含了全局对象的原有属性,还包括在全局定义的变量和函数,在定义函数的时候,还为 函数 添加了一个内部属性scope,并将scope指向了VO
  • 活动对象 AO( Activation Object ):是VO的分支, 包含了函数的形参、arguments对象、this对象、以及局部变量和内部函数的定义
  • 作用域作用域链: 在javascript中,每个执行环境都有自己的作用域链,用于标识符解析,执行环境被创建时,它的作用域链就初始化为当前运行函数的scope所包含的对象

什么是执行上下文

参考:深入理解 JavaScript 执行上下文和执行栈

执行上下文就是当前 JavaScript 代码被解析和执行时所在环境的抽象概念, JavaScript 中运行任何的代码都是在执行上下文中运行。

当JavaScript代码文件被浏览器载入后,默认最先进入的是一个全局的执行上下文。当在全局上下文中调用执行一个函数时,程序流就进入该被调用函数内,此时引擎就会为该函数创建一个新的执行上下文,并且将其压入到执行栈顶部(作用域链) 。浏览器总是执行位于执行栈顶部的当前执行上下文,一旦执行完毕,该执行上下文就会从执行栈顶部弹出,并且控制权将进入其下的执行上下文。这样,执行栈中的执行上下文就会被依次执行并且弹出,直到回到全局的执行上下文。

执行上下文的类型

  • 全局执行上下文: 这是默认的、最基础的执行上下文。不在任何函数中的代码都位于全局执行上下文中。它做了两件事:1. 创建一个全局对象,在浏览器中这个全局对象就是 window 对象。2. 将 this 指针指向这个全局对象。一个程序中只能存在一个全局执行上下文。
  • 函数执行上下文: 每次调用函数时,都会为该函数创建一个新的执行上下文。每个函数都拥有自己的执行上下文,但是只有在函数被调用的时候才会被创建。一个程序中可以存在任意数量的函数执行上下文。每当一个新的执行上下文被创建,它都会按照特定的顺序执行一系列步骤。
  • Eval 函数执行上下文: 运行在 eval 函数中的代码也获得了自己的执行上下文,但由于 Javascript 开发人员不常用 eval 函数。

VO和GO

实例分析—VO(G)

let a = {
    n: 1
};
let b = a;
a.x = a = {
    n: 2
};
console.log(a.x);
console.log(b);

首先浏览器有ECStack 执行环境栈【栈内存】,当JavaScript代码文件被浏览器载入后,会进入全局执行上下文EC(G),全局执行上下文中有一个全局变量对象VO(G),全局执行上下文整体进栈(执行环境栈)执行。

优先级处理

  1. a=b=10

执行流程(从右到左):

  • 创建10
  • b=10
  • a=10
  1. a.x=b=10

执行流程(从右到左):

  • 创建10
  • a.x = 10
  • b =10
  • 浏览器在堆内存开辟一块空间,假设内存地址是16进制0x000,存储{n:1},将堆内存地址放入VO(G)中,供后期调用。
  • var a 接下来声明一个全局变量a,a关联0x000
  • var b = a,VO(G)声明一个b,关联0x000
  • a.x = a = {n:2} 开辟堆内存空间0x001,存储{n:2},地址放入VO中
  • a.x----0x001, 先找到a的地址0x000,再在0x000中添加成员变量 x:0x001
  • a-----0x001, 将a的地址换成0x001
  • console.log(a.x) 此时a中没有成员变量 x,输出undefined

访问对象中的某个成员,若成员不存在,不会报错,结果为undefined

  • console.log(b) 输出{n:1, x:{n:2}}

当页面关闭,全局执行上下文出栈释放内存。

思考

通过上述实例分析,可以解释:

  • 原始值(基础数据类型)存在栈中,对象存在堆中

  • 代码执行完毕,全局变量还可以继续使用

总结

VO(G) :在栈内存全局执行上下文中,存放全局声明的变量。是栈内存中的一块区域。

浏览器会在其中默认创建全局变量对象window【注意区分Node环境下的全局变量对象global】,关联地址0x000,可以调用GO中的浏览器提供的API

GO:在堆内存中,默认开辟的空间,地址0x000,是一个全局对象, 存放浏览器默认提供给JS调用的API,如:定时器setTimeout、setInterval、JSON...

新旧浏览器机制对比

对于下面代码:

var a = 12;
let b = 10;

旧版本浏览器:

var a,a既会在VO(G)中,也会在GO中,两者会存在映射关系,VO(G)中的a和GO中的a任一改变,另外一个都会改变

新版本浏览器:

前提是全局上下文中才有的机制

  • 基于 var/function 声明的变量或函数、目前不会在VO(G)中存储,直接存储在GO中,作为键值对。如:window.a = 12; console.log(a);首先检查是否为全局变量(VO(G)),若没有再去GO中看是否存在,如果也没有,则报错,ReferenceError;

a=13,此处等价于window.a = 13;

  • 基于 let/const 声明的的变量,还是存储在VO(G)中,和GO没有关系
  • 如果不带任何的声明关键字,则直接给GO设置的,类似于window.a = 14;

实例分析

//ES5
var a = 12;
function b() {}

//ES6
let c = 14;
const d = function () {};

校验:

函数底层运行机制

内存知识

GO:全局对象「堆内存中分配的一块空间 0x000」,存储浏览器的内置API

EC(G)全局执行上下文

VO(G)全局变量对象:存储全局上下文中声明的变量的{排除基于var/function声明的}

window ---> 0x000

y -> 13

AO:私有变量对象,用于存储当前上下文中声明的私有变量

console.log(a); 
//首先会到VO(G)查找,看是否为全局变量,如果不是,则再去GO中找,看是否为全局对象的一个属性,如果还不是,则报错 Uncaught ReferenceError: a is not defined
console.log(window.a); //直接去GO中查找是否存在a这个成员,如果没有则不会报错,值是undefined

实例分析

var x = [12, 23];

function fn(y) {
    y[0] = 100;
    y = [100];
    y[1] = 200;
    console.log(y);
}
fn(x);
console.log(x);

详解:

JavaScript 代码是由浏览器中的 JavaScript 解析器来执行的。JavaScript 解析器在运行 JavaScript 代码的时候分为两步:预解析和代码执行。

预解析:

在当前上下文(全局上下文/私有上下文/块级上下文),JS代码自上而下执行之前,首先会把当前上下文中带 “var/function” 关键字进行变量提升:带var的只是提前声明,并没有赋值「定义」,但是带function关键字的,是提前的声明+赋值(定义);基于“let/const”声明的变量不具备这个机制的...

  • EC(G)进栈执行,变量提升:var x,function fn(y){...},在堆内存中开辟空间0x001,其中分为三部分,1. 创建作用域[[scope]]EC(G) (由于该函数在全局上下文EC(G)下创建,所以取值为EC(G));2. 存储代码字符串;3. 设置静态属性方法【键值对】,在VO(G)中声明变量x,声明变量fn,关联地址0x001;
  • 代码开始执行,堆内存开辟空间0x002,存储数组对象{0:12,1:23,length:2},地址放入VO(G)中,变量x关联0x002
  • 函数fn(x)即fn(0x002)执行,形成一个全新的私有上下文EC(FN),进栈执行。
  • 函数代码执行之前,
    • 初始化作用域链<EC(FN),EC(G)>
    • 初始化this:window
    • 初始化arguments{0:0x002,length:1}
    • 形参赋值:y=0x002。AO(FN)中有一个私有变量y
    • 变量提升:无(函数体中没有其它变量声明)

函数中私有变量:形参变量、当前私有上下文中声明的变量

    • 代码执行:y[0]=100,操作私有变量y,找到堆内存中的0x002中的数组对象,索引为0的值,修改为100;
    • y=[100],在堆内存中开辟空间0x003,存储{0:100,length:1},VO(FN)中y关联地址0x002
    • y[1] = 200,找到堆内存中的0x003中的数组对象,自动扩容,索引2的值为200,{0:100,1:200,length:2}
    • console.log(y),输出[100,200]
  • 函数代码执行完毕,EC(FN)出栈释放,以此优化栈内存
  • console.log(x),输出[100,23]

通过浏览器开发者工具可以看到,函数fn的作用域就是EC(G)

总结

函数底层运行机制如下:

创建函数

@1 在堆内存中分配一个16进制内存地址的空间

@2 把函数中的内容存储到空间中

+声明函数作用域[[scope]]【在哪个上下文创建的,作用域就是谁

+把函数体中的代码当作字符串存储起来 “代码字符串”

+作为对象,存储它的键值对(静态私有属性和方法) [name:fn.length:1形参个数]

@3 把空间地址放在栈中,供变量【函数名】引用

函数执行

ps:实参,传递的是变量的值(原始值或地址)

@1 形成一个全新的私有上下文(EC(fn)),然后进栈执行。在私有上下文中,有一个AO(fn)私有变量对象【AO是VO的分支,在函数中,变量对象是AO】,用来存储当前上下文中声明的私有变量。

@2 函数在代码执行之前需要做的事情:

  • 初始化作用域链 <自己私有上下文,函数的作用域{上级上下文}>
  • 初始化this
  • 初始化arguments实参集合
  • 形参变量赋值: 形参变量也是当前上下文中的私有变量,是要存储到AO中的
  • 变量提升

@3 代码开始自上而下执行

执行中遇到某个变量,首先看是否为自己的私有变量【形参是私有的、当前上下文声明过的变量是私有的,这些私有变量都在 AO(fn)

  • 是自己私有的,则操作是自己的,和外界变量没有关系【保护作用】
  • 若不是是自己私有的,则向上级上下文查找....,一直没有,找到全局位置,这种查找机制,称之为作用域链查找机制

@4 代码执行完,当前形成的私有上下文出栈释放【也可能不释放】