当Brendan Eich在1995年设计了JavaScript的第一个版本时,他犯了很多错误,包括从那时起就成为该语言一部分的一些错误,比如Date对象和当你不小心将它们相乘时对象会自动转换为NaN。然而,事后看来,他做对的事情都是非常重要的事情:对象;原型;具有词法作用域的一级函数;默认可变性。这种语言很好。比大家一开始意识到的要好。

尽管如此,Brendan还是做出了一个与今天的文章相关的特殊设计决定——我认为这个决定可以被定性为一个错误。这是一件小事。一种微妙的东西。你可能用了好几年,甚至都没注意到它。但这很重要,因为这个错误出现在我们现在认为是“好的部分”的语言方面。

它和变量有关。

问题1:块{}不是作用域

这条规则听起来很无害:在JS函数中声明的var的作用域就是该函数的整个函数体。但这有两种让人抱怨的后果。

一、在块中声明的变量的作用域不仅仅是块本身。它是整个函数。

你可能从来没有注意到这一点。恐怕这是你无法忘记的事情之一。让我们来看看一个场景,它会导致一个棘手的错误。假设你有一些使用名为t的变量的现有代码:

function runTowerExperiment(tower, startTime) {
var t = startTime; tower.on("tick", function () {
... code that uses t ...
});
... more code ...
}

到目前为止,一切都很好。现在你想要添加保龄球速度测量值,因此你向内部回调函数添加了一个小小的if语句。

function runTowerExperiment(tower, startTime) {
var t = startTime; tower.on("tick", function () {
... code that uses t ...
if (bowlingBall.altitude() <= 0) {
var t = readTachymeter();
...
}
});
... more code ...
}

你无意中添加了第二个名为t的变量。现在,在“使用t的代码”中(之前运行良好),t指向新的内部变量t,而不是现有的外部变量。

JavaScript中的var的作用域就像Photoshop中的油漆桶工具。它从声明开始,在两个方向上扩展,向前和向后,一直扩展到函数边界({})。由于变量t的作用域向后扩展了这么多,所以必须在我们一进入函数时就创建它。这叫做变量提升(hoisting)。我喜欢想象JS引擎用一个小小的代码起重机将每个varfunction提升到外围函数的顶部。

变量提升有它的优点。如果没有它,许多在全局作用域中工作良好的完美的cromulent技术将无法在IIFE(立即执行函数)中工作。但是在上面的代码中,变量提升会导致一个严重的错误:使用t的所有计算将开始产生NaN。它也很难跟踪,特别是如果你的代码比这个demo更大。

但与第二个var问题相比,这是小菜一碟。

问题2:循环中的变量过度共享

你可以猜到运行这段代码时会发生什么。很简单:

var messages = ["Hi!", "I'm a web page!", "alert() is fun!"];

for (var i = 0; i < messages.length; i++) {
alert(messages[i]);
}

运行这段代码,浏览器会顺序弹出3次alert框,消息内容分别为"Hi!", "I'm a web page!", "alert() is fun!"。现在我们把代码稍微改动一下:

var messages = ["Meow!", "I'm a talking cat!", "Callbacks are fun!"];

for (var i = 0; i < messages.length; i++) {
setTimeout(function () {
console.log(messages[i]);
}, i * 1500);
}

再次运行发现,结果出乎预料。浏览器没有按顺序说出打印三条信息,而是打印了三次undefined。你能发现漏洞吗?

这里的问题是只有一个变量i。它由循环本身和所有三个setTimeout回调函数共享。当循环运行结束时,i的值为3(因为messages.length为3),并且此时还没有调用任何回调函数。(异步,事件循环)

因此,当第一个setTimeout回调函数触发并调用console.log(messages[i])时,它使用的是messages[3](messages[3]肯定是undefined)

有很多种解决的方法,下面是一种:

var messages = ["Meow!", "I'm a talking cat!", "Callbacks are fun!"];

for (var i = 0; i < messages.length; i++) {
setTimeout((function (index) {
return function() {console.log(messages[index])};
})(i), i * 1500);
}

如果一开始就没有这种问题,那就太好了。

let, const是新的var

在大多数情况下,JavaScript(也包括其他编程语言,尤其是JavaScript)中的设计错误是无法修复的。向后兼容性意味着永远不会改变Web上现有JS代码的行为。即使是标准委员会也没有能力,比如说,解决JavaScript自动分号插入的奇怪问题。浏览器制造商不会实现破坏性的更改,因为这种更改会惩罚用户。大约十年前,当Brendan Eich决定解决这个问题时,只有一种方法。

他添加了一个新的关键字let,可以用来声明变量,就像var一样,但是有更好的作用域规则。

let t = readTachymeter();

for (let i = 0; i < messages.length; i++) {
...
}

letvar是不同的,所以如果你只是做一个全球搜索替换整个代码,可以破坏部分的代码(可能是无意中)。但在大多数情况下,在新ES6代码,你应该停止使用var,并在之前使用var的位置使用let。因此有这样的口号:“let是新的var”。

let和var之间到底有什么区别?

  • let变量是块作用域的。

    用let声明的变量的作用域只是封闭的块,而不是整个封闭的函数。使用let还是会有变量提升,但不是不分青红皂白。runTowerExperiment示例可以通过简单地将var更改为let来修复。如果你在任何地方都使用let,你就不会有那种bug了。

  • 全局let变量不是全局对象的属性

    也就是说,您不会通过写入window.variableName来访问它们。相反,它们存在于一个无形的块的范围内,该块理论上包含了在网页中运行的所有JS代码。

  • for (let x…)形式的循环在每次迭代中为x创建一个新的绑定。

    这是一个非常微妙的差别。这意味着,如果for (let…)循环执行多次,并且该循环包含一个闭包,就像在我们正在讨论的console.log示例中那样,每个闭包将捕获循环变量的不同副本,而不是所有闭包捕获相同的循环变量。所以上面那个例子可以用let替换var就可以解决错误:

var messages = ["Meow!", "I'm a talking cat!", "Callbacks are fun!"];

for (let i = 0; i < messages.length; i++) {
setTimeout(function () {
console.log(messages[i]);
}, i * 1500);
}

这适用于所有三种for循环:for-offor-in和带有分号的老式C类型循环。

  • 在到达let变量声明之前尝试使用它是错误的。

    在控制流到达声明变量的代码行之前,变量是未初始化的。例如:
function update() {
console.log("current time:", t); // ReferenceError
...
let t = readTachymeter();
}

这条规则是用来帮助你捕捉bug的。你将在问题所在的代码行上得到一个异常,而不是NaN

当变量在作用域内但未初始化时,这个时间段称为临时死区(temporal dead zone)。我一直在期待这句有灵感的行话能一跃成为科幻小说。还没有。

一个琐碎的性能细节:在大多数情况下,你可以通过查看代码来判断声明是否已经运行,因此JavaScript引擎实际上不需要在每次访问变量时执行额外的检查,以确保它已初始化。然而,在一个封闭的内部,有时是不清楚的。在这些情况下,JavaScript引擎将执行运行时检查。这意味着let比var要慢。

一个复杂的交替域作用域细节:在一些编程语言中,变量的作用域从声明点开始,而不是向后覆盖整个封闭块。标准委员会考虑对let使用这种范围规则。这样的话,t的使用导致这里的ReferenceError不会在后面的let t的范围内,所以它根本不会引用那个变量。它可以指封闭作用域中的t。但这种方法不适用于闭包或函数提升,因此最终被放弃。

  • 用let重新声明变量是一个SyntaxError错误。

    这条规则也可以帮助你发现微小的错误。不过,如果你尝试全局的let-to-var转换,这种差异很可能会给你带来一些问题,因为它甚至适用于全局的let变量。

如果你有几个脚本都声明了相同的全局变量,你最好继续使用var。如果切换到let,那么无论第二次加载哪个脚本都会失败并出现错误。

或者使用ES6模块。

一个的语法细节let是严格模式代码中的保留字。在非严格模式的代码中,为了向后兼容,你仍然可以声明变量、函数和名为let的参数——你可以写var let = 'q'! let let = 1这是不允许的。

除了这些区别之外,let和var几乎是相同的。例如,它们都支持声明用逗号分隔的多个变量,并且都支持解构。注意,类声明的行为类似于let,而不是var。如果你多次加载一个包含类的脚本,第二次重新声明类时就会得到一个错误。

const

ES6还引入了第三个可与let一起使用的关键字:const

用const声明的变量就像let一样,你只能在它们被声明的地方赋值。否则是一个SyntaxError。

const MAX_CAT_SIZE_KG = 3000; // 

MAX_CAT_SIZE_KG = 5000; // SyntaxError
MAX_CAT_SIZE_KG++; // nice try, but still a SyntaxError

很明显,不能在没有赋值的情况下声明const。

const theFairest;  // SyntaxError, you troublemaker

秘密特工:命名空间(namespace)

“Namespaces are one honking great idea—let’s do more of those!” —Tim Peters, “The Zen of Python”

在幕后,嵌套作用域是编程语言构建的核心概念之一。从什么时候开始就这样了,ALGOL?大概57年吧。今天更是如此。

在ES3之前,JavaScript只有全局作用域函数作用域。(让我们忽略with语句。)ES3引入了try-catch语句,这意味着添加了一种新的作用域,仅用于catch块中的异常变量。ES5添加了一个由strict eval()使用的作用域。ES6添加了块作用域for-loop作用域新的全局let作用域模块作用域以及在计算参数的默认值时使用的附加作用域

从ES3开始添加的所有额外作用域都是必要的,以使JavaScript的面向过程和面向对象特性像闭包一样流畅、精确和直观地工作,并与闭包无缝合作。也许你在今天之前从未注意过这些范围规则。如果是这样的话,JS语言正在默默完成它的工作。

[ES6深度解析]13:let const的更多相关文章

  1. [ES6深度解析]15:模块 Module

    JavaScript项目已经发展到令人瞠目结舌的规模,社区已经开发了用于大规模工作的工具.你需要的最基本的东西之一是一个模块系统,这是一种将你的工作分散到多个文件和目录的方法--但仍然要确保你的所有代 ...

  2. ES6深度解析3:Generators

    介绍ES6 Generators 什么是Generators(生成器函数)?让我们先来看看一个例子. function* quips(name) { yield "hello " ...

  3. [ES6深度解析]12:Classes

    我们将讨论一个老问题:在JavaScript中创建对象的构造函数. 存在的问题 假设我们想要创建最典型的面向对象设计的示例:Circle类.假设我们正在为一个简单的Canvas库编写一个Circle. ...

  4. [ES6深度解析]14:子类 Subclassing

    我们描述了ES6中添加的新类系统,用于处理创建对象构造函数的琐碎情况.我们展示了如何使用它来编写如下代码: class Circle { constructor(radius) { this.radi ...

  5. [WebKit内核] JavaScript引擎深度解析--基础篇(一)字节码生成及语法树的构建详情分析

    [WebKit内核] JavaScript引擎深度解析--基础篇(一)字节码生成及语法树的构建详情分析 标签: webkit内核JavaScriptCore 2015-03-26 23:26 2285 ...

  6. Go netpoll I/O 多路复用构建原生网络模型之源码深度解析

    导言 Go 基于 I/O multiplexing 和 goroutine 构建了一个简洁而高性能的原生网络模型(基于 Go 的I/O 多路复用 netpoll),提供了 goroutine-per- ...

  7. 《C++深度解析》课程目录

    <C++深度解析>课程目录 第1课 - 学习 C++ 的意义 第2课 - C到C++的升级     第3课 - 进化后的const分析 第4课 - 布尔类型和引用 第5课 - 引用的本质分 ...

  8. Kafka深度解析

    本文转发自Jason’s Blog,原文链接 http://www.jasongj.com/2015/01/02/Kafka深度解析 背景介绍 Kafka简介 Kafka是一种分布式的,基于发布/订阅 ...

  9. 深度解析Java8 – AbstractQueuedSynchronizer的实现分析(上)

    本文首发在infoQ :www.infoq.com/cn/articles/jdk1.8-abstractqueuedsynchronizer 前言: Java中的FutureTask作为可异步执行任 ...

随机推荐

  1. 不用SCRAPY也可以应用selector

    在PY文件中: from scrapy.selector import Selectorfrom scrapy.http import HtmlResponse url="https://m ...

  2. 你好,我是B树

    一.什么是B树? B树是一棵是具备以下特点的有根树. 1.节点属性 a)x.n:为节点中存储的关键字个数. b)x.key:为节点中存储的关键字.x.key1.x.key2 ... x.keyx.n  ...

  3. Docker的学习体验

    由于兴致使然,便想学习一点Docker技术.于是,写了这篇学习Docker的体会.笔拙,见谅. 第一件事--把网线插上 相信很多人都被官网的<Sample application>的 do ...

  4. shell编程-ssh免交互批量分发公钥脚本

    脚本基本原理 1.控制端免交互创建秘钥和公钥: 1 ssh-keygen -t rsa -f /root/.ssh/id_rsa -N "" 2.免交互发送公钥 1 sshpass ...

  5. YsoSerial 工具常用Payload分析之CC5、6(三)

    前言 这是common-collections 反序列化的第三篇文章,这次分析利用链CC5和CC6,先看下Ysoserial CC5 payload: public BadAttributeValue ...

  6. java跨平台性说明

    一.举例说明 我们知道,只要是用标准C开发的程序,使用不同的编译器编译后的可执行文件是可以在对应平台运行的,比如windows可以使用VC编译,那编译后的exe文件就可以在windows下运行:liu ...

  7. TCP协议与HTTP协议区别

    一.TCP协议与HTTP协议区别 1.直观认识 TCP协议对应于传输层,而HTTP协议对应于应用层,从本质上来说,二者没有可比性.Http协议是建立在TCP协议基础之上的,当浏览器需要从服务器获取网页 ...

  8. Linux虚拟机与主机网络连接配置与文件传输

    网络配置 对于VMware虚拟机 1. 设置linux系统的网络配置,如下(NAT为默认配置,这里采用这一配置) 2. 主机中配置本地连接-属性-共享-勾选红框配置项,如下:     3.重启虚拟机. ...

  9. [BSidesCF 2020]Had a bad day 1--PHP伪协议

    首先先打开主页,审查代码,并没有什么特别的地方使用dirsearch,发现flag.php![在这里插入图片描述](https://img-blog.csdnimg.cn/82348deddfd94c ...

  10. 手写RPC

    服务端代码 package com.peiyu.rpcs.bios; import java.io.IOException; public interface IRpcServers { void s ...