JavaScript 函数作用域

函数作用域

每一个作用域都可以作为容器,其中包含了标识符(变量、函数)的定义。这些作用域互相嵌套并且整齐地排列成蜂窝型(没有交集),排列的结构在写代码时定义。

【前提知识】:

【疑问】:究竟是什么生成了一个作用域?只有函数会生成新的作用域吗?JavaScript 中的其他结构能生成作用域吗?

函数中的作用域

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

【优点】:这种设计方案非常有用,能充分利用 JavaScript 变量可以根据需要改变值类型的“动态”特性。

【注意】:如果不细心处理那些可以在整个作用域范围内被访问的变量,可能会带来意想不到的问题。

隐藏内部实现

【前言】:对函数的传统认知就是先声明一个函数,然后再向里面添加代码。但反过来想也可以带来一些启示:从所写的代码中挑选出一个任意的片段,然后用函数声明对它进行包装,实际上就是把这些代码“隐藏”起来。

根据前言可以推断出,开发者可以用函数将一些代码片段包装起来。实际结果就是在代码片段周围创建了一个作用域气泡,也就是说这段代码中的任何声明(变量或函数)都将绑定在这个新创建的包装函数的作用域中,而不是先前所在的作用域中。那么这些变量和函数外部就无法访问(除非内部主动暴露出来),从而实现了“代码隐藏”。

【问】:为什么“隐藏”变量和函数是一个有用的技术?

【答】:有很多原因促成了这种基于作用域的隐藏方法。它们大都是从最小特权原则中引申出来的,也叫最小授权或最小暴露原则。这个原则是指在软件设计中,应该最小限度地暴露必要内容,而将其他内容都“隐藏”起来,比如某个模块或对象的 API 设计。这个原则可以延伸到如何选择作用域来包含变量和函数。如果所有变量和函数都在全局作用域中,当然可以在所有的内部嵌套作用域中访问到它们。但这样会破坏前面提到的最小特权原则,因为可能会暴露过多的变量或函数,而这些变量或函数本应该是私有的。正确的做法是要阻止对这些变量或函数进行访问。

【示例】:

function doSomething(a) {
    b = a + doSomethingElse(a * 2);
    
    console.log(b * 3);
}

function doSomethingElse(a) {
    return a - 1;
}

var b;

doSomething(2); // 15
// 好的做法
function doSomething(a) {
    function doSomethingElse(a) {
        return a - 1;
    }
    
    var b;
    
    b = a + doSomethingElse(a);
    
    console.log(b * 3);
}

doSomething(2); // 15

【解释】:之前的代码,变量 b 和函数 doSomethingElse() 放置在全局作用域中,可能被有意或无意地以非预期的方式使用。合理的设计会将这些私有的具体内容隐藏在 doSomething() 内部。

规避冲突

“隐藏”作用域中的变量和函数所带来的另一个好处:是可以避免同名标识符之间的冲突,两个标识符可能具有相同的名字但用途却不一样,无意间可能造成命名冲突。冲突会导致变量的值被意外覆盖。

【示例】:

function foo() {

    function bar(a) {
        i = 3;
        console.log(a + i);
    }
    
    for(var i = 0; i < 10; i++) {
        bar(i * 2);
    }
}
foo();

【说明】:bar() 内部的赋值表达式 i = 3 意外地覆盖了声明在 foo() 内部 for 循环中的 i,因此在这个例子中将会导致无限循环。

【解决】:

  1. bar() 内部的赋值操作声明一个本地变量来使用,采用任何名字都可以,var i = 3; 也可以满足这个需求(“遮蔽变量”)。
  2. 将 for 循环中声明的 i 变量“隐藏”起来。
function foo() {

    function bar(a) {
        i = 3;
        console.log(a + i);
    }

    (function() {
        for(var i = 0; i < 10; i++) {
            bar(i * 2);
        }
    })()
}
foo();
1. 全局命名空间

变量冲突的一个典型例子存在于全局作用域中。当程序中加载了多个第三方库时,如果它们没有妥善地将内部私有的函数或变量隐藏起来,就会很容易引发冲突。

这些库通常会在全局作用域中声明一个名字足够独特的变量,通常是一个对象。这个对象被用作库的命名空间,所有需要暴露给外界的功能都会成为这个对象(命名空间)的属性,而不是将自己的标识符暴露在顶级的词法作用域中。

【示例】:

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

使用模块管理器工具。使用这些工具,任何库都无需将标识符加入到全局作用域中,而是通过依赖管理器的机制将库的标识符显式地导入到另外一个特定的作用域中。

显而易见,这些工具并没有能够违反词法作用域规则的“神奇”功能。它们只是利用作用域的规则强制所有标识符都不能注入到共享作用域中,而是保持在私有、无冲突的作用域中,这样就可以有效规避所有的意外冲突。

函数作用域

通过在任意代码片段外部添加包装函数,可以将内部的变量和函数定义“隐藏”起来,外部作用域无法访问包装函数内部的任何内容。

var a = 2;

function foo() { // 添加这一行
    var a = 3;
    
    console.log(a); // 3
} // 以及这一行
foo(); // 以及这一行
console.log(a); // 2

【额外的问题】:

  1. 必须声明一个具名函数 foo(),意味着 foo 这个名称本身“污染”了所在作用域。
  2. 必须显式地通过函数名 foo() 调用这个函数才能运行其中的代码。

【解决的办法】:使用 IIFE。

匿名和具名

函数表达式和函数声明的区别规则

  • 当 js 解析器看到 function 是这一行代码中第一个词,function 被认为是声明。
  • 当 function 作为语句的一部分出现的,都会是表达式。

【区别】:函数声明会将其函数名绑定在函数所在的作用域中。而函数表达式的函数名被绑定在函数表达式自身的函数中而不是所在的作用域中。换句话说,(function() {..}) 作为函数表达式,意味着函数只能在 .. 所代表的位置中被访问,外部作用域则不行。

【注意】:函数表达式可以匿名,而函数声明不可以是匿名,这在 JavaScript 的语法中是非法的。

【优点】:

  1. 书写简单快捷。
  2. 很多库和工具也倾向鼓励使用这种风格的代码。

【缺点】:

  1. 匿名函数在栈追踪中不会显示出有意义的函数名,使得调试很困难。
  2. 如果没有函数名,当函数需要引用自身时只能使用已经过期的 arguments.callee 引用,比如在递归中。另一个函数引用自身的例子是在事件触发后事件监听器需要解绑自身。
  3. 匿名函数省略了对于代码可读性/可理解性很重要的函数名。一个描述性的名称可以让代码不言自明。

【建议】:使用行内函数表达式

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

【解释】:匿名和具名之间的区别并不会对函数有任何影响。给函数表达式指定一个函数名可以有效地解决匿名函数表达式存在的缺点。因此给函数表达式命名是一个最佳实践。

立即执行函数表达式

在函数的外围包裹一对()括号,将其转换为函数表达式。通过在末尾加上另外一个()可以立即执行这个函数。也就是说第一对()将函数转变为函数表达式,第二对()执行该函数。

var a = 2;

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

console.log(a);

这种模式被称为 IIFE,代表立即执行函数表达式(Immediately Invoked Function Expression)。函数名对 IIFE 不是必须的,最常见的用法是使用匿名函数表达式。虽然使用具名函数的 IIFE 不常见,但它具有上述匿名函数表达式的所有优势,因此也是一个值得推广的实践。

【形式】:功能上并无区别。

// 方式一
(function() {
    // ...
})()
// 方式二
(function() {
    // ...
}())

【进阶用法】:将 IIFE 当作函数调用并传递参数进去。

var a = 2;
(function IIFE(global) {
    var a = 3;
    console.log(a); // 3
    console.log(global.a); // 2
})(window);
console.log(a); // 2

【其他用法】:

  1. 解决 undefined 标识符的默认值被错误覆盖导致的异常(不常见)。
(function IIFE(undefined) {
    var a;
    if(a === undefined) {
        console.log("Undefined is safe here!");
    }
})();
  1. 倒置代码的运行顺序,将需要运行的函数放在第二位,在 IIFE 被执行之后当作参数传递进去。
var a = 2;
(function IIFE(def) {
    def(window);
})(function def(global) {
    var a = 3;
    console.log(a);
    console.log(global.a);
});

小结

  • 函数是 JavaScript 中最常见的作用域单元。本质上,声明在一个函数内部的变量或函数会在所处的作用域中“隐藏”起来,这是有意为之的良好软件的设计原则。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,290评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,107评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,872评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,415评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,453评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,784评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,927评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,691评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,137评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,472评论 2 326
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,622评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,289评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,887评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,741评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,977评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,316评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,490评论 2 348

推荐阅读更多精彩内容