函数作用域
JS中具有基于函数的作用域,意味着每声明一个函数都会为其自身创建一个作用域气泡。
属于这个函数的所有变量都可以在整个函数的范围内使用及复用(事实上嵌套的作用域也可以使用)。
隐藏内部实现
我们可以从写的代码中挑选出一个任意片段,然后用函数声明对它进行包装,实际上就是把这些代码“隐藏”起来了。此时代码中的任何声明(变量或函数)都将绑定在这个新创建的包装函数的作用域中,而不是先前所在的作用域中。
也就是我们可以把变量和函数包裹在一个函数的作用域中,然后用这个作用域来“隐藏”他们
【为什么内部要隐藏他们】
如果所有的变量和函数都在全局作用域中,我们当然可以在所有的内部嵌套中访问他们。但这样就可能会暴露过多的变量和函数,而这些变量和函数本应该是私有的,因此正确的代码应该是可以组织对这些变量或函数进行访问的
规避冲突
“隐藏”作用域中的变量和函数是可以避免同名标识符之间的冲突
两个标识符可能具有相同的名字,但是用途却不一样,无意间就可能会造成命名冲突,而冲突就会导致变量的值意外覆盖
function foo() {
function bar(a){
i = 3 // 会修改到for循环所属作用域中的i
console.log(a + i)
}
for(var i = 0; i < 10; i++){
bar(i*2) // 糟糕,被无限循环了
}
}
for
循环中的i是在 foo()
的作用域中被声明的,因此在整个foo
作用域中都可以使用,而bar
中也使用到了i变量,但是bar
中并没有对i变量的声明,因此就去它的上级作用域查找,也就是foo
创建的作用域。恰好有。因此bar
中使用到的i
也是foo
作用域中的变量i,
而在bar()
中,i
总是被赋值为3。而for
循环每次都会调用bar()
,因此就会被无限循环
function foo() {
function bar(a){
var i = 3 // 会修改到for循环所属作用域中的i
console.log(a + i)
}
for(var i = 0; i < 10; i++){
bar(i*2) // 糟糕,被无限循环了
}
}
此时bar
中使用的i
就是他自己作用域中的i
了,因此不会影响到for
循环中的i
函数表达式
虽然在任意代码片段外部添加包装函数,可以将内部的变量和函数定义“隐藏”起来,让外部作用域无法访问包装函数内部的任何内容。
var a = 2
function foo() {
var a = 3
console.log(a) //3
}
foo()
console.log(a) //2
虽然这种技术可以解决一些问题,但它并不是最理想的,因为他会导致一些额外的问题。
- 首先我们必须得声明一个具名函数foo(),意味着foo这个名称本身“污染”了所在作用域(这个例子是全局作用域)
- 其次必须显示地通过函数名(foo())调用这个函数才可以运行其中的代码
如果函数不需要函数名,并且可以自动运行,这样得话就更加理想了
解决办法:同时使用函数表达式的方式
var a = 2
(function foo() { // 添加这一行
var a = 3
console.log(a) //3
})() //以及这一行
console.log(a) //2
首先 (function....
开头就说明它是一个函数表达式,因为只有以function
开头才是函数声明。
而函数表达式是可以立即执行的,因此最后的()
就是来立即执行函数表达式的
函数声明和函数表达式的区分
函数表达式和函数声明的区别
var a = 2
function foo() {
var a = 3
console.log(a) //3
}
foo()
console.log(a) //2
函数声明的函数:这里的foo被绑定在所在的作用域中,也就是全局作用域上,可以直接通过foo()来调用它
var a = 2
(function foo() { // 添加这一行
var a = 3
console.log(a) //3
})() //以及这一行
console.log(a) //2
函数表达式:这里的foo被绑定在函数表达式自身的函数中,而不是所在的作用域中
换句话说,
(function foo(){...})
作为函数表达式,意味着foo
只能在{...}
所代表的的位置中被访问到,外部的作用域则不行。而此时的foo
变量名被隐藏在自身中则意味着不会非必要的污染外部作用域
也就是下面代码所示:
var a = 2
(function foo() { // 添加这一行
var foo = xxx // foo只可以在这里被调用
var a = 3
console.log(a) //3
})() //以及这一行
console.log(a) //2
匿名和具名
对于函数表达式最熟悉也是最常见的场景可能就是【回调参数】了,比如:
setTimeout(function(){
console.log("I waited 1 second")
},1000)
这里面的传入的参数时一个函数,并且还是一个【匿名函数表达式】,因为它没有函数名,也是以function
来单独声明的。
函数表达式可以是匿名的,但是函数声明是不可以省略函数名。(因为这在js语法中是非法的)
【匿名函数的缺点】:
-
匿名函数栈追踪中不会显示出有意义的函数名,这也使得调试很困难
-
如果没有函数名,当函数需要引用自身时只能使用已经过期的argument.callee引用。
比如在递归中
也比如:在事件触发后,事件监听器需要解绑自身 -
匿名函数不利于代码的可能性与可理解性。因为一个有意义的函数名可以让代码不言自明
【小结】
行内函数表达式非常强大且有用,而匿名和具名之间的区别并不会对这点有任何影响。因此给函数表达式指定一个函数名可以有效的解决以上的问题。
因此推荐始终给函数表达式命名是一个最佳的实践
setTimeout(function timeoutHandler(){ // 哈哈,我又名字啦
console.log("I waited 1 second")
},1000)
立即执行函数表达式
var a = 2
(function foo() {
var a = 3
console.log(a) // 3
})()
因为函数被包含在一对()
中,因此成为了一个表达式。而通过在末尾加上另一个()
可以立即执行这个函数。
这种模式很常见,社区给它规定了一个术语: IIFE (Immediately Invoked Function Express)即立即执行函数表达式
又因为函数名对于函数表达式不是必须的,因此IIFE最常见的用法就是匿名函数表达式
var a = 2
(function () { // 匿名函数表达式
var a = 3
console.log(a) // 3
})()
虽然具名的IIFE并不常见,但是它具有上述匿名函数表达式的所有优势,并且增加了代码的可读性,因此也是一个非常推广的实践
var a = 2
(function IIFE() { // 具名函数表达式
var a = 3
console.log(a) // 3
})()
【两个IIFE的常用形式】
// 第一种
(function () {
...
})()
//第二种
(function () {
...
}())
【IIFE的普遍进阶用法】:把IIFE当做函数调用,并传递参数进去
var a = 2 // 全局对象的a
(function (global) { // 接收传递进来的参数
var a = 3
console.log(a) // 3
console.log(global.a) // 2
})(wnidow) // 将window作为参数传递进去
块作用域
with
with
关键字也是块作用域的一种形式。用with
从对象中创建出来的作用域仅在with
声明中,而非外部作用域中有效
function foo(obj) [
with(obj) {
a = 2
}
}
var o1 = {
a: 3
}
var o2 = {
b: 3
}
foo(o1)
console.log(o1.a) // 2
foo(o2)
console.log(o2.a) // undefined
console.log(a) // 糟糕,a被泄露到全局作用域上了
当我们把o1
传递进去,with(o1)
会将o1
处理为一个完全隔离的词法作用域(块作用域),而o1
的属性也会被处理为定义在这个作用域中的词法标识符。
但是,尽管
with
可以将一个对象处理为一个词法作用域,如果我们在这个块内部正常var
声明并不会限制在这个块作用域中,而是被添加到with
所处的函数作用域中
try/catch
js3中规范的try/catch
的catch
分局会创建一个块作用域,其中的声明仅仅在catch
内部有效
try {
undefined(); // 执行一个非法操作符来强制创造一个异常
} catch (err) {
console.log(err) // 将捕获的异常打印出来
}
console.log(err) // Reference : err not found
即err
仅存在catch
分句内部,当试图从别处引用它是会抛出错误
尽管这个行为已经被标准化,而且被大部分标准的js环境所支持。但是当同一个作用域中的两个或多个
catch
分句用同样的标识符名称声明错误变量是,很多静态检查工具还是会发出警告,要知道的,这实际上并不是重复定义,因为所有的变量都被安全的限制在块作用域内部。
但是静态检查工具还是会发生烦人的警告,因此很多开发者会将
catch
的参数命名为err1
、err2
等。也有的开发者直接关闭了静态检查工具对重复变量名的检查
var 和 let
var foo = true
if (foo) {
var bar = foo * 2
console.log(bar)
}
使用var
声明变量时,它写在哪里都是一样的,因为他们最终都会属于外部作用域
【es6中的let关键字】
它是es6中提供的一种变量的声明方式,let
关键字可将变量绑定在所在的任意作用域上(通常是{...}
内部)
换句话说:
let
为其声明的变量隐式的劫持了所在的块作用域
var foo = true
if (foo) {
let bar = foo * 2
console.log(bar)
}
console.log(bar) // ReferenceError
【要注意的事】:
将let
变量附加在一个已经存在的作用域的行为是隐式的。而在开发和修改代码的过程中,如果没有注意到哪些块作用域中存在let
绑定的变量,并且还习惯性的移动这些块,或将其包含在其他块中,就会很容易导致代码变得混乱、
【解决办法】:
因此为块作用域侠士地创建块就可以解决这个问题,因为这样可以使得变量的附属关系更加清晰。
var foo = true
if (foo) {
{ // 显示的块
let bar = foo * 2
console.log(bar)
} // 显示的块
}
console.log(bar) // ReferenceError
即我们在if
声明内部显式地创建了一个块,如果需要对其进行重构,只需移动整个块而不用担心会对外部if
声明的位置和语义产生任何影响
要知道的:使用let进行的声明不会在块作用域中进行提升。即声明的代码被运行前,“声明”并不存在
提升是指声明会被视为存在于其所出现的作用域的整个范围内
【let
循环】
for( let i = 0; i < 4; i++) {
console.log(i)
}
console.log(i) // ReferenceError
for循环的头部let实际上不仅仅将i绑定到的for循环的块中,事实上它还将其重新绑定到了循环的每一个迭代中,因此可以确保使用上一个循环迭代结束时的值重新赋值
{ // 将j绑定在这个块中
let j;
for (j = 0; j < 4; j++) {
let i = j // 将i重新绑定到每一个循环的块中
console.log(i)
}
}
考虑一下代码
const
es6中还引入了const
,同样可以创建块作用域变量,但是它的值是固定的常量。之后任何试图修改值的操作都会引起错误
提升
什么是提升??
无论作用域中的声明出现在什么地方,都将在代码本身被执行前首先进行处理。简单的可以认为是将所有的声明(变量和函数)都移动到各自作用域的最顶端。这个过程被称为提升
函数作用域和块作用域的行为是一样的:任何声明在某个作用域的变量,都将附属于这个作用域
【要知道的】:
引擎会在解释js代码之前首先对其进行编译。而编译的一部分工作就是找到所有的声明,并用合适的作用域把他们关联起来。
即,包括变量和函数在内的所有声明都会在任何代码被执行之前首先被处理
函数优先
当遇到重复的声明时,函数声明的优先级更高