前言

《ECMAScript入门》是一本开源的JavaScript语言教程,全面介绍ECMAScript6新引入的语法特性。

let和const命令,是第一章开始介绍,也是比较基础的知识。我在学习之后,把它总结记录下来,以便自己以后复习查看。

以下代码,于Chrome57 DevTools运行

node为6.3版本

先总结

先总结区别,再分别阐述

let 与 const 相同点和区别

1:let命令用来声明变量,用法类似于var,但是所声明的变量。

const声明一个只读的常量,一旦声明,常量的值就不能改变。

const保证的是内存地址不得改动。常量(内存地址保存的是值),复合类型的数据(内存地址保存的是指针)

2:两者都不存在变量提升。

3:两者都有暂时性死区(TDZ)。

4:两者都不允许重复声明。

5:两者为JavaScript新增了块级作用域。两者作用域,只在声明所在的块级作用域有效。

基本用法

let命令介绍

let命令,用来声明变量。它的用法类似于var, 但是所声明的变量,只在let命令所在的代码块内有效。

浏览器中的结果

上面代码在代码块之中,分别用letvar声明了两个变量。然后在代码块之外调用这两个变量,结果let声明的变量报错,var声明的变量返回了正确的值。这表明,let声明的变量只在它所在的代码块有效。上面代码在代码块之中,分别用letvar声明了两个变量。然后在代码块之外调用这两个变量,结果let声明的变量报错,var声明的变量返回了正确的值。这表明,let声明的变量只在它所在的代码块有效。

let实用举例

for循环的计数器

  1. for (let i =0; i < 10; i++) {}
  2. console.log(i) // ReferenceError: i is not defined

浏览器中的结果

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

下面的代码如果使用var, 最后输出的是10

  1. var a = [];
  2. for (var i = 0; i < 10; i++) {
  3. a[i] = function () {
  4. console.log(i);
  5. };
  6. }
  7. a[6](); // 10

浏览器中的结果

上面代码中,变量ivar命令声明的,在全局范围内都有效,所以全局只有一个变量i。每一次循环,变量i的值都会发生改变,而循环内被赋给数组a的函数内部的console.log(i),里面的i指向的就是全局的i。也就是说,所有数组a的成员里面的i,指向的都是同一个i,导致运行时输出的是最后一轮的i的值,也就是10

如果使用let, 声明的变量仅在块级作用域内有效,最后输出的是6

  1. var a = [];
  2. for (var i = 0; i < 10; i++) {
  3. a[i] = function () {
  4. console.log(i);
  5. };
  6. }
  7. a[6](); // 6

浏览器结果

上面代码中,变量ilet声明的,当前的i只在本轮循环有效,所以每一次循环的i其实都是一个新的变量,所以最后输出的是6。你可能会问,如果每一轮循环的变量i都是重新声明的,那它怎么知道上一轮循环的值,从而计算出本轮循环的值?这是因为 JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。

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

  1. for (let i = 0; i < 3; i++) {
  2. let i = 'abc';
  3. console.log(i);
  4. }
  5. // abc
  6. // abc
  7. // abc

浏览器结果

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

不存在变量提升

var命令会发生变量提升现象,即变量可以在声明之前使用,值为undefined。这种现象多多少少是有些奇怪的,按照一般的逻辑,变量应该在声明语句之后才可以使用。

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

  1. // var 的情况
  2. console.log(foo); // 输出undefined
  3. var foo = 2;
  4. // let 的情况
  5. console.log(bar); // 报错ReferenceError
  6. let bar = 2;

浏览器结果

上面代码中,变量foovar命令声明,会发生变量提升,即脚本开始运行时,变量foo已经存在了,但是没有值,所以会输出undefined。变量barlet命令声明,不会发生变量提升。这表示在声明它之前,变量bar是不存在的,这时如果用到它,就会抛出一个错误。

暂时性死区(TDZ)

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

  1. var tmp = 123;
  2. if (true) {
  3. tmp = 'abc'; // ReferenceError
  4. let tmp;
  5. }

浏览器中的结果

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

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

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

  1. if (true) {
  2. // TDZ开始
  3. tmp = 'abc'; // ReferenceError
  4. console.log(tmp); // ReferenceError
  5. let tmp; // TDZ结束
  6. console.log(tmp); // undefined
  7. tmp = 123;
  8. console.log(tmp); // 123
  9. }

浏览器结果



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

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

  1. typeof x; // ReferenceError
  2. let x;

浏览器结果

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

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

  1. typeof undeclared_variable // "undefined"

浏览器结果

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

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

  1. function bar(x = y, y = 2) {
  2. return [x, y];
  3. }
  4. bar(); // 报错

浏览器结果

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

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

浏览器结果

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

  1. // 不报错
  2. var x = x;
  3. // 报错
  4. let x = x;
  5. // ReferenceError: x is not defined

浏览器结果

上面代码报错,也是因为暂时性死区。使用let声明变量时,只要变量在还没有声明完成前使用,就会报错。上面这行就属于这个情况,在变量x的声明语句还没有执行完成前,就去取x的值,导致报错”x 未定义“。

ES6 规定暂时性死区和letconst语句不出现变量提升,主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。这样的错误在 ES5 是很常见的,现在有了这种规定,避免此类错误就很容易了。

总之,暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。

不允许重复声明

let不允许在相同作用域内,重复声明同一个变量。

浏览器结果

因此,不能在函数内部重新声明参数。

  1. function func(arg) {
  2. let arg; //在chrome浏览器下,不报错。使用node执行,会报错。
  3. }
  4. function func(arg) {
  5. {
  6. let arg;//在chrome浏览器下,不报错。使用node执行,也不报错。
  7. }
  8. }

浏览器环境和node执行情况

块级作用域

块级作用域,没有返回值。

为什么需要块级作用域?

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

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

  1. var tmp = new Date();
  2. function f() {
  3. console.log(tmp);
  4. if (false) {
  5. var tmp = 'hello world';
  6. }
  7. }
  8. f(); // undefined

浏览器结果

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

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

  1. var s = 'hello';
  2. for (var i = 0; i < s.length; i++) {
  3. console.log(s[i]);
  4. }
  5. console.log(i); // 5

浏览器结果

上面代码中,变量i只用来控制循环,但是循环结束后,它并没有消失,泄露成了全局变量。

ES6的块级作用域

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

  1. function f1() {
  2. let n = 5;
  3. if (true) {
  4. let n = 10;
  5. }
  6. console.log(n); // 5
  7. }

浏览器结果

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

浏览器结果

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

  1. {{{{{let insane = 'Hello World'}}}}};

上面代码使用了一个五层的块级作用域。外层作用域无法读取内层作用域的变量。

  1. {{{{
  2. {let insane = 'Hello World'}
  3. console.log(insane); // 报错
  4. }}}};

浏览器结果

内层作用域可以定义外层作用域的同名变量。

  1. {{{{
  2. let insane = 'Hello World';
  3. {let insane = 'Hello World'}
  4. }}}};

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

  1. // IIFE 写法
  2. (function () {
  3. var tmp = ...;
  4. ...
  5. }());
  6. // 块级作用域写法
  7. {
  8. let tmp = ...;
  9. ...
  10. }

块级作用域与函数声明

  • 允许在块级作用域内声明函数。
  • 函数声明类似于var,即会提升到全局作用域或函数作用域的头部。
  • 同时,函数声明还会提升到所在的块级作用域的头部。

const命令

基本用法

const也是用来声明变量,但是声明的是一个只读的常量。一旦声明,常量的值就不能改变。

  1. const PI = 3.1415;
  2. PI // 3.1415
  3. PI = 3;
  4. // TypeError: Assignment to constant variable.

浏览器结果

上面代码表明改变常量的值会报错。

const声明的变量不得改变值,这意味着,const一旦声明变量,就必须立即初始化,不能留到以后赋值。

  1. const foo;
  2. // SyntaxError: Missing initializer in const declaration

浏览器结果

上面代码表示,对于const来说,只声明不赋值,就会报错。

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

  1. if (true) {
  2. const MAX = 5;
  3. }
  4. MAX // Uncaught ReferenceError: MAX is not defined

浏览器结果

const命令声明的常量也是不提升,同样存在暂时性死区,只能在声明的位置后面使用。

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

浏览器结果

上面代码在常量MAX声明之前就调用,结果报错。

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

  1. var message = "Hello!";
  2. let age = 25;
  3. // 以下两行都会报错
  4. const message = "Goodbye!";
  5. const age = 30;

浏览器结果

对于复合类型的变量,变量名不指向数据,而是指向数据所在的地址。 const命令只是保证变量名指向的地址不变,并不保证该地址的数据不变,所以,将一个对象声明为常量必须非常小心。

  1. const foo = {};
  2. // 为 foo 添加一个属性,可以成功
  3. foo.prop = 123;
  4. foo.prop // 123
  5. // 将 foo 指向另一个对象,就会报错
  6. foo = {}; // TypeError: Assignment to constant variable.

浏览器结果

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

跨模块常量

上面说过,const声明的常量只在当前代码块有效。如果想设置跨模块的常量,可以采用下面的写法。

  1. // constants.js 模块
  2. export const A = 1;
  3. export const B = 3;
  4. export const C = 4;
  1. //test1.js 模块
  2. import * as constans from './constants';
  3. console.log(constants.A); // 1
  4. console.log(constants.B); // 3
  1. // test2.js 模块
  2. import {A, B} from './constants';
  3. console.log(A); //1
  4. console.log(B); //3

这里其实用到了Module(模块)的导入导出功能。以后会讲到。

全局对象的属性

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

  1. window.a = 1;
  2. a //1
  3. a = 2;
  4. window.a //2

浏览器结果

上面代码中,全局对象的属性赋值与全局变量的赋值,是同一件事。(对于Node.js来说,这一条只对REPL环境适用,模块环境中,全局变量必须显示声明成global对象的属性。)这种规定被市委JavaScript语言的一大问题。因为很容易不知不觉就创建了全局变量。

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

  1. var a = 1;
  2. // 如果在Node的REPL环境,可以写成global.a
  3. // 或者采用通用方法,写成this.a
  4. window.a // 1
  5. let b = 1;
  6. window.b // undefined

浏览器结果

上面代码中,全局变量avar命令声明,所以它是全局对象的属性;全局变量blet命令声明,所以它不是全局对象的属性,返回undefined

参考

let 和 const 命令

ECMAScript6-let与const命令详解的更多相关文章

  1. es6学习笔记(1) let和const命令详解

    let和const命令: Es6新增了let命令,声明变量,但与var不一样的,只在let命令所在的代码块内有效(如for循环之外let声明的变量就不再有效).并且let不像var那样会发生" ...

  2. ES6中let与const命令详解

    阮一峰ES6入门 let 作用域 let命令用来声明变量,但声明的变量只在let命令所在的代码块内有效. { let a = 10; var b = 1; } a // ReferenceError: ...

  3. 转载:MySQL EXPLAIN 命令详解学习

    转载自:https://blog.csdn.net/mchdba/article/details/9190771 MySQL EXPLAIN 命令详解 MySQL的EXPLAIN命令用于SQL语句的查 ...

  4. MYSQL-EXPLAIN 命令详解 (转载)

    EXPLAIN 命令详解   在工作中,我们用于捕捉性能问题最常用的就是打开慢查询,定位执行效率差的SQL,那么当我们定位到一个SQL以后还不算完事,我们还需要知道该SQL的执行计划,比如是全表扫描, ...

  5. MySQL EXPLAIN 命令详解

    MySQL EXPLAIN 命令详解 MySQL的EXPLAIN命令用于SQL语句的查询执行计划(QEP).这条命令的输出结果能够让我们了解MySQL 优化器是如何执行SQL 语句的.这条命令并没有提 ...

  6. const用法详解(转)

    http://www.cnblogs.com/StudyRush/archive/2010/10/06/1844690.html 面向对象是C++的重要特性. 但是c++在c的基础上新增加的几点优化也 ...

  7. Git初探--笔记整理和Git命令详解

    几个重要的概念 首先先明确几个概念: WorkPlace : 工作区 Index: 暂存区 Repository: 本地仓库/版本库 Remote: 远程仓库 当在Remote(如Github)上面c ...

  8. linux yum命令详解

    yum(全称为 Yellow dog Updater, Modified)是一个在Fedora和RedHat以及SUSE中的Shell前端软件包管理器.基於RPM包管理,能够从指定的服务器自动下载RP ...

  9. Linux下ps命令详解 Linux下ps命令的详细使用方法

    http://www.jb51.net/LINUXjishu/56578.html Linux下的ps命令比较常用 Linux下ps命令详解Linux上进程有5种状态:1. 运行(正在运行或在运行队列 ...

随机推荐

  1. jQuery实现的快速查找

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...

  2. [leetcode-507-Perfect Number]

    We define the Perfect Number is a positive integer that is equal to the sum of all its positive divi ...

  3. jQuery与原生js实现banner轮播图

    jQuery与原生js实现banner轮播图: (jq需自己加载)(图片需自己加载) <!DOCTYPE html> <html> <head> <meta ...

  4. 使用matplotlib快速绘图

    matplotlib的pyplot子库提供了和matlab类似的绘图API,方便用户快速绘制2D图表.让我们先来看一个简单的例子: # -*- coding: utf-8 -*- import num ...

  5. Asp.Net Core 中无法使用 ConfigurationManager.AppSettings

    刚刚接触.net core ,准备把之前的一些技术常用工具先移植到.net Standard上面来, 方便以后使用,结果用到ConfigurationManager 的 AppSettings 就出现 ...

  6. Redis基本数据结构总结之SET、ZSET和HASH

    Redis基本数据结构总结 前言 Redis的特点在于其读写速度特别快,因为是存储在内存中的,其非常适合于处理大数据量的情况:还有一个是其不同于其他的关系型数据库,Redis是非关系型数据库,也就是我 ...

  7. Python爬虫从入门到放弃(十六)之 Scrapy框架中Item Pipeline用法

    当Item 在Spider中被收集之后,就会被传递到Item Pipeline中进行处理 每个item pipeline组件是实现了简单的方法的python类,负责接收到item并通过它执行一些行为, ...

  8. ActiveMQ结合WebScoket应用例子以及介绍

    一.ActiveMQ的介绍? 1.JMS基础概念 JMS(java Message Service) 即使java消息服务,它提供标准的产生.发送.接收的接口简化企业应用开发,它支持两种消息通信模型: ...

  9. 一步一步学Vue(七)

    前言:我以后在文章最后再也不说我下篇博文要写什么,之前说的大家也可以忽略,如果你不忽略,会失望的

  10. iOS开发 MVVM+RAC 的使用

    好长一段时间没有敲简书了! 主要是因为一直在跑面试. 终于还是在上海入职了! 由于项目原因最终还是入了MVVM+RAC的坑 下面是正题. Demo效果 使用MVVM+RAC请求网络数据 Reactiv ...