理解闭包

闭包

原文链接:http://wwsun.github.io/posts/javascript-closure.html

在JavaScript中,闭包是个常令新手困惑的术语,并且很容易和匿名函数相混淆。一句话来讲, 闭包是指有权访问另一个函数(嵌套函数)作用域中的变量的函数。本文将着重解释JavaScript中的闭包概念, 及其用法。

语法作用域

处于种种原因,有时候我们需要得到函数内部的局部变量。通常情况下,这是无法做到的(函数内部变量属于局部变量), 只有通过变通方法才能实现。我们需要在函数内部再定义一个函数。比如有下面这个例子:

function init() {
    
  var name = "Mozilla"; // name是一个局部变量
        
  // 内部函数,它是一个闭包
  function displayName() {
     console.log(name); // displayName()使用了外部函数中定义的变量    
  }
displayName();    
}

init();

上面的代码中,函数displayName是函数init的内函数,我们发现,可以在displayName中访问init的局部变量,但反过来却不可以。 也就是displayName内部的局部变量对init是不可见的。这就是Javascript语言特有的”链式作用域”结构(chain scope), 子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。

简单的说,内函数displayName()之所以能访问外函数中的name变量是因为JavaScript中的词法作用域的作用: 在avaScript中,变量的作用域是由它在源代码中所处位置决定的(词法),并且嵌套的函数可以访问到其外层作用域中声明的变量。

闭包

既然displayName可以访问init的局部变量,那么只要把displayName返回, 我们不就可以在init的外部读取到它的内部变量了吗!
现在考虑下面这个例子:

function makeFunc() {
  var name = "Mozilla";

  // 内函数可以访问外部函数的局部变量
  function displayName() {
    console.log(name);
  }
  return displayName; // 返回内部函数
}

var myFunc = makeFunc(); // myFunc成为了闭包
myFunc();  // Mozilla

代码的运行结果并没有变,所不同的是,内函数displayName()在执行之前被外函数返回了。

这段代码看起来别扭却能正常运行。通常,函数中的局部变量仅在函数的执行期间可用。 一旦 makeFunc() 执行过后,我们会很合理的认为 name 变量将不再可用。虽然代码运行的没问题,但实际并非如此。

对这一问题的解释就是,myFunc变成成一个闭包了。闭包是一种特殊的对象,它由两部分构成:

函数:displayName()
创建该函数的环境:环境由闭包创建时在作用域中的任何局部变量组成,即局部变量name
你可以简单的理解为:闭包就是能够读取其他函数内部变量的函数。而闭包通常是“定义在函数内部的函数”。 我们可以认为,闭包充当了函数内部和函数外部连接起来的桥梁。

创建闭包的一个常见方式就是在一个函数内部创建另一个函数。

闭包用途

  1. 读取函数内部的变量
  2. 让这些变量的值始终保存在内存中

注意,因为闭包会使得函数中变量都保存在内存中,因此不能滥用闭包,否则会造成内存溢出。

下面是一个更有趣的例子,一个makeAddr函数

function makeAddr(x) {
  return function(y) {
    return x+y;  // 在内函数中访问外函数中的变量x
  };
}

// add5和add10都是闭包
var add5 = makeAddr(5);
var add10 = makeAddr(10);

console.log(add5(2)); // 7
console.log(add10(2)); // 12

使用闭包来仿造私有方法

在Java中你可以声明私有方法,私有方法表示只能在当前类中使调用该方法。 但是JavaScript并没有提供一个直接的方法去声明私有方法,但是可以使用闭包来仿造私有方法。 私有方法的作用不仅仅在于可以有效的限制代码的访问, 也为管理你的全局命名空间提供了一种强有力的方式,使得无关的方法不被公开的暴露出来。
下面来看如何定义可以访问私有函数和变量的公共函数,我们使用闭包来达到这一目的, 这也被称为模块模式:

// 所定义的匿名函数表达式会立即执行,并将返回对象(含有三个方法)赋值给counter
var counter = (function() {
  var privateCounter = 0;

  function changeBy(val) {
    privateCounter += val;
  }

  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  };
})();

console.log(counter.value()); // 0
counter.increment();
counter.increment();
console.log(counter.value()); // 2
counter.decrement();
console.log(counter.value()); // 1

这里有好多细节。在以往的示例中,每个闭包都有它自己的环境;而这次我们只创建了一个环境,为三个函数所共享, 分别是:counter.increment, counter.decrement, counter.value。

被共享的环境是在一个匿名函数的函数体内被创建的,这将会在其被定义后立即执行(立即执行函数 IIFE)。 这个共享环境包含两个私有项:变量privateCounter和函数changeBy, 外界无法直接访问匿名函数内部的这两个私有项。取而代之的是,只能通过访问三个公开的接口函数,也就是被匿名函数返回的三个函数。

这三个公共函数是共享同一个环境(共享相同的privateCounter和changeBy())的闭包, 而它们之所以都可以访问变量privateCounter和函数changeBy,是因为JavaScript词法作用域的作用。

您应该注意到了,我们定义了一个匿名函数用于创建计数器,然后直接调用该函数,并将返回值赋给 counter 变量。 也可以将这个函数保存到另一个变量中,以便创建多个计数器。

// 此时我们没有使用立即执行函数表达式,而是直接定义了一个匿名函数
var counter = function() {
  var privateCounter = 0;

  function changeBy(val) {
    privateCounter += val;
  }

  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  };
};

// 我们可以借助counter创建两个不同的计数器
var counter1 = counter();
var counter2 = counter();

// 每个计数器都有自己独立的环境
counter1.decrement();
counter1.decrement();
counter2.increment();

console.log(counter1.value());  // -2
console.log(counter2.value());  // 1

可以发现,这两个计数器是彼此独立的,它们的环境在函数counter()在调用期间是彼此不同的。 闭包变量privateCounter在每一次包含不同的实例。

利用这种方式使用闭包可以向OOP编程一样提供非常多的好处,尤其是数据隐藏和封装。

立即执行函数 IIFE

通过IIFE这种方式我们可以构造块作用域,通常的模式为:

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

推荐阅读更多精彩内容