三、函数作用域和块作用域(学习笔记)—— 《你不知道的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 入门》

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

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

原文地址:https://www.cnblogs.com/lwl0812/p/9802491.html