我用了两个月的时间才理解 let

2023-01-13 0 497

总之并非Bourbonne完备的三个月天数来认知 let,而要Revigny地认知,与此同时我更要想著是并非写下一首诗把那个事说确切。

在 let 刚出的这时候,我就「误以为」我认知了 let。然鹅在往后的三个月里,我对 let 的认知出现了峰回路转的变动。

我写这首诗,是期望我的自学操作过程,能对你自修有协助。

贝唐 let

跟许多人那样,我第二次介绍 let 的优点从 MDN 的文件格式:

我用了两个月的时间才理解 let

我获得的重要信息有那么四条:

let 新闻稿的变量的返回值是块级的;let 无法多次重复新闻稿已存有的表达式;let 有暂死区,无法被提高。

绝大部分人如果都是那么指出的,我也是那么认知的。

那个认知「没难题」,但不如「全面性和真切」。

第二次批评

我第二次批评我的认知是在碰到 for 循环式的这时候,标识符如下表所示。

// 程序标识符1 var liList = document.querySelectorAll(li) // 共5个li for( var i=0; i<liList.length; i++){ liList[i].onclick = function(){ console.log(i) } }

大家都知道依次点击 li 会打印出 5 个 5。如果把 var i 改成 let i,就会分别打印出 0、1、2、3、4

// 程序标识符2 var liList = document.querySelectorAll(li) // 共5个li for( let i=0; i<liList.length; i++){ liList[i].onclick = function(){ console.log(i) } }

然而,用我之前的知识来认知那个标识符是无法自圆其说的。因为标识符中依然只新闻稿了一个 i,在 for 循环式结束后,i 的值还是会变成 5 才对。

这说明我的认知有误。

于是我去看 MDN 的例子,发现鸡贼的 MDN 巧妙地避开了那个难题,它的例子是这样的:

我用了两个月的时间才理解 let

你看 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可以近似近似近似地认知为

// 程序标识符3 var liList = document.querySelectorAll(li) // 共5个li for( let i=0; i<liList.length; i++){ let i = 隐藏返回值中的i // 看这里看这里看这里 liList[i].onclick = function(){ console.log(i) } }

那样的话,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),都存有提高,理由是如下表所示标识符:

x = “global”; // function scope:(function() { x; // not “global” var/let/ x; }()); // block scope (not for `var`s): { x; // not “global” let/const/ x; }

我觉得他说得挺有道理的。于是我又去 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 新闻稿的「创建、初始化和赋值」操作过程

假设有如下表所示标识符:

function fn(){ var x = 1 var y = 2 } fn()

在执行 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 新闻稿的「创建、初始化和赋值」操作过程

假设标识符如下表所示:

fn2() function fn2(){ console.log(2) }

JS 引擎会有一下过程:

找到所有用 function 新闻稿的表达式,在环境中「创建」这些表达式。将这些表达式「初始化」并「赋值」为 function(){ console.log(2) }。开始执行标识符 fn2()

也就是说 function 新闻稿会在标识符执行之前就「创建、初始化并赋值」。

接下来看 let 新闻稿的「创建、初始化和赋值」操作过程

假设标识符如下表所示:

{ let x = 1 x = 2 }

我们只看 {} 里面的操作过程:

找到所有用 let 新闻稿的表达式,在环境中「创建」这些表达式开始执行标识符(注意现在还没初始化)执行 x = 1,将 x 「初始化」为 1(这并并非一次赋值,如果代码是 let x,就将 x 初始化为 undefined)执行 x = 2,对 x 进行「赋值」

这就解释了为什么在 let x 之前使用 x 会报错:

let x = global { console.log(x) // Uncaught ReferenceError: x is not defined let x = 1 }

原因有三个

console.log(x) 中的 x 指的是下面的 x,而并非全局的 x执行 log 时 x 还没「初始化」,所以无法使用(也就是所谓的暂死区)

看到这里,你如果明白了 let 到底有没提高:

let 的「创建」操作过程被提高了,但初始化没提高。var 的「创建」和「初始化」都被提高了。function 的「创建」「初始化」和「赋值」都被提高了。

接下来我考考你:

var foo 和 function foo 与此同时存有会怎样

标识符1:

var foo function foo(){} console.log(foo)

标识符2:

function foo(){} var foo console.log(foo)

请问标识符1 和 标识符2 的输出分别是什么?

答案:由于 function 比 var 多一个「赋值」操作过程,所以三个标识符的输出都是函数。你也可以记住结论:function 比 var 牛逼。

那如果 function foo 和 let foo 与此同时出现呢?无法有这种情况的,因为 let 发现重名就会报错,叫你滚去改标识符。

const

最后看 const,其实 const 和 let 只有一个区别,那就是 const 只有「创建」和「初始化」,没「赋值」操作过程。

这四种新闻稿,用下图就可以快速认知:

我用了两个月的时间才理解 let

所谓暂时死区,就是无法在初始化之前,使用表达式。

完了吗?

故事依然没结束,这周我在知乎上问了一个难题:如何认知 let x = x 报错之后,再次 let x 依然会报错?(那个难题是饥人谷的学生问我的)

我用了两个月的时间才理解 let

那个难题说明:如果 let x 的初始化操作过程失败了,那么

x 表达式就将永远处于 created 状态。你无法再次对 x 进行初始化(初始化只有一次机会,而那次机会你失败了)。由于 x 无法被初始化,所以 x 永远处在暂死区(也就是盗梦空间里的 limbo)!有人会觉得 JS 坑,是并非能出现这种情况;其实难题不大,因为此时标识符已经报错了,后面的标识符想执行也没机会。

细节参见我的另一首诗:JS表达式封禁大法:薛定谔的X

以上,就是一个 let 引发的思考。

重要参考:JavaScript variables lifecycle: why let is not hoisted

————————-分割线————————-

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务