《你不知道的JavaScript》--函数作用域和变量提升(03)

一、函数中的作用域

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

在这个代码片段中,foo的作用域气泡中包含了标识符a,b,c和bar。可以在foo内部访问,同样在bar内部也可以访问,但是无法从foo的外部对他们进行访问,也就是说,这些标识符全部无法从全局作用域中进行访问。

总结来说,函数作用域是指,属于这个函数的变量都可以在整个函数范围(包括嵌套的作用域)内使用及复用。

二、隐藏内部实现

函数经常用来创建一个作用域气泡,把功能代码放在函数中,用作用域隐藏他们。

为什么隐藏?

相信大家都听过最小暴露原则,就是在软件设计中,应该最小限度的暴露必要内容,而将其他内容隐藏起来,比如某个模块或对象的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(..)的“访问权限”不仅没有必要,而且可能是“危险”的,因为它们可能被有意或无意地以非预期的方式使用,从而导致超出了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(..)所控制。功能性和最终效果都没有受影响,但是设计上将具体内容私有化了,设计良好的软件都会依此进行实现。

隐藏作用域中的变量好函数带来的另一个好处,是可以避免同名标识符之间的冲突,这个就不过多赘述了。

其实这也是模块化发展中的一小步,后面会专门出一篇js模块化。

二、匿名和具名

setTimeout(function () {
   console.log('some code')
},1000)

这叫做匿名函数表达式, 因为function()...没有形成标识符,函数表达式可以是匿名的,而函数声明不可以省略函数名(在JavaScript语法中这是非法的)

区分函数声明和表达式最简单的方法是看function关键字出现在声明中的位置(不仅仅是一行代码,而是整个声明中的位置)。如果function是声明中的第一个词,那么就是一个函数声明,否则就是一个函数表达式。

匿名函数书写起来简单快捷,但是也有几个缺点。

1、匿名函数在栈追踪中不会显示出有意义的函数名,使调试变得困难。

2、如果没有函数名,当函数需要引用自身时只能使用已经过期的arguments.callee引用,比如在递归中。

3、匿名函数省略了对于代码可读性/可理解性很重要的函数名。一个描述性的名称可以让代码不言自明。

综上所属,给函数表达式指定一个函数名可以有效解决以上问题。

setTimeout(function timeoutHandler () {
   console.log('some code')
},1000)

三、立即执行函数表达式

var a = 2
(function foo() {
  var a = 3
  console.log(a)  // 3
})()
console.log(2) // 2

函数包含在一对()括号内部,因此成为了一个表达式,通过在末尾加上另外一个()可以立即执行这个函数 比如(function foo(){ .. })()。第一个()将函数变成表达式,第二个()执行了这个函数。

(function foo(){ .. })()也可以写成(function(){ .. }())

这种模式也有个术语:IIFE(Immediately Invoked Function Expression).

IIFE的进阶用法

1、把它们当做函数调用并传递参数进去

var a = 2;
(function IIFE(global){
  var a = 3;
  console.log(a) // 3
  console.log(global.a)  // 2
})(window)
console.log(a) // 2

2、倒置代码的运行顺序,将需要运行的函数放在第二位,在IIFE执行之后当作参数传递进去。

var a = 2;
(function IIFE(def){

})(function def (global){
  var a = 3;
  console.log(a) // 3
  console.log(global.a)  // 2
})

三、变量提升

在这本书中,作者将变量提升描述为一个'先有鸡还是先有蛋'的问题,即倒是到声明(蛋)在前,还是赋值(鸡)在前。

先回顾一下《你不知道的JavaScript》-作用域是什么(01)
中关于编译器的内容,引擎会在解释JavaScript代码之前首先对其进行编译。编译阶段一部分工作就是找到所有声明,并用合适的作用域将他们关联起来。

因此,包括变量和函数在内的所有声明都会在任何代码被执行钱首先被处理。

当你看到var a = 2时,JavaScript会将其看成两个声明,var aa = 2,第一个定义是在编译阶段进行的,第二个赋值声明会被留在原地等待执行阶段。

这个过程就好像变量和函数声明在他们代码中出现的位置被‘移动到了最上面,这个过程就叫做变量提升’。

换句话说,先有蛋(声明)后有鸡(赋值)。

只有声明本身会被提升,而赋值或其他运行逻辑会留在原地。

另外,每个作用域都会进行提升操作。

比如

foo() ; // TypeError
bar() ; // ReferenceError 
var foo = function bar (){
    console.log(a) // undefined
    var a = 2 ;
}

上述代码片段经过提升后,实际上会被理解为以下形式:

var foo ;

foo() ; // TypeError
bar() ; // ReferenceError 

foo = function(){
    var bar = ...self...
    var a 
    console.log(a) // undefined
    a = 2 ;
}

1、函数声明foo被提升并分配给做在作用域(这里是全局作用域),因此foo不会导致ReferenceError ,但是foo此时没有赋值(如果它是一个函数声明而不是函数表达式,那么就会赋值), foo()由于对undefined值进行函数调用而导致非法操作,因此抛出TypeError异常。

2、如果只是函数function bar (){},那么它本身bar是可以进行提升的,可以正常调用,但是function bar (){}现在被赋值给了foo,所以function bar (){}变成了函数表达式,函数声明会被提升,函数表达式不会被提升,所以bar报的错误是ReferenceError

3、如果忽略foo和bar的错误,在bar内部,a也会提升在bar作用域顶端

四、函数优先

函数声明和变量声明都会被提升,但是一个值得注意的细节,是函数首先会被提升,然后才是变量。

看以下代码

foo() //1
var foo;
function foo(){
  console.log(1)
}
foo = function (){
  console.log(2)
}

var foo尽管出现在function foo()...的声明之前,但是还是被忽略了,因为函数声明会被提升到普通函数变量之前。

但是如果是2个重名的函数声明,那么后面的可以覆盖前面的.

foo() //3
var foo;
function foo(){
  console.log(1)
}
foo = function (){
  console.log(2)
}
function foo(){
  console.log(3)
}

虽然这些听起来都是些无用的学院理论,但是它说明了在同一个作用域中进行重复定义是非常糟糕的,而且经常会导致各种奇怪的问题。

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

推荐阅读更多精彩内容