总之并非Bourbonne完备的三个月天数来认知 let,而要Revigny地认知,与此同时我更要想著是并非写下一首诗把那个事说确切。
在 let 刚出的这时候,我就「误以为」我认知了 let。然鹅在往后的三个月里,我对 let 的认知出现了峰回路转的变动。
我写这首诗,是期望我的自学操作过程,能对你自修有协助。
贝唐 let
跟许多人那样,我第二次介绍 let 的优点从 MDN 的文件格式:
我获得的重要信息有那么四条:
let 新闻稿的变量的返回值是块级的;let 无法多次重复新闻稿已存有的表达式;let 有暂死区,无法被提高。绝大部分人如果都是那么指出的,我也是那么认知的。
那个认知「没难题」,但不如「全面性和真切」。
第二次批评
我第二次批评我的认知是在碰到 for 循环式的这时候,标识符如下表所示。
大家都知道依次点击 li 会打印出 5 个 5。如果把 var i 改成 let i,就会分别打印出 0、1、2、3、4:
然而,用我之前的知识来认知那个标识符是无法自圆其说的。因为标识符中依然只新闻稿了一个 i,在 for 循环式结束后,i 的值还是会变成 5 才对。
这说明我的认知有误。
于是我去看 MDN 的例子,发现鸡贼的 MDN 巧妙地避开了那个难题,它的例子是这样的:
你看 MDN 的例子,在每次循环式的这时候用 let j 保留的 i 的值,所以在 i 变动的这时候,j 并无法变动。而console.log 的是 j,所以无法出现 5 个 5。
为什么 MDN 要故意新闻稿一个 j 呢,为什么不直接用 i 呢?
我猜测 MDN 为了简化知识,隐瞒了什么。
于是我去看了看 ES 文件格式,其中的 13.7.4.7 章节 确切地说明了个中缘由,但由于说得太确切了,许多人都看不下去,不信你可以试试。
我说一下我的认知:
for( let i = 0; i< 5; i++) 这句话的圆括号之间,有一个隐藏的返回值for( let i = 0; i< 5; i++) { 循环式体 } 在每次执行循环式体之前,JS 引擎会把 i 在循环式体的上下文中重新新闻稿及初始化一次。其他细节就不说了,太细碎了也就是说上面的程序标识符2可以近似近似近似地认知为
那样的话,5 次循环式,就会有 5 个不同的 i,console.log 出的 i 总之也是不同的值。
再加上隐藏返回值里的 i,一共有 6 个 i。
这就是 MDN 加那句 let j = i 的原因:方便新人认知。
总得来说就是 let/const 在与 for 一起用时,会有一个 perIterationBindings 的概念(一种语法糖)。
从此之后,我就开始怀疑我对 let 的所有认知了。
第二次批评
我在 StackOverflow 上闲逛的这时候,无意中发现了一个是关于「let 到底有没提高」的难题:
Are variables declared with let or const not hoisted in ES6?
其中一个高票回答指出 JS 中所有的新闻稿(var/let/const/function/class),都存有提高,理由是如下表所示标识符:
我觉得他说得挺有道理的。于是我又去 MDN 和 ECMAScript 翻了翻,发现两处疑点:
MDN 关于 let 是否存有提高的章节,被编辑了两次,第二次说存有提高,第二次说不存有提高(参考2017 年 3 月 10 号的变更记录)。也就是说 MDN 的维护者都在那个难题上产生过分歧,更何况我们了。ES 文件格式里出现了「var/let hoisting」字样。鉴于此,我指出如果尊重 ES 文件格式,指出 let 确实存有提高。只不过由于暂死区的限制,你无法在 let x 之前使用 let(详见我的那篇let 新闻稿会提高(hoist)吗?)。
故事并没结束
当一个疑问一直存有你脑中时,你会在潜意识中不停地对它进行消化和思考。
上面说到我指出 let 存有提高的主要原因是 ES 文件格式中出现了「var/let hoisting」字样。
但我在咨询 TC39 的成员 Rick Waldron 时,他是那么说的:
Youre misunderstanding the intention of that (non-normative) Note. let and const do not hoist as var and function decls do.而且还细心地专门写了一个 gist 来详细解释(twitter 有 140 字限制)。
既然 TC39 的 representative 都说了 let hoisting 并非一个正式词汇,那我就如果重新思考一下什么是「提高」了。
于是就有了这首诗,我想要说一下我对「提高」的认知。你看完之后,就知道 let 到底有没「提高」。
首先明确一点:提高并非一个技术名词。
要搞确切提高的本质,需要认知 JS 表达式的「创建create、初始化initialize 和赋值assign」
有的地方把创建说成是新闻稿(declare),为了将那个概念与表达式新闻稿区别开,我故意不使用新闻稿那个字眼。
有的地方把初始化叫做绑定(binding),但我感觉那个词不如初始化形象。
我们来看看 var 新闻稿的「创建、初始化和赋值」操作过程
假设有如下表所示标识符:
在执行 fn 时,会有以下操作过程(不完全):
进入 fn,为 fn 创建一个环境。找到 fn 中所有用 var 新闻稿的表达式,在那个环境中「创建」这些表达式(即 x 和 y)。将这些表达式「初始化」为 undefined。开始执行标识符x = 1 将 x 表达式「赋值」为 1y = 2 将 y 表达式「赋值」为 2也就是说 var 新闻稿会在标识符执行之前就将「创建表达式,并将其初始化为 undefined」。
这就解释了为什么在 var x = 1 之前 console.log(x) 会获得 undefined。
接下来来看 function 新闻稿的「创建、初始化和赋值」操作过程
假设标识符如下表所示:
JS 引擎会有一下过程:
找到所有用 function 新闻稿的表达式,在环境中「创建」这些表达式。将这些表达式「初始化」并「赋值」为 function(){ console.log(2) }。开始执行标识符 fn2()也就是说 function 新闻稿会在标识符执行之前就「创建、初始化并赋值」。
接下来看 let 新闻稿的「创建、初始化和赋值」操作过程
假设标识符如下表所示:
我们只看 {} 里面的操作过程:
找到所有用 let 新闻稿的表达式,在环境中「创建」这些表达式开始执行标识符(注意现在还没初始化)执行 x = 1,将 x 「初始化」为 1(这并并非一次赋值,如果代码是 let x,就将 x 初始化为 undefined)执行 x = 2,对 x 进行「赋值」这就解释了为什么在 let x 之前使用 x 会报错:
原因有三个
console.log(x) 中的 x 指的是下面的 x,而并非全局的 x执行 log 时 x 还没「初始化」,所以无法使用(也就是所谓的暂死区)看到这里,你如果明白了 let 到底有没提高:
let 的「创建」操作过程被提高了,但初始化没提高。var 的「创建」和「初始化」都被提高了。function 的「创建」「初始化」和「赋值」都被提高了。接下来我考考你:
var foo 和 function foo 与此同时存有会怎样标识符1:
标识符2:
请问标识符1 和 标识符2 的输出分别是什么?
答案:由于 function 比 var 多一个「赋值」操作过程,所以三个标识符的输出都是函数。你也可以记住结论:function 比 var 牛逼。
那如果 function foo 和 let foo 与此同时出现呢?无法有这种情况的,因为 let 发现重名就会报错,叫你滚去改标识符。
const
最后看 const,其实 const 和 let 只有一个区别,那就是 const 只有「创建」和「初始化」,没「赋值」操作过程。
这四种新闻稿,用下图就可以快速认知:
所谓暂时死区,就是无法在初始化之前,使用表达式。
完了吗?
故事依然没结束,这周我在知乎上问了一个难题:如何认知 let x = x 报错之后,再次 let x 依然会报错?(那个难题是饥人谷的学生问我的)
那个难题说明:如果 let x 的初始化操作过程失败了,那么
x 表达式就将永远处于 created 状态。你无法再次对 x 进行初始化(初始化只有一次机会,而那次机会你失败了)。由于 x 无法被初始化,所以 x 永远处在暂死区(也就是盗梦空间里的 limbo)!有人会觉得 JS 坑,是并非能出现这种情况;其实难题不大,因为此时标识符已经报错了,后面的标识符想执行也没机会。细节参见我的另一首诗:JS表达式封禁大法:薛定谔的X
以上,就是一个 let 引发的思考。
重要参考:JavaScript variables lifecycle: why let is not hoisted
————————-分割线————————-