作用域(三)——函数中的作用域

对于前面提出的问题,最常见的答案是JavaScript具有基于函数的作用域,意味着每声明一个函数都会为其自身创建一个气泡,而其他结构都不会创建作用域气泡。但事实上这并不完全正确,下面我们来看一下。大家还可以关注我的微信公众号,蜗牛全栈。

首先需要研究一下函数作用域机器背后的一些内容。

考虑下面的代码:

function foo(a){
  var b = 2;
  function bar(){
  }
  var c = 3;
}

在这个代码片段中,foo的作用域气泡中包含了标识符a、b、c和bar【其中a在函数foo的参数内】。无论标识符声明出现在作用域中的何处,这个标识符所代表的变量或函数都将附属于所处作用域的气泡。我们将在后续的文章讨论具体的原理。

Bar拥有自己的作用域气泡。全局作用域也有自己的作用域气泡,它只包含了一个标识符:foo。

由于标识符a、b、c和bar都附属于foo的作用域气泡,因此无法从foo的外部对他们进行访问。也就是说,这些标识符全都无法从全局作用域中进行访问,因此下面的代码会导致ReferenceError错误:

bar(); // 失败
console.log(a, b, c); // 三个全都失败

【调用bar的时候,因为在全局作用域中也没有找到,所以出现的错误是ReferenceError,,而不是TypeError】

但是,这些标识符(a、b、c、foo和bar)在foo内部都是可以被访问的,同样在bar内部也可以被访问(假设bar内部没有同名的标识符声明)【如果有同名的标识符声明,会出现之前说的遮蔽效应】

函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复用(实际上在嵌套的作用域中也可以使用)。这种设计方案是非常有用的,能充分利用JavaScript变量可以根据需要改变值类型的“动态”特性。

但与此同时,如果不细心处理那些可以在整个作用域范围内被访问的变量,可能会带来意想不到的问题。

隐藏内部的实现

对函数的传统认知就是先声明一个函数,然后再向里面添加代码。但反过来想可以带来一些启示:从所写的代码中挑选任意的一个片段,然后用函数声明对它进行包装,实际上就是把这些代码“隐藏”起来了。

实际的结果就是在这个代码片段的周围创建了一个作用域气泡,也就是说这段代码中的任何声明(变量或者函数)都将绑定在这个新创建的包装函数的作用域中,而不是先前所在的作用域中。换句话说,可以把变量和函数包裹在一个函数的作用域中,然后用这个作用域来“隐藏”他们。【就相当于在原来的集体圈一个自己的小集体出来,只能一部分和外界沟通,至于怎么沟通,由这个小集体内部自己决定】

为什么“隐藏”变量和函数是一个有用的技术?

有很多原因促成了这种基于作用域的隐藏方法。他们大都是从最小特权中引申出来的,也叫最小授权或最小暴露原则。【这个应该是防止暴露的太多,会出现作用域的问题,就像之前提到的with关键字用法实例】这个原则是指在软件设计中,应该最小限度地暴露必要内容,而将其他内容都“隐藏”起来,比如某个模块或对象的API设计。

这个原则可以延伸到如何选择作用域来包含变量和函数。如果所有变量和函数都在全局作用域中,那当然在所有的内部嵌套作用域中访问到他们。但这样会破坏前面提到的最小特权原则,因为可能会暴露过多的变量或函数,而这些变量或函数本应该是私有的,正确的代码应该是可以阻止对这些变量或函数进行访问的。

例如

function doSomething(a){
  b = a + doSomethingElse(a * 2);
  console.log(b * 3);
}

function doSomethingElse(a){
  return a - 1;
}
var b;
doSomething(2); // 15

在这个代码片段中,变量b和函数doSomethingElse应该是doSomething内容具体实现的“私有”内容。给予外部作用域对b和doSomethingElse的“访问权限”不仅没有必要,而且可能是“危险”的,因为他们可能被有意或无意地以非预期的方式使用,从而导致超出了doSomethingElse的适用条件。更“合理”的设计会将这些私有的具体内容隐藏在doSomething内部,例如

function doSomething(a){
  function doSomethingElse(a){
    return a - 1;
  }
  var b;
  b = a + doSomethingElse(a * 2);
  console.log(b * 3);
}

doSomething(2); // 15

现在,b和doSomethingElse都无法从外部被访问,而只能被doSomething控制。功能性和最终效果都没有受影响,但是设计上将具体内容私有化了,设计良好的软件都会以此进行实现。【这个在项目重构上,也会有一席之地】

避免冲突

“隐藏”作用域中的变量和函数所带来的另一个好处,是可以避免同名标识符之间的冲突,【这个小编想到了同名的变量和函数】,两个标识符可能具有相同的名字但用途却不一样,无意间可能造成命名冲突。冲突会导致变量值被意外覆盖。
例如:

function foo(){
  function bar(a){
    i = 3; // 修改for循环所属作用域中的i
    console.log(a + i);
  }
  for(var I=0;i<10;i++){
    bar(I * 2); // 糟糕,无限循环了!
  }
}

foo();

Bar内部的赋值表达式i=3外意外地覆盖了声明在foo内部for循环中的i。在这个例子中将会导致无限循环,因为i被固定设置为3,永远满足小于10这个条件。

Bar内部的赋值操作需要声明一个本地变量来使用,采用任何名字都可以,var i=3;就可以满足这个需求(同时会为i声明一个前面提到过的“遮蔽变量”)。另外一种方法是采用一个完全不同的标识符名称,比如var j=3;。但是软件设计在某种情况下可能自然而然地要求使用同样的标识符名称,因此在这种情况下使用作用域来“隐藏”内部声明是唯一的最佳选择。【一定程度上也为重构提供了更多要注意的事项和方案】

全局命名空间

变量冲突的一个典型例子存在于全局作用域中。当程序中加载了多个第三方库时,如果他们没有妥善地将内部私有的函数或变量隐藏起来,就会很容易引发冲突。

这些库通常会在全局作用域中声明一个名字足够独特的变量,通常是一个对象,这个对象被用作库的命名空间,所有需要暴露给外界的功能都会成为这个对象(命名空间)的属性,而不是将自己的标识符暴露在顶级的词法作用域中。

例如:

var MyReallyCoolLibrary = {
  awesome: ’stuff’,
  doSomething: function(){
  },
  doAnotherThing: function(){
  }
}

模块管理

另外一种避免冲突的办法和现代的模块机制很接近,就是从众多模块管理器中挑选一个来使用。使用这些工具,任何库都无需将标识符加入到全局作用域中,而是通过依赖管理器的机制将库的标识符显式地引入到另外一个特定的作用域中。

显而易见,这些工具并没有能够违反词法作用域规则的“神奇”功能。它们只能利用作用域的规则强制所有标识符都不能注入到共享作用域中,而是保持在私有、无冲突的作用域中,这样可以有效规避掉所有的意外冲突。【就相当于每个模块都在自己的小盒子里,大家互不干扰】

因此,只要你愿意,即使不适用任何依赖管理工具也可以实现相同的功效。

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

推荐阅读更多精彩内容