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