什么是闭包
我们先来看看闭包的定义:
所谓“闭包”,指的是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。
上面这句话对于初学者来说毫无意义,要弄懂闭包,首先我们来看看js变量的作用域:
- 同大多数语言一样,JS的变量分为全局变量和局部变量。
- 函数内部可以直接调用全局变量
- 外部无法调用函数内的局部变量
ps:如果在函数内部使用未被 var 定义的变量,相当于实际声明了一个全局变量!!!
下面是代码实例:
//函数内部调用全局变量
var value = 'abc';
function show() {
alert(value);
}
//外部直接调用局部变量
function define() {
var innerValue = 'hello';
}
alert(innerValue);//报错 Uncaught ReferenceError: innerValue is not defined
上面关于js的变量作用域的内容应该很容易理解。但似乎对于理解闭包人没有太大的帮助。
看完上边的内容,我仍然有以下的问题
-
什么是闭包(毫无帮助)
-
闭包有什么用?
-
闭包的写法
不要放弃,我们继续往下看 》》》
我们先来看看闭包的写法:
//type 1:最基本的形式,方便理解
function a() {
var value= 'local';
function b() {
alert(value);
}
return b;
}
var c = a();
c();
//type 2:构造方法赋值,
function a(r) {
//do something;
}
a.value='value';
a.prototype.show= function() {
return a.value ;
}
var c = new a(1.0);
alert(c.show());
//type 3:与type 1类似,不过是将被引用的变量和方法都放入了同一个对象obj中
var a= function() {
var obj = new Object();
obj.value = 'value';
obj.show = function() {
return obj.value;
}
return obj;
}
var c = new a();
alert(c.show());
//type 4:声明一个对象,定义对象的属性和方法
var a = new Object();
a.value = 'value';
a.show = function() {
return this.value;
}
alert(a.show());
//type 5:对象的另一种形式
var a = {
value:'value',
show: function() {
return this.value;
}
}
alert(a.show());
//type 6:Function方式
var a= new Function("this.value = 'value';this.show= function() {return this.value;}");
alert((new a()).show());
深入理解闭包
接下来,我们基于type 1的方式,深入的解析一下闭包。首先我们必须掌握以下几个概念:
- 函数的执行环境(excution context)
- 活动对象(call object)
- 作用域(scope)
- 作用域链(scope chain)
概念解释:
执行环境:每调用一个函数(执行函数)时,系统会为该函数创建一个封闭的局部的运行环境,即该函数的执行环境。函数总是在自己的执行环境中执行,如读取局部变量、函数参数、运行内部逻辑。创建执行环境的过程包含了创建函数的 作用域,函数也是在自己的作用域下执行的。从另一个角度说,每个函数执行环境都有一个 作用域链,子函数的 作用域链 包括它的父函数的 作用域链。
函数作用域 分为 词法作用域(lexical scope) 和 动态作用域。
词法作用域:函数定义时的 作用域,即静态作用域。当一个函数定义时,它的 词法作用域 就确定了,词法作用域 说明的是在函数结构的嵌套关系下,函数作用的范围。这个时候也就形成了该函数的 作用域链。作用域链 就是把那些具有嵌套层级关系的 作用域 串联起来。函数的内部[[scope]]属性指向了该 作用域链。
动态作用域:函数调用时的 作用域。当一个函数被调用时,首先将函数内部[[scope]]属性指向了函数的 作用域链,然后会创建一个 调用对象,并用 该调用对象 记录 函数参数 和函数的 局部变量,将其至于 作用域链 的顶部。动态作用域 就是通过把该调用对象加到 作用域链 的顶部来创建的,此时的[[scope]]除了具有定义时的 作用域链,还具有了调用时创建的 调用对象。换句话说,执行环境下的 作用域 等于该函数定义时就确定的 作用域链 加上该函数刚刚创建的 调用对象,从而也形成了新的 作用域链。所以说是 动态的作用域,并且 作用域链 也随之发生了变化。再看这里的 作用域,其实是一个对象链,这些对象就是函数调用时创建的 调用对象,以及它上面一层层的 调用对象 直到最上层的全局对象。
a函数从定义到被调用的过程:
1.当定义函数a的时候,javascript解释器会将函数a的作用域链(scope chain)设置为 定义a时a所在的环境,如果a是一个全局函数,则a的作用域链中只有window对象;
2.当执行函数a的时候,a会进入相应的 执行环境(excution context) ;
3.在创建 执行环境 的过程中,首先会为a添加一个scope属性,即a的 作用域,这个值为第1步中的 作用域链;
4.接下来 执行环境 会创建一个 活动对象(call object) 。活动对象 也是一个拥有属性的对象,但它不具有原型而且 不能通过JavaScript代码直接访问 。创建完 活动对象 后,把 活动对象 添加到a的 作用域链 的最顶端。此时a的 作用域链 包含了两个对象:a的 活动对象 和window对象;
5.接着在 活动对象 上添加一个arguments属性,用来保存调用函数a时所传递的参数;
6.最后,把所有函数a的形参和内部函数b的引用也添加到a的 活动对象 上。这一步中,完成了函数b的定义。
ps:当函数b执行的时候过程也如上边一样,因此,执行时b的 作用域链 包含了3个对象:b的 活动对象 、a的 活动对象 和window对象。
ok,虽然这么两大段东西没有直接讲闭包,不过是不是大家的头脑都清晰了许多?现在再回头看看js里面闭包的定义,是不是也不在一头雾水了?清楚的掌握函数从定义到调用的整个细节,是掌握闭包的基础。所以小伙伴要是还不理解,请认真仔细的将上面的内容多读几遍。
为什么要使用闭包
最后我们来看看,为什么要使用闭包?什么情况需要用到闭包呢?
书面的解释是这样的:在动态执行环境中,数据实时地发生变化,为了保持这些非持久型变量的值,我们用闭包这种载体来存储这些动态数据。(哦,好像很有道理,不过我还是不懂)
闭包的应用举例,包括但不限于以下场景
- setTimeout/setInterval
- 数据缓存(这个有待商榷,看后面的实例,似乎并不需要用到闭包)
- 对象封装(模拟面向对象的代码风格)
- 回调函数(callback) //无实例
- 事件句柄(event handle) //无实例
ps:无实例表示技师还没理解透彻,,以后再补上。
代码示例:
1.setTimeout
var arr = [4, 5, 6, 8, 7, 9, 3, 2, 1, 0];
var $ = function(id) {
return document.getElementById(id);
}
var Sort = {
Insert: function() {
for (var i = 1; i < arr.length; i++) {
for (var j = 0; j < i; j++) {
if (arr[i] < arr[j]) {
arr[i] = [arr[j], arr[j] = arr[i]][0];
}
}
//非闭包,无法保存排序过程,每次输出都是最终结果
setTimeout(function() {
$("proc").innerHTML += arr + "<br/>";
}, i * 500);
//闭包写法
/* //临时变量方式
setTimeout((function() {
var m = [];
for (var j = 0; j < arr.length; j++) {
m[j] = arr[j];
}
return function() {
$("proc").innerHTML += m + "<br>";
}
})(), i * 500);*/
//or参数方式
/*
setTimeout((function(m) {
return function() {
$("proc").innerHTML += m + "<br>";
}
})(arr.join(",")), i * 500);
*/
}
return arr;
}
}
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<div>
v ar a = [4, 5, 6, 8, 7, 9, 3, 2, 1, 0];</div>
<div>
<input type="button" value="插入排序" onclick="Sort.Insert();" />
</div>
Proc:
<div id="proc">
</div>
</body>
</html>
2.数据缓存
var CachedBox = (function() {
var cache = {}
, catchNameArr = []
, catchMax = 10000;
return {
getCatch: function(name) {
if (name in cache) {
return cache[name];
}
var value = name;
cache[name] = value;
catchNameArr.push(name);
this.clearOldCatch();
return value;
},
clearOldCatch: function() {
if (catchNameArr.length > catchMax) {
delete cache[catchNameArr.shift()];
}
}
};
});
var box = new CachedBox();
//字段不在缓存中,需要重新创建value变量
var name = box.getCatch('cache');
alert(name);
//直接读取缓存数据
var nameAgain = box.getCatch('cache');
alert(nameAgain);
3.封装与面向对象模拟
//封装
var person = function(){
//变量作用域为函数内部,外部无法访问
var name = "default";
return {
getName : function(){
return name;
},
setName : function(newName){
name = newName;
}
}
}();
console.log(person.name); //直接访问,结果为undefined
console.log(person.getName());
console.log(person.setName("abruzzi"));
console.log(person.getName());
//模拟面向对象
function Person() {
var name = "default";
return {
getName : function(){
return name;
},
setName : function(newName){
name = newName;
}
}
}
var jack = new Person();
//do something;
另外,从阮一峰大神博客看到的一个思考题:
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
return this.name;
};
}
};
alert(object.getNameFunc()());
大家可以自己思考一下,预测一下结果,自己放到浏览器里运行一下,看是否预测正确,是否真的清楚结果是怎么得来的(没错,这道题集合了this和闭包)。我看阮大神博客下面的评论,貌似没有一个和我预期的一致,告诉我你的答案,欢迎一起交流学习。