Javascript中Closure及其相关概念
我相信学过Javascript这门语言的程序员应该都对Closure这个概念有所了解,然而网上以及各种Javascript书籍里面对Closure这个概念的定义有各种说法。我本人觉得很多地方对Closure这个概念的定义都是片面的,目前看到的比较全面准确的定义应该是Wikipedia上面的定义了,但是Wikipedia上面的定义不是很好理解。
我通过网上查阅了些资料后结合Wikipedia的定义,下面给出我自己对Closure这个概念的理解。
要想正确理解闭包必须要先对一些概念有所了解:
非本地变量(non-local variables):
又叫自由变量(free variables),是一个既不在本地作用域也不在全局作用域里的变量,通常存在于嵌套或匿名函数的上下文对象里面。
第一类函数(First-Class Functions):
在编程语言里,如果函数也能够像其他数据类型一样被操纵如在运行时被构造、被赋值给一个变量、能够被当做参数传递或者被其他函数返回,那么这种类型的函数即为第一类函数;通常闭包出现在支持这种类型函数的语言中。如:
var foo = function() {
alert("Hello World!");
};
var bar = function(arg) {
return arg;
};
bar(foo)();
内部函数(Inner Functions):
内部函数也叫做嵌套函数,是一种被定义在另外一个函数体(也叫外部函数)里面的函数。每一次外部函数被调用的时候,就会创建一个内部函数的实例。如:
function outer(info){
//print is inner function
function print(msg){
return "Error:"+msg;
}
return print(info);
}
内部函数有一个非常重要的特性就是它可以隐式的访问外部函数的作用域,这意味着内部函数能够访问外部函数的参数、本地变量等。
词法环境(Lexical environment):
词法环境是一种特殊的内部对象,在某一代码块里面的所有本地函数、函数参数及本地变量都是该内部对象的属性。在浏览器里最顶层代码块里面的词法环境对象是window,而window对象有一个隐含的属性[[scope]]保存上下文作用域,对window来说[[scope]]应该为null,而对最顶层代码块里面定义的函数来说,当函数被创建时的词法环境里面的[[scope]]属性就是window对象。
如下代码所示:
//这个位置的词法环境是window对象
//在代码执行前window={f:function...,a:undefined,g:undefined} 注:这里省略了其他不相关的属性
var a = 5;
function f(arg){//创建f函数时添加隐藏属性f.[[scope]]=window
//调用函数时该函数的词法环境对象为{arg:...}
alert("f:"+arg);
}
var g = function(arg){
alert("g:"+arg);
}
f();//每次调用时创建自己的词法环境,并通过[[scope]]属性形成作用域链
什么是闭包:
如果不了解词法环境这个概念,就很难理解Wikipedia上面对Closure实现的定义:
Closures are typically implemented with a special data structure that contains a pointer to the function code, plus a representation of the function's lexical environment。
翻译出来后就是:闭包就是一个对象(一种特殊的数据结构),它包含一个指向构成该闭包的函数的指针以及在闭包创建时外部函数的词法环境的引用。同时在闭包里面访问的外部函数词法环境里面属性也叫做非本地变量。
什么时候会创建闭包:
当在一个内部函数被暴露到定义它的外部函数的外面时,也就是说如果一个内部函数能够从定义它的函数的外面被访问时,这个时候闭包就被创建了(这里说创建是因为闭包也是一个对象),同时该闭包函数的[[scope]]属性有对外部函数词法环境的引用,所以在外部函数调用结束后,闭包依然能够访问外部函数的词法环境对象。
闭包大部分的时候都是通过函数调用返回一个内部函数的形式创建,那么下面的代码有产生闭包吗?
function Person(name) {
this._name = name;
this.getName = function() {
return this._name;
};
}
var p = new Person();
什么时候使用闭包:
Javascript里面的闭包可以做许多有用的事情,如配置回调函数或者模仿Java语言中的private data等等。
Callback function:
window.addEventListener("load", function() {
var showMessage = getClosure("some message<br />");
window.setInterval(showMessage, 1000);
});
function getClosure(message) {
function showMessage() {
document.getElementById("message").innerHTML += message;
}
return showMessage;
}
Private data:
function Person(name) {
var _name = name;
this.getName = function() {
return _name;
};
}
什么时候不使用闭包或闭包的误用:
对于初学者来说最经典的就是Loop循环:
window.addEventListener("load", function() {
for (var i = 1; i < 4; i++) {
var button = document.getElementById("button" + i);
button.addEventListener("click", function() {
alert("Clicked button " + i);
});
}
});
上面代码的错误非常明显,因为在循环里面的事件绑定回调函数都引用了相同的外部函数词法环境,当循环结束后变量i的值为4,所以当事件触发时,弹出来的肯定都是Clicked button 4这条消息了。
使用闭包同样要注意内存泄露问题,在闭包里面一不小心就会形成循环引用的问题如:
function setHandler() {
var elem = document.getElementById('id')
elem.onclick = function() {
...
}
}
在上面的代码,elem通过onclick引用一个函数,同时在函数里面通过[[scope]]引用到外部函数的词法环境。
如果在构造器里面有方法但没用使用到private data,那么最好将其移至prototype里面去,因为每次实例化该构造器的一个对象都会在this对象中创建相应的函数对象,如:
function Person(name) {
var _name = name;
this.getName = function() {
return _name;
};
this.sayHello = function() {
alert("Hello!");
};
}
//这里sayHello并有使用private data,为了避免每次new Person都创建sayHello函数,那么可以将其移至prototype里面
function Person(name) {
var _name = name;
this.getName = function() {
return _name;
};
}
Person.prototype.sayHello = function() {
alert("Hello!");
};
注意:如果内部函数使用的是new Function()那么它的[[scope]]则不会引用外部函数的词法环境,而是window对象。
window.a = 1
function getFunc() {
var a = 2
var func = new Function('', 'alert(a)')
return func
}
getFunc()() // 1, from window