概要:本篇博客主要介绍了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. jquery 分页控件2

    jquery 分页控件(二) 上一章主要是关于分页控件的原理,代码也没有重构.在这一章会附上小插件的下载链接,插件主要就是重构逻辑部分,具体可以下载源文件看下,源代码也有注释.为了测试这个插件是能用的 ...

  2. SQLSERVER利用FOR XML PATH实现分组拼接字符串

    首先看一下数据结构表 IF(OBJECT_ID('tempdb..#tProduct')IS NOT NULL) DROP TABLE #tProduct SELECT * INTO #tProduc ...

  3. CSS学习小记

    搜狗主页页面CSS学习小记 1.边框的处理   要形成上图所示的布局效果,即,点选后,导航下面的边框不显示而其他的边框形成平滑的形状.相对于把导航的下面边框取消然后用空白覆盖掉下面搜索栏的边框比较而言 ...

  4. Bottle GET method. Request

    python bottle framework #!/usr/bin/python # -*- coding utf-8 -*- from bottle import route, run, debu ...

  5. Linux kernel中网络设备的管理

    kernel中使用net_device结构来描述网络设备,这个结构是网络驱动及接口层中最重要的结构.该结构不仅描述了接口方面的信息,还包括硬件信息,致使该结构很大很复杂.通过这个结构,内核在底层的网络 ...

  6. 使用Android网络编程实现简易聊天室

    在Java中我们可以利用socket编程实现聊天室,在Android中也一样,因为Android完全支持JDK本身的TCP.UDP网络通信API.我们可以使用ServerSocket.Socket来建 ...

  7. Java7新特性

    ① 新增了switch对字符串的支持,也就是说可以在switch之后直接使用字符串来进行判断,语法基本与Java7之前支持的语法一样. ② 对数值字面量的增强支持,首先是可以在源代码中直接使用二进制数 ...

  8. 用curl自动登录HTTPS站点

    前文http://blog.csdn.net/sheismylife/article/details/9237925 演示了如何手动的通过运行curl命令登录HTTPS站点,然后获取cookie, 再 ...

  9. 使用JDK中的安全包对数据进行加解密

    本文以使用DES对称加密算法为例使用jdk对数据进行加密解密. 首先需要了解Provider类,它是jdk引入的密码服务提供者概念,实现了Java安全性的一部分或者全部.Provider 可能实现的服 ...

  10. unix网络io模型

    阻塞I/O(bloking I/O) 阻塞IO的特点就是在IO执行的两个阶段(recvfrom和数据从内核空间转移到用户空间)都被block了 非阻塞I/O(non-bloking I/O)   非阻 ...