JavaScript - 作用域和“闭包”

继续一个人自言自语_

今天想聊聊 JavaScript 的作用域,以及“闭包”。当然,仍旧带着我的个人特色。

作用域

JavaScript 据我了解只有一种作用域,叫做“函数作用域”,先看栗子:

var a = "a-out";
(function () {
    console.log(a);
    var a = "a-in";
})();

你觉得上面匿名函数执行后,会输出什么?

实际试下,会发现是undefined,这个我慢慢来说吧。

首先,JavaScript 没有“块级作用域”,不能在花括号中定义“局部变量”。所有的变量的作用域都是函数级的,也就是说,在函数内部任意的地方声明的变量,在函数内的任意位置都可以使用。

当然,例外就是,不在任何函数内部的变量,就成了“全局变量”啦。同样,在函数内部,忘记使用 var 声明就直接使用的变量,也会成为全局变量,所以很多时候会把函数内部要使用的变量都在头部进行显式声明,以尽量避免麻烦。

回过头来看上面的栗子:

  1. 第一行,声明了一个全局变量 a,然后进行了赋值。

  2. 匿名函数的第一行,向控制台输出变量 a 的值,由于函数内部的确声明了 a,所以这里输出内部变量 a 的值。但是对内部变量 a 的赋值在当前行的后面,目前该变量没有值,所以输出的是 undefined

  3. 匿名函数的第二行,声明了内部变量 a,然后进行了赋值。

对于这件事情我的理解:

既然 JavaScript 只有“函数作用域”(我说的),所以脚本解释器会先扫描下函数内部,识别出所有的声明的变量,记录下来。然后,在执行函数的这个阶段,遇到一个“变量名”,就先在函数内部的变量列表里面进行查找,找到了就把这个内部变量的当前值交给当前语句使用(当然如果是赋值语句,则为变量“绑定”了一个新的值)。

那么,如果在函数内部,使用一个变量时,该变量并没有在函数内部声明呢?

我把上面的栗子修改下:

var a = "a-out";
(function () {
    console.log(a);
    // var a = "a-in";
})();

试一下,会发现这里匿名函数打印出的是"a-out"。所以,在函数内部没有声明这个变量的话,就在函数的外部来找啦。对于多级嵌套的函数来说,就该是一层一层往外找,直到找到同名的变量,或者到“顶层”也找不到就停下来(这个情况下如果执行函数就出错了,提示变量没有定义)。

当然,这个查找变量的过程并不直观,仅仅是我的描述而已,希望能帮你理解而不是相反。

对于函数的参数,我也看作是函数的内部变量(我不用“局部变量”的说法,但我想你大概知道我指的是什么),不过其值是要等到函数执行时才能确定的。

把函数的定义,和函数的执行分开来看。

函数定义时,是在一个静态的环境下,函数的内外部的环境是固定的。尽管有很多变量的值还在变动,甚至只在每次执行时才能确定,但这并不妨碍我们去“引用”它。在函数执行的过程中,在每个具体的引用到变量的位置,都会被变量当时的值所替代(同样,声明语句例外,是重新赋值)。

特别地,在函数定义阶段(或者说解释器解析而非执行函数时),能够使用哪些变量,也是确定的了。例如,函数内部声明了一个 a 变量,那么函数内部使用到 a 变量的语句,就是跟这个 a 关联的了。而如果内部没有,则在一层层的外部作用域(外部函数的作用域,如果有的话)里查找,如果还没有的话呢?那么你执行函数时就报错了呗。

接着这个话题,我们引出“闭包”。

闭包

对于“闭包”这样严肃的东西,还是来看维基百科的定义:

在计算机科学中,闭包(Closure)是词法闭包(Lexical Closure)的简称,是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。

来看一个栗子:

var count = (function () {
    var i = 0;
    return function () {
        return ++i;
    };
})();

count(); // 1
count(); // 2

在这个栗子中,有两个匿名函数,其中一个嵌套在另一个的内部。外层的匿名函数被立即执行了(通过 (function () {})() 的方式),然后的是内部的匿名函数,被作为返回值赋给了变量 count。所以,经过上面的过程,count 的值是一个函数对象。

而 count 所关联的这个函数比较特别,在定义时,它引用了外部函数的变量 i,于是,外部变量 i 和这个函数构成了上面定义中的“闭包”,也就产生了上面的现象。

那么这一切,和这个诡异的名字一起,有什么作用呢?只是让人觉得迷惑,或者让不熟悉的人大呼“NB”?

我曾经看到过一种说法,是说使用闭包是为了在 JavaScript 中提供一种使用局部变量的机制。且不论这个对于闭包的评价本身正确与否,我们来试着理解下“局部变量”这回事。还是举个栗子:

function Person(name) {
    this.name = name;
    this.getName = function () {
        return this.name;
    };
}

假设我们希望,其他人在使用到这个 Person 类(暂且叫做“类”吧,后面我想专门写一篇东西来聊聊 JavaScript 的继承和类)时,只能通过 getName() 来获取 name,而不能直接访问 name 并改变其值的话,我感觉不太现实。因为 JavaScript 没有提供像 private, public 这样的东东,所有的东西都默认是公开的。而如果非要这样做,毕竟这样做也是有着合理的应用场景的,通常可以通过闭包来严格实现(只是把属性名改为类似 _name 这样来提示他人不要乱改,毕竟不严格不是):

function Person(name) {
    var _name = name;
    this.getName = function () {
        return _name;
    };
}

当然,前面提到过,也可以把函数的参数看作是内部变量,所以也可以直接这样:

function Person(name) {
    this.getName = function () {
        return name;
    };
}

我们来使用下这个“类”:

var me = new Person("luobo");
me.getName(); // "luobo"

显然,没有 name 属性,也就无法直接修改这个值啦。(当然,如果要作为“私有”成员使用的是对象,那么即便采用上述方法,由于返回的是对象本身,所以仍旧可以修改对象)

回到上面的问题,关于闭包的作用,我还是觉得:闭包是语言本身提供的一种机制,并不见得就一定是为了什么特定目的而创造的,更加不会是为了创造“局部变量”的这一个目的。根据自己的需要,在合适的地方使用它就是了,只要你是真的会用就好_

小结

抱歉,今天情绪不佳,写东西不是很有激情,所以上面的文字尽管我的确花了心思,但自己都不太满意。作用域和“闭包”是我理解的 JavaScript 中的一个很重要的主题,花了很长时间我才有了上面的那些体会,但是叙述地有点没有头绪啦。

关于作用域,我认为先要对于 JavaScript 代码的执行有一定的理解。(我说说我的理解吧,欢迎交流。)在浏览器环境下,没有写在任何函数内部的语句,就直接执行了。写在函数内部的代码,则只有等到函数被调用的时候,才会执行。但浏览器虽然没有执行函数,还是会先把函数“读”一遍,“理解”了之后记录下来。这样当任何时候需要使用这个函数的时候,就能直接拿来,结合当时的环境来执行啦。

这个过程的细节中就有关作用域、闭包的身影啦。

当然上面是我个人的理解,描述也不够准确和正确。但是我想对于函数的定义和执行的机制有更深入的理解还是很必要的,特别是想真的对 JavaScript 这门语言有更深入的理解的话。显然,我也还需要加油啊!

关于“闭包”,这真的是一个“高级”的话题。而且,在不领会闭包的原理的情况下,很有可能不知不觉就给自己挖了一个坑出来,我就干过。

var arr = ['a', 'b', 'c'], funcs = [];
for (var i = 0, len = arr.length; i < len; i++) {
    funcs[i] = function () {
        return arr[i];
    };
}

上面这个造作的栗子中,我的本意是:得到一个数组 funcs,该数组的每一项都是一个返回数组 arr 中对应位置的值的函数。有点绕,不过我想你能明白是什么意思。但是,结果却并非如此:

funcs[1](); // undefined

奇怪,不应该返回 arr[1] 也就是 'b' 吗?

曾经我为此烦恼过....后来我意识到,我不知不觉用闭包给自己挖了这个坑。

如果你还没有看明白(当然,我的叙述本身就乱,难为你了),我来给些提示:

  • 试着在控制台输出变量 i,看下当前值
  • 然后在控制台输出 arr[i],看下当前值
  • for 循环中,其实每次构建的匿名函数,返回的就是 arr[i]
  • 通过 i = 1 把变量 i 的值改为 1
  • 再执行下上面的 funcs[1](),或者 funcs[0]() funcs[2]() 都一样,看下结果你应该就能明白了....

好吧,这就是闭包。

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

推荐阅读更多精彩内容