<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          作用域與閉包 - 最簡解釋器實(shí)現(xiàn)

          共 6574字,需瀏覽 14分鐘

           ·

          2020-06-09 23:21

          (給前端大學(xué)加星標(biāo),提升前端技能.

          作者:fedeoo

          https://fedeoo.github.io/learning-ecma262/01 - Scope & Closure.html

          張飛,叫fedeoo,畢業(yè)之后工作7年一直專注前端,在新浪和阿里工作一段時(shí)間,現(xiàn)在在澳洲工作。

          正文從這開始~~

          如果你對 JS 閉包的實(shí)現(xiàn)有興趣,就跟著這篇文章一起實(shí)現(xiàn)這一個(gè)最簡單的解釋器來重新認(rèn)識(shí)一下閉包。

          開始之前

          如果是從頭開始實(shí)現(xiàn)一個(gè)解釋器,避免不了從詞法分析和語法分析開始。我們對這部分工作沒有興趣,所以直接使用現(xiàn)有的分析器 acorn 。acorn 是一款比較流行的分析器,babel 底層依賴的就是 acorn。另外,esprima 也是一個(gè)不錯(cuò)的選擇。這兒我們選擇 acorn,還有一個(gè)經(jīng)常會(huì)用到的在線工具 astexplorer。acorn 解析得到的抽象語法樹符合 estree 的規(guī)范,項(xiàng)目中也依賴了 @types/estree。

          在此之前,希望你大概明白 AST 抽象語法樹是怎么回事,知道如何遍歷一棵樹。

          這兒選擇 Typescript,在開發(fā)中也隨時(shí)可以查看特定節(jié)點(diǎn)的數(shù)據(jù)類型,代碼也比較容易閱讀。測試使用 Jest,運(yùn)行起來比較簡單。即使并不追求 TDD,測試也可以幫助我們持續(xù)迭代。建議配置在 IDE 中運(yùn)行和調(diào)試測試。

          算術(shù)表達(dá)式解析

          我們從最簡單的算術(shù)表達(dá)式開始,這節(jié)的目的就是對算術(shù)表達(dá)式進(jìn)行求值:給定任意的算術(shù)表達(dá)式,形如 1 + 2 * 3 - 4 / 5 我們能夠正確得到結(jié)果。

          AST 抽象語法樹

          在開始之前,我們需要先打開 astexplorer,對 AST 有一個(gè)感性的認(rèn)識(shí)。當(dāng)左側(cè)的表達(dá)式為 1 + 2 時(shí),右側(cè)的得到的 AST 是這樣子的 (這兒以 JSON 形式表示):

          1. {

          2. "type":"Program",

          3. "body":[

          4. {

          5. "type":"ExpressionStatement",

          6. "expression":{

          7. "type":"BinaryExpression",

          8. "left":{

          9. "type":"Literal",

          10. "value":1

          11. },

          12. "operator":"+",

          13. "right":{

          14. "type":"Literal",

          15. "value":2

          16. }

          17. }

          18. }

          19. ]

          20. }

          這兒移除了標(biāo)注代碼位置的數(shù)據(jù),以及字面量的 raw 屬性,其中 raw 表示原始字符串。如果將右操作數(shù)改為 16 進(jìn)制形式 1 + 0x2,我們就會(huì)發(fā)現(xiàn) raw 為 '0x2'。這棵語法樹根節(jié)點(diǎn)表示一段程序,其 body 屬性是一個(gè)數(shù)組表示可能有很多條表達(dá)式語句。對于二元表達(dá)式來說,有左右兩個(gè)節(jié)點(diǎn)。這兒的左右節(jié)點(diǎn)是字面量。我們改動(dòng)一下左側(cè)內(nèi)容為 1 + 2 * 3 我們看到,右側(cè)的節(jié)點(diǎn)成為了一個(gè)二元表達(dá)式,而這個(gè)新的二元表達(dá)式的兩個(gè)左右節(jié)點(diǎn)分別為字面量 2 和 字面量 3。

          代碼實(shí)現(xiàn)

          我們給自己的解釋器起一個(gè)名字,隨便起一個(gè)就叫 Z2(V8)吧。我們可以先寫一個(gè)簡單的測試:

          1. it('should return sum when adding two numbers',()=>{

          2. const z2 =new Z2();

          3. expect(z2.run('3 + 4')).toBe(7);

          4. });

          我們借助 acorn 得到 AST,之后遍歷這棵語法樹得到執(zhí)行結(jié)果就可以了。對每個(gè)節(jié)點(diǎn)類型,我們在一個(gè)單獨(dú)的函數(shù)中處理。整個(gè)的文件結(jié)構(gòu):

          1. import{Parser}from'acorn';

          2. class Z2 {

          3. run(code:string){

          4. const program:Node=Parser.parse(code);

          5. returnthis.evaluate(program);

          6. }


          7. evaluate(node:ESTree.Node){

          8. switch(node.type){

          9. case'Program':

          10. returnthis.evaluateProgram(node);

          11. case'ExpressionStatement':

          12. returnthis.evaluateExpressionStatement(node);

          13. case'BinaryExpression':

          14. returnthis.evaluateBinaryExpression(node);

          15. case'Literal':

          16. returnthis.evaluateLiteral(node);

          17. default:

          18. thrownewError(`Unknown node type: ${node.type}`);

          19. }

          20. }

          21. }

          現(xiàn)在我們只需要實(shí)現(xiàn)單獨(dú)的各個(gè)節(jié)點(diǎn)處理函數(shù)就可以了。各個(gè)節(jié)點(diǎn)只關(guān)注當(dāng)前的節(jié)點(diǎn)操作就可以了,對 evaluateBinaryExpression 來說,我們需要遞歸的調(diào)用 this.evaluate 以對子節(jié)點(diǎn)進(jìn)行求值。

          1. evaluateBinaryExpression(node:ESTree.BinaryExpression){

          2. const{operator}= node;

          3. const left =this.evaluate(node.left);

          4. const right =this.evaluate(node.right);

          5. switch(operator){

          6. case'+':

          7. return left + right;

          8. case'-':

          9. return left - right;

          10. case'*':

          11. return left * right;

          12. case'/':

          13. return left / right;

          14. default:

          15. thrownewError(`Unknown operator: ${node.type}`);

          16. }

          17. }

          而字面量的處理就更簡單了

          1. evaluateLiteral(node:ESTree.Literal){

          2. return node.value;

          3. }

          現(xiàn)在可以可以測試一下,對于混合的算術(shù)運(yùn)算,Z2 依然返回正確的結(jié)果。

          1. it('should return correct value given mixed math expression',()=>{

          2. const z2 =new Z2();

          3. expect(z2.run('3 + 4 * 2')).toBe(11);

          4. expect(z2.run('8 + 12 / 3 + 4 * 2')).toBe(20);

          5. expect(z2.run('1 + 2 * 3 - 4 / 5')).toBe(6.2);

          6. });

          當(dāng)然即使有括號(hào)改變優(yōu)先級(jí),一樣不會(huì)影響程序的正確性,因?yàn)?acorn 已經(jīng)給我們返回了正確的語法樹。

          1. it('should return correct value given expression include parentheses',()=>{

          2. const z2 =new Z2();

          3. expect(z2.run('(3 + 4) * 2')).toBe(14);

          4. });

          變量和環(huán)境

          這次我們嘗試引入變量,我們的代碼也很簡單。我們可以添加一條新的測試用例:

          1. it('should calculate correct value given expression with variable',()=>{

          2. const z2 =new Z2();

          3. expect(z2.run(`

          4. var a = 3;

          5. a * 9;

          6. `)).toBe(27);

          7. });

          我們在 astexplorer 查看一下這兩個(gè)代碼的語法樹:第一部分是變量聲明;第二部分的表達(dá)式?jīng)]有太大變化,只是左側(cè)的操作數(shù)類型是標(biāo)識(shí)符 { type: 'Identifier', name: 'a' },右側(cè)依然是字面量。也就是說在對表達(dá)式求值時(shí),我們知道的只有標(biāo)識(shí)符的名稱 a,所以,我們需要從一個(gè)地方獲取到標(biāo)識(shí)符的值,簡單的理解為變量環(huán)境 Environment?,F(xiàn)在這個(gè)變量環(huán)境只是一個(gè)符號(hào)表,即便如此,我們還是創(chuàng)建一個(gè)單獨(dú)的類吧。

          1. classEnvironment{

          2. private symbolTable ={};


          3. set(name, value){

          4. this.symbolTable[name]= value;

          5. }


          6. get(name:string){

          7. returnthis.symbolTable[name];

          8. }

          9. }

          在 Z2 run() 中每次運(yùn)行代碼之前,都先創(chuàng)建一個(gè)新的環(huán)境 this.env = new Environment();。接下來的工作就是添加對變量聲明和標(biāo)識(shí)符的處理函數(shù),

          1. evaluateVariableDeclaration(node){

          2. node.declarations.forEach((declaration)=>{

          3. if(declaration.id.type ==='Identifier'){

          4. this.env.set(declaration.id.name,this.evaluate(declaration.init));

          5. }

          6. });

          7. }

          變量聲明是一個(gè)數(shù)組因?yàn)槲覀兛梢酝瑫r(shí)聲明多個(gè)變量,這兒我們只處理左側(cè)類型是標(biāo)識(shí)符的變量聲明。暫時(shí)不考慮形如 const { name, id } = user; 復(fù)雜的聲明。我們做的也很簡單,只是將變量值存放在環(huán)境中。至于標(biāo)識(shí)符的處理就更簡單了,只需讀取變量的值。

          1. evaluateIdentifier(node:ESTree.Identifier){

          2. returnthis.env.get(node.name);

          3. }

          同樣的方式,我們添加對賦值語句的支持,同時(shí)添加一些測試用例。

          函數(shù)和閉包

          接下來我們要引入函數(shù)和閉包。我們先來考察一下最最簡單的函數(shù)聲明和調(diào)用:

          1. var a =3;

          2. function outer(){

          3. var a =5;

          4. return a;

          5. }

          6. outer();

          相比較于變量聲明,函數(shù)聲明包含的屬性有點(diǎn)多,不過我們先只關(guān)注最基本的情況,其中最重要的是參數(shù) params 和函數(shù)體 body,暫時(shí)不考慮 params,在解釋函數(shù)聲明時(shí),同樣的我們需要把函數(shù)存放到上下文中, 簡單起見,我們只是把整個(gè)節(jié)點(diǎn)存放進(jìn)去。對于函數(shù)調(diào)用,這是一個(gè) CallExpression 我們需要做的就是根據(jù)標(biāo)識(shí)符名稱找到我們存放的函數(shù)對象(現(xiàn)在只是一個(gè)節(jié)點(diǎn)),然后解釋執(zhí)行函數(shù)。在上面的例子中,兩個(gè)同名變量是不同的,函數(shù)內(nèi)部是一個(gè)單獨(dú)的環(huán)境。在進(jìn)入函數(shù)之前,我們需要?jiǎng)?chuàng)建一個(gè)新的環(huán)境,將當(dāng)前環(huán)境設(shè)置為新環(huán)境,在函數(shù)調(diào)用結(jié)束之后,我們需要恢復(fù)原來的環(huán)境。

          實(shí)現(xiàn)代碼如下,同時(shí)我們還要添加對 ReturnStatement 和 BlockStatement 的處理函數(shù)。

          1. evaluateFunctionDeclaration(node:ESTree.FunctionDeclaration){

          2. this.env.init(node.id.name, node);

          3. }

          4. evaluateCallExpression(node:ESTree.CallExpression){

          5. const fnNode =this.env.get(node.callee.name);

          6. const env =newEnvironment();

          7. const currentEnv =this.env;// 保存調(diào)用者環(huán)境

          8. this.env = env;

          9. const result =this.evaluate(fnNode.body);

          10. this.env = currentEnv;// 恢復(fù)調(diào)用者環(huán)境

          11. return result;

          12. }

          閉包的處理

          我們希望函數(shù)內(nèi)部可以訪問函數(shù)外部的變量,我們添加一個(gè)測試用例:

          1. it('should be able to access outer variable in function',()=>{

          2. const z2 =new Z2();

          3. expect(z2.run(`

          4. var a = 3;

          5. function outer() {

          6. return a;

          7. }

          8. outer();

          9. `)).toBe(3);

          10. });

          在上面的實(shí)現(xiàn)中,我們創(chuàng)建的 Environment 是一個(gè)全新的。為了訪問到外部的環(huán)境變量,我們做需要一點(diǎn)點(diǎn)的變通:把當(dāng)前的環(huán)境作為外部環(huán)境引用傳進(jìn)去。const env = new Environment(this.env); 我們的思路是:在查找標(biāo)識(shí)符的時(shí)候,也就是 Environment.get(name) 函數(shù)實(shí)現(xiàn)中,先在當(dāng)前的環(huán)境查,如果查找不到,我們就在外部查。修改后的 Environment 類:

          1. classEnvironment{

          2. private outer:Environment;


          3. private symbolTable ={};


          4. constructor(outer:Environment=null){

          5. this.outer = outer;

          6. }


          7. set(name, value){

          8. this.symbolTable[name]= value;

          9. }


          10. get(name){

          11. if(this.symbolTable[name]!==undefined){

          12. returnthis.symbolTable[name];

          13. }

          14. if(this.outer){

          15. returnthis.outer.get(name);

          16. }

          17. returnundefined;

          18. }

          19. }

          修改代碼通過上面的測試用例。

          現(xiàn)在我們思考一下在我們創(chuàng)建一個(gè)新的變量環(huán)境時(shí),是否應(yīng)該傳入當(dāng)前的變量環(huán)境。的確,我們這樣做通過了測試。但是,我們得思考一下,這意味著什么?這意味著,一個(gè)函數(shù)中的變量依賴于調(diào)用者環(huán)境。而一個(gè)函數(shù)可能在幾十個(gè)地方被調(diào)用,而我們根本無法得知函數(shù)里面的外部環(huán)境變量究竟是在哪兒定義的。

          我們來看看實(shí)際當(dāng)中我們希望的外部環(huán)境是什么?現(xiàn)在我們添加一個(gè)閉包的測試用例,當(dāng)然這個(gè)測試現(xiàn)在不會(huì)通過。

          1. var b =5;

          2. function outer(){

          3. var b =10;

          4. function inner(){

          5. var a =20;

          6. return a + b;

          7. }

          8. return inner;

          9. }

          10. var fn = outer();

          11. fn();

          我們對閉包并不陌生,上面這個(gè)的這個(gè)例子中,fn 只是 inner 的引用,在我們調(diào)用 fn 時(shí),我們希望外部環(huán)境是 inner 所在的環(huán)境,也就是說我們希望將函數(shù)聲明時(shí)的環(huán)境作為外部環(huán)境。修改代碼,在函數(shù)聲明時(shí)保存整個(gè)函數(shù)和當(dāng)時(shí)的變量環(huán)境。

          1. evaluateFunctionDeclaration(node:ESTree.FunctionDeclaration){

          2. this.env.init(node.id.name,this.createFunction(node));

          3. }

          4. createFunction(node){

          5. return{

          6. parentEnv:this.env,

          7. node,

          8. };

          9. }

          在函數(shù)調(diào)用時(shí)將其作為外部環(huán)境

          1. const fn =this.env.get(node.callee.name);

          2. const env =newEnvironment(fn.parentEnv);

          修改代碼通過上面的測試用例。現(xiàn)在我們再來看閉包的話,我們發(fā)現(xiàn)所謂的閉包就是函數(shù)和它所在的環(huán)境。完整的代碼見 https://github.com/fedeoo/learning-ecma262/tree/master/src

          總結(jié)

          閉包已經(jīng)被大家講爛了,個(gè)人覺得理解一個(gè)東西最好的辦法就是實(shí)現(xiàn)一遍。簡單的解釋器非常容易實(shí)現(xiàn)(如果不需要做語法分析),建議大家讀完之后自己實(shí)現(xiàn)一遍,也就 100 多行代碼。這兒并沒有遵照 ECMAScript 規(guī)范來實(shí)現(xiàn),因?yàn)橐氩簧俑拍睿热邕\(yùn)行時(shí)返回的是 CompletionRecord 類型,解析標(biāo)識(shí)符得到的是 Reference 類型。如果想嚴(yán)格的按照規(guī)范實(shí)現(xiàn)或者學(xué)習(xí)規(guī)范,可以參考項(xiàng)目 engine262。針對想從頭開始寫解釋器的同學(xué),建議閱讀 esprima 源碼。除非你是在學(xué)編譯原理,跟人覺得沒有必要從頭開始,事實(shí)上已經(jīng)有不錯(cuò)的工具,根據(jù)產(chǎn)生式來直接生成語法樹了。

          分享前端好文,點(diǎn)亮?在看?e9372a22d3a66bd45465a96e88960435.webp

          瀏覽 28
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  97激情| 永久天堂 | 在线观看免费无码 | 怡红院爽妇网 | 性欧美性爱豆花视频 |