关于ecmascript:let-和-const-命令

39次阅读

共计 9070 个字符,预计需要花费 23 分钟才能阅读完成。

let 和 const 命令
good luck

let 和 const 命令

let 命令

根本用法

ES6 新增了 let 命令,用来申明变量。它的用法相似于 var,然而所申明的变量,只在 let 命令所在的代码块内无效。


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

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

下面代码在代码块之中,别离用 let 和 var 申明了两个变量。而后在代码块之外调用这两个变量,后果 let 申明的变量报错,var 申明的变量返回了正确的值。这表明,let 申明的变量只在它所在的代码块无效。

for 循环的计数器,就很适合应用 let 命令。

for (let i = 0; i < 10; i++) {// ...}

console.log(i);
// ReferenceError: i is not defined

下面代码中,计数器 i 只在 for 循环体内无效,在循环体外援用就会报错。

上面的代码如果应用 var,最初输入的是 10。


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

下面代码中,变量 i 是 var 命令申明的,在全局范畴内都无效,所以全局只有一个变量 i。每一次循环,变量 i 的值都会产生扭转,而循环内被赋给数组 a 的函数外部的 console.log(i),外面的 i 指向的就是全局的 i。也就是说,所有数组 a 的成员外面的 i,指向的都是同一个 i,导致运行时输入的是最初一轮的 i 的值,也就是 10。

如果应用 let,申明的变量仅在块级作用域内无效,最初输入的是 6。


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

下面代码中,变量 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

下面代码正确运行,输入了 3 次 abc。这表明函数外部的变量 i 与循环变量 i 不在同一个作用域,有各自独自的作用域。

不存在变量晋升

var 命令会产生“变量晋升”景象,即变量能够在申明之前应用,值为 undefined。这种景象多多少少是有些奇怪的,依照个别的逻辑,变量应该在申明语句之后才能够应用。

为了纠正这种景象,let 命令扭转了语法行为,它所申明的变量肯定要在申明后应用,否则报错。


// var 的状况
console.log(foo); // 输入 undefined
var foo = 2;

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

下面代码中,变量 foo 用 var 命令申明,会产生变量晋升,即脚本开始运行时,变量 foo 曾经存在了,然而没有值,所以会输入 undefined。变量 bar 用 let 命令申明,不会产生变量晋升。这示意在申明它之前,变量 bar 是不存在的,这时如果用到它,就会抛出一个谬误。

暂时性死区

只有块级作用域内存在 let 命令,它所申明的变量就“绑定”(binding)这个区域,不再受内部的影响。


var tmp = 123;

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

下面代码中,存在全局变量 tmp,然而块级作用域内 let 又申明了一个局部变量 tmp,导致后者绑定这个块级作用域,所以在 let 申明变量前,对 tmp 赋值会报错。

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
}

下面代码中,在 let 命令申明变量 tmp 之前,都属于变量 tmp 的“死区”。

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

typeof x; // ReferenceError
let x;
下面代码中,变量 x 应用 let 命令申明,所以在申明之前,都属于 x 的“死区”,只有用到该变量就会报错。因而,typeof 运行时就会抛出一个 ReferenceError。

作为比拟,如果一个变量基本没有被申明,应用 typeof 反而不会报错。

typeof undeclared_variable; // “undefined”
下面代码中,undeclared_variable 是一个不存在的变量名,后果返回“undefined”。所以,在没有 let 之前,typeof 运算符是百分之百平安的,永远不会报错。当初这一点不成立了。这样的设计是为了让大家养成良好的编程习惯,变量肯定要在申明之后应用,否则就报错。

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


function bar(x = y, y = 2) {return [x, y];
}

bar(); // 报错

下面代码中,调用 bar 函数之所以报错(某些实现可能不报错),是因为参数 x 默认值等于另一个参数 y,而此时 y 还没有申明,属于“死区”。如果 y 的默认值是 x,就不会报错,因为此时 x 曾经申明了。

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

另外,上面的代码也会报错,与 var 的行为不同。

// 不报错
var x = x;

// 报错
let x = x;
// ReferenceError: x is not defined
下面代码报错,也是因为暂时性死区。应用 let 申明变量时,只有变量在还没有申明实现前应用,就会报错。下面这行就属于这个状况,在变量 x 的申明语句还没有执行实现前,就去取 x 的值,导致报错”x 未定义“。

ES6 规定暂时性死区和 let、const 语句不呈现变量晋升,次要是为了缩小运行时谬误,避免在变量申明前就应用这个变量,从而导致意料之外的行为。这样的谬误在 ES5 是很常见的,当初有了这种规定,防止此类谬误就很容易了。

总之,暂时性死区的实质就是,只有一进入以后作用域,所要应用的变量就曾经存在了,然而不可获取,只有等到申明变量的那一行代码呈现,才能够获取和应用该变量。

不容许反复申明

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

块级作用域

为什么须要块级作用域?

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

第一种场景,内层变量可能会笼罩外层变量。

var tmp = new Date();

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

f(); // undefined

下面代码的原意是,if 代码块的内部应用外层的 tmp 变量,外部应用内层的 tmp 变量。然而,函数 f 执行后,输入后果为 undefined,起因在于变量晋升,导致内层的 tmp 变量笼罩了外层的 tmp 变量。

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


var s = "hello";

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

console.log(i); // 5

下面代码中,变量 i 只用来管制循环,然而循环完结后,它并没有隐没,泄露成了全局变量。

ES6 的块级作用域

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

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

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

ES6 容许块级作用域的任意嵌套。


{
  {
    {
      {
        {let insane = "Hello World";}
        console.log(insane); // 报错
      }
    }
  }
}

下面代码应用了一个五层的块级作用域,每一层都是一个独自的作用域。第四层作用域无奈读取第五层作用域的外部变量。

内层作用域能够定义外层作用域的同名变量。


{
  {
    {
      {
        let insane = "Hello World";
        {let insane = "Hello World";}
      }
    }
  }
}

块级作用域的呈现,实际上使得取得广泛应用的匿名立刻执行函数表达式(匿名 IIFE)不再必要了。


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

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

块级作用域与函数申明

函数能不能在块级作用域之中申明?这是一个相当令人混同的问题。

ES5 规定,函数只能在顶层作用域和函数作用域之中申明,不能在块级作用域申明。


// 状况一
if (true) {function f() {}}

// 状况二
try {function f() {}} catch (e) {// ...}

下面两种函数申明,依据 ES5 的规定都是非法的。

然而,浏览器没有恪守这个规定,为了兼容以前的旧代码,还是反对在块级作用域之中申明函数,因而下面两种状况理论都能运行,不会报错。

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 会被晋升到函数头部,理论运行的代码如下。


// ES5 环境
function f() {console.log("I am outside!");
}

(function () {function f() {console.log("I am inside!");
  }
  if (false) { }
  f();})();

ES6 就齐全不一样了,实践上会失去“I am outside!”。因为块级作用域内申明的函数相似于 let,对作用域之外没有影响。然而,如果你真的在 ES6 浏览器中运行一下下面的代码,是会报错的,这是为什么呢?


// 浏览器的 ES6 环境
function f() {console.log("I am outside!");
}

(function () {if (false) {
    // 反复申明一次函数 f
    function f() {console.log("I am inside!");
    }
  }

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

下面的代码在 ES6 浏览器中,都会报错。

原来,如果扭转了块级作用域内申明的函数的解决规定,显然会对老代码产生很大影响。为了加重因而产生的不兼容问题,ES6 在附录 B (opens new window)外面规定,浏览器的实现能够不恪守下面的规定,有本人的行为形式 (opens new window)。

容许在块级作用域内申明函数。
函数申明相似于 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

思考到环境导致的行为差别太大,应该防止在块级作用域内申明函数。如果的确须要,也应该写成函数表达式,而不是函数申明语句。


// 块级作用域外部的函数申明语句,倡议不要应用
{
  let a = "secret";
  function f() {return a;}
}

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

另外,还有一个须要留神的中央。ES6 的块级作用域必须有大括号,如果没有大括号,JavaScript 引擎就认为不存在块级作用域。


// 第一种写法,报错
if (true) let x = 1;

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

下面代码中,第一种写法没有大括号,所以不存在块级作用域,而 let 只能呈现在以后作用域的顶层,所以报错。第二种写法有大括号,所以块级作用域成立。

函数申明也是如此,严格模式下,函数只能申明在以后作用域的顶层。


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

// 报错
("use strict");
if (true) function f() {}
#const 命令
#根本用法
const 申明一个只读的常量。一旦申明,常量的值就不能扭转。const PI = 3.1415;
PI; // 3.1415

PI = 3;
// TypeError: Assignment to constant variable.

下面代码表明扭转常量的值会报错。

const 申明的变量不得扭转值,这意味着,const 一旦申明变量,就必须立刻初始化,不能留到当前赋值。

const foo;
// SyntaxError: Missing initializer in const declaration
下面代码示意,对于 const 来说,只申明不赋值,就会报错。

const 的作用域与 let 命令雷同:只在申明所在的块级作用域内无效。


if (true) {const MAX = 5;}

MAX; // Uncaught ReferenceError: MAX is not defined
const 命令申明的常量也是不晋升,同样存在暂时性死区,只能在申明的地位前面应用。if (true) {console.log(MAX); // ReferenceError
  const MAX = 5;
}

下面代码在常量 MAX 申明之前就调用,后果报错。

const 申明的常量,也与 let 一样不可反复申明。


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

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

实质

const 实际上保障的,并不是变量的值不得改变,而是变量指向的那个内存地址所保留的数据不得改变。对于简略类型的数据(数值、字符串、布尔值),值就保留在变量指向的那个内存地址,因而等同于常量。但对于复合类型的数据(次要是对象和数组),变量指向的内存地址,保留的只是一个指向理论数据的指针,const 只能保障这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就齐全不能管制了。因而,将一个对象申明为常量必须十分小心。


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,就会报错。

如果真的想将对象解冻,应该应用 Object.freeze 办法。

const foo = Object.freeze({});

// 惯例模式时,上面一行不起作用;
// 严格模式时,该行会报错
foo.prop = 123;
下面代码中,常量 foo 指向一个解冻的对象,所以增加新属性不起作用,严格模式时还会报错。

除了将对象自身解冻,对象的属性也应该解冻。上面是一个将对象彻底解冻的函数。


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

ES6 申明变量的六种办法

ES5 只有两种申明变量的办法:var 命令和 function 命令。ES6 除了增加 let 和 const 命令,前面章节还会提到,另外两种申明变量的办法:import 命令和 class 命令。所以,ES6 一共有 6 种申明变量的办法。

顶层对象的属性

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

window.a = 1;
a; // 1

a = 2;
window.a; // 2
下面代码中,顶层对象的属性赋值与全局变量的赋值,是同一件事。

顶层对象的属性与全局变量挂钩,被认为是 JavaScript 语言最大的设计败笔之一。这样的设计带来了几个很大的问题,首先是没法在编译时就报出变量未声明的谬误,只有运行时能力晓得(因为全局变量可能是顶层对象的属性发明的,而属性的发明是动静的);其次,程序员很容易人不知; 鬼不觉地就创立了全局变量(比方打字出错);最初,顶层对象的属性是到处能够读写的,这十分不利于模块化编程。另一方面,window 对象有实体含意,指的是浏览器的窗口对象,顶层对象是一个有实体含意的对象,也是不适合的。

ES6 为了扭转这一点,一方面规定,为了放弃兼容性,var 命令和 function 命令申明的全局变量,仍旧是顶层对象的属性;另一方面规定,let 命令、const 命令、class 命令申明的全局变量,不属于顶层对象的属性。也就是说,从 ES6 开始,全局变量将逐渐与顶层对象的属性脱钩。

var a = 1;
// 如果在 Node 的 REPL 环境,能够写成 global.a
// 或者采纳通用办法,写成 this.a
window.a; // 1

let b = 1;
window.b; // undefined
下面代码中,全局变量 a 由 var 命令申明,所以它是顶层对象的属性;全局变量 b 由 let 命令申明,所以它不是顶层对象的属性,返回 undefined。

globalThis 对象

JavaScript 语言存在一个顶层对象,它提供全局环境(即全局作用域),所有代码都是在这个环境中运行。然而,顶层对象在各种实现外面是不对立的。

浏览器外面,顶层对象是 window,但 Node 和 Web Worker 没有 window。
浏览器和 Web Worker 外面,self 也指向顶层对象,然而 Node 没有 self。
Node 外面,顶层对象是 global,但其余环境都不反对。
同一段代码为了可能在各种环境,都能取到顶层对象,当初个别是应用 this 变量,然而有局限性。

全局环境中,this 会返回顶层对象。然而,Node.js 模块中 this 返回的是以后模块,ES6 模块中 this 返回的是 undefined。
函数外面的 this,如果函数不是作为对象的办法运行,而是单纯作为函数运行,this 会指向顶层对象。然而,严格模式下,这时 this 会返回 undefined。
不论是严格模式,还是一般模式,new Function(‘return this’)(),总是会返回全局对象。然而,如果浏览器用了 CSP(Content Security Policy,内容安全策略),那么 eval、new Function 这些办法都可能无奈应用。
综上所述,很难找到一种办法,能够在所有状况下,都取到顶层对象。上面是两种勉强能够应用的办法。


// 办法一
typeof window !== "undefined"
  ? window
  : typeof process === "object" &&
    typeof require === "function" &&
    typeof global === "object"
  ? global
  : this;

// 办法二
var getGlobal = function () {if (typeof self !== "undefined") {return self;}
  if (typeof window !== "undefined") {return window;}
  if (typeof global !== "undefined") {return global;}
  throw new Error("unable to locate global object");
};

ES2020 (opens new window)在语言规范的层面,引入 globalThis 作为顶层对象。也就是说,任何环境下,globalThis 都是存在的,都能够从它拿到顶层对象,指向全局环境下的 this。

垫片库 global-this (opens new window)模仿了这个提案,能够在所有环境拿到 globalThis。

正文完
 0