什么是变量
大多数的程序都需要跟踪值的变化,因为程序在执行任务的过程中会执行各种操作,值会不断发生变化。
要在程序中实现这一点,最简单的办法就是将值赋值给一个符号,这个符号称为变量。
在一些语言中需要声明一个变量存放指定类型的值(如数字),称之为静态类型。这样可以保证值类型的不可变性。人们认为静态类型提高了程序的正确性。这也是我们日常开发中很多项目引入TypeScript的原因之一。
我们都知道JavaScript变量是可以保存各种类型的,这样的叫做动态类型,而且不需要在声明时指定类型,所以叫做弱类型语言。人们认为这种方式比较灵活。
变量声明
有三个关键字可以声明变量var、let、const。var是JavaScript一开始就有的,let、const是为了解决一些var存在的问题,在ES6推出的。
接下来看看用var声明一个JavaScript变量
var name;
很简单,这样就完成了一个变量的声明。接着我们可以给这个变量赋值。
name = '南墨'
name = 200 // 体现了JavaScript变量的灵活性,合法,但是开发中不推荐
可以看到我们先将name赋值成字符又赋值成数字,在JavaScript中是允许的。
var声明
作用域
在函数内通过 var 声明的变量,会变成函数的局部变量,也就是说函数销毁了,变量也就没了。
function query() {
var name = '南墨'
}
query()
console.log(name) // 错误
name报错了是因为函数执行完就摧毁了,所以就报错了。不过有个办法可以不报错,那就是省略掉name前面的var
function query() {
name = '南墨'
}
query()
console.log(name) // 南墨
很自然的输出了南墨,因为去掉var后,name变成了全局变量。只要调用一次函数query,就会定义这个变量,并且可以在函数外部访问到。
声明提升
如果声明一个变量之前,调用这个变量会如何?像下面这样
function fn() {
console.log(age)
var age = 18
}
fn()
age输出的undefined。
以我们正常的思维考虑,一段代码应该是自上而下的。没声明的变量怎么可以使用呢?
再来考虑一下另一段代码
a = 2;
var a;
console.log(a)
很多人肯定会认为这段代码输出的是undefined,他们认为a在a = 2之后又重新被赋值了。但是真正输出的结果是2。
两个问题都没办法一下子理解过来。那么到底发生了什么?
要搞清楚这个原因(提升)的本质,就需要理解javaScript引擎是如何运行的?
JavaScript引擎会在解释javaScript代码之前首先对其进行编译。编译阶段中的一部分工作就是找到所有的声明,并用合适的作用域将它们关联起来。
简单理解,变量在被声明过程要经历三个阶段:[创建create、初始化initialize、 和赋值assign]。
来看下代码片段
function fn() {
console.log(age)
var age = 18
}
fn()
在执行 fn 时,会有以下过程:
- 进入
fn,为fn创建一个环境 - 找到
fn中所有用var声明的变量,在这个环境中创建(编译阶段)这些变量(即age) - 将这些变量初始化为
undefined。 - 开始执行代码
- 将age变量赋值为
18,即age = 18
也就是说 var 声明会在代码执行之前就将创建变量,并将其初始化为 undefined。
因此,正确的思考思路是,所有变量都会在任何代码被执行前被处理(初始化)。
当你看到 var age = 18 时,可能会认为这是一个声明。但是JavaScript实际上将其看成两个部分: var age 和 age = 18。
第一个声明变量在初始化阶段(编译阶段进行的)。第二个赋值会被留在原地等待执行阶段。
所以我们的第一段代码会以如下形式进行处理:
function fn() {
var age = 18
console.log(age)
}
fn()
其中第一部分是编译,而第二部分是执行。
类似地,第二段代码实际是按照
var a;
a = 2;
console.log(a)
这个过程就好像变量申明从它们在代码被“移动”到了最顶部。这个过程就叫做“提升”。
重复声明
var声明可以多次声明同一个变量,如以下代码
function foo() {
var age = 16;
var age = 26;
var age = 36;
console.log(age);
}
foo(); // 36
let声明
let 的出现是为了解决 var 存在的问题,下面我们来分析一下它们的不同之处
作用域
- let声明的范围是块作用域,而var声明的范围是函数作用域
if (true) {
var name = '南墨';
console.log(name); (1) // 南墨
};
console.log(name); (2) // 南墨
(1)处输出南墨好理解,(2) 处能够输出南墨是为什么?
因为var是函数作用域,这里var把全局当作它的函数作用域,做了一次变量提升。操作过程就好比:
var name;
if (true) {
name = '南墨'
console.log(name)
}
console.log(name)
这样(2)处输出南墨就很好理解了。
声明提升
console.log(name) // undefined
var name = '南墨'
这里为什么输出undefined,相信认真看过上文var的声明提升的伙伴都很了解了。
那么let存在提升吗?
来看一个例子
{
let age = 18
x = 28
}
let也按 [创建、初始化和赋值] 过程:
-
找到所有用let声明的变量,在环境中创建这些变量
-
开始执行代码(注意还没有开始初始化)
-
如果
age存在初始值,就将age初始化为18,否则初始化为undefined。 -
执行
age = 28,对age进行赋值
再来看个例子
{
console.log(name) // name is not defined
let name = '南墨';
}
也按照上面的过程
- 找到所有用let声明的变量,在环境中创建这些变量
- 开始执行代码,即
console.log(name)。因为还没初始化所以直接报错了
看到这里,你应该明白了 let 到底有没有提升
- let 的创建过程被提升了,但是初始化没有提升。(也就是所谓的暂时性死区)
- var 的创建和初始化都被提升了。
什么是暂时性死区?
let声明的变量,在使用之前不能用任何方式出现在代码中。let声明之前的执行阶段瞬间被称为暂时性死区,在这阶段如果在let声明之前使用了变量都会抛出ReferenceError。
重复声明
在这里let重复声明会报错,而上文提到过var是可以重复声明而不报错。
let name = '南墨'
let name = '小明' // Uncaught SyntaxError: Identifier 'name' has already been declared
全局作用域
当在全局作用域使用var声明的时候,会创建一个新的全局变量作用全局对象的属性
var name = '南墨'
console.log(window.name) // '南墨'
而let不会
let name = '南墨'
cnsole.log(window.name) // undefined
const 声明
const 的行为与let基本相同,唯一的区别是,const在声明时必须要初始化变量,且修改const声明的变量会导致运行时报错。
作用域
const 声明的作用域范围也是块级的
if(true) {
const name = '南墨'
}
console.log(name) // 报错
声明提升
来看个例子
console.log(name) // Uncaught ReferenceError: name is not defined
const name = '南墨'
和let一样,const 也是因为存在暂时性死区导致报错,而且声明过程和let一样,只是没有赋值阶段。
重复声明
从实例中可以看出,const也不允许重复声明
const name = '南墨'
const name = '小明' // Uncaught SyntaxError: Identifier 'name' has already been declared
不支持修改
const 在声明之后就不可以修改变量
const MAX_NUM = 2
MAX_NUM = 3 // Uncaught TypeError: Assignment to constant variable.
总结
let 与 var 的不同:
- let声明的范围是块作用域,而var声明的范围是函数作用域
- let存在暂时性死区,即在let声明之前无法使用变量,否则报错
- let无法像var那样重复申明同一个变量
- let在全局作用域中声明变量不会变成全局变量而,而var会
const 与 let 的不同
- const 在声明变量时,必须初始化一个值
- const 存在暂时性死区
- const 无法重复申明同一个变量
- const 在全局声明变量不会变成全局变量
- const 声明变量后无法修改
for循环中的声明
循环体外输出值
我们先来看个例子
for(var i = 0; i < 10;i++) {
// 循环内容
}
console.log(i); // 10
使用var在for循环括号中声明的i,可以在循环体外输出。
改成使用let之后,会发生什么,看下面的例子
for(let i = 0; i < 10; i++) {
// 循环内容
}
console.log(i) // Uncaught ReferenceError: i is not defined
可以看出来不能在循环体外面输出变量,因为let声明的变量的作用域仅限于for循环块内部。
循环体内输出值
我们来看下,在var声明变量的循环体内输出值
for(var i = 0; i < 10; i++) {
console.log(i)
}
这个循环输出0 1 2 3 4 5 6 7 8 9
换成let呢?
for(let i = 0; i < 10; i++) {
console.log(i)
}
也是输出0 1 2 3 4 5 6 7 8 9
这样如我们所预料的,那接下来我们来看看另一个例子
for(var i = 0; i < 10; i++) {
setTimeout(() => console.log(i))
}
一些JavaScript的初学者可能以为会输出0 1 2 3 4 5 6 7 8 9 10, 但实际输出的是10个 10
为什么会这样呢?
因为js的代码执行机制,等循环完毕之后才会执行 setTimeout,
而循环完毕时,i 保存的值是 10,因而输出的都是同一个最终值。
我们写个伪代码理解:
(
(i) => setTimeout(() => console.log(i))
)(10)
.... // 执行10次这样的代码
那既然这样,我们改成let看看,
for(let i = 0; i < 10; i++) {
setTimeout(() => console.log(i))
}
正常输出0 1 2 3 4 5 6 7 8 9
我们可能会有两个疑惑的点
- 如果let不能重复声明,在循环到第二次时,应该报错才对
- 就算能重复声明,因为代码中依然只有一个变量 i,在 for 循环结束后,i 的值还是会变成
10才对。
因此上文中提到 let 声明不提升,不能重复声明等等特性,在这里不适用。
想深入研究为什么前面所学的知识都不适用了,需要忘记之前的那些特性。
我们可以通过阅读ES规范 ECMAScript 规范13.7.4.7节,简单的归纳如下:
在 for (let i = 0; i < 10; i++) 中,即圆括号之内建立一个隐藏的作用域,在每次执行循环体之前,JS 引擎会把 i 在循环体的上下文重新声明及初始化一次
可以理解为在for循环中使用 let 声明变量时,如下所示
for(let i = 0; i < 10; i++) {
let i = 隐藏作用域中的i // 看这里看这里看这里
setTimeout(() => console.log(i))
}
那就是说,10次循环就会有 10 个不同的 i。可以理解为每次迭代循环时都创建一个新变量,并以之前迭代中同名变量的值将其初始化。 上面的代码就相当于可以写成
// 伪代码
(let i = 0) {
setTimeout(() => console.log(i))
}
(let i = 1) {
setTimeout(() => console.log(i))
}
(let i = 2) {
setTimeout(() => console.log(i))
}
(let i = 3) {
setTimeout(() => console.log(i))
}
(let i = 4) {
setTimeout(() => console.log(i))
}
(let i = 5) {
setTimeout(() => console.log(i))
}
(let i = 6) {
setTimeout(() => console.log(i))
}
(let i = 7) {
setTimeout(() => console.log(i))
}
(let i = 8) {
setTimeout(() => console.log(i))
}
(let i = 9) {
setTimeout(() => console.log(i))
}
(let i = 10) {
setTimeout(() => console.log(i))
}
参考文献:
《JavaScript高级程序设计(第4版)》
《你不知道JavaScript(下)》