let 和 var 学习笔记

82 阅读3分钟

一、对比

let、constvar备注备注2
ES6有(带来了块作用域和暂时性死区)没有
变量提升没有,不声明使用会报ReferenceError“变量提升”现象即变量可以在声明之前使用,值为undefined。
for循环每个循环都是独立子作用域(因为有块作用域)for(var i)i在for外面可以引用,值会变当前的i只在本轮循环有效,所以每一次循环的i其实都是一个新的变量。如果每一轮循环的变量i都是重新声明的,那它怎么知道上一轮循环的值,从而计算出本轮循环的值?这是因为 JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。
暂时性死区let和const有暂时性死区,强行绑定代码块,形成封闭作用域,不受外部影响,即不能使用外部声明的值,在声明之前使用,就会报Reference Error本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。死区例子:function bar(x = y, y = 2) y未定义,先使用 let x = x; x未定义
typeof百分百安全,永远不会报错typeof x; // ReferenceErrorlet x;变量x使用let命令声明,在声明之前,都属于x的“死区”,只要在死区使用到该变量就会报错。因此,typeof运行时就会抛出一个ReferenceError。作为比较,如果一个变量根本没有被声明,使用typeof反而不会报错。typeof y; // "undefined"
重复声明同一个变量let不允许在相同作用域(最小到块级)内,重复声明同一个变量function func(arg) { { let arg; }}func() // 不报错,不是同一个块级作用域
块级作用域必须有大括号,如果没有大括号,JavaScript 引擎就认为不存在块级作用域。为什么需要块级作用域?内层可能会覆盖外层(提升)、用来计数的循环变量泄露为全局变量var s = 'hello';for (var i = 0; i < s.length; i++) { console.log(s[i]);}console.log(i); // 5循环泄露function f1() { var n = 5; if (true) {//提升到{}平级,var n不会再声明,但是会赋值 n=10 var n = 10; } console.log(n); // 10}

二、ES6 && let

  1. ECMAScript 和 JavaScript 的关系是,前者是规范是后者的规格,后者是实现。
  2. Babel 是一个广泛使用的 ES6 转码器,将 ES6 代码转为 ES5 代码。
  3. ES5 只有两种声明变量的方法:var命令和function命令。ES6 除了添加let和const命令,还有两种声明变量的方法:import命令和class命令。所以,ES6 一共有 6 种声明变量的方法。
  4. 顶层对象,在浏览器环境指的是window对象,在 Node 指的是global对象。ES5 之中,顶层对象的属性与全局变量是等价的。
  5. ES6 为了改变这一点,一方面规定,为了保持兼容性,var命令和function命令声明的全局变量,依旧是顶层对象的属性;let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。从 ES6 开始,全局变量将逐步与顶层对象的属性脱钩。
  6. for循环有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。
  7. typeof undeclared_variable // "undefined" 但是死区时typeof运行时就会抛出一个ReferenceError。
  8. ES5 只有全局作用域和函数作用域,没有块级作用域,ES6块级作用域属于函数作用域,所以ES6作用域还是分两类
  9. ES6 允许块级作用域的任意嵌套。可以嵌套多层,每个块都是一个单独的作用域
  10. 匿名立即执行函数表达式(匿名 IIFE)块级作用域的出现,实际上使得获得广泛应用的匿名立即执行函数表达式(匿名 IIFE)不再必要了。
  11. ES6 明确规定,如果区块中存在let和const命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。---好好理解一下
  12. 在代码块(块级作用域)内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。
  13. ES6 规定暂时性死区和let、const语句不出现变量提升,主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。
  14. ES5 规定,函数只能在顶层作用域和函数作用域之中声明,不能在块级作用域声明。但是,浏览器没有遵守这个规定,为了兼容以前的旧代码,还是支持在块级作用域之中声明函数,因此都能运行,不会报错。
  15. ES6 的块级作用域必须有大括号,如果没有大括号,JavaScript 引擎就认为不存在块级作用域。
  16. let只能出现在当前作用域的顶层
  17. 函数声明在严格模式下,函数只能声明在当前作用域的顶层。
// 第一种写法,报错
if (true) let x = 1;

// 第二种写法,不报错
if (true) {
  let x = 1;
}

第一种写法没有大括号,所以不存在块级作用域,而let只能出现在当前作用域的顶层,所以报错。 第二种写法有大括号,所以块级作用域成立。

// 不报错
'use strict';
if (true) {
  function f() {}
}

// 报错
'use strict';
if (true)
  function f() {}
  1. ES6 引入了块级作用域,明确允许在块级作用域之中声明函数。ES6 规定,块级作用域之中,函数声明语句的行为类似于let,在块级作用域之外不可引用。
function f() { console.log('I am outside!'); }

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

  f();
}());

上面代码在 ES5 中运行,会得到“I am inside!”,因为在if内声明的函数f会被提升到函数头部,

ES6 就完全不一样了,理论上会得到“I am outside!”。  
但是实际会报错// Uncaught TypeError: f is not a function
  1. 浏览器的 ES6 环境中,块级作用域内声明的函数,行为类似于var声明的变量。非ES6浏览器,还是类似let;
如果改变了块级作用域内声明的函数的处理规则,显然会对老代码产生很大影响。
为了减轻因此产生的不兼容问题,ES6 在附录 B里面规定,浏览器的实现可以不遵守上面的规定,有自己的行为方式。
ES6浏览器允许在块级作用域内声明函数。但是函数声明类似于var,即会提升到全局作用域或函数作用域的头部。
同时,函数声明还会提升到所在的块级作用域的头部。
注意,上面三条规则只对 ES6 的浏览器实现有效,其他环境的实现不用遵守,还是将块级作用域的函数声明当作let处理。
根据这三条规则,浏览器的 ES6 环境中,块级作用域内声明的函数,行为类似于var声明的变量。上面的例子实际运行的代码如下。
// 浏览器的 ES6 环境
function f() { console.log('I am outside!'); }
(function () {
  var f = undefined;
  if (false) {
    function f() { console.log('I am inside!'); }
  }

  f();
}());
// Uncaught TypeError: f is not a function
  1. 考虑到环境导致的行为差异太大,应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句。
// 块级作用域内部的函数声明语句,建议不要使用
{
  let a = 'secret';
  function f() {
    return a;
  }
}

// 块级作用域内部,优先使用函数表达式
{
  let a = 'secret';
  let f = function () {
    return a;
  };
}

三、Const

  1. const声明一个只读的常量。一旦声明,常量的值就不能改变。这意味着,const一旦声明变量,就必须立即初始化,不能留到以后赋值。
const foo;
// SyntaxError: Missing initializer in const declaration

其他特性同let

  1. const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。
  2. 对象和数组本身都是可写的
const foo = {};

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

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

常量foo储存的是一个地址,这个地址指向一个对象。
不可变的只是这个地址,即不能把foo指向另一个地址,
但对象本身是可变的,所以依然可以为其添加新属性。

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

上面代码中,常量a是一个数组,
这个数组本身是可写的,但是如果将另一个数组赋值给a,就会报错。
  1. 想将对象冻结,应该使用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] );
    }
  });
};

四、易错题

var tmp = 123;

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

上面代码中,存在全局变量tmp,但是块级作用域内let又声明了一个局部变量tmp,
导致后者绑定这个块级作用域,所以在let声明变量前,对tmp赋值会报错。
if (true) {
  // TDZ开始
  tmp = 'abc'; // ReferenceError
  console.log(tmp); // ReferenceError

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

  tmp = 123;
  console.log(tmp); // 123
}
var tmp = new Date();

function f() {
    //这个括号里面提升
    //只提升声明,不提升赋值,所以变成了undefined,因为是在函数作用域里面,不会用外面全局作用域里面的tmp
  console.log(tmp);
  if (false) {
    var tmp = 'hello world';
  }
}

f(); // undefined

var s = 'hello';

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

console.log(i); // 5


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

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

相关资料 es6.ruanyifeng.com/#docs/let