浅谈JS中的执行上下文和作用域链(进阶)(包含变量提升原理、AO、VO等)

关于JS中的执行上下文和作用域链的介绍,本篇是进阶篇,关于变量提升的原理和上下文的创建相关内容

点这里,关于关上下文和作用域链内容请看基础篇


先看两段代码:

function test1() {
    console.log(a)
    console.log(b)

	var b="变量b的值"
    var a
    
    console.log(a)
    console.log(b)
    a=1
    console.log(a)

	//a()
    function a() {
        console.log("hello");
    };
    a=function (){
        console.log("world")
    }
    console.log(a)
    a()
}
test1()
function test2(i) {
    if(i  == 3) {
        return;
    }
    test2(i+1);
    console.log(i);
}
test2(0)

先思考一下控制台的打印结果:…

请添加图片描述

下面是控制台的输出(分两次打印)
代码片段一(test1)打印结果

请添加图片描述

代码片段二(test2)打印结果

请添加图片描述

-----------(下面会有详细解析过程)

如果这个结果不在你的意料之中,那么恭喜你,这篇文章很适合你。当然,如果有大佬看到有理解偏差的地方也可以指正。

如果不知道什么是上下文、作用域链、上下文栈的话可以去看上一篇,连接在顶部。


要向弄清原因,我们首先要知道函数上下文创建前后的具体过程

当代码执行流进入对应的代码片段,但在代码执行前,会有个预加载的过程,这个时候会创建对应的上下文环境(这里想表达的意思就是上下文创建过程是在代码执行前完成的)。

创建阶段:

创建上下文一共分为(包含)三部分:

  • 创建变量对象(VO)(按照下面顺序创建变量对象;注意:变量对象(VO)是JS引擎上实现的,并不能在JS环境中直接访问)**

    1. 创建arguments伪数组对象,搜索实参并赋值(函数上下文中的变量对象会创建arguments,在全局上下文中是没有arguments的)
    2. 扫描函数声明:扫描代码中的函数(用function定义),将对应的函数名作为VO的一个属性,该属性保存了一个指针,该指针指向函数在内存中的位置。扫描时如果有同名的属性,则对应的属性值会被覆盖(覆盖掉原有的)。
    3. 扫描变量声明(var):扫描代码中的变量,会将变量名作为VO对象的一个属性,不过不同的是属性值会被初始化为undefined。扫描时如果有同名的属性则会直接跳过(不会覆盖了)。
  • 创建作用域链

    作用域链里面每一级上下文的变量对象,最顶端是正在执行上下文的变量对象,往下依次是上一级上下文的变量对象。

  • 确定上下文中this指向

    全局上下文中 this指向window对象

    函数上下文中 this指向取决于函数调用方式

执行阶段:

此时会为VO变量对象被激活成了AO活动对象,我们所访问的数据都是AO对象上的,随着代码的执行,不断有变量被赋值(原本的变量保存的值是undefined,会随着代码执行被赋予新值)。

执行阶段结束,出栈等待回收
举个例子:
function test(a,b){
    console.log(a)		//f a()
    console.log(b)		//6
    console.log(num)	//undefined
    console.log(fn)		//undefined
	var a=1				
    console.log(a)		//1
    var num=2
    function a(){	
    }

    var fn=function(){
    }
    console.log(num)	//2
    console.log(fn)		//f (){}
}

test(5,6)

创建全局上下文(伪代码)

ExecutionContext={
    VO:{...},
    scopeChain:{...},
    this:null
}

代码执行流进入first,创建函数上下文(伪代码):

firstExecutionContext={
    //变量对象
    VO:{
        arguments:{
            0:f a()//注意:属性名相同时,函数会覆盖掉原有的属性;原本的形参变量为a和函数名相同
            1:6,
            length:2
        },
        a:f a(),		
        b:6		//原本的形参
        num:undefined,
    	fn:undefined
    },
    scopeChain:{VO(first),window},
    this:window
}

随着代码执行,第6行a被赋值1,第7行打印出1,第八行num原本的值为undefined,被赋值为2,同理代码执行到第12行,fn被赋值一个匿名函数。


如果上述内容你看懂了的话(如果没看懂那一定是我没讲清楚hhh),对于第一个代码片段应该就有了思路了,其实很简单的。下面先回顾一下第一个代码片段

function test1() {
    console.log(a)
    console.log(b)

	var b="变量b的值"
    var a
    
    console.log(a)
    console.log(b)
    a=1
    console.log(a)

	//a()
    function a() {
        console.log("hello");
    };
    a=function (){
        console.log("world")
    }
    console.log(a)
    a()
}
test1()
解析:

代码执行流进入test1函数,创建对应上下文环境:

firstExecutionContext:{
	VO:{
        arguments:{...}
        a:pointer to function a(...console.log("hello")...)		//注意函数申明优先于变量申明
        b:undifined
    },
    scopeChain:{...},
    this:{...}
}

代码执行:

console.log(a)
    //ƒ a() {
                console.log("hello");
            }

打印的a为保存在变量对象中的属性a,这个a保存了函数在内存中的位置,所以打印出a函数(函数体内部有 console.log(hello)的函数)

console.log(b)		//undefined	

此时在变量对象中的b并没有赋值,打印undefined

var b="变量b的值"
var a

console.log(a)		//同样是 打印函数体内部有 console.log(hello)的函数
console.log(b)		//变量b的值

此时为变量b赋值为“变量b的值”,b的值不在是undefined·,打印结果为 “变量b的值”

此处的var a 可以理解成无效(就算删掉了var关键字,打印结果也是一样的),因为代码执行到这以前对于变量a的申明,函数申明优先于变量申明,所以这个地方不会打印undefined 而是打印hello的那一个函数。

a=1
console.log(a)		//1

这里为a 赋值为1,打印a,a不再是函数了,而是一个保存数值1的变量

//a()	//  a  is not a function

如果这里把注释解开,真的去执行这行代码一定会报错的,a is not a function

a=function (){
    console.log("world")
}
console.log(a)		// 打印函数体内部有 console.log(world)的函数
a()		//world

在这里,变量a保存了一个函数,(这里相当于对变量a的一个赋值,和上面提到的函数申明是不一样的)

所以打印出一个函数(函数体内部有 console.log(world)的函数),当然此时可以执行a(), 执行函数体内部代码,打印出world

请添加图片描述

说完了第一个代码片段,下面来说说第二个代码片段:

function test2(i) {
    if(i  == 3) {
        return;
    }
    test2(i+1);
    console.log(i);
}
test2(0)

第二个代码片段就涉及到了上下文栈的相关内容,代码执行流进入test2(0),创建test2(0)函数上下文并将其压如栈顶,此时看函数内部:i的值为0不等于3,所以代码继续执行,代码执行流进入test2(1),创建上下文,并压入栈顶(原本的test2(0)上下文被压入了栈中),i的值为1不等于3,函数继续执行,test2(2)…test2(3),代码执行流进入test2(3)后,创建上下文,压入栈顶,代码执行…函数内部 i等于3满足条件,执行return语句,该上下文被弹出上下文栈。上下文栈控制权交给test2(2),i的值为2,执行打印语句打印2,函数执行完毕出栈,将控制权交给test2(1), 打印1…控制权交给test2(0),打印0,test2(0)被弹出上下文栈被销毁。

请添加图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值