概要:本篇博客主要介绍了JavaScript的闭包

1.闭包的工作原理

  简单地说,闭包就是一个函数在创建时允许该自身函数访问并操作该自身函数之外的变量时所创建的作用域。

例如:

var outerValue = 'ninja';
var later;
function outerFunction(){
var innerValue = 'samurai';
function innerFunction(){
debugger
console.assert(outerValue,"I can see the ninja.");
console.assert(innerValue,"I can see the samurai.");
};
later = innerFunction;
};
outerFunction();
later();

  第一个断言肯定会通过,因为outerValue是在全局作用域内的。在外部函数中声明innerFunction()的时候,不仅是声明了函数,还创建了一个闭包,该闭包不仅包含函数声明,还包含了函数声明的那一时刻点上该作用域中的所有变量。最终,当innerFunction()执行的时候,当时声明的作用域已经消失了,通过闭包,该函数还是能够访问到原始作用域的。

再看一个例子:

var outerValue = 'ninja';
var later;
function outerFunction(){
var innerValue = 'samurai';
function innerFunction(paramValue){
console.assert(outerValue,"Inner can see the ninja.");
console.assert(innerValue,"Inner can see the samurai.");
console.assert(paramValue,"Inner can see the wakizashi.");
console.assert(tooLate,"Inner can see the ronin.");
};
later = innerFunction;
};
console.assert(!tooLate,"outer can't see the ronin");
var tooLate = 'ronin'; outerFunction();
later('wakizashi');

测试结果说明了三个关于闭包的更有趣的概念:

  ● 内部函数的参数是包含在闭包中的。

  ● 作用域之外的所有变量,即便是函数声明之后的那些声明,也都包含在闭包中。

  ● 相同的作用域内,尚未声明的变量不能进行提前引用。

  注意:每个通过闭包进行信息访问的函数都有一个"锁链",如果我们愿意,可以在它上面附加任何信息。使用闭包时,闭包里的信息会一直保存在内存中,直到这些信息确保不再被使用。

2.使用闭包

- 私有变量

  闭包的一种常见用法是封装一些信息作为"私有变量"。

function Ninja(){
var feints = 0;
this.getFeints = function(){
return feints;
};
this.feint = function(){
feints++;
};
};
var ninja = new Ninja();
ninja.feint(); console.assert(ninja.getFeints()==1,
"We're able to access the internal feint count.");
console.assert(ninja.feints===undefined,
"And the private data is inaccessible to us.");

  在上述代码中,我们创建了一个函数构造器,在函数上使用new关键字时,就会创建一个新对象实例,该函数会被调用,并将新对象作为它的上下文,函数会作为该对象的构造器。所以函数内的this就是新实例化的对象。在构造器内,我们定义了一个变量feints用于保存状态。JavaScript的作用域规则显示了它的可访问性只能在构造器内部。要让外部的代码可以访问到该内部变量,我们定义了一个存取方法getFeints(),该方法只能对内部变量进行读取,但不能写入。

  接下来,创建实现方法feint(),以便在一个受控制的方法内控制变量的值。

  建立了构造器之后,使用new操作符进行调用,然后再调用feint()方法。

- 回调与计时器

  另一个使用闭包最常见的情形就是在处理回调或使用计时器的时候。在这两种情况下,函数都是在后期未指定的时间进行异步调用,在这种函数内部,我们经常需要访问外部数据。闭包可以作为一种访问这些数据的很直观的方式,特别是在避免创建全局变量来存储信息时。例如:

<div id = 'testSubject'></div>
<button type = 'button' id = 'testButton'>Go!</button>
<script>
jQuery('#testButton').click(function(){
var elem$ = jQuery('#testSubject');
elem$.html("Loading...");
jQuery.ajax({
url:'test.html',
success: function(html){
console.assert(elem$,"We can see elem$,via the closure for this callback.");
elem$.html(html);
}
})
})
</script>

在计时器间隔回调中使用闭包:

<div id="box"></div>
<script>
function animateIt(elementId){
var elem = document.getElementById(elementId);
var tick = 0;
var timer = setInterval(function(){
if(tick < 100){
elem.style.left = elem.style.top = tick + 'px';
tick++;
}else {
clearInterval(timer);
console.assert(tick == 100,
"Tick accessed via a closure.");
console.assert(elem,
"Element also accessed via a closure.");
console.assert(timer,
"Timer reference also obtained via a closure.");
}
},10);
};
animateIt('box');
</script>

  以上代码的重要作用是:使用一个独立的匿名函数完成特定元素的动画效果。通过闭包,该函数使用三个变量控制动画的过程。

3.绑定函数上下文

  给函数绑定一个特定的上下文:

<button id="test">Click Me!</button>
<script>
var button = {
clicked: false ,
click: function(){
this.clicked = true ;
console.assert(button.clicked , "The button has been clicked" ) ;
};
};
var elem = document.getElementById("test");
elem.addEventListener('click',button.click,false);
</script>

  在本例中有一个button按钮,我们想知道它是否曾经被单击过。为了保持状态信息,我们创建一个名为button的支持对象用于保存按钮的单击状态。在该对象中我们还定义一个方法,该方法将作为一个事件处理程序,在按钮被单击的时候进行触发。该方法将会作为按钮的click事件处理程序,设置clicked属性为true,然后测试按钮的状态是否正确记录在支持对象上。但是上述代码是错误的,因为浏览器的事件处理系统认为函数调用的上下文是事件的目标元素,所以我们将click状态设置在错误的对象上了。修改如下:

function bind(context,name){
return function(){
return context[name].apply(context,arguments);
};
};
var button = {
clicked: false,
click: function(){
this.clicked = true;
console.assert(button.clicked,"The button has been clicked.");
console.log(this);
};
};
var elem = document.getElementById("test");
elem.addEventListener("click",bind(button,"click"),false);

  使用了添加的bind()方法,该方法用于创建并返回一个匿名函数,该函数使用apply()调用了原始函数,以便我们可以强制将上下文设置成我们想要的任何对象。本例中,传递给bind()的第一个参数就是要设置的上下文对象。

4.函数重载

- 使用闭包实现缓存记忆

Function.prototype.memoized = function(key){
this._values = this._values || {};
return this._values[key] !== undefined ? this._values[key] : this._values[key] = this.apply(this, arguments);
};
Function.prototype.memoize = function(){
var fn = this;
return function(){
return fn.memoized.apply(fn, arguments);
};
};
var isPrime = (function(num){
var prime = num != 1;
for(var i = 2; i < num; i++){
if(num % i == 0){
prime = false;
break;
};
};
return prime;
}).memoize();
console.assert(isPrime(17),"17 is prime");

  本例中,首先创建了memoized()方法,然后又添加另外一个新方法memoized()。该方法返回了一个包装了原始函数并且调用了memoized()方法的新函数,这样,它返回的始终是原始函数的缓存记忆版本。这使调用者无需再调用memoized()。

- 函数包装

  函数包装是一种封装函数逻辑的技巧,用于在单个步骤内重载创建新函数或继承函数。最有价值的场景是,在重载一些已经存在的函数时,同时保持原始函数在被包装后仍然能够有效使用。

例如:

function wrap(object,method,wrapper){
var fn = object[method]; return object[method] = function(){
return wrapper.apply(this,[fn.bind(this)].concat(
Array.prototype.slice.call(arguments)));
};
} if (Prototype.Browser.Opera) {
wrap(Element.Methods,"readAttribute",function(original,elem,attr){
return attr == "title" ? elem.title : original(elem,attr);
});
}

  以上代码首先将原有方法保存在变量fn中,稍后我们会在后面会通过匿名函数的闭包来访问它。然后,我们使用一个新的匿名函数来重载该方法。新函数执行了之前传进来的包装器函数wrapper,并传递一个重新构造过的参数列表。在构建这个参数列表时,我们希望第一个参数是我们要重载的原有函数,所以创建了一个数组,其中包含原始函数的引用并将原始参数也追加到该数组中。

5.即时函数

(function(){......})();

  这个简单的构造函数被称之为即时函数,它创建了一个函数实例,第二个圆括号对表示执行该函数,在语句结束之后立即销毁该函数没有任何引用。

- 临时作用域和私有变量

  利用即时函数我们可以创建一个临时的作用域用于存储数据状态。例如:

  创建一个独立的作用域

document.addEventListener("click", ( function(){
var numClicks = 0;
return function(){
alert( ++numClicks );
};
})(), false);

  创建一个即时函数,并返回了一个值作为事件处理程序的一个函数。该即时函数的返回值被传递到了addEventListener()方法。但是,我们创建的内部函数依然可以通过闭包获取numClicks变量的值。

  通过参数限制作用域内的名称

  我们也可以像其他普通函数一样,在即时调用的时候向即时函数传递参数,通过形参名称来引用这些参数。示例如下:

(function(what){alert(what);})("Hi there!");

  另一个使用即时函数的更加实际的例子,在页面上同时使用Prototype库和jQuery库时,使用$引用Prototype的时候必须使用jQuery变量来引用jQuery。通过即时函数,我们可以将$重新分配回jQuery。

<img src="../images/ninja-with-pole.png">
<script>
$ = function(){ alert('not jQuery!'); };
(function($){
$('img').on('click',function(event){
$(event.target).addClass('clickedOn');
})
})(jQuery);
</script>

  首先将$定义为其他内容,而不是jQuery,以便其他代码占用$。但是,由于想在代码片段中使用$来引用jQuery,所以定义一个接收单个参数的即时函数。在函数体中,参数$将优先于全局变量$。通过向即使函数传入jQuery参数,函数内部的$就变成了jQuery。

  这种技术常用于在开发插件的时候,由于假设将$引用jQuery是不安全的,所以这些开发人员在开发插件的时候,都将插件代码放在即时函数内部,从而使用$安全引用jQuery。

  使用简洁名称让代码保持可读性

  在代码中我们会频繁的引用一个对象,如果引用长且复杂会让代码变得难以阅读,一个便捷的方法就是把这个引用赋值给一个短小的变量名,像下面这样:

var short = Some.long.reference.to.something;

  但是,在我们使用简洁名称short来代替Some.long.reference.to.something的时候,我们在当前作用域内却引用了一个不必要的新名称。因此可以使用即时函数将短名称引用到一个有限的作用域内。如:

(function(v){
Object.extend(v,{
href: v._getAttr,
src: v._getAttr,
type: v._getAttr,
action: v._getAttrNode,
disabled: v._flag,
checked: v._flag,
readonly: v._flag,
multiple: v._flag,
onload: v._getEv,
onunload: v._getEv,
onclick: v._getEv,
...
});
})(Element.attributeTranslations.read.values);

  在代码中,将Element.attributeTranslations.read.values作为即时函数的第一个参数传递进去,也就意味着参数v就是这个长名称数据结构的引用,并存在于即时函数的作用域内。这种在作用域内创建临时变量的技巧,对没有延迟调用的循环遍历来说尤其有用。

- 循环

  即时函数另一个有用的地方就是可以利用循环和闭包解决一些棘手的问题。如:

<div>DIV 0</div>
<div>DIV 1</div>
<script>
var divs = document.getElementsByTagName("div");
for (var i = 0; i < divs.length; i++) {
divs[i].addEventListener("click",function(){
alert("divs #" +i+ "was clicked.");
},false);
}
</script>

  我们的目的是单击每个<div>元素。以上代码中遇到了一个使用闭包和循环时常见的问题,也就是说函数绑定之后,闭包抓取的变量被更新了。这意味着,每一个绑定的函数处理程序都会一直显示i最后的值。处理方法如下:

<div>DIV 0</div>
<div>DIV 1</div>
<script>
var divs = document.getElementsByTagName("div");
for (var i = 0; i < divs.length; i++) (function(n){
divs[n].addEventListener("click",function(){
alert("div #" +n+ "was clicked.");
},false);
})(i);
</script>

  通过在for循环内加入即时函数,我们可以将正确的值传递给即时函数,进而让处理程序也得到正确的值。这意味着,在for循环每次迭代的作用域中,i变量都会重新定义,从而给click处理程序的闭包传入我们期望的值。

JavaScript忍者秘籍——闭包的更多相关文章

  1. JavaScript忍者秘籍——运行时代码求值

    1. 代码求值机制 JavaScript中,有很多不同的代码求值机制. ● eval()函数 ● 函数构造器 ● 定时器 ● <script>元素 - 用eval()方法进行求值 作为定义 ...

  2. JavaScript忍者秘籍——函数(下)

    概要:本篇博客主要介绍函数的一些类型以及常见示例 1.匿名函数 使用匿名函数的常见示例: window.onload = function(){ assert(true,'power!'); }; / ...

  3. javascript 忍者秘籍读书笔记

    书名 "学徒"=>"忍者" 性能分析 console.time('sss') console.timeEnd('sss') 函数 函数是第一类对象 通过字 ...

  4. JavaScript忍者秘籍——驯服线程和定时器

    1.定时器和线程 - 设置和清除定时器 JavaScript提供了两种方式,用于创建定时器以及两个相应的清除方法.这些方法都是window对象上的方法. 方法 格式 描述 setTimeout   i ...

  5. JavaScript忍者秘籍——原型

    概要:本篇博客主要介绍JavaScript的原型 1.对象实例化 - 初始化的优先级 初始化操作的优先级如下: ● 通过原型给对象实例添加的属性 ● 在构造器函数内给对象实例添加的属性 在构造器内的绑 ...

  6. JavaScript忍者秘籍——函数(上)

    概要:本篇博客主要介绍了JavaScript的函数. 1.第一型对象 JavaScript的函数可以像对象一样引用,因此我们说函数是第一型对象.除了可以像其他对象类型一样使用外,函数还有一个特殊的功能 ...

  7. 深入理解JavaScript中的函数操作——《JavaScript忍者秘籍》总结

    匿名函数 对于什么是匿名函数,这里就不做过多介绍了.我们需要知道的是,对于JavaScript而言,匿名函数是一个很重要且具有逻辑性的特性.通常,匿名函数的使用情况是:创建一个供以后使用的函数.简单的 ...

  8. javascript 忍者秘籍读书笔记(二)

    闭包的私有变量 function Ninja() { let feints = 0; this.getFeints = function () { return feints }; this.fein ...

  9. 【JavaScript忍者秘籍】定时器

随机推荐

  1. IOS研究院之打开照相机与本地相册选择图片(六)

    原创文章如需转载请注明:转载自雨松MOMO程序研究院本文链接地址:IOS研究院之打开照相机与本地相册选择图片(六) Hello 大家好 IOS的文章好久都木有更新了,今天更新一篇哈. 这篇文章主要学习 ...

  2. struts2讲义----建立一个struts2工程

    建立一个Struts2 工程 Ø 1在MyEclipse中新建web工程 Ø 2在struts-2.2.1.1-all\struts-2.2.1.1解压struts2-blank.war( 最基础的示 ...

  3. Go Revel 学习指南

    Go Revel 学习指南 CONTROLLERS(控制器) Routing(路由)http://www.cnblogs.com/hangxin1940/p/3267065.html Paramete ...

  4. spring.net AOP通知类型

    上篇介绍了spring.net AOP的基本实现,其中有说到通知类型,首先在这里补充解释一下.最后出一个异常通知的实例,因为他的实现和别的通知有些不一样. 1.拦截环绕通知:在Spring中最基础的通 ...

  5. C#操作Kentico cms 中的 content(winform环境)

    前段时间做了个winform程序,去管理kentico网站的content,包括content节点的增删改查,以及相应节点内容的修改.现在将对content的操作方法简单的介绍一下. 我们想要操作ke ...

  6. 在MVC3中使用WebForm

    Mvc和WebForm一直是有争议的两个平台,园子里也有很多人写过这方面的文章,给我印象比较深的是去年的时候看过的两篇文章http://www.cnblogs.com/mikelij/archive/ ...

  7. 设置共享文件夹,samba和chmod到底谁的权限大

    1,必备知识.已经知道的请跳过. 首先科普一下这两个东西:samba和chmod其实是完全不同层面的东西,一个是共享服务协议,一个是权限设置语句.但是他们有一个共同的用途:可以用来实现设置一个共享文件 ...

  8. [C# 开发技巧]实现属于自己的截图工具

    [C# 开发技巧]实现属于自己的截图工具 一.引言 之前一直都是写一些C#基础知识的内容的,然而有些初学者可能看完了这些基础知识之后,会有这样一个疑惑的——我了解了这些基础知识之后,我想做一些工具怎么 ...

  9. 关于default的位置问题:default放在前面

    在linux内核的文件系统中,有这样的一段代码: 473 if (this.name[0] == '.') switch (this.len) { 474 default: 475 break; 47 ...

  10. hdu 1559 最大子矩阵(DP)

    题目链接:点击链接 #include<stdio.h> #include<string.h> #define max(a,b) a>b?a:b int d[1005][1 ...