小记1 - let 与 const

背景:var声明与变量提升

众所周知,ES5 只有全局作用域和函数作用域,没有块级作用域,这带来很多不合理场景。
用 var 关键字声明变量,会出现所谓的变量提升。
即无论其实际声明位置在何处,都会被视为声明于所在函数的顶部(如果声明不在任意函数内,则视为在全局作用域的顶部)。

示例说明变量提升:
function getValue(condition) {
if (condition) {
var value = "blue";
// 其他代码
return value;
} else {
// value 在此处可访问,值为 undefined
return null;
}
// value 在此处可访问,值为 undefined
}

如果不太熟悉 JS ,或许会认为 仅当 condition 的值为 true 时,变量 value 才会被创建。但实际, value 无论如何都会被创建。 JS 引擎在后台对 getValue 函数调整成了下面这样:

function getValue(condition) {
var value;
if (condition) {
value = "blue";
// 其他代码
return value;
} else {
return null;
}
}

也就是说:value 变量的声明被提升到了顶部,而初始化工作则保留在原处。这意味着:在 else 分支内value 变量也是可访问的,此处它的值会是 undefined ,因为它并没有被初始化。

为什么需要块级作用域?--先看两种不合理场景

第一种场景,内层变量覆盖外层变量。

var tmp = new Date();
console.log(tmp );  //  Wed Dec 11 2019 10:39:11 GMT+0800 (中国标准时间)
function f() {
  console.log(tmp);
  if (false) {
    var tmp = 'hello world';
  }
}
f(); // undefined,原因在于变量提升,导致内层的tmp变量覆盖了外层的tmp变量。

第二种场景,用来计数的循环变量泄露为全局变量。

var s = 'china';
for (var i = 0; i < s.length; i++) {
  console.log(s[i]);
  console.log(i);
}
console.log(i); // 5  
//由于变量提升,变量i只用来控制循环,但循环结束,它并没消失,泄露成了全局变量。

因此,ES5求我们习惯var声明变量提升,如不理解,就可能导致bug 。
也因此, ES6 引入了块级作用域,让变量的生命周期更加可控。

块级作用域和块级声明

块级声明也就是让所声明的变量在指定块的作用域外无法被访问。
块级作用域(又称词法作用域)在如下情况被创建:
1、在一个函数内部
2、 在一个代码块(由一对花括号包裹)内部
块级作用域是很多类 C 语言的工作机制, ES6 引入块级声明,给 JS 添加灵活性也与其他语言保持了一致性。

一、let 声明变量

基本用法

let 命令,用来声明变量。用法与 var 类似,基本可用 let 代替 var 声明变量。

  1. let所声明的变量,只在let命令所在的代码块内有效
  2. 不存在变量提升
  3. 不允许(禁止)重复声明
  4. 暂时性死区(TDZ)

即 let将变量的作用域限制在当前代码块中, let 声明不会被提升到当前代码块顶部,因此需手动将 let 声明放置到顶部,以便让变量在整个代码块内可用。

// var 的情况-var命令会发生“变量提升”现象,即变量可以在声明之前使用,值为undefined。
console.log(foo); // 输出undefined
var foo = 2;

// let 的情况-为纠正var现象,let改变了语法行为,let声明变量一定要声明后使用,否则报错
console.log(bar); // 报错ReferenceError
let bar = 2;
//--let不允许在相同作用域内,重复声明同一个变量。
function func() {
  let a = 10;
  var a = 1;  // 报错
}
function func() {
  let a = 10;
  let a = 1; // 报错
}

//--因此,不能在函数内部重新声明参数。
function func(arg) {
  let arg;  // 报错
}

function func(arg) {
  {
    let arg; // 不报错
  }
}
func() 
暂时性死区

ES6 明确规定,如果区块中存在let和const命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。
在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。

if (true) {
  // TDZ开始
  tmp = 'abc'; // ReferenceError
  console.log(tmp); // ReferenceError

  let tmp; // TDZ结束
  console.log(tmp); // undefined

  tmp = 123;
  console.log(tmp); // 123
}

“暂时性死区”也意味着typeof不再是一个百分之百安全的操作。

if (condition) {
console.log(typeof value); // 引用错误
let value = "blue";
}

然而,在变量被定义的代码块之外可以对该变量使用 typeof。

console.log(typeof value); // "undefined"
if (condition) {
let value = "blue";
}
/*当 typeof 运算符被使用时,value 并没在暂时性死区内,而在定义 value 变量的代码块外。
这意味着此时并没有绑定 value 变量,而 typeof 仅单纯返回了"undefined" 。
*/

有些“死区”比较隐蔽,不太容易发现。

function bar(x = y, y = 2) {
  return [x, y];
}
bar(); // 报错--因为参数x默认值等于另一个参数y,而此时y还没有声明,属于“死区”。

//--如果y的默认值是x,就不会报错,因为此时x已经声明了。
function bar(x = 2, y = x) {
  return [x, y];
}
bar(); // [2, 2]

var x = x;  // 不报错
let x = x; // ReferenceError: x is not defined  // 报错--与var的行为不同。
//因为暂时性死区。使用let声明变量时,只要变量在还没有声明完成前使用,就会报错。

暂时性死区只是块级绑定的一个独特表现,而另一个独特表现则是在循环时使用它。

二、const 声明常量

基本用法

const 命令,声明的变量会被认为是常量。一旦声明,常量的值就不能改变。
意味着,const一旦声明变量,就必须立即初始化赋值,不能留到以后赋值。对于const来说,只声明不赋值,就会报错。

const与let相似之处:
  1. const所声明的变量,只在const声明所在的块级作用域内有效
  2. 不存在变量提升
  3. 不允许(禁止)重复声明
  4. 暂时性死区(TDZ)
const与let重大区别:

试图对用const 声明的常量进行重新赋值会抛出错误,无论是在严格模式还是非严格模式下:

const maxItems = 5;
maxItems = 6; // 抛出错误
const 本质

const 保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。
对于简单数据类型(number、string、boolean),值就保存在变量指向的那个内存地址,因此等同于常量。
但对于复杂数据类型的(主要是对象和数组),变量指向的内存地址(栈内存),保存的只是一个指向实际数据(堆内存)的指针。const只能保证(栈内存中)这个指针是固定的(即总是指向堆内存的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。因此,如果const声明的常量是一个对象,它所包含的值是可以被修改的。

可以简单理解为:
const 声明阻止的是 变量绑定与变量自身值的修改,而不阻止对变量成员的修改。

const person = {
name: "Nicholas"
};
// 工作正常 --修改的是变量成员
person.name = "Greg";

// 抛出错误 --修改的是变量自身值,所以报错
person = {
name: "Greg"
};

三、块级绑定新的最佳实践

在 ES6 的发展阶段,被广泛认可的变量声明方式是:默认情况下应当使用 let 而不是 var。对于多数 JS 开发者来说, let 的行为方式正是 var 本应有的方式,因此直接用 let 替代 var 更符合逻辑。在这种情况下,你应当对需要受到保护的变量使用 const 。

一种替代方案更为流行,那就是在默认情况下使用 const 、并且只在知道变量值需要被更改的情况下才使用 let 。
其理论依据是大部分变量在初始化之后都不应当被修改,因为预期外的改动是 bug 的源头之一。这种理念有着足够强大的吸引力,是值得在代码中照此进行探索实践的。

小结

  1. let 与 const所声明的变量,只在它们声明所在的块级作用域内有效
  2. let 与 const 块级声明都不会进行变量提升
  3. let 与 const 块级声明都禁止重复声明
  4. let 与 const 都存在暂时性死区:不能在变量声明位置之前访问。
    即便使用的是 typeof 这样的安全运算符。由于块级绑定存在暂时性死区( TDZ ),试图在声明位置之前访问它就会导致错误。-副作用
  5. 另 const声明的变量必须初始化,且不得对变量重新赋值,否则报错。
块级绑定当前的最佳实践

默认情况下使用 const ,在你知道变量值需要被更改的情况下才使用 let 。
这在代码中能确保基本层次的不可变性,有助于防止某些类型的错误。

ES6 声明变量的六种方法

ES5 只有两种声明变量的方法:var命令和function命令。
ES6 添加 let和const命令 及另两种声明变量的方法:import和class命令。
所以,ES6 一共有 6 种声明变量的方法。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容