JavaScript的作用域与闭包
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
i
是function f
中定义的变量,在传递给setTimeout
的函数中,i
仍然有效(其实它叫做闭包变量)。
所以1s后console.log
时i
仍然是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的闭包机制则可以完美地提供封装。你只需要:
- 将public属性赋值到对象属性。
- 将private属性声明为局部变量或内部函数。
例如:
function Counter(){
var i = 0;
return {
count: function(){ i++; }
get: function(){ return i; }
}
}
var counter = Counter();
counter.count();
counter.get(); // 1
其中i
便是private
,count
与get
便是public
。
这里JavaScript有一个设计错误:调用内部函数时this
不会正确传递。
解决办法详见JavaScript 方法的4种调用模式一文。
本文采用 知识共享署名 4.0 国际许可协议(CC-BY 4.0)进行许可,转载注明来源即可: https://harttle.land/2016/02/05/js-scope.html。如有疏漏、谬误、侵权请通过评论或 邮件 指出。