众所周知,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声明变量带来的问题
- 允许重复声明变量,导致数据被覆盖
- 变量提升,导致怪异的数据访问,还有闭包问题
- 全局变量挂载到全局对象(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特点:
- let声明的变量不会挂载到全局对象window
- let声明的变量只有当前作用域或者里面的作用域可以使用(和var差不多)
- let声明的变量,不允许当前作用域范围内重复声明
- 使用let不会有变量提升
1. let声明的变量不会挂载到全局对象window
单单是这个优点,就已经足以抛弃var该用let了好吧
let a = 123;
console.log(window.a) //undefine
❗等等,有个问题
⭐var声明的全局变量是window的属性,但是let声明的全局变量不是window的属性那是什么?
经过我的测试,let全局声明的变量是放在Script这个域里面的
在谷歌浏览器的控制台里,我们可以看到,有 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来声明变量,以保证变量的值不会随意篡改,原因如下:
-
根据经验,开发中的很多变量,都是不会更改,也不应该更改的。 (开发时可以上来就const,需要重新赋值的量再改成let)
-
后续的很多框架或者是第三方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都可以声明变量
区别:
- var具有变量提升的机制,let 和 const 没有变量提升的机制
- var可以多次声明同一个变量,let 和 const 不可以多次声明同一个变量
- var、let声明变量,const声明常量
- var声明的变量没有自身的块级作用域,let 和 const声明的变量有自身的块级作用域