前端基础进阶(三):变量对象详解
变量对象,新版本中,准确的说法应该是环境记录对象。而环境记录对象,又区分词法环境对象与变量环境对象,词法环境对象用于解析当前上下文中,由 const 声明的标识符引用,变量环境对象,用于解析由 var 声明的标识符引用。执行上下文内部的实现逻辑过于复杂,并不利于理解,因此此处为了理解方便,仍然统一采用变量对象的说法.
在JavaScript中,肯定不可避免的需要声明变量和函数,JS编译器是如何找到这些变量的呢?
我们还得对执行上下文有一个进一步的了解。
在上一篇文章中已经知道,当调用一个函数时(激活),一个新的执行上下文就会被创建。一个执行上下文的生命周期可以分为以下几个阶段:
创建阶段
在这个阶段中,执行上下文会分别创建变量对象,确定this指向,以及其他需要的状态。代码执行阶段
创建完成之后,就会开始执行代码,会完成变量赋值,以及执行其他代码。销毁阶段
可执行代码执行完毕之后,执行上下文出栈,对应的内存空间失去引用,等待被回收
变量对象(Variable Object)
变量对象的创建,依次经历了以下几个过程。
// 这里a为属性名,20是属性值 |
一、建立arguments对象:检查当前上下文中的参数,建立该对象下的属性与属性值。
二、检查当前上下文的函数声明,也就是使用function关键字声明的函数。在变量对象中以函数名建立一个属性,属性值为指向该函数所在内存地址的引用。
三、检查当前上下文中的变量声明,每找到一个变量声明,就在变量对象中以变量名建立一个属性,属性值为undefined,const/let 声明的变量没有赋值,不能提前使用。
如果 var 变量与函数同名,则在这个阶段,以函数值为准,在下一个阶段,函数值会被变量值覆盖。
console.log(foo); // function foo |
// 上例的执行顺序为 |
根据这个规则,理解变量提升就变得十分简单了。在很多文章中虽然提到了变量提升,但是具体是怎么回事还真的很多人都说不出来,以后在面试中用变量对象的创建过程跟面试官解释变量提升,简直逼格满满。
在上面的规则中我们看出,function声明会比var声明优先级更高一点。为了帮助大家更好的理解变量对象,我们结合一些简单的例子来进行探讨。
// demo01 |
在上例中,我们直接从test()
的执行上下文开始理解。全局作用域中运行test()
时,test()
的执行上下文开始创建。为了便于理解,我们用如下的形式来表示:
// 创建过程 |
未进入执行阶段之前,变量对象中的属性都不能访问!但是进入执行阶段之后,变量对象转变为了活动对象,里面的属性都能被访问了,然后开始进行执行阶段的操作。
变量对象和活动对象其实都是同一个对象,只是处于执行上下文的不同生命周期。不过只有处于函数调用栈栈顶的执行上下文中的变量对象,才会变成活动对象。
// 执行阶段 |
因此,上面的例子demo1,执行顺序就变成了这样。
function test() { |
再来一个例子,巩固一下我们的理解。
// demo2 |
// 创建阶段 |
// 执行阶段 |
全局上下文的变量对象
以浏览器中为例,全局对象为window
。全局上下文有一个特殊的地方,它的变量对象,就是window对象
。而这个特殊,在this
指向上也同样适用,this也是指向window。
// 以浏览器中为例,全局对象为window |
除此之外,全局上下文的生命周期,与程序的生命周期一致,只要程序运行不结束,比如关掉浏览器窗口,全局上下文就会一直存在。其他所有的上下文环境,都能直接访问全局上下文的属性。
let/const
let/const声明的变量,是否还会变量提升?
我们来做个试验,验证一下这个问题:
第一步,我们直接使用一个未定义的变量
console.log(a); |
报错信息如下:
第二步,我们在let之前调用变量
console.log(a); |
不能在初始化之前访问a。
这个报错说明了什么问题呢?变量定义了,但是没有初始化。
所以在这里我们就可以得出结论:let/const声明的变量,仍然会提前被收集到变量对象中,但和var不同的是,let/const定义的变量,不会在这个时候给他赋值undefined。
因为完全没有赋值,即使变量提升了,也不能在赋值之前调用它。这就是我们常说的 暂时性死区。
最后,变量提升的现象确实会对我们的代码造成一些负面影响,因此,开发中的好习惯,尽量将变量声明放在最前面来写。