天天看点

深入浅出-ES6作用域

文章内容词语‘白话’;

  • 声明:定义标识符,起名字。
  • 初始化:赋值
  • 声明但未初始化:有名字能访问,但没给它赋值,所以它的值为undefined

变量提升

在函数作用域或全局作用域中通过关键字

var

声明的变量,无论实际是在哪里声明的,都会被当成在当前作用域顶部声明的变量,这就是我们常说的提升机制。

引用一个简单例子:

function getValue(condition){
	if(condition){
		var value = "blue";
		return value;
	}else{
		//此处可以访问变量value,其值为undefined
		return null;
	}
	//此处可以访问变量value,其值为undefined
}
           

无论如何,变量都会被创建,在预编译阶段,JavaScript引擎会把

getValue

函数修改成:

function getValue(condition){
	//声明
	var value;
	
	if(condition){
		//初始化
		value = "blue";
		return value;
	}else{
		return null;
	}
}
           

变量

value

的声明和初始化被拆成两步执行,声明被提升至函数顶部,而初始化操作还在原处执行,所以在

else

字句中也可以被访问到,只是此时变量还未初始化,所以访问到的值是

undefined

块级声明

块级声明用于声明在指定块的作用域之外无法访问的变量。块级作用域(亦被称为词法作用域)存在于:
  • 函数内部
  • 块中(字符

    {

    }

    之间的区域)

let声明

let可以把变量的作用域限制再当前代码块中,由于let声明不会被提升,所以通常将let声明语句放在封闭代码块的顶部,以便整个代码块都可以访问。

function getValue(condition){
	if(condition){
		let value = "blue";
		return value;
	}else{
		//变量value在此处不存在
		return null;
	}
	//变量value在此处不存在
}
           

执行流离开

if

块,

value

立即被销毁。如果

condition

的值为

false

,就永远不会声明并初始化

value

禁止重声明

在同一作用域中不能用

let

重复定义已经存在的标识符,否则会抛出错误。

var count = 30;
let count = 40;//抛出语法错误
           
var count = 30;
//当前作用域内嵌另一个作用域(if块),在内嵌的作用域中用let声明就不会报错
if(con){
	let count = 40;//只能在if块中被访问到
}
           

const声明

使用

const

声明的是常量,一旦声明则不可更改。所以:每个通过

const

声明的常量必须初始化。
const a = 30;	//有效常量
const b;	//语法错误,常量未初始化
           

const

let

类似,不存在变量提升、执行到块外就会被立即销毁、重复定义会语法错误。

无论严格模式还是非严格模式,都不可为

const

定义的常量再赋值,否则就会报错。

const a = 30;
a = 40;//抛出语法错误
           

但是如果常量是对象,则对象中的值是可以被修改的。

const person = {
	name:'111'
};

//可以修改对象属性的值
person.name='222';

//抛出语法错误
person = {
	name:'222'
}
           

解析:const声明不允许修改绑定,但允许修改绑定的值。

原来给

person

绑定的是一个包含一个属性

name

的对象,修改属性

name

,其实是在修改这个对象包含的值,即修改绑定的值,但直接给

person

赋值,相当于改了

person

的绑定,所以会抛出错误。

临时死区(Temporal Dead Zone)

let

const

声明的变量不会被提升到作用域的顶部,如果在声明之前访问这些变量,即使是相对安全的

typeof

操作符也会触发引用错误。
if(con){
	console.log(typeof value);	//引用错误
	let value = "blue";
}
           

由于

console.log(typeof value);

语句会抛出错误,所以

let value = "blue";

不会执行,此时

value

还位于所谓的“临时死区”。只有执行过变量声明语句

let value = "blue";

后,变量才会从TDZ中移除,然后才可以正常访问。

console.log(typeof value);	//"undefined"
if(con){
	let value = "blue";
}
           

在声明

value

的代码块外执行,此时

value

并不在TDZ中,也就是不存在

value

这个绑定,所以

typeof

返回

"undefined"

循环中的块作用域绑定

for(var i =  0; i < 10; i++){
	
}
console.log(i);	//10

for(let j =  0; j < 10; j++){
	
}
console.log(j);	//抛出错误
           

let

声明的变量值存在于

for

循环当中,一旦循环结束,在其他地方均无法访问该变量。

小问题:依次输出0-9

var funcs=[];
for(var i =  0; i < 10; i++){
	funcs.push(function(){
		console.log(i);
	});
}
funcs.foreach(function(func){
	func();	//输出10次数字10
})
           

为什么输出10次数字10?

因为循环里的每次迭代同时共享着变量

i

,循环内部创建的函数全都保留了对相同变量的引用,循环结束时变量

i

的值为10,所以每次调用

console.log(i)

时就会输出10。

解决办法: 在循环中使用立即调用函数表达式(IIFE),以强制生成计数器变量的副本。

var funcs=[];
for(var i =  0; i < 10; i++){
	funcs.push(
		(function(value){
			return function(){
				console.log(value);
			}
		}(i))
	);
}
funcs.foreach(function(func){
	func();	//输出0,1,2...9
})
           

为什么会这样?

IIFE表达式为接受的每一个变量

i

都创建了一个副本并存储为变量

value

value

的值就是相应迭代创建的函数所使用的值,所以调用每个函数得到的都是对应的循环中的值。

循环中的let声明

var funcs=[];
for(let i =  0; i < 10; i++){
	funcs.push(function(){
		console.log(i);
	});
}
funcs.foreach(function(func){
	func();	//输出0,1,2...9
})
           

为什么?

每次循环的时候

let

声明都会创建一个新变量

i

,并将其初始化为

i

的当前值,所以循环内部创建的每个函数都能得到属于他们自己的

i

的副本。对于

for-in

循环和

for-of

循环是一样的。

循环中的const声明

如果上述循环

let

声明改成

const

声明,在循环的第一个迭代中,

i

是0,迭代执行成功,然后执行

i++

,因为这条语句试图修改常量,所以抛出错误。

如果后续循环不会修改该变量,那就可以使用

const

声明。

可以运用在

for-in

for-of

循环中,因为每次迭代不会修改已有绑定,而是会创建一个新绑定。

全局块作用域绑定

var

被用于全局作用域的时候,它会创建一个新的全局变量作为全局对象(浏览器环境中的window对象)的属性。

但是如果使用

let

const

,会在全局作用域下创建一个新的绑定,且不会添加为全局对象的属性。

var

声明的变量可能会覆盖掉已经存在的全局属性,可以应用在浏览器中跨frame或跨window访问代码。所以如果不想为全局对象创造属性,用

let

const

安全。

建议

默认使用

const

,只有确实需要改变变量的值时使用

let

。因为大部分变量的值在初始化后不应再改变,而预料外的变量值的改变是很多bug的源头。

继续阅读