三、变量对象

简介: 在上一篇文章中已经知道,当调用一个函数时(激活),一个新的执行上下文就会被创建。一个执行上下文的生命周期可以分为两个阶段。•创建阶段在这个阶段中,执行上下文会分别创建变量对象,建立作用域链,以及确定this指向。•代码执行阶段创建完成之后,就会开始执行代码,会完成变量赋值,函数引用,以及执行其他代码。

微信图片_20220509210555.png


在JavaScript中,肯定不可避免的需要声明变量和函数,JS编译器是如何找到这些变量的呢?


我们还得对执行上下文有一个进一步的了解。


在上一篇文章中已经知道,当调用一个函数时(激活),一个新的执行上下文就会被创建。一个执行上下文的生命周期可以分为两个阶段。


创建阶段


在这个阶段中,执行上下文会分别创建变量对象,建立作用域链,以及确定this指向。


代码执行阶段


创建完成之后,就会开始执行代码,会完成变量赋值,函数引用,以及执行其他代码。


微信图片_20220509210601.png


从这里可以看出详细了解执行上下文极为重要,因为其中涉及到了变量对象,作用域链,this等很多人没有怎么弄明白,但是却极为重要的概念,它关系到我们能不能真正理解JavaScript。在后面的文章中我们会一一详细总结,本文的核心是变量对象。


变量对象(Variable Object)


变量对象的创建,依次经历了以下几个过程。


// 这里a为属性名,20是属性值
{
a: 20
}


一、建立arguments对象:检查当前上下文中的参数,建立该对象下的属性与 属性值。


函数参数


二、检查当前上下文的函数声明,也就是使用function关键字声明的函数。在变量对象中以函数名建立一个属性,属性值为指向该函数所在内存地址的引用


三、检查当前上下文中的变量声明,每找到一个变量声明,就在变量对象中以变量名建立一个属性,属性值为undefined


如果变量与函数同名,则在这个阶段,以函数值为准

console.log(foo); // function foo
function foo() { console.log('function foo') }
var foo = 20;


// 上栗的执行顺序为
// 首先将所有函数声明放入变量对象中
function foo() { console.log('function foo') }
// 其次将所有变量声明放入变量对象中,但是因为foo已经存在同名函数,此时以函数值为准,而不会被undefined覆盖
// var foo = undefined;
// 然后开始执行阶段代码的执行
console.log(foo); // function foo
foo = 20;


微信图片_20220509210604.png


根据这个规则,理解变量提升就变得十分简单了。在很多文章中虽然提到了变量提升,但是具体是怎么回事还真的很多人都说不出来,以后在面试中用变量对象的创建过程跟面试官解释变量提升,简直逼格满满。


在上面的规则中我们看出,function声明会比var声明优先级更高一点。为了帮助大家更好的理解变量对象,我们结合一些简单的例子来进行探讨。


// demo01
function test() {
    console.log(a);
    console.log(foo());
    var a = 1;
    function foo() {
        return 2;
    }
}
test();



在上例中,我们直接从test()的执行上下文开始理解。全局作用域中运行test()时,

test()的执行上下文开始创建。为了便于理解,我们用如下的形式来表示


// 创建过程
testEC = {
    // 变量对象
    VO: {},
    scopeChain: {}
}
// 因为本文暂时不详细解释作用域链,所以把变量对象专门提出来说明
// VO 为 Variable Object的缩写,即变量对象
VO = {
    arguments: {...},  //注:在浏览器的展示中,函数的参数可能并不是放在arguments对象中,这里为了方便理解,我做了这样的处理
    foo: <foo reference>  // 表示foo的地址引用
    a: undefined
}


未进入执行阶段之前,变量对象中的属性都不能访问!但是进入执行阶段之后,变量对象转变为了活动对象,里面的属性都能被访问了,然后开始进行执行阶段的操作。


这样,如果面试的时候被问到变量对象和活动对象有什么区别,就可以自如的应答了,他们其实都是同一个对象,只是处于执行上下文的不同生命周期。不过只有处于函数调用栈栈顶的执行上下文中的变量对象,才会变成活动对象。


// 执行阶段
VO ->  AO   // Active Object
AO = {
    arguments: {...},
    foo: <foo reference>,
    a: 1,
    this: Window
}

因此,上面的例子demo1,执行顺序就变成了这样


function test() {
    function foo() {
        return 2;
    }
    var a;
    console.log(a);
    console.log(foo());
    a = 1;
}
test();


再来一个例子,巩固一下我们的理解。

// demo2
function test() {
    console.log(foo);
    console.log(bar);
    var foo = 'Hello';
    console.log(foo);
    var bar = function () {
        return 'world';
    }
    function foo() {
        return 'hello';
    }
}
test();
// 创建阶段
VO = {
    arguments: {...},
    foo: <foo reference>,
    bar: undefined
}
// 这里有一个需要注意的地方,var声明的变量与函数同名,以函数为准
// 执行阶段
VO -> AO
VO = {
    arguments: {...},
    foo: 'Hello',
    bar: <bar reference>,
    this: Window
}

需要结合上面的知识,仔细对比这个例子中变量对象从创建阶段到执行阶段的变化,如果你已经理解了,说明变量对象相关的东西都已经难不倒你了。


全局上下文的变量对象


以浏览器中为例,全局对象为window。全局上下文有一个特殊的地方,它的变量对象,就是window对象。而这个特殊,在this指向上也同样适用,this也是指向window。


// 以浏览器中为例,全局对象为window
// 全局上下文
windowEC = {
    VO: Window,
    scopeChain: {},
    this: Window
}


除此之外,全局上下文的生命周期,与程序的生命周期一致,只要程序运行不结束,比如关掉浏览器窗口,全局上下文就会一直存在。其他所有的上下文环境,都能直接访问全局上下文的属性。


let/const


ES6中,新增了使用let/const来声明变量。我想他们的使用肯定难不倒大家。可是有一个问题不知道大家思考过没有,let/const声明的变量,是否还会变量提升?


是的,这个刁钻的问题也成为了各大面试官爱问的细节。很贱!可也没办法,还是要弄明白怎么回事!


我们来做个试验,验证一下这个问题:


第一步,我们直接使用一个未定义的变量


console.log(a);


报错信息如下:


微信图片_20220509210608.png


第二步,我们在let之前调用变量


console.log(a);
let a = 10;


会发生什么?会打印出undefined吗?


看看结果


微信图片_20220509210612.png


不能在初始化之前访问a


这个报错说明了什么问题呢?变量定义了,但是没有初始化。


所以在这里我们就可以得出结论:let/const声明的变量,仍然会提前被收集到变量对象中,但和var不同的是,let/const定义的变量,不会在这个时候给他赋值undefined。


因为完全没有赋值,即使变量提升了,我们也不能在赋值之前调用他。这就是我们常说的暂时性死区


最后,变量提升的现象确实会对我们的代码造成一些负面影响,因此,开发中的好习惯,就是尽量将变量声明放在最前面来写。

相关文章
|
8月前
|
JavaScript 前端开发
变量和对象的解构赋值
变量和对象的解构赋值
48 0
|
8月前
|
存储 Java 编译器
【Java变量】 局部变量、成员变量(类变量,实例变量)、方法参数传递机制
【Java变量】 局部变量、成员变量(类变量,实例变量)、方法参数传递机制
110 0
|
28天前
|
存储 Python
变量赋值
变量赋值
30 7
|
6月前
|
开发者
局部变量,在使用时再定义
关于局部变量,适时定义可以提高代码可读性并规避不必要的bug。示例代码中,为了避免误解`checkTaskApplyDTO`仅设置了`userId`,在`existAppliedTask`方法内部,可以通过将`checkTaskApplyDTO`的定义与设置属性的操作靠近,以明确其所有属性值的来源。 另外,本文还展示了一个因提前定义变量`ret`而导致的bug实例。如果将此变量的定义延迟至其实际使用前,则可以避免此类问题。适时定义变量有助于减少混淆,提高代码质量。
49 4
|
8月前
|
Shell
变量的定义和引用
变量的定义和引用。
93 0
对象定义-解构-枚举属性遍历以及对象内函数
对象定义-解构-枚举属性遍历以及对象内函数
80 0
|
人工智能 Shell
将结果分别赋值给变量
将结果分别赋值给变量
70 0
定义了一个类A,S是类外的一个函数,通过A.S=S进行赋值
假设类 A 已经定义好了,现在可以通过 A.S = S 的方式将函数 S 赋值给类 A。这样做的效果是,将 S 函数作为类 A 的一个属性,并且可以通过该属性来调用函数 S。 下面是一个简单的例子:
|
存储 Unix PHP
变量的引用赋值与传值赋值
一、使用 memory_get_usage() 查看PHP内存使用量 1. 传值赋值
 变量的引用赋值与传值赋值
使用final关键字修饰一个变量时,是引用不能变,还是引用的对象不能变
使用final关键字修饰一个变量时,是指引用变量不能变,但是引用变量所指向的对象中的内容还是可以改变的。
424 0
使用final关键字修饰一个变量时,是引用不能变,还是引用的对象不能变