JS深入:var、let、const

# JS深入:var、let、const

这里对js最基本、常用的var、let、const进行简单地梳理~

# 简介

ES6之前我们都是通过var关键字定义JavaScript变量。ES6才新增了letconst关键字。

# var

var声明语句声明一个变量,并可选地将其初始化为一个值。详见MDN (opens new window)

ECMAScript的变量是松散类型的,所谓松散类型就是可以用来保存任何类型的数据。换句话说,每个变量仅仅是一个用于保存值的占位符而已。定义变量时要使用var操作符(注意var是一个关键字),后跟变量名(即一个标识符),如下所示:

// 这行代码定义了一个名为 message 的变量,该变量可以用来保存任何值(像这样未经过初始化的 变量,会保存一个特殊的值::undefined
var a;

// 1.在全局作用域下使用 var 声明一个变量,默认它是挂载在顶层对象 window 对象下(Node 是 global)
var b = 1;
console.log(b,window.b); // 1 1


// 2.用 var 声明的变量的作用域是它当前的执行上下文,可以是函数也可以是全局
var x = 1 // 声明在全局作用域下
function foo() {
    var x = 2 // 声明在 foo 函数作用域下
    console.log(x) // 2
}
foo()
console.log(x)  // 1


// 3.如果赋值给未声明的变量,该变量会被隐式地创建为全局变量(它将成为顶层对象的属性)
function foo(){
    b = 3
}
foo()
console.log(window.b) // 3

变量声明,无论发生在何处,都在执行任何代码之前进行处理。用var声明的变量的作用域是它当前的执行上下文,它可以是嵌套的函数,或者对于声明在任何函数外的变量来说是全局。

# 变量提升(hoisted)

由于变量声明(以及其他声明)总是在代码执行之前处理的,所以在代码中的任意位置声明变量总是等效于在代码开头声明。这意味着变量可以在声明之前使用,这个行为叫做“hoisting”。“hoisting”就像是把所有的变量声明移动到函数或者全局代码的开头位置。

关于为什么会发生变量提升和函数提升,在我的另一篇博文js深入:从执行上下文到闭包中有更细致的分析,这里不再赘述~

console.log(b) // undefined
var b = 3

// 上面代码可以隐式的理解为:
var b
console.log(b) // undefined
b = 3

建议始终在作用域顶部声明变量(全局代码的顶部和函数代码的顶部),这可以清楚知道哪些变量在函数作用域内,哪些变量是在全局作用域内。

  • 看一个例子:
var x = y, y = 'A';
console.log(x + y); // undefinedA

在这里,x 和 y 在代码执行前就已经创建了,而赋值操作发生在创建之后。当"x = y"执行时,y 已经存在,所以不抛出ReferenceError,并且它的值是'undefined'。所以 x 被赋予 undefined 值。然后,y 被赋予'A'。于是,在执行完第一行之后,x === undefined && y === 'A' 才出现了这样的结果。

# 作用域规则

var只能在全局作用域和函数作用域内声明变量,多次声明同一个变量并不会报错。

// a. 里层的 for 循环会覆盖变量 i,因为所有 i 都引用相同的函数作用域内的变量;这些问题可能在代码审查时漏掉,引发无穷的麻烦。
function sumArr(arrList) {
    var sum = 0;
    for (var i = 0; i < arrList.length; i++) {
        var arr = arrList[i];
        for (var i = 0; i < arr.length; i++) {
            sum += arr[i];
        }
    }

    return sum;
}


// b. 用来计数的循环变量泄露为全局变量
var a = [];
for (var i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 10


// c. 内层变量可能会覆盖外层变量
var tmp = new Date();
function f() {
  console.log(tmp);
  if (false) {
    var tmp = 'hello world';
  }
}

f(); // undefiend

注意

  1. 所有未声明直接赋值的变量都会自动挂在顶层对象下,这样容易造成全局环境变量不可控、混乱
  2. 允许多次声明同一变量而不报错,造成代码不容易维护

# let

let 语句声明一个块级作用域的本地变量,并且可选的将其初始化为一个值。详见MDN (opens new window)

let x = 1;
if (true) {
  let x = 2;
  console.log(x);// 2
}
console.log(x); // 1

let允许你声明一个作用域被限制在块级中的变量、语句或者表达式。与var关键字不同的是, var声明的变量只能是全局或者整个函数块的。varlet的不同之处在于后者是在编译时才初始化。

# 块级作用域{}

JS中作用域有:全局作用域、函数作用域。没有块作用域的概念。ECMAScript 6(简称ES6)中新增了块级作用域。 块作用域由{ }包括,if语句和for语句里面的{}也属于块作用域。

ES6的块级作用域必须有大括号,如果没有大括号,JavaScript引擎就认为不存在块级作用域;块级作用域的出现,实际上使得获得广泛应用的匿名立即执行函数表达式(匿名 IIFE)不再必要了:

// IIFE 写法
(function () {
  var tmp = ...;
  ...
}());

// 块级作用域写法
{
  let tmp = ...;
  ...
}

# 作用域规则

let声明的变量只在其声明的块或子块中可用,这一点,与var相似。二者之间最主要的区别在于var声明的变量的作用域是整个封闭函数。

function varTest() {
  var x = 1;
  {
    var x = 2;  // 同样的变量!
    console.log(x);  // 2
  }
  console.log(x);  // 2
}

function letTest() {
  let x = 1;
  {
    let x = 2;  // 不同的变量
    console.log(x);  // 2
  }
  console.log(x);  // 1
}

# 暂时性死区(TDZ)

所谓暂时性死区(Temporal Dead Zone),指let声明的变量在被声明之前不能被访问。与通过var声明的有初始化值undefined的变量不同,通过let声明的变量直到它们的定义被执行时才初始化。在变量初始化前访问该变量会导致ReferenceError。该变量处在一个自块顶部到初始化处理的“暂存死区”中。

console.log(x) // Uncaught ReferenceError: x is not defined
let x = 1

为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。这样的错误在 ES5 是很常见的,现在有了这种规定,避免此类错误就很容易了。

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

与通过var声明的变量, 有初始化值 undefined和只是未声明的变量不同的是,如果使用typeof检测在暂存死区中的变量, 会抛出ReferenceError异常:

// results in a 'ReferenceError'
console.log(typeof i);
let i = 10;
  • 特点
  1. 使用let在全局作用域下声明的变量也不是顶层对象的属性
let b = 2
window.b // undefined

在全局作用域中,用 let 和 const 声明的全局变量没有在全局对象中,只是一个块级作用域(Script)中。

  1. 不允许同一块中重复声明
let x = 1
let x = 2 // Uncaught SyntaxError: Identifier 'x' has already been declared
  1. 关于let有没有变量提升可以参考这篇博文:我用了两个月的时间才理解 let (opens new window);里面讲的大致意思就是,变量主要有创建、初始化(声明)、赋值三个阶段,而let只是在创建阶段提升了,而初始化和赋值阶段没有提升:
    • let 的「创建」过程被提升了,但是初始化没有提升。
    • var 的「创建」和「初始化」都被提升了。
    • function 的「创建」「初始化」和「赋值」都被提升了。

# const

const声明一个只读的常量。一旦声明,常量的值就不能改变。const的作用域与let命令相同:只在声明所在的块级作用域内有效。

const a = 1
a = 2 // Uncaught TypeError: Assignment to constant variable.

const s // 声明未赋值
// Uncaught SyntaxError: Missing initializer in const declaration

const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。

# 对比

  1. var会污染全局对象,let/const不会
  2. var是可以重复申明
  3. var存在变量提升
  4. let、const可以形成块级作用域,var不会形成块级作用域
  5. let、const会形成一个暂时性死区,var不会
  6. 指针的变化:let、const都是ES6新增声明的变量的语法,区别是let创建的变量是可以更改指针指向,也就是可以重新赋值,但是const申明的变量是不允许改变指针指向的。

# 几个例子

var a = 111
{
    console.log(a,window.a)
    a = 222
    console.log(a,window.a)
    function a(){}
    console.log(a,window.a)
}

打印顺序为:

ƒ a(){} 111
222 111
222 222
{
    function a() {
        console.log(20);
    }
    var b = 1;

    window.a = a;
    window.b = b;

    a = 20;
    b = 2;
    
    window.a = a;
    window.b = b;

    a = 30;
    b = 3;
}

console.log(a); // 20
console.log(b); // 3

函数a是存在于Block作用域中,刚开始执行window.a = a时,window下的a被赋值为函数;之后又被赋值为20;所以最后打印20;而var没有块级作用域,全局的{}中声明的变量会自动挂载到window下面作为全局变量,所以最后打印3

# 参考

  1. es6入门-let 和 const 命令 (opens new window)
  2. 【译】终极指南:变量提升、作用域和闭包 (opens new window)
  3. var、let、const 有什么区别 (opens new window)
Back
上次更新: 1/24/2022, 5:38:42 PM
最近更新
01
taro开发实操笔记
09-29
02
前端跨端技术调研报告
07-28
03
Flutter学习笔记
07-15
更多文章>