函数作用域和闭包

闭包

  引用红皮书 p178 上对闭包的陈述:

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

  有两个要点:

  • 闭包是函数
  • 它可以访问另一个函数的作用域中的变量

  闭包有几个特点:

1. 闭包可以访问当前函数以外的变量

function getOuter() {
    let date = '112';
    function getDate(str) {
        console.log(str + date);    // 访问函数外部的 date
    }

    return getDate('今天是:');
}

getOuter(); // 今天是:112

  在上面的代码中, getDate 是一个闭包,它有一个作用域,但其作用域中并没有变量 date,但它能通过父级作用域找到变量 date

2. 即使外部函数已结束,闭包仍能访问其中定义的变量

function getOuter() {
    let date = '112';
    function getDate(str) {
        console.log(str + date);    // 访问函数外部的 date
    }

    return getDate;
}

let today = getOuter();
today('今天是:');  // 今天是:112
today('明天不是:');  // 明天不是:112

3. 闭包可以更新外部变量的值

function updateCount() {
    let count = 0;

    function setCount(newCount) {
        count = newCount;
        console.log('count: ', count);
    }

    return setCount;
}

let counter = updateCount();
counter(112);   // count: 112
counter(113);   // count: 113

作用域链

  JS 中有一个执行上下文的概念,执行上下文中定义了函数有权访问的数据,每个执行上下文都有一个与之关联的变量对象,函数中定义的所有变量和函数都记录在这个对象中。

  变量对象中有一个作用域链,当访问一个变量时,会首先在当前作用域中查找,如果没有找到,就会顺着其父级作用域一路找上去。作用域链的顶端是全局对象。

  作用域链和原型继承有点相似,不过也有区别:如果查找一个普通对象的属性时,在当前对象和其原型链中都找不到时,会返回 undefined;而查找的属性在作用域链中不存在时会抛出 ReferenceError

全局环境

  有一个 my_script.js 文件,内容如下:

var foo = 1;
var bar = 2;

  执行这段代码,会在全局环境中创建两个变量,此时的变量对象就是全局环境:

js_closure_1

非嵌套的函数

  修改 my_script.js 中的代码如下:

var foo = 1;
var bar = 2;
function myFunc() {
    // 定义局部变量
    var a = 1;
    var b = 2;
    console.log('inside myFunc!');
}
console.log('outside');

myFunc();

  上面的代码中,当 myFunc 被定义时,myFunc标识符就被加到了当前的作用域对象中(在这边就是全局对象),这个标识符所引用的是一个函数对象(function object)。

  函数对象中包含的是这个函数的源代码和其他的相关属性。其中,有一个内部属性 [[scope]] 指向的就是当前的作用域对象。也就是当函数标识符被创建时我们能够直接访问的那个作用域对象(这边就是全局对象)。

js_closure_2.png

  当 myFunc 被调用时,会创建对应的执行上下文,执行上下文中包含着其作用域对象,这个作用域对象中又包含了 myFunc 函数所定义的局部变量已经参数列表(arguments)。它的父级作用域对象就是在运行 myFunc 函数时我们能直接访问的那个作用域对象。

  因此,当 myFunc 运行时,对象之间的关系如下图所示:

scope1

嵌套的函数

  当函数运行结束时,若其没有被其他对象引用,就会被垃圾回收器回收。不过若生成了闭包,即使外表函数调用结束了,函数对象仍会引用它被创建时的作用域对象

function createCounter(initial) {
    var counter = initial;

    function increment(value) {
        counter += value;
    }

    function get() {
        return counter;
    }

    return {
        increment: increment,
        get: get
    };
}

var myCounter = createCounter(100); // 闭包创建
console.log(myCounter.get());   // 100
myCounter.increment(5);
console.log(myCounter.get());   // 105

  当调用了 createCounter(100) 后,对象之间的关系如下(省略了前面蓝色的函数对象,函数对象的标识符指向当前作用域相当于是函数标识符指向的函数对象的 [[scope]] 指向了这个作用域对象):

js_closure_4

  内嵌函数 incrementget 都有指向 createCount(100) scope 的引用,如果 createCounter(100) 没有任何返回值,那么 createCounter(100) scope 不再被引用,就会被垃圾回收器回收。不过由于我们代码中 createCounter(100) 是有返回值的,且我们使用了 myCounter 变量保存了这个返回值,因此对象之间的引用关系变成了下面这样:

js_closure_5

  因此即使 createCounter(100) 已经返回,其作用域仍在,并且只能通过调用 myCounter.increment()myCounter.get() 来直接访问 createCounter(100) 的作用域。

  当 increment()get() 被调用时,新的作用域会被创建,且该作用域的父级作用域对象会是当前可以直接访问的作用域对象。如当调用 myCounter.get() 时,引用关系如下:

js_closure_6

  当执行到 get 函数中的 return counter; 时,在 get() scope 中没有找到对应的标识符,就会沿着作用域链往上找,直到找到变量 counter ,然后返回该变量。

  而若调用了 increment(5),引用关系与 get() 相似,不过因为 increment 带有参数 value,因此引用关系会像这样:

js_closure_6

  当访问 value 时,能在当前作用域中马上找到,但当要访问 counter 时,当前作用域中没找到,于是沿着作用域链往上找,在 createCounter(100) 这个作用域中找到了对应的标识符,然后 increment() 就会修改 counter 的值。

  除此之外,没有其他方式能够修改这个变量。闭包的强大也在于此,能够贮存私有变量。

多个闭包

  再次修改 my_script.js 中的代码:

function createCounter(initial) {
    var counter = initial;

    function increment(value) {
        counter += value;
    }

    function get() {
        return counter;
    }

    return {
        increment: increment,
        get: get
    };
}

var myCounter1 = createCounter(100);
var myCounter2 = createCounter(200);	// 新增代码

  我们使用了同样的函数 createCounter() 创建了两个闭包,这两个闭包的作用域是不同的:

js_closure_7

  这才有了下面的结果:

var a, b;
a = myCounter1.get();   // a 等于 100
b = myCounter2.get();   // b 等于 200
myCounter1.increment(1);
myCounter1.increment(2);
myCounter2.increment(5);
a = myCounter1.get();   // a 等于 103
b = myCounter2.get();   // b 等于 205

REF:https://github.com/dwqs/blog/issues/18


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!

 目录