闭包的形成与变量的作用域以及变量的生存周期密切相关。
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 tvcommand.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 tvcommand.undo(); // close tv复制代码