由面试题浅析JS的底层运行机制

第一道题

首先来看第一道面试题,看似简单,涉及到的底层知识却很多,今天带领大家来一起分析这道题,题目如下

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

在分析这道题之前咱们首先来了解几个概念:

  • 堆(Heap) /栈(Stack) 内存
  • 执行上下文栈 ECStack(Execution [ˌeksɪˈkjuːʃn] Context Stack)和 执行上下文 EC(Execution Context )
  • 全局对象 GO(Global Object)
  • 变量对象 VO(Variable Object)
  • 活动对象 AO(Activation Object)

这道图的流程图如下:
在这里插入图片描述
上图分析:

  1. 首先函数fn执行,就是把全局变量x的值作为实参传递给函数的形参。
  2. 函数执行的目的:想让之前存储在堆中的代码字符串执行 -> 代码执行就要有自己的执行环境;
  3. 函数执行的时候:
    1> 首先形成一个全新的私有的上下文,供代码执行
    2> 在执行代码之前先初始化作用域链(scopeChain):作用域链的组成规则是–<EC(FN),EC(G)>。 EC(FN):自己所在的上下文,EC(G):函数的作用域
  • 当前函数的作用域指什么:作用域是在函数创建的时候声明的,在哪个环境(上下文中)创建的,函数的作用域就是谁–比如此题中的函数fn在EC(G)全局执行上下文中创建的,那么函数fn的作用域就是全局执行上下文;

初始化作用域链规则:当前所在上下文是谁链的开头第一条就是谁,链接向谁就看当前作用域是谁,因为此函数在全局上下文中创建的,所以它的作用域就是全局执行上下文,所以这个函数的作用域链开头是自己的上下文,结尾是函数的上下文。

作用域链查找机制:如果函数fn在代码执行过程中遇到一个变量a,先看这个a是不是自己的私有变量,如果是私有的那接下来进行的所有操作都是自己的,跟别人没关系。如果a在私有变量中没有那就不是私有的,那就找到链的另外一头即全局上下文中,全局上下文中有a就是全局变量,这就是所谓的作用域链查找机制。

初始化作用域链目的:初始化作用域链就是为了构建出我们用变量时,这个变量是自己的还是它的上级上下文中的,作用域链就是给我们的查找提供了一个方向/路径,这是作用域链的目的。
作用域链机制(初始化作用域链的目的):日后在私有上下文执行代码的时候,遇到一个变量,我们首先看是否为自己的私有变量(在自己的变量对象中有是私有的,没有则不是);不是私有的按照作用域链找上级上下文中的……一直找到全局上下文为止。

  • 函数在执行之前: (初始化顺序就按照以下的顺序记忆即可)
    No.1 初始化作用域链 <EC(FN),EC(G)>
    No.2 初始化this执行:window
    No.3 初始化实参集合:arguments
    No.4 形参赋值:y=AAAFFF000(形参赋值的时候也会放在函数的变量对象,
    形参变量也是存放到自己上下文中的私有变量对象中 => 它是私有变量 AO(FN))
    No.5 变量提升:
    No.6 代码执行: y都是自己私有的:AAAFFF000
    y[0] = 100;//y虽然是自己私有的,但是y跟全局中的x指向同一个堆AAAFFF000,所以X也改变了
    y = [100];//私有的y重新指向了[100]这个对象,不再跟全局变量x共用一个堆
    y[1] = 200;//这一步操作就是私有变量y自己的跟全局的x不再有关系
    console.log(y);//[100,200]

  • 每次函数执行的时候都先形成一个全新的私有的上下文来供函数体中的代码执行的执行环境,但最终执行的时候都会放进栈中执行,所以私有上下文先进栈执行,如果此时栈中有一个没有被释放的上下文,比如这题中全局上下文还没被释放,就先把全局上下文压缩到底部,把自己的上下文放到栈的顶部。

  • 所有的形成的私有上下文在执行的时候都会放在栈的顶部,原来的会被压在栈的底部。

私有上下文EC(FN)在执行的时候,先进栈,把之前的全局上下文EC(G)压到栈的底部,自己在栈的顶端
新形成的上下文要进栈执行:

  1. 把全局上下文放到栈的底部(压缩栈)
  2. 新进来的上下文放到栈的顶部
  3. 上下文中的代码执行完,就会被出栈(释放);
  4. 此时栈中只剩下全局EC(G)了,fn出栈后,全局移动到上面开始执行

VO变量对象(Variable Object)
AO活动对象(Activation Object)
AO是VO的一个分支,都是变量对象;AO是活动对象,函数中的变量对象都称为AO
私有上下文EC(FN)中有活动对象AO(FN),AO(FN) 是私有变量对象,用来存储在代码执行的过程中创建的私有变量的。

在这里插入图片描述

此题整体思路总结:

  • 函数的执行目的是让代码执行,一定要形成一个自己的全新的私有环境,叫做私有执行上下文EC(FN),此处的这个FN是指目前例题中的函数,代码执行都要放进栈中执行,所以形成私有上下文之后的第一件事就是进栈。

  • 进栈之前发现栈ECStack中已经有一个全局上下文EC(G)了,就把这个全局上下文压缩到栈的底部,然后再把这个私有上下文EC(FN)放进栈的顶部来进行执行。

  • 每一个上下文里都有一个自己的私有变量对象,全局下叫VO(G),函数私有上下文中叫AO,AO是VO的一个分支也是代表私有变量对象。在私有上下文中创建的变量都会放进私有变量对象中进行存储。
    除了以上这些,函数代码在执行之前还要进行一些步骤:

  1. 初始化作用域链 <EC(FN),EC(G)> 起始端是自己的上下文,结尾端是函数作用域,即函数创建时所在的上下文。形成这个链条的作用是:遇到一个变量比如说y就看是否为自己私有的变量,如果是自己私有的就不再顺着链条向上查找了,如果不是自己私有的变量就沿着链条向上查找,假设如果此题中有一个变量a,在当前上下文中没找到,那就去链条的尾端即创建函数的上下文中找,一直找到全局变量为止。
  2. 初始化this执行:window
  3. 初始化实参集合:arguments
  4. 形参赋值:y=AAAFFF000(形参赋值的时候也会放在函数的变量对象,
    形参变量也是存放到自己上下文中的私有变量对象中 => 它是私有变量 AO(FN))
  5. 变量提升:
  6. 代码执行:

代码执行时的流程解析:

  1. 形参变量也是私有变量,要放进私有变量对象中存储。把X作为实参传递进来,因为X是引用数据类型,所以传递进来的实参实际是X关联的堆地址AAAFFF000,y会基于这个地址AAAFFF000找到跟X共用的这个堆,将索引为0的元素修改为100,那么全局X再访问这个堆也会受到影响。
  2. y = [100]相当于在私有上下文中创建了一个新的值,是一个数组地址为BBBFFF000,此时的y都是私有的。y[1] = 200就是给这个数组增加一个元素[100,200],length也变为2,然后打印出y这个私有变量就是[100,200]。
  3. 当前函数这个上下文一旦执行完,正常情况下这个函数没有东西被别的占用,我们就把它移出栈内存释放掉,这样可以保证栈内存中的上下文不会特别多,栈内存的空间也不会特别大,提高运行速度,性能优化。
  4. 把函数的私有上下文释放掉后,把全局的上下文移动到栈的顶部,继续执行剩下的代码。
第二道巩固题
  • 巩固练习题:写出下列代码的输出结果
var a = {n: 1};
var b = a;
a.x = a = {n: 2};
console.log(a.x);
console.log(b.x);

首先了解几个概念
堆(Heap) 栈(Stack) 内存
ECStack(Execution [ˌeksɪˈkjuːʃn] Context Stack)和 EC(Execution Context )
GO(Global Object)
VO(Variable Object)
AO(Activation Object)

此题的解析图如下:
在这里插入图片描述

  • 解析图的详细分析:
  1. 浏览器提供一个供代码执行的环境=>栈内存
  2. 执行环境栈 ECStack:1.供代码执行;2.存储基本数据类型值(变量/堆的引用地址)。
  3. JS中存在多种作用域(全局、函数私有的、块级私有的),代码执行之前,首先形成自己的执行上下文,然后把上下文进栈(ECStack),进栈后在当前上下文中再去执行代码。
  4. EC(G)全局执行上下文:全局代码执行的环境。当页面关闭,全局上下文会出栈释放。
  5. 每一个执行上下文中一定存在一个空间,用来存储创建的变量=> 变量对象。
  6. VO(G)全局变量对象:就是在全局上下文中定义的变量,在全局上下文[EC(G)]中用来存储全局变量的空间,它不是GO => 只不过是某些情况下VO(G)中的东西会和GO中的东西有所关联而已–‘映射机制’
  7. window是GO:全局对象,它是一个堆内存(存储的都是浏览器内置的API属性方法),在浏览器端,让window指向它
  8. 所有的赋值操作都是以下三步(顺序不可变):
    <1>.创建值–基本类型值直接存储在栈内存中;引用值是先开一个堆,把健值对存储在堆中,最后把堆的16进制地址放到栈中供变量调用
    <2>.创建变量
    <3>.变量和值指针关联

以上分析结束后再看此题答案就很容易得出了

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

此题中注意一个点:
a.x = a = {n: 2};
a = a.x = {n: 2};
这两种写法都是先进行赋值(底层机制决定赋值优先进行):就是先:a.x = {n: 2} ;再a = {n: 2};

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值