likes
comments
collection
share

JavaScript之彻底理解变量

作者站长头像
站长
· 阅读数 62

什么是变量

大多数的程序都需要跟踪值的变化,因为程序在执行任务的过程中会执行各种操作,值会不断发生变化。

要在程序中实现这一点,最简单的办法就是将值赋值给一个符号,这个符号称为变量

在一些语言中需要声明一个变量存放指定类型的值(如数字),称之为静态类型。这样可以保证值类型的不可变性。人们认为静态类型提高了程序的正确性。这也是我们日常开发中很多项目引入TypeScript的原因之一。

我们都知道JavaScript变量是可以保存各种类型的,这样的叫做动态类型,而且不需要在声明时指定类型,所以叫做弱类型语言。人们认为方式比较灵活。

变量声明

有三个关键字可以声明变量varletconstvar是JavaScript一开始就有的,letconst是为了解决一些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,他们认为aa = 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 ageage = 18

第一个声明变量在初始化阶段(编译阶段进行的)。第二个赋值会被留在原地等待执行阶段。

所以我们的第一段代码会以如下形式进行处理:

function fn() {
     var age = 18
     console.log(age)
 }
 fn()

其中第一部分是编译,而第二部分是执行。

类似地,第二段代码实际是按照

var a;
console.log(a)
a = 2;

这个过程就好像变量申明从它们在代码被“移动”到了最顶部。这个过程就叫做“提升”。

重复声明

var声明可以多次声明同一个变量,如以下代码

function foo() { 
    var age = 16; 
    var age = 26; 
    var age = 36; 
    console.log(age); 
} 
foo(); // 36

let声明

let 和 var差不多。 与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 到底有没有提升

  1. let 的创建过程被提升了,但是初始化没有提升。(也就是所谓的暂时性死区
  2. 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.

for循环中的声明

循环体外输出值

我们先来看个例子

for(var i = 0; i < 10;i++) {
    // 循环内容
}
console.log(i); // 10

使用varfor循环括号中声明的i,可以在循环体外输出。

改成使用let之后,这个问题就消失了,因为let声明的变量的作用域仅限于for循环块内部

for(let i = 0; i < 10; i++) {
    // 循环内容
}
console.log(i) // Uncaught ReferenceError: i is not defined

循环体内输出值

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

为什么会这样呢?

之所以会这样是因为在退出循环的时候,迭代的变量保存的是导致循环退出的值: 10。在之后执行超时逻辑时,所有的i都是同一个变量,因而输出的都是同一个最终值。

我们写个伪代码理解:

((i) => setTimeout(() => console.log(i)))(10)
((i) => setTimeout(() => console.log(i)))(10)
((i) => setTimeout(() => console.log(i)))(10)
((i) => setTimeout(() => console.log(i)))(10)
((i) => setTimeout(() => console.log(i)))(10)
((i) => setTimeout(() => console.log(i)))(10)
((i) => setTimeout(() => console.log(i)))(10)
((i) => setTimeout(() => console.log(i)))(10)
((i) => setTimeout(() => console.log(i)))(10)
((i) => setTimeout(() => console.log(i)))(10)

改成let看看,

for(let i = 0; i < 10; i++) {
    setTimeout(() => console.log(i))
}

正常输出0 1 2 3 4 5 6 7 8 9

上文中提到 let 声明不提升不能重复声明等等特性,用之前的知识我们好像没办法给自己一个解释。

有两个疑惑的点

  • 如果是不重复声明,在循环第二次的时候,又用 let 声明了 i,应该报错才对
  • 就算能重复声明,因为代码中依然只有一个变量 i,在 for 循环结束后,i 的值还是会变成 10 才对。

想深入研究为什么前面所学的知识都不适用了,需要忘记之前的那行特性,因为在for循环中不适用。

经过阅读 ECMAScript 规范13.7.4.7节,可以简单的归纳一下

  • 在 for (let i = 0; i < 3; i++) 中,即圆括号之内建立一个隐藏的作用域
  • for( let i = 0; i< 5; 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))
}

最佳实践

经过前面的学习,我们来总结一下。

在上文提到过let和const的出现就是为了改正var的许多问题而出现的

随着这两个关键字的出现,新的最佳实践也逐渐显现。

  • 一般建议不使用 var

有了let和const,大多数开发者会发现自己不再需要var了。限制自己只使用let和const有助于提升代码质量,因为变量有了明确的作用域、声明位置,以及不变的值。

  • const优先,let次之

在我们开发的时候,为了保护变量,我认为只要变量后续不再改变那就默认使用 const,只有当确实需要修改变量的值时才使用 let。这是因为大部分的变量的值在初始化后不应再改变,而预料之外的变量之的改变是很多 bug 的源头。

参考文献:

《JavaScript高级程序设计(第4版)》

《你不知道JavaScript(下)》

我用了两个月的时间才理解 let

转载自:https://juejin.cn/post/7186953065059057725
评论
请登录