第四章:提升

特别说明,为便于查阅,文章转自https://github.com/getify/You-Dont-Know-JS

至此,你应当对作用域的想法,以及变量如何根据它们被声明的方式和位置附着在不同的作用域层级上感到相当适应了。函数作用域和块儿作用域的行为都是依赖于这个相同规则的:在一个作用域中声明的任何变量都附着在这个作用域上。

但是关于出现在一个作用域内各种位置的声明如何附着在作用域上,有一个微妙的细节,而这个细节正是我们要在这里检视的。

先有鸡还是先有蛋?

有一种倾向认为你在 JavaScript 程序中看到的所有代码,在程序执行的过程中都是从上到下一行一行地被解释执行的。虽然这大致上是对的,但是这种猜测中的一个部分可能会导致你错误地考虑你的程序。

考虑这段代码:

a = 2;

var a;

console.log( a );

你觉得在 console.log(..) 语句中会打印出什么?

许多开发者会期望 undefined,因为语句 var a 出现在 a = 2 之后,这很自然地看起来像是这个变量被重定义了,并因此被赋予了默认的 undefined。然而,输出将是 2

考虑另一个代码段:

console.log( a );

var a = 2;

你可能会被诱导而这样认为:因为上一个代码段展示了一种看起来不是从上到下的行为,也许在这个代码段中,也会打印 2。另一些人认为,因为变量 a 在它被声明之前就被使用了,所以这一定会导致一个 ReferenceError 被抛出。

不幸的是,两种猜测都不正确。输出是 undefined

那么。这里发生了什么? 看起来我们遇到了一个先有鸡还是先有蛋的问题。哪一个先有?声明(“蛋”),还是赋值(“鸡”)?

编译器再次袭来

要回答这个问题,我们需要回头引用第一章关于编译器的讨论。回忆一下,引擎 实际上将会在它解释执行你的 JavaScript 代码之前编译它。编译过程的一部分就是找到所有的声明,并将它们关联在合适的作用域上。第二章向我们展示了这是词法作用域的核心。

所以,考虑这件事情的最佳方式是,在你的代码的任何部分被执行之前,所有的声明,变量和函数,都会首先被处理。

当你看到 var a = 2; 时,你可能认为这是一个语句。但是 JavaScript 实际上认为这是两个语句:var a;a = 2;。第一个语句,声明,是在编译阶段被处理的。第二个语句,赋值,为了执行阶段而留在 原处

于是我们的第一个代码段应当被认为是这样被处理的:

var a;
a = 2;

console.log( a );

……这里的第一部分是编译,而第二部分是执行。

相似地,我们的第二个代码段实际上被处理为:

var a;
console.log( a );

a = 2;

所以,关于这种处理的一个有些隐喻的考虑方式是,变量和函数声明被从它们在代码流中出现的位置“移动”到代码的顶端。这就产生了“提升”这个名字。

换句话说,先有蛋(声明),后有鸡(赋值)

注意: 只有声明本身被提升了,而任何赋值或者其他的执行逻辑都被留在 原处。如果提升会重新安排我们代码的可执行逻辑,那就会是一场灾难了。

foo();

function foo() {
    console.log( a ); // undefined

    var a = 2;
}

函数 foo 的声明(在这个例子中它还 包含 一个隐含的、实际为函数的值)被提升了,因此第一行的调用是可以执行的。

还需要注意的是,提升是 以作用域为单位的。所以虽然我们的前一个代码段被简化为仅含有全局作用域,但是我们现在检视的函数foo(..)本身展示了,var a被提升至foo(..)的顶端(很明显,不是程序的顶端)。所以这个程序也许可以更准确地解释为:

function foo() {
    var a;

    console.log( a ); // undefined

    a = 2;
}

foo();

函数声明会被提升,就像我们看到的。但是函数表达式不会。

foo(); // 不是 ReferenceError, 而是 TypeError!

var foo = function bar() {
    // ...
};

变量标识符 foo 被提升并被附着在这个程序的外围作用域(全局),所以 foo() 不会作为一个 ReferenceError 而失败。但 foo 还没有值(如果它不是函数表达式,而是一个函数声明,那么它就会有值)。所以,foo() 就是试图调用一个 undefined 值,这是一个 TypeError —— 非法操作。

同时回想一下,即使它是一个命名的函数表达式,这个名称标识符在外围作用域中也是不可用的:

foo(); // TypeError
bar(); // ReferenceError

var foo = function bar() {
    // ...
};

这个代码段可以(使用提升)更准确地解释为:

var foo;

foo(); // TypeError
bar(); // ReferenceError

foo = function() {
    var bar = ...self...
    // ...
}

函数优先

函数声明和变量声明都会被提升。但一个微妙的细节(可以 在拥有多个“重复的”声明的代码中出现)是,函数会首先被提升,然后才是变量。

考虑这段代码:

foo(); // 1

var foo;

function foo() {
    console.log( 1 );
}

foo = function() {
    console.log( 2 );
};

1 被打印了,而不是 2!这个代码段被 引擎 解释执行为:

function foo() {
    console.log( 1 );
}

foo(); // 1

foo = function() {
    console.log( 2 );
};

注意那个 var foo 是一个重复(因此被无视)的声明,即便它出现在 function foo()... 声明之前,因为函数声明是在普通变量之前被提升的。

虽然多个/重复的 var 声明实质上是被忽略的,但是后续的函数声明确实会覆盖前一个。

foo(); // 3

function foo() {
    console.log( 1 );
}

var foo = function() {
    console.log( 2 );
};

function foo() {
    console.log( 3 );
}

虽然这一切听起来不过是一些有趣的学院派细节,但是它强调了一个事实:在同一个作用域内的重复定义是一个十分差劲儿的主意,而且经常会导致令人困惑的结果。

在普通的块儿内部出现的函数声明一般会被提升至外围的作用域,而不是像这段代码暗示的那样有条件地被定义:

foo(); // "b"

var a = true;
if (a) {
   function foo() { console.log( "a" ); }
}
else {
   function foo() { console.log( "b" ); }
}

然而,重要的是要注意这种行为是不可靠的,而且是未来版本的 JavaScript 将要改变的对象,所以避免在块儿中声明函数可能是最好的做法。

复习

我们可能被诱导而将 var a = 2 看作是一个语句,但是 JavaScript 引擎 可不这么看。它将 var aa = 2 看作两个分离的语句,第一个是编译期的任务,而第二个是执行时的任务。

这将导致在一个作用域内的所有声明,不论它们出现在何处,都会在代码本身被执行前 首先 被处理。你可以将它可视化为声明(变量与函数)被“移动”到它们各自的作用域顶部,这就是我们所说的“提升”。

声明本身会被提升,但不是赋值,即便是函数表达式的赋值,也 不会 被提升。

要小心重复声明,特别是将一般的变量声明和函数声明混在一起 —— 如果你这么做的话,危险就在眼前!

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

推荐阅读更多精彩内容