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 入门》

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

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

  • 相关阅读:
    c#使用SoundPlayer播放wav格式音频
    c#NAudio 录音功能实现
    c#异步方法调用
    c# 读取文件目录下的信息
    angular笔记_1
    js获取form元素,不使用id
    事物回滚机制
    ckplayer跨域调用
    帝国移动pc站文章
    页面切换导航样式也随之改变
  • 原文地址:https://www.cnblogs.com/lwl0812/p/9802491.html
Copyright © 2011-2022 走看看