2

JavaScript 的var、let 与const

 2 years ago
source link: https://www.fly63.com/article/detial/11729
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

JavaScript 因为历史悠久, 所以你可能会遇到经过多人更改过的JavaScript 程式, 里面混杂了不同时期JavaScript 的语法, 导致有时候JavaScript 程式就是难懂, 本文将针对宣告变数的几种方法加以说明, 期望能让大家快速理解其中的差别。

使用var 宣告函式层级的变数

过去最常看到的变数宣告方是就是使用 , 它宣告的变数是函式层级, 也就是只要离开宣告时所在的函式, 这个变数就失效了, 例如: var

function  foo ()  {
  var a = 23;
  console.log(a);
}

foo () ; 
console.log(a);

执行结果如下:

23 
Uncaught ReferenceError: a is not  defined

由于 a 是在函式 foo 中宣告, 所以叫用 foo 函式时可以取用变数 a , 但是在函式外取用变数 a 就会引发未定义识别字的错误。

如果在函式外使用 var 宣告变数, 它就会变成全域变数, 在程式内任何地方都可以取用, 例如:

var a = 23 ;

function  foo ()  {
  console.log(a);
  a = 24;
}

foo () ; 
console.log(a);

执行结果如下:

宣告与设定初值分离

你也可以把宣告和设定初值分开来, 不一定要同时完成, 像是这样:

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

如果你希望在同一个地方集中宣告变数, 但是在要用到该变数的时候才设定值, 这样的写法就会很有用。

你也可以重复宣告同一个变数, 只要没有设定新的值, 就会保留原值, 例如:

var a = 23 ;
 console .log(a);
 var a;
 console .log(a);
 var a = 24 ;
 console .log(a);

执行结果如下:

第三行虽然重新宣告 a , 但是没有重新设定值, 所以 a 仍然是23。

变数提升(variable hoisting)

变数有一个我其实不知道有什么用途, 但是很多人喜欢拿来考别人的功能, 叫做 , 会把宣告变数的动作提升到执行其他程式前先完成, 意思就是在进入变数的有效范围内时, 会在执行第一行程式前就先宣告变数。因此, 在执行第一行程式的时候, 变数就已经存在了。例如: 變數提升 (variable hoisting)

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

执行结果如下:

undefined 
23

由于在执行第一行程式前, 就会先宣告变数, 因此第一行程式并不会引发变数尚未宣告的错误。不过对于以 var 宣告的变数, 变数提升 只会先宣告变数, 并不会执行设定初值的程式, 以上例来说就是不会执行 , 而是设定初值为 , 所以你会看到第一行印出 的值是 。等执行到第二行才会设定变数 的值为23, 因此第三行就会印出23 了。 a = 23undefinedaundefineda

全域变数会成为全域物件的属性

以 var 宣告的全域变数会成为全域物件的属性, 例如:

var a = 23 ;
 console .log(globalThis.a);

执行结果如下:

不过这个属性是 不可设定(non-configurable) 的, 也就是不能使用 移除, 例如以下的程式虽然不会发生错误, 但是 却没有作用: delete delete

var a = 23;
 delete a;
 delete globalThis.a;
console.log(a);

执行结果 a 仍然存在, 印出的值也是正确的:

如果采用严格模式, 就会看到错误讯息:

'use strict' ;

var a = 23 ;
 delete globalThis.a;
 console .log(a);

执行结果如下:

Uncaught TypeError: property  "a"  is non-configurable and can 't be deleted

使用let 宣告区块层级的变数

所谓的区块, 就是由一对大括号括起来的区域, 使用 宣告的变数只要出了所在的区块, 就会失效, 例如: let

{
   let a = 23 ;
  console. log (a);
}

console. log (a);

执行结果如下:

23 
Uncaught ReferenceError: a is not  defined

由于第二次取用 a 时已经离开了宣告变数的区块, 因此变数 a 已经失效, 就会引发未定义识别字的错误。

在大部分的情况下, 我们很容易辨识区块, 不过在像是 for 的叙述中, 初始设定也是区块的一部份, 因此在初始设定内宣告的变数在 for 结束后也一样会失效, 例如:

for (let i = 0 ; i < 2 ; i ++)
{
  console.log(i);
}

console. log ( i );

执行结果如下:

0 
1 
Uncaught ReferenceError: i is not  defined

如果改用 var 宣告变数, 由于并没有离开函式范围, 所以不会引发错误, 例如:

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

console. log ( i );

执行结果最后会印出回圈结束时的 i 值:

在任何区块外使用 let 建立的变数一样是全域变数, 可在程式中任何地方取用, 例如:

let i = 0 ;

for ( i = 0 ; i < 2 ; i ++) {}

console. log ( i );

结果如下:

不能重复宣告变数

变数提升不会设定初值

以 let 宣告变数也一样具有变数提升功能, 但是并不会设定初值, 在使用变数前一定要先透过 let 宣告, 例如:

console. log (a);
 let a = 0 ;

执行时就会引发错误:

Uncaught ReferenceError: can 't access lexical declaration ' a ' before initialization

表示不能在设定初值前就取用已宣告的变数 a 。

以let 宣告的变数不会成为全域物件的属性

以 let 宣告的变数并不会像是以 var 宣告的变数那样成为全域物件的属性, 例如:

let a = 23 ;
console. log (globalThis.a);

执行时印出的并不是 a 的值, 而是 undefined :

undefined

表示全域物件中并没有 a 这个属性。

使用const 宣告不能变更的变数

你也可以使用 宣告变数, 不过这种变数如同 字面所示, 是不能变的, 中文翻译为 常数 。先来看看以下的例子: const const

const a = 23 ;
console. log (a);
a = 24 ;

执行后如下:

23 
Uncaught TypeError: invalid assignment to  const  'a'

在第三行尝试设定常数内容时就会引发错误, 告诉你不能设定以 const 宣告的常数。

宣告常数时一定要设定值

以 const 宣告常数时必须一并设定初值, 不能将宣告与设定初值分开进行, 像是以下的例子就会引发错误:

const a;
a = 23 ;
console. log (a);

执行结果如下:

Uncaught SyntaxError : missing = in  const declaration

错误讯息告诉我们在 const 宣告时少了设定初值的 = 。

变更常数所参照的物件

请特别注意, 不能变更以 const 宣告的常数指的是不能重新设定常数本身, 如果常数的内容是一个物件, 你还是可以变更物件内的属性, 例如:

const a = {name: "John" };
a.name = "Mary" ;
console. log (a);

执行结果如下:

Object { name : "Mary" }

由于变更的是物件的内容, 而不是变更常数 a , 所以可以成功执行。同样的道理, 如果常数的内容是一个阵列, 也可以变更阵列内的项目:

const a = [ 1 , 2 , 3 ];
a.push( 4 );
a[ 0 ] = 10 ;
console. log (a);

执行结果如下:

Array ( 4 ) [ 10, 2, 3, 4 ]

除了不能重新设值外, const 跟 let 是一样的。

没有宣告直接设定变数

JavaScript 是很宽松的, 你甚至会看到有些程式中根本 没有宣告就直接设定变数的值 , 像是这样:

a = 23;
console.log(a);

执行时并不会引发错误, 而且可以正确印出 a 的值:

你甚至还可以随意在函式或是区块直接用同样的方式运作:

function  foo ()  {
  a = 23
}

{
  b = 24
}

foo () 
console . log (a) ; 
console.log(b);

执行结果如下:

你会看到 a 和 b 虽然是在函式以及区块内设定, 可是不像是 var 或是 let 有范围的限制, 两个都变成全域变数那样可以在任何地方使用。

未宣告的变数其实是全域物件的属性

之所以会有前述范例的结果, 是因为当JavaScript 看到识别字时, 会一层层的往外找寻是否有符合该名称的宣告, 例如:

let a = 23

function  foo () {
   console .log(a)
  b = 24 ;
  {
    c = 25 ;
    {
      console .log(b)
    }
  }
}

foo();

执行结果如下:

在 foo 函式中因为没有宣告 a , 所以会往上一层找到全域变数 a ;而最内层区块列印的 b 也是往上一层区块找到的 b 。

如果在全域变数里也找不到, 就会往 全域物件 globalThis 找它的属性, 这也是为什么你可以直接以 alert 叫用定义在 globalThis 物件内的 alert :

alert ( 'call globalThis.alert' );
 globalThis .alert ( 'property of globalThis' );

以上两种写法其实是一样的, 当JavaScript 看到 alert 时, 会发现程式中并没有定义 alert 函式, 因此会往全域物件 globalThis 寻找, 发现全域物件有 alert 属性, 因此变成叫用 globalThis.alert 。

在设定值的时候也是一样, 对于没有宣告过的识别字, JavaScript 会将之当成是要设定全域物件的属性, 例如:

a = 23 ;
globalThis.b = 24 ;
console. log (globalThis.a);
console. log (b);

执行结果如下:

第一行要设定 a 时, 会发现程式中没有宣告过 a , 因此实际上执行的是 globalThis.a = 23 , 你可以在第三行看到透过 globalThis.a 取用的就是同一份资料。相同的道理, 第二行虽然是设定 globalThis 的 b 属性, 但是在第四行却可以像是使用变数一样直接以 b 来取得属性值。

你可以在任何地方用这种方式帮全域物件增加属性, 并且以像是全域变数的方式使用该属性。也就是说, 若不使用 var 、 let 、 const 宣告而直接设定值, 并不会建立变数, 而是 设定全域物件 globalThis 的属性 。这样的作法看起来好像很方便, 随时想用就用, 但是却容易造成混淆, 搞不清楚到底是在哪里设定初值, 若要避免这个问题, 可以强制使用 严格模式 , 例如:

'use strict' 
a = 23 ;
 console .log(a);

执行时就会引发错误:

Uncaught ReferenceError: assignment to undeclared variable a

它会认为你是设值给一个未宣告的变数。

var 全域变数与纯全域物件属性的差异

你可能会想到, 前面不是有提到以 var 宣告的全域变数也会变成全域物件的属性, 这样和刚刚提到单纯全域物件的属性不是一样吗?还记得前面说明过, 以 var 宣告的全域变数会成为全域物件中不可设定的属性, 具体的表现就是你无法用 delete 移除它, 但若是纯全域物件的属性, 就可以用 delete 移除, 例如:

globalThis. a = 23 
console. log ( a )
 delete  a 
console. log ( a )

执行结果如下:

23 
Uncaught ReferenceError: a is not  defined

第二次要列印 a 时, 就会因为第三行已经将 a 移除变成未定义的识别字而引发错误。

这样的差异很合理, 因为以 var 宣告的全域变数是真的全域变数, 如果可以删除, 就不再是可以在程式中任何地方取用的全域变数了, 但是全域物件本来就是一个JavaScript 物件, 自然可以随意增删属性。

以上我们就把宣告变数的几种方法介绍完了, 希望有助于厘清为什么这里可以使用这个变数、或者是为什么这个变数变成没有定义的疑惑。简单来说, 为了避免意外, 建议在程式中都只以 let 、 const 宣告, 不要使用 var , 也不要随意帮全域物件新增属性。

链接: https://www.fly63.com/article/detial/11729


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK