zoukankan      html  css  js  c++  java
  • 【翻译】JavaScript中的作用域和声明提前

    原文:http://www.adequatelygood.com/JavaScript-Scoping-and-Hoisting.html

    ===翻译开始===

    你知道下面的JavaScript脚本执行结果是什么吗?

    1 var foo=1;
    2 function bar(){
    3     if(!foo){
    4         var foo=10;
    5     }
    6     alert(foo);
    7 }
    8 bar();

    如果你对弹出的结果是"10"感到惊讶的话,那么下面这段脚本会让你晕头转向的:

    1 var a=1;
    2 function b(){
    3     a=10;
    4     return ;
    5     function a(){};
    6 }
    7 b();
    8 alert(a);

    浏览器会弹出"1",这到底是怎么回事呢?这看起来很奇怪,事实上这恰好是语言的一个强大而又富有表现力的特性。我不知道这种特别的行为是否有一个标准的命名,但我喜欢把它叫做"hoisting"。接下来我会试着分析一下这种机制,但是我们有必要先理解一下JavaScript的作用域。

    JavaScript中的作用域

    对于JavaScript初学者来说,作用域常让他们感到困惑。事实上,一些资深的JavaScript开发者也不是完全理解作用域。JavaScript的作用域之所以让人如此困惑,因为它跟C系语言有点相似,请看下面的C程序:

     1 #include <stdio.h>
     2 int main(){
     3     int x=1;
     4     printf("%d
    ",x);//1
     5     if(1){
     6         int x=2;
     7         printf("%d
    ",x);//2
     8     }
     9     printf("%d
    ",x);//1
    10 }

    程序会依次输出1,2,1,这是因为C系语言有块级作用域。当程序运行到一个程序块的时候(比如if语句),在该程序块里定义的新变量不会影响到外部作用域。但在JavsScript中却不相同,试着执行下面的脚本:

    1 var x=1;
    2 console.log(x);//1
    3 if(true){
    4     var x=2;
    5     console.log(x);//2
    6 }
    7 console.log(x);//2

    脚本执行后会依次输出1,2,2,这是因为JavsScript只有函数级作用域,这和C系语言有着根本的不同,程序块(比如if语句)是不会创建新的作用域的,只有函数才会。

    对于一些使用C、C++、C#或者Java语言的开发者来说,这简直让人难以接受。还好JavaScript的函数足够灵活,可以用其它变通方法。如果你一定要在函数内创建一个临时的作用域,可以这样做:

     1 function foo(){
     2     var x=1;
     3     if(x){
     4         (function(){
     5             var x=2;
     6             //some other code
     7         }());
     8     }
     9     //x is still 1.
    10 }

    这个方法相当灵活,在任何需要的地方都可以使用,不止在块语句里。但是我强烈建议你花一些时间来真正理解和欣赏JavaScript的作用域,这是我最喜欢的语言特性之一,它真的非常强大。如果你理解了作用域,那么对于声明提前你会更容易理解。

    声明,变量名,声明提前

    在JavaScript中,一个变量可以通过以下四种方式之一进入作用域:

    1、语言内置:所有作用域都默认包含"this"和"arguments"变量。
    2、函数形参:函数可以拥有形参,所属作用域就是该函数体。
    3、函数声明:形如"function foo(){}"的声明。
    4、变量声明:形如"var foo;"的声明。

    函数声明和变量声明总会被JavaScript解释器自动放到所属作用域的顶端,函数参数和语言内置的变量默认都是在最顶端。举个例子,有如下代码:

    1 function foo(){
    2     bar();
    3     var x=1;
    4 }

    被解析器解析后变为:

    1 function foo(){
    2     var x;
    3     bar();
    4     x=1;
    5 }

    这说明了,无论声明语句放在哪里都会被执行,比如说下面两个函数,它们是相等的:

     1 function foo(){
     2     if(false){
     3         var x=1;
     4     }
     5     return;
     6     var y=1;
     7 }
     8 
     9 function foo(){
    10     var x,y;
    11     if(false){
    12         x=1;
    13     }
    14     return;
    15     y=1;
    16 }

    要注意的是,有时声明和赋值会写在一起,但是赋值部分并没有被提前,只有声明被提前了。函数声明就有些特别了,整个函数体也会被提前。但是不要忘了函数声明有两种方式,请看下面的代码:

     1 function test(){
     2     foo(); //TypeError "foo is not a function"
     3     bar(); //"this will run!"
     4     var foo=function(){ //function expression assigned to local variable "foo"
     5         alert('this won't run!');
     6     };
     7     function bar(){//function declaration,given the name 
     8         alert('this will run!');
     9     }
    10 }
    11 test();

    在这个例子里,只有使用函数声明的函数体会被提前至顶端,而使用函数表达式赋值方式,只有名字"foo"被提前至顶端,函数体是没有的。

    上面的例子基本覆盖了自动提前的情况,看起来并不是那么复杂让人迷惑。当然,一些其它比较特别的例子还是有一些复杂的。

    变量识别顺序

    我们要特别记住变量的识别顺序,前面说过变量名进入作用域有四种方式,我上面列举的顺序就是它们被识别的顺序。通常,如果一个变量名已经定义了,那么它就不会被其它相同名称的变量所覆盖。这意味着函数声明比变量声明优先级高,但这并不影响赋值操作,只是声明部分会被忽略而已。

    PS:补充一段代码,表达作者的意思

    function foo(){}
    var foo=3;
    console.log(foo);//3
    
    这段代码会被解析为:
    function foo(){}
    //var foo; //这条语句就被忽略了
    foo=3;
    console.log(foo);
    
    依我理解是这样子:
    var foo;
    foo=function foo(){}
    foo=3;
    console.log(foo);

    接着原文,以下是几种特别情况:

    1、内置的变量"arguments"表现比较奇怪,它好像定义在函数形参和函数声明之间。这意味着如果形参中有个变量为"arguments",那么它的优先级将高于内置的"arguments",即使它是undefined。这不是一个好的特性,不要使用"arguments"作为形参变量名。
    2、使用"this"作为一个标识符会引起语法错误,这是一个好的特性。
    3、如果多个形参中出现同名,那么最后一个将拥有最高的优先级,即使它是undefined。

    带有名字的函数表达式

    你也可以给函数表达式中的函数起个名字,采用类似函数声明的语法。但这并不能使它变成一个函数声明,并且这个函数名不会被添加到作用域,函数体也不会被提前至顶端,下面用一些代码来演示我说的意思:

     1 foo(); //TypeError "undefined is not a function"
     2 bar(); //valid
     3 baz(); //TypeError "undefined is not a function"
     4 spam();//ReferenceError "spam is not defined"
     5 
     6 var foo=function(){}; //anonymous function expression('foo' get hoisted)
     7 function bar(){};     //function declaration ('bar' and the function body get hoisted)
     8 var baz=function spam(){};//named function expression('only 'baz' get hoisted)
     9 
    10 foo(); //valid
    11 bar(); //valid
    12 baz(); //valid
    13 spam();//ReferenceError "spam is not defined"

    怎么利用这些知识编程

    现在你已经理解作用域和声明提前特性了,那么这些在JavaScript编程中有什么影响?最重要的是声明变量时要使用"var"关键字,我强烈建议你在每个作用域的顶端只写一个var语句(多变量的时候,用逗号连接)。如果你强制自己这样做,就不会对声明提升产生困惑了。不过,这么做会让你在当前作用域中寻找已经声明的变量变得更困难,我建议使用"JSLint"的"onevar"选项来验证代码,如果你照做了,你的代码看起来会像这样子:

    1 /*jslint onevar: true [...]*/
    2 function foo(a,b,c){
    3     var x=1,
    4         bar,
    5         baz="something";
    6 }

    看看规范怎么说

    我发现经常查阅ECMAScript规范文档有助于直接理解这些机制是怎么运行的,以下是规范对于变量声明和作用域的描述:

    1 如果变量声明语句在函数声明里面,那么变量就是定义在函数内部作用域(参考章节10.1.3),否则它们就是定义在全局作用域内(作为全局对象的成员变量,参考章节10.1.3)。变量进入作用域的时候就会被创建,块语句不会定义一个新的执行作用域,只有程序和函数声明会产生新的作用域。变量在创建的时候会被初始化为"undefined",一个带有初始化语句的变量,在赋值语句执行的时候才会被赋上其赋值表达式对应的值,并不是变量创建的时候就赋值。

    我希望这篇文章能够帮助JavaScript开发者理清一些困惑的问题,我已经尽可能的彻底把问题讲清楚,以免造成更多的疑惑。如果你发现我写错了或者遗漏了某些重要的东西,请一定让我知道。

    ===翻译完===

    翻译参考:http://ju.outofmemory.cn/entry/85659

    以下是一个例子:

     1 var x=0;
     2 var f=function(){
     3     x=1;
     4 }
     5 f();
     6 console.log(x);
     7 function f(){
     8     x=2;
     9 }
    10 f();
    11 console.log(x);
  • 相关阅读:
    c++ ShellExecuteEx调用java打包的exe程序
    麻省理工学院公开课-第四讲:快速排序 及 随机化 算法
    Win10的IIS与以前版本的一个区别
    干就行了!!!写程序就像珊瑚,分支太多,哪有那么多复用!
    NPoco的使用方法
    为什么前端要写标准代码?
    对于委托、事件、观察者模式最一目了然的代码段
    delphi处理消息的几种方式
    哎呀妈呀,吓死我了,幸好服务器没崩溃。
    Delphi的Hint介绍以及用其重写气泡提示以达到好看的效果
  • 原文地址:https://www.cnblogs.com/yanyd/p/4196375.html
Copyright © 2011-2022 走看看