let

let具有块级作用域,用法类似于var但是又不同于它。

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

a // 10
b // 无法访问到b,因为b只在代码块内有效
// for循环很适合
for (let i = 0; i < 10; i++) {
  // ...
}

console.log(i);
// ReferenceError: i is not defined
// 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

另外、for循环有一个特殊之处,在设置变量的那部分是一个父作用域,在循环体内又是一个单独的作用域。

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

这表明函数内部的变量i与循环变量i不在同一个作用域,有各自单独的作用域。

不存在变量提升

var命令会发生”变量提升“现象,即变量可以在声明之前使用,值为undefined。

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

// let 的情况
console.log(bar); // ReferenceError: bar is not defined
let bar = 2;

暂时性死区

var tmp = 123;
if(true) {
    tmp = '111',
    let tmp;
}
// 会报错的
// 因为在块级作用域中,使用了let,那么就不能再let之前使用这个变量了,而这里tmp = '111'。已经违反了规则。

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 x; // ReferenceError
let x;

// 这里为什么报错呢,其实还是暂时性死区的原因,变量x使用let命令声明,所以在声明之前,都属于x的“死区”
// 作为比较
typeof undeclared_variable // "undefined"
// 
function bar(x = y, y = 2) {
  return [x, y];
}

bar(); // 报错 ,是因为将y赋值给x,但是x并未申明呢
function bar(x = 2, y = x) {
  return [x, y];
}
bar(); // [2, 2] 因为x已经是默认值了,y 赋值 x
let x = x; // 报错, 在变量x的声明语句还没有执行完成前,就去取x的值,导致报错”x 未定义“。

var x = x; // 不报错

ES6 规定暂时性死区和let、const语句不出现变量提升,主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。帮助开发者养成好多的习惯

不能次重复申明

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; // 不报错
    }
}

块级作用域

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

// var存在的一些弊端

// 【1】 内层变量可能会覆盖外层变量
var tmp = new Date();

function f() {
  console.log(tmp);          // 所以这里会输出undefined
  if (false) {
    var tmp = 'hello world'; // 这里发生了变量提升
  }
}

f(); // undefined
// if代码块的外部使用外层的tmp变量,内部使用内层的tmp变量。但是,函数f执行后,输出结果为undefined,原因在于变量提升,导致内层的tmp变量覆盖了外层的tmp变量。

// 【2】 用来计数的循环变量泄露为全局变量。

var s = 'hello';

for (var i = 0; i < s.length; i++) {
  console.log(s[i]);
}

console.log(i); // 5 这里就是全局的了

相信大家看了以上两个例子,应该知道为什么块级作用域好了吧。

var变量提升

JavaScript引擎的工作方式是,先解析代码,获取所有被声明的变量,然后再一行一行地运行。这造成的结果,就是所有的变量的声明语句,都会被提升到代码的头部,这就叫做变量提升.

// 在学习块级作用域之前我们先来回顾一下var吧
console.log(a);
var a =1;

// 以上语句并不会报错,只是提示undefined。实际运行过程:
var a;
console.log(a);
a =1;

块级作用域

// 先来一个直观的例子

function f1() {
  let n = 5;
  if (true) {
    let n = 10;
  }
  console.log(n); // 5
}
// 外层作用域无法读取内层作用域的变量。

块级作用域中允许声明函数

// 
function f() { console.log('I am outside!'); }

(function () {
  if (false) {
    // 重复声明一次函数f
    function f() { console.log('I am inside!'); }
  }

  f();
}());

// 上诉代码在ES5中会输出I am inside!, 因为if语句中所定义的函数发生了变量提升

// 在ES6中执行道理上是输出I am outside!,因为无法访问块级作用域中的函数,实际上则会报错 f is not a function
// 在ES6下实际运行的是
function f() { console.log('I am outside!'); }
(function () {
  var f = undefined;  // 在浏览器的 ES6 环境中,块级作用域内声明的函数,行为类似于var声明的变量。发生了变量提升
  if (false) {
    function f() { console.log('I am inside!'); }
  }

  f();
}());
// Uncaught TypeError: f is not a function

考虑到环境导致的行为差异太大,应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句。

const命令

基本语法

// 
const PI = 3.1415;
PI // 3.1415

PI = 3;
// 报错

// 也就是const 声明里变量就必须立即初始化,不然之后就无法赋值了

const的作用域与let命令相同:只在声明所在的块级作用域内有效。

if (true) {
  const MAX = 5;
}

MAX // Uncaught ReferenceError: MAX is not defined
//  const命令声明的常量也是不提升,同样存在暂时性死区,只能在声明的位置后面使用。
if (true) {
  console.log(MAX); // 报错
  const MAX = 5;
}
// const声明的常量,也与let一样不可重复声明。
var message = "Hello!";
let age = 25;

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

const本质

const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指针,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'];    // 报错

现在我们已经学了四种变量声明的方法了, var function let const


顶层对象属性

顶层对象,在浏览器环境指的是window对象,在 Node 指的是global对象。 ES5 之中,顶层对象的属性与全局变量是等价的。

window.a = 1;
a // 1

a = 2;
window.a // 2

ES6 一方面规定,var命令和function命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。

未完待续

ES6 入门


Meils
1.6k 声望157 粉丝

前端开发实践者