一篇了解 JS的上下文(context)

查阅了大量文章文献,终于把js的执行顺序搞通了,我发觉这里面难搞的原因在于,很多文章只写了某一块知识点,或者某块知识点还没讲透彻导致有点懵,现在我总算是搞懂了,所以我想从头到尾彻彻底底的讲一遍。

1.JS的执行顺序

先简单讲下整个执行顺序

1.我们所写的js代码在执行阶段前,会有个预加载过程,目的是建立当前js代码的执行环境,而这个执行环境就是上下文(context)。
2.这个上下文包含了VO(Variable Object变量声明对象),AO(Active Object动态变量对象),scopeChain(作用域链)。
3.建立好上下文后,才会开始执行我们写的js代码。

1.1上下文(Execution Context)

通过上面我们可以直到,上下文就是每段代码的执行环境。

它分为三种类型:
1.全局级别的代码 - 这个是默认的代码运行环境,一旦代码被载入,引擎最先进入的就是这个环境。
2.函数级别的代码 - 当执行一个函数时,运行函数体中的代码。
3.Eval的代码 - 在Eval函数内运行的代码。

先讲函数级别的上下文(执行环境)。

上下文建立的阶段:
1.建立VO(Variable Object)
2.建立AO(Active Object)
3.建立 scopeChain
在这里插入图片描述

1.1.1 静态变量声明(Variable Object)

我们看下面的代码:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
可以看到foot在声明前打印会显示“undefind”,但ball在声明前打印会报错显示 “ball is not defined”。两者的区别在于,后面foot被声明了,而ball至始都没有声明过。

那么,就抛出一个问题了,既然代码是从上到下执行,按道理来说两者都应该是“is not defined”,为什么前者会是undefind并且无报错执行。

答案就是在js代码执行前有个预加载阶段用来建立代码的执行环境(上下文),而建立上下文的第一个阶段就是建立当前环境声明过的变量Variable Object。

我们来看下面代码所生成的上下文对象

function fn(){
        var foot = '脚';
        var head = function(){
            return '头';
        }
        function hand(){
            return '手';
        }
    }

Variable Object :
// VO创建

fnExecutionContext  = {//fn函数的上下文对象
  VO: {
    arguments: { .... },
    hand:hand函数的引用地址,//函数声明
    head: undefined, // 变量函数声明
    foot: undefined, // 变量声明
    this:Window,//this指向
  },
  scopeChain: {} //作用域链
}

我们可以看出,就跟我们刚才打印的一样,声明过的参数全部都被记录在了这个VO对象中,只不过var声明的参数值都为undefined。
也因此我们为什么刚才的打印会undefined了。

那么会有个疑问,为什么function声明的函数就有值呢,这个简称变量提升:

所谓的变量提升,就是在建立上下文是会把function声明的函数都提升到上面来,这样即时你把function写到下面,他依然会比你var声明的值更快获得实际的值。
那么什么时候var声明的变量才能有值呢,这个就是AO的原因了。

1.1.2 动态变量声明(Active Object)

所谓AO就是Active Object的简写,那么什么是AO呢,他其实就是VO,只不过在初步建立完VO后,就会开始执行我们的js代码。
js代码从上到下执行的过程中,如果遇到赋值的情况,那么就会更新VO变成了AO。我们来看看AO对象。

fnExecutionContext  = {//fn函数的上下文对象
  AO: {
    arguments: { .... },
    hand:hand函数的引用地址,//函数声明
    head: head函数的引用地址, // 变量函数声明
    foot: '脚', // 变量声明
  },
  scopeChain: {} //作用域
}

是不是一下子就懂了。

接下来我们看VOAO的arguments参数

1.1.3 Variable Object的arguments属性

每个函数的调用,都必然存在arguments对象,它存在于Variable对象当中,接下来我们来看看arguments对象里面到底有些什么东西。

function showargs() {
	console.log( arguments );
}

showargs(1,2,3,4,5);

打印结果如下:
在这里插入图片描述
从上图可以明白,其实就是把调用这个函数时所传入的参数,通过一个类数组对象接收了。

类数组对象是什么意思呢,就是类似数组的对象,他没有数组的一些方法,但是他却是以{“0”:‘您好’,“1”:我好,“2”:大家好}
这种很像数组的方式存在。

除了传入的参数,我们通过arguments.callee还能获得当前函数的代码

function showcallee() {
    var a = '这里是代码';
    var b = '这是另一段代码';
    var c = a + b;
    
    console.log(arguments.callee);
    
    return c;
}
showcallee();

在这里插入图片描述
到此,arguments对象我们就了解完毕了,他在上下文中起着,记录调用此函数时所传入的参数的作用。

1.1.4 scopeChain作用域链

在上下文对象中,我们看到了一个scopeChain作用域链,那么这个作用域链有什么用呢。
我们看如下代码:

function fn(){
        var foot = '脚';
        function footBall(){
            var footBall = '足球';
            console.log(footBall+ '需要:' +foot);
        }
        footBall();
    }
    打印结果如下:足球需要脚

从上面代码看出,函数footBall可以使用到函数fn所声明的变量foot。
如果没有scopeChain,这是做不到的,scopeChain就是充当这一角色。

JS权威指南指出”JavaScript中的函数运行在它们被定义的作用域里,而不是它们被执行的作用域里.”

ECMA262中所述 任何执行上下文时刻的作用域, 都是由作用域链(scope chain)来实现.在一个函数被定义的时候, 会将它定义时刻的scope chain链接到这个函数对象的[[scope]]属性.

作用域链其实就是所有内部上下文的变量对象的列表。用于变量查询。比如,在上述例子中,“footBall”上下文的作用域链包含了VO(footBall),VO(foot)和VO(global)。

看完上面的概念,我们可能会有点懵,接下来我们看下这个footBall的上下文对象。

footBallExecutionContext = {
	  VO: {
	    arguments: { .... },
	    footBall : undefined, // 变量声明
	    this: fn,
  		scopeChain: {
			VO(footBall函数的VO),
			VO1(fn函数的VO),
			VOGlobal(全局的VO)
		} //作用域链
	  }
	  .....
	}

可以看出来footBall的VO对象是没有foot的变量声明,那为什么执行下来不会报错,而且还有值呢。
这就是scopeChain的意义了,如果在当前VO找不到相关的定义,便会通过scopeChain对象找父级的VO,找不到就再往上找,直到全局的VO。所以当footBall的VO对象里没有foot的定义时,便会去找父级的VO也就是fn函数的VO,在fn函数里有foot的定义。

如此下来,整个上下文就讲完啦。简单来说就是一个执行代码的环境,但细讲会发觉他有好多的属性,这都是为了在执行每一段js代码时都能有序,无误的执行。不得不赞叹,语言类的开发者真的要顾全很多的东西。

总结:

一个js的代码的执行顺序简单来讲是两步

1.建立当前所要执行的js代码的执行环境,简称上下文。
2.从上到下一次执行代码。

而这篇文章主要讲的是建立环境的过程和顺序
下一篇我们会讲我们所写的代码的执行顺序,这里我们

参考文献:https://blog.51cto.com/enjoyjava/1124940
参考文献:https://www.tensweets.com/article/5ba71b42f0cb0c04f86b23dd
js代码的执行顺序参考文献:https://www.jb51.net/article/127025.htm

拓展

我们都知道,在es6多了let这个声明方式,虽然用是用的很多,也知道他的作用,但其实他和var的区别还是蛮大的。特别是在上下文环境建立的过程中。

比如最简单的:
console.log(k);//undifined
var k=2;

比如最简单的:
console.log(k);//报错
let k=2;

这里其实说的就是你定义了let的时候,在预编译阶段let定义的时候就会分配一个内存空间给这个变量,但分配后并不像var定义的那样,一旦分配内存空间,立刻定义值为undefined;用let定义的分配了内存,但这个内存并没有被分配任何值,为uninitialized状态。

executionContextObj={
‘scopeChain’: {VO},
‘variableObject’: {
k:uninitialized //uninitialized
},
‘this’: {window}
}

2.执行代码阶段(AO的建立阶段 VO=>AO)

执行第一行代码后,executionContextObj对象中的variableObject中的name:修改为jackiewillen.继续执行第二行,修改subName为kevin;这个时候你可能会有这样的疑问,如果这个subName只被let定义,而一直都未被赋值,那么值一直是uninitialized吗?
来看下面这样一段代码:

let k;
console.log(k);//the result is undefined;

那么为什么这里打印出了undefined呢,却没有报错呢,因为之前的console.log是放在let定义之前的,那个时候k的值是uninintialized;但过了预编译进入执行阶段程序运行到let k;时发现其未赋值,只是定义了这个变量,这个时候编译器才决定给k赋值为undefined。这个可以参见es6标准。
这也就是为什么let没有变量提升的原因。

进入到for循环,我们知道在es6中增加了块级作用域。也就是只要有{}都是一个块,在块外访问不到块内部的内容。
let声明的变量,事实上会另外开辟一个空间,叫词法环境,专门存储let声明的变量,只有在这个块中,才能使用。下面的代码blockObject就是所谓的此法环境。

如if(true){ let a=4;}
console.log(a);
这样就是访问不到这个a的值的。

接下来执行当for循环中的i值为1的时候,executionContextObj如下所示:

	var name = 'name';	
	var myName = 'yin';
	for(let i = 0; i<2;i++){
		let yourName = 'herry'
		console.log(i);
	}
	iftrue){
		let subName = 'kevin'
	}
executionContextObj={
	'scopeChain': {},
	'variableObject': {
		name:jackiewillen,
		myName:yin, //因为对于var定义的变量,不存在块作用域
		anonymous1:reference to function(){console.log(i)};
	},/* function arguments / parameters, inner variable and function declarations */
	'blockObject':{
		subName:kevin
	},
	'blockObject':{
		i:1,
		yourName:herry //为什么这里在block中呢,而不像subName那样在variableObject中呢,因为用let定义的都在它所待的块定中
	},
	'this': {window}
}

注:scopeChain每次生成时都会检查有没有闭包Closure存在,然后再检查有没有Block的然后把上一个variableObject拿过来(除去其中已经是闭包的元素,和block的元素。其样式如同scopeChain={closure+vo+bo+vo+bo…} ,还有,只有在函数被激活也就是调用的那一刻,其内部声明的变量vo和bo才会加到scopeChain中去。打算再写一篇文章专门去写这个scopeChain
注:此时的blockObject是一个全新的blockObject因为前一个已经被销毁,当一对花括号运行过后,相应的block也就销毁了。

1.第一步,看看有没有闭包,发现没有。

 'scopeChain': {}

2.第二步,看看有没有引用block块元素。发现引用了一个executionObject中的blockObject中的i,所以scopeChain为:

'scopeChain': {
		blockObject:{
			i:1
		}
	}

3.第三步,把区块链上的vo和bo都拿过来。

 'scopeChain': { blockObject:{ i:1 }}'variableObject': { 			
  		name:jackiewillen, 
  		myName:yin, //因为对于var定义的变量,不存在块作用域
 		anonymous1:reference to function(){console.log(i)}; }, 
		'blockObject':{
			subName:kevin
		}, 
 }

这个就是匿名函数anynmous1中的scopeChain

当for当中的i运行到2的时候,executionObject如下所示:

executionContextObj={
	'scopeChain': {},
	'variableObject': {
		name:jackiewillen,
		myName:yin, //因为对于var定义的变量,不存在块作用域
		anonymous1:reference to function(){console.log(i)};
		anonymous2:reference to function(){console.log(i)};
	},/* function arguments / parameters, inner variable and function declarations */
	'blockObject':{
		subName:kevin
	},
	'blockObject':{
		i:2,
		yourName:herry, //为什么这里在block中呢,而不像subName那样在		variableObject中呢,因为用let定义的都在它所待的块定中
	},
	'this': {window}
}

通过以上实验,我们知道let声明的变量会存放在一个单独开辟的词法环境当中,这个环境在这个{}花括号运行中才能拿到,直到这个花括号结束后,这个词法环境也会被销毁。

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值