你不知道的JavaScript——从var到const的演变与闭包的魅力

102 阅读9分钟

JavaScript的历史之旅:从var到const的演变

嘿,各位JavaScript爱好者们!今天咱们来聊聊JavaScript中变量声明那点事儿。你是否曾经被var、let和const弄得晕头转向?别担心,今天我们就以一种轻松幽默的方式来揭开它们神秘的面纱,看看这些变量声明是如何从一个简单的网页脚本语言发展成为企业级应用开发利器的一部分。

var的时代

最早的JavaScript只有var这一个关键字用来声明变量。但是,var有一个致命的缺点:它的作用域是函数级的,而不是块级的。这意味着即使在if语句或者循环这样的代码块内声明了一个变量,这个变量在整个函数内部都是可见的。这种特性有时候会带来意想不到的结果,就像在一个拥挤的房间里试图悄悄地告诉某人一个秘密,结果所有人都听见了!

而且,使用var声明的变量还会挂在全局对象(如浏览器中的window)上。这就像是你在公共场合大声说话,不仅你的朋友听到了,连路过的人都知道了。显然,这不是我们想要的。

简单介绍一下块级作用域和函数作用域,块级作用域就是zai{}里面声明的变量所处的词法环境,函数作用域是在函数里面声明的变量所处的词法环境

可以看到以下代码

if (false) {
    var a = 1
}
console.log(a)//undefined

if里面的条件为假,是不会执行里面的代码的,但是我们的仍然被声明了,但是没有赋值,这是因为var是函数作用域的,而if(){}这是块级作用域,var声明时会进行变量的提升,简单来说就是a被提升到全局作用域了,但是赋值不会发生,因为if里面的代码不会执行简单用代码来解释一下

var a
if(false){
    a=1
}
console.log(a)//undefined

可以看到我们的a被挂到window,所有人都可以访问到a image.png

es6带来的变革:let和const

时间来到2015年,ES6(现在称为ES2015)横空出世,带来了两个新的变量声明方式:letconst。这两个新成员的加入,终于让JavaScript有了真正的块级作用域。这意味着你可以更加精确地控制变量的作用范围,就像给你的秘密加上了一把锁,只让特定的人听到。

let: 变量的新时代

let允许你在块级作用域内定义变量,解决了之前var所带来的混乱局面。想象一下,你现在可以在一个房间里的一个小角落里悄悄地说话,而不必担心整个房子的人都能听到。此外,let还解决了变量提升的问题,使得代码更易于理解和维护。

用let和const声明的变量收到块级作用域限制,其实也会变量提升,只是提升到TDZ(暂时性死区),如果我们先使用再声明就会报错,而不是显示undefined

比如以下代码

console.log(a);//ReferenceError: Cannot access 'a' before initialization
let a = 1

const: 不可变的承诺

与let不同的是,const用于声明常量。但这里有一个小陷阱:对于基本数据类型(如字符串、数字等),一旦赋值就不能再改变;但对于复杂数据类型(如对象和数组),虽然引用本身不能改变,但是其内容是可以修改的。这就像是你承诺永远不搬家,但可以随意更换家里的家具一样。

以下代码,我们给a赋值一个简单数据类型数字型,当我们修改a的值报错

const a = 2
a = 3
console.log(a);//TypeError: Assignment to constant variable.

而对象我们是可以修改的,看以下代码,我们添加了一个新的对象

        const age = 18;
        const friends = [
            {
                name: '小吴',
                hometown: '上饶',
                college: '湖南工业大学',

            },
            {
                name: '小谢',
                hometown: '赣州',
            }
        ];
        friends.push({ name: '小邓', hometown: '宜春' })
        const newFriends=friends
        

可以看到并没有报错,我们新添加的对象成功添加进了friends数组的 image.png

我们再来看一段代码,将friends赋值给newFriends,然后给newFriends加一个新的对象,我们再访问friends

        const friends = [
            {
                name: '小吴',
                hometown: '上饶',
                college: '湖南工业大学',

            },
            {
                name: '小谢',
                hometown: '赣州',
            }
        ];
        const newFriends=friends
        newFriends.push({ name: '小宋', hometown: '宜春' })

可以看我们的friends也加入小宋 image.png 这是为什么呢?我们来引进栈和堆的概念来解释

内存分配的秘密——栈和堆

谈到变量,不得不提的就是内存分配。JavaScript中的变量存储分为栈(stack)和堆(heap)。简单数据类型通常存储在栈中,因为它们占用的空间较小且大小固定(JS是弱类型的语言,当我们赋值时,就定义了这个数据的类型);而复杂数据类型则存储在堆中,因为它们的大小可能会变化,所以我们需要引用堆,给其分配大空间,在栈里面存储对应的堆的地址。理解这一点有助于我们更好地管理内存,避免不必要的性能损耗。 我们来通过一个图解来更好的理解上述的概念

当我们存储复杂数据类型时,存储的是复杂数据类型所存放的堆的地址 image.png

const 声明的简单和复杂数据类型,简单数据类型会直接存放在栈,复杂数据类型栈里面存储的是复杂数据类型所存放的堆的地址,而const只要栈里面的内容没有修改,就不会报错 理解这一点我们就可以很好的解释为什么给a重新赋值会报错,而给数组加一个对象不会报错,还有为什么对newFriends操作,friends的值也会改变

当我们给修改复杂数据类型,实际上是修改堆里面的内容,并没有修改我们栈里面的所存放的地址,const只要栈里面的内容没有被修改就不会报错

当我们把friends赋值给newFriends,实际上是把friends数组存放数据的堆的地址给newFriends,当我们修改newFriends,实际上是通过栈所存放的地址,找到堆,修改堆里面的内容,而newFriends和friends公用一个地址,所以friends也会被修改

image.png

修改堆的内容了 image.png

到这里你肯定明白了为什么了,又学到了新知识

闭包——代码中的魔法

我们看下面的代码,你会觉得会打印什么呢,是不是认为会打印0-9,我也不卖关子直接告诉你,这是错误的,其实会打印10个10,这是为什么呢

for (var i = 0; i < 10; i++) {
            setTimeout(() => {
                console.log(i);
            }, 1000);
        }

什么是闭包

要搞清楚上面的就要明白什么是闭包,那么什么是闭包呢

函数作为返回值

function fn1() {
    let a = 1
    function fn2() {
        console.log(a)
    }
    return fn2
}
let fn3 = fn1()
fn3()//1

函数作为参数

function fn1(fn){
    let a=1
    fn()
}

const a=2
function fn2(){
    console.log(a);
    
}
fn1(fn2)//2

fn1函数返回了fn2函数,这个fn2函数就形成了闭包,简单来说,闭包会阻止fn1()函数的销毁,fn1词法作用域的销毁,让我们可以访问定义这个函数的当前作用域 什么意思呢

function fn1() {
    let a = 1
    fn2()
}

let a = 2
function fn2(bar) {
    console.log(a);

}

fn1()//2

是定义函数的地方,fn2定义在全局作用域,当在fn2的函数作用域没有找到,回到上一级作用域寻找,也就是全局作用域,找到了a相信大家明白了上面的意思

现在我们来解释一下,循环为什么是打印10个10, var是函数作用域,而for(){}是块级作用域,var会变量提升,也就是提升到全局作用域,所以每一个for(){}访问的都是全局作用域的i,

JavaScript 是单线程语言,setTimeout 是异步操作,它不会立即执行,而是被放入“任务队列”,等主线程的同步任务执行完之后才会执行。

也就是说,这 10 个 setTimeout 回调会在 for 循环结束后才开始执行。,此时i=10,由于是var声明的变量提升,所所有的循环访问的都是i都是10所以打印了10个10

当我们用let声明

for (let i = 0; i < 10; i++) {
            setTimeout(() => {
                console.log(i);
            }, 1000);
        }

let 是块级作用域我们看setTimeout(()=>{},1000) 这是不是一个闭包,其实就是我们的函数作为参数,给setTimeout()这个函数传了一个匿名函数的参数一秒钟后执行这个函数,这个就是我们的函数作为参数,这也是闭包,还记不记得我们闭包的特性,会能够“记住”并访问到它的词法作用域(即定义它的那个作用域),即使这个作用域在匿名函数实际执行时已经不再活跃。我们前面提到let是块级作用域,就是每一个循环里面的i是独立的,而我们的闭包,会记得当前词法作用域,所以会打印0-9

结语

从var到let和const,再到闭包,JavaScript经历了巨大的变革。这些变化不仅使代码更加清晰、易于维护,也为构建大型应用程序提供了坚实的基础。如今,JavaScript已经不再是一个只能用来做页面交互的小脚本语言,而是成长为一门能够支撑起整个Web生态系统的强大编程语言。通过理解闭包,我们可以写出更加灵活、功能强大的代码。希望这篇博客不仅能让你对JavaScript中的变量声明有一个全新的认识,还能激发你探索闭包这一神奇特性的兴趣。记住,无论技术如何发展,保持学习的热情和好奇心总是最重要的!