你真的会声明变量吗?细说 var, let, const 之不同
这段时间在面试应聘者时,发现一个很有趣的现象,就是就算是多年开发经验的老司机,在遇到很基础的题目时,也会突然短路,说不上答案。有些是因为平常中没有遇到由此引发的问题,有些老司机是知道怎么用,但不会表达原理。例如接下来要说的变量声明的问题,虽然我们几乎每天都会声明一大堆变量,但大家有谁真会说清楚他们的区别?在实际使用中又会遇到什么问题?让我用最简单的白话告诉大家吧。
var, let, const
首先看一下声明变量的方法,像下面这样,相当简单:
var a = 'letter a';
let b = 'letter b';
const c = 'letter c';
JavaScript 不需要声明变量类型,不像Java、C之类的强语言那么复杂,但这几个方式有什么不一样呢?
常量值和变量值
我们先来说说常量和变量,常量是用 const
来声明,而变量则用 var
和 let
来声明,它们最大不一样的地方就是,const
声明的常量是只读的,不可修改的,相反 let
和 var
声明的变量则是可以修改的。不好理解?我们直接用代码来帮助理解:
// 声明常量值
const NAME = 'James';
// 按照大众的习惯,常量一般使用全大写,用下划线分割
// 例如:MY_CAR
const OBJ = { child: 'Ella' };
// 1 、不可以重新赋值
// 此处报错
NAME = 'Wade'; // Uncaught TypeError: Assignment to constant variable.
// 2、可以改变对象的属性值
// 此处可以正常执行,OBJ.child 会变成 'Dva'
OBJ.child = 'Dva';
// 重新赋值仍是不允许的
OBJ = { child: 'Dva' }; // Uncaught TypeError: Assignment to constant variable.
// 3、初始化常量时,必须赋值
// 像下面没有赋任何值就会报错
const NONE; // Uncaught SyntaxError: Missing initializer in const declaration
// 赋什么值不要紧,即使 undefined 也不会报错
const DEFINED = undefined;
而 let
跟 const
不一样的地方就是,它可以重新赋值,不管什么值。
// 声明可变变量
let name = 'James';
let obj = { child: 'Ella' };
// 下面都可以正常执行
name = 123;
obj = [1,2,3];
变量声明提升
在事情变得复杂之前,我们先看一个有趣的现象,我们日常撸码的过程中,偶尔会不小心调用一个忘记声明的变量名,这时浏览器的控制台就会温柔地提醒你:Uncaught ReferenceError: xxx is not defined
。但有些情况却不会,我们来看一下这段代码:
var fn = function() {
console.log(a);
var a = 'a';
};
当我们执行 fn
函数时,你猜控制台会不会报错?不会,会正常输出 undefined
。这就是 var
声明变量的一个特殊地方,它可以在js预编译时,提升变量的作用域到函数的顶部(但没赋值)。这就很混乱了,我引用了没声明的变量,你应该给我报错,并且停止执行才对啊,不然我都不知道程序那儿错了,要debug 大半天。所以,后来引进的 let
和 const
就改正了这个问题,看下面代码:
var fn = function() {
console.log(a); // Uncaught ReferenceError: Cannot access 'a' before initialization
let a = 'a';
};
fn();
执行函数 fn
时,控制台就会报错,并且停止继续执行,我们就能及早地发现问题并且修复。
作用域
最后,我们来了解它们最复杂,也是最不同的地方,就是:作用域。我们先来分类,var
是一类,let
和 const
是另一类,前者是函数作用域,后者是区块作用域。
用 var
声明变量,它的作用域会限制在函数体内:
var fn = function() {
var val = 'local';
console.log(va); // local
};
console.log(val); // Uncaught ReferenceError: val is not defined
而 let
和 const
则限制在区块内,也就是大括号内:
var fn = function() {
{
let val = 'block';
console.log(val); // block
}
console.log(val); // Uncaught ReferenceError: val is not defined
};
console.log(val); // Uncaught ReferenceError: val is not defined
为什么要弄区块作用域呢?函数作用域有啥不好?我们看一个经典的老问题:遍历数组
var fn = function() {
var li = document.querySelectorAll('li'); // length = 3
for(var i = 0; i < liEls.length; i++) {
li[i].addEventListener('click', function() { console.log(i); });
}
}
fn();
当用户去点击任意 li
元素时,控制台输出全是 2
,而不是我们期待对应的 0,1,2
,为什么呢?
这是因为 i
变量的作用域在整 个 fn
函数体内,在 fn
函体内的所有引用 i
的变量,都是同一个变量,如果用代码来解释的话,就像下面这样子:
var fn = function() {
var i = 0;
li[0].addEventListener('click', function() { console.log(i); });
var i = 1;
li[1].addEventListener('click', function() { console.log(i); });
var i = 2;
li[2].addEventListener('click', function() { console.log(i); });
}
fn();
这个 i
最开始被赋值为 0
时,第一个 li
元素点击时,i
会输出 0
,这个没什么问题。但到了下一步, i
被同名重复赋值为 1
(var
允许重复声明,相当于改变变量值,但 let,
,const
都不可以),第一个 li
里的 i
变量就会由于 i
作用于整个 fn
函数体的原因,也改为了 1
。如此类推,最终所有的事件回调里的 i
都会变成 2
。
救世者:区块作用域
为了解决上面那个问题,ES2015 引进了 let
,const
,这两个关键字声明的变量都是区块作用域,利用这两个关键字,我们再也不用担心作用域混乱的问题了,像上面的那个代码就可以改造成这样:
const FN = function() {
const li = document.querySelectorAll('li'); // length = 3
for(let i = 0; i < liEls.length; i++) {
li[i].addEventListener('click', function() { console.log(i); });
}
}
拆解后,其实是这样:
var fn = function() {
{
let i = 0;
li[0].addEventListener('click', function() { console.log(i); });
}
{
let i = 1;
li[1].addEventListener('click', function() { console.log(i); });
}
{
let i = 2;
li[2].addEventListener('click', function() { console.log(i); });
}
}
每个大括号内的 i
都是唯一的,相互不影响,每个点击事件的回调函数引用的 i
都是各自大括号内的 i
,所以就能正确地输出 0, 1, 2
了。
无关键字声明变量
除了上面使用 var
,let
和 const
来声明变量之外,我们还有超级神奇的无关键字声明方法,看下面代码:
var fn = function() {
a = 'a';
};
fn();
console.log(a);
你们猜控制台会输出什么?会正常输出 a
哦,是不是很神奇。无关键字声明的变量都会变成全局变量,上面的代码跟下面代码是一样的道理。
// 假设运行环境是浏览器
var fn = function() {
windows.a = 'a';
};
fn();
console.log(windows.a); // 'a'
这种写法虽然是可运行,也符合标准要求,但为了避免别的同事抱怨你的变量名污染环境,真不建议大家用这种方法声明变量,会影响友谊的哦。
其它
哦对了,差点忘记很重要一点,就是 let
和 const
必须在严格模式下才可以使用哦,也就是我们常在文件头放的那个 use strict;
,至于为什么用了它才能使用 let
和 const
?那就有点复杂了,我们下次讨论一下吧,拜拜个喵~