关于Javascript的作用域


函数作用域

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