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方法)

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

  • 当在函数内部定义了其他函数时, 就创建了闭包. 闭包有权访问函数内部的所有变量.
    -闭包的作用域链, 包含着自己的作用域, 包含函数的作用域和全局的作用域
    -通常, 函数的作用域和变量会在函数调用结束后销毁.
    -但是, 当函数返回了闭包时, 函数的作用域会一直保存直到闭包不存在为止
  • 创建并立即调用函数可以模仿块级作用域
  • 闭包可以实现封装
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容