3

什么是闭包

补充:闭包形成的本质是内层作用域中堆地址暴露,原因是局部作用域中访问了其他局部变量?

最原始定义

闭包(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 会找出不再使用的变量,不再使用意味着这个变量生命周期的结束。
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的活动对象才会销毁

从作用域链理解闭包的形成

  1. 从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。
  2. 从实践角度:以下函数才算是闭包:

i. 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
ii. 在代码中引用了自由变量

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}

var foo = checkscope();
foo();

fContext = {//f函数的执行上下文
    Scope: [AO, checkscopeContext.AO, globalContext.VO],
}

对的,就是因为这个作用域链,f函数在声明的时候压入了上层的变量对象,f 函数依然可以读取到 checkscopeContext.AO 的值,并且如果当 f 函数引用了 checkscopeContext.AO 中的值的时候,即使 checkscopeContext 被销毁了,但是 JavaScript 依然会让 checkscopeContext.AO 活在内存中(和垃圾回收机制有关下文会说),f 函数依然可以通过 f 函数的作用域链找到它,正是因为 JavaScript 做到了这一点,从而实现了闭包这个概念。

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

任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数外部访问这些变量。
私有变量包括函数的参数、局部变量和函数内定义的其他函数。
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 () {
        setTimeout(function () {
        // 这样虽然也是闭包,但没有缓存i的值到作用域
        // 导致1000ms到时,然后还是去外层作用域链找i的值,已经变成了11
            console.log(i);
        }, 1000);
    })();
}

优化写法:

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

ES6写法:

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

for循环里面用let,()会生成一个隐藏作用域,产生多个不同的块级作用域,类似于闭包,如下:

// 伪代码
(let i = 0) {
    setTimeout(function () {
        console.log(i);
    }, 1000);
}

(let i = 1) {
    setTimeout(function () {
        console.log(i);
    }, 1000);
}

(let i = 2) {
    setTimeout(function () {
        console.log(i);
    }, 1000);
};
......
......

联系Static静态变量

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

总结

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


ipromise
584 声望25 粉丝

xxxxxx