第五章 闭包
闭包是如何工作的
闭包是一个函数的创建时允许该自身函数访问并操作该自身函数吱哇IDE变量时所创建的作用域。闭包可以让函数访问所有的变量和函数,只要这些变量和函数存在于该函数声明时的作用域内就行。声明的函数在后续的时候都可以被调用,即便是声明时的作用域消失之后。
一个简单的闭包:
var outerValue = 'ninja';
var later;
funtion outerFunction(){
var innerValue = 'samurai';
function innerFunction(){
/**/
}
later = innerFunction; // 将内部函数引用到later变量上
}
// 调用外部函数,将会声明内部函数,并将内部函数赋值给later变量
outerFunction();
later(); // 通过later调用内部函数,不能直接调用内部函数
在外部函数中声明innerFunction()
的时候,不仅声明了函数,还创建了一个闭包,该闭包不仅包含函数声明,还包含了函数声明的那一时刻点上该作用域中的所有变量。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>闭包可以访问到什么内容</title>
<style>
/*定义结果样式*/
li.pass{color: green;}
li.fail{color: red;text-decoration: line-through;}
</style>
</head>
<body>
<!--显示测试结果-->
<ul id="results"></ul>
<script>
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');
// 定义assert方法
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? "pass" : "fail";
li.appendChild(document.createTextNode(desc));
document.getElementById("results").appendChild(li);
}
</script>
</body>
</html>
测试结果说明了三个关于闭包的概念:
- 内部函数的参数是包含在闭包中的。
- 作用域之外的所有变量,即便是函数声明之后的那些声明,也都包含在闭包中。
- 相同的作用域内,尚未声明的变量不能进行提前引用。
重要的是,这些结构不是可以轻易看到的,这种方式在存储和引用信息方面会直接影响性能。每个通过闭包进行信息访问的函数都有一个“锁链”,可以在它上面附加任何信息。使用闭包时,闭包里的信息会一直保存在内存里,直到这些信息确保不再被使用,或页面卸载时,JavaScript引擎才能清理这些信息。
使用闭包
闭包的一种常见用法是封装一些信息作为“私有变量”,限制这些变量的作用域。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>闭包可以访问到什么内容</title>
<style>
li.pass{color: green;}
li.fail{color: red;text-decoration: line-through;}
</style>
</head>
<body>
<ul id="results"></ul>
<script>
// 定义一个Ninja构造器
function Ninja(){
// 私有变量,不能直接被外部访问
var feints = 0;
// 创建一个访问feints计数的方法
this.getFeints = function(){
return feints;
};
this.feint = function(){
feints++;
};
}
var ninja = new Ninja();
ninja.feint();
assert(ninja.getFeits() == 1, "We're able to access the internal feint count.");
assert(ninja.feints === undefined, "And the private data is inaccessible to us")
// 定义assert方法
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? "pass" : "fail";
li.appendChild(document.createTextNode(desc));
document.getElementById("results").appendChild(li);
}
</script>
</body>
</html>
在计时器间隔回调闭包,创建动画:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>在计时器间隔回调闭包</title>
<style>
#box{
position: absolute;
}
</style>
</head>
<body>
<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);
}
},100);
}
animateIt('box');
</script>
</body>
</html>
没有闭包,同时做多件事的时候,无论是事件处理,还是动画,甚至是Ajax请求,都将是极其困难的。闭包不是在创建那一时刻点的状态快照,而是一个真实的状态封装,只要闭包存在,就可以对其进行修改。
绑定函数上下文
创建 bind()
方法,使用apply()
调用原始函数,强制将上下文设置成任何对象。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>给事件处理绑定特定的上下文</title>
</head>
<body>
<button id="test">click me</button>
<script>
function bind(context,name){
return function(){
return context[name].apply(context,arguments);
};
}
var button = {
clicked: false,
click: function(){
this.clicked = true;
if(button.clicked) alert("The button has been clicked");
console.log(this);
}
};
var elem = document.getElementById('test');
// 如果不使用bind,this指向的是<button>标签
// elem.addEventListener("click",button.click,false);
elem.addEventListener("click",bind(button,'click'),false);
</script>
</body>
</html>
自 JavaScript 1.8.5起,已有原生
bind()
方法。
偏应用函数
偏应用函数返回一个含有预处理参数的新函数,以便后期可以调用。
这种在一个函数中首先填充几个参数,然后再返回一个新函数的技术称为柯里化。
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.leng; i++){
if(args[i] === undefined){
args[i] = arguments[arg++];
}
}
return fn.apply(this,args);
};
};
使用闭包实现缓存记忆
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.memoized = 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;
}).memoized();
函数包装
函数包装是一种封装函数逻辑的技巧,用于在单个步骤内重载创建新函数或继承函数。最有价值的场景是,在重载一些已经存在的函数时,同时保持原始函数在被包装后仍然能够有效使用。
// 定义一个包装函数,接收3个参数:方法所属对象、方法名称、执行方法
function wrap(object, method, wrapper) {
// 保存原函数,后面可以通过闭包进行引用
var fn = object[method];
// 创建新函数调用作为包装器传入的函数
return object[method] = function () {
// 使用apply强制将函数上下文设置为object对象,并将其作为参数与原始参数一起传递给原有方法
return wrapper.apply(this, [fn.bind(this)].concat(Array.prototype.slice.call(arrguments)));
};
}
if (Prototype.Browser.Opera) {
wrap(Element.Methods, "readAttribute", function (original, elem, attr) {
return attr == "title" ? elem.title : original(elem, attr);
});
}
即时函数
利用即时函数创建一个临时的作用域,用于存储数据状态。
通过参数限制作用域内的名称
![](./ww.jpg)
<script>
// 定义$表示其它内容
$ = function(){alert('no jquery!');};
(function($){
$('img').on('click',function(event){
$(event.target).addClass('clickedOn');
})
})(jQuery);
</script>
</body>
使用简洁名称让代码保持可读性
(function(v){
Object.extend(v,{
href: v._getAttr,
src: v._getAttr,
type: v._getAttr,
...
});
})(Element.attributeTranslations.read.values);
利用即时函数处理迭代问题
<body>
<div>DIV 1</div>
<div>DIV 2</div>
<div>DIV 3</div>
<script>
var div = document.getElementsByTagName("div");
for(var i=0; i<div.length; i++){
(function(n){
div[n].addEventListener("click",function(){
alert("div #" + n + " was clicked.");
},false);
})(i);
}
</script>
</body>
类库包装
使用闭包和即时函数,让类库尽可能的保持私有,并且可以有选择性的让一些变量暴露到全局命名空间内。
网友评论