D6 - 你不知道 Combo : 主菜 Scope 字汇环境

前言

前一章解释 Hoising 时说到,JavaScript 一开始会先记下变数名称 和 Function Declaraion,因此可以做到提前呼叫 function 并印出结果。

但,JavaScript 到底是怎麽追踪这些资讯? 是怎麽纪录又纪录在哪里呢?

这一切的答案,都是有关 Scope 或称为 字汇环境 Lexical Environment,来到此系列的主菜
在寻找资料时,看到 你所不知道的JS 这本书以拟人化的方式模拟了引擎解读 code 的过程,觉得有趣又好理解,因此这篇文的开头决定也这种方式切入,绝对好入口
各位食客请用餐


程序小剧场

介绍一下即将出场的角色:

🤖 编译器 、💻 引擎、 Mr. 🐷 Scope、 Miss 🐰 Scope

对话如下:
🤖:在引擎开始上班前我要赶快先做好份内的工作!让我先扫扫这段程序码,哦,在第2行发现一个 var 的变数宣告,Mr. 🐷 Scope 请您纪录一个名为 cookie 的 变数
🐷:好的,记下了

🤖:(继续扫描) 在第 5 行发现了一个 Function Declaration,Mr. 🐷 Scope 请你记录一个名为 getCookie 的函数内容
🐷:好的,记下了

🤖:(继续扫描) 在第6行发现一个 var 的变数宣告, Miss 🐰 Scope 请你记录一个名为 number 的 变数
🐰:好的,记下了

🤖:搜查完所有的宣告,引擎先生,我的工作结束换你接手罗
💻:各位,现在开始正式执行程序!从第 1 行开始, console.log 要印出 cookie, Mr. 🐷 Scope 请问您知道 cookie 吗?
🐷:有,我这里记录它的值是 undefined
💻:谢谢,第一行印出 undefined。第 2 行赋予变数 cookie 值为字串 'Oreo',Mr. 🐷 Scope 请记录下这个值
🐷:好的,小本本已修改,cookie 初始化成功,从 undefined 改为 'Oreo'

💻:继续执行,第 3 行呼叫 getCookie, Mr. 🐷 Scope,请问你知道 getCookie 吗?
🐷:有,我这里记录它是一个 function,位置在第 5 行
💻:好的,前往第 5 行执行 getCookie function,哈罗, Miss 🐰 Scope 来到你的管辖区域罗!
🐰:好的好的
💻:我看到这里第 6 行是宣告一个变数 number,Miss 🐰 Scope 请去找到这个变数并将它的值纪录为 2
🐰:好的,小本本已修改,number 初始化成功从 undefined 改为 2

💻:第 7 行使用 console.log 要印出一串字串,需要知道两个变数的值,Miss 🐰 Scope 请问你知道 number 和 cookie 是什麽吗?
🐰:我的小本本上纪录,number 值是 2,但不知道 cookie,你可以问问我的上一层 Mr.🐷 Scope 那有没有记录
💻:好的,谢谢。Mr. 🐷 Scope 请问你知道 cookie 是什麽吗?
🐷:哦哦知道啊,我的小本本纪录 cookie 的值是 'Oreo'
💻:太棒了,两个变数的值都拿到,第 7 行成功印出 'Give me 2 pieces of Oreo'
💻:程序码执行结束,下班!


以上就是模拟 JavaScript 引擎在做的事情,其实隐含了很多底层执行的动作,像是编译及查找还可以探讨很多,但今天先着重在如何取得变数及 function 资讯这件事上。
从对话中发现,引擎在确认值的时候都是询问 🐰 🐷 ,有着一个小本本纪录着这些讯息,他们是谁??
可以称呼他们 Scope , 或是有个更正式的名称为 Lexical environment(也有人称 Lexical Scope)

字汇环境 Lexical Environment

故事中的小本本是真实存在的! 在 JavaScript 的每个 Scope (不论区块、function 或全域),都有一个隐藏的 Object,它的 property 就是上述的那些资讯,但无法读或取到它,所以它是隐藏的 Object。

Lexical Environment 字汇环境内的资讯可以分成两部分:

第一部分:该区域内的记录

记下了该范围内的变数、function 及参数 parameter,没错,连参数的值也会被记录在这,毕竟参数也可以算是个存有值的容器嘛

第二部分:连结上一层环境的参照 Reference

参照 Reference 是什麽呢? ;
故事中,为什麽在 🐰 小本本内不存在的 cookie 还是印得出来?那不是记录在 🐷 小本本吗?

原因就是,连结上一层环境的参照

当 引擎询问 🐰 cookie 的资讯时,因为 🐰 存有他的上一层环境 🐷 的参照,所以引擎可以往 🐷 的小本本内查找,而因为程序码层层包裹的结构,构成了内外环境的对应关系,这个就称为 Nested Scope

巢状 Nested Scope

故事讲完了,我们来改画图解说

将这段程序码的字汇环境画出来会长得像下面这样,getCookie function 写在全域下,所以对 getCookie 来说,全域就是它的外部环境 outer environment,每个字汇环境都存有其外部字汇环境的 参照 reference,如图中的箭头指向了上一层的字汇环境,提供路径般供引擎可以层层往上查找,直到最末端的全域为止,而这一个指向一个的箭头像个链条连起每个 Scope,也就是 Scope Chain 的概念。

上述的例子只有提到两个 Scope,分别是 全域 Scope 及 function Scope,但在 JavaScript 还存有另一个更小的 Scope 范围 - 区块 Block Scope

区块 Block Scope

来看看另一个更复杂的结构,直接将各个 console.log 印出的结果备注在後面,比较一下跟你心里想的答案是不是一样?

开始来看看每个 Scope ,第 2 行的 cookie 可以成功在全域的字汇环境中找到,顺利印出 Oreo,没问题

再来进入到 countCookie function 的范围内,首先是第 6 行,如果以为会印出 Oreo 那就不对了!你可能会混乱以为这时引擎会往上查找到全域 Scope,但别忘了,这个 function 区域也有个 cookie 宣告,编译器会留意到这个宣告,在执行前将它存入 countcookie 的字汇环境中,因此不需要往外查找,只是尚未初始化前 countcookie Scope 只知道 cookie 的名字但不知道它的值(忘了请看上一章),这时要印出它当然抛出错误!

经过第 7 行赋值初始化成功後,第 8 行就可以印出 Ritz 了。
继续往下,这时碰到 for loop

它是 function Scope 内的 Block Scope,将第 6 行的解释同理套用到第 11 行的错误讯息,此时 Block Scope 字汇环境内的 cookie 尚未初始化,一样也是抛出错误。

好的,让我们将会产生错误讯息的 code 拿掉然後将字汇环境画出来


注意:i:1 为回圈刚开始的值,随着每一圈重新赋与新的数字

这样对照着看是不是就很清楚每个 scope 内可以取到什麽值
那,若今天的所有宣告改为 var 有什麽不一样吗?

var 宣告下的区块 Block Scope

大家来找碴!直接帮你圈出哪里不一样

  1. 第 6 行印出 undefined
  2. 第 11 行不是抛出错误讯息,随着回圈第一次印出 Ritz,第二次後印出 Lays
  3. 第 15 行印出 Lays
  4. 字汇环境图中的 Block Scope 内容跑到 Function Scope 内了

var 就是这麽的出奇不意,第 6 行同刚刚 let 的解释一样,因为在 function scope 内也有 cookie 变数宣告,因此执行到第 7 行的赋值前,字汇环境记录的值还是 undefined。
那第 11 行呢? 为什麽不是 undefined,而是可以印出值呢?

答案就是:var 无视 Block Scope,它的宣告只会存在最近的 function Scope 内或是全域 Scope

所以当程序执行到第 11 行时,在 Block Scope 内查无 cookie 这个变数,便会循着 refernce 往外层的 countCookie function Scope 查找,此时的 cookie 值是 Ritz,因此第一次便会印出 Ritz,接下来程序执行到第 12 行,cookie 被 重新赋值为 Lays,此时 countCookie 的字汇空间修改 cookie 的值为 Lays,所以第 13 行以及随着回圈重新来到的第 11 行,都会印出 Lays

而 第 15 行也是一样的结论,由於第 12 行的关系,countCookie Function Scope 的 cookie 重新赋值为 Lays,所以跟 let 举例的结果不同,这里会印出 Lays。

整理了 Block Scope 字汇环境下不同的宣告差异:

  • letconst 宣告的变数会存在最近的 Scope
  • var 宣告的变数不会存在 Block Scope,而是最近的 function Scope 或 全域 Scope

结论

参考了很多文章和书籍,很怕讲的不完整,有很多不同的切入点做解释,每个知识点都细探分享的话这篇就不是主菜而是满汉全席,所以最终拍板决定这篇文着重在 Scope 字汇环境的部分,编译和 Execution Context的部分留到下一餐!

希望这样的讲解能让各位食客们好下咽
/images/emoticon/emoticon25.gif

Reference:

书籍:
[你所不知道的JS] by Kyle Simpon
[忍者 JavaScript 开发技巧探秘第二版] by John Resig, Bear Bibeault, Josip Maras

文章:
How Lexical Environments affect JavaScript Variables, Hoisting & Closures
Hoisting in JavaScript
我知道你懂 hoisting,可是你了解到多深?
Hoisting in JavaScript


<<:  Watch

>>:  [Day 16] 以 Programmatic 取代 Annotation 的方式撰写 OpenAPI 文件

《赖田捕手:番外篇》第 40 天:用 Netlify 整合前後端服务

《赖田捕手:番外篇》第 40 天:用 Netlify 整合前後端服务 故事还没完呢,阿部。你也许会想...

Proxmox VE 建立排程备份及制订保留策略

随着 Proxmox VE 上线後重要的客体机服务越来越多,如何确保所有重要客体机都有完整的备份就...

[Day11] Face Detection - 使用OpenCV & Dlib:Dlib HOG + Linear SVM

不要被标题一堆名词吓到;当你用过它後,你会惊讶它的易用以及,最重要的,无缝接轨辨识人脸关键点 本文...

DAY 29 制作表格-为表格上色

为了方便检视这边我挑了6种颜色帮表格上色 cursor = conn.cursor() cursor...

Progressive Web App Notifications API (21)

什麽是 Web Notifications API? 透过 Web Notifications AP...