闭包的形成与变量的作用域以及变量的生存周期密切相关。
1. 什么是闭包?
- 闭包就是函数的局部变量集合,只是这些局部变量在函数返回结果后依然存在
- 闭包就是函数的
堆栈
在函数返回以后并不释放,我们可以理解这些函数堆栈并不是在栈上分配而是在堆上分配。 - 当一个函数内部定义另外一个函数就会产生闭包 一句话总结:指代有权限访问另外一个函数作用域中的变量的函数。
2. 作用域
变量的作用域有两种,全局变量和局部变量。javascript语言的特殊之处就是:
- 处于函数内部可以直接读取全局变量。
- 处于函数外部无法读取到函数内部的局部变量。 变量定义规则:
- 变量声明后,会被添加到所处位置最近的环境中。
- 没有使用var 定义的变量,会直接添加到全局环境。
- 没有使用var进行声明的变量,可以使用delete进行删除
变量提升与函数提升
- 将变量替身到函数顶部(只是提升声明,不会提升值).
- 将函数提升到函数顶部(函数创建三种方式: 函数声明,函数表达式,构造函数,只有函数声明方式能函数提升)
// 变量提升
var a = "global";
(function() {
console.log(a); // undefined. 由自己的作用域查找开始,没找到再往外部作用域查找。在函数作用域的变量对象中(varaible object)a变量,只是还没有被赋值。所以为undefined.
var a = "part";
})();
// 函数提升
function external() {
internal(); // internal , 函数提升
console.log(internalVariable); // undefined, 变量提升
console.log(internalFunc); // undefined, 变量提升
function internal() {
console.log("internal");
}
var internalVariable = "internalVariable";
var internalFunc = function () {
console.log("internalFunc");
}
}
复制代码
3 闭包
开发中,处于某些原因,我们有时候需要得到函数内部的局部变量,在上面的作用域解释中,外部是不能访问内部变量的,因此我们可以通过闭包实现。
function f1(){
var n=999;
nAdd=function(){n+=1}
function f2(){
lert(n);
}
return f2;
}
var result=f1();
result();// 弹出999
复制代码
在这段代码中,result实际上就是闭包f2函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。 为什么会这样呢?原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。 这段代码中另一个值得注意的地方,就是"nAdd=function(){n+=1}"这一行,首先在nAdd前面没有使用var关键字,因此nAdd是一个全局变量,而不是局部变量。其次,nAdd的值是一个匿名函数(anonymous function),而这个匿名函数本身也是一个闭包,所以nAdd相当于是一个setter,可以在函数外部对函数内部的局部变量进行操作。
3.1 注意点
- 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
- 闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
3.2 闭包案例
案例1
来源:http://www.cnblogs.com/zichi/p/5092997.html
function fun(n,o) {
console.log(o)
return {
fun:function(m){
return fun(m,n);
}
};
}
var a = fun(0); a.fun(1); a.fun(2); a.fun(3);//undefined,?,?,?
var b = fun(0).fun(1).fun(2).fun(3);//undefined,?,?,?
var c = fun(0).fun(1); c.fun(2); c.fun(3);//undefined,?,?,?
复制代码
先看第一组执行,fun(0) 后首先打印 undefined,没有问题。之后变量 a 便被赋值为 fun 函数所 return 的对象。这里要重点注意的是参数 n,值为 0,这就是闭包和作用域链
a = {
fun: function(m) {
// 这里的n替换为了外层传入的n值
return fun(m, 0);
}
};
复制代码
接着执行 a.fun(1) a.fun(2) a.fun(3),我们以 a.fun(1) 举例。a.fun(1) 的执行结果,因为没有赋值(其实有个 return value),所以其实就是执行了一遍 fun(m, n),上面说了,n 值为 0,所以控制台输出为 0。后两个输出同理。这里要注意的就是这个 n,因为作用域链,所以 n 能获取值,为 0,因为 n 被变量 a 所引用,所以它一直贮藏在内存中。
第二组,我们拆分了来看,实际可以修改为如下:
var a = fun(0);
var b = a.fun(1);
var c = b.fun(2);
var d = c.fun(3);
复制代码
第一行,打印 undefined,没有问题,a 返回对象,然后执行 a.fun(1),打印 0,这些跟第一次的执行相同。a.fun(1),其实就是执行 fun(m, n),其实就是 fun(1, 0),return 的对象赋值给 b。
var b = {
fun: function(m) {
return fun(m, n); // n=1
}
};
复制代码
类似的结果,唯一不同的是 n 的值变了,这是由 fun() 传入的参数所决定的。接下去,b.fun(2),执行 fun(2, 1),打印出 1,然后将 return 的对象赋值给 c。
var c = {
fun: function(m) {
return fun(m, n); // n=2
}
};
复制代码
最后一步也是类似,所以依次打印 undefined, 0, 1, 2。 第三组的与第二组没什么大的区别,打印结果: undefined, 0, 1, 1
个人认为这道题的 "恶心" 之处多数在于函数中调用函数本身(fun 函数中调用 fun 函数),而引起的思路混乱,其他部分其实跟下面代码类似,归根结底就是被引用的变量会始终存在在内存中。
案例2 - 计算乘积的简单函数
闭包可以帮助把一些不需要暴露在全局的变量封装成私有变量
。
- 设计为闭包方式,cache为函数内部局部变量,外部不能访问
- 将计算规则提取为一个方法calculate
- 返回一个匿名函数
var mult = (function() {
var cache = {};
var calculate = function() {
console.log(arguments);
var temp = 1;
[].forEach.call(arguments, function(value) {
temp *= value;
})
return temp;
}
return function() {
var args = Array.prototype.join.call(arguments, ',');
if (args in cache) {
return cache[args];
}
console.log(arguments);
return cache[args] = calculate.apply(null, arguments); // 等同于cache[args] = calculate(...arguments)
}
})();
mult(1, 2, 3, 4);
复制代码
案例3 - 包装一个对象
过程与数据的结合是形容面向对象中的“对象”时经常使用的表达。对象以方法的形式包含了过程,而闭包则是在过程中以环境的形式包含了数据。通常用面向对象思想能实现的功能,用闭包也能实现。
var user = {
name: 'yezi',
sayHello: function(content) {
console.log(this.name += content);
}
};
user.sayHello('hello')
复制代码
下面我们使用闭包的方式来完成上述功能:
var User = function() {
var name = "yezi";
return {
sayHello: function(content) {
console.log(name += content);
}
}
};
var user = User();
user.sayHello('hello');
复制代码
案例4 - 开关电视
使用对象的方式,来对电视实现开关的命令执行操作。
var TV = {
open: function() {
console.log('open tv');
},
close: function() {
console.log('close tv');
}
};
var OpenCommand = function(receiver) {
this.receiver = receiver;
}
OpenCommand.prototype.excute = function() {
this.receiver.open();
};
OpenCommand.prototype.undo = function() {
this.receiver.close();
};
var command = new OpenCommand(TV);
command.excute(); // open tv
command.undo(); // close tv
复制代码
使用闭包的方式完成:
var TV = {
open: function() {
console.log('open tv');
},
close: function() {
console.log('close tv');
}
};
var openCommand = function(reciever) {
function excute() {
reciever.open();
}
function undo() {
reciever.close();
}
return {
excute: excute,
undo: undo
};
}
var command = openCommand(TV);
command.excute(); // open tv
command.undo(); // close tv
复制代码