zoukankan      html  css  js  c++  java
  • you don't know js -- Scope and Closures学习笔记——第四章(声明提升 Hoisting)

    以下内容为自己看原版尝试做的翻译,仅当一个自己的看书记录,书中内容绝大部分都翻译了,但由于个人能力有限,建议各位看客不要迷信翻译的质量,推荐购买其英文原版学习观看。

    现在,一想到作用域以及变量声明的位置和方式决定其所属的作用域层,你应该感到很舒适。函数作用域和块级作用域遵守相同的规则,在这个方面:在一个作用域内部声明的任何变量属于这个作用域。

    先有鸡还是先有蛋

    有一种倾向认为在一个JavaScript程序中你所看到的所有代码都是一行接一行从上到下顺序执行的。虽然大体上这是正确的,但这种说法中的一部分会带来错误的思维。

    考虑如下代码:

    a = 2;
    var a;
    console.log(a);
    

    你认为在console.log(..)语句中会打印什么?

    许多代码猴都认为是undefined,因为var a语句在a = 2的后面,这看起来会很自然的假定变量被重定义了,并且为其赋了默认的undefined。然而,输出的结果将会是2

    考虑另外一段代码:

    console.log(a);
    var a = 2;
    

    你可能倾向于假设,以为前一个代码段表现出了less-than-top-down的行为,可能在这里,同样也会打印2。另一些人可能认为因为变量a在其声明前被使用,这将会抛出一个ReferenceError

    不幸的是,上面两种猜测都是错的,输出的结果会是undefined

    那么,咋回事儿捏?似乎我们遇到了一个先有鸡还是先有蛋的问题。哪一个先出来,声明(“蛋”)?还是赋值(“鸡”)?

    编译器再现江湖

    为了回答这个问题,我们需要重提第一章关于编译器的讨论。回想下引擎会在解释你的JS代码前先进行编译。编译阶段的一部分工作就是找到并关联对应作用域中的变量。第二章告诉我们这是词法作用域的核心。

    所以,最好的思考方式是所有的声明,包括变量和函数,都会在代码执行前优先处理。

    当你看到var a = 2;时,你可能认为他是一条语句。但是JavaScript实际上将它看做两条语句: var a;a = 2;第一条语句(声明语句),在编译阶段被处理。第二条语句(赋值语句)留在原地等待执行阶段。

    我们的第一个代码段应该考虑被处理成如下样子:

    var a;
    a = 2;
    console.log(a);
    

    第一部分是声明,第二部分是执行。

    同样的,第二个代码段被处理成:

    var a;
    console.log(a);
    a = 2;
    

    所以,可以把这个处理过程想象成变量和函数的声明被从他们定义的地方“移动”到这个代码流的顶端。这种行为有一个名字叫hoisting

    换句话说,蛋(声明)在鸡(赋值)的前面。

    Only the declarations themselves are hoisted, while any assignments
    or other executable logic are left in place. If hoisting
    were to re-arrange the executable logic of our code, that
    could wreak havoc.

    foo();
    function foo(){
    	cosole.log(a); // undefined
    	var a = 2;
    }
    

    函数foo的声明被提升了,因此在第一行对函数的调用可以被执行。

    同时需要注意的是变量提升是per-scope的。因此上面的函数更准确的说会被解释成下面的样子:

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

    正如我们所见的,函数声明被提升了。但是函数表达式并不会被提升。

    foo();	// not ReferenceError, but TypeError!
    var foo = function bar(){
    	// ...
    };
    

    变量foo被提升并附加到这个程序的global作用域,因此foo()并不会抛出RefferenceError。但是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声明被有效的忽略,连续的function声明却会覆盖前面的。

    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两条语句,第一个是编译阶段的任务,第二个是执行阶段的任务。

    带来的结果是所有在一个作用域中的声明,不管在何处,都会在代码被执行前先被处理。你可以把这个想象成声明(变量和函数)被“移动”到他们各自作用域的顶部,这个动作我们成为声明提升(hoisting)。

    声明会被提升,但是赋值,包括函数表达式的赋值,都不会被提升。

    小心对待重复的声明,特别是混合普通var和函数的声明。

  • 相关阅读:
    exFAT移动硬盘写保护怎么去掉
    ORACLE:一列的多行数据拼成字符串
    cxgrid中,如何根据列名或字段名取得footer值
    Delphi天气预报查询
    Delphi ListView基本用法大全
    datasnap的初步
    Delphi ListView基本用法大全
    Delphi实现树型结构
    获取身份证号码信息
    让Delphi XE5跟其他版本的Delphi共存
  • 原文地址:https://www.cnblogs.com/terrible/p/4438250.html
Copyright © 2011-2022 走看看