javascript
为脚本语言平反-JavaScript篇(3)
http://blog.csdn.net/aimingoo/archive/2009/09/08/4532496.aspx
(書接上回,繼續!)
?
?
五、這個DSL框架有什么問題?
=============
有什么問題嗎?有一點,并不嚴重。比如說,我們在Env中聲明了一些屬性和方法。對于Env這個對象
Env = { max: 100, min: -3, calc: function(adj) { … } }
?
我們要在calc()方法中訪問max/min屬性,應該寫成“this.max/this.min”,這一則是不方便,另外,在用戶的dsl代碼中還不得不考慮“當前this是誰”的問題。這是問題之一。
?
第二個,我們傳入了一個evaluator(),相當于腳本執行器,那么我們能不能在dsl()代碼中也使用這個執行器呢?也就是說,我們的dsl不單是“domain-specific language”,也可以是一個“domain-script language”的。
?
第三個問題,我們是不是需要一個類似在JavaScript中的window對象的東西,以便能引用到執行環境的全局。
?
這三個問題都應該是在DSL()層面解決的。簡單說來,第二、三個問題,實質是在初始化環境environment,使之具有某些在dsl代碼中能訪問到的性質。所以很容易處理:
function DSL(environment, evaluator, parser) { var dsl = Scope(environment, Weave.call(evaluator, /^/, Block(parser, ‘body’)+’/n/n’)); environment.system = environment; environment[Block(evaluator, 'Name')] = dsl; return Owner(environment, dsl); }
?
同理的,用戶可以在上面這里對environment加入更多性質,這些都是可以在用戶的dsl(…)中訪問到的。以上面為例,當用戶傳入的執行器evaluator是一個具名函數的時候,則該函數名會成為dsl(…)環境中的可用的執行函數(類似于exec, execScript或eval等)。例如:
function myeval() { … } dsl = DSL(aEnv, myeval, aParser); dsl(function(){ myeval(…); });
注意在dsl()訪問到的myeval()方法,其實不是用戶原始的myeval(),而是上述dsl變量的一個引用。這個,從DSL()函數的實現中可以看到。
?
接下來,就是上面三個問題中的第一個,亦即是在calc()方法從必須使用this.max/this.min的問題。事實上,這是因為聲明calc方法的時候,該函數位于Env變量所在的全局閉包里面。這樣,它就默認只能訪問到全局的變量、標識符。所以,解決這個問題的方法,仍然和前面一樣:改變它的閉包位置——使用Scope()函數。如下:
function DSL(environment, evaluator, parser) { var dsl = Scope(environment, Weave.call(evaluator, /^/, Block(parser, ‘body’)+’/n/n’)); for (var n in environment) { if (environment[n] instanceof Function) environment[n] = Scope(environment, environment[n]); } … }
?
現在有了一個新的、完善的DSL()。使用方法與前面是一致的。比如:
Env = { max: 100, calc: function(adj) { return max + adj }, //可以直接訪問max了 show: function(msg) { alert(msg) } }; dsl = DSL(Env, myeval, myparser); dsl(function() { show(calc(30)); //顯示130 });
?
最后,留意一下當調用DSL()的時候,我們標出了”Env”這個全局變量。注意的是,我們直接使用了這個對象。那么它與使用Unique(Env)有什么不同呢?答案是,直接使用Env時,在dsl(…)中的代碼可以直接修改到Env中的成員,而如果使用Unique(Env),則dsl(…)中的代碼只會修改到Env的一個副本。這樣一來,我們就有機會為不同的dsl語言提供各各獨立的環境了——這有點象沙箱。
?
?
六、變量泄漏?
========
在JavaScript語言中有一個“根深蒂固”的問題,就是“當在函數內訪問一個不存在的變量時,引擎會試圖在全局變量環境中打找該變量”。這通常是很多很多爛系統的根源。對于我們上面的dsl語言來說,系統其實只給出了五個標識符:max/calc/show/system/myeval。其中的后面兩個,是DSL()函數在“語言引擎層面”提供的,其它的則是Env環境變量提供的。“變量泄漏”帶來的直接問題是,對于上面的這個例子,dsl(…)中除了能訪問這五個標識符之外,還能訪問全局的window/String/Number/Math/RegExp/NaN等等預定義對象和屬性。而這,可能根本就不是我們的dsl語言需要的。
?
這怎么辦呢?
?
由于Unique()得到了Env環境對象的一個副本,而且在dsl(…)中無法通過這個副本來修改原始的Env的成員,也不能delete它。所以如果我們在Env的屬性中加入這些“受保護的標識符”,那么dsl(…)就只能訪問到Env的這些屬性,而不會訪問到全局里面的了。下面的代碼簡單地實現這一效果:
Env = { … }; protoected = ['window', 'setTimeout', 'setInterval', //window和Global的成員... 'Array', 'Object', 'Function', // 全局的對象構造器... 'null', 'undefined', //引擎定義的,類似系統關鍵的... 'Env', 'tinyParser', 'dsl', 'myeval' //用戶代碼環境中的... ]; protoected.forEach(function(item) { this[item] = undefined}, Env); dsl = DSL(Unique(Env), myeval, myparser); dsl(function() { show(Array); // 顯示undefined Array = ‘local defined’; show(Array); // 顯示local defined });
?
?
七、evaluator/parser是不是太簡單了?
=================
當然。我們在evaluator, parser中基本什么也沒有做,當然是相當簡單的。如果你要做一個完整的DSL,那么你得花一些工夫來做語法解析,并實現在語法樹的基礎上的代碼執行、運行環境的維護等等。我QoBean的DSL()中,主要是提供了一個運行你的代碼的基礎語言環境,有點象是——嗯——沙箱。
當然,除了沙箱的基本功能之外。DSL()通過environment來維護給用戶代碼的一組基本標識符(或稱為保留字),并保證用戶在不同的environment之間不會相互影響。
?
除了上述的基本描述之外,我們最后再關注一下evaluator和parser的實現。對于下面的代碼:
function myeval(source) { return eval(source); } function myparser(source){ source = Block(source); } dsl(function() { show(min+max); show(calc(min+max)); });
實際上的效果是dsl()將紅色顯示部分的函數作為一個一個參數source,傳入myparser()和myeval()。parser通過Block()取出這個函數代碼的body部分,然后交給myeval()中的eval()函數執行。也就是說,我們在DSL()中調用Weave()的效果就是,將myparser()和myeval()并在一起,變成了:
function(source) { source = Block(source); return eval(source); }
?
而dsl()最終執行的就是上面這個匿名函數。更進一步,在environment上也會有一個名為’myeval’的方法,指向這個匿名函數。
?
但是,首先這里就有一個不小的問題:’source’在這里也是一個標識符。在eval(…)中執行時,代碼是可以感知到這個標識符的——而對于dsl(…)中的用戶代碼,source可能是另外需要的一個標識符,所以這里我們要想辦法屏蔽掉對這個變量名的依賴。這其實處理起來很簡單:
function myeval(source) { return eval(arguments[0]); } function myparser(source){ arguments[0] = Block(arguments[0]); }
?
你應該注意到,我們用arguments[0]就可以簡單地繞過一個入口參數名的使用了。這個,很簡單,也很實用。
?
接下來,我們總不能要求用戶每次執行dsl(…)時都要傳入一個函數吧?我們最終聲明的用戶的DSL可能是相當怪異的、完全不符合JS的語法的,根本就不能寫到一個函數中去,又該怎么辦呢?這個問題,顯然的——首先的——他該是parser的問題。因此我們也就簡單地講一下擴充myparser()的方法。比如說,我們想實現下面的效果:
例如如下的調用:
=========
// 示例1 dsl(”/ apple.more->hi(form) % / tree.clear+>do(function() .. ). / “); //示例2 dsl(function(){/* apple.more->hi(form) % tree.clear+>do(function() .. ). */});
?
?
現在我們需要進一步完善我們的myparser(),提供一個基本的模式來支持這種設計。簡單的方法如下:
function tinyParser(){ switch (typeof arguments[0]) { case ‘function’: arguments[0] = Block(arguments[0]); arguments[0] = arguments[0].replace(/^/s*///*([/d/D]*)/*///s*$/, ‘$1′); break; } /* 現在你需要 1、對字符串arguments[0]進行語法分析,形成語法樹或符號某種規則的代碼塊, 2、將結果傳回arguments[0]。 */ }
?
當然,由于代碼的語法規則改變了,所以myeval()的設計也應該發生相應的變化了。而這些,就應該是DSL語言設計者的工作,而不是QoBean在DSL()框架上要考慮的事情了。
?
?
八、終結:DSL,關鍵不在用什么語言實現,而在于為什么Domain設計什么樣的語言
=============
我們用Javascript,只寫了不到了10行代碼,就實現了一個DSL()的通用框架,但是,我們卻沒有做出對任何一個真實的Domain有意義的DSL。對于Ruby、Python、Erlang還是Scala,或者更原始的LISP或更新的F#這些基礎語言,對他們的選擇更多的只是喜好或者出于某些局部的優異與方便的考慮,與我們“設計一個DSL”是沒有多大的關系的。一個DSL的設計,在于對領域的、領域相關業務的分析與抽象。在這些分析、抽象的基礎上,進行語法設計、語義定義,最終才表現為“怎樣的一個語言”。當我們看到這個“表現”的時候,整個DSL的設計都已經結束了——我們接下來只需要構建基本運行庫(runtime library),以及其上的應用邏輯就好了。所以,大多數看到某個DSL的人,只是它的實現者和使用者,而不是它的設計者。多數人只是埋頭于使用,或者激情于評說,而忘了看看“一個具體DSL的背景”。
?
例如,難道DOS批處理不是一個DSL嗎?10行的JavaScript難道不就是一個完整的DSL framework嗎?如果是,那么我們還有必要討論“什么是DSL”,以及“怎樣的DSL開發環境更好”的問題嗎?我們是不是看看“我們在什么Domain”,以及“這個Domain如何描述、如何結構化和如何邏輯驅動之”,這些問題是不是才是更關鍵的?
?
上面兩個示例中都有一個相同的dsl代碼片斷——這是一種假想的、完全不符合javascript的規范的新語言。示例1是通過一個字符串傳給dsl()的,示例2仍然是通過一個函數,但函數體內是從/*..*/的一個注釋塊。
?
轉載于:https://www.cnblogs.com/encounter/archive/2009/09/08/2188594.html
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的为脚本语言平反-JavaScript篇(3)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 第四期《Summer Tree》 已经整
- 下一篇: 任务太多?学着突破重围