let,var,const那些事

169 阅读6分钟
ES5 只有全局作用域和函数作用域,没有块级作用域,这带来很多不合理的场景。

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

var tmp = new Date();

function f() {
  console.log(tmp);
  if (false) {
    var tmp = 'hello world';
  }
}

f(); // undefined 

上面这段代码中,console.log(tmp)var tmp = 'hello world 都处于同一个作用域,即函数f创造的作用域。由于发生了变量提升,导致执行结果为undefined

第二种场景,用来计数的循环变量泄露为全局变量。这种场景即下面for循环的例子。

ES6 的块级作用域

let实际上为 JavaScript 新增了块级作用域。

function f1() {
  let n = 5;
  if (true) {
    let n = 10;
  }
  console.log(n); // 5
}

上面的函数有两个代码块,都声明了变量n,运行后输出 5。这表示外层代码块不受内层代码块的影响。如果两次都使用var定义变量n,最后输出的值才是 10。


————————————    下面就来看看let是怎么使用的吧!    ————————————

let 基本用法:用来声明变量,但是声明的变量只能在块级作用域中使用。

🌰  栗子:

{
  let a = 10;
  var b = 1;
}

a // ReferenceError: a is not defined.
b // 1

上面的代码中,声明a,b两个变量。但let声明的变量报错,var声明的变量返回了正确结果,说明let声明的变量只能在块级作用域中使用。

最常见到的场景:for循环的计数器就很适合用let声明

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

上面的代码中i是由var声明的,在全局都有效,因此全局中只有一个i变量。每次循环都会改变全局i的值。因此,循环结束之后,全局的i值变成10 。这时,无论执行数组哪一项所对应的函数,取到的都是全局的i值。

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

使用let声明计数器i后,每次循环中的i都具有块级作用域,即本次循环。因此,当循环结束之后,数组a每一项对应的i都是不同的,所以可以打印出期望的结果。

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

另外for循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。

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

上面代码正确运行,输出了 3 次abc。这表明函数内部的变量i与循环变量i不在同一个作用域,有各自单独的作用域。但是在日常书写中,不建议使用这样容易引发冲突的命名方式,命名要有唯一性且尽量具有语意。

———————————————    这是一条美丽的分割线    ———————————————

let不存在变量提升

var声明的变量会产生”变量提升“,即变量可以在声明前使用,值为undefined,可以想象成在使用未定义的变量时,变量声明语句“自动提升至”代码最上方,由于未真正声明赋值,所以值为undefined

为了纠正这种看起来有些奇怪的现象,let命令改变了语法行为,它所声明的变量一定要在声明后使用,否则报错。

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

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

———————————————     这是一条美丽的分割线   ———————————————

暂时性死区 (先定义再使用)

只要块级作用域出现了let声明,那么这个他所声明的变量就绑定了这个区域,不再受外部变量影响。

var tmp = 123;

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

在上面的代码中,存在全局tmp变量声明,但是又因为let声明了块级作用域,导致提前赋值报错。

ES6 明确规定,如果区块中存在letconst命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。 

总之,在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。

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

 typeof x; // ReferenceError
 let x; 

上面代码中,变量x使用let命令声明,所以在声明之前,都属于x的“死区”,只要用到该变量就会报错。因此,typeof运行时就会抛出一个ReferenceError。 

作为比较,如果一个变量根本没有被声明,使用typeof反而不会报错。

 typeof undeclared_variable // "undefined" 

上面代码中,undeclared_variable是一个不存在的变量名,结果返回“undefined”。

所以希望大家养成良好的编程习惯,任何变量先定义再使用。

———————————————    这是一条美丽的分割线    ———————————————

不允许重复声明

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() // 不报错 

———————————————    这是一条美丽的分割线    ———————————————

除了let外,es6新增了另一个命名关键字:const

const基本用法:声明一个不可以改变的常量。

const foo;
// SyntaxError: Missing initializer in const declaration

由于const不可以重复声明,因此需要声明的同时赋值。

if (true) {
  console.log(MAX); // ReferenceError
  const MAX = 5;
}

constlet一样也存在暂时性死区,要求先声明再使用。

if (true) {
  const MAX = 5;
}

MAX // Uncaught ReferenceError: MAX is not defined

const一样也存在块级作用域,只在当前声明的作用域内有效。

var message = "Hello!";
let age = 25;

// 以下两行都会报错
const message = "Goodbye!";
const age = 30;

const声明的变量,也与let一样不可重复声明。

🌟  ❓  那么,是不是“所有”由const声明的变量都是不可变的呢?

上个 🌰  栗子:

const foo = {};

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

// 将 foo 指向另一个对象,就会报错
foo = {}; // TypeError: "foo" is read-only

再来一个:

const a = [];
a.push('Hello'); // 可执行
a.length = 0;    // 可执行
a = ['Dave'];    // 

从上面两个例子中看到,分别对由const声明的对象和数组进行操作时,都是没有报错的,这是为什么呢?

const本质

const只能保证创建时变量指向的内存地址不变。对于简单数据类型,内存地址存放的就是这个值,所以不能改变。而对于复杂数据类型,内存地址存放的是一个指针,所以改变对象或数组的数据结构并不报错。