闭包全面详解

39次阅读

共计 3052 个字符,预计需要花费 8 分钟才能阅读完成。

什么是闭包

最原始定义

闭包(closure),是指函数变量可以保存在函数作用域内,因此看起来是函数将变量“包裹”了起来。

// 根据定义,包含变量的函数就是闭包
function foo() {var a = 0;}
cosole.log(a) 
// Uncaught ReferenceError: a is not defined

《JavaScript 高级程序设计》对闭包定义

闭包是指有权访问另一个函数作用域中的变量的函数

 // 访问上层函数的作用域的内层函数就是闭包
function foo() {
    var a = 2;
    function bar() {console.log(a);
    }
    bar();}
foo();

《JavaScript 权威指南》对闭包定义

函数对象可以通过作用域链相互关联起来,函数体内部变量可以保存在函数作用域内,这就是闭包。

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

《你不知道的 JavaScript》这样描述

当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。

//fn3 就是 fn2 函数本身。执行 fn3 能正常输出 name
// 这不就是 fn2 能记住并访问它所在的词法作用域,而且 fn2 函数的运行还是在当前词法作用域之外了。function fn1() {
    var name = 'iceman';
    function fn2() {console.log(name);
    }
    return fn2;
}
var fn3 = fn1();
fn3();

MDN 上面这么说:

闭包是一种特殊的对象。它由两部分构成:函数,以及创建该函数的环境。环境由闭包创建时在作用域中的任何局部变量组成。
简单说就是指那些能够访问自由变量的函数。

严格来说,闭包需要满足三个条件:

【1】访问所在作用域;
【2】函数嵌套;
【3】在所在作用域外被调用

闭包还和垃圾回收机制有关

Javascript 会找出不再使用的变量,不再使用意味着这个变量生命周期的结束。
Javascript 中存在两种变量——全局变量和局部变量,全部变量的声明周期会一直持续,直到页面卸载而局部变量声明在函数中,它的声明周期从执行函数开始,直到函数执行结束。在这个过程中,局部变量会在堆或栈上被分配相应的空间以存储它们的值,函数执行结束,这些局部变量也不再被使用,它们所占用的空间也就被释放。
但是有一种情况的局部变量不会随着函数的结束而被回收,那就是局部变量被函数外部的变量所使用,其中一种情况就是闭包,因为在函数执行结束后,函数外部的变量依然指向函数内的局部变量,此时的局部变量依然在被使用,所以也就不能够被回收

var scope = 'global scope';
function checkScope() {
    var scope = 'local scope';
    return function() {console.log(scope);
    }
}

var result = checkScope(); 
result();   // local scope checkScope 变量对象中的 scope, 非全局变量 scope

此匿名函数的作用域链包括 checkScope 的活动对象和全局变量对象, 当 checkScope 函数执行完毕后,checkScope 的活动对象并不会被销毁, 因为匿名函数的作用域链还在引用 checkScope 的活动对象, 也就是 checkScope 的执行环境被销毁, 但是其活动对象没有被销毁, 留存在堆内存中, 直到匿名函数销毁后,checkScope 的活动对象才会销毁

闭包的作用 - 模仿块级作用域,封装私有变量

任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数外部访问这些变量。
私有变量包括函数的参数、局部变量和函数内定义的其他函数。

function module() {var arr = [];
    function add(val) {if (typeof val == 'number') {arr.push(val);
        }
    }
    function get(index) {if (index < arr.length) {return arr[index]
        } else {return null;}
    }
    return {
        add: add,
        get: get
    }
}
var mod1 = module();
mod1.add(1);
mod1.add(2);
mod1.add('xxx');
console.log(mod1.get(2));// 外部是无法直接拿到 arr 的只能通过 get 来拿

闭包的作用 - 使变量保存在内存中不被销毁

实例 1 - 计数器

我们来实现一个计数器,每调用一次计数器返回值加一:

var counter = 0;
function add() {return counter += 1;}
add();
add();
add();// 计数器现在为 3 

问题:

  • 全局变量容易被其他代码改变
  • 如果我需要同时用两个计数器,但这种写法只能满足一个使用,另一个还想用的话就要再写个 counter2 函数,再定义一个 counter2 的全局变量。

那我们把 counter 放在 add 函数里面不就好了么?

function add() {
    var counter = 0;
    return counter += 1;
} 
add();
add();
add();// 本意是想输出 3, 但输出的都是 1

所以这样做的话,每次调用 add 函数,counter 的值都要被初始化为 0,还是达不到我们的目的。

使用闭包来写就会解决这些问题

function add() {
    var index = 1;
    function counter() {return index ++;}
    return counter;
}

// test
var addA = add() ;
var addB = add() ;
addA();        // 1
addA();        // 2
addB();        // 1
addB();        // 2

实例 2 - 延时打印

这样打印出来的全部都是 10,原因是 for 循环是同步的会在延时 1000 毫秒的过程中一直执行
等 function 执行的时候变量 i 指向的是同一个内存地址,且值已经变成的 10

for (var i = 1; i <= 10; i++) {setTimeout(function () {console.log(i);
    }, 1000);
}

改进,用自执行的函数创建简单的闭包,让每一次 for 循环的 i 都在不同的内存地址中且不被销毁

for (var i = 1; i <= 10; i++) {(function () {
        var j = i;
        setTimeout(function () {console.log(j);
        }, 1000);
    })();}

优化写法

for (var i = 1; i <= 10; i++) {(function (j) {setTimeout(function () {console.log(j);
        }, 1000);
    })(i);
}

联系 Static 静态变量

闭包的作用主要就是让变量的值始终保持在内存中。
C++ 或 C 语言还有 Java 中都有 static 静态变量也是让变量始终保存在内存中。
这样来看好像闭包好像有点 static 静态变量的意思。

总结

闭包就是子函数可以有权访问父函数的变量、父函数的父函数的变量、一直到全局变量。
归根结底, 就是利用 js 得词法 (静态) 作用域, 即作用域链在函数创建的时候就确定了。
子函数如果不被销毁, 整条作用域链上的变量仍然保存在内存中, 这样就形成了闭包

正文完
 0