程序的内存空间:
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引擎核心组件
- 执行环境栈 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代码文件被浏览器载入后,默认最先进入的是一个全局的执行上下文。当在全局上下文中调用执行一个函数时,程序流就进入该被调用函数内,此时引擎就会为该函数创建一个新的执行上下文,并且将其压入到执行栈顶部(作用域链) 。浏览器总是执行位于执行栈顶部的当前执行上下文,一旦执行完毕,该执行上下文就会从执行栈顶部弹出,并且控制权将进入其下的执行上下文。这样,执行栈中的执行上下文就会被依次执行并且弹出,直到回到全局的执行上下文。
执行上下文的类型
- 全局执行上下文: 这是默认的、最基础的执行上下文。不在任何函数中的代码都位于全局执行上下文中。它做了两件事: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),全局执行上下文整体进栈(执行环境栈)执行。
优先级处理
- a=b=10
执行流程(从右到左):
- 创建10
- b=10
- a=10
- 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 代码执行完,当前形成的私有上下文出栈释放【也可能不释放】