熟悉OO语言的程序员,对JavaScript中的闭包Closure可能一时难以理解。本篇介绍下究竟什么是闭包,及一些典型的闭包的应用。
要理解闭包需要先学会3个基本的事实:
function sayHello(){
var s = "Hello world";
function say(words){
return s + words; //say函数允许使用定义在外的变量s
}
return say("Jack");
}
sayHello(); //Hello world Jack
你可能会感到惊讶,say函数虽然定义在sayHello函数内部,但在say函数作用域内,函数外部的变量s应该不可见才对,换句话说应该报错才对。但请结合这个代码例子再多念几遍事实一的标题 ^_^
简单地说就是闭包比创建它们的函数有更长的生命周期
function sayHello(){
var s = "Hello world";
function say(words){
return s + words;
}
return say; //返回函数对象的引用
}
var f = sayHello();
f("Jack"); //Hello world Jack
f("Zhang"); //Hello world Zhang
你可能会感到惊讶,变量f得到say函数的引用这没问题,毕竟函数指针大家已经见怪不怪了。但sayHello函数已经返回,sayHello函数内部的变量s应该被自动销毁,不可见才对。因此通过变量f调用say函数时,say函数内部会认为s是undefined才对。
实际结果已经表明即使外部函数已经返回,内部say函数仍旧能记住变量s的值。
因为JavaScript的函数值包含了比调用它们时执行所需要的代码还要多的信息。JavaScript函数值在内部存储它们可能会引用的定义在其封闭作用域的变量。那些在其所涵盖的作用域内跟踪变量的函数被称为闭包。
say函数就是个闭包,能访问并存储了外部变量s,每次调用say函数都能获得这个变量s。
函数可以引用在其作用域内的任何变量,包括参数和外部变量,利用这点改进函数:
function sayHello(s){
function say(words){
return s+ words;
}
return say;
}
var f = sayHello("Hello world");
f("Jack"); //Hello world Jack
f("Zhang"); //Hello world Zhang
var g = sayHello("Hi");
g("Jack"); //Hi Jack
g("Zhang"); //Hi Zhang
都是自解释代码,一看就能明白。闭包是如此优雅,因此JavaScript还提供了一种更方便的构建闭包的字面量语法,即函数表达式。继续改进:
function sayHello(s){
return function(words){ //返回匿名函数,因为根本不想在局部调用该函数,因此不需要函数名
return s + words;
};
}
感受一下代码之美吧!
实际上,闭包存储的是外部变量的引用,而不是它们的副本。因此对于任何可以访问这些外部变量的闭包都可以进行更新
function box(){
var val = undefined;
return {
set: function(v) { val = v; },
get: function() { return val; },
type: function() { return typeof val; }
};
}
var b = box();
b.type(); //undefined
b.set(100);
b.get(); //100
b.type(); //number
该例子产生了个包含3个闭包(set,get,type)。它们共享val变量。
闭包的应用非常广泛,只举两个例子作抛砖引玉之用:
前一篇文章【JavaScript对象创建模式】http://blog.csdn.net/hongse_zxl/article/details/44595809 里已经介绍了JavaScript中并没有类,只能通过原型对象模拟实现类。普通OO语言的类中有个很重要的概念就是访问权限,即public,protected,private。
那JavaScript中如何实现私有变量呢?有人习惯在属性前加上_下划线,但这只是在命名方法给别人一些建议,希望别人不要修改这些属性,已达到私有变量的目的。当然每个人都应该遵守这些潜规则,但毕竟不是在技术上彻底实现私有,如果有人违反这个约定,开发者也只能两手一摊,无能为力。
如何在技术上实现私有呢?用闭包。
因为对象和闭包有相反的策略:对象的属性会自动暴露,闭包中的变量会自动隐藏。闭包是一种简朴的数据结构,将数据存储到封闭的变量中而不提供对这些变量的直接访问,获取闭包内部结构的唯一方式是该函数显式地提供获取它的途径。
我们利用闭包特性在对象中存储真正的私有数据。不是将数据作为对象的属性来存储,而是在构造函数中以变量的方式来存储它,并将对象的方法转变为引用这些变量的闭包:
function Person(name, age) {
this.sayName = function(){
alert(name);
};
this.sayAge = function(){
alert(age);
};
}
这里sayName和sayAge方法是以变量的方法来引用name和age的,而非以this.name属性和this.age属性方式引用的。现在Person实例根本不包含任何实例属性,因此外部代码无法直接访问Person实例对象的name和age变量。
(当然该方法会导致方法副本的扩散,详见前一篇文章【JavaScript对象创建模式】http://blog.csdn.net/hongse_zxl/article/details/44595809 但为了安全,这代价是值得考虑的)
比如setTimeout(func, time),该方法大家都很熟悉,第一个参数为一个函数对象的引用,第二个参数为时间。作用是在指定时间后执行某方法。但其无法给方法传递参数:
function sayName(n1, n2) {
alert(n1 + n2);
}
sayName("Jack", "Zhang"); //JackZhang
setTimeout(sayName, 1000); //NaN
上面这样用setTimeout通知浏览器1秒之后执行sayName方法,结果显示NaN,这并不意外,因为你并没有为sayName方法提供参数。
可以用闭包来为sayName方法提供参数:
function sayName(n1, n2) {
alert(n1 + n2);
}
function callLater(func, paramA, paramB){
return function(){
func.call(this, paramA, paramB);
};
}
var sayNameLater = callLater(sayName, "Jack", "Zhang");
setTimeout(sayNameLater, 1000); //1秒之后,显示JackZhang
各种注册事件如onClikc,onMouseMove同样可以