深入浅出-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;	//语法错误,常量未初始化

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

无论严格模式还是非严格模式,都不可为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)

letconst声明的变量不会被提升到作用域的顶部,如果在声明之前访问这些变量,即使是相对安全的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都创建了一个副本并存储为变量valuevalue的值就是相应迭代创建的函数所使用的值,所以调用每个函数得到的都是对应的循环中的值。

循环中的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-infor-of循环中,因为每次迭代不会修改已有绑定,而是会创建一个新绑定。

全局块作用域绑定

var被用于全局作用域的时候,它会创建一个新的全局变量作为全局对象(浏览器环境中的window对象)的属性。
但是如果使用letconst,会在全局作用域下创建一个新的绑定,且不会添加为全局对象的属性。
var声明的变量可能会覆盖掉已经存在的全局属性,可以应用在浏览器中跨frame或跨window访问代码。所以如果不想为全局对象创造属性,用letconst安全。

建议

默认使用const,只有确实需要改变变量的值时使用let。因为大部分变量的值在初始化后不应再改变,而预料外的变量值的改变是很多bug的源头。

发布了20 篇原创文章 · 获赞 38 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/jiamiao66/article/details/103667750