JS中的var/let/const和暂时性死区实例分析

寻技术 JS脚本 2023年12月14日 104

本篇内容介绍了“JS中的var/let/const和暂时性死区实例分析”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!

js中变量的特征

js的变量是松散类型的。变量可以用于保存任何类型的数据。所以js也被称为弱类型语言。

变量的定义与访问

简单说下作用域

什么是作用域,简单来说就是这个变量起作用,能被访问到、使用到的区域。

语法

定义

变量的值的存储位置

变量声明时从栈中申请空间来存储变量的值。

使用关键字

可以使用var、let、const三个关键字来定义变量,后两个关键字是自ES6才拥有的,推荐只使用后两个,不用第一个。当然考虑兼容性的话另说。

定义变量的关键词 变量的标识符名称[[ = 初始值], 第二个变量名称[ = 初始值2], 变量3[ = 初始值3], ...];

其中中括号括住的内容为选写内容,

...
表示重复语法。

使用逗号间隔可以同时声明多个变量。

变量 = 初始值
相当于给变量赋予一个初始的值。当第一次访问该变量时,若先前没有对变量进行改变的话,那么获取到的就是这个初始值。这样的操作我们称为初始化。

若是省略初始化这一步,那么获取到的值就是undefined,该值表示该变量没有进行初始化,值未定义。

例如

let a = 3, b, c = 4;
这就生成了3个变量
a,b,c
,初始值分别为3,undefined,4。

不用关键字

直接给一个之前没定义成变量的或是定义后失效的标识符赋予一个值。那么在执行完该赋值语句时,该标识符将代表一个全局变量【可以先知道这么叫,具体理解需要作用域知识】。

从此刻位置开始,向下任何位置都可以访问到该变量,哪怕你变量定义在一个块中,我也可以在块外访问到。

但是若是在赋值语句前对该变量进行访问,会报错。

此时该变量的作用域称为全局作用域

console.log(m);//访问不到,报错导致整个程序终止,下面的语句不执行
m = 109;
console.log(m);//删除上面报错的语句删除后,该句会执行,且访问到

注意

当这个语法放在函数中时,仍会创建出一个全局变量而不是局部变量。只不过当这个函数在真正执行之前,该变量都不会被创建。当函数执行之后,才会被创建。后面的代码才能访问到该变量。

这是因为js为解释型语言。可以近似看作解析一句代码执行一句,当然这么说不准确,但可以简单这样子理解。所以函数未被执行前,该全局变量无法被浏览器获知,也就没有创建。

function test() {
    m = 10;
}
//此行以及以上的代码,除了test函数内部外,都无法访问m,m未被创建
test();//此行代码执行完毕后,可以访问m

var

使用该关键字声明的变量拥有的特点:

声明的变量为局部或是全局变量:

若是在一个函数中或是在一个块中定义该变量,那么这个变量将会是属于该块【或函数】的局部变量。在块或函数的内部是可以访问到的。但是在外部就不行了。同时也意味着,在出了这个块的时候,该变量就会被销毁。下次在访问到该块的时候,会创建一个同名的新的变量。而不是原来的。 不过定义在最外面的全局作用域中,它就变成了个全局变量。

function a() {
    var x = 1;
    console.log(x);//当执行函数时,该语句会输出x
}
a();//输出x的值,也就是1.说明x在a函数内部可以访问
console.log(x);//报错

作用域提升:

我们知道当我们访问一个未定义的变量时,他会报错。但是下方代码却好像违背了常识。

console.log(a);//不报错且输出undefined
var a = 1;
console.log(a);//1

按理说,解释一句代码,执行一句代码的话,只能在定义了变量之后,我才知道这个变量。那为什么使用了var定义后就可以在前面访问呢?

这是因为js会将使用

var
所定义的变量的作用域提升,将
var a = 1;
分成了两个部分:
var a;
a = 1;
。并且将
var a;
放到a定义位置所处的那个块中的最前面【最外层的代码我们说他处于一个同步代码块中,虽然没有花括号,但是也看做一个块】。并将
a = 1;
留在了原地。

所以上面的代码可以认为等同于下方代码。

注意:变量提升【作用域提升】,仅仅会将变量的声明提升,初始化的赋值语句则会留在原地。所以块开头到声明部分的中间那段区域中,变量的值为

undefined
var a;
console.log(a);//不报错且输出undefined
a = 1;
console.log(a);//1

同样下方两个代码效果相同:

function a() {
    console.log(x);
    var x = 1;
    console.log(x);
}
function a() {
    var x;
    console.log(x);
    x = 1;
    console.log(x);
}

定义的变量的作用域为函数作用域或全局作用域

当var定义的变量是函数作用域时,var是在块中定义的变量。从块的开始到块的结束都能访问到他所定义的变量。

当var定义的变量为全局作用域时,var是在最外层的同步代码块定义的变量。在代码中的任何一个位置都可访问到。

声明的变量若是在最外层同步代码块【也叫全局作用域】中会作为window对象的属性

字面意思。当定义变量语句放在全局作用域中,那么它就会被挂载在全局上下文的变量对象身上【这里以后讲上下文就懂了】,而全局上下文的变量对象可以通过window访问。

所以会作为window的属性。

允许冗余声明

即允许var定义多个同名的变量。

你可以将它理解为:var定义的语句都被分为

var 标识符;
标识符 = 初始值;
两部分。第一部分都被提到块的最前面,重合的都被合并。第二部分留在原位。

所以相当于一个变量在不同的位置被不停地变换值而已。

var a = 1, a = 2;
console.log("结果");
console.log(a);//输出2
var a = 3;
var a;
a = 1;
var a;
console.log(a)//输出1

for循环头部定义的变量不会渗透在循环外部

比如说:

for(var i = 0; i < 5; i++) { 
    console.log(i); 
}
console.log(i);

在输出1,2,3,4,5后,仍然会输出5,而不是报错。

其实就是相当于下面的代码:

var i;
for(i = 0; i < 5; i++) { 
    console.log(i); 
}
console.log(i);

let与暂时性死区

使用let命名的变量所拥有的特点与var差不多。但是【下面就是5个区别了,唯2的共同点可以说是定义的语法和定义出来的变量可以为局部或是全局变量吧】

不具作用域提升这个特点

而从块的开头到定义这个变量的位置之间的区域,我们叫做暂时性死区

let定义的变量为块级作用域

根据作用域的概念来讲。由于var与let所定义的变量的可访问的区域不同。

所以let定义的变量的作用域自然与var所定义的作用域不同。

块级作用域:变量从定义处到块结束都可以被访问到,其他地方不行。

不允许同一个块中冗余声明

同字面意思。

首先,不允许let在同一个块中定义多个标识符相同的变量。

而且,若是有不用标识符定义的全局变量或是var定义的变量,与let定义的变量的标识符相同,且处于同一个块中,那么,会报错。

但是嵌套重复定义是允许的。【js中声明和定义是一回事】【以下理论出自红宝书】

由于js引擎【就是负责执行js代码的那个东东】会记录:声明或使用的标识符和该标识符所在的块作用域。

并且进行对比查重。而查重过程中只有块作用域不同而标识符相同时,才会报冗余定义的错。

像是

let a = 1; a = 3;
这段代码中,这两个a是同一个块作用域。

let a = 2,a = 3;
这段代码中,这两个a是不同的块作用域,因为声明的位置不同,所以该变量的块作用域的起始位置不同,块作用域本身自然不同。所以会报错。

所以若是在不同的块中定义相同的变量是可以的,不会报错。

但是多层嵌套重复定义的变量在使用时究竟用哪一个呢

这种现象我们叫做作用域覆盖。其实听名字都能大概猜出最终会用哪个了。

外层的作用域被内层的作用域覆盖掉。而当前重复的区域的归属权自然不是被覆盖掉的作用域,而是覆盖者。而变量使用谁自然是看当前位置是处于哪个作用域中。

function a() {
    let a = 1;
    //下方对a访问是访问a变量而不是a函数,也算是一个作用域覆盖。
    console.log(a);//输出1
    function b() {//若是改成a也会报错。同样冗余定义
        //console.log(a); 会报错,也是冗余问题。使用的也会被记录
        let a = "s";
        console.log(a);
    }
    b();//输出s
}
a();//执行的是上面的函数。

在全局作用域中定义变量,变量不会作为window的属性

字面意思,let定义的变量他不会被挂载在全局上下文的变量对象上。但是它仍然是在全局作用域中被定义的,所以在全局的定义位置到全局结束都可访问到该变量。

for循环头部定义的变量会渗透到循环外部

与var不同,let在for头部定义的变量并不会渗透出去。它相当于定义在了for循环的循环体的那个代码块内部。

for (let i = 0; i < 5; i++) {
    console.log(i);
}
console.log(i);//报错
//和下方的代码效果等价
while(true) {
    let i;
    if (i < 5) {
        console.log(i);
    }
    else
        break;
    i++;
}
console.log(i);

像这种的区别要是单纯使用for循环那么不足一提。但是若是在for循环使用闭包函数引用这个迭代变量i,就会出问题了。

最典型的例子是这个:【不懂闭包的就光看结果吧,以后介绍闭包的时候会旧事重提的。】

for(var i = 0; i < 5; i++) {
    setTimeout(()=>console.log(i), 4);
}
//最终输出5个5,等同于下面代码
var i;
for(i = 0; i < 5; i++) { 
    setTimeout(()=>console.log(i), 4);
}

setTimeout
是延时函数,第一个参数是回调函数【要执行的函数】,第二个参数是延迟的时间【单位ms】。表示延迟多长时间执行该函数。

由于闭包函数会延长变量的生命周期。所以i的生命周期会被延长。当执行

setTimeout
函数的回调【就是第一个参数】时,
i
仍然是当时调用
setTimeout
函数时的那层
for
循环的
i
。但是由于每一层
for
循环使用的其实是同一个变量。而回调又是延时后执行的,
for
循环在回调执行时早就运行完了。所以最终输出的会是
i
这个唯一的迭代变量在经过5次循环后的值&mdash;&mdash;5.

而若是let则没这个问题了。

for (let i = 0; i < 5; i++) {
    setTimeout(()=>console.log(i), 4);
}
//和下方的代码效果等价
while(true) {
    let i;
    if (i < 5) {
        setTimeout(()=>console.log(i), 4);
    }
    else
        break;
    i++;
}

由于每一层循环都是一个新的变量。所以回调所引用的是不同的变量。输出的自然是我们期望的

0,1,2,3,4

const

和let的效果、特点近乎一样。

唯一的不同就是:const变量在定义的同时必须初始化。再者定义完成后变量对应的那个栈中的存储空间的值就不允许被改变了,否则会报错。【变量声明时从栈中申请空间来存储变量的值】

透露一点。若是const变量被赋值为一个js对象【准确的来说是一个引用类型的数据】时,对象的属性是允许被改变的。因为const仅仅限制了栈中值不允许被改变。而对象在给变量时,是将这个对象本身的地址赋给了栈中的存储空间。而他自身的数据存储在堆之中。所以堆中的数据如何修改不关const的事。

不同<script>中声明的变量能否使用

可以使用,无论是内嵌代码块还是外部引用的代码块,但凡是该页面中的代码。只要是已经声明且声明在全局作用域中,那么在定义的

<script>
后面的
<script>
中就可以访问到该变量。

但是有一个要注意的点是:一定要保证一个标识符在之前的代码块中没有被定义过,才能再次定义,否则会报错。

COOKBOOK

  • 推荐使用关键字来创建变量。因为在块中定义的全局变量很难维护,容易造成困惑。且在严格模式下不允许不使用关键字就创建变量。

  • 当要考虑兼容不允许ES6的浏览器时,请全部使用var定义变量。

  • 当不用考虑ES6以前标准的兼容时,请尽量全部使用const和var。其中不改变的一些内容,要使用const定义。最好做到const优先使用。

关闭

用微信“扫一扫”