阅读 40

设置模式基础 之 3闭包

闭包的形成与变量的作用域以及变量的生存周期密切相关。

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
复制代码
文章分类
前端
文章标签