JavaScript忍者秘籍——闭包
概要:本篇博客主要介绍了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忍者秘籍——闭包的更多相关文章
- JavaScript忍者秘籍——运行时代码求值
1. 代码求值机制 JavaScript中,有很多不同的代码求值机制. ● eval()函数 ● 函数构造器 ● 定时器 ● <script>元素 - 用eval()方法进行求值 作为定义 ...
- JavaScript忍者秘籍——函数(下)
概要:本篇博客主要介绍函数的一些类型以及常见示例 1.匿名函数 使用匿名函数的常见示例: window.onload = function(){ assert(true,'power!'); }; / ...
- javascript 忍者秘籍读书笔记
书名 "学徒"=>"忍者" 性能分析 console.time('sss') console.timeEnd('sss') 函数 函数是第一类对象 通过字 ...
- JavaScript忍者秘籍——驯服线程和定时器
1.定时器和线程 - 设置和清除定时器 JavaScript提供了两种方式,用于创建定时器以及两个相应的清除方法.这些方法都是window对象上的方法. 方法 格式 描述 setTimeout i ...
- JavaScript忍者秘籍——原型
概要:本篇博客主要介绍JavaScript的原型 1.对象实例化 - 初始化的优先级 初始化操作的优先级如下: ● 通过原型给对象实例添加的属性 ● 在构造器函数内给对象实例添加的属性 在构造器内的绑 ...
- JavaScript忍者秘籍——函数(上)
概要:本篇博客主要介绍了JavaScript的函数. 1.第一型对象 JavaScript的函数可以像对象一样引用,因此我们说函数是第一型对象.除了可以像其他对象类型一样使用外,函数还有一个特殊的功能 ...
- 深入理解JavaScript中的函数操作——《JavaScript忍者秘籍》总结
匿名函数 对于什么是匿名函数,这里就不做过多介绍了.我们需要知道的是,对于JavaScript而言,匿名函数是一个很重要且具有逻辑性的特性.通常,匿名函数的使用情况是:创建一个供以后使用的函数.简单的 ...
- javascript 忍者秘籍读书笔记(二)
闭包的私有变量 function Ninja() { let feints = 0; this.getFeints = function () { return feints }; this.fein ...
- 【JavaScript忍者秘籍】定时器
随机推荐
- [转]Patching the Mach-o Format the Simple and Easy Way
From:http://secureallthethings.blogspot.jp/2014/08/patching-mach-o-format-simple-and-easy.html I'm r ...
- SharePoint RBS 安装(集成Office Web Apps)
前言 本文完全原创,转载请说明出处,希望对大家有用. 本篇博客是个人总结,一方面以便日后查看,另一方面希望能为其他人提供一些便利. 阅读目录 安装RBS 为多个内容数据库开启RBS 正文 目的:在Sh ...
- AsyncTasLoader不进行加载操作的原因及解决方法
使用AsyncTaskLoader加载数据.但是LoadInBackground却不会被回调.这是什么情况?我要怎么解决这个问题?如果你和我一样有这样的疑问.你可以移步至我的blog的这篇文章找到答案 ...
- iOS app 发布错误 ERROR ITMS-90167: "No .app bundles found in the package"
今天iOS 上传 APP 突然发生了这个错误,在排查非证书错误后感到非常奇怪, 因为昨天刚刚上传了另一个APP,一切正常. 仔细回忆了下 昨天和今天唯一的不同就是我升级了 电脑操作系统至 macOS ...
- Struts2更改配置文件struts.xml默认路径
struts2配置文件默认存放路径在/WEB-INF/classes目录下,即将struts.xml放在src的目录下. 但是为了协作开发与方便管理,我们有时需要把struts.xml放到其他位置 s ...
- WebView 实现MiniBrowser
package org.hjw.minibrowser; import android.os.Bundle; import android.app.Activity; import android.v ...
- shell脚本作为保证PHP脚本不挂掉的守护进程实例
前几天开始跑一份数据名单,名单需要提供用户名.是否有手机号.是否有邮箱,用户名单我轻易的获取到了,但是,用户名单有2000w之多,并且去检测用户是否有手机号.是否有邮箱必须得通过一个对外开放的安全接口 ...
- Hadoop 统计文件中某个单词出现的次数
如文件word.txt内容如下: what is you name? my name is zhang san. 要求统计word.txt中出现“is”的次数? 代码如下: PerWordMapper ...
- ajax系列之用jQuery的ajax方法向服务器发出get和post请求
打算写个ajax系列的博文,主要是写给自己看,学习下ajax的相关知识和用法,以更好的在工作中使用ajax. 假设有个网站A,它有一个简单的输入用户名的页面,界面上有两个输入框,第一个输入框包含在一个 ...
- linux 开启防火墙操作
1)在/etc/sysconfig/ 下新建iptables文件,添加如下代码: *filter :INPUT ACCEPT [0:0] :FORWARD ACCEPT [0:0] :OUTPUT ...