JS中的作用域、变量提升

此文章著作权归饥人谷_Lyndon和饥人谷所有,转载请注明

前言

比较绕的并不是作用域与变量提升,而是作用域链,经常容易在写伪码时遇到死循环/(ㄒoㄒ)/~~相对于作用域来说,变量提升会稍微绕一些,不过只需牢记原则就不会出错,熟悉变量提升的机制能够更好地理解作用域链,降低犯错风险。


一、作用域



在大多数编程语言中,会用花括号{}来形成一个作用域,俗称“块作用域”,例如C语言、C++等。但是在JS中{}并不能产生块作用域,JS中的作用域是依靠函数形成的。


在ECMAScript5中,JS只有两类作用域:全局作用域、函数作用域。

  • 全局作用域:全局对象的作用域,在代码的任何地方都可访问,但有时会被函数作用域覆盖

  • 函数作用域:作用于整个函数范围内,不管到底是在函数中的何处进行声明

// 全局变量
var i = 100;
// 函数声明,outer是一个外部函数
function outer(){
    // 访问全局变量
    console.log(i);  // 100
    // 函数声明,inner是一个内部函数
    function inner(){
        // 内部函数的内部进行了变量提升,也就是第二部分叙述的内容
        console.log(i);  // undefined
        // 这里的i是局部变量,作用域仅在函数内
        var i = 1;
        // 局部变量覆盖全局变量,或者说是函数作用域覆盖全局作用域
        console.log(i);  // 1
    }
    inner();
    // 这里的i是全局变量
    console.log(i);  // 100
}
outer();



定义变量时,如果不写var,那么就会相当于声明了一个全局变量,作用域为全局作用域;否则声明的是局部变量,作用域为函数作用域。在以上代码段中,第一行的var i = 0是全局变量,虽然它添加var,但是在全局范畴中声明,而且不在函数范围内,因此效果等同于i = 0。但是在JS编程中应该尽力避免不加var,即使真的需要全局变量,也应该在最外层作用域中使用var声明。


二、变量提升



关于变量提升(hoisting)的定义,Kenneth Truyers曾经在博客中这样写道

In Javascript, you can have multiple var-statements in a function. All of these statements act as if they were declared at the top of the function. Hoisting is the act of moving the declarations to the top of the function.



变量的声明会被自动移到函数或者全局代码的最顶上。移动的仅仅是declarations,变量的定义并不会随之提升。


因为变量提升非常的weird,所以很多代码的欺骗性非常强,尤其是在前端面试或者笔试中考官非常青睐于类似的题目,因此我建议理解代码的等价形式,也就是在纸上根据变量提升原则来书写新的等价代码从而找出正确答案。如以下代码段:

var date = new Date();
function fn(){
    console.log(date);
    if(true){
        var date = 'hello';
    }
}
fn();



结果并不是datetoString方法返回的结果,而是undefined,因为以上代码等价于:

// 变量声明提升
var date;
date = new Date();
function fn(){
    // 变量声明提升,但是此时未定义变量的值
    var date;
    console.log(date);
    if(true){
        date = "hello";
    }
}
fn();



但是在变量提升中还存在着一些特殊情况,因为在ES5中,变量声明、函数声明都会被提升,这就衍生出很多值得辨析的问题。


在ES6中,function *, let, class, const也会被提升,但是提升机制又与变量提升、函数提升有所区别。

>>> 情境1:重复声明



如以下代码,重复声明变量a

var a = 10;
console.log(a);  // 10
if(true){
    var a = 20;
    console.log(a);  // 20
}
console.log(a);  // 20



分析这一段代码时,记住两点:JS变量只有全局作用域、函数作用域两种作用域形态,a只会在代码顶部声明一次,而var a = 20的作用仅是赋值,因此以上代码等价于:

var a;
var a;  // 是流程控制语句中的a,实际上在JS解析中这一句是不存在的,因为变量`a`已经声明过了
a = 10;
console.log(a);
if(true){
    a = 20;
    console.log(a);
}
console.log(a);


>>> 情境2:命名冲突



console.log处于需要提升的变量与方法的下方时,如果在同一个作用域中定义了名字相同的变量与方法,那么无论顺序如何,变量的赋值都会覆盖掉方法的赋值。其实用正常思维方式就可以理解。

var fn = 3;
function fn(){};
console.log(fn);  // 3



以上代码等价于:

var fn;
function fn(){};
fn = 3;
console.log(fn);



可以明显看出:经过转换后是很容易被理解的。但是还需要考虑到当函数执行有命名冲突的时候,函数执行的载入顺序是变量、函数、参数,如以下代码:

function f(f){
    console.log(f);
    var f = 100;
    console.log(f);
}
f(20);



这是一段复杂的代码,但是结合载入顺序来理解,可以将代码等价转换为如下形式:

function f(f){
    var f;
    console.log(f);
    f = 100;
    console.log(f);
}



传入参数f = 20后,函数内部相当于虚拟形成:var f = 20,这个变量声明其实可以认为是覆盖了函数内部变量提升的var f,因此第一个console.log(f)的结果为20,接下来是第二个f = 100覆盖之前的变量值,那么第二个console.log(f)的结果为100,所以在执行这个很annoying的函数的时候,先提升变量,再执行函数体,接下来传入参数20,事实上也很好理解。

>>> 情况3: 函数与变量同时提升



如下代码示例:

console.log(f);
function f(){};
var f = 'text';



输出结果是:function f(){}


但是稍作转变成如下形式:

console.log(f);
var f = function(){};
var f = 'text';



输出结果是:undefined


这里涉及到的知识实际上和变量提升关系不大,而是和函数声明方式有关:


ECMAScript里面规定三种声明函数方式,常用的有以下两种:

// 第一种:函数声明
function f(){
    statement;
}
// 第二种:函数表达式
var f = function(){
    statement;
}



针对第一段代码,其中运用函数声明,函数声明的方式所能保证的是:即使函数写在最后也能在之前语句中进行调用,但是函数声明部分必须已经被下载至本地;


而第二段代码,其中运用函数表达式,实质上是定义了一个变量f,然后把function(){}赋给变量,因此第二段代码实际上等价于:

var f;
console.log(f);
f = function(){};
f = 'text';



第一段代码,函数声明在提升的时候,实际上是会把整个函数提升上去,包括函数定义的部分,所以第一段代码的等价形式是:

var f;
function f(){};
console.log(f);
f = 'text';



但是再将代码进行转换,会得到不一样的结果:

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



返回的结果是:function foo(){}

>>> 情况4:函数与函数重复声明



当两个函数声明重复时,其原则是后者覆盖前者。以下代码段:

console.log(foo);
function foo(n){return n+2};
function foo(n){return n+1};



输出结果是:function foo(n){return n+1}


原理:在函数声明提升时,遵循先来后到的函数声明提升原则,之后后者会覆盖前者,因此以上代码等价于:

function foo(n){return n+2};
function foo(n){return n+1};
console.log(foo);



如果调换代码秩序,那么代码输出结果会变化:

function foo(n){return n+1};
function foo(n){return n+2};
console.log(foo);  // function foo(n){return n+2};

总结



经过以上操作,可以归纳出四项原则:

  • 所有声明都会被提升到对应作用域的顶上(as if they were declared at the top of the function
  • 同一个变量声明只进行一次,其他重复声明会被JS解析忽略
  • 函数声明进行提升时会连带函数定义一起提升
  • 遵循前三项原则多多动手写等价转换,就一定不会出错
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • 继承 一、混入式继承 二、原型继承 利用原型中的成员可以被和其相关的对象共享这一特性,可以实现继承,这种实现继承的...
    magic_pill阅读 1,019评论 0 3
  • You don't KnowJS 引语:你不懂的JS这本书�github上已经有了7w的star最近也是张野大大给...
    Sleet阅读 557评论 0 0
  • 官方中文版原文链接 感谢社区中各位的大力支持,译者再次奉上一点点福利:阿里云产品券,享受所有官网优惠,并抽取幸运大...
    HetfieldJoe阅读 2,292评论 0 21
  • 无意在网站看到高三字眼的纪录片,就下载来看。片中的高三,对我来说,才是真正意义上的高三,压抑的让人喘不过气。回想起...
    江雨南梦阅读 261评论 1 3
  • 忘记是多久没有静心去思考了,每天在悠闲又内疚中度过,总觉得没有思考的日子像是在飘着,空空的,站不直。记不清自己发誓...
    可曾知晓蓝色如我阅读 164评论 0 1