JavaScript的执行环境和this详解

执行环境(也称执行上下文)是JavaScript中最为重要的一个概念,它描述的是执行中的代码所处的环境;在JavaScript中,执行环境分为全局执行环境函数执行环境

全局执行环境是最基本且唯一的执行环境,在执行环境栈中,全局环境处于栈底的位置,直到退出应用程序,全局环境才会从栈中出栈,即全局环境被销毁;
函数执行环境,是指函数被调用时就会创建并且压入执行环境栈顶的环境;
对于一个应用来说,刚开始的执行环境栈中都只有一个全局执行环境,编译遇到函数调用时,就会创建该函数的函数执行环境并压入栈顶,如果该函数中又调用了另一个函数B,就会创建B的函数执行环境并压入栈顶,控制权交给B的函数执行环境。

代码分为编译和执行两个部分,函数调用(未执行函数体)发生在编译阶段,下文中提到的创建执行环境都是在函数调用时发生的,也就是说,代码在编译时就已经确定了执行环境的变量对象、作用域链和this等,这就是为什么在JavaScript中会有变量提升。

一、执行环境始末

一个执行环境的生命周期包含创建和执行阶段;在创建阶段,执行环境会经历以下过程:创建与该执行环境关联的变量对象、生成作用域链、确定this的指向;

1. 变量对象
创建变量对象时,编译器会检索当前执行环境中的变量声明、函数声明和参数。

  • 检索到变量声明时,则把变量名作为属性名,undefined作为属性值的键值对写入变量对象;
  • 检索到函数声明时,则把函数名作为属性名,函数所在地址的引用作为属性值的键值对写入变量对象;
  • 检索到参数时,生成一个Arguments对象,并把参数名作为属性名,参数值作为属性值的键值对写入变量对象;但全局执行环境没有参数传递,因此也不会有检索参数这一步;

因此在执行环境的创建阶段,与执行环境关联的变量对象可表示为:

variableObject = { // 变量对象
	variableName: undefined,
	functionName: <function reference>,
	Arguments: {},
	params: '参数的具体值'
}

2. 作用域链
JavaScript中的作用域分为全局作用域和函数作用域(暂不考虑块级作用域),与函数执行环境不同,函数在定义时其作用域就已经确定。函数被定义时,函数的[[scope]]属性会保存其上层执行环境的变量对象作为自身的作用域;函数被调用时,创建函数执行环境,并创建关联的变量对象,把该变量对象添加到作用域链的最前端,即生成了作用域链。

  • 作用域链必须在变量对象创建后才会生成,且作用域链实际上是由一个个的变量对象链接而成;
  • 作用域链的前端,始终都是当前执行环境的变量对象,全局执行环境的变量对象始终都是作用域链中的最后一个对象;
  • 标识符的寻找过程始终从作用域链的最前端开始,然后逐级向后回溯,直至找到标识符为止;

3. 函数中的this
函数中this指向当前执行环境,即指向this所在函数的函数执行环境;需要注意的是,函数在内存中是独立存储的,所以函数执行环境是在函数被调用时确定,那么this的指向也是在函数调用时确定。
判断this的指向,首先要判断的是全局执行环境还是函数执行环境,关键是看函数的调用方式,例如以下代码:

let name = '张三';
let obj = {
	name: '李四',
	sayName: function() {
		console.log('name', this.name);
	}
}
let f = obj.sayName;
obj.sayName(); // 打印李四
f(); // 打印张三
  1. 通过obj.sayName()调用方法时,当前的执行环境就是函数执行环境,但函数执行环境应该理解为“函数执行时所在的环境”,而非“只有函数范围大小的环境”;这时的函数执行时所在环境指的就是obj这个对象,obj对象作为了函数执行环境,而不是只有sayName函数内部为函数执行环境。obj对象作为函数执行环境,内部除了包含变量对象(活动对象)和作用域链属性,以及指向自身的this对象,明显还有一个name属性,因此最终输出obj.name;
  2. 通过f()调用方法时,f作为全局对象的方法被调用,因此当前执行环境就是全局执行环境,最终输出window.name;
  3. 重点是看this所在函数的调用方式,才能确定当前是什么类型的执行环境,从而确定“环境范围”。

4.执行环境的执行阶段
变量对象中未被赋值的属性会在这个阶段中被赋值,被赋值完毕的变量对象被称为活动对象;

二、变量提升和作用域

执行流遇到某个变量时,判断该变量能否使用就看这个变量是否在当前执行环境的作用域链之中。
1.为什么会存在变量提升
了解执行环境中的变量对象,就很容易理解JavaScript中为什么会存在变量提升的现象了。以函数内部的变量为例,正如前面所述,函数被调用但未执行函数内部语句(也就是代码编译阶段)之时,就会创建函数执行环境并压入栈顶,函数执行环境又创建了变量对象,这个变量对象会把所有声明的变量标识符记录起来,即使这些变量现在还没有值。所以,无论变量是在代码块的哪个地方声明的,都会先被保存在变量对象,等到执行函数内部的语句(也就是代码执行阶段)时,变量对象中未被赋值的属性会被赋值,成为活动对象,执行语句时若需要用到变量,则从变量对象中取出即可。
2.为什么没有块级作用域
在let和const关键字出现之前,JavaScript只有全局作用域和函数作用域,而没有块级作用域同样是因为执行环境的变量对象。例如下面的代码所示:

for (var i = 0; i < 10; i++) {
	console.log("i", i);
}
console.log("i=", i); // 打印i=10

执行完for循环以后,变量i依然能在for语句的外部访问,这是因为执行流创建全局执行环境的变量对象时,也会把变量i记录到变量对象中,所以只要全局执行环境未被销毁,变量i都能访问到。再看下面的代码所示:

function ex() {
	var i = 123;
	console.log("i=", i);
}
ex();
console.log("i=", i); // 打印undefined

函数执行环境的变量对象会记录变量i,这个变量对象被插入作用域链的最前端,所以通过作用域链可以访问变量i。但函数执行完毕,即销毁函数执行环境之后,返回上一层的执行环境,这个例子中上一层是全局执行环境, 而在全局执行环境的作用域链中是不包含函数执行环境的变量对象的,所以无法访问变量i。
3.如何实现块级作用域
新增的let和const关键字能够声明一个块级作用域,实现原理简单来说,就是使用这两个关键字声明的变量不再被记录到执行环境的变量对象中,而是保存在执行环境中新开辟的一个被称为词法环境的空间,当let变量和const变量被使用完毕后,就会从这个词法环境中移除,所以ES6可以做到全局作用域、函数作用域和块级作用域都支持。
本文是参考了一些文章后根据自己的理解所写,若有错误之处欢迎指出!
参考文章:
JS-执行上下文&this指向
js中的执行上下文、作用域、闭包和this
JavaScript进阶之this指向

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值