《你不知道的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 a
和 a = 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)
}
虽然这些听起来都是些无用的学院理论,但是它说明了在同一个作用域中进行重复定义是非常糟糕的,而且经常会导致各种奇怪的问题。