变量声明

2023/6/21 JavaScript

# 前言

相信很多同学都能够说出var,let,const三者之间的区别,即便说的不全,也总能说出其中几点内容,但如果问你var为什么可以重复声明,而let、const却不能重复声明呢,以及在全局作用域中,用letconst 声明的变量没在 window 上,那在哪里呢?我们如何去获取呢?这些应该有不少人不清楚吧,今天我们就一起来看看这三者之间的区别,以及揭开后面这几个问题的答案吧~

如果这篇文章有帮助到你,❤️关注+点赞❤️鼓励一下作者,文章公众号首发,关注 前端南玖 第一时间获取最新的文章~

# var

# 全局变量

在ES6出来之前,声明变量就只有var这一个关键词,并且当时没有块级作用域的概念,所以顶层对象的属性与全局对象的属性是一样的,用var声明的全局变量也就是顶层变量。

顶层变量,在浏览器中是指window对象,在node环境中是指Global对象

var name = 'nanjiu'
console.log(window.name) // 'nanjiu'

# 局部变量

在函数中用var声明的变量是局部变量,通过windowglobal访问不到

function sayName() {
    var s_name = '南玖'
    console.log(s_name) // '南玖'
  }
sayName()
console.log(window,window.s_name) // undefined

所以当时为了解决全局变量混乱的问题,一般都会借用函数作用域在解决,这一点,你可以去看jq的源码,它的代码都是放在一个自执行函数中的,与外界隔开~

如果在函数中不使用var声明变量,该变量也是全局的

function sayName() {
    s_name = '南玖'
    console.log(s_name) // '南玖'
  }
sayName()
console.log(window.s_name) // ‘南玖

# 变量提升

使用var声明的变量会存在变量提升的情况,也就是在变量声明之前,你可以访问到它,只不过它的值是undefined

console.log(s_name) // undefined
var s_name = '南玖'

上面的代码在编译时会变成以下代码,这也就是为什么你可以访问到它,但它的值是undefined

var s_name
console.log(s_name) // undefined
s_name = '南玖'

# 可以重复声明

使用var关键字声明的变量可以重复声明,后者会覆盖前者

var s_name = 'nanjiu'
var s_name = 'frontend'
console.log(s_name) // 'frontend'

# let

该关键字是ES6新增的,用来声明变量,用法与ES5中的var类似,但是所声明的变量,只在let命令所在的代码块中生效。(块级作用域)

# 块级作用域

ES5 只有全局作用域和函数作用域,没有块级作用域,这带来很多不合理的场景。

{
  let s_name = 'nanjiu'
  var age = 18
}
console.log(s_name) // 报错 Uncaught ReferenceError: s_name is not defined
console.log(age) //18

# 不存在变量提升

var命令会发生“变量提升”现象,即变量可以在声明之前使用,值为undefined。这种现象多多少少是有些奇怪的,按照一般的逻辑,变量应该在声明语句之后才可以使用。

为了纠正这种现象,let命令改变了语法行为,它所声明的变量一定要在声明后使用,否则报错。

console.log(s_name) //报错
let s_name = 'nanjiu'

# 暂时性死区

只要块级作用域内存在let命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。

在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。

var name = 'nanjiu'
{
  console.log(name) // 报错
  let name = '南玖'
}

在上面代码中声明了一个全局变量name和一个块级作用域中的局部变量name,导致后者绑定这个块级作用域,所以在let声明变量前,对name访问会报错。在let声明变量name之前都是变量name暂时性死区

ES6 明确规定,如果区块中存在letconst命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。

ES6 规定暂时性死区和letconst语句不出现变量提升,主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。这样的错误在 ES5 是很常见的,现在有了这种规定,避免此类错误就很容易了。

总之,暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。

# 不允许重复声明

let不允许在相同作用域内,重复声明同一个变量。

// 报错
function sayName() {
  let name = 'nanjiu'
  var name = '南玖'
}
// 报错
// 重新声明函数参数也不行
function say (name) {
  let name
}

# const

该关键字是ES6新增的,用来声明常量,一旦声明,该常量的值就不能改变。

# 声明常量

const PI = 3.14
PI // 3.14

PI = 3 // 报错

const实际上保证的并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动

对于简单类型的数据,值就保存在变量指向的那个内存地址,因此等同于常量

对于复杂类型的数据,变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的,并不能确保改变量的结构不变

const obj = {
  name: 'nanjiu',
  age: 18
}
obj.name = 'hahaha'  //可以正常运行

obj = {} // 报错

# 声明与赋值必须同时进行

const声明的变量不得改变值,这意味着,const一旦声明变量,就必须立即初始化,不能留到以后赋值。

const name
// 报错 Uncaught SyntaxError: Missing initializer in const declaration

# 块级作用域

const的作用域与let命令相同:只在声明所在的块级作用域内有效。

{
  const name = 'nanjiu'
  console.log(name) // 'nanjiu'
}
console.log(name) // 报错

# 不存在提升,暂时性死区

let类似,const命令声明的常量也是不提升,同样存在暂时性死区,只能在声明的位置后面使用,否则报错。

console.log(name) // 报错
const name = 'nanjiu'

# 不允许重复声明

const声明的常量,也与let一样不可重复声明。

var name = 'nan'
const name = 'jiu' // 报错

# 三者的区别

# 变量提升、暂时性死区

var声明的变量存在变量提升,即变量可以在声明之前调用,但是值为undefined

letconst不存在变量提升,即它们所声明的变量一定要在声明后使用,否则就会报错

var不存在暂时性死区

letconst存在暂时性死区,只有等到声明变量的那一行代码出现,才可以获取和使用该变量

// var
console.log(name1)  // undefined
var name1 = 'xiaoming'

// let 
console.log(name2)  // Cannot access 'name2' before initialization
let name2 = 'xiaohong'

// const
console.log(name3)  // Cannot access 'name3' before initialization
const name3 = 'xiaobai'

# 块级作用域

var不存在块级作用域

letconst存在块级作用域

// var
{
    var name1 = 'xiaoming'
}
console.log(name1) // 'xiaoming'
// let
{
    let name2 = 'xiaohong'
}
console.log(name2) // 报错 Uncaught ReferenceError: name2 is not defined
// const
{
    const name3 = 'xiaobai'
}
console.log(name3) //报错 Uncaught ReferenceError: name3 is not defined

# 重复声明

var允许重复声明变量

letconst在同一作用域不允许重复声明变量

// var
var name1 = 'xiaoming'
var name1 = 'xiaoming2' //xiaoming2

//let 
{
    let name2 = 'xiaohong'
    let name2 = 'xiaohong2' //报错
}

//const
{
    const name3 = 'xiaobai'
    const name3 = 'xiaobai' // 报错
}

# 修改变量

varlet声明的变量可以修改

const声明一个只读的常量。一旦声明,常量的值就不能改变

// var
var name1 = 'xiaoming'
var name1 = 'xiaoming2' //'xiaoming2'

//let 
{
    let name2 = 'xiaohong'
    name2 = 'xiaohong2' // 'xiaohong2'
}

//const
{
    const name3 = 'xiaobai'
    name3 = 'xiaobai2' // 报错 Uncaught TypeError: Assignment to constant variable.
}

# 为什么var可以重复声明?

我们有时候可能会觉得JS很奇怪,与其它语言差别很大,容错率很高,一些其他语言常见的小错误JS都能大度得包容,我们可以来看下面两段代码:

var name = 'nanjiu'
var name = '南玖' 
console.log(name) // '南玖'

使用var重复声明一个变量时,后面会覆盖前者,所以上面会打印出南玖

var name = 'nanjiu'
var name
console.log(name) // 'nanjiu'

这里却是打印出nanjiu,而不是undefined,这个可以由我们上面讲到的变量提升来解释,但它能够重复声明是为什么呢?

我们得从JS代码的运行机制说起:

在JS代码运行过程中: 引擎负责整个代码的编译和执行,编译器负责语法分析、词法分析、代码生成等,而作用域则负责维护所有的标识符(变量) 当执行上面的代码时,可以简单的理解为给新变量分配一块内存,命名为name,并赋值为nanjiu;但在运行的时候编译器与引擎还会进行两项额外的操作,即判断变量是否已经声明:

  • 首先编译器对代码拆解,从左往右遇到了var name, 然后去询问作用域是否存在这个变量,如果不存在就让作用域声明一个新的变量name,如果存在就忽略var继续往下编译,这时name='nanjiu'被编译成可执行的代码供引擎使用
  • 引擎遇见name='nanjiu'时也会去询问作用域是否存在这个变量,若存在,则赋值为nanjiu,若不存在,就沿着作用域往上查找,若找到了,赋值为nanjiu,若没找到,让作用域声明一个新的变量nanjiu

用代码解释就是:

var name
name = 'nanjiu'
// var name // 忽略
name = '南玖'
console.log(name) // '南玖'
var name
name = 'nanjiu'
// var name // 忽略
console.log(name) // 'nanjiu'

# 为什么let、const不能重复声明?

在ES6规范有一个词叫做Global Enviroment Records(也就是全局环境变量记录),它里面包含两个内容,一个是Object Enviroment Record(它不等同于window对象),另一个是Declarative Enviroment Record

  • 函数声明和使用var声明的变量会添加进入Object Enviroment Record中。

  • 使用let声明和使用const声明的变量会添加入Declarative Enviroment Record中。下面是ECMAscript规范中对var,let,const的一些约束。

  • 使用var声明时,V8引擎只会检查Declarative Enviroment Record中是否有该变量,如果有,就会报错,否则将该变量添加入Object Enviroment Record中。

  • 使用letconst声明时,引擎会同时检查Object Enviroment RecordDeclarative Enviroment Record是否有该变量,如果有,则报错,否则将将变量添加入Declarative Enviroment Record中。

##在全局作用域中,用 let 和 const 声明的变量没在 window 上,那在哪里呢?我们如何去获取呢?

# ES5与ES6的区别

ES5中,顶层对象的属性与全局变量是等价的,varfunction声明的全局变量也是顶层变量,我们可以通过windowglobal来访问

var s_name = 'nanjiu'
function say() {}
console.log(window.s_name) // 'nanjiu'
console.log(window.say) // ƒ say() {}

但是ES6规定,varfunction声明的全局变量,依旧是顶层对象的属性,但letconstclass声明的全局变量,不属于顶层的属性。

let s_name2 = 'nanjiu'
const age = 18
class Person{}
console.log(window.s_name2) // undefined
console.log(window.age) // undefined
console.log(window.Person) // undefined

###用 let 和 const 声明的变量在哪里呢?

既然用 let 和 const 声明的变量通过window访问不到,我们可以来看下浏览器是如何处理的:

var age = 20
let s_name2 = '南玖'
const gender = 'man'
{
    let gzh = '前端南玖'
    debugger
}

image-20220220221827523

通过上图也可以看到,在全局作用域中,用var声明的变量存在于全局变量window上,用 letconst 声明的全局变量并没有在全局对象中,只是一个块级作用域(Script)中,而用 letconst 配合{}声明的变量,则存在于块级作用域Block中。