Javascript的闭包

对js的广大初学者来说,闭包绝对是个难点。而且经常出现今天感觉懂了,明天就又不懂了的情况。本文就尝试从我自己的学习体会出发,尝试把这个概念讲清楚。
简单来说,闭包是指有权访问另一个函数作用域中的变量的函数
下面这个函数是一个根据初始值自加的函数。

function count(init) {

    return function() {
        init++;
        return init;
    }
}

var f1 = count(1);
console.log(f1());  //2
console.log(f1());  //3

var f2 = count(11);
console.log(f2());  //12
console.log(f2());  //13

上面就是一个闭包的例子。count函数在执行完之后返回了内部匿名函数,并赋值给f1和f2,f1和f2依然可以访问count函数中init变量,f1和f2就是两个闭包。
要搞清楚其中的细节,我们就必须理解f1和f2在第一次调用的时候到底发生了什么。我们首先来看两个基本观念:执行环境及作用域。

执行环境及作用域

执行环境

执行环境(execution context,有时直接简称为“环境”)是ECMAScirpt中最为重要的一个概念,用来描述js代码执行的抽象概念。执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。换句话说,所有的js都是在某个执行环境中运行的,我们可以把执行环境想成一个执行js代码的盒子。每个执行环境都有一个与之关联的变量对象(variable object),环境中定义的所有变量和函数都保存在这个对象中。
全局执行环境是最外围的一个执行环境,根据ECMAScript实现所在的宿主环境的不同,表示执行环境的对象也不一样。在Web浏览器中,全局执行环境被认为是window对象,因此所有全局变量和函数都是作为window对象的属性和方法创建的。某个执行环境的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁。
每个函数都有自己的执行环境。当执行流进入一个函数时,函数的环境就会被推入到环境栈中。而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。

作用域链

当js代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain)。作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端, 始终是当前执行代码所在环境的变量对象. 如果这个环境是一个函数, 则将其活动对象(activation object)作为变量对象. 活动对象在最开始时只包含一个变量, 即arguments对象(这个对象在全局环境中是不存在的). 作用域链中的下一个变量对象来自包含(外部)环境, 而再下一个变量对象则来自下一个包含环境. 这样一直延续到全局执行环境.
标识符解析是沿着作用域链一级一级地搜索标识符的过程. 搜索过程始终从作用域链的前端开始, 然后逐级地向后回溯, 直至找到标识符为止(如果找不到标识符, 通常导致错误发生)

闭包

我们再来看看我们的demo

function count(init) {

    return function() {
        init++;
        return init;
    }
}

var f1 = count(1);
console.log(f1());  //2
console.log(f1());  //3

f1之所以还能访问 变量 init, 是因为f1函数的作用域链包含 count函数的作用域.
下面是最关键的部分:

  1. 在创建count()函数时,会创建一个预先包含全局变量对象的作用域链,这个作用域链被保存在内部的[[Scope]]属性中。
  2. 当调用count()函数时,会为函数创建一个执行环境,然后通过复制函数的[[Scope]]属性中的对象构建起执行环境的作用域链. 此后, count()函数的活动对象被创建, 并被推入到执行环境作用域链的前端.
  3. 在count()函数内部的匿名函数会将count()函数的执行环境的作用域链初始化成自己的作用域链中. 这样匿名函数就可以访问count()函数中的所有变量了.
  4. 当count()函数中的匿名函数最终返回并赋值给f1, f1的作用域链就包含全局变量对象和count()函数的活动对象, 所以count()函数的活动对象不会被销毁. 换句话说, count()函数执行完毕后, count()函数的执行环境被销毁, 但是count()函数的活动对象直到f1被销毁后, 才会被销毁.

到这里我们就明白了, 只要你在一个函数内部定义了另一个函数, 闭包就产生了.

this对象

在闭包中使用this对象会遇到一些问题. 我们知道this对象指向了当前代码的执行环境. 也就是说, 在全局环境中this等于window(浏览器环境), 当被当做某个对象的方法调用时, this指向的就是那个方法.

当然, 也可以通过apply()和call()改变函数的执行环境

我们看一下下面的例子:

var name = "The Window";

var object = {
    name : "My Object",

    getNameFunc : function () {
        return function () {
            return this.name;
        };
    }
};

console.log(object.getNameFunc()());

这时候return回来的是"The Window", 而不是"My Object"
我们分解一下来看:

  1. object.getNameFunc()执行时, getNameFunc()是作为object的方法执行的, this指向object, 然后返回一个匿名函数.
  2. 这个匿名函数在调用的时候, 实际上是在全局环境中执行的, 所以this指向全局环境, 返回this.name就是"The Window"

如果我们想返回"My Object"该咋办? 那我们就得想着怎么把第一步中的this传到第二步的匿名函数中.

    getNameFunc : function () {
        var that = this;
        return function () {
            return that.name;
        };
    }

在定义匿名函数前, 我们把this保存在that变量中, 这样闭包也可以访问that变量.

模仿块级作用域

我们知道Javascript中没有块级作用域, 也就是定义块中变量, 它的作用域是当前函数, 和块没有关系. 我们可以利用函数的作用域来模仿块级作用域.

!function() {
    var i = 10;
    console.log(i); //10
}();

console.log(i+1);   //i is not defined

我们创建了一个函数并立即调用它, 这样其中的代码执行了, 而且因为函数执行完毕, 它的执行环境和其中的变量对象都会被销毁, 所以下面的代码提示i is not defined

封装

面向对象的三大基石之一就是封装. 封装简单来说就是只公开代码单元的对外接口, 而隐藏内部的具体实现.
Javascript是面向对象的语言, 那它如何实现封装呢? 我们知道Javascript中没有私有成员的概念, 所有对象的属性都是公开的. 但是呢, Javascript有私有变量的概念, 函数内部的变量外部是无法访问的. 这里, 我们就可以利用闭包来完成封装.

function Account() {
    var balance = 0;
    function save(money){
        balance += money;
        query();
    }

    function draw(money){
        if(money > balance){
            balance = 0;
        }
        else{
            balance -= money;
        }
        query();
    }
    
    function query(){
        console.log("Your balance is " + balance);
    }

    return {
        Save : function(money){
            save(money);
        },
        Draw : function(money){
            draw(money);
        }
    }
}

var acount = new Account();

acount.Save(10);
acount.Draw(5);

acount.save(10);    //save is not a function
console.log(acount.balance);    //undefined

例子是个银行账户对象, 对外公开了存钱和取钱两种操作. 这里用工厂模式来创建对象, 用构造函数也是同样的道理. 我们把有权访问私有变量和方法的公有方法成为特权方法(Save和Draw方法)

呼呼, 好像我想说的都说完了, 下面开始一分钟满分作文时间, 来回顾一下我们都学到了什么:

  • 当在函数内部定义了其他函数时, 就创建了闭包. 闭包有权访问函数内部的所有变量.
    -闭包的作用域链, 包含着自己的作用域, 包含函数的作用域和全局的作用域
    -通常, 函数的作用域和变量会在函数调用结束后销毁.
    -但是, 当函数返回了闭包时, 函数的作用域会一直保存直到闭包不存在为止
  • 创建并立即调用函数可以模仿块级作用域
  • 闭包可以实现封装

你可能感兴趣的:(Javascript的闭包)