# 函数作用域
js 里若我们包装函数的声明,则该函数会被当做一个函数表达式,而不是一个函数声明。如何理解?
var a = 2; | |
(function foo(){ | |
var a = 3; | |
console.log(a); | |
})(); // 输出 3 | |
console.log(a); // 输出 2 | |
// 当我们继续想要调用 foo | |
foo(); //ReferenceError | |
--------- | |
//foo 函数的书写还有一个改进的形式 | |
(function foo(){ | |
var a = 3; | |
console.log(a); // 输出 3 | |
}()) // 即将 () 调用也包含进外层的 () 中,书写形式变了但其功能含义与上面的写法是一致的 |
上述例子中,foo 函数的作用域仅限于 {...}
即第五行,foo 函数不能再在后续代码被调用。这种情况就是被包装函数声明 foo,被编译器当作一个函数表达式而不是一个标准的函数声明。
(function foo(){...})
作为函数表达式意味着,foo 只能在 {...}
所代表的位置中被访问,外部作用域无法访问。但是若是有一个变量去接收该函数表达式的返回值,则该函数依然可以被继续保留调用
// 最常见的函数表达式 | |
var b = function foo(){ | |
console.log(20); | |
} | |
// | |
var n = (function foo(){ | |
var a = 3; | |
console.log(a); | |
}); | |
n();// 输出 3 |
# 变量作用域
在 js 中只有全局作用域和函数作用域这两个基本的单位,块级作用域在 js 中是不存在的。如何理解呢,请看如下代码
if(true){ | |
var i = 10; | |
console.log(i); // 此时输出 10 | |
} | |
console.log(i) // 输出?并不是输出 ReferenceError 而是输出 10 |
在 C++ 或 Java 中,i 这个变量其作用域仅在 {...} 中,在 {...} 外调用变量 i 是无法通过编译的,会产生一个报错。而在 js 中,如果你用 var
关键字声明的变量不在一个函数中,则其作用域会为上级外部作用域。这就导致了如果我们在后续代码中同样声明一样的变量,使用该变量时就会产生预料外的结果。
很多时候我们希望变量仅在块级作用域里生效,用完以后就被 GC 回收。例如
for(var i = 0;i<10;i++) // 在这个循环里我们通常希望 i 在 for 循环执行完之后就失效,这一我们就可以在后续代码中重复定义使用 i | |
{ // 但是实际上 变量 i for 循环完以后并不会失效,而是继续作用在全局作用域中 | |
console.log(i); | |
} | |
console.log(i) // 输出 10 | |
-------- | |
上述代码等效于 | |
var i; //i 的作用域不是仅限于 for 循环内 | |
for(i=0;i<10;i++) | |
{ | |
console.log(i); | |
} |
为了解决这个问题,es6 中引入了 let
关键字进行解决。我们使用 let
关键字对其进行改进,让 i 的作用域限定在 for 循环内
for(let i = 0;i<10;i++) | |
{ | |
console.log(i); | |
} | |
console.log(i) // 此时输出 ReferenceError |
# let
let
的作用域会与其最近的 {...} 绑定,也就是说 js 在编译 let
关键字声明的变量时,会将该变量的作用域限定在包含该变量的 {...} 内。举个例子
if(true){ | |
let i = 10; | |
console.log(i); // 输出 10 | |
} | |
console.log(i) // 输出 ReferenceError | |
变量 i 的作用域被限定在if结构的代码块内 | |
------ | |
如果我们用var声明 | |
if(true){ | |
var i = 10; | |
console.log(i); // 输出 10 | |
} | |
console.log(i); // 依然输出 10 | |
上述代码等效于 | |
var i; | |
if(true){ | |
i = 10; | |
console.log(i); | |
} | |
console.log(i); |
这就是 var
关键字与 let
关键字的区别。 var
关键字定义的变量如果不是被包裹在函数体中,那么它就可以被外部作用域所访问。而 let
关键字声明的变量会将作用域绑定在最近的 {...} 包裹的块级作用域中。做个小测验
if(true){ | |
{ | |
let i = 10; | |
console.log(i); | |
} | |
console.log(i); | |
} | |
该程序会输出啥? | |
会是 10 10 吗? | |
---------- | |
if(true){ | |
{ | |
var i = 10; | |
console.log(i); | |
} | |
console.log(i); | |
} | |
console.log(i); | |
该程序的输出是? |
第一题的答案为: 10 ReferenceError,第二题的答案为:10 10 10。这个例子可以很形象的说明 let
关键字的特性。
# const
const
关键字的特性与 let
一样,都是将变量的作用域绑定在最近 {...} 中,唯一的不同就是当我们在其作用域中不允许修改其变量值,被 const
定义的变量都为常量,若想修改则会产生 TypeError.
{ | |
const i = 10; | |
i = 20; //TypeError | |
} | |
console.log(i) //ReferenceError |
# 提升
我们都知道函数和变量若都声明在调用后面,js 依然会通过编译。因为函数和变量的声明会被提升至所在作用域顶部,而赋值却留在原地。若代码中出现多个重复的声明,函数提升会先于变量提升。而且若有多个重复的函数声明,则以最后一个为准
foo(); // 30 | |
var foo; | |
function foo(){ | |
console.log(10); | |
} | |
foo = function(){ | |
console.log(20); | |
} | |
function foo(){ | |
console.log(30); | |
} | |
为什么foo();输出的是30? | |
// 若再调用一次 foo () | |
foo(); // 输出?输出 20 | |
---------- | |
// 在编译器眼中上述代码的执行顺序 | |
function foo(){ | |
console.log(30); | |
} | |
foo(); | |
foo = function(){ | |
console.log(20); | |
} |
普通块内的函数声明通常会被提升至顶级作用域的顶部,且这个过程不可被条件判断控制
foo(); //'asuhe' | |
if(true){ | |
function foo(){ | |
console.log('ashitahe'); | |
} | |
}else{ | |
function foo(){ | |
console.log('asuhe'); | |
} | |
} |