JavaScript语言提供了函数作用域,同时JavaScript允许嵌套的函数定义。 有趣的是,内部函数(inner function)的生命周期可能超过父级函数。 这时便会显示出JavaScript的一个特殊现象:闭包。 闭包为面向对象编程提供了另外一种有趣的封装方式,我们无需为私有变量声明private

函数作用域

程序设计语言中的作用域(scope)控制着变量和参数的可见性和生命周期。 它的重要性体现在避免命名冲突和自动内存管理两个方面,极大地减少了程序员的工作。

多数编程语言都拥有块作用域(block scope),由一对大括号限定其中变量的作用域。 比如C++的变量只在块内可见,变量被定义时执行内存申请和构造函数, 控制流退出代码块后内部的变量又被析构和内存回收。

不幸的是JavaScript提供了块语法,却不提供块作用域,而是提供函数作用域。 这意味着参数和变量在函数外部不可见,在函数内部始终可见。

闭包

JavaScript允许在函数中嵌套定义函数,这意味着内部函数(inner function)也可以访问父级函数的上下文(包括参数和变量)。 有趣的是,有时内部函数拥有更长的生命周期。来一个典型的面试题:

function f(){
    for(var i=0; i<3, i++){
        setTimeout(function(){
            console.log(i);
        }, 1000);
    }
}

输出:

333

ifunction f中定义的变量,在传递给setTimeout的函数中,i仍然有效(其实它叫做闭包变量)。 所以1s后console.logi仍然是function f中定义的那个i,这时它的值为3

这意味着内部函数可以访问真正的外部变量,而不是外部变量的副本。 函数可以访问它被创建时的所有上下文变量,这就叫做闭包现象

如果我们希望上述代码输出123,我们需要开启一个子作用域。而JavaScript只提供函数作用域,所以我们需要添加一层函数:

function f(){
    for(var i=0; i<3, i++){
        !function(i){
            setTimeout(function(){
                console.log(i);
            }, 1000);
        }(i);
    }
}

这时输出便是123了。其中的!是为了让JavaScript将这一句解析为表达式,而不是函数声明。 其实任何运算符都可以(+-),但如果没有便是语法错。

变量提升

一般编程语言建议将变量定义推迟,越晚越好。然而在JavaScript中不是这样, 应当将一切变量都在函数体最上方声明。例如:

var p = {name: 'harttle'};
function foo(){
    console.log(p.name);
    var p = {name: 'alice'};
}

上述代码将产生运行时错误:

Uncaught TypeError: Cannot read property 'name' of undefined

这是因为在JavaScript的函数作用域实现中,所有变量声明(var xxx)都会被提升。 就像在函数体刚进入时声明这些变量一样。上述代码相当于:

var p = {name: 'harttle'};
function foo(){
    var p;
    console.log(p.name);
    p = {name: 'alice'};
}

Note:变量声明与初始化是两回事,变量提升指的是声明提升,初始化/赋值并不会提升。

闭包封装

与基于类声明的面向对象语言不通,JavaScript基于原型继承。 JavaScript并未提供public, protected, private等关键字。 而JavaScript的闭包机制则可以完美地提供封装。你只需要:

  1. 将public属性赋值到对象属性。
  2. 将private属性声明为局部变量或内部函数。

例如:

function Counter(){
    var i = 0;
    return {
        count: function(){ i++; }
        get: function(){ return i; }
    }
}
var counter = Counter();
counter.count();
counter.get();      // 1

其中i便是privatecountget便是public。 这里JavaScript有一个设计错误:调用内部函数时this不会正确传递。 解决办法详见JavaScript 方法的4种调用模式一文。

本文采用 知识共享署名 4.0 国际许可协议(CC-BY 4.0)进行许可,转载注明来源即可: https://harttle.land/2016/02/05/js-scope.html。如有疏漏、谬误、侵权请通过评论或 邮件 指出。