JavaScript 中的作用域(scope)和提升(hoisting)

JavaScript

在说有关知识之前先让大家看几段代码:

var i = "Hellow world!";
alert(i);

很简单对吧?输出结果是“Hellow world!”,没有任何问题,好,我们继续:

var i = "Hellow World!";
function myFunction(){
    alert(i);
}
myFunction();

结果不出意料,还是“Hellow World!”,我们接着看:

var i = "Hello World!"; 
function myFunction(){ 
    alert(i); 
    var i = "I love you"; 
}
myFunction();

大家来猜猜看,结果是什么?
有多少人猜到了运行结果是 undefined
那这一段代码呢?

var i = 1;
function myFunction(){
    if(!i){
    var i  = 2;
    alert(i);
    }
}
myFunction();

为什么结果是 2?

这其中有关 JavaScript 中的一个术语** “提升”(Hoisting)**。

作用域(scope)

在说有关提升的知识之前必须要说一下作用域(Scope)。对于初学者来说,弄清楚作用域是十分重要的。

什么是作用域?

摘自 维基百科
在电脑程序设计中,作用域(scope,或译作有效范围)是名字(name)与实体(entity)的绑定(binding)保持有效的那部分计算机程序。不同的编程语言可能有不同的作用域和名字解析)。而同一语言内也可能存在多种作用域,随实体的类型变化而不同。作用域类别影响变量的绑定方式,根据语言使用静态作用域还是动态作用域变量的取值可能会有不同的结果。

  • 包含标识符的宣告或定义;
  • 包含语句和/或表达式,定义或部分关于可运行的算法;
  • 嵌套嵌套或被嵌套嵌套。

名字空间是一种作用域,使用作用域的封装性质去逻辑上组群起关相的众识别子于单一识别子之下。因此,作用域可以影响这些内容的名字解析。
程序员常会缩进他们的源代码中的作用域,改善可读性。

简单说就是,我们定义一个变量,该变量的有效范围,就是它的作用域。就有了两个名词全局变量局部变量。其中就涉及到变量的可见性,以及变量的生命周期等细节,这里不做太多赘述,有不清楚的同学可以自行查阅相关资料。

懂得 C 语言的同学来看一下以下这一段代码,如果不懂的同学可以跳过:

#include<stdio.h>
void main(){
    int i = 1;
    printf("%d,",i);
    if(i){
        int i = 2;
        printf("%d,",i);
    }
    printf("%d",i);
}

输出结果为1,2,1 。

熟悉 C语言的人都知道为什么会是这么输出。在 C 语言中是块级作用域,在 if() 语句中,声明了同名的局部变量,覆盖了全局变量的值。而当出了块后,局部变量销毁,重新输出全局变量的值。

再稍微科普一下什么是块级作用域:

任何一对花括号({ 和 })中的语句集都属于一个块,在这之中定义的所有变量在代码块外都是不可见的,我们称之为块级作用域。

我们再来看一段 JavaScript 代码:

var i = 1;
alert(i);
if (i){
    var i = 2;
    alert(i);
}
alert(i);

输出的结果为 1,2,2 。

这是因为,在 JavaScript 中是函数级作用域。类似 if 等块语句,并不会创建新的作用域。只有在函数中,才会创建新的作用域,定义在函数中的变量在函数外是不可见的。

那么,该如何解决呢?我们只需要在块级作用域中临时创建新的作用域就可以了:

var i = 1; 
if (i) { 
    function temporary() { 
        var i = 2; 
        //其他代码
    }
    temporary();
} 
// i 的值依然为 1

不过在 ES 5 中规定,函数只能在顶层作用域和函数作用域之中声明,不能在块级作用域声明。但是浏览器并没有遵守这个规定,上面的代码实际情况下还是可以正常运行。但是在严格模式中是会报错的。而且在不同的浏览器中对该语句的支持情况不一样,考虑到环境导致差异太大,应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句(函数提升影响,下文会说)。

看到这里是不是已经有人认为 JavaScript 没有块级作用域,只有函数级作用域了?这句话在前年说,那就是对的。但是在 2015 年 6 月,随着 ECMAScript 6(即 ECMAScript 2015,以后简称ES 6)的发布 ,JavaScript 也拥有块级作用域了。

var 在 JavaScript 中声明的变量要么是全局变量,要么为函数级变量,在 ES 6之前,是没有块级作用域的。在 ES 6 中新增了两个声明变量的方式 letconst

  • let 声明了一个块级作用域的局部变量,它的作用范围仅仅为最接近的块作用域(如果在所有块以外就是全局作用域),这将会比 var 的函数作用域更小。
  • const 声明的是一个只读的常量,而该常量一旦声明,常量的值就无法更改。const 声明的变量不得改变值,也就是说,const 一旦声明变量,就必须立即初始化,不能留到以后赋值。

注意:

  1. constlet 只在声明所在的块级作用域内有效。
  2. constlet 不存在提升。
console.log(i); // 输出 undefined
console.log(j); // 报错
var i = 1;
let j = 1;
  1. 只要块级作用域内存在 letconst 命令,它所声明的变量就 绑定(binding)这个区域,不再受外部的影响。ES 6 明确规定,如果区块中存在 letconst 命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错,在声明变量之前,该变量都是不可用的,只能在声明的位置后面使用。这在语法上,称为暂时性死区(temporal dead zone,简称TDZ)暂时性死区中不存在变量的提升。
var i = 1;
if (true) {
        i = "1"; // 报错
        let i;
}
  1. countlet 不可重复声明参数。
  2. 必须在严格模式下才可以使用 countlet

为什么我先那么大篇幅的介绍了作用域?如果我们能充分理解了作用域,再去理解提升就会比较容易。

提升(hoisting)

变量提升

JavaScript 的函数定义有个特点,它会先扫描整个函数体的语句,把所有申明的变量“提升”到函数顶部。但是,变量提升所提升的仅仅为变量的声明,并不会将变量的赋值也提升上来。

"use strict";
function myFunction() {
    var x = "Hellow," + y; 
    alert(x);
var y = "World";
}
myFunction();

输出结果为:Hellow,undefined。
虽然是 strict 模式,但是也不会报错,因为在运行之前,浏览器会先进行一次预编译,会将变量和函数先在函数的最顶部进行预编译。上面的代码经过编译后输出的结果为:

"use strict";
function myFunction() {
    var x;
    var y;
    x = "Hellow," + y; 
    alert(x);
    y = "World";
}
myFunction();

所以结果并不会报错,而会输出 undefined。

所以,在我们写 JavaScript 代码的时候,需要养成习惯,要把变量放在函数级作用域的最顶端,防止出现意外。

函数提升

变量提升是将变量提升到函数级作用域的最顶端,而函数的提升则是将整个函数都提到整个作用域的最顶端。不过函数的声明跟变量的声明有一点不一样。函数的声明会连函数体也会被一同提升。函数有两种声明方式,一种是变量指向的函数表达式,另外一种是函数的声明。需要注意的是,只有函数的声明形式才会被提升。二话不多说,先上代码:

function myFunctionOne(){ 
    myFunctionTwo(); 
    function myFunctionTwo(){ 
        alert("我是 myFunctionTwo"); 
    } 
} 
myFunctionOne(); 

输出结果为 :我是 myFunctionTwo。

我们再来看第二段:

function myFunctionOne(){ 
    myFunctionTwo(); 
    var i =function myFunctionTwo(){ 
      alert("我是 myFunctionTwo"); 
    } 
} 
myFunctionOne(); 

结果报错。

好的,有关的知识到这里就结束了,现在大家应该明白了有关 JavaScript 中的提升和作用域了吧。


有没有同学在看完了整篇文章之后再回头看第一段代码发现还是没看懂?先把最早的代码贴上来:

var i = "Hello World!"; 
function myFunction(){ 
    alert(i); 
    var i = "I love you"; 
}
myFunction();

不知道有没有人有疑惑,在外部有一个全局变量 i ,但是为什么在函数内部为什么无法引用输出外面的全局变量。就算应用了作用域和提升后也无法解释。

我们都知道,在 JavaScript 中是可以重复声明的,而且重复声明并不会修改赋值。但是,在局部变量中如果声明了重名的全局变量,局部变量就会在作用域中覆盖掉全局变量

我们只需要对 var i = "I love you";中的赋值注释掉后运行,然后再将整个语句注释后再运行,就可以的得到验证,两次输出的结果先后为 undefined,Hello World!。

结尾

有关作用域和提升相关的知识就总结了这一些,如有遗漏希望读者们评论补充。这些特性我们总会在不经意间遇到,当我们有时候遇到某些“坑”的时候,要记得想想这些 JavaScript 中的特性,说不定就能找到问题的所在。

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

推荐阅读更多精彩内容

  • let 和 const 命令 let 命令 块级作用域 const 命令 顶层对象的属性 gl...
    安小明阅读 978评论 0 0
  • 《你不知道的JavaScript》真的是一本好书,阅读这本书,我有多次“哦,原来是这样”的感觉,以前自以为理解了(...
    然并阮阅读 619评论 2 9
  • 介绍 JavaScript中有一个被称为作用域(Scope)的特性。虽然对于许多新手开发者来说,作用域的概念并不是...
    安_6dd1阅读 958评论 0 8
  • let 命令 块级作用域 const 命令 顶层对象的属性 global 对象 let 命令 基本用法 ES6 新...
    卞卞村长L阅读 587评论 0 0
  • 花到了,挺开心。唯有瑕疵便是提前让她知晓我准备的惊喜。 本该开心的一天却因为我工作的事情再三拖延,导致原本不舒服的...
    Ermao阅读 185评论 0 1