jtwang7 / JavaScript-Note

JavaScript学习笔记
10 stars 2 forks source link

ES6 - let / const #41

Open jtwang7 opened 3 years ago

jtwang7 commented 3 years ago

参考文章:

let 命令

作用

ES6 新增了 let 命令,用来声明变量。它的用法类似于 var,但是所声明的变量,只在 let 命令所在的代码块内有效。 以 for 循环为例:

// var 声明
var a = [];
for (var i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 10

// let 声明
var a = [];
for (let i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 6

当循环变量 i 通过 var 声明时,其在全局范围内都有效,所以全局只有一个变量 i。每一次循环,变量 i 的值都会发生改变,而循环内被赋给数组 a 的函数内部的 console.log(i),里面的 i 指向的就是全局的 i。也就是说,所有数组 a 的成员里面的 i,指向的都是同一个 i,导致运行时输出的是最后一轮的i的值,也就是 10。 当循环变量 i 通过 let 声明时,当前的 i 只在本轮循环有效,所以每一次循环的 i 其实都是一个新的变量,所以最后输出的是 6。

如果每一轮循环的变量i都是重新声明的,那它怎么知道上一轮循环的值,从而计算出本轮循环的值? 这是因为 JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量 i 时,就在上一轮循环的基础上进行计算。

此外,在 for 循环中,设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。

for (let i = 0; i < 3; i++) {
  let i = 'abc';
  console.log(i);
}
// abc
// abc
// abc

上述代码成功运行,说明函数内部的变量 i 与循环变量 i 不在同一个作用域,有各自单独的作用域,因为同一个作用域不可使用 let 重复声明同一个变量。

特点

不存在变量提升

var 命令会发生“变量提升”现象。变量提升实际指的是变量声明提升,var 声明的变量会被提升到全局或函数作用域的顶部,但变量的初始化赋值不会被提升,而是要等待到相应的赋值语句执行,这就导致变量可以在声明之前使用,但此时值为 undefined。

// var 的情况
console.log(foo); // 输出undefined
var foo = 2;

等价于

var foo;
console.log(foo);
foo = 2;

上面代码中,变量 foo 用 var 命令声明,会发生变量提升,即脚本开始运行时,变量 foo 已经在开头被声明了,但是没有初始化赋值,所以会输出 undefined。

// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;

变量 bar 用 let 命令声明,不会发生变量提升。这表示在声明它之前,变量 bar 是不存在的,这时如果用到它,就会抛出一个错误。

暂时性死区

let 声明的变量会绑定当前变量所在的块级作用域,不受外部影响。

var tmp = 123;

if (true) {
  tmp = 'abc'; // ReferenceError
  let tmp;
}

上面代码中,块级作用域内let声明了一个局部变量 tmp,导致后者绑定这个块级作用域。因此,块级作用域中 tmp 变量指向 let 声明而非 var 的全局声明,由于 let 不存在变量提升,所以在 let 声明变量前,对 tmp 赋值会报错。

ES6 明确规定,如果区块中存在let和const命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。总之,在代码块内,使用 let 命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。 暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经确定,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。

隐蔽的暂时性死区

function add (x = y, y = 2) {
  return [x, y];
}
bar(); // 报错

函数声明中,函数条件与函数体处于同级作用域,条件块在函数体之前,并且函数形参会在函数调用时自动声明。因此上述代码等同于:

{
  let x = y; // 报错
  let y = 2;
  return [x, y];
}

可以看到,在声明形参的时候,let y = 2 之前的区域属于 y 的暂时性死区,因此 let x = y 使用 y 变量时报错。

不允许重复声明

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

因此在函数声明中,不能在函数体内重复声明形参。(函数体与条件块处于同级作用域)


// 报错:var a 声明了变量并发生变量提升,因此 a 变量实际在 let 声明前已存在
function func() {
let a = 10;
var a = 1;
}

// 报错 function func() { let a = 10; let a = 1; }

function func(arg) { let arg; } func() // 报错:函数体内重复声明了形参变量

function func(arg) { { let arg; } } func() // 不报错:函数体内嵌套了块级作用域,不与形参处于同级作用域

jtwang7 commented 3 years ago

参考文章:

const 命令

作用

const 声明一个只读的常量。一旦声明,常量的值就不能改变。因此,const 一旦声明变量,就必须立即初始化,不能留到以后赋值。

const PI = 3.1415;
PI = 3; // TypeError: Assignment to constant variable. - const 声明的常量不可修改

const foo; // SyntaxError: Missing initializer in const declaration - const 声明的同时必须立即初始化

特点

const 特点同 let 一样:

本质

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

const foo = {};

// 为 foo 添加一个属性,可以成功
foo.prop = 123;
foo.prop // 123

// 将 foo 指向另一个对象,就会报错 - const 保证了引用类型的指针地址不变
foo = {}; // TypeError: "foo" is read-only

如果真的想将对象冻结,应该使用 Object.freeze 方法,冻结的对象不能添加新的属性。

const foo = Object.freeze({});

// 常规模式时,下面一行不起作用;
// 严格模式时,该行会报错
foo.prop = 123;

除了将对象本身冻结,对象的属性也应该冻结。下面是一个将对象彻底冻结的函数。

var constantize = (obj) => {
  Object.freeze(obj);
  Object.keys(obj).forEach( (key, i) => {
    if ( typeof obj[key] === 'object' ) {
      constantize( obj[key] );
    }
  });
};