新手秒懂 - 高逼格解释变量提升

2,213 阅读6分钟

作者废话

最近为了未来去大城市面试特意重新稳固基础知识

面试多了也会发现,很多都会涉及到平时工作中不去关心的问题, 接来下会不定时地像同样的朋友们分享平常工作中不会接触却又常被问到的面试知识点

变量提升?

这个问题试问刚毕业的前端小白都能侃侃而谈,我们以两道很常见的又很基础的面试题一步一步揭开它的面纱。(作为初学者的我经常搞混)

    function fun () {}
    var fun = 'fuck bitch'
    console.log(fun) //???
    console.log(fun) //???
    function fun () {}
    var fun = 'fuck bitch'

大佬都会说,太简单了。

  1. 首先,第一题输出的是funck bitch,这不涉及变量提升,只是同名的变量产生了覆盖。(注意: 这发生在执行阶段)
  2. 然后,第二道题, 输出ƒ () {} , 具体的原因,会谈到大家都知道的变量提升知识

我们习惯将 var a = 2; 看作一个声明,而实际上 JavaScript 引擎并不这么认为。它将 var a 和 a = 2 当作两个单独的声明,第一个是编译阶段的任务,而第二个则是执行阶段的任务。

这意味着无论作用域中的声明出现在什么地方,都将在代码本身被执行前 首先 进行处理。 可以将这个过程形象地想象成所有的声明(变量和函数)都会被“移动”到各自作用域的最顶端,这个过程被称为提升。

声明本身会被提升,而包括函数表达式的赋值在内的赋值操作并不会提升。

参考地址

如此可以解释 fun 没有报错的原因,但很多跟我一样的初学者就会问为何输出 ƒ () {} 而不是 undefined 或者 fuck bitch

对于这样的问题,按照我之前的理解。在我眼里,执行代码其实就是

function fun () {}
console.log(fun) // --> fun(){}
fun = 'fuck bitch'

但如果问我为啥会是这样的,我就说不出来个所以然了 (果然还是太菜了。。)

为何会产生变量提升??

不知有没有同学想过这样的问题,本来一般只是为了应付面试而去瞄一眼说出个所以然就可以的。但作为要成为未来资深的秃头披风大佬,这样做是远远不够的。

这就涉及到javascript语言中执行上下文之变量对象的知识了

image

当 JavaScript 代码执行一段可执行代码(executable code)时,会创建对应的执行上下文(execution context)。

image

一个执行上下文的生命周期可以分为两个阶段。

  1. 创建阶段

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

  1. 代码执行阶段

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

变量对象(Variable Object)

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

  1. 建立arguments对象。检查当前上下文中的参数,建立该对象下的属性与属性值。
  2. 检查当前上下文的函数声明,也就是使用function关键字声明的函数。在变量对象中以函数名建立一个属性,属性值为指向该函数所在内存地址的引用。如果函数名的属性已经存在,那么该属性将会被新的引用所覆盖。(常说的函数优先被提升, 且同名会产生覆盖)
  3. 检查当前上下文中的变量声明(未声明的会报错 not defined),每找到一个变量声明,就在变量对象中以变量名建立一个属性,属性值为undefined。如果该变量名的属性已经存在,为了防止同名的函数被修改为undefined,则会 直接跳过 (注意: 跳过是在创建阶段,跟第一题相对应,不要搞混),原属性值 不会被修改

对照两道题,结合描述,我们就可以了解到具体执行原理:

    function fun () {}
    var fun = 'fuck bitch'
    console.log(fun) // 'fuck bitch'

因为 fun = 'fuck bitch' 是在执行上下文的执行过程中运行的,而不会产生覆盖,跳过操作是在创建阶段,因此输出结果自然会是fuck bitch

而第二道题

    console.log(fun) // fun () {}
    function fun () {}
    var fun = 'fuck bitch'

因为 function 是优先被提升, 而接下来的变量 fun 因为同名而不会在创建阶段产生覆盖,所以输出 fun () {}。 具体如下图相同。

// 上例的执行顺序为

// 首先将所有函数声明放入变量对象中
function fun () {}

// 其次将所有变量声明放入变量对象中,但是因为fun已经存在同名函数,因此此时会跳过undefined的赋值
// var fun = undefined;

// 然后开始执行阶段代码的执行
console.log(fun); // function fun
fun = 'fuck bitch';

这样一解释,是不是觉得自己逼格瞬间上升了一个档次 ?!

深入拓展

还不用太兴奋,我们为了加深理解,换个例子继续一步一步来详细地讲。

function test() {
    console.log(a);
    console.log(foo());

    var a = 1;
    function foo() {
        return 2;
    }
}

test();

VO AND AO

在函数上下文, 我们使用活动对象(activation object, AO)来表示变量对象。

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

它们其实都是同一个对象,只是处于执行上下文的不同生命周期。

按照上例:
// 创建过程
testEC = {
    // 变量对象
    VO: {},
    scopeChain: {}
}

AO = {
    arguments: {...},  //注:在浏览器的展示中,函数的参数可能并不是放在arguments对象中,这里为了方便理解,我做了这样的处理
    foo: <foo reference>  // 表示foo的地址引用
    a: undefined
}
// 执行阶段
VO ->  AO   // Activation Object
AO = {
    arguments: {...},
    foo: <foo reference>,
    a: 1,
    this: Window
}

按此来说, 上例的执行顺序应该是:

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

接下来增加难度:

// demo2
function test() {
    console.log(1, foo);
    console.log(2, bar);

    var foo = 'Hello';
    console.log(3, foo);
    var bar = function () {
        return 'world';
    }

    function foo() {
        return 'hello';
    }
}

test(); // ??? 

可转换为

// demo2
function test() {
    function foo() {
        return 'hello';
    }
    // 其次将所有变量声明放入变量对象中,但是因为foo已经存在同名函数,因此此时会跳过undefined的赋值
    // var foo = undefined;
    
    var bar = undefined;
    
    console.log(1, foo);
    console.log(2, bar);

    var foo = 'Hello';
    
    console.log(3, foo);
    
    bar = function () {
        return 'world';
    }
}

test(); 
// 1 ƒ foo() {
// return 'hello';
// }
// 2 undefined
// 3 "Hello"

到此,便是我想向大家分享的内容。努力,奋斗。💪💪

参考文献

变量对象详解

深入之变量对象