前言:
这篇文章将探索JavaScript中var的历史,let、const出现的需求以及他们之间的区别。这里包含两部分:故事解说+技术详解。
朋友们,开始吧!!!
故事解说:三种工匠(var、let、const)的故事
JavaScript是一座繁荣的滨海小镇,有着一块高楼林立的商业区域。
自太古时期,小镇的居民就使用被var造出来的盒子来储存他们的变量,尤其是他们引以为傲的金耀之石。这样做的话,居民有两个选择:
-
可以直接把一个金耀之石放进这种盒子里面(直接赋原始值:
var = value
); -
如果有大量的金耀之石,多到根本就不适合放在一个盒子里;聪明的村民们就会先造一个小仓库,把这些宝藏放在仓库里,用小纸条写上仓库的地址,最后把小纸条放在盒子里面;这样盒子里就只有宝藏的地址了,真是既方便又准确啊!例如:有一个小纸条写着 “东西放在2号仓库的0032集装箱里面”(赋引用值:
var = {}
);小镇还为他们的法律和秩序而自豪,设立了一些规则和程序:
一、商店(作用域)的规则
- 为了维护小镇的和平稳定,一般商店及杂货店等只可以建在山丘上(例如:创建函数会产生它自己的作用域);
- 特殊的商店,像小镇唯一的国际超市,这种可以建在海边(全局作用域);
- 一个商店可以开二级分店、三级分店等等(嵌套函数)。然而,每个下级商店都得建在比上级商店更高的山丘上,这样他们就都有自己地盘地域(嵌套函数作用域);
- 商店可以有特殊服务柜台(流程控制语句),比如:“
if
你年满18,就可以买18J的盒子”,和“for
(对于)你家里的孩子,每个人都可以领一份礼品”(其他控制语句:while、do…while、for、switch、 if…else等等); - 每一个商店的入口旁都必须设置一个"declaration-initialization(声明初始化)"柜台并安排一名守卫来维护保养登记册。登记册会记录每一个刚做出来的盒子的名字然后把棉花塞(
undefined
)放里面,无论盒子是不是空的。(变量提升); - 每一个商店都应该有一个“资源分配”工作室,居民们可以开盒子但是不能自己存放金耀之石,工作人员会把
undefined
拿掉,然后将居民带过来的金耀之石放进盒子里;
二、盒子市场的规则
- 盒子可以被海边或者山丘上的商店生产、采购。(全局变量和局部变量);
- 无论在海边还是哪个山上的商店,都不可以出现两个同样的盒子,居民拿的每一个盒子在该商店都是独一无二的,它们或形状不一样或颜色不一样,反正没有两个一模一样的盒子。(变量名不重复);
- 每一个盒子从出生那一刻就不是空的,因为一旦被造出来就会先塞好棉花进去。盒子里每时每刻都有东西,棉花或金耀之石。(变量提升的影响);
- 山丘商店里的生产的盒子只可以往上送而不可以往下送;比如,二级商店的盒子可以被三级商店或更低的商店使用,而绝不能被一级商店或海边的商店使用。居民出了商店之后,只要他下山了,所有他拿到的盒子都会消失。(变量的生存域);
三、居民们买var
类型的盒子的流程
- 有一天,隔壁老王进了一个商店,挖着鼻孔,站在入口的柜台对守卫叫道“兄弟,给我两个盒子呗,一个黄色一个绿色”。(声明两个变量)
- 守卫叫他们的
var
工匠造一个盒子出来,拿出登记册登记一下,然后塞一点点棉花进去,对老王说“拿着!”。(变量初始化) - 老王拿着盒子在商店里转悠,过了一会儿,他儿子拉来了一马车的金耀之石。直到这时,老王才发现打开不了盒子。然后他到“资源分配”工作室排队;
- 等了两个小时,终于到老王了,老王嘴里嘀咕着“老子等的花都谢了”。不过还是乐呵呵的把所有金耀之石交给这里的工作人员,他们打开盒子,拿掉棉花然后把所有的金耀之石放进去,最后把两个盒子还给老王。
自然而然,这些规则带来了一些奇特的问题:
- 在长时间的排队过程中,老王耐不住性子先去瞅瞅旁边的女服务员,然后在商店转悠转悠,等到没那么多人在过去。然后就忘记他还没有把金耀之石放进盒子里,就向女服务员打开他的盒子炫耀,发现 “TMD怎么全是棉花!?
- 在存完金耀之石后,老王在店子里转悠一会儿后,可能忘记他已经两个装有财富的盒子了,然后去入口找守卫哥们要两个盒子“一个黄色一个绿色”。这个时候非常可怕的事情就发生了,他之前的盒子突然就消失不见了,而且他连感觉都感觉不到!!!(消失的还有那一马车的金耀之石!真是一个败家子!)。然后守卫给他一个黄色和绿色的盒子,里面赛的全是棉花。这是没有警告的!这其实是广为流传的“特殊服务”,嘿嘿。商人果然还是有那么一套滴。
你可以想象事后老王会有多的沮丧,这种情况对于老老实实工作的居民们是多么地糟糕。随着居民们总是莫名其妙的丢掉自己的存好的金耀之石,小镇委员会决定要采取行动。
在2015年的居民代表大会里,他们自豪的宣布已经从其他镇子引进了两项技术:let
盒子制造工艺,const
盒子制造工艺。
他们也介绍了未来商店要进行改造:商店的“特殊服务”一律用let
和const
代替。商店升级后可以在商店里面建小山丘和内置商店。
四、采购let
和const
类型盒子的规则
- 过了一年,老王又来到了那一家商店,虽然很气愤,但是之前也是没有办法。现在新规矩出来了,老王一点都不怕了。他进入商店并在入口的柜台上说出了他想要的类型和颜色的盒子。守卫把他说的记录在登记册。这些信息和盒子会朦胧地出现在墙壁上,他们可以被看见但是不能被使用,被称为“临时区域”;
- 老王带了一些金耀之石。因为盒子没有在声明时就完完整整造出来,所以还用不了;
下面是let
和const
盒子的区别
let
盒子:
- 老王在入口说完他想要的盒子之后,就拿着财富去了一个叫“initialization(初始化)”的柜台。
- 在这个柜台上,他可以选择是要一个空的
let
盒子还是const
盒子,然后把他的金耀之石立马放进去; - 老王说“咋整啊,,那就来个
let
盒子吧”,工作人员就叫工匠造好一个let
盒子塞好棉花然后一并送到“资源分配”工作室,在那里老王的金耀之石才被真正放进盒子里。
const
盒子:
const
盒子及其特殊的一种也是工艺最难的一种。最里用来放东西,一旦放了东西进去后就立马自锁,永远都打不开了。所以工匠们不会随随便便就造出这种盒子出来,除非他们知道里面具体会被放一些什么东西。
- 有一次,老王到了初始化柜台,他想试一试这个
const
盒子; - 他一说要
const
盒子,工作人员就要求他交出他要放的金耀之石。然后叫工匠造出这个盒子,立马把东西放进去,盒子立马就永久的锁住了。
如果你还记得,老王可以直接放金耀之石到盒子里面或者放一张小纸条在里面。小字条里写好了他那一大批金耀之石的存放地址;
- 如果他在
const
盒子放的是金耀之石,那么它永远都不能把它们去掉或更改,哪怕再多放一个金耀之石也是不允许的。因为一锁锁到死; - 然而,要是他放的是一张特殊的小纸条,那就有一点点不一样了;虽然他不能对这个小字条怎么样,但是他可以直接去仓库里存拿金耀之石啊,因为小字条里写好了地址嘛
现在, 我们回到之前的那两个奇特的问题。然后看看引进的var
和const
盒子是不是解决了这两个问题
- 在长时间的排队过程中,老王耐不住性子先去瞅瞅旁边的女服务员,然后在商店转悠转悠,等到没那么多人在过去。然后就忘记他还没有把金耀之石放进盒子里,就向女服务员打开他的盒子炫耀,发现 “TMD怎么全是棉花!?”1. 在长时间的排队过程中,老王耐不住性子先去瞅瞅旁边的女服务员,然后在商店转悠转悠,等到没那么多人在过去。然后就忘记他还没有把金耀之石放进盒子里,就向女服务员打开他的盒子炫耀,发现 “TMD怎么全是棉花!?”
因为var
和const
盒子不会在声明的瞬间就被造出来,它们会在"临时区域"内显示一会儿,直到老王到了"初始化"柜台才会真正被造出来。所以老王知道自己没有盒子,更不用说打开了。如果他真的拿到了盒子,那么装在商店里的警告铃也会报警,警告老王这个盒子有问题。
- 在存完金耀之石后,老王在店子里转悠一会儿后,可能忘记他已经两个装有财富的盒子了,然后去入口找守卫哥们要两个盒子“一个黄色一个绿色”。这个时候非常可怕的事情就发生了,他之前的盒子突然就消失不见了,而且他连感觉都感觉不到!!!(消失的还有那一马车的金耀之石!真是一个败家子!)。然后守卫给他一个黄色和绿色的盒子,里面赛的全是棉花。这是没有警告的!这其实是广为流传的“特殊服务”,嘿嘿。商人果然还是有那么一套滴。
因为所有商店的"特殊服务"都已经没有了,在加上下面这一条规则。这个问题也被完美解决了。
一旦居民在商店入口里登记了他们想要的var
或者const
盒子,这些盒子就不能再次登记了,因为“特殊服务”已经被合法取缔了。如果老王再去守卫那里要一个他已经登记过的盒子,商店的警报铃就会报警
这些精美绝伦的新盒子和规则给JavaScript小镇带来了更繁荣的未来,让大家永远开心地生活下去。
超详细——技术解析:
让我们进入激动人心的技术解析部分!!让我们以技术的视角来理解故事里的var
、let
和const
类型盒子。
如果你不来了解变量提升和作用域,那么不要紧张。我们会用到这一点知识,我会简短介绍一下。如果大家觉得不好理解,我后续再写一篇这方面的文章来详细解读变量提升的作用域。
下面是一些额外的比喻来帮助我们理解小说中的“山丘:
- 为了提高我们对全局作用域、块级作用域和函数作用域的理解。我们可以认为全局作用域就是海边,而山丘就是局部作用域。函数作用域和块级作用域都是局部作用域。如果你站在山丘上,你可以看到海边或者比你当前海拔更低的山丘的全貌,而你站在海边是看不清楚山丘的全貌的。也就是说,局部作用域可以访问全局作用域的变量,而反过来却不行。
- 在C++里面,每一个
{}
都会产生一个小山丘(局部作用域)。小山丘是闭合的,不能从外面访问到里面,很多的{}
会产生很多的小山丘,各个山丘互不联系。 - 在JavaScript中,只有跟在函数后面的
{}
才会产生一个新的小山丘。像跟在if
后面的就不会产生小山丘,里面的东西还是在当前的海拔。 - 因此,如果一个变量在一个确定的山丘里被声明了,那么它就可以在这山丘里被访问。当然,那些在这个山丘里产生的新山丘也可以访问。
变量的生命周期:
变量从出生会经历三个阶段:
- 声明阶段:你在一个域声明一个变量后,系统不会马上给这个变量分配内存。这个变量可以是在全局作用域、块级作用域或者函数作用域里。
- 初始化阶段: 系统给变量分配内存,然后你声明的变量就绑定了这个内存,内存里面放的是
undefined
。 - 赋值阶段:将值赋给这个变量。把值放进内存里面,覆盖掉了
undefined
。
变量声明和变量的声明阶段是完全两个不一样的概念!!
变量声明是一条var a
这样子的语句。而变量声明阶段是JavaScript编译器处理变量的一个步骤。在这个步骤里,当编译器遇到一条变量声明语句时,会在这个变量相应的域里面注册该变量的名字(当然,这个名字之前是没有被声明过的)。
var
的特性
- 全局作用域或者函数作用域。
- 可以被更新
- 可以重复声明
- 变量提升:在域里面被注册,值为
undefined
。
下面,用几行简单的代码来解释这些特性:
console.log(a); //undefined
var a = 10;
console.log(a); // 10
var a = 10;
a = 20;
console.log(a); // 20, 变量被更新了
var a = 10;
a = 20;
var a = 30;
console.log(a); // 30, 变量被重复声明了
因为变量提升,所有声明变量都会被提升到它所在作用域的顶端,然后被默认赋值为undefined
。声明阶段和初始化阶段是一起的。因此,变量a
从作用域顶端就可访问使用。所有当我们在它在声明之前就访问也不会出现一个错误,相反会打印一个undefined
出来。
下面来个例子,var
在函数作用域中的变量提升:
function outerFunc(){
var a = 10;
if (a > 5){
var a = 20;
console.log(a);// 20
}
console.log(a);// 20
}
变量a
一开始就在函数作用域里被声明了。因为if
后面的{}
并不会产生一个新的块级作用域,所以当我们重新声明一个a
的时候,之前的变量a
就被覆盖了,新产生的a
被赋以值为20;
以下是系统的暗箱操作:
function outerFunc(){
var a;
a = undefined;
a = 10;
if (a > 5){
a = 20;
console.log(a);// 20
}
console.log(a);// 20
}
由于变量提升,所以一开始a
就被弄到作用域的顶端了,这时var
对a
的使命就完成了,然后给它undefined
,这个过程就是变量提升。
因为var
变量的意外重新声明而导致数据出错是困扰很多开发者的常见错误,因为在函数作用域里面它并不会报错
let
- 块级作用域
- 值可以被更新
- 禁止重新声明
- 无变量提升
下面是几个简单的例子,我们用let
来声明变量,然后初始化、更新值、尝试重新声明。
consle.log(a);// ReferenceError: a is not defined 参考错误: a 未被定义
let a = 10;
consloe.log(a);// 10
let a = 10;
a = 20;
console.log(a);// 20;
let a = 10;
a = 20;
let a = 30;// SyntaxError: Identifier 'a' has already been declared 语法错误: 名字“a”早已被声明;
更新a
的值是被允许的。然而,如果你尝试重新声明它那就会得到一个语法错误。这其实保护了开发者防止意外重新声明一个声明过的变量。
下面是一个解释let有块级作用域的例子:
function oterFunc(){
let a = 10;
if (a > 5) {
let a = 20;
console.log(a);// 20
}
console.log(a);// 10
变量a
的第一个声明是在函数作用域里面。if
里面产生了一个块级作用域,当我们声明第二个变量a
的时候,它就会在块级作用域里被“注册”。块级作用域是独立于函数作用域的。因此,我们创造了两个不同的变量a
,然后我们观察到里面的a
并不会影响到外面的。
这让开发者可以在条件和循环语句写上临时变量,而不用担心这个变量名有没有在函数里面被用过。
const
- 块级作用域
- 值不能更新
- 禁止重新声明
- 无变量提升
- 声明时要赋值
下面是一些简单的例子,我们初始化一个变量然后尝试去更新值、重新声明等等;
const a = 10;
console.log(a);// 10
const a = 10;
a = 20; // TypeError: Assignment to constant variable 类型错误:给const 更新值
const a = 10;
const a = 30;// SyntaxError: Identifier 'a' has already been declared. 语法错误: “a” 已经被声明过了
const b; // SyntaxError: Missing initializer in const declaration 语法错误: const 类型的声明缺少初始值
如果我们声明一个const
变量而不赋值的话就会出现语法错误,如果重新声明一个const
变量,那会导致语法错误。
来看看它的块级作用域:
function outerFunc() {
const a = 10;
if (a > 5) {
const a = 20;
console.log(a); // 20 }
console.log(a); // 10
}
我们看到,const
有和let
一样的规则,有着块级作用域。
但是const
最特殊的就是它所绑定的值是不能被更新的,但是这并不是绝对的。原始值不会变,引用值可以;
在赋值之后,const
变量初始化赋的值是不会可以被改变的,因此存放在变量里面的东西是禁止被修改的。也就是说,你不能用=
再次给变量赋值。它的特点就是:声明立即赋值,之后不可以被修改。
但是具体的修改与否取决于值得类型:
- 原始值:Boolean,Null, Undefined, Number, String, Symbol 类型的值永远都修改不了。
- 引用值:数组、函数和对象类型。如果把对象赋个它,其实就是把对象的地址赋进去,赋进去的地址是不可以被修改的,但是对象里面的值是可以被修改的。
我们来看几个具体的例子:
//布尔值:
const a = true;
a = false;// TypeError: Assignment to constant variable.
// Null
const b = null;
b = 10; // TypeError: Assignment to constant variable.
//undefined
const c = undefined;
c = 10; // TypeError: Assignment to constant variable.
//munber 类型
d = 50;
d = 100; // TypeError: Assignment to constant variable.
//string 类型
const e = 'hello';
e = 'world'; // TypeError: Assignment to constant variable.
可以看到,我们是不能改变原始值的。那我们来看看引用值怎么样。
const c = [1,2,3];
c.push(10);
console.log(c); // [1,2,3,10]
c.pop();
console.log(c); // [1,2,3]
c = [4,5,6];// TypeError: Assignment to constant variable.
正如我们看到的,我们可以改变数组里面的内容,但是不能改变这个数组本身。在故事里面,就是我们不可以改变const
盒子里面放的那个写上了地址的小纸条但是我们按照这个地址到仓库里面改变那里的东西;
那我们再来试试对象:
const d = {
name: 'John Doe',
age: 35
}
d.age = 40; // Modifying a property:
console.log(d); // { name: 'John Doe', age: 40};
d.zipCode = '52534'; // Adding a property
console.log(d); // { age: 40, name: "John Doe", zipCode: '52534; }
d = { name: 'Mary Jane', age: 25}; // TypeError: Assignment to constant variable.
就和数组一样,我们可以修改对象里面的值,但是不能修改对象本身。
那我该在什么时候用哪个?
JavaScript现在有三种变量,很自然地就会想我们到底在什么情况下使用哪一个。
在介绍了let
的块级作用域之后我们就不推荐在函数作用域里面使用var
了 ,因为这很容易导致那些问题。除非你有特殊的要求,不然建议多使用let
。
建议使用const
来存储实数,那些不会改变的实数,比如圆周率这些;或者那些在整个程序中都不允许被修改的值。
一个常见的编程手段就是所有的声明都使用const
,然后如果有需要再把它们转换成let
。对于我个人来说,我喜欢用let
,然后在把需要的修改为const
。其实,这没有什么硬性规定,你应该使用最适合你的。
如果你有时间,我强烈建议你再去读一下这个故事,读完后会增强你对技术部分的理解。
非常感谢你的阅读! 我忠诚地希望你能够从这篇文章中学到新东西,我十分希望能够看到你们在评论区下留言。
参考文献:
1.https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let
2.https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/const
3.https://dmitripavlutin.com/variables-lifecycle-and-why-let-is-not-hoisted/
4.https://github.com/getify/You-Dont-Know-JS