# 函数作用域

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');
	}
}