前一章解释 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)
故事中的小本本是真实存在的! 在 JavaScript 的每个 Scope (不论区块、function 或全域),都有一个隐藏的 Object,它的 property 就是上述的那些资讯,但无法读或取到它,所以它是隐藏的 Object。
Lexical Environment 字汇环境内的资讯可以分成两部分:
记下了该范围内的变数、function 及参数 parameter,没错,连参数的值也会被记录在这,毕竟参数也可以算是个存有值的容器嘛
参照 Reference 是什麽呢? ;
故事中,为什麽在 🐰 小本本内不存在的 cookie 还是印得出来?那不是记录在 🐷 小本本吗?
原因就是,连结上一层环境的参照
当 引擎询问 🐰 cookie 的资讯时,因为 🐰 存有他的上一层环境 🐷 的参照,所以引擎可以往 🐷 的小本本内查找,而因为程序码层层包裹的结构,构成了内外环境的对应关系,这个就称为 Nested Scope
故事讲完了,我们来改画图解说
将这段程序码的字汇环境画出来会长得像下面这样,getCookie function 写在全域下,所以对 getCookie 来说,全域就是它的外部环境 outer environment,每个字汇环境都存有其外部字汇环境的 参照 reference,如图中的箭头指向了上一层的字汇环境,提供路径般供引擎可以层层往上查找,直到最末端的全域为止,而这一个指向一个的箭头像个链条连起每个 Scope,也就是 Scope Chain 的概念。
上述的例子只有提到两个 Scope,分别是 全域 Scope 及 function Scope,但在 JavaScript 还存有另一个更小的 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大家来找碴!直接帮你圈出哪里不一样
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。
let
和 const
宣告的变数会存在最近的 Scopevar
宣告的变数不会存在 Block Scope,而是最近的 function Scope 或 全域 Scope参考了很多文章和书籍,很怕讲的不完整,有很多不同的切入点做解释,每个知识点都细探分享的话这篇就不是主菜而是满汉全席,所以最终拍板决定这篇文着重在 Scope 字汇环境的部分,编译和 Execution Context的部分留到下一餐!
希望这样的讲解能让各位食客们好下咽
书籍:
[你所不知道的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
>>: [Day 16] 以 Programmatic 取代 Annotation 的方式撰写 OpenAPI 文件
《赖田捕手:番外篇》第 40 天:用 Netlify 整合前後端服务 故事还没完呢,阿部。你也许会想...
随着 Proxmox VE 上线後重要的客体机服务越来越多,如何确保所有重要客体机都有完整的备份就...
不要被标题一堆名词吓到;当你用过它後,你会惊讶它的易用以及,最重要的,无缝接轨辨识人脸关键点 本文...
为了方便检视这边我挑了6种颜色帮表格上色 cursor = conn.cursor() cursor...
什麽是 Web Notifications API? 透过 Web Notifications AP...