乐趣区

关于javascript:深度剖析JavaScript中块级作用域与函数作用域

前言

系列首发于公众号『前端进阶圈』,若不想错过更多精彩内容,请“星标”一下,敬请关注公众号最新消息。

面试官必问系列:深刻了解 JavaScript 块和函数作用域

  • 在 JavaScript 中,到底是什么会生成一个新的作用域,只有函数才会生成新的作用域吗?那 JavaScript 其余构造能生成新的作用域吗?

    3.1 函数中的作用域

  • 在之前的词法作用域中可见 JavaScript 具备基于函数的作用域,这也就意味着一个函数都会创立一个新的作用域。但其实并不完全正确,看以下例子:

    function foo(a) {
    var b = 2;
    function bar() {// ...}
    var c = 3;
    }
  • 以上代码片段中,foo() 的作用域中蕴含了标识符 a, b, c 和 bar。无论示意申明呈现在作用域中的何处,这个标识符所代表的变量和函数都从属于所处作用域的作用域中。
  • bar() 中也领有属于本人的作用域,全局作用域也有属于本人的作用域,它只蕴含了一个标识符: foo()
  • 因为标识符 a, b, c 和 bar 都从属于 foo() 的作用域内,因而无奈从 foo() 的内部对它们进行拜访。也就是说,这些标识符在全局作用域中是无奈被拜访到的,因而如下代码会抛出 ReferenceError:

    bar(); // ReferenceError: bar is not defined
    
    console.log(a, b, c); // 全都抛出 ReferenceError
  • 但标识符 a, b, c 和 bar 可在 foo() 的外部被拜访的。
  • 函数作用域的含意 属于这个函数的全副变量都能够在整个函数的范畴内应用及复用(在嵌套的作用域中也能够应用)。这种设计方案可依据须要扭转值类型的 “ 动静 ” 个性。

3.2 暗藏外部实现

  • 咱们对函数的传统认知就是先申明一个函数,而后再向外面增加代码,但 反过来 可带来一些启发:从所写的代码中挑选出一个任意片段,而后就用函数申明的形式对它进行包装,实际上就是把这些代码 "暗藏" 起来了。
  • 理论的后果就是在这个代码片段的四周创立了一个新的作用域 ,也就是说这段代码中的任何申明(变量或函数) 都将绑定在这个新创建的函数作用域中,而不是先前所在的作用域中。换句话说,可把变量和函数包裹在一个函数的作用域中,而后用这个作用域来 “ 暗藏 ” 他们。
  • 为什么 “ 暗藏 ” 变量和函数是一个有用的技术?

    function doSomething(a) {b = a + doSomethingElse( a * 2);
    console.log(b * 3);
    }
    function doSomethingElse(a) {return a - 1;}
    var b;
    doSomething(2); // 15
  • 上述代码片段中,变量 b 和函数 doSomethingElse(..) 应该是 doSomething(..) 外部具体实现的 “ 公有 ” 内容。而上述代码将变量 b 和函数 doSomethingElse(..) 的拜访权限放在了内部作用域中,这可能是 “ 危险 ” 的。更 “ 正当 ” 的设计应该是将这些公有内容放在 doSomething(…) 的外部。
  • 如下:

    function doSomething(a) {function doSomethingElse(a) {return a - 1;}
    
    var b;
    b = a + doSomethingElse(a * 2);
    console.log(b * 3);
    }
    doSomething(2); // 15
  • 躲避抵触
  • "暗藏" 作用域中的变量和函数的另一个益处是可防止同名标识符的抵触,两个标识符名字雷同但用处不同,无意间可能会造成命名抵触,而抵触会导致变量的值被意外笼罩。
  • 例如:

    function foo() {function bar(a) {
        i = 3; // 批改 for 循环所属作用域中的 i
        console.log(a + i);
    }
    for (var i=0; i<10; i++) {bar( i * 2); // 蹩脚,有限循环了!}
    }
    foo();
  • bar(…) 外部的赋值表达式 i = 3 意外地笼罩了申明在 foo(..) 外部 for 循环中的 i。在这个例子中将会导致有限循环,因为 i 被固定设置为 3,永远满足小于 10 这个条件。
  • 规定抵触的形式

    1. 全局命名空间:在全局作用域中申明一个足够独特的变量,通常为一个对象,如下:

      var MyReallyCoolLibrary = {
      awesome: "stuff",
      doSomething: function() {// ...},
      doAnotherThing: function() {// ...}
      }
    2. 模块治理

      3.3 函数作用域

  • 当初晓得,在任意代码片段内部增加包装函数,可将外部的变量和函数定义 “ 暗藏 ” 起来,内部作用域无法访问包装函数外部的任何内容。
  • 如下:

    var a = 2;
    function foo() { // <-- 增加这一行
    var a = 3;
    console.log(a); // 3
    } // <-- 以及这一行
    foo(); // <-- 以及这一行
    console.log(a); // 2
  • 上述代码会导致一些额定的问题,首先,必须先申明一个具名函数 foo(), 这就意味着 foo 这个名称自身 “ 净化 ” 了所在作用域(上述代码为全局作用域)。其次,必须显式地通过 foo() 来调用这个函数。
  • 如果函数不须要函数名(或者至多函数名能够不净化所在作用域),且可能自行运行,这将会更现实。
  • JavaScript 提供了两种计划来解决:

    var a = 2;
    (function foo() {
    // <-- 增加这一行
    var a = 3;
    console.log(a); // 3
    })(); // <-- 以及这一行
    console.log(a); // 2
  • 在上述代码中,包装函数的申明以 (function... 而不仅是以 function... 开始。函数会被当做函数表达式而不是一个规范的函数申明来解决。
  • 如何辨别函数申明和表达式?

    • 最简略的形式就是看 function 关键字呈现在申明中的地位 (不仅仅是一行代码,而是整个申明中的地位)。 如果 function 为申明中的第一个关键字,那它就是一个函数申明,否则就是一个函数表达式。
    • 函数申明和函数表达式之间最重要的区别就是他们的名称标识符将会绑定在何处。
  • 比拟一下后面两个代码片段。第一个片段中 foo 被绑定在所在作用域中,能够间接通过 foo() 来调用它。第二个片段中 foo 被绑定在函数表达式本身的函数中而不是所在作用域中。
  • 换句话说,(function foo(){...}) 作为函数表达式意味着 foo 只能在 ... 所代表的地位中被拜访,内部作用域则不行。

3.3.1 匿名和具名

  • 对于函数表达式最相熟的就是回调参数了,如下:

    setTimeout(function () {console.log("I waited 1 second!");
    }, 1000);
  • 这叫作 匿名函数表达式,因为 function().. 没有名称标识符。函数表达式能够是匿名的,而函数申明则不能够省略函数名——在 JavaScript 的语法中这是非法的。
  • 匿名函数表达式的毛病

    1. 匿名函数在栈追踪中不会显示出有意义的函数名,这使调试很艰难。
    2. 如果没有函数名,当函数须要援用本身时只能通过曾经 过期 arguments.callee 来援用。
    3. 匿名函数对 代码可读性 不是很敌对。
  • 上述代码的革新后果:

    setTimeout(function timeoutHandler() {console.log("I waited 1 second!");
    }, 1000);

3.3.2 立刻执行函数表达式

var a = 2;
(function IIFE() {
    var a = 3;
    console.log(a); // 3
})();
console.log(a); // 2
  • 因为函数被蕴含在一对() 括号外部,因而成为了一个表达式,通过在开端加上另外一个() 能够立刻执行这个函数,比方(function foo(){..})()。第一个() 将函数变成表达式,第二个() 执行了这个函数。
  • 立刻执行函数表达式的术语为:IIFE(Immediately Invoked Function Expression);
  • IIFE 的利用场景

    1. 除了上述传统的 IIFE 形式,还有另一个形式,如下:

      var a = 2;
      (function IIFE() {
      var a = 3;
      console.log(a); // 3
      }());
      console.log(a); // 2
    2. 第一种模式中函数表达式被蕴含在 () 中,而后在前面用另一个 () 括号来调用。第二种模式中用来调用的 () 括号被移进了用来包装的 () 括号中。
    3. 这两种形式的抉择全凭集体爱好。
    4. IIFE 还有一种进阶用法,就是 把他们当做函数调用并传递参数进去, 如下:

      var a = 2;
      (function IIFE(global) {
      var a = 3;
      console.log(a); // 3
      console.log(global.a); // 2
      })(window);
      console.log(a); // 2
    5. IIFE 的另一个利用场景是 解决 undefined 标识符的默认值被谬误笼罩导致的异样

      • 将一个参数命名为 undefined, 但在对应的地位不传入任何值,这样就能够就保障在代码块中 undefined 标识符的值为 undefined

        undefined = true; // 给其余代码挖了一个大坑!相对不要这样做!(function IIFE(undefined) {
        var a;
        if (a === undefined) {console.log("Undefined is safe here!");
        }
        })();
    6. IIFE 的另一种变动的用处是 倒置代码的运行程序,将须要运行的函数放在第二位,在 IIFE 执行之后当做参数传递进去

      var a = 2;
      (function IIFE(def) {def(window);
      })(function def(global) {
      var a = 3;
      console.log(a); // 3
      console.log(global.a); // 2
      });
    7. 函数表达式 def 定义在片段的第二局部,而后当做参数 (这个参数也叫做 def) 被传递 IIFE 函数定义的第一局部中。最初,参数 def(也就是传递进去的函数)被调用,并将 window 传入当做 global 参数的值。

      3.4 块作用域

  • 如下:

    for (var i = 0; i < 5; i++){console.log(i);
    }
  • 在 for 循环中定义了变量 i,通常是想在 for 循环外部的上下文中应用 i, 而疏忽 i 会绑定在内部作用域 (函数或全局) 中。
  • 批改后:

    var foo = true;
    if(foo) {
    var bar = foo * 2;
    bar = something(bar);
    console.log(bar);
    }
  • 上述代码中,变量 bar 仅在 if 的上下文中应用,将它申明在 if 外部中式十分一个清晰的构造。
  • 当应用 var 申明变量时,它写在哪里都是一样的,因为它最终都会属于内部作用域。(这也就是变量晋升)

3.4.1 with

  • 在词法作用域中介绍了 with 关键字,它不仅是一个难于了解的构造,同是也是一块作用域的一个例子 (块作用域的一种模式), 用 with 从对象中创立出的作用域仅在 with 所处作用域中无效

3.4.2 try/catch

  • 很少有人留神,JavaScript 在 ES3 标准 try/catch 的 catch 分句会创立一个块作用域,其中申明的变量仅会在 catch 外部无效。

    try {undefined(); // 目标是让他抛出一个异样
    } catch (error) {console.log("error ------>", error); // TypeError: undefined is not a function
    }
    console.log("error ------>", error); // ReferenceError: error is not defined
  • error 仅存在于 catch 分句外部,当视图从别处援用它时会抛出谬误。
  • 对于 catch 分句看起来只是一些实践,但还是会有一些有用的信息的,后续文章会提到。

3.4.3 let

  • JavaScript 在 ES6 中引入了 let 关键字。
  • let 关键字将变量绑定到所处的任意作用域中(通常是 { ...} 外部)。换句话说,let 申明的变量隐式地了所在的块作用域。

    var foo = true;
    if(foo) {
    var bar = foo * 2;
    bar = something(bar);
    console.log(bar);
    }
    console.log(bar); // ReferenceError: bar is not defined
  • 应用 let 进行的申明不会再块作用域中进行晋升。申明的代码被运行前,申明并不 "存在"。

    {console.log(bar); // ReferenceError
    let bar = 2;
    }

    1. 垃圾收集

  • 另一个块作用域很有用的起因和闭包中的内存垃圾回收机制相干。
  • 如下代码:

    function process(data) {// do something}
    
    var someObj = {};
    process(someObj);
    
    var btn = document.getElementById('my_button');
    btn.addEventListener('click', function click(evt) {console.log('clicked');
    }, /*capturingPhase=*/false);
  • click 函数的点击回调并不需要 someReallyBigData 变量。实践上这意味着当 process(..) 执行后,在内存中占用大量空间的数据结构就能够被垃圾回收了。然而,因为 click 函数造成了一个笼罩整个作用域的闭包,JavaScript 引擎极有可能仍然保留着这个构造(取决于具体实现)。
  • 批改后:

    function process(data) {// do something}
    
    // 在这个块中定义内容就能够销毁了
    {var someObj = {};
    process(someObj);
    }
    
    var btn = document.getElementById('my_button');
    btn.addEventListener('click', function click(evt) {console.log('clicked');
    }, /*capturingPhase=*/false);

    2. let 循环

  • 代码如下:

    for(let i = 0; i < 10; i++) {console.log(i);
    };
    console.log(i); // ReferenceError
  • for 循环中的 let 不仅将 i 绑定了 for 循环外部的块中,事实上他将其从新绑定到了循环的每一次迭代中,确保应用上一个循环迭代完结时的值从新进行赋值。
  • 上面通过另一种形式来阐明每次迭代时进行从新绑定的行为;

    {
    let i;
    for(i = 0; i < 10; i++) {
        let j = i; // 每次迭代中从新绑定
        console.log(j);
    };
    }
  • let 申明从属与一个新的作用域而不是以后的函数作用域(也不属于全局作用域)。
  • 考虑一下代码:

    var foo = true, baz = 10;
    
    if (foo) {
    var bar = 3;
    
    if (baz > bar) {console.log( baz);
    }
    
    // ...
    }
  • 这段代码能够简略地被重形成上面的等同模式:

    var foo = true, baz = 10;
    
    if (foo) {
    var bar = 3;
    // ...
    }
    
    if (baz > bar) {console.log( baz);
    }
  • 然而在应用块级作用域的变量时须要留神以下变动:

    var foo = true, baz = 10;
    
    if (foo) {
    let bar = 3;
    
    if (baz > bar) { // <-- 挪动代码时不要忘了 bar!
        console.log(baz);
    }
    }

3.4.4 const

  • ES6 还引入了 const, 同样可用来创立块级作用域,但其值是固定的(常量), 不可批改。

    var foo = true;
    
    if (foo) {
    var a = 2;
    const b = 3; // 蕴含在 if 中的块作用域常量
    
    a = 3; // 失常 !
    b = 4; // 谬误 !
    }
    
    console.log(a); // 3
    console.log(b); // ReferenceError!

3.5 小结

  1. 函数时 JavaScript 中最常见的作用域单元。
  2. 块作用域值的是变量和函数布局能够属于所处的作用域,也能够属于某个代码块(通常指 {...} 外部)
  3. ES3 开始,try/catch 构造在 catch 分句中具备块作用域
  4. ES6 引入了 let,const 关键字来创立块级作用域
退出移动版