干货分享—var、let与const之间如何选择
前言
我与JavaScript的相遇很巧合,在我第一次使用JavaScript前,我从来没有或者说从来没有机会去学习它。在那之前,我主要是进行android应用的开发,主语言是Java,后面为了迎合团队的技术栈,不得不使用JavaScript进行开发。我现在还记得,当时我的leader三令五申,只能使用var(为什么只使用var?我想使用let怎么办?),为此我经历了莫名其妙的bug,我写这篇文章,最大的目的,除了是对自己经验的总结,还希望能够帮助一些刚刚学习JavaScript的人(正如当年的我一样),要是正在看这篇文章的你发现了错误或者有什么不同的意见,也希望能够指出,不至于让错误的总结去误导更多的人。
为什么只使用var?我想使用let怎么办?
这里我出示一张图1,从中可以看到各大浏览器厂商对let的支持性(这里没提const了,感兴趣的自己去看一下)
浏览器厂商对let的支持限制了当时的我只能使用var,这一切直到我使用了 babel。关于babel如何将let、const转化为var,如果感兴趣可以看看扩展。
var和let的区别
块级作用域
这里是我当初用var最懵比的地方,我以前用过c和Java,而这两款语言的变量声明的作用域都是被锁定在方括号里面的,而var是没有块级作用域2的
{
var val = "我是用var声明的变量";
}
console.log(val);//我是用var声明的变量
{
let val = "我是用let声明的变量";
}
console.log(val);//val is not defined
Hoisting(变量提升)
从概念的字面意义上说,“变量提升”意味着变量和函数的声明会在物理层面移动到代码的最前面,但这么说并不准确。实际上变量和函数声明在代码里的位置是不会动的,而是在编译阶段被放入内存中。3
console.log(val);//undefined
var val = 0;
console.log(val);//Cannot access 'val' before initialization
let val = 0;
造成这样结果的原因就是因为使用var声明的变量会比使用let的变量多一个变量提升的操作,它的效果更像下面的代码(这里的代码单独理解变量提升是没问题的,但是并不等于这种效果):
let val;
console.log(val);//undefined
val = 0;
全局作用域下赋值
var i = 1;//使用var关键字
j = 2;//不使用任何关键字 这种情况在严格模式不适用 会报j is not defined
let num = 3;//使用let关键字
如上所述,进行输出:
console.log('i : ' + i);//i : 1
console.log('j : ' + j);//j : 2
console.log('num : ' + num);//num : 3
console.log('window.i : ' + window.i);//window.i : 1
console.log('window.j : ' + window.j);//window.j : 2
console.log('window.num : ' + window.num);//window.num : undefined
再使用delete关键:
delete i;
delete j;
delete num;
console.log('i : ' + i);//i : 1
console.log('j : ' + j);//报错 被删除了
console.log('num : ' + num);//num : 3
delete window.i;
delete window.j;
delete window.num;
console.log('window.i : ' + window.i);//window.i : 1
console.log('window.j : ' + window.j);//window.j : undefined
console.log('window.num : ' + window.num);//window.num : undefined
由此我们可以得出结论,在全局作用域下赋值(只适用于非严格模式):
1.在使用var关键字的情况下会同时赋值到window下,并且不能通过delete关键字删除;
2.在不使用用关键字的情况下也会同时赋值到window下,并且能通过delete关键字删除;
3.在使用let关键字的情况下不会赋值到window下;
扩展
例1:
var arr = [];
for(let i = 0,length = 10;i<length;i++){
arr.push((() => {
return i;
}));
}
arr[1]();//1
例2:
var arr = [];
for(var i = 0,length = 10;i<length;i++){
arr.push((() => {
return i;
}));
}
arr[1]();//10
注意两段代码的唯一差别就是for循环中例1使用的是let,而例2中使用的是var,但是输出的结果却是天壤之别,为什么会造成这种效果呢?我们来分析一下:
let是有块级作用域的,所以for循环里面的代码可以视作:
{
//i是在for循环的{}里面的 现在循环的长度是10 就有10个这样的块级作用域
//这10个块级作用域的i是互不影响的
i = 1;
arr.push((() => {
return i;
}));
}
var是没有块级作用域的 它只有函数作用域和全局作用域,所以里面的代码可以视作:
//没有块级作用域,所以已经提升到for循环的{}外面
//i是在for循环的{}外面的 现在的循环长度是10 虽然有这样的10个块级作用域
//但是只有一个i变量 当循环完后 i==10
//这时 arr.push(function) 这里的function是还没执行的 所以不管怎么样
//输出都是10 因为i只有一个
i = 1;
{
arr.push((() => {
return i;
}));
}
请仔细查看我在上面代码中加的注释,为了更方便理解,特意加上例3的代码
例3:
var arr = [];
let i = 0,length = 10;
for(;i<length;i++){
arr.push((() => {
return i;
}));
}
arr[1]();//10
例3虽然还是let,但是我已经把它放到for循环的{}外面了,所以效果与例2一致
接下来还有例4
例4:
var arr = [];
for(var i = 0,length = 10;i<length;i++){
arr.push((() => {
return i;
})());
}
arr[1];//1
例4虽然输出和例1一致,但是原理是完全不一样的。在例4中,arr.push(function) functon立即执行,虽然i没在for循环的{}内,但是在i变化的过程中立马调用了i的返回
实际效果:
i = 1;
{
// arr.push((() => {
// return i;
// })());
arr.push(i);
}
例5
这里的知识点涉及了闭包,如果对闭包暂时还不是很了解的可以先去了解下 一篇文章带你弄懂闭包
function outside(num){
function inside(){
return num;
}
return inside;
}
var arr = [];
for(var i = 0,length = 10;i<length;i++){
arr.push(outside(i));
}
arr[1]();//1
例5是非常有意思。babel为了兼容浏览器,在进行代码以及语法的转换时,是会把像例1中for循环中的let替换成var的(毕竟现在还不是所有浏览器都兼容es6),如果是直接替换,像例2,是肯定不对的,但是像例5,不用块级作用域,也能达到例1的效果。
babel做兼容转换代码还有一点也非常有意思,比如像下面的代码:
{
let num = 100;
}
console.log('num : ' + num);
如果只是替换声明变量的关键字:
{
var num = 100;
}
console.log('num : ' + num);
根本就达不到实用let的效果,babel中的做法是:
{
var _num = 100;//不单单替换关键字,还会改变变量名
}
console.log('num : ' + num);
是不是非常有意思?
const和let的区别
不变的变量
我是这样理解const的,加强版的let,因为它除了拥有let的一些特性之外,还有一个特性,就是不可修改,这里扯远一点,就像加了final(JavaScript是没有这个关键字的)的let。
let i = 0;
i++;
console.log('i:'+i);//i:1
const j = 0;
j++;
console.log('j:'+j);//Assignment to constant variable.
const一旦赋值过后是不能重新赋值的,但是这时会出现一种情况:
const obj = {};
obj.name = 'object';
console.log(obj);//{name: "object"}
const obj = {};
obj = {};
console.log(obj);//Assignment to constant variable.
如果你看到这里,对上面的两张输出结果带有疑惑的话,个人建议多看看JavaScript中基础类型和引用类型
扩展
babel对const关键字的转化也非常的有意思,const具有和let一样的块级作用域,所以一些地方的转化和let是一样的,但是const还有一个不能修改的特性,对于这一点,我们继续往下面看:
const num = 10;
num = 20;
如果是单纯的替换声明变量的关键字:
var num = 10;
num = 20;
同样的达不到我们需要的效果,babel中是怎么去做的呢?
function _readOnlyError(name) { throw new Error("\"" + name + "\" is read-only"); }
var num = 10;
num = (_readOnlyError("num"), 20);
看到这里是不是觉得越看越有意思?是不是就算不使用let和const关键字也能自己写出想要的效果?
总结
作用域有全局作用域、函数作用域、块级作用域;
var的作用域是不受块级作用域影响的;
let是会受块级作用域影响的;
let是在编译的时候才会初始化;按道理 我理解这里的编译器是逐句编译的
var在声明的时候会变量提升;let是在编译的时候才会初始化就是说它不会变量提升
常见的提升还有函数声明的提升,这也是函数声明和函数表达式的区别之一
const就是加了final的let;
结尾
这篇博客写的挺久的,可能看完10分钟左右吧,但是我写完这个应该是花了我挺长时间的,为什么我会花费我的时间去写这个呢?首先是因为对自己学过的知识的总结,再者是希望帮助一些刚刚学习JavaScript的人,当然,这些我在开篇就已经说过了。不过其实我还有一点最重要的没有说,有时看一些博客,要是作者使用var来用作变量申明的关键字,而不去使用let,下面的留言中总有一些“云大佬”在线指点,要使用let,不要去使用var,但是仅此而已,没有下文了。我个人的理解是,一定要结合当前的情况来判断到底使用什么关键字。如果项目中没使用babel(或者其它功能相似),又要兼容ES5,这个时候还一股脑的追求使用let和const,这不是在搞笑吗?JavaScript是一门很有魅力的语言,就如同我上面写的let扩展中的例1和例2一样,稍微变动一点点,输出的结果大大不同。