在ES6之前,前端coder苦于 var 变量声明,一个简单的例子:
for(var i=0;i<5;i++){
setTimeOut(function(){
console.log(i);
},0)
}
试问:该程序段的输出结果是多少?
js新手可能会毫不含糊的说出:0 1 2 3 4;遗憾的是,答案并非如此,而是:5 5 5 5 5。这或许是var变量声明的一个缺陷,虽然现在es6出现了新的变量申明let和const。但是,(可能,也许)为了维护比较老的系统,以及兼容版本较低的浏览器,我们依然不得不使用var变量声明。所以清楚的了解var变量声明的一些特性是十分必要的。同时本文也将对let和const变量声明作相应的介绍。
ES6之前的变量声明——var
默认初值undefined
在学习一门编程语言的时候,我们首先可能就会了解如何声明其变量。当我们学习js的时候,必不可少的会学习到ES5的变量声明var。
var a;
console.log(a); // undefined;
如此我们便申明了一个变量a;在js中,如果一个变量申明后没有赋初值,那么就会有一个默认初值,即undefined;我们打印变量a即可轻松验证。
变量声明提前(变量提升)
var存在变量声明提前,即使用var申明的变量,变量声明语句总是会被提前到当前作用域的顶部,集中声明创建。
当我们使用console.log()打印一个未经声明的变量的时候,会报变量未定义的错误。
console.log(a); // ReferenceError: a is not defined;
但是,当我们编写如下程序段的时候:
console.log(a); // undefined;
var a = 10;
console.log(a); // 10
尽管我们的变量声明语句位于console.log(a)后面,但是其并不会报错,其原因就在于使用var声明的变量存在变量声明提前,但是仅仅是变量声明提前,而赋值保持在原地不动。
上面的程序相当于:
var a;
console.log(a);
a = 10;
console.log(10);
如此便能解释得通:为什么在变量声明前使用console.log不会报错,以及为什么输出为undefined和10。
方法作用域
var的作用域为方法作用域,即只要在方法内定义了,整个方法内的定义变量后的代码都可以使用。
这也就解释了文章开头的那一段程序,为什么最后输出的结果是:5 5 5 5 5,因为在setTimeOut的回调函数内部中,变量i都指向外层同一个变量,而i的最后一次赋值为5,故最后的结果是: 5 5 5 5 5。
在函数内部,我们可以访问到函数外部的变量,但是当我们在函数内部定义了一个与外部相同的变量名时,外部同名的变量就会被屏蔽。
var a = 10;
(function(){
console.log(a); // undefined
var a = 0;
console.log(a); // 0
})()
console.log(a); // 10
全局变量挂载到window对象
如果是在浏览器环境下,使用var来声明一个变量,会默认将其该变量挂载到window对象。在上面的代码中,我们说var声明的变量是方法作用域,当方法中重新声明了一个与全局相同的变量名(假设为a),那么我们将不能通过直接访问变量名访问到全局变量,而只能访问到方法内部的变量。当我们确实需要使用到该全局变量时,我们可以使用window.a来进行访问。
var a = 10;
(function(){
console.log(a); // undefined
var a = 0;
console.log(a); // 0
console.log(window.a); // 10
})()
console.log(a); // 10
ES6之后的变量声明——let、const
天下苦var久矣,一声惊雷落下,ES6规范横空出世,同时带来了新的变量声明关键字let和const。
变量提升?不存在的
使用let和const关键字声明的变量不存在变量提升。变量在什么定义,才能从什么时候开始使用。
console.log(a); // ReferenceError
console.log(b); // ReferenceError
let a = 10;
const b = 20;
更小的作用域——块级作用域
前面我们提到过,var声明的变量具有方法作用域,而let和const声明的变量则是块级作用域,即在被{}包裹的范围内有效。
{
let a = 10;
}
console.log(a); // ReferenceError
在上面的代码中,变量a的声明在{}内部,即该变量的作用域在该花括号内部,在外部使用console.log(a)访问不到内部的作用域,故报错Reference。
有一点值得注意的是,变量声明关键字let和const虽然有了更小的作用域,但是会有暂存死区的问题。
在定义一个变量之前的区域就是暂存性死区,死区里边拿不到外边的变量,也拿不到作用域内的变量。如果块级作用域中用let或const声明了变量,一开始就会遵循封闭的变量,即使向上的作用域中存在同一样的变量,也无法拿到。
全局变量不会挂载到window对象
let a = 10;
console.log(a); // 10
console.log(window.a); // undefined
const关键字
从字面意义既可以看出,const关键字可以用来声明一个常变量,即虽然他是一个变量,但这个变量必须在声明的时候赋初值,且在运行时不可改变。
const a = 10;
a = 20; //TypeError: Assignment to constant variable.
众所周知,现如今js有八种数据类型,其中包含7种基本的数据类型,以及1种引用类型。对于基本数据类型,其变量存储的是具体的值;而对于引用类型,其变量存储的是指向数据的内存地址(或者指针),即对引用类型的引用。当我们使用const定义一个常变量的时候,我们应该清楚的知道,对于基本数据类型,其值不可改变;对于引用类型,其指向的对象不可改变,但是对象中具体的内容是可以改变的。
const a = {}
a.name = "xiaohe"; // ok
a.age = 18; // ok
a = {
name:"xiaohe",
age:18
}; // TypeError: Assignment to constant variable.
简而言之,被定义为常变量的变量,其不可作为左值。
总结
现在能使用let与const的,都尽量使用。尽量不使用var。
对于var:
- 声明的变量存在变量提升,即所有的变量声明都会提升至作用域最前面。但赋值语句任会保留在原地;
- 函数作用域;
- 在浏览器环境下,全局变量会挂载到window对象。
- 可以重复声明变量,且不报错
let和const:
- 不存在变量提升,需要定义后才能使用。
- 块级作用域,会有暂存死区(感觉这个也不是问题)。
- 全局变量不会挂载到window对象。
- 不可重复声明变量。