zoukankan      html  css  js  c++  java
  • 三、函数作用域和块作用域(学习笔记)—— 《你不知道的JavaScript》

    因为都是文字,有些不好理解,所以尽量将文字都以图文的形式画出来,便于理解。

    作用域包含了一系列的气泡。每一个都是一个容器,包含了标识符的定义。 这些气泡相互嵌套,排列成蜂窝型,排列结构在写代码时定义。

    下面考虑以下几个问题:

    • 是什么生成了一个新气泡?
    • 只有函数会生成新气泡吗?
    • JavaScript 中的其他结构能生成作用域气泡吗?

    带着这几个问题往下看。

    函数中的作用域

    对于上面问题的常见答案是,

    1. JavaScript 具有基于函数的作用域。
    2. 每声明一个函数,就生成一个作用域气泡。
    3. 其他结构不会生成作用域气泡

    但是,这并不完全正确。

    先来看一个代码片段。

    function foo(a) {
        var b = 2;
        function bar() {
            
        }
        var c = 3;
    }

    这些标识符代码的变量和函数都属于所处作用域的气泡,所以在外部是无法访问的。

    尝试在全局作用域访问 foo 内部的标识符,会报错:

    function foo(a) {
        var b = 2;
        function bar() {
            
        }
        var c = 3;
    }
    bar(); // Uncaught ReferenceError: bar is not defined
    console.log(a); // Uncaught ReferenceError: a is not defined

    函数作用域含义:属于这个函数的全部变量都可以在整个函数的范围内使用及复用(在嵌套的作用域中也可以使用)。

    隐藏内部实现

    函数的认知

    可简单参考下图:

    为什么要隐藏函数:从最小特权原则中引申出来。也叫最小授权或最小暴露原则。

    最小特权原则:在软件设计中,应最小限度的暴露必要内容,其他内容“隐藏”起来。比如某个模块或对象的API接口。

    这个原则可以延伸到如何选择作用域来包含函数和变量。

    // 在全局作用域中声明 b 和 doSomethingElse
    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; // 无意将 i 重写成 3,3 永远小于 10
            console.log( a + i);
        }
        for (var i = 0; i < 10; i++) { // 此处会死循环
            bar(i * 2);
        }
    }
    foo();

    解决上面死循环的方法有两种:

    • bar 中用 var 声明 i,达到遮蔽效果
    • bar 中的变量换个名字

    但是软件设计可能要求使用同样的标识符名称,所以在这种情况下,使用作用域来“隐藏”内部声明是唯一的最佳选择。

    1. 全局命名空间

    变量冲突的典型例子出现在全局作用域。

    当加载多个第三方库的时候,如果没有隐藏内部私有的变量和函数,就会出现冲突。

    通常做法是在全局作用域声明一个独特变量(通常是对象),这个变量叫做命名空间,所有变量都是它的属性。

    例如:

    var myLibrary = {
        name: 'library',
        doSomething: function() {},
        doSomethingElse: function() {}
    }

    2. 模块管理

    从众多模块管理器中挑一个来使用:

    函数作用域

    虽然用函数将任意代码片段包装起来,可以将内部变量和函数隐藏起来,但是并不理想(会导致额外问题)。

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

    以上代码的问题:

    • 必须显示声明具名函数 foo(污染了所在作用域)
    • 必须显示调用 foo() 才能运行其中代码

    如果函数不需要名称且可以自动运行,就好了。

    var a = 2;
    (function foo() { // 以 (function 这种形式开头声明,函数会被当前函数表达式来处理,而不是函数声明
        var a = 3;
        console.log(a);
    })();
    console.log(a);

    第一个片段中,foo 被绑定在所在的作用域中;第二个片段,foo 被绑定在函数表达式自身的函数中,而不是所在的作用域中,不会污染外部作用域。

    匿名和具名

    函数表达式最熟悉的就是回调函数:

    setTimeout(function() { // 其中 function() {} 叫做匿名函数表达式
        console.log(1);
    }, 1000);

    匿名函数表达式使用起来简单快捷,很多库和工具都倾向鼓励使用这种风格的代码。

    但是也有缺点:

    给函数表达式指定一个名字,可以有效解决以上问题:

    setTimeout(function timeHandler() { // <-- 指定名字 timeHandler
        console.log(1);
    }, 1000);

    立即执行函数表达式

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

    还有另外一种形式:

    var a = 2;
    (function foo() {
        var a = 3;
        console.log(a);
    }()); // <-- 将最后的括号移入最前面的括号中
    console.log(a);

    立即调用函数的进阶用法:

    var a = 2;
    (function foo(global) {
        var a = 3;
        console.log(a); // 3
        console.log(global.a); // 2
    })(window); // <-- 将立即调用函数当做函数调用,并传递参数进去
    console.log(a); // 2

    块作用域

    for (var i = 0; i < 10; i++) { // i 被绑定在全局作用域中
        console.log(i);
    }

    表面上看,JavaScript 没有块作用域。

    with

    with 是块作用域的一个例子(一种形式)。用 with 从对象中创建出的作用域仅在 with 声明中而非外部作用域中有效。

    try/catch

    ES3 中规定,try/catch 的 catch 分句会创建一个块级作用域。

    try {
        throw 'error';
    }
    catch(err) {
        console.log(err); // error 正常执行
    }
    console.log(err); // err is not defined

    let

    let 关键字可以将变量绑定到所在的任意作用域(通常是 {} 中)。

    let 为其声明的变量隐式地藏在了所在的块作用域中。

    var foo = true;
    if (foo) {
       let bar = 1;
       console.log(bar); // 1
    }
    console.log(bar); // bar is not defined

    var foo = true;
    if (foo) {
        { // <-- 显示的块
           let bar = 1;
           console.log(bar); // 1
       }
    }
    console.log(bar); // bar is not defined

    const

    关于 let 和 const, 可以参考阮一峰老师的 《ECMAScript 6 入门》

    有理解的不对的地方,烦请指出,谢谢!!

    注:以上所有的文字、代码都是本人一个字一个字敲上去的,图片也是一张一张画出来的,转载请注明出处,谢谢! 

  • 相关阅读:
    83. Remove Duplicates from Sorted List
    35. Search Insert Position
    96. Unique Binary Search Trees
    94. Binary Tree Inorder Traversal
    117. Populating Next Right Pointers in Each Node II
    116. Populating Next Right Pointers in Each Node
    111. Minimum Depth of Binary Tree
    169. Majority Element
    171. Excel Sheet Column Number
    190. Reverse Bits
  • 原文地址:https://www.cnblogs.com/lwl0812/p/9802491.html
Copyright © 2011-2022 走看看