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忍者秘籍】定时器
随机推荐
- 大IT公司笔试
都是一些非常非常基础的题,是我最近参加各大IT公司笔试后靠记忆记下来的,经过整理献给与我一样参加各大IT校园招聘的同学们,纯考Java基础功底,老手们就不用进来了,免得笑话我们这些未出校门的孩纸们,但 ...
- Objective-C Blocks测试题与解析
Objective-C Blocks测试 你真的理解blocks在objective-c中是如何工作的了吗,做个测试检验一下吧. 所有的测试结果已被以下版本的LLVM验证: Apple clang v ...
- Xml转化为DataTable
/// <summary> /// XML转换为DataTable /// </summary> /// <param name="fileName" ...
- Make Things Move -- Javascript html5版(一)文件目录结构和工具方法准备
从这一篇开始,就来开始我们的make things move之旅吧 在此之前,要知道ActionScript(AS)的语法和JS是不一样的,AS是相对于JS而言更好的支持了面向对象的特性,所以我们可以 ...
- Makefile常用信息查询页
这是博主第一次尝试在博客中使用markdown来写博文,目前感觉还不错.大家也可以尝试尝试. 符号说明 符号 作用 换行符 @ 放在命令前面隐藏命令输出 - 放在命令前面忽略命令错误 : 以来规则 ...
- 开源来自百度商业前端数据可视化团队的超漂亮动态图表--ECharts
开源来自百度商业前端数据可视化团队的超漂亮动态图表--ECharts 本人项目中最近有需要图表的地方,偶然发现一款超级漂亮的动态图标js图表控件,分享给大家,觉得好用的就看一下.更多更漂亮的演示大家可 ...
- Ajax实现xml文件数据插入数据库(二)--- ajax实现与jsp的数据交互。
在上一篇文章中我们成功得到了重新组织后的数据,接下来需要做的便是将数据插入到数据库中了.在与数据库打交道的过程中有一些方法是普遍的,我们将这些通用方法封装到一个DbUtil类中,以便复用,封装好的Db ...
- 【Android LibGDX游戏引擎开发教程】第08期:中文字体的显示和绘制(下)
在上一篇的文章中,我们介绍了Hiero这个非常好用工具的使用,但是LIbgdx的BitmapFont不支持多图,常用汉字 3500个,你总不能用hiero自己做吧,那怎么办呢?这其实微软早就解决这个问 ...
- codeforces 6A. Triangle
A. Triangle time limit per test 2 seconds memory limit per test 64 megabytes input standard input ou ...
- ESLint--定制你的代码规则
简介 ESLint是一个开源的项目,由Nicholas C. Zakas(<JavaScript高级程序设计>作者)于2013年六月创建.它的目标是为JavaScript提供一个完全可配置 ...