[ES6深度解析]13:let const
当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引擎用一个小小的代码起重机将每个
var和
function提升到外围函数的顶部。
变量提升有它的优点。如果没有它,许多在全局作用域中工作良好的完美的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++) { ... }
let和
var是不同的,所以如果你只是做一个全球搜索替换整个代码,可以破坏部分的代码(可能是无意中)。但在大多数情况下,在新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-of、
for-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语言正在默默完成它的工作。
- JavaScript中ES6语法Let,Const变量定义解析
- ES6语法之 let 与 const
- ES6-let,const
- ES6 之 let和const命令
- ES6的let和const命令(一)
- (1)ES6中let,const,对象冻结,跨模块常量,新增的全局对象介绍
- es6 箭頭函數 let, const
- ES6 let命令和块级作用域和const命令
- ES6 let 和 const
- ES6中let与const命令的基础知识
- es6-let和const命令 -- 001
- ES6语法中let与const的简单了解
- es6学习-let和const命令
- ES6 ---声明变量 let const 笔记
- ES6 let & const
- ES6 let,const与var的区别与理解
- ES6-----let和const
- 【ES6】let、const变量提升的验证,以及TDZ死区的理解
- ES6语法之let、const和解构赋值
- ES6 之 let和const命令 Symbol Promise对象