你真的会声明变量吗?细说 var, let, const 之不同

发布:elantion 日期:2020-03-17 阅读:195 评论:0

这段时间在面试应聘者时,发现一个很有趣的现象,就是就算是多年开发经验的老司机,在遇到很基础的题目时,也会突然短路,说不上答案。有些是因为平常中没有遇到由此引发的问题,有些老司机是知道怎么用,但不会表达原理。例如接下来要说的变量声明的问题,虽然我们几乎每天都会声明一大堆变量,但大家有谁真会说清楚他们的区别?在实际使用中又会遇到什么问题?让我用最简单的白话告诉大家吧。

var, let, const

首先看一下声明变量的方法,像下面这样,相当简单:

var a = 'letter a';
let b = 'letter b';
const c = 'letter c';

JavaScript 不需要声明变量类型,不像Java、C之类的强语言那么复杂,但这几个方式有什么不一样呢?

常量值和变量值

我们先来说说常量和变量,常量是用 const 来声明,而变量则用 varlet 来声明,它们最大不一样的地方就是,const 声明的常量是只读的,不可修改的,相反 letvar 声明的变量则是可以修改的。不好理解?我们直接用代码来帮助理解:

// 声明常量值
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;

letconst 不一样的地方就是,它可以重新赋值,不管什么值。

// 声明可变变量
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 大半天。所以,后来引进的 letconst 就改正了这个问题,看下面代码:

var fn = function() {
    console.log(a); // Uncaught ReferenceError: Cannot access 'a' before initialization
    let a = 'a';
};
fn();

执行函数 fn 时,控制台就会报错,并且停止继续执行,我们就能及早地发现问题并且修复。

作用域

最后,我们来了解它们最复杂,也是最不同的地方,就是:作用域。我们先来分类,var 是一类,letconst 是另一类,前者是函数作用域,后者是区块作用域。

var 声明变量,它的作用域会限制在函数体内:

var fn = function() {
    var val = 'local';
    console.log(va); // local
};
console.log(val); // Uncaught ReferenceError: val is not defined

letconst 则限制在区块内,也就是大括号内:

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 被同名重复赋值为 1var 允许重复声明,相当于改变变量值,但 let,const 都不可以),第一个 li 里的 i 变量就会由于 i 作用于整个 fn 函数体的原因,也改为了 1。如此类推,最终所有的事件回调里的 i 都会变成 2

救世者:区块作用域

为了解决上面那个问题,ES2015 引进了 letconst,这两个关键字声明的变量都是区块作用域,利用这两个关键字,我们再也不用担心作用域混乱的问题了,像上面的那个代码就可以改造成这样:

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 了。

无关键字声明变量

除了上面使用 varletconst 来声明变量之外,我们还有超级神奇的无关键字声明方法,看下面代码:

var fn = function() {
    a = 'a';
};
fn();
console.log(a);

你们猜控制台会输出什么?会正常输出 a 哦,是不是很神奇。无关键字声明的变量都会变成全局变量,上面的代码跟下面代码是一样的道理。

// 假设运行环境是浏览器
var fn = function() {
    windows.a = 'a';
};
fn();
console.log(windows.a); // 'a'

这种写法虽然是可运行,也符合标准要求,但为了避免别的同事抱怨你的变量名污染环境,真不建议大家用这种方法声明变量,会影响友谊的哦。

其它

哦对了,差点忘记很重要一点,就是 letconst 必须在严格模式下才可以使用哦,也就是我们常在文件头放的那个 use strict;,至于为什么用了它才能使用 letconst ?那就有点复杂了,我们下次讨论一下吧,拜拜个喵~