【JS脚丫系列】重温闭包

206 阅读9分钟

【JS脚丫系列】重温闭包

闭包概念解释:

闭包(也叫词法闭包或者函数闭包)。

在一个函数parent内声明另一个函数child,形成了嵌套。函数child使用了函数parent的参数或变量,那么就形成了闭包。

闭包(closure)是可以访问外部函数作用域中的变量或参数的函数。

此时,包裹的函数称为外部函数。内部的称为内部函数或闭包函数。(吃码小妖自定义:或称为父函数和子函数)。

[闭包wiki](en.wikipedia.org/wiki/Closur…

JS采用词法作用域(lexical scoping),函数的执行依赖于函数作用域,这个作用域是在函数定义时决定的,而不是函数调用时决定的。

词法作用域:词法作用域也叫静态作用域,是指作用域在词法解析阶段就已经确定了,不会改变。 动态作用域:是指作用域在运行时才能确定。

参看下面的例子,引自杨志的回答

var foo=1;

function static(){
    alert(foo);
}

!function(){
    var foo=2;
    
    static();
}();

在js中,会弹出1而非2,因为static的scope在创建时,记录的foo是1。
如果js是动态作用域,那么他应该弹出2

吃码小妖:识别闭包,在词法分析阶段已经确定了。

当外部函数运行的时候,一个闭包就形成了,他由内部函数的代码以及任何内部函数中指向外部函数局部变量的引用组成。

注意事项

01,闭包函数作用域中,使用的外部函数变量不会被立刻销毁回收,所以会占用更多的内存。过度使用闭包会导致性能下降。建议在非常有必要的时候才使用闭包。

02,同一个闭包函数,所访问的外部函数的变量是同一个变量。

03,如果把闭包函数,赋值给不同的变量,那么不同的变量指向的是不同的闭包函数,所使用的外部函数变量是不同的。

04,闭包函数分为定义时,和运行时。只有运行时,才会访问外部函数的变量。

05,在for循环的闭包函数,只有在运行时,才在作用域中寻找变量。for循环会先运行完毕,此时,闭包函数并没有运行。

06,如果在for循环中,使用闭包的自执行函数。那么闭包会使用for循环的变量i(0-*,假设i从0开始)。

07,一个函数里,可以有多个闭包。

匿名自执行函数,可以封装私有变量。不会污染全局作用域。匿名函数中定义的任何变量,都会在执行结束时被销毁。

eval+with(仅了解)

在评论中贺师俊还提到,eval 和 with可以产生动态作用域的效果:

比如 with(o) { console.log(x) } 这个x实际有可能是 o.x 。所以这就不是静态(词法)作用域了。

var x = 0;
void function (code) {
    eval(code);
    console.log(x)
}('var x=1')

不过注意eval在strict模式下被限制了,不再能形成动态作用域了。

为什么闭包函数可以访问外部函数的变量?

因为闭包函数的作用域链包含了外部函数的作用域。

如何创建闭包?

在一个函数类内创建另外一个函数。内部函数使用了外部函数的变量,就形成了闭包。

普通函数的内部函数是闭包函数么?

吃码小妖:不是。


函数第一次被调用时,会发生什么?

当函数第一次被调用时,会创建一个执行环境(execution context)和相应的作用域链,并把作用域链赋值给一个内部属性(即[[Scope]])。

然后,使用this、arguments和其他参数来初始化函数的活动对象(activation object)。

在作用域链中,内部函数的活动对象处于第一位,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位,……直至作为作用域链终点的全局执行环境。

在函数执行过程中,读取和写入变量的值,都需要在作用域链中查找变量。

每次调用JS函数,会为之创建一个新的对象来保存所有的局部变量(函数定义的变量,函数参数。),把这个对象添加到作用域链中。函数体内部的变量都保存在函数作用域内。

我们将作用域链看做一个对象列表,而不是一个栈。

(吃码小妖:栈是一种线性表,仅允许在表的一端进行插入和删除操作。)

当函数返回的时候,就从作用域链中将这个绑定变量的对象删除。

如果这个函数不存在嵌套的函数,也没有其他引用指向这个绑定变量的对象,它就会被当做垃圾回收掉。

(吃码小妖:这个操作由浏览器自动完成)。

如果这个函数有嵌套的函数,每个嵌套的函数都各自对应一个作用域链。

这时:

  • 内部函数,被作为返回值返回,或存储在某处属性中,就是会有一个外部引用指向这个它,那么它就不会被当做垃圾回收,并且它所使用外部变量所在的对象也不会被当做垃圾回收。只有内部函数被销毁后,外部函数的活动对象才会被销毁。

在函数中访问一个变量时,就会从作用域链中查找变量。

一般来讲,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域(全局执行环境的环境对象)。

但是,闭包的情况又有所不同。闭包函数的作用域链上有外部函数的作用域链。所以闭包函数可以访问外部函数的变量。

闭包函数必须返回(return)么,return这个闭包函数?

吃码小妖:不必要返回,只要使用外部函数的变量即可。

代码:

function fn1() {
    var a = 1;
    function fn2() {
        console.log(a);
    }
    fn2();
}
fn1();

如果用不同的变量引用函数中的闭包函数,那么是不同的闭包变量。

简单的例子:

function outter(){
    var x = 0;
    return function(){
        return x++;
    }
}
var a = outter();
console.log(a());
console.log(a());
var b = outter();
console.log(b());
console.log(b());

运行结果为: 0 1 0 1

闭包的用途:

可以创建私有变量。

因为只有闭包函数可以访问外部函数的变量。

因为在闭包内部保持了对外部活动对象的访问,但外部的变量却无法直接访问内部,避免了全局污染;

function setMoyu(){
    var name = "moyu";
    return function(newValue){
        name=newValue;
        console.log(name);
        
    }
}

var setValue = setMoyu();
setValue("world");//world
/*吃码小妖:这时name是私有属性了,只能通过闭包函数设置它*/

闭包的缺点?

  1. 可能导致内存占用过多,因为闭包携带了自身的函数作用域。
  2. 闭包只能取得外部函数中的最后一个值。

作用域:

变量声明如果不使用 var 关键字,那么它就是一个全局变量,即便它在函数内定义。

变量生命周期

全局变量的作用域是全局性的,即在整个JS程序中,全局变量处处都在。

而在函数内部声明的变量,只在函数内部起作用。

这些变量是局部变量,作用域是局部性的;

函数的参数也是局部性的,只在函数内部起作用。

在JS中,所有函数都能访问它们上一层的作用域。

例子:

function compare(value1, value2){
    if (value1 < value2){
        return -1;
    } else if (value1 > value2){
        return 1;
    } else {
        return 0;
    }
}

var result = compare(5, 10);

内存泄漏

由于IE 的JS对象和DOM对象使用不同的垃圾收集方式,因此闭包在IE中会导致内存泄漏的问题,也就是无法销毁驻留在内存中的元素。

事件绑定种的匿名函数也是闭包函数。

如果闭包的作用域链中保存着一个HTML元素,那么就意味着该元素将无法被销毁。

function box() {
    var oDiv = document.getElementById('oDiv'); //oDiv 用完之后一直驻留在内存
    oDiv.onclick = function () {
        alert(oDiv.innerHTML); //这里用oDiv 导致内存泄漏
    };
}
box();

那么在最后应该将oDiv 解除引用来避免内存泄漏。

function box() {
    var oDiv = document.getElementById('oDiv');
    var text = oDiv.innerHTML;
    oDiv.onclick = function () {
        alert(text);
    };
    oDiv = null; //解除引用
}

PS:如果并没有使用解除引用,那么需要等到浏览器关闭才得以释放。

闭包和this和arguments

闭包函数中的this问题

对于某个函数来说,如果函数在全局环境中,this指向window。如果在对象中,就指向这个对象。

而对象中的闭包函数,this指向window。因为闭包并不属于这个对象的属性或方法。

var user = 'The Window';
var obj = {
    user : 'The Object',
    getUserFunction : function () {
        return function () { //闭包不属于obj,里面的this 指向window
            return this.user;
        };
    }
};
alert(obj.getUserFunction()()); //The window
//可以强制指向某个对象
alert(obj.getUserFunction().call(obj)); //The Object
//也可以从上一个作用域中得到对象
getUserFunction : function () {
    var that = this; //从对象的方法里得对象
    return function () {
        return that.user;
    };
}

例子:

var self = this; // 将this保存至一个变量中,以便嵌套的函数能够访问它

绑定arguments的问题与之类似。

arguments并不是一个关键字,但在调用每个函数时都会自动声明它,由于闭包具有自己所绑定的arguments,因此闭包内无法直接访问外部函数的参数数组,除非外部函数将参数数组保存到另外一个变量中:

var outerArguments = arguments;  //保存起来以便嵌套的函数能使用它

在通过call()或apply()改变函数执行环境的情况下,this就会指向其他对象。

例子:

var scope = "global scope";             // 全局变量
function checkscope() {
        var scope = "local scope";      // 局部变量
        function f() { return scope; }  // 在作用域中返回这个值
        return f();
}
checkscope()                            // => "local scope"

checkscope()函数声明了一个局部变量,并定义了一个函数f(),函数f()返回了这个变量的值,最后将函数f()的执行结果返回。

你应当非常清楚为什么调用checkscope()会返回"local scope"。现在我们对这段代码做一点改动。

var scope = "global scope";             // 全局变量
function checkscope() {
        var scope = "local scope";      // 局部变量
        function f() { return scope; }  // 在作用域中返回这个值
        return f;
}
checkscope()()                          // 返回值是什么?//local scope