JavaScript声明提升的背后:js执行环境

​(function() {
    console.log(typeof foo); // 这里会打印出什么?
    console.log(typeof bar); // 这里会打印出什么?
    var foo = 'hello',
        bar = function() {
            return 'vian';
        };
    function foo() {
        return 'hello';
    }
})();​

我们在接触JavaScript这门语言时,会经常遇到这种问题,经过后续的学习,我们可能知道了这种现象在JavaScript中叫声明提升(hoisting),但是我们可能只知道声明提升的现象,却不清楚造成这种现象的本质,而这个本质却是JavaScript最为重要的知识之一。解答上述代码中的问题之前,我们先看一下JavaScript中的执行环境。

执行环境(执行上下文)

执行环境(execution context)定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行环境都有一个与之关联的变量对象(variable object),环境中定义的所有变量和函数都保存在这个对象中。虽然我们编写的代码无法访问这个对象,但解析器在处理数据时会在后台使用它。

在JavaScript中可执行代码可以分为三类:

  • 全局代码 - JavaScript 代码开始运行的默认环境,即全局的、不在任何函数里面的代码
  • 函数代码 - 函数体内的代码
  • eval代码 - eval(...)函数中动态执行的代码(我们这里不讨论)

全局执行环境是最外围的一个执行环境。在Web浏览器中,全局执行环境是window对象。当代码载入浏览器时,全局执行环境被创建,直到网页或浏览器被关闭,全局执行环境才被销毁。
在一个Javascript程序中,必须且只能存在一个全局执行环境,和任意个的非全局执行环境,程序每调用一个函数,都会创建新的执行环境,如下图所示。

图一.png

该JavaScript程序中含有一个全局执行环境、三个函数执行环境。为以正确的顺序执行代码,JavaScript中用堆栈的形式来处理执行环境——执行环境栈(Execution Context Stack)。

执行环境栈

浏览器中JavaScript是单线程的,这意味着运行在浏览器中的JavaScript代码,同一时间只有一件事件、动作发生。除了当前正在运行的代码,其他代码都在一个队列中排队,这个队列就是执行环境栈。
为了更好的说明执行环境栈,我们结合一个例子来说明:

(function test(i) {
    if (i === 1) {
        return;
    }
    test(++i);
})(0);

下面我们用图来表示上述代码执行时,执行环境栈中的变化过程:


ecs1.png
  1. 当JavaScript代码执行时,第一个创建的总是全局执行环境,因此全局执行环境也总是在执行环境栈的最底部。


    ecs2.png
  2. 代码执行时,调用了函数test(0),此时创建新的执行环境test EC,并压入执行栈。


    ecs3.png
  3. 在执行第一次执行函数test(i)时(i=0),执行到函数体中的代码test(++i),程序再一次调用函数test(i),创建新的执行环境test EC1,并压入执行栈。


    ecs4.png
  4. 函数test(i = 1)中的代码执行完毕,该执行环境出栈销毁,程序回到上一层执行环境中继续执行。


    esc5.png

5.函数test(i = 0)中的代码执行完毕,该执行环境出栈销毁,程序回到全局执行环境中继续执行,全局环境的代码即使执行完毕,也不会销毁,直至网页或浏览器被关闭了才出栈销毁。

上面就是程序执行过程中,执行环境栈的变成过程。关于执行环境栈,有以下几点要特别注意:

  • 单线程
  • 同步执行
  • 只有一个全局执行环境
  • 可以有无数个的非全局执行环境
  • 每一次函数调用都会创建一个新的执行环境,即使是调用自身

详解执行环境

从上文我们已经知道了,每当一个函数被调用时,一个新的执行环境就会被创建。实际上,执行环境可以分为两个阶段,创建阶段和执行阶段。JavaScript声明提升的秘密也在其中,我们继续往下看。

  • 创建阶段 (函数被调用,同时在执行函数内的代码前)
    在这个阶段会发生以下的事:
    创建变量对象(VO,Variable Object)
    建立作用域链(Scope Chain)
    确定this的指向
  • 执行阶段
    在这个阶段进行赋值、函数引用、执行代码。

我们完全可以把执行环境当作一个含有三个属性的对象,如下:

executionContextObj = {
    'variableObject': {...}, //函数的arguments、参数、函数内的变量及函数声明
    'scopeChian': {...}, //本层变量对象及所有上层执行环境的变量对象
    'this': {}
}

声明提升的秘密就发生在变量对象VO中。

变量对象/活动对象(VO/AO)

每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。

说到执行环境的创建过程就会涉及到变量对象和活动对象,很多人对这两个概念会模糊不清。其实,变量对象VO和活动对象AO是同一个对象在不同阶段的表现形式。当进入执行环境的创捷阶段时,变量对象被创建,这时变量对象的属性无法被访问。进入执行阶段后,变量对象被激活变成活动对象,此时活动对象的属性可以被访问。
下面来看一下,执行环境创建阶段中变量对象创建中,JavaScript解析器做的事情:

  • 根据函数参数,创建并初始化arguments对象,及形参属性
  • 检查上下文中的函数声明,将函数名作为变量对象的属性,函数引用作为值。如果该函数名在变量对象中已存在,则覆盖已存在的函数引用。
  • 检查上下文的变量声明,将变量名作为变量对象的属性,值设置为undefined。如果该变量名在变量对象中已存在,为防止与函数名冲突,则跳过,不进行任何操作。

JavaScript中声明提升的背后原因已经很清晰了,你发现了吗?请先思考一下,我们下文将结合例子进行讲解。先让我们结合一段代码,结合上文的知识,回顾一下代码执行时,会发生什么事情。

function greet(name) {
    var say = 'hello';
    function action() {
        console.log(say + name);
    }
    action();
}
greet('vian');
  1. 进入全局执行环境,执行代码。(JavaScript代码执行时,第一个进入的总是全局执行环境)
  2. 调用函数greet(...),执行函数内的任何代码前,创建执行环境。
  3. 进入创建阶段:
    • 创建变量对象:
      • 检查函数参数创建arguments对象,及设置函数形参。此时:
      executionContextObj = {
             arguments: {0: 'vian', length: 1},
             name: 'vian'
      }
      
      • 扫描函数声明,设置函数名为变量对象的属性,函数引用为属性值,遇到同名属性则覆盖函数引用值。此时:
      executionContextObj = {
             arguments: {0: 'vian', length: 1},
             name: 'vian',
             action: <action>
      }
      
      • 扫描变量声明,设置变量名为变量对象的属性,undefined为属性值,为遇到同名属性则跳过。此时:
      executionContextObj = {
             arguments: {0: 'vian', length: 1},
             name: 'vian',
             action: <action>,
             say: undefined
      }
      
    • 初始化作用域链
    • 确定this指向
  4. 进入执行阶段,执行代码。变量对象变成活动对象,遇到查找变量和函数引用的时候,先去活动对象中找,找不到的情况下沿作用域链往上找。直至找到为止,否则为undefined。
  5. 函数内的代码执行完毕,该函数执行环境出栈销毁,程序执行流回到全局执行环境..

再看声明提升

通过对JavaScript中执行环境的了解,令人奇怪的声明提升机制也变得清晰明了。回到本文的开头,造成这种声明提升现象的本质,究竟是什么呢?——执行环境的创建阶段,变量对象创建的方式所造成。下面我们来解释一下本文开头的代码。

​(function() {
    console.log(typeof foo); // 这里会打印出什么?
    console.log(typeof bar); // 这里会打印出什么?
    var foo = 'hello',
        bar = function() {
            return 'vian';
        };
    function foo() {
        return 'hello';
    }
})();​

根据上文中分析变量对象创建过程的方法:

executionContextObj = {}
1.初始化arguments对象,及形参
executionContextObj = {
    arguments: {length: 0}
}
2.扫描函数声明,并进行处理:
遇到函数声明 function foo(){}
executionContextObj中没有foo属性,将foo设为executionContextObj的属性,函数引用作为值。
executionContextObj = {
    arguments: {length: 0},
    foo: <function>
}
3.扫描变量声明,并进行处理:
遇到变量声明var foo,executionContextObj已存在foo属性,跳过。
遇到变量声明var bar,executionContextObj不存在bar属性,将其设置为变量对象属性,值为undefined。
executionContextObj = {
    arguments: {length: 0},
    foo: <function>,
    bar: undefined
}

结论:

    console.log(typeof foo); // 'function'
    console.log(typeof bar); // 'undefined'

到这里,我们就再也不怕声明提升的坑和问题啦。需要注意的是,es6中新增的let和const变量声明都不会进行变量提升,重复赋值及声明前引用变量都会报错。

希望本文对大家有所帮助,互相学习,一起提高。转载请注明原帖。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,837评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,551评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,417评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,448评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,524评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,554评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,569评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,316评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,766评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,077评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,240评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,912评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,560评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,176评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,425评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,114评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,114评论 2 352

推荐阅读更多精彩内容