执行上下文与词法环境

执行上下文

可以理解为当前代码的运行环境,同一个函数在不同环境中运行,会因为访问的数据不同导致产生不一样的结果,你可以抽象为一个执行代码的地方或者是抽象为内部对象用来描述代码的执行时环境,包含代码执行时的信息,该环境由js引擎构建。

执行上下文的类型:

  1. 全局上下文:只有一个,js引擎首次遇到<script></script>中的代码时(执行前)创建,它会在浏览器中创建一个全局对象(window),使this指向这个对象。
  2. 函数上下文:函数被调用时创建,每次调用都会为该函数创建一个新的执行上下文。
  3. Eval函数上下文:运行Eval函数中的代码时创建的执行上下文,少用且不建议使用。

调用栈
也叫执行上下文栈,是一种LIFO的数据结构(后进先出),用于存储执行上下文,用于追踪执行环境的执行。
js引擎首次遇到<script></script>时,会创建全局上下文并压入栈顶,然后每次执行函数都会创建新的上下文压入栈中,执行完后,执行上下文从栈顶弹出。

代码实例:
以下有三个上下文: 全局、changeColor、swapColors,swapColors在changeColor环境中运行,changeColor又在全局环境中运行。

var color = 'blue';
 
function changeColor() {
  var anotherColor = 'red';
  
  function swapColors() {
    var tempColor = anotherColor;
    anotherColor = color;
    color = tempColor;
  }

swapColors();
}
 
changeColor();

console.log(color);

执行过程可以在devTool的call stack中看到,其中anonyomus为全局执行上下文,其余为函数上下文。
在这里插入图片描述
以上执行过程:
1.js首次遇到全局代码,创建全局环境并压入栈顶,该环境中的代码开始执行。
2.调用changeColor,js引擎暂停执行全局环境中的代码,创建该函数的环境并压入栈顶,该环境中的代码开始执行。
3.changeColor调用了swapColors,因此changeColor中的代码暂停执行,创建swapColors环境并压入栈顶,其中的代码开始执行。
4.swapColors函数执行,该环境从栈顶中出栈,然后回到上一层changeColor中执行
5.中也没有代码执行了,意味着该函数也结束了,该函数的环境也从栈顶出栈,回到全局中开始执行代码。
6.一旦所有代码执行完毕,js引擎将从当前栈中移除全局环境。

注意:函数中遇到return能直接终止代码的执行,因此会直接将执行环境出栈

我们可以将ECStack看成是一个数组来模拟上述过程:

var ECStack = {};

js首次遇到全局代码,创建全局环境并压入栈顶,我们用global表示它,当整个应用程序结束时被清空,所以ECStack底部永远有个globalContext:

ECStack.push(<changeColor> functionContext);
ECstack.push(<swapColors> functionContext);
//swapColors出栈
ECStack.pop();
//changeColor出栈
ECStack.pop();

示列:

function f1(){
  var n = 999;
  function f2(){
    console.log(n);
  }
  return f2;
}
f1()(); //999

伪代码模拟上述行为:

var ECStack = {};
//global压栈
ECStack.push(globalContext);
//f1环境压栈
ECStack.push(<f1> Context);
//f1环境出栈
ECStack.pop(<f1> Context);
//ECStack.push(<f2> Context);
//f2环境压栈
ECStack.push(<f2> Context);
//f2环境出栈
ECStack.pop(<f1> Context);

f2并没有在f1中调用,而是f1将f2返回出栈后,f2在全局中在调用的,因此执行f1时候,f2不会创建执行环境,而在全局调用中才创建了一个新的。
具体演变过程如下:
在这里插入图片描述

ES3版本:

ES3版本执行上下文包含3个信息:

  • 变量对象
  • 作用域链
  • this

将执行上下文看成是一个对象,以上可以看成是它的属性。

var EvecutionContextObject = {
	//作用域链
	scopChain:{
		保存的是当前环境的变量对象(variableObject)和
		所有父环境的变量对象
	},
	//当前执行环境的变量对象
	[variableObject | activetionObject]:{ 
      保存函数的arguments/参数/内部标识符,
      内部变量和函数声明会在代码执行前执行上下文创建阶段就得到初始化
    ...
  },
  this:{}
}

变量对象
用来存储上下文中定义的变量和函数声明的容器。

不同的执行环境中的变量对象也不一样:

  • 全局环境的变量对象,在浏览器中就是window对象,在js代码的顶层中可以使用this引用它。所有全局标识符都作为window的属性存在。
console.log(this); //window
var a = 1; //挂到window上的属性
console.log(window.a); //1  
console.log(this.a); //1
  • 函数环境的变量对象,我们用活动对象AO(activation Object)来表示,活动对象就是变量对象,只不过处在不同的状态和阶段而已,当变量对象激活时就是活动对象,你可以将window和AO统称为变量对象。

作用域链
对于js来说变量的查询,是通过执行上下文来实现,查找变量时,先从当前环境中的变量对象中查找,如果没有,就往上找父环境中的变量对象,最终找到全局环境,如果没有就报错,这样由多个执行上下文的变量对象构成的链表叫做作用域链。

作用域和执行上下文有什么区别?
函数环境是在调用函数时创建,函数调用结束时就会自动释放。因为不同的调用可能有不同的参数:

var a = 10;
function fn(x){
  var a = 20;
  console.log(arguments);
  console.log(x);
}
fn(20);
fn(10); //不同的调用可能由不同的参数

JavaScript采用的是词法作用域(静态作用域),fn函数的作用域在定义时就确定了。
在这里插入图片描述
关联
作用域只是范围,只是表示变量在哪个范围内起效,其中并没有代码执行时的信息,当要获取变量值时,要通过执行环境来获取变量的值,所以作用域是静态的,而执行环境是动态的。

同一作用域下,对同一个函数的不同的调用会产生不同的执行环境,继而产生不同的变量的值所以,作用域中的变量是在执行时确定的,而作用域是在函数创建时就确定的。

生命周期
就是执行上下文创建到销毁的过程,共3个阶段:

  • 环境创建阶段
    • 生成变量对象(变量对象属性初始化)
      • 创建arguments
      • 扫描函数声明
      • 扫描变量声明
    • 建立作用域链
    • 确定this的指向
  • 代码执行阶段
    • 变量赋值
    • 函数的引用
    • 执行其他代码
  • 环境销毁阶段

创建阶段

生成变量对象(变量对象初始化)
这个阶段也叫函数声明和变量声明的预解析

  1. 创建aguments:如果是函数上下文,会首先创建arguments对象,将形参名作为变量对象的属性名,并赋予形参值,如果形参没有值则初始化为undefiend。
  2. 扫描函数声明: 找函数声明,将函数声明的名做为VO对象的属性名,将函数引用作为属性值,如果VO中已经有同名函数,则进行属性值覆盖。
  3. 扫描变量声明: 找变量声明,将变量名作为VO的属性名,并将属性值初始化为undefiend。如果变量对象里存在同名属性,则不会进行任何操作并继续扫描。

例子:

function person(age){
  console.log(typeof name); //function
  console.log(typeof getName); //undefined
  var name = 'abby';
  var hobby = 'game';
  var getName = function getName(){
    return 'Lucky';
  }
  function name(){
    return 'Abby';
  }
  function getAge(){
    return age;
  }
  console.log(typeof name); //string
  console.log(typeof getName); //function
  name = function(){};
  console.log(typeof name); //funciton
}
person(20);

在调用person时候,函数代码执行前,创建的状态是这样:

var PersonExecutionContext = {
	ActivationObject:{
		aguments: {
			0 : 20,
			length:1
		},
		age: 20,
		name: fn, // reference to function name(),
		getAge: fn, // reference to function getAge(),
		hobby: undefined,
		getName:undefined
	},
	scopeChain: {...},
	this:{...}
}

注: 全局上下文没有创建arguments这一步。
建立作用链
作用域链本身包含变量对象,作用域链是在变量对象之后创建的。
1.当定义函数时,会创建词法作用域,这个域是内部属性[[scope]],它保存着父变量对象,所以[[scope]]就是一条链。

person.[[scope]] = {
	globalContext.VariableObject
}

2.函数调用,此时创建函数上下文并压入栈中,然后复制函数的[[scope]]属性创建上下文的作用域链。

personContext = {
  scopeChain:person.[[scope]]
}

3.创建活动对象,然后将其推到作用域链的最前端。

var PersonExecutionContext = {
	scopeChain: {
		person.[ActivationObject,[[[scope]]]
	},
	ActivationObject:{....},
	this:{...}
}

确定this
如果当前函数被作为对象方法调用,则指向该对象,如果使用bind、call、apply等API进行委托调用,则指向委托的对象,否则默认指向全局对象。

执行阶段

当引擎进入函数执行代码时,js引擎开始对定义的变量对象赋值、开始顺则作用域链访问变量,如果内部有函数则创建新的执行环境压栈并把控制权交出。

此时代码从上到下执行的时候激活阶段的过程:

  1. 第一次执行console.log,此时name在VO是函数。getName在VO中是undefined。
  2. 执行到赋值代码,getName被赋值为函数表达式,name被赋值为abby
  3. 第二次执行console.log,name被字符串赋值,因此是String类型,getName是function类型
  4. 第三次执行console.log,此时的name由于又被覆盖因此是function类型

理解了执行环境就很好理解了变量提升,实际上变量和函数声明在在代码的位置是不会变的,而是在预解析阶段(代码执行前)被js引擎就放入内存中,这就解释了为什么在变量声明前就可以访问它。

es6引入了let和const关键字,从而使 JavaScript 也能像其他语言一样拥有了块级作用域,很好解决了变量提升带来的一系列问题。
最后代码执行完后的上下文:

var PersonExecutionContext = {
	ActivationObject:{
		aguments: {
			0 : 20,
			length:1
		},
		age: 20,
		name: fn, // reference to function name(),
		getAge: fn, // reference to function getAge(),
		hobby: 'game',
		getName: fn,fn to getName()
	},
	person.[ActivationObject,[[[scope]]]
	this: window
}

销毁阶段
一般来讲当函数执行完成后,当前执行上下文(局部环境)会被弹出执行上下文栈并且等待虚拟机回收,控制权被重新交给执行栈上一层的执行上下文。
在这里插入图片描述

ES5版本

ES5去除了ES3中的变量对象,以词法环境组件(LexicalEnvironment component)和 变量环境组件( VariableEnvironment component) 替代。也就是说词法环境代替变量对象来保存标识符。

生命周期

ES5的生命周期也是一样 创建 -> 执行 -> 销毁

创建阶段

做了三件事:

  1. 确定this
  2. 创建LexicalEnvironment(词法环境) 组件
  3. 创建VariableEnvironment(变量环境) 组件

伪代码如下:

var ExecutionContext = {
	thisBind: <this value>,
	LexicalEnvironment:{...},
	VariableEnvironment: {...}
}

thisBinding:
与es3一样,和执行上下文绑定,this的值在执行时确定,定义时不能确定。

创建词法环境
结构如下:

GlobalExecutionContext = {	//全局上下文
	LexicalEnvironment:{	//词法环境
		EnvironmentRecord:{	//环境记录
			Type: "Object", //全局环境
			// 标识符绑定在这里
		},
		outer: <null> //对外部环境的引用
	}
}

FunctionExecutionContext = { //函数上下文
	LexicalEnvironment:{	//词法环境
		EnvironmentRecord:{	//环境记录
			Type: "Declarative", //函数环境
			// 标识符绑定在这里
		},
		//对外部环境的引用
		outer: <Global or outer function environment reference> 
	}
}

可以看到词法环境有两种类型:

  • 全局环境: 是最外层的词法环境,因此对外部的环境引用为null,拥有一个全局对象(window)及其关联的方法和属性以及任何用户自定义的全局变量,this指向该对象。
  • 函数环境: 用户在函数中定义的变量被存储在环境记录中,包含了 arguments 对象。对外部环境的引用可以是全局环境,也可以是包含内部函数的外部函数环境。

词法环境有两个组件:

  • 环境记录器 :存储变量和函数声明的实际位置。
  • 外部环境的引用 :它指向作用域链的下一个对象,可以访问其父级词法环境(作用域),作用与 es3 的作用域链相似

环境记录器也有两种类型:

  • 在函数环境中使用 声明式环境记录器,用来存储变量、函数和参数。
  • 在全局环境中使用 对象环境记录器,用来定义出现在全局上下文中的变量和函数的关系。

因此:

  • 创建全局上下文的词法环境使用 对象环境记录器 ,outer 值为 null;
  • 创建函数上下文的词法环境时使用 声明式环境记录器 ,outer 值为全局对象,或者为父级词法环境(作用域)

创建变量环境
也是词法环境,他具有上面词法环境的所有属性。
在ES6中词法环境和变量环境的区别:

  1. 词法环境: 保存函数声明和let || const声明的变量
  2. 变量环境: 只保存var声明的变量(不保存函数声明)。

变量环境实现只能实现函数作用域,而词法环境在函数作用域的基础上实现块级作用域。

注意:
在es6下

  1. let || const声明的全局标识符会绑定到Sceipt对象,因此不能以window.xx获取。
  2. 使用var声明的变量会被绑定到Widow上,因此可以window.xx获取
  3. 使用 var || let || const 声明的局部变量都会被绑定到 Local 对象
  4. Script 对象、Window 对象、Local 对象三者是平行并列
    在这里插入图片描述
    在这里插入图片描述

箭头函数没有自己的执行上下文,因此就没有arguments、this指向,也没有变量提升,它的this用的是离他最近的外层普通函数的this。

例子:

let a = 20;  
const b = 30;  
var c;
 
function multiply(e, f) {  
 var g = 20;  
 return e * f * g;  
}
c = multiply(20, 30);

调用multiply时的执行上下文:

var GlobalExectionContext = {
 
  ThisBinding: <Global Object>,
 
  LexicalEnvironment: {  
    EnvironmentRecord: {  
      Type: "Object",  
      // 标识符绑定在这里  
      a: < uninitialized >,  
      b: < uninitialized >,  
      multiply: < func >  
    }  
    outer: <null>  
  },
 
  VariableEnvironment: {  
    EnvironmentRecord: {  
      Type: "Object",  
      // 标识符绑定在这里  
      c: undefined,  
    }  
    outer: <null>  
  }  
}


var FunctionExectionContext= {
	ThisBinding: <Global Object>
	LexialEnvironment:{
		EnvironmentRecord:{
			Type: 'Declarative',
			arguments:{
				0:20,
				1:30,
				length: 2
			},
			 outer: <GlobalLexicalEnvironment> 
		}
	}
	VariableEnvironment:{
		EnvironmentRecord:{
			Type: 'Declarative',
			// 标识符绑定在这里  
			g: undefined
		},
		outer: <GlobalLexicalEnvironment> 
	}
}

变量提升的原因:
在创建阶段,变量会被设置成undefined(在var的情况下),或保持未初始化uninitialized(let||const情况),这就是为什么在声明前可以访问var定义的变量,在声明前访问let||const的变量会报引用错误的原因。

图解变量提升

var myname = "极客时间"
function showName(){
  console.log(myname);
  if(0){
   var myname = "极客邦"
  }
  console.log(myname);
}
showName()

在这里插入图片描述
在创建变量环境阶段就会将myname保存进来并初始化为undefined,所以当在执行阶段访问myname的值是undefiend。

执行阶段

执行代码为变量赋值,如果js引擎在词法环境中找到let变量的值,则值默认赋予undefined。

销毁阶段

执行上下文出栈,等待虚拟机回收执行上下文。

实例讲解:
将词法环境中 outer 抽离出来,执行上下文结构如下:
在这里插入图片描述下面我们以如下示例来分析执行上下文的创建及执行过程:

function foo(){
  var a = 1
  let b = 2
  {
    let b = 3
    var c = 4
    let d = 5
    console.log(a)
    console.log(b)
  }
  console.log(b) 
  console.log(c)
  console.log(d)
}   

第一步: 调用 foo 函数时创建执行上下文,在创建阶段 var 声明的变量存放到变量环境中,let 声明的变量存放到词法环境中,需要注意的是在块作用域中 let 声明的变量不会被存放到词法环境中,如下图所示 👇:
在这里插入图片描述

第二步: 继续执行代码,当执行代码时,a设置为1,b设置为2,此时函数的执行上下文如图所示:
在这里插入图片描述
当进入函数的作用域块执行时,通过 let 声明的变量,会创建另外一个区域存储,这个区域中的变量并不影响作用域块外面的变量,因此示例中在块作用域的变量 b 与函数作用域的 b 都是互不影响的。

在词法环境内部,实际上维护了一个小型栈结构,栈底是函数最外层的变量,进入一个作用域块后,就会把该作用域内部的变量压到栈顶;当该块级作用域执行完成之后,该作用域的信息就会从栈顶弹出,这就是词法环境的结构。

第三步: 当代码执行到作用域块中的 console.log(a) 时,就需要在词法环境和变量环境中查找变量 a 的值了,具体查找方式是:沿着词法环境的栈顶向下查询,如果在词法环境中的某个块中查找到了,就直接返回给 JavaScript 引擎,如果没有查找到,那么继续在变量环境中查找。
在这里插入图片描述

第四步: 当函数体内块作用域执行结束之后,其内部变量就会从词法环境的栈顶弹出,此时执行上下文如下图所示:
在这里插入图片描述
第五步: 当foo函数执行完毕后执行栈将foo函数的执行上下文弹出。
所以,块级作用域就是通过词法环境的栈结构来实现的,而变量提升是通过变量环境来实现,通过这两者的结合,JavaScript 引擎也就同时支持了变量提升和块级作用域了。

outer引用

outer是一个外部引用,用来指向外部的执行上下文,其实由词法环境作用域指定的

function bar(){
  console.log(myName);
}

function foo(){
  var myName = "极客邦"bar();
}

var myName = "极客时间";

foo();

当一段代码使用了一个变量时,js引擎首先会在当前的执行上下文中查找变量,比如上面那段代码在myName变量时,如果在当前的变量环境中没找到,那么js引擎会继续在outer所指向的执行上下文中查找。为了直观理解,你可以看下面这张图:
在这里插入图片描述

从图中可以看出,bar函数和foo函数的outer都指向全局上下文,这就意味着如果在bar函数或foo函数中使用了外部变量,那么js引擎会去全局上下文中查找。我们把这个查找的链条就称为作用域链。现在你知道变量是通过作用域链来查找的了,不过还有一个疑问没有解开,foo函数调用bar函数,哪为什么bar函数的外部引用是全局上下文,而不是foo函数的执行上下文?

这是因为在js执行过程中,其作用域链是由词法作用域决定的。词法作用域指作用域是由代码中函数声明的位置来决定的,因此是静态作用域

结合变量环境、词法环境以及作用域链,我们来看下面代码:

function bar(){
  var myName = "极客世界";
  let test1 = 100;
  if(1){
    let myName = "Chorme 浏览器";
    console.log(test);
  }
}

function bar(){
  var myName = "极客邦";
  let test2 = 2;
  {
    let test3 = 3;
    bar();
  }
}

var myName = "极客时间";
let myAge = 10;
let test1 = 1;
foo();

在这里插入图片描述

解释下这个过程,首先是在bar函数的执行上下文中查找,但因为bar函数的执行上下文中没有定义test变量,所以根据词法作用域的规则,下一步在bar函数的外部作用域中查找。也就是全局作用域。

转载于: https://blog.csdn.net/Java0258/article/details/115352852?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522165252280316782248510782%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=165252280316782248510782&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2blogfirst_rank_ecpm_v1~rank_v31_ecpm-1-115352852-null-null.nonecase&utm_term=%E6%89%A7%E8%A1%8C%E4%B8%8A%E4%B8%8B%E6%96%87&spm=1018.2226.3001.4450

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值