30天学习计划 js忍者秘籍 第5章 闭包
2016.9.1
第5章 闭包
5.1 闭包是如何工作的
闭包是一个函数在创建时允许该自身函数访问并操作该自身函数之外的变量时所创建的作用域。换句话说,闭包可以让函数访问所有的变量和函数,只要这些变量和函数存在于该函数声明时的作用域内就行。
示例5.1 一个简单的闭包
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
var outervalue = 'ninja';
function outerfunction(){
assert(outervalue == 'ninja','i can see the ninja')
}
outerfunction();
如上,该函数能够“看到”并访问outervalue变量,这其实就是一个闭包。
示例5.2 不那么简单的闭包
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
var outervalue = 'ninja';
var later;
function outerfunction(){
var innervalue = 'samurai';
function innerfunction(){
assert(outervalue,'I can see the ninja')
assert(innervalue,'I can see the samurai')
}
later = innerfunction;
}
outerfunction();
later();
在外部函数中声明innerfunction()的时候,不仅是声明了函数,还创建了一个闭包,该闭包不仅包含函数声明,还包含了函数声明的那一时刻点上该作用域中的所有变量。
最终当innerfunction()执行的时候,当时声明的作用域已经消失了,通过闭包,该函数还是能够访问到原始的作用域的。
像保护气泡一样,只有innerfunction()函数一直存在,它的闭包就保持该作用域中即将被垃圾回收的变量。
这种“汽泡”,包含了函数及其变量,和函数本身停留在一起。
示例5.3 闭包可以访问到什么内容
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
var outervalue = 'ninja';
var later;
function outerfunction(){
var innervalue = 'samurai';
function innerfunction(paramvalue){
assert(outervalue,'Inner can see the ninja')
assert(innervalue,'Inner can see the samurai')
assert(paramvalue,'Inner can see the wakizashi');
assert(toolate,'Inner can see the ronin')
}
later = innerfunction;
}
assert(!toolate,'outer can\'t see the ronin.');
var toolate = 'ronin';
outerfunction();
later('wakizashi');
测试结果说明了三个关于闭包的更有趣概念:
.内部函数的参数是包含在闭包中的。
.作用域之外的所有变量,即使是函数声明之后的那些声明,也都包含在闭包中。
.相同的作用域内,尚未声明的变量不能进行提前引用。
每个通过闭包进行信息访问的函数都有一个“锁链”,可以在它上面附加任何信息。
使用闭包时,闭包里的信息会一直保存在内存里,直到这些信息确保不再被使用(可以安全进行垃圾回收),或页面卸载时,js引擎才能清理这些信息。
5.2 使用闭包
5.2.1 私有变量
闭包的一种常见用法是封装一些信息作为“私有变量”——也就是说,限制这些变量的作用域。
示例5.4 使用闭包模拟私有变量
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
function Ninja(){
var feints = 0;
this.getFeints = function(){
return feints;
};
this.feint = function(){
feints++;
}
}
var ninja = new Ninja();
ninja.feint();
assert(ninja.getFeints() == 1,'we\'re able to access the internal feint count.');
assert(ninja.feints == undefined,'And the private data is inaccessible to us.');
示例创建一个函数作为构造器。在函数上使用new关键字时,就会创建一个新对象实例,该函数会被调用,并将新对象作为它的上下文,函数会作为该对象的构造器。
在构造器内隐藏变量,使其在外部作用域不可访问,但是可以存在于闭包内。
5.2.2 回调(callback)与计时器(timer)
示例5.5 在Ajax请求的callback里使用闭包
test suite
#results .pass{color:green;}
#results .fail{color:red;}
Go!
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
jQuery('#testButton').click(function(){
var elem$ = jQuery('#testSubject');
elem$.html('loading...');
jQuery.ajax({
url:'test.html',
success:function(html){
assert(elem$,'We can see elem$,via the closure for the callback');
elem$.html(html);
}
})
})
示例5.6 在计时器间隔回调(timer interval callback)中使用闭包
test suite
#results .pass{color:green;}
#results .fail{color:red;}
#box{width:50px;height:50px;line-height:50px;text-align:center;background-color:#ccc;position:absolute;}
box
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
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);
assert(tick == 100,'Tick accessed via a closure.');
assert(elem,'Element also accessed via a closure.');
assert(timer,'Timer reference also obtained via a closure.')
}
},10)
}
animateIt('box');
通过在函数内部定义变量,并依赖闭包,可以将它们在计时器回调函数调用的时候进行使用,这样,每个动画都有自己的私有“气泡”变量了。
通过创建多个闭包,我们一次可以做很多事情。
示例说明,函数在闭包里执行的时候,不仅可以在闭包创建的时刻点上看到这些变量的值,我们还可以对其进行更新。换句话说,闭包不是在创建那一时刻点的状态的快照里,而且是一个真实的状态封装,只要闭包存在,就可以对其进行修改。
2016.9.2
5.3 绑定函数上下文
示例5.7 给函数绑定一个特定的上下文
test suite
#results .pass{color:green;}
#results .fail{color:red;}
#box{width:50px;height:50px;line-height:50px;text-align:center;background-color:#ccc;position:absolute;}
click me
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
var button = {
clicked:false,
click:function(){
this.clicked = true;
assert(button.clicked,'the button has been clicked');
}
}
var elem = document.getElementById('test');
elem.addEventListener('click', button.click,false);
示例测试失败,是因为click函数的上下文不是我们所期待的button对象,而是元素。
将上下文设置为调用事件处理程序时的目标元素,是默认行为。
通过使用匿名函数、apply()和闭包,我们可以强制让特定的函数在调用时都使用特定所需的上下文。
示例5.8 给事件处理程序绑定特定的上下文
test suite
#results .pass{color:green;}
#results .fail{color:red;}
#box{width:50px;height:50px;line-height:50px;text-align:center;background-color:#ccc;position:absolute;}
click me
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
function bind(context,name){
return function(){
return context[name].apply(context,arguments);
}
}
var button = {
clicked:false,
click:function(){
this.clicked = true;
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()的第一个参数就是要设置的上下文对象。上下文和方法名称,通过匿名函数的闭包进行传入,在函数结束时进行调用,而匿名函数闭包则包含了传递给bind()的参数。
接下来,在建立事件处理程序时,我们使用了bind()方法来指定事件处理程序,而不是直接使用button.click。这会让包装的匿名函数成为事件处理程序。当单击按钮时,将调用匿名函数,然后反过来再调用click方法,同时将上下强制设置成button对象。
示例5.9 在Prototype库中,函数bind代码的示例
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
Function.prototype.bind = function(){
var fn = this,args = Array.prototype.slice.call(arguments),
object = args.shift();
return function(){
return fn.apply(object,args.concat(Array.prototype.slice.call(arguments)));
}
}
var myObject = {};
function myFunction(){
return this == myObject;
}
assert(!myFunction(),'context is not set yet');
var aFunction = myFunction.bind(myObject);
assert(aFunction(),'context is set properly')
示例将自身方法作为Function的prototype属性和属性,以便将该方法附加到所有的函数上。
Prototype的bind()方法的潜在目的是通过匿名函数和闭包控制后续执行的上下文。这个重要的区别使apply()和call()对事件处理程序和定时器的回调进行延迟执行特别有帮助。
2016.9.5
5.4 偏应用函数
在一个函数中首先填充几个参数(然后再返回一个新函数)的技术称之为柯里化。
示例5.10 在原生函数上进行分部参数应用
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
String.prototype.csv = String.prototype.split.partial(/,\s*/);
var results = ('Mugan, jin, Fuu').csv();
assert(results[0]=='Mugan' && result[1] == 'Jin' && result[2] == 'Fuu','The text values were split properly')
这个示例目前不能执行,因为需要定义partial()方法
示例5.11 柯里化函数示例(在第一个特定参数中进行填充)
Function.prototype.curry = function(){
var fn = this,
args = Array.prototype.slice.call(arguments);
return function(){
return fn.apply(this,args.concat(Array.prototype.slice.call(arguments)));
}
}
示例5.12 一个更复杂的“分部”函数
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
Function.prototype.curry = function(){
var fn = this,
args = Array.prototype.slice.call(arguments);
return function(){
return fn.apply(this,args.concat(Array.prototype.slice.call(arguments)));
}
}
Function.prototype.partial = function() {
var fn = this, args = Array.prototype.slice.call(arguments);
return function(){
var arg = 0;
for (var i = 0; i < args.length && arg < arguments.length; i++) {
if (args[i] === undefined) {
args[i] = arguments[arg++];
}
}
return fn.apply(this,args)
}
}
var delay = setTimeout.partial(undefined,10);
delay(function(){
assert(true,'a call to this funcetion will be delayed 10 ms')
})
var bindClick = document.body.addEventListener.partial('click',undefined,false);
bindClick(function(){
assert(true,'Click event bound via curried function.')
})
String.prototype.csv = String.prototype.split.partial(/,\s*/);
var results = ("Mugan, Jin, Fuu").csv();
assert(results[0]=='Mugan' && result[1] == 'Jin' && result[2] == 'Fuu','The text values were split properly')
2016.9.6
5.5 函数重载
5.5.1 缓存记忆
缓存记忆是一个让函数具备一种可以记忆它历史被调用时所产生的运算结果的能力的过程。
示例5.13 函数的记忆方法
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
Function.prototype.memoized = function(key){
this._values = this._valess || {};
return this._values[key] !== undefined ? this._values[key] : this._values[key] = this.apply(this,arguments);
}
function isPrime(num){
var prime = num != 1;
for(var i=2; i
if(num%i == 0){
prime = false;
break;
}
}
return prime;
}
assert(isPrime.memoized(5),'The function works; 5 is prime.');
assert(isPrime._values[5],'The answer has been cached.')
通过该方法调用一个函数时,首先检查数据存储对象里是否已经有值了,如果有,则直接返回。否则就开始对值进行计算,并将结果保存在缓存里,以便下次调用的时候可以再使用。
这种方式的缺点是,isPrime()函数的调用者也必须记住要调用memoized()方法才能使用缓存记忆功能。调用者根本不可能记得住。
示例5.14 使用闭包实现的缓存记忆功能
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
Function.prototype.memoize = function(key){
var fn = this;
return function(){
return fn.memoize.apply(fn,arguments)
}
}
var isPrime = (function(num){
var prime = num != 1;
for(var i=2; i
if(num%i == 0){
prime = false;
break;
}
}
return prime;
}).memoize();
assert(isPrime(17),'17 is prime.');
在memoize()方法中,我们构建了一个闭包,通过将上下文复制到一个变量中从而记住需要缓存记忆的原始函数(通过上下文)。
5.5.2 函数包装
函数包装是一种封装函数逻辑的技巧,用于在单个步骤内重载创建新函数或继承函数。最有价值的场景是,在重载一些已经存在的函数时,同时保持原始函数在被包装后仍然能够有效使用。
示例5.15 使用新功能包装旧函数
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
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)
})
}
一个函数可以很安全的被重载,并且同时仍然保留原有的功能。
2016.9.7
5.6 即时函数
(function(){})()
(...)(),第一组圆括号仅仅是用于划定表达式的范围,而第二个圆括号则是一个操作符。
(function(){
statement-1;
statement-2;
})()
这段代码的最终结果是一个执行如下操作的单条语句表达式:
创建一个函数实例,执行该函数,销毁该函数。
5.6.1 临时作用域和私有变量
利用即时函数,我们建立一个有趣的封闭空间来做一些事情。由于函数是立即执行,其内部所有的函数、所有的变量都局限于其内部作用域。我们可以使用即时函数创建一个临时的作用域,用于存储数据状态。
(function(){
var numClicks = 0;
document.addEventListener('click', function(){
alert(numClicks++)
},false)
})()
document.addEventListener('click', (function(){
var numclicks = 0;
return function(){
alert(++numclicks)
}
})(),false)
利用即时函数,我们可以将作用域限制于代码块、子代码块或各级函数中。
1)通过参数限制作用域内的名称
可以在即时函数调用的时候向即时函数传递参数,通过形参名称来引用这些参数。
(function(what){alert(what)})('Hi there!');
示例5.16 在封闭作用域内,强制使用一个名称
$ = function(){alert('not jquery!');};
(function($){
$('img').on('click',function(event){
$(event.target).addClass('clickedon')
})
})(jQuery)
在函数体中,参数$将优先于全局变量$。无论我们向该函数传递什么内容,其都会在函数内被$进行引用。通过向即时函数传入jQuery参数,函数内部的$就变成了jQuery.
$参数会成为函数体内所创建内部函数的闭包的一部分,包括给JQuery的on()方法所传递的事件处理程序。所以,即便事件处理程序在即时函数执行并消失以后很长一段时间才执行,该处理程序还是可以将$引用于jQuery的。
2)使用简洁名称让代码保持可读性
(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)
在上面的示例中,Prototype库正在给对象扩展新的属性和方法。在代码中,Prototype并没有为Element.attributeTranslations.read.values创建临时变量,而是将其作为即时函数的第一个参数传递进去了。参数v就是这个长名称数据结构的引用,并存在于即时函数的作用域内。
这种在作用域内创建临时变量的技巧,对没有延迟调用的循环遍历来说尤其有用。
5.6.2 循环
示例5.17 闭包迭代中的代码没有按预期执行
test suite
div 0
div 1
var divs = document.getElementsByTagName('div');
for(var i=0; i
divs[i].addEventListener('click', function(){
alert('divs #'+i+'was clicked.')
},false)
}
函数绑定之后,闭包抓取的变量i被更新了。
闭包记住的是变量的引用,而不是闭包创建时刻该变量的值。
示例5.18 利用即时函数妥善处理迭代问题
test suite
div 0
div 1
var divs = document.getElementsByTagName('div');
for(var i=0; i
(function(n){
divs[n].addEventListener('click', function(){
alert('divs #'+n+'was clicked.')
},false)
})(i)
}
通过在for循环内加入即时函数,我们可以将正确的值传递给即时函数,进而让处理程序也得到正确的值。
在for循环每次迭代的作用域中,i变量都会重新定义,从而给click处理程序的闭包传入我们期望的值。
5.6.3 类库包装
当我们开发一个类库时,很重要的一点是不希望让一些不必要的变量去污染全局命名空间,尤其是那些临时变量。
闭包和即时函数可以帮助我们让类库尽可能的保持私有,并且可以有选择性的让一些变量暴露到全局命名空间内。jQuery就专注于这一原则,完整封装了它的所有功能,并选择性的将一些变量关联到全局空间。例如:
(function(){
var jQuery = window.jQuery = function(){
//initialize
}
//...
})();
jQuery构造器赋值给了window.jQuery,这样就将其作为一个全局变量了。
处理我们控制之外的代码可能会改变或删除该jQuery变量。为了避免这个问题,我们将其赋值给了一个局部变量jQuery,强制将其保持在即时函数的作用域内。
也就是,不管外部的变量发生了什么变化,在整个jQuery库代码中,我们都可以一直使用jQuery这个名称。
另外一种实现方式:
var jQuery = (function(){
function jQuery(){
//initialize
}
// ...
return jQuery;
})()
这里,我们在匿名函数作用域中定义了一个jQuery函数,它可以自由地存在于该作用域中,然后将其返回,并赋值给一个全局变量,名称同样是jQuery。通常在只输出一个变量的时候,优先使用这种技巧,这样看起来更能体现出赋值的意义。