到ES6为止,我们一共有4种方式可以定义变量,今天我们就来聊一聊这4种方式都会有怎样的行为。
一、不使用任何关键字
a = 1
在大部分的学习资料上对这行代码的解释是:声明一个全局变量。这么说没问题,但是不严谨。有如下代码:
function fn() {
var a;
function fn2() {
a = 1 //只是单纯的赋值,并没有声明全局变量
}
}
从上述代码可以看出,a=1不再是声明全局变量,而仅仅是单纯的赋值。因此,更严谨一点的说法应该是只有在不存在变量a的情况下才会隐式地声明一个全局变量
二、var
var a = 1 //在当前作用域内声明变量a
但是用var定义变量会存在一个非常大的问题。先看下面的代码
function fn() {
if (true) {
console.log(a) //理论上此处应打印 a is not defined
} else {
var a;
}
}
fn()
按照我们正常的思路,应该打印出'a is not defined',但是事实上代码执行完打印出的结果是undefined,这不符合我们一般的理解。实际上,用var定义的变量会存在一种叫做变量/函数声明提升的行为。上述代码和下面代码等价:
function fn() {
var a;
if (true) {
console.log(a) //undefined
} else {}
}
fn()
下面再看一个例子:
{
var a = 1;
window.fn = function() {
console.log(a)
}
}
//需求: 有且仅有fn为全局函数,a不可被外部(window)访问
通过上面的解释我们知道,a变量声明会提升,于是全局存在了变量a和函数fn,显然不符合我们的要求。
我们知道要想一个变量不被外部访问,可以用函数的方式来包裹变量,于是有了以下的代码:
function fn2() {
var a = 1;
window.fn = function() {
console.log(a)
}
}
fn2()
这种写法下变量a的确不会被外部(window)访问,但是问题由来了,全局下多了fn2函数,这也不符合我们的需求。那既然这样,我们会想到,那我把fn2藏起来不就行了,于是我写出了这样的代码:
//立即执行匿名函数
(function () {
var a = 1;
window.fn = function() {
console.log(a)
}
}())
我们可以看到,我仅仅是想要让变量a不可被外部(window)访问,就得写一个立即执行匿名函数,太麻烦了。于是let应运而生!
三、let
同样是上述的代码:
{
let a = 1;
window.fn = function() {
console.log(a) //1
}
}
console.log(a) // a is not defined
这种情况下,a变量就像'被困在了大括号里面',你只能在大括号之间活动,出了大括号,就再也访问不到a了。
let还有另外两个特点,其中一个叫做Temp Dead Zone(临时死区),看如下代码:
{
let a = 1
{
console.log(a) // a is not defined
let a = 2
{
let a = 3
}
}
}
也就是说,在let定义的变量还没有初始化之前是无法使用该变量的。
另外一个特点就是不能重复定义
{
let a = 1;
let a = 2; //报错
}
四、const
const只有一次赋值机会,而且必须在声明的时候立马赋值
{
const a; //报错 必须赋值
const b = 1;
b = 2 // 报错
}
刚刚我们用const定义了基本数据类型的值,接下来我们看下定义引用数据类型的值。
const a = {
b: 4
}
//第一种
a.b = 5;
console.log(a) // {b:5}
//第二种
a = {
b: 6
}
console.log(a) //Uncaught TypeError: Assignment to constant variable.
也就是说,用const定义的对象内部的值是可以改变的,但是不能改变对象的引用地址。
到此为止,ES6的4种定义变量的方式就说完了,但是还有一个问题,为什么会发生Temp Dead Zone(临时死区),那我们就说说创建变量的过程。变量创建大体分为3步(忽略分配内存空间什么的):
- 变量声明
- 变量初始化
- 变量赋值
那我们就用var/function/let/const来举例:
- var
function fn(){
var x = 1
}
fn()
fn执行大致进行了以下步骤:
- 进入fn后,声明x
- x初始化为undefined
- 执行代码, x赋值为1
即代码执行之前就已经完成了声明和初始化的过程
- function
fn()
function fn() {
console.log(1)
}
执行步骤大致如下:
- 找到所有用function声明的变量
- 初始化并赋值
- 执行代码
即代码执行之前就已经完成了声明、初始化和赋值的过程
- let
{
let x = 1 //x初始化为1
//let x //x初始化为undefined
x = 2
}
执行步骤大致如下:
- 找到所有用let声明的变量
- 执行代码
- 初始化x=1
- x赋值为2
- const
const由于必须在声明的时候初始化,所以只存在声明和初始化的过程,不存在赋值过程
由上述解释我们知道: 临时死区产生的原因是用let定义的变量在没有被初始化之前是无法被使用的
下面有个有意思的现象:
就是说,如果let声明变量的初始化过程失败了,那么:
- 变量将永远处于声明状态
- 无法再次对变量进行初始化
- 由于变量无法被初始化,所以永远处在临时死区
面试题
for (var i=0; i<6;i++) {}
console.log(i) //6
这个没啥好说的,由于变量提升,i上升为全局变量,执行完循环之后被赋值为6
for (var i=0; i<6;i++) {
function fn() {
console.log(i)
}
/** 第一种情况,如果直接调用fn
* fn() // 0, 1, 2, 3, 4, 5
*/
/**
* 第二种情况,绑定在button的点击事件上
* btn.onclick = fn // 6
* 这是因为在点击时循环已经结束,i的值是6
*/
}
var tags = document.querySelectorAll('li') //6个
for (var i=0; i
那么怎么才能输出0 1 2 3 4 5呢?
- 第一种: 利用let保存每一次循环的变量
var tags = document.querySelectorAll('li') //6个
for (var i=0; i
- 第二种: 利用立即执行匿名函数,把每次循环产生的变量传入
var tags = document.querySelectorAll('li') //6个
for (var i=0; i
- 第三种: 将循环的变量用let定义
var tags = document.querySelectorAll('li') //6个
for (let i=0; i