let和const
首先先了解一下let和const把,
let和const是在es6中新引入了两种方式来申明变量的关键字,虽然我们仍然可以使用广为传诵的var变量(然而你不应该继续使用它了,继续阅读来了解其中原因),但是现在我们有了两种更牛的工具去使用:let和const。
首先 let
let和var非常的相似,在使用方面,我们可以使用完全相同的方式来声明变量
示例
let myLetName= "林江涛";
var myVarName= "林江涛";
console.log(myLetName); //林江涛
console.log(myVarName); // 林江涛
效果
但是实际上,他们之间有几处明显的不同。他们不仅仅是关键字变了,而且实际上它还让会简化我们的一些工作,防止一些奇怪的bug,其中这些不同点是:
-
let是块状作用域(是的JavaScript终于支持块状作用域了),而var是函数作用域。
-
let不能在定义之前访问该变量(var是可以的,它确实是js世界中许多bug和困扰的源头)。
-
let不能被重新定义。
在我们讲解这些不同点之前,首先我们看一个更酷的变量:const1,也就是,常量
const和let非常像(跟var相比来说,他们之间有许多相同点),但是他们有一个主要的不同点:let可以重新赋值,但是const不能。因此const定义的变量只能有一个值,并且这个值在声明的时候就会被赋值。因此我们来看下下面的例子。
示例
let myLetStrName= "林江涛";
const myConstStrName= "林江涛";
console.log(myLetStrName); //林江涛
console.log(myConstStrName); // 林江涛
myLetStrName="JiuMei";
console.log(myLetStrName); //JiuMei
myConstStrName="JiuMei";
console.log(myConstStrName); // VnXRW9N:17 Uncaught TypeError: Assignment to constant variable.
效果
但是const并非是完全不可变的
有一个常见的问题:虽然变量不能被重新赋值,但是也不能让他们真正的变为不可变的状态。如果const变量有一个数组或者对象作为其值的话,你可能会像下面的代码一样改变它的值。
示例
const myConstObject = { name: "林江涛"};
// myConstObject = {}; - TypeError
myConstObject.name = "JiuMei"; //ok
console.log(myConstObject.name); // JiuMei
const myConstArray = [1];
// myConstArray = []; - TypeError
myConstArray.push(2) //ok
console.log(myConstArray); // [1, 2]
效果
可以看到对象的name属性被我们改变了,数组的长度和成员都被改变了
当然我们不能用原始数据类型的值,比如string,number,boolean等,因为这些数据类型他们本质上是不可变的。
真正的不可变
如果想让我们的变量真正的不可变的话,可以使用Object.freeze(), 它可以让你的对象保持不可变。
const myConstNestedObject = {
name:"林江涛"
};
Object.freeze(myConstNestedObject);
myConstNestedObject.name = "jiumei"; // won't change
console.log(myConstNestedObject)
效果
可以看到,虽然我们给对象myConstNestedObject的name属性重新赋值了,但是并没卵用
虽然Object.freeze(), 能使对象不能改变,但很不幸的是,他仅仅是浅不可变,如果你对象里嵌套着对象的话,它依然是可变的。
示例
const myConstNestedObject = {
name:"林江涛",
immutableObject:{ name:"林江涛"}
};
Object.freeze(myConstNestedObject);
myConstNestedObject.name = "jiumei"; // won't change
console.log(myConstNestedObject);
//将myConstNestedObject里面的immutableObject属性的name属性更改为jiumei
myConstNestedObject.immutableObject.name="jiumei";
console.log(myConstNestedObject)
效果
可以看到immutableObject对象的name属性被改变了
变量的作用域
在介绍了一些基础知识以后,在我们进入下一个更高级的话题前, 我们首先恭喜JavaScript终于支持块级作用域了,可喜可贺
注意:因为let和const的作用域完全一样,所以这里就只拿let举例
全局变量和函数作用域变量2
在js中,变量的作用域决定了变量的可用位置。从不同的角度来看,可以说作用域是你可以在特定区域内使用的那些变量(或者是函数)的声明。作用域可以是全局的(因此在全局作用域中定义的变量可以在你代码中任何部分访问)或者是局部的。(小声逼逼:如果你会c#,java之类的编程语言,这个是很容易理解的)
很显然,局部作用域只能在内部访问。在ES6以前,它仅仅允许一种方式来定义局部作用域 - function也就是 函数的闭包.,咱们来看一下下面的例子:
示例
var myName = "林江涛";
function functionWithVariable() {
// local scope
var myAge = 18;
console.log(myName); // 林江涛
console.log(myAge); // 15
}
functionWithVariable();
//global scope again
console.log(myName); // 林江涛
console.log(myAge); // Uncaught ReferenceError: myAge is not defined
效果
上面的例子中,变量myName 是全局变量,所以它可以在我们代码中的函数内或者是其他区域内被访问到,但是变量myAge定义在函数内,所以它只在函数内可访问。
ps:这里有一个例外,那就是在JavaScript里你不使用关键字声明时,这个变量是默认全局的,但我这里并不打算介绍它,因为这篇文章主要是介绍let和const的
因此,所有在函数内创建的内容都可以在函数内被访问到,包括函数内部里所有的嵌套函数(可能会被嵌套多层)。在这里可能要感谢闭包了,但是在这篇文章里我并不打算介绍它。因为我已经在别的文章里写过了 JavaScript函数详解.
提升
简单来说,提升是一种吧所有的变量和函数声明“移动”到作用域的最前面的机制。让我们看一下下面的例子。
示例
function func() {
console.log(localVariable); // undefined
var localVariable = 5;
console.log(localVariable); // 5
}
func();
效果
它为什么依然会正常工作呢?我们还没有定义这个变量,但是它依然通过console.log()打印出了undefined。为什么不会报出一个变量未定义的错误呢?让我们再仔细看一下。
编译变量
Javascript解析器要遍历这个代码两次。第一次被称为编译状态,这一次的话,代码中定义的变量就会提升。在他之后,我们的代码就变成类似于下面的这样子的(我已经做了一些简化,只展示出相关的部分)
function func() {
var localVariable = undefined;
console.log(localVariable); // undefined
localVariable = 5;
console.log(localVariable); // 5
}
func();
我们看到的结果是,我们的变量localVariable已经被移动到func函数的作用域的最前面。严格来说,我们变量的声明已经被移动了位置,而不是声明的相关代码被移动了位置。我们使用这个变量并打印出来。它是undefined是因为我们还没有定义它的值,它默认使用的undefined。
提升的例子 - 会出什么问题
来让我们看一个令人讨厌的例子,我们的作用域范围对于我们来说,是弊大于利的。也不是说函数作用域是不好的。而是说我们必须要警惕一些由于提升而引起的一些陷进。我们来看看下面的代码:
注意:callbacks.push(() => console.log(m));是箭头函数3
var callbacks = [];
for (var i = 0; i < 4; i++) {
callbacks.push(() => console.log(i));
}
callbacks[0]();
callbacks[1]();
callbacks[2]();
callbacks[3]();
你认为输出的值是多少呢?你猜可能是0 1 2 3,是吗?如果是的话,对于你来说,可能会有一些惊喜。
效果
实际上,他真实的结果是4 4 4 4。等等,它到底发生了什么?我们来“编译”一下代码,代码现在看起来就像这样:
var callbacks;
var i;
callbacks = [];
for (i = 0; i < 4; i++) {
callbacks.push(() => console.log(i));
}
callbacks[0]();
callbacks[1]();
callbacks[2]();
callbacks[3]();
你看出问题所在了吗?变量i在整个作用域下都是可以被访问到的,它不会被重新定义。它的值只会在每次的迭代中不断地被改变。然后呢,当我们随后想通过函数调用打印它的值得时候,他实际上只有一个值 - 就是在最后一次循环赋给的那个值。
这个问题可以通过JavaScript函数的闭包来解决但是那样很麻烦(主要是不易于理解)
var callbacks = [];
for (var i = 0; i < 4; i++) {
(function(m){
callbacks.push(() => console.log(m));
}(i));//通过IIFE产生闭包
}
callbacks[0]();
callbacks[1]();
callbacks[2]();
callbacks[3]();
效果
IIFE的详细介绍请查看前端笔记 JavaScript函数详解
我们只能这样了吗?不是的
es6除了定义变量的新方式以外,还引入了一种新的作用域:块级作用域。块就是由花括号括起来的所有的内容。所以它可以是if,while或者是for声明中的花括号,也可以是单独的一个花括号甚至是一个函数(对,函数作用域是块状作用域)。let和const是块作用域。意味着无论你在块中无论定义了什么变量,什么时候定义的,它都不会跑到块作用域外面去。我们来看一下下面的例子:
function func() {
// function scope
let localVariable = 5;
var oldLocalVariable = 5;
if (true) {
// block scope
let nestedLocalVariable = 6;
var oldNestedLocalVariable = 6;
console.log(nestedLocalVariable); // 6
console.log(oldNestedLocalVariable); // 6
}
// those are stil valid
console.log(localVariable); // 5
console.log(oldLocalVariable); // 5
// and this one as well
console.log(oldNestedLocalVariable); // 6
// but this on isn't
console.log(nestedLocalVariable); // ReferenceError: nestedLocalVariable is not defined
能看出来差别吗?能看出来怎么使用let来解决早些时候提出问题的吗?我们的for循环包含一组花括号,所以它是块作用域。所以如果在定义循环变量的时候,使用的是let或者是const来代替var的话,代码会转为下面的形式。注意:我实际上已经简化了很多,不过我确定你能理解我的意思。
let callbacks = [];
for (let i = 0; i < 4; i++) {
//, 1, 2, 3
callbacks.push(() => console.log(i));
}
callbacks[0]();
callbacks[1]();
callbacks[2]();
callbacks[3]();
如上代码所示,现在的每一次循环都有它自己的变量定义,所以变量不会被重写,这样我们就可以抛弃那个不易于理解的IIFE了
效果
和IIFE闭包效果完全一样
这是这一部分结束的例子,但是我们再看一下下面的例子,我相信你明白打印出来的值的原因,以及对应的表现是什么。
function func() {
var functionScopedVariable = 10;
let blockScopedVariable = 10;
console.log(functionScopedVariable); // 10
console.log(blockScopedVariable); // 10
if (true) {
var functionScopedVariable = 5;
let blockScopedVariable = 5;
console.log(functionScopedVariable); // 5
console.log(blockScopedVariable); // 5
}
console.log(functionScopedVariable); // 5
console.log(blockScopedVariable); // 10
}
func();
let和const的新的提升方式
新的声明方式(let,const)较之之前的声明方式(var),还有一个区别,就是新的方式不允许在变量声明之前就使用该变量,但是var是可以得。请看下面的代码,下面这个代码是可以正常运行的:
function func() {
console.log(varStrName); // undefined
var varStrName= "林江涛";
console.log(varStrName); // 5
}
func();
但是这种却不可以
function func() {
console.log(letStrName); // ReferenceError: localVariable is not defined
let letStrName= 10;
console.log(letStrName); // 10
}
func();
等下,我上面曾经介绍了一个叫“提升”的概念,它会吧所有的变量定义在作用域的最前面。这是否意味着如果我不在实际的定义之前使用变量,然后就不会有提升了呢?答案是否定的。提升依然会有,并且适用于所有类型的变量类型。但是const和let却不是这样的。
首先,我们看一下var关键字是怎么工作的。规范对其是这样进行的描述的。var声明定义了在正在运行的执行上下文(running execution
context)作用域内的变量环境(VariableEnvironment中)的变量。var变量在当包含的词法环境(Lexical
Environment)初始化时被创建,在创建的时候被赋值为undefined。[…]
在执行VariableDeclaration时,由带有Initializer的VariableDeclaration定义的变量被赋其设定项的Initializer’s
AssignmentExpression的值。
规范中有许多的细节,让我们简单的来看一下:
-
当你进入到一个作用域中,在内部被定义的所有的变量都会被创建。
-
所有存在的变量,都可以被访问,并且会把undefined赋值给该变量。
-
当代码(执行时)到达初始化时,会被分配给一个实际的值。
我们来再看一下规范中对let和const的表述:
let和const声明是定义在当前执行上下文作用域中的词法环境中的变量。当包含的词法环境被初始化的时候,变量被创建。但是在变量的词法绑定时被计算之前是不允许通过任何方式来访问的。当词法绑定计算时而不是在变量被创建的时候,由词法绑定定义的变量的初始值被被赋予赋值表达式的值(也就是“=”右边的表达式)。当词法绑定被计算的时候,如果let声明中没有初始化的值的时候(也就是“let
a;”这样的形式),会被赋值为undefined。
说人话就是:
-
如果你进入到了指定的作用域中,它里面定义的所有的变量都会被初始化,这一点和var很像。
-
这里有一个不同点:像var一样,所有的变量都会存在,但是他们目前还不能被访问(里面没有值,甚至是undefined)。
-
如果let变量在相同的地方被定义和初始化,他们会被赋予合适的值,反之,变量就是undefined。const变量必须在定义的时候初始化。
光说不练很难理解,我们来看一些例子。
示例一:
临时死区
实际上,这种描述引出了我们的另一个定义。他很让人可怕,因为他叫:临时死区(TDZ)。这个属于明确了一个我们无法访问我们的变量的代码的区域。我们来看一下下面的代码和相关联的注释,来简单的解释一下TDZ是什么。
function func() {
// Start of TDZ for deadVariable
// we can still do something here, just our deadVariable is not available yet
const exampleVariable = 5;
console.log(exampleVariable); // 5
// End of TDZ for deadVariable
let deadVariable = 10;
console.log(deadVariable); // 10
}
func();
有一件事情值得去提醒。就是对于名字的建议,这是一个临时死区,意思这个区域是由时间定义的,而不是位置。因此当运行代码的时候,你的声明在被JS解析器解析之前是不能被访问的。因此你把使用的变量的位置放在哪里并不重要,只要是在声明执行后访问该变量就可以。所以看下面的代码:
function func() {
return deadOrAlive;
}
let deadOrAlive = 'alive!'
console.log(func()); // alive!
效果
这样做是可行的,他并不会报错
这是运行代码的步骤:
-
函数被声明
-
变量deadOrAlive被声明,并且初始化了一个值“alive”
-
调用我们的函数func。
-
由于变量deadOrAlive已经被声明,是可访问的,因此会打印出正确的结果 “alive”。
但是下面的例子却会报错
示例
function func() {
return deadOrAlive;
}
console.log(func()); // ReferenceError: deadOrAlive is not defined
let deadOrAlive = 'dead!'
效果
思考一下原因。(注意上面说的let和const的特殊提升方式)
答案揭晓
这是JavaScript为了避免因先使用后声明而导致的一些诡异的bug而出现的一个很好机制(具体看“提升”相关内容)。我们不需要去额外做什么事情,就是记住永远不要在变量声明之前使用这个变量。即使我们这样做了,我们也会得到一个很好的报错信息。只有一个条件-你必须使用let或者是const来替换掉var。
双定义
上面说过var和let,const的另一个区别是 - 后者仅仅可以被定义一次。而对于var的话,如果被同时定义多次,程序也依然会很好的运行。
var doubledVariable = 5;
var doubledVariable = 6;
console.log(doubledVariable); // 6
但是现在,当你用let和const来做同样的事情,就会得到一个语法错误:
let doubledVariable = 5;
let doubledVariable = 6; // SyntaxError: Identifier 'doubledVariable' has already been declared
但是,在嵌套的块级作用域中,使用相同名字的变量依然会很好的工作的,这个我想大家已经清楚了,就不用过多解释了吧。
let doubledVariable = 5;
if (true) {
let doubledVariable = 6;
console.log(doubledVariable); // 6
}
console.log(doubledVariable); // 5
不能重复定义这个功能实际上是很有用的,可以组织很多bug的发生。比如说你曾经在一个函数内,在不同地方用var定义了多个相同名称的变量,此时之前定义的变量可能会被覆盖,这样对于代码来说无疑是一个隐患,也就是因为这样,这个特性实际上是一个简单的,开箱即用的解决方案。
总结
总结一下,在ES6中有两种新方法来声明变量:通过let和const关键字,除此之外,两者都是块级作用域,并且在声明之前不能访问该变量。与之前的var相比是一个主要的升级。并且会消除你很多的困扰。我提出了几个例子,可能会帮助你节省了不少调试的时间,但是还有更多。如果你感兴趣的话,可以在网上简单的搜索一下。很久之前,我个人曾建议停止使用var关键字,所以现在我的代码里充满了let和const。我建议你也是这样,在以后当你想改变变量的值,就使用let和const。不要再使用var了。
const关键字是constant的缩写,通常翻译为常量、常数等 常量是块级范围的,非常类似用 let 语句定义的变量。但常量的值是无法(通过重新赋值)改变的,也不能被重新声明。 ↩︎
块级作用域定义:是一个语句,将多个操作封装在一起,通常是放在一个大括号里,没有返回值。为何需要块级作用域?
在ES6之前,函数只能在全局作用域和函数作用域中声明,不能在块级作用域中声明。没有块级作用域导致很多场景不合理: ↩︎箭头函数:箭头函数表达式的语法比函数表达式更简洁,并且没有自己的this,arguments,super或new.target。箭头函数表达式更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数。 ↩︎