var、let、const相关问题(超详细)

93 阅读14分钟

众所周知,JS不像其他编程语言那样,定义变量的时候需要指定对应的数据类型,这也是前端开发者写代码的时候觉得很kimoji的地方。

虽然说var一招鲜吃遍天,但是对于初学者来说可能对于它自身所存在的缺陷,认知还是不够深入,以至于不能理解为什么后面ES6还需要出let和const来定义变量。

以下是个人对于var、let、const的一些知识点的整理,希望能对各位学习前端的小伙伴提供些许的帮助,学海无涯,一起共勉。有些描述可能有不正确的地方,希望各位大佬发现描述不正确的地方可以留言批评指正 (我只是个前端菜鸡,也算是做个笔记给自己看)

对于JS变量声明的发展,主要分为两个时间段:ES5以前(var);ES6(let、const)

ES5及其以前:

背景:js设计之初,只是负责执行一些有关网页的逻辑代码,当时的网页还有浏览器的发展与现在相距甚远,js并没有承担很大工作量,因此那时的js代码远没有现在的复杂度,以至于有的网页只需要几十行、几百行的代码。而js的发展,谷歌v8引擎的出现,js的应用前景越来越广阔,js的应用场景越来越多,代码复杂度不断上升,前端项目工程越来越复杂。以至于当初设计js时没有考虑到的一些问题,也随之暴露出来,并且越来越阻碍JS的发展,一直无法与主流的编程语言的语言相提并论。

很多语言中都有块级作用域,但此时的JS没有,它使用var声明变量,以函数(function)为界,每个函数内部拥有一个局部作用域,任何其他的块(包括普通代码块,for循环、if、while等代码块)不存在局部作用域,大括号“{}” 限定不了var的作用域。

用var声明的变量具有变量提升(declaration hoisting)的效果。

var

定义变量的方式只有一个:var

变量的作用域分别有全局作用域局部作用域(也称函数作用域)

  • 全局作用域:作用于所有代码的执行环境
  • 局部作用域:作用于函数内的代码执行环境 与之相对应的自然就是全局变量和局部变量
  • 全局变量是定义在全局范围或者未经var声明直接赋值的变量
  • 局部变量是定义在函数中的变量

var声明变量带来的问题

  1. 允许重复声明变量,导致数据被覆盖
  2. 变量提升,导致怪异的数据访问,还有闭包问题
  3. 全局变量挂载到全局对象(window),导致全局对象成员污染

1.允许重复声明变量

我们初学者往往对这个var可以重复声明变量这个特点不以为意,甚至觉得有的时候还挺好用的,没觉得有什么问题。但在其他语言,这是不允许的。

其实这并不好,项目体量大很容易因为变量覆盖而出错,且找错很麻烦,这导致js不适合开发大型项目

var a = 1;
//这个函数的目的就是打印等于1的a变量
function printA(){
    console.log(a)
}

//假设这里有一千行代码
//而此时我忘了之前定义过a,想着定义一个新变量,但出于习惯,用了a的名字
var a = 2;

printA(); //此时得到的东西就不是我们想要的了

2. 变量提升

① 变量提升会导致了代码逻辑改变👇

//源代码
if (Math.random() < 0.5) {
    var a = "abc";
    console.log(a);
}
else {
    console.log(a);
}

console.log(a);

这段代码我们想要的逻辑是:
如果随机数小于0.5,则声明变量a,赋值为abc,输出a
如果不小于0.5,则输出a (当然,此时a未定义,本应报错。可是因为变量声明提升,就是不会报错)

由于变量提升,导致了代码逻辑出现问题👇

//变量提升后逻辑上代码变成了这个样子
var a;
if (Math.random() < 0.5) {
    a = "abc";
    console.log(a);
}
else {
    console.log(a);
}

console.log(a);

先声明一个变量a
如果随机数小于0.5,赋值为abc,输出a
否则输出a

② 变量提升导致闭包问题👇

经典闭包题目:在一个div id="divBox"容器里面定义10个按钮,如何实现点击相应的按钮输出相应的序号?(点击按钮1 输出1,点击按钮4 输出4)

面对这种题目,很容易就写出这样的答案:

//获取这个容器div
var div = document.getElementById("divBox");
for (var i = 1; i<=10; i++) {
    var btn = document.createElement("button");
    btn.innerHTML = "按钮" + i;
    div.appendChild(btn);
    btn.onclick = function () {
        console.log(i);
    }
}
//但这是错了,这样写无论点击每个按钮,输出的都是11

这忽略了闭包的问题,而造成这个问题的本质原因,就是变量提升。变量提升导致的逻辑结构其实是这样的↓

var i;  //i声明提升了,它是一个全局变量,而不是每个循环单独的变量
var div = document.getElementById("divBox")
for (i = 1; i <= 10; i++) {  //每个按钮使用的都是同一个变量i,而不是每个循环有一个独特的变量
    var btn = document.createElement("button");
    btn.innerHTML = "按钮" + i;
    div.appendChild(btn);
    btn.onclick = function () {
        console.log(i); 
    }
}
//你可以打印一下全局的i
console.log(window.i)  //输出11

//btn.onclick绑定的事件只是点击按钮的时候输出i,当你点这个按钮的时候,循环早就结束了,此时i是11,
//而无论哪个按钮,绑定的都是那个i,来来去去都是它,所以输出的全是11

这个问题如何解决?如果是说i有问题,那我在for循环里面定义一个j=i,会不会就可以了呢?

var div = document.getElementsByClassName("divBox")[0];
for (var i = 1; i<=10; i++) {
    var j = i;  //在循环里面定义一个j接收i此时的值,然后打印j,可不可以呢?
    var btn = document.createElement("button");
    btn.innerHTML = "按钮" + i;
    div.appendChild(btn);
    btn.onclick = function () {
        console.log(j);  //打印j
    }
}

//这样是不是就可以呢?不行的,输出的全是10

显然,我都这么问了,肯定是不可以的,因为j也会变量提升,j也会成为全局变量,for不是函数,因此for里面定义的变量不像函数里面定义的那样可以变成局部变量

var i;
var j;
var div = document.getElementsByClassName("divBox")[0];
for (i = 1; i<=10; i++) {
    j = i;
    var btn = document.createElement("button");
    btn.innerHTML = "按钮" + i;
    div.appendChild(btn);
    btn.onclick = function () {
        console.log(j);
    }
}

console.log(window.i)  //11
console.log(window.j)  //10

此时网上就会有人说,闭包问题用立即执行函数解决,确实,这个题目可以这么解↓↓↓

var div = document.getElementsByClassName("divBox")[0];

for (var i = 1; i<=10; i++) {
    var btn = document.createElement("button");
    btn.innerHTML = "按钮" + i;
    div.appendChild(btn);
    (function (i) {
        btn.onclick = function () {
        console.log(i);
        }
    })(i)
}
//立即执行函数的能够解决的原因,无非就是每次循环的时候创建了一个函数作用域
//此时点击事件绑定的i,不再是全局的i,而是当时循环里面的i

在过往的知识中,想要for循环每一圈都用的是自己的i,我们需要用立即执行函数来解决,这样弄得代码不够美感,出现这种情况都要往外套一层立即执行函数,也太恶心了吧= =。

其实这个问题,只需要把最开始那个代码中的var换成let就可以解决,是不是觉得let一下子高大上了起来??

3. 全局变量挂载到window

全局定义的变量其实是挂载到window对象里的,全局变量多了就会导致window的属性越来越多,污染全局对象

var abc = "123";  
console.log(window.abc);  //输出123

并且window里面的属性和方法是可以随便覆盖的var console = "abc"; console.log方法就不能用了,console在window里本来就是系统写好的对象

并且开发中name是很常用的一个变量名,但是window.name是一个有意义的全局属性,如果在全局中var name = “abc” 就会把window原本的name属性覆盖掉

ES6出现之后

ES6不仅引入let关键字用于解决变量声明的问题,同时引入了块级作用域的概念 (在此之前只有全局作用域和函数作用域两种作用域)

这里就必须搞清楚一个概念:什么是块级作用域?块级作用域和局部作用域有什么区别?

块级作用域和局部(函数)作用域是不同概念,每个花括号内部就是一个块级作用域,常见有流程语句、循环语句、函数体。而局部作用域是函数(function)里面的声明的变量才会生成

块级作用域: 所谓的“块”,就是用一对花括号括起来的一块代码,代码执行时遇到花括号 {} ,会创建一个块级作用域,花括号结束,销毁块级作用域

对于 let 和 const 来说,不管是全局作用域,还是函数作用域,其实都是块级作用域,无非是大块还是小块的问题。

let a = 123; //全局作用域定义a

{
    let a = 456; //块级作用域定义a
    console.log(a); //使用的是块级作用域中的a  输出456
}

console.log(a)  //输出123

let

let在用法上几乎和var相同,但作用域限定在块级,let声明的变量不存在变量提升,解决了var之前出现的问题。

let特点:

  1. let声明的变量不会挂载到全局对象window
  2. let声明的变量只有当前作用域或者里面的作用域可以使用(和var差不多)
  3. let声明的变量,不允许当前作用域范围内重复声明
  4. 使用let不会有变量提升

1. let声明的变量不会挂载到全局对象window

单单是这个优点,就已经足以抛弃var该用let了好吧

let a = 123;
console.log(window.a)  //undefine

❗等等,有个问题

var声明的全局变量是window的属性,但是let声明的全局变量不是window的属性那是什么?

经过我的测试,let全局声明的变量是放在Script这个域里面的

image.png

在谷歌浏览器的控制台里,我们可以看到,有 4 个作用域(scope),分别是global(全局)、script(脚本)、local(局部)、block(块级)。

Global作用域里的内容全是window对象的属性。

不同情况下let声明的变量可能出在不同的作用域里 比如我下面这端代码👇

let v1 = 10;  //Script域
if (true){
    let v2 = 20;  //Block域
    console.log(v1); //输出10
}
function f1(){  
    let v3 = 30;  //Local域
    if (true){  
        let v4 = 40;  //Block域
        console.log(v3);  //输出30
    }
}

function f2(){
    let v5 = 50;   //Local域
    let v6 = function() {  //Local域
        let v7 = 70;//嵌套函数里声明的也是Local域
        console.log(v7); 
    }
    return v6;
}

f1();
f2()();

//测试不同数据类型在全局中声明的效果
let v8 = "12";  //Script域
let v9 = true;   //Script域
let v10 = {};   //Script域
let v11 = function(){}   //Script域

//测试循环语句
let i = 1;
while(i <= 5){
    let v12; //Block
    i++;
}

for(let v13 = 1; v13 <= 5; v13++){  //v13在Block域
    let v14;  //Block域
}

可以看到,let声明的变量中,全局的在Script域,函数中的在Local域,条件判断语句和循环语句里的在Block域。

2. let声明的变量只有当前作用域或者里面的作用域可以使用

外面的不能访问里面的,里面的可以访问外面的

// 外面不能访问里面的
function getVal(boo) {
    if (boo) {
        let val = 111;  //定义在当前块级作用域
        console.log(val);  //打印当前作用域的a
    } else {
        //这是另外一个块级作用域,该作用域中找不到a
        console.log(val);
    }
    // 外面的块级作用域不能访问 val
    console.log(val);
}
getVal(1)//     111      报错
getVal(0)//   报错

------------------华丽的分界线-----------------

// 里面的可以访问外面的
function getVal(boo) {
    let val = 111;
    if (boo) {
        // 里面的块可以访问外面的块
        console.log(val);
    } else {
        // 里面的块可以访问外面的块
        console.log(val);
    }
    // 这里才可以访问 val
    console.log(val);
}
getVal(1)//   111   111
getVal(0)//   111   111

------------------华丽的分界线-----------------

if (true) {
    let i = 1;
    console.log(i);  // 1
}
console.log(i);  //报错

3. let声明的变量,不允许当前作用域范围内重复声明

在块级作用域中用let定义的变量,在作用域外不能访问

let a = 123;
let a = 456; // 报错。检查到,当前作用域(全局作用域)已声明了变量a

//不同作用域内可以声明同名变量
let a = 123;  //声明在全局
if (true) {
    let a = 456;  //声明在当前if这个块级作用域
    console.log(a);  //打印当前作用域的456
}
console.log(a);   //123

4、使用let不会有变量提升,因此,不能在定义let变量之前使用它

用了let的代码逻辑就更符合其他常规的语言了,不会代码运行效果上不会出现恶心的变量提升

console.log(a)
let a = 123;   //报错,因为从运行效果上,let是不会变量提升的
-----------------------------------------------------------------
console.log(a)
var a = 123;   //因为变量提升而不会报错

⭐但其实吧⭐

底层实现上,let声明的变量实际上也会有提升 ,但是,提升后会将其放入到 “暂时性死区” ,如果访问的变量位于暂时性死区,则会报错:“Cannot access 'a' before initialization”。当代码运行到该变量的声明语句时,会将其从暂时性死区中移除。

⭐在循环中,用let声明的循环变量,会特殊处理,每次进入循环体,都会开启一个新的作用域,并且将循环变量绑定到该作用域(每次循环,使用的是一个全新的循环变量)

在循环中使用let声明的循环变量,在循环结束后会销毁

这也就解释了上面var涉及到的闭包问题为什么用let就可以完美解决

let div = document.getElementById("divBox")
//用let就不会出现任何问题
for (let i = 1; i <= 10; i++) {
    var btn = document.createElement("button");
    btn.innerHTML = "按钮" + i;
    div.appendChild(btn);
    btn.onclick = function () {
        console.log(i); //输出11
    }
}

const

const和let完全相同,区别仅在于用const声明的变量,必须在声明时赋值,而且不可以重新赋值

⭐实际上,在开发中,应该尽量使用const来声明变量,以保证变量的值不会随意篡改,原因如下:

  1. 根据经验,开发中的很多变量,都是不会更改,也不应该更改的。 (开发时可以上来就const,需要重新赋值的量再改成let)

  2. 后续的很多框架或者是第三方JS库,都要求数据不可变,使用常量可以一定程度上保证这一点。

注意的这 3 个细节

1. 常量不可变,是指声明的常量的内存空间不可变,并不保证内存空间中的地址指向的其他空间不可变。(也就是说const一个对象,虽然不能修改这个对象本身,但你还是可以修改这个对象里面的属性的)

const a = {
    name: "newcy",
    age: 44
};
a.name = "abc";  //尝试改变a.name的属性值
console.log(a) // 输出{name: 'abc', age: 44}
可以更改,因为const存的始终都是这个对象的地址

2. 常量的命名方式

  • 特殊的常量:该常量从字面意义上,一定是不可变的,比如圆周率、月地距地或其他一些绝不可能变化的配置。通常, 该常量的名称全部使用大写,多个单词之间用下划线分割

const PI = 3.14;

const MOON_EARTH_DISTANCE = 3245563424; //月地距离

  • 普通的常量:使用和之前一样的命名即可

3. 在for循环中,循环变量不可以使用常量,for in循环可以

var obj = {
    name:"newcy",
    age:44
}

for (const prop in obj) {
    console.log(prop)
}
//可以成功输出

循环变量不可以使用常量很容易理解,for (const i = 1; true ; i++) {} 你都把i用常量写死了,还怎么++啊

⭐而for in循环可以使用常量,是因为每一次循环都是指向对象的地址,一个对象的内存空间地址一般来说是不会改变的

var、let、const的区别

共同点

var、let、const都可以声明变量

区别:

  1. var具有变量提升的机制,let 和 const 没有变量提升的机制
  2. var可以多次声明同一个变量,let 和 const 不可以多次声明同一个变量
  3. var、let声明变量,const声明常量
  4. var声明的变量没有自身的块级作用域,let 和 const声明的变量有自身的块级作用域