Skip to content
快速预览

var、let、const声明

✍️ w 🕒 2023-06-28 12:51:11(a year ago) 🔗 A.前端知识整理

六种声明情况 let const var function class import 这六种声明情况本质就是三种对应:var 变量声明、let 变量声明和 const 常量声明。但这三种又可以分为两大类

  • 变量声明(varName) -- 在JavaScript中,当我们使用var关键字声明一个变量时,这个变量的初始值会被自动设置为undefined。这个过程被称为变量声明。例如,当我们写下"var x;",我们就声明了一个名为x的变量,并且它的初始值为undefined。

  • 词法声明(lexicalDecls) -- let和const关键字用于词法声明,这是一种在编译阶段就确定变量作用域的声明方式。与var不同,let和const声明的变量在声明之前是不能访问的,这就是所谓的暂时性死区(Temporal Dead Zone)。只有在运行阶段,当代码执行到let或const声明的地方时,变量才会被创建并可以使用。

varName lexicalDecls 可以参考 ES3和ES5+执行过程阶段章节

词法环境(Lexical Environment) 又可以分为三种 LexicalEnvironment/VariableEnvironment/PrivateEnvironment

  • LexicalEnvironment:它是用于描述代码在执行过程中如何访问变量和函数声明的环境。LexicalEnvironment 主要用于处理 let、const 和函数声明等。

  • VariableEnvironment:它与 LexicalEnvironment 类似,但主要用于处理 var 声明的变量。在 ES6 之前,JavaScript 只有 var 声明,因此 VariableEnvironment 和 LexicalEnvironment 实际上是相同的。从 ES6 开始,由于引入了 let 和 const,LexicalEnvironmentVariableEnvironment 开始有所区别。

  • PrivateEnvironment:它是 ECMAScript 规范中描述私有字段(private fields)的概念。私有字段是 ES6 引入的类(class)中的一种特殊字段,它们以 # 开头,只能在类的内部访问。PrivateEnvironment 用于存储这些私有字段及其值

通过一个案例 发现 var 变量声明 和 z 的隐式全局变量 都在 window 全局上,但二者 configurable 不同,并且虽然全局作用域里用let,const创建的变量,虽然也是全局可见,但它并没有创建在 'global' 或者'window'上

js
let a = 100
var b = 100
const c= 100
z = 100
console.log(Object.getOwnPropertyDescriptor(window, 'a'))  // undefined 
console.log(Object.getOwnPropertyDescriptor(window, 'b'))  // {value: 100, writable: true, enumerable: true, configurable: false} 
console.log(Object.getOwnPropertyDescriptor(window, 'c'))  // undefined 
console.log(Object.getOwnPropertyDescriptor(window, 'c'))  //  {value: 100, writable: true, enumerable: true, configurable: true}

在ES6之前,JS中的全局变量是通过'var'关键字声明的。然而,全局变量在编程中通常被视为不好的实践,因为它们可以在任何地方被访问和修改,这可能导致代码的混乱和不可预测的行为。

然而,当ES6出现后,JS并没有完全废弃全局变量的概念。这是因为JS需要向下兼容,也就是说,它需要继续支持在旧版本中编写的代码。因此,尽管全局变量被视为不好的实践,但它们仍然在JS中存在。

但为了区分声明的全局变量 和 隐式全局变量,整个对其进行了细分 ,'隐式全局变量'--'varDecls' 存着,'var' -- 'varNames'存着,let/const -- lexicalDecls

全局作用域

全局变量的使用范围,任何地方都可以使用,全局变量,如果页面不关闭,那么就不会释放,就会占空间,消耗内存,无论是 let const var 只要他们定义在全局的位置,其中 var 的全局定义(函数外部声明),let 和 const(即不在任何函数或块级作用域内)声明,这样的全局变量在整个程序运行期间都会存在,除非你显式地删除它,否则它会一直占用内存

虽然let和const声明的全局变量在全局作用域中,但它们并不会成为全局对象的属性(在浏览器中,全局对象通常是window)。这与var声明的全局变量不同,var声明的全局变量会成为全局对象的属性。

隐式全局变量,就是那些声明的变量没有let const var 声明直接赋值操作的,就叫隐式全局变量,隐式全局解决函数变量只能在函数中诞生的

js
function f1(){
    num = 1
};
f1();
console.log(num)

打印结果1

虽然 let const var 隐式全局变量 他们都是全局但是最大区别,全局变量是不能被删除的,隐式全局变量是可以被删除的

js
var num1=10;
num2=20;

delete num1;//把num1 不会删除
delete num2;//把num2 会彻底删除

var

在其他语言中都有类似块级作用域的概念即所声明的变量只能在块级内使用。但在es6之前,js的变量都是通过 'var' 声明,被声明的变量,就会 变成全局变量(除了在function 中声明的变量)

在其他语言中'{}'即为一个作用域,因此下面的案例在其他语言中在'{}'外是访问不到在'{}'定义的变量,但在'js'中不是这样,if中通过'var' 声明的变量name在'{}' 作用域外可以访问到

js
if(true){
    var name = 'wang'
}
console.log(name)

打印结果
wang

但在函数时候因为之前 说过函数是在函数执行上下文(Functional Execution Context,FEC)他有自己的作用域,此时会产生自己的 VariableEnvironment 环境,在function 中定义的变量,在function 外打印'age'变量时候控制台报出未定义的提示

js
function test() {
    var age = 17
}
console.log(age)

打印结果
age is not defined

变量提升机制

看一个案例

js
showName()
console.log(myname)
function showName() {
    console.log('函数 showName 被执行');
}

然后再次执行这段代码时,JavaScript 引擎就会报错,结果如下:

图 1

可以得出如下三个结论:

  1. 在执行过程中,若使用了未声明的变量,那么 JavaScript 执行会报错(因为变量没有定义,这样在执行 JavaScript 代码时,就找不到该变量,所以 JavaScript 会抛出错误)。
  2. 在一个变量定义之前使用它,不会出错,但是该变量的值会为 undefined,而不是定义时的值。
  3. 在一个函数定义之前使用它,不会出错,且函数能正确执行。

JavaScript中 var 变量和函数声明的提升(hoisting)特性:

  • JavaScript的变量提升特性,即无论变量在哪里声明,都会被提升到当前作用域的最上面。但需要注意的是,只有声明会被提升,赋值不会。
  • 这是指JavaScript的函数提升特性,即无论函数在哪里声明,都会被提升到当前作用域的最上面。这使得我们可以在声明函数之前调用它。
  • "所谓'var' 声明能被提前使用”的效果,事实上是'var' 变量总是被引擎预先初始化为 'undefined'的一种后果",这是指由于变量提升特性,我们可以在声明变量之前使用它,但此时变量的值为undefined。

为什么会产生变量提升,实际上变量和函数声明在代码里的位置是不会改变的,而且是在编译阶段被 JavaScript 引擎放入内存中

图 2

**代码,经过编译后,会生成两部分内容:执行上下文(Execution context)和可执行代码。**执行上下文是 JavaScript 执行一段代码时的运行环境,比如调用一个函数,就会进入这个函数的执行上下文,确定该函数在执行期间用到的诸如 this、变量、对象以及函数等

图 3

js
console.log(num)  // 运行后报错
js
var num;
console.log(num)  //undefined
js
var num = 5;
console.log(num) //5
js
console.log(num) //undefined
var num = 5;
js
var num=5
function f1() {
    console.log(num);
    var num=6;
}
f1() //undefined
  • 再来看看函数

第一种正常输出正常使用

js
function f1() {
    var num = 5;
    console.log(num);
}
f1()  // 打印结果 5

第二种会把整个代码块提前,也就是实际跟案例一一样

js
f1()  //打印结果5
function f1() {
    var num = 5;
    console.log(num);
}

第三种也是把整个函数提前到调用的上面,导致了最上面是function 第二层是f1()第三层是var num=5,又根据变量var num 会提前声明 就出现了undefined

js
f1() //undefined
var num=5
function f1() {
    console.log(num);
}

函数变量提升的特点 会因为重名导致 声明覆盖的问题,首先是编译阶段遇到了第一个 f1 函数,会将该函数体存放到变量环境中。接下来是第二个 f1 函数,继续存放至变量环境中,但是变量环境中已经存在一个 f1 函数了,此时,第二个 f1 函数会将第一个 f1 函数覆盖掉。这样变量环境中就只存在第二个 f1 函数了

接下来是执行阶段。先执行第一个 f1 函数,但由于是从变量环境中查找 f1 函数,而变量环境中只保存了第二个 f1 函数,所以最终调用的是第二个函数

一段代码如果定义了两个相同名字的函数,那么最终生效的是最后一个函数。

js
function f1() {
    var num=1;
    console.log(num);
}
function f1() {
    var num=2;
    console.log(num);
}
f1() // 2
f1() // 2
js
function f1() {
    var num=1;
    console.log(num);
}
f1()  // 2

function f1() {
    var num=2;
    console.log(num);
}
f1() //2

综合案例

js
function f1() {
   var a;//局部变量
   a=9;
   //隐式全局变量
   b=9;
   c=9;
   console.log(a);//9
   console.log(b);//9
   console.log(c);//9
 }
 f1();
 console.log(c);//  9
 console.log(b);// 9
 console.log(a);//报错

var 缺点

  • 好多js自带的变量都是在'window'变量中,就会导致变量覆盖问题",这是指如果我们声明的全局变量和JavaScript自带的全局变量同名,就会覆盖原来的全局变量,可能导致程序出错。
js
var RegExp = 'wang'
console.log(window.RegExp) // wang  覆盖了原先的正则对象
  • 没有块级作用域只有函数级别的,否则就是全局级别

let / const

为了解决'var' 或多多少的问题,es6 引入了let/const ,let/const 可以很好的限制变量的作用域使用范围,不会出现和var将window变量覆盖的问题,变量是不会挂载到window 全局上,let和const 也不会出现像'var' 一样的变量提升问题

但也因此 由于 JavaScript 需要保持向下兼容,var 和 let const 这两套机制还是同时运行在一套系统中的。

如果从执行上下文来看两者的区别在规范中的定义不同产生了不同效果

  • LexicalEnvironment:它是用于描述代码在执行过程中如何访问变量和函数声明的环境。LexicalEnvironment 主要用于处理 let、const 和函数声明等。

  • VariableEnvironment:它与 LexicalEnvironment 类似,但主要用于处理 var 声明的变量。在 ES6 之前,JavaScript 只有 var 声明,因此 VariableEnvironment 和 LexicalEnvironment 实际上是相同的。从 ES6 开始,由于引入了 let 和 const,LexicalEnvironmentVariableEnvironment 开始有所区别。

从作用域来看作用域是指在程序中定义变量的区域,该位置决定了变量的生命周期。通俗地理解,作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期

在 ES6 之前,ES 的作用域只有两种:全局作用域和函数作用域。

  • 全局作用域中的对象在代码中的任何地方都能访问,其生命周期伴随着页面的生命周期。
  • 函数作用域就是在函数内部定义的变量或者函数,并且定义的变量或者函数只能在函数内部被访问。函数执行结束之后,函数内部定义的变量会被销毁。

随着es6 let const 支持了块级作用域 js 才开始逐步解决之前一些历史问题

let

使用let 后的变量也可以像其他语言一样作用域只在'{}' 生效,并且不会出现将 window 中的同名变量覆盖问题

js
if(true){
    let sex = '妖'
}
console.log(window.sex) // undefined
console.log(sex) // 报错

const

const 和let 作用域基本一致,只是使用起来的场景不同,const声明的是常量,被设置后事不允许被修改的,本质是 const 声明其实本质判断是内存是否改变, 因此如果声明的变量是对象,是允许修改绑定对象的值,但不允许修改被绑定的对象,换成其他对象,即使来说 const a = 3, a = 3 此时你觉前后都是a 指向都是3内存没发生变化,实际在堆栈说过 这里基础类型创建在栈中,每次重新赋值都会创建新的变量空间 此时 a 和上一次a只是看着一样但在栈中是两个空间 两个地址

const 是常量因此声明时候必须要赋值,下面的写法let 和 var 都可以但是const会报错

js
const name
name = 'w'

关于块作用域

js
if(true){
    const sex = '妖'
}
console.log(window.sex) // undefined
console.log(sex) // 报错

let 和 const 没有变量提升(效果)

  • 变量声明(varName) -- 在JavaScript中,当我们使用var关键字声明一个变量时,这个变量的初始值会被自动设置为undefined。这个过程被称为变量声明。例如,当我们写下"var x;",我们就声明了一个名为x的变量,并且它的初始值为undefined。

  • 词法声明(lexicalDecls) -- let和const关键字用于词法声明,这是一种在编译阶段就确定变量作用域的声明方式。与var不同,let和const声明的变量在声明之前是不能访问的,这就是所谓的暂时性死区(Temporal Dead Zone)。只有在运行阶段,当代码执行到let或const声明的地方时,变量才会被创建并可以使用。

let const 之所以没有提升效果,注意我说的是效果,但实际在声明阶段他们 let 和 const 在声明阶段确实会被提,但是他们又和 var 提升不同 var会将赋值一个 undefined,const 和 let并且必须执行过变量声明语句,变量才会从临时锁死区中移除才能访问其变量内容,简单说:这些变量会被创建在包含他们的词法环境被实例化时,但是是不可以访问它们的,直到词法绑定被求值

js
// value 所在的作用域'{}' 当在这个作用域的时候,value
// 被let 声明所以放到了对应的TDZ 区域,但想获取必须还要执行,因此没有执行过所以报错
if(true){
    console.log(typeof value)
 // Cannot access 'value' before initialization 初始化前无法访问value
    let value = 'bule'
}
js
// 这时候value 和if 中的value 所在的区域不同
// 所以打印结果是undefined
console.log(typeof value)
if(true){
    // 这时候的value他 的作用域只在if中
    let value = 'bule'
}

三者要注意的问题-- 不允许重复声明

  1. let/const不允许在相同作用域内,重复声明同一个变量。
  2. 即使同一个变量名用三个不同修饰也是不允许的,一个变量名只能有唯一的修饰,下面的写法是错误的
js
let name = 'wang'
var name = 'wang'    
const name = 'wang'

在循环语句中 let / const / var 变量

因为他们的特性,因此在循环语句里也会又不同的效果

var

下面案例打印0到10

js
for(var i =0;i<10;i++){
    console.log(i) // 0..4..9 0到10
}
console.log(i)  // 10

第二个是打印10次10

js
const list = []
for(var i =0;i<10;i++){
    list.push(()=>{
        console.log(i)
    })
}
list.forEach(item=>{
    item() // 打印10次10
})
console.log(i) // 10

第一个案例,for 内部打印是每个阶段的赋值结果,但在最有 因为 var 作用域在全局的关系,所以 此时 var 定义的变量都是共享一个,最后打印出来的是 10

第二个案例内部创建的函数最后引用了相同的变量引用导致打印是十次10,因为 var 没有块级作用域,此时 var 明显是一个全局作用域,因此实际都在改变一个变量

这类问题在之前解决方案就是利用闭包,因为 var 除了全局作用域 还有 函数作用域,利用函数作用域来解决这类问题就产生了闭包,也就是限制作用域链

js
const list = []
for(var i =0;i<10;i++){
    list.push(((value)=>{
       return ()=>{ console.log(value)}
    })(i))
}
list.forEach(item=>{
    item() // 打印0-9
})
console.log(i) // 10

再来看同样逻辑的案例下面{1}打印出来的次数不想想象中打印了9次,就是因为作用域声明的问题,内外的for相当于用的都是同一个i,内层循环的时候,已经将i的整体变为了3,因此外层只循环了一次,可以用闭包 let const来解决计数器内外名相同的问题,但是开发原则上是不建议相同的名字

js
for(var i=0;i<3;i++){
    console.log(i,'外')
    for (var i = 0; i < 3; i++) {
        console.log(i) // {1}
    }
    console.log(i,'内')   
}
// 打印结果:
0 '外'
0
1
2
3 '内'

let

随着 let 支持 块级作用域 新的解决方式变得简单 不用采用闭包

js
const list = []
for(let i =0;i<10;i++){
    list.push(()=>{
        console.log(i)
    })
}
list.forEach(item=>{
    item() // 打印0-9
})
console.log(i) // 报错此时i作用域只在for循环的{}中

let 是块级作用域只作用在块 此时并不是全局作用域 因此在外面 打印报错 i is not defined

js
for(let i =0;i<10;i++){
    console.log(i) // 0..4..9 0到10
}
 console.log(i)  //  i is not defined

const

const 不能解决类似 问题,下面代码会报错,因为i++,将const 赋值i 进行了更改

js
const list = []
for(const i =0;i<10;i++){
    list.push(()=>{
        console.log(i)
    })
}
list.forEach(item=>{
    item() 
})
console.log(i)

循环一些其他奇怪问题

下面的案例使用了let 声明了变量i,只不过分别在for 里面 和 for 外面都声明了,但最后执行结果是,打印了三遍'foo' 这段代码可以理解成,两个i分别在不同的作用域因此是不会相互影响的

js
for (let i = 0; i < 3; i++) {
  let i = 'foo'
  console.log(i)
}
// 打印结果:
foo
foo
foo

如果换成var 声明的i共享了,当下一次进入for循环时候其实是 'foo'❤️ 是false 因此循环结束了

js
for (var i = 0; i < 3; i++) {
    var i = 'foo'
    console.log(i)
}
// 打印结果:
foo

实际开发中

也就是这三者在三个不同的地方被管理,所以他们也确实是创建在不同的地方。但是从'作用域链'的角度上来说,它们并没有级别高低(也就是parent没有相互指向)。使得它们存取的效果有差别的,是因为'全局环境'采用了词法环境优先(也就是let/const声明)的顺序。

图 4

在实践时候明确可以使用es6的情况下,应该不用var,主要const ,配合let

参考

浏览器工作原理与实践_李兵

Released under the MIT License.