写在前面
往期内容
作为一个前端攻城狮,你真的了解浏览器是如何将我们写的 JS 代码运行的吗?了解 ECStack、EC、GO、VO、CO 这些名词代表什么含义并如何作用的吗?你知道我们 JS 文件中的变量是如何定义、赋值的吗?另外什么是变量提升?在新老版本浏览器下有什么区别吗?
这篇文章,我们就深入 JS 底层运行机制,一一分析以上问题吧。
JS 中的堆(Heap)和栈(Stack)
浏览器是如何执行我们写的 JS 代码的呢?
我们编写的 JS 代码都是需要在一个运行环境中运行的,浏览器想要执行 JS 代码,需要提供一个供代码执行的环境。常见的运行环境有:
- 浏览器引擎
- node(基于 V8 渲染 JS)
- webView(V8 引擎)
注意: 以下我们只分析浏览器引擎。
首先我们先来了解几个高大上的名词,并作出分析。
ECStack(Execution Context Stack)执行环境栈
在代码运行时,浏览器会从计算机内存中申请一部分内存,用作代码的运行,这部分内存叫做:
ECStack(Execution Context Stack): 执行环境栈,它是栈内存,遵循FILO(First In Last Out)的方式工作
EC(Execution Context)执行上下文
在代码执行过程中,为了区分全局和函数执行所处的不同作用域(为了保证每个词法作用域下代码的独立性),产生了
**EC(Execution Context):**执行上下文,即代码执行所在的词法作用域
VO(Varibale Object)/ AO(Active Object) 变量对象
在每一个执行上下文中代码执行的时候,都有可能创建一些变量,所以在每一个上下文中(不论是全局还是私有),都会生成一个存储变量的空间,叫做:
**VO(Varibale Object):**变量对象,存放当前上下文中的变量
**AO (Active Object):**变量对象,存放当前私有上下文中的变量
GO(Global Object) 全局对象
浏览器在新建一个页面的时候,会将所有之后可能会被 JS 调用的属性和方法(内置)都存放在一个地方,叫做:
**GO(Global Object):**全局对象,存放所有浏览器后期需要共 JS 调用的属性和方法(内置)
在创建完GO(Global Object)全局对象之后,会放入ECStack,然后在全局创建一个叫做 window 的变量指向GO(Global Object)
以上就是浏览器在代码执行之前,默默做的准备工作。
注:
VO和AO区别不是很大,不过全局上下文称为VO(G),私有上下文中的变量对象称为AO(xx)GO跟VO/AO没有关系
Heap(堆内存)
在了解堆内存之前,我们先来分析一下 var a = 1这样一件事的过程:
声明(declare):var a;
定义(defined):a = 10;
- var a = 1 分为三个步骤:
- 生成一个值(本例中为 1)
- 声明**(declare)**,一个变量存放在当前执行上下文(EC)的变量对象(VO)中
- 定义(defined),让变量和值进行关联(实际上是一个指针指向过程)
- 在生成一个值的过程中,也分为两种不同的情况
- 基本类型值直接存储在栈内存中
- 引用数据类型值需要先开辟一个堆内存,将内容存储进去后,生成一个 16 进制的地址
- 然后会将此堆内存地址放入栈内存中供变量调用
所以堆内存就是存放一系列引用数据类型值的一部分内存
用一段代码分析以上过程
了解了以上这么多定义,可能各位小伙伴们都会比较懵,那我们就通过一段代码分析一下执行的过程,并给出图解。
var x = [23,34]
function fn(y){
y[0] = 200;
y = [300];
y[1] = 400;
console.log(y)
}
fn(x);
console.log(x)
! 重要提示:以下内容建议先看一遍分析图,然后对照解析文字,重新理解分析图并在脑海中形成整个代码执行逻辑
代码进入执行环境,生成执行环境栈
ECStack。之后生成全局对象GO,放入执行环境栈生成全局执行环境
EC(G), 在其中生成变量对象VO(G),即全局变量对象,将代码放入 EC(G)中,压入执行环境栈ECStack中执行EC(G)中代码依次执行(如果存在变量提升进行
变量提升,下一部分详解)var x = [23,24];上文有分析堆内存的时候有说明var声明定义一个值的步骤,进行以下操作:- 生成值。发现值[23,24]为引用类型值,就开辟一块堆内存,我们假定堆内存地址为
AAAFFF000。 - 再声明变量
x,将其放入当前上下文中的变量对象VO(G)中去。 - 最后进行定义,将变量
x和堆内存地址进行关联。 =====》x <------> AAAFFF000
- 生成值。发现值[23,24]为引用类型值,就开辟一块堆内存,我们假定堆内存地址为
function fn(y){...};遇到函数定义,就会开辟一块函数堆内存,我们假定地址为AAAFFF111。生成该函数堆内存的时候,会进行以下操作:生成一个
[[scope]],其中包含着该函数生成时所处的执行上下文,本例中为EC(G)对函数的形参进行声明,即声明一个参数
y将函数执行体以
字符串的形式放入堆内存中,以待之后函数调用。本例中为:y[0] = 200; y = [300]; y[1] = 400; console.log(y)以函数名
fn为变量名进行声明并定义,放入当前变量对象VO(G)中,以便之后调用
fn(x);执行函数fn- 其实就是调用我们存储在堆内存中的
AAAFFF111 - 函数执行时会形成一个私有的执行上下文,我们叫他
EC(FN)。 - 在其中生成私有变量对象
AO(FN) - 生成作用域链
scope-chain, 这里解释一下,作用域链中保存了函数的当前作用域及其上级作用域。链的左边是当前作用域,链的右边是上级作用域。本例中当前作用域为:EC(FN),上级作用域为EC(G)。形如:<EC(FN), EC(G)>。有小伙伴可能会问了,这样做有什么用呢? 在当前作用域中取值的时候,如果该变量未定义,则会向作用域链的右侧作用域(也就是上级作用域)继续查找,直到找不到为止。 哦吼吼,是不是有点能 get 作用域链是怎么来的了? - 对形参进行定义(赋值),此时的形参变量是
私有变量,会放入该执行上下文(EC(FN))的私有变量对象AO(FN)中去 - 之后这里还会进行
变量提升,定义arguments(箭头函数没有),等等一系列操作 - 将堆内存中存储的函数执行体依次执行
- 。。。
- 其实就是调用我们存储在堆内存中的
补充
对象数据类型
由 0 到多组键值对(属性名和属性值)组成的数据。
属性名类型可以是任何基本类型,处理中可以和字符串互通
但是不管怎么说,属性名肯定得是一个值,不能是引用数据类型,且到最后也是转换成字符串处理的
在
for in循环当中 ,获取的属性名都会变为字符串,且无法迭代到属性名为 Symbol 类型的属性let obj = { x:100 } obj[x] // 把x变量存储的值当做属性名,获取对象的属性值,x不存在 则报错 obj['x'] // 获取属性名为x的属性值 =》 10
变量提升
名词释义:
在当前上下文中(全局/私有/块级), JS 代码自上而下执行之前,浏览器会提前处理一些事情(可以理解为词法解析的一个步骤,词法解析一定发生在代码执行之前),使得我们可以在创建变量代码执行之前使用变量而不报错。
**变量提升:**浏览器会把当前上下文中所有带有VAR/FUNCTION关键字的内容进行提前的声明、定义,此过程发生在代码执行之前。
带 VAR 的只会提前进行声明,默认值为 undefined。当代码运行到真正的声明语句时,已经声明过的变量不会重新进行声明。
带 FUNCTION 的会提前
声明和定义两个操作,在全局上下文中的变量提升中,func = 函数 函数在这个阶段已经赋值了。基于
VAR和FUNCTION在全局上下文中生命的变量(全局变量),会映射到 GO(全局对象 window)上一份,作为它的属性 。之后有一个修改了,另外一个也会跟着修改var a = 12; console.log(a) // => 12 console.log(window.a) // => 12 映射到GO上的属性 window.a = 13; console.log(a); // => 13 映射机制导致都进行了修改变量提升的顺序:按照代码执行顺序
自上而下进行变量提升,不存在优先级问题。只有
VAR/FUNCTION会发生变量提升(ES6 中的 LET 和 CONST 不会)
!! 新老版本差异:
- 全局上下文
EC(G)中的变量提升,不论判断条件是否成立,都要进行变量提升- 重要细节:条件执行块中 FUNCTION 的 在新版本浏览器中只会提前声明,不会再提前赋值了。
老版本:var a ; func = 函数新版本:var a; func- 这里
老版本指 IE10 以下和低版本 Chrome;新版本指新版本现代浏览器
console.log(a, func); // 老版本 undefined function func(){}
console.log(a, func); // 新版本 undefined function func(){}
if(!('a' in window)){ // ’a‘ in window 检测a是否为window的一个属性 !True => false
var a = 1;
function func(){}
}
console.log(a) // => undefined
在
新版本中,存在块级作用域,当大括号中出现let/const/function定义的时候,就会形成块级作用域,与其他作用域隔离。在
新版本中,因为要兼容 ES3/ES6, 在遇到function a在全局声明过,在私有作用域下也有声明的情况,浏览器会把私有函数定义那行之前所有对a的操作,映射给全局一份。但是之后的代码和全局没有关系。
一道特别火的变量提升题目
为了更好理解上述描述3,我们来做一道前一段时间非常火的面试题
var a = 0;
if(true){
a = 1;
function a(){};
a = 21;
console.log(a)
}
console.log(a)
老版本输出
21 21// 老版本浏览器没有块级作用域,所以a一直都是全局下的a,最后被定义为21新版本输出
21 1新版本中 由于产生了块级作用域,该作用域下的 a 会被重新声明,跟全局作用域下的 a 隔离起来
在运行到块级作用域
function a(){};这一句的时候,参考上文描述 3浏览器会把私有函数定义那行之前所有对
a的操作,映射给全局一份所以此时全局作用域下的变量
a,会被function a(){};定义之前的a=1映射为a = 1所以最终,输出
21 1
写在最后
欢迎访问我的博客fxflying.com