我将如何扩展JavaScript语言以支持新的运营商?
问题的答案是否可以在JavaScript中创build自定义运算符? 还没有 ,但@本杰明build议 ,可以添加一个新的运营商使用第三方工具 :
可以使用像sweet.js这样的第三方工具来添加自定义操作符,尽pipe这需要额外的编译步骤。
我将采用与上一个问题相同的示例:
(ℝ,∘),x∘y = x + 2y
对于任何两个实数x和y : x∘y是x + 2y也是一个实数。 如何在扩展的JavaScript语言中添加此运算符?
以下代码将运行后:
var x = 2 , y = 3 , z = x ∘ y; console.log(z);
输出将包含
8
(因为8
是2 + 2 * 3
)
我将如何扩展JavaScript语言以支持新的运营商?
是的,这是可能的,甚至不是很难:)
我们需要讨论几件事情:
- 什么是语法和语义。
- 编程语言如何被parsing? 什么是语法树?
- 扩展语言语法。
- 扩展语言语义。
- 如何将操作符添加到JavaScript语言中?
如果你懒惰,只是想看到它的行动 – 我把工作代码放在GitHub上
1.什么是语法和语义?
一般来说 – 一种语言由两件事组成。
-
语法 – 这些语言中的符号像一元运算符(如
++
)以及Expression
一样,是表示“内联”函数的FunctionExpression。 语法只代表使用的符号,而不代表其含义。 简而言之,语法只是字母和符号的graphics – 它没有固有的意义。 -
语义将这些符号的含义联系起来。 语义是什么说
++
意味着“增加一”,实际上这里是确切的定义 。 它将语义和我们的语法联系起来,没有它,语法只是一个带有顺序的符号列表。
2.如何parsing编程语言? 什么是语法树?
在某些时候,当某些东西用JavaScript或其他编程语言执行代码时,它需要理解代码。 这种被称为lexing的一部分(或者标记化,我们不要在此细微区别)意味着分解代码,如:
function foo(){ return 5;}
在其有意义的部分 – 这就是说,这里有一个function
关键字,后面跟着一个标识符,一个空的参数列表,然后是一个块{
包含一个返回关键字,文字5
,然后是分号,然后是结束块}
。
这个部分完全是在语法上,它所做的只是把它分解成function,foo,(,),{,return,5,;,}
。 它仍然不了解代码。
之后 – 构build一个Syntax Tree
。 语法树更加注意语法,但仍然完全是句法。 例如,一个语法树可以看到以下的标记:
function foo(){ return 5;}
并找出“嘿!这里有一个函数声明 !”。
它被称为树,因为它只是 – 树允许嵌套。
例如,上面的代码可以产生如下所示的内容:
Program FunctionDeclaration (identifier = 'foo') BlockStatement ReturnStatement Literal (5)
这很简单,只是为了向您展示它并不总是如此线性,让我们来检查5 +5
:
Program ExpressionStatement BinaryExpression (operator +) Literal (5) Literal(5) // notice the split her
这种分裂可能发生。
基本上,一个语法树允许我们expression语法。
这是x ∘ y
失败的地方 – 它看到∘
,并且不理解语法。
3.扩展语言语法。
这只需要一个parsing语法的项目。 我们在这里要做的是读取“我们的”语言的语法,它与JavaScript不一样(并且不符合规范),用JavaScript语法可以替代我们的操作符。
我们将要做的不是 JavaScript。 它不遵循JavaScript规范,标准投诉JSparsing器将抛出exception。
4.扩展语言语义
无论如何,我们总是这样做:)我们在这里所做的只是定义一个调用操作符时调用的函数。
5.如何添加一个操作符到JavaScript语言。
让我从这个前缀开始说,我们不会在这里添加一个运算符到JS,而是 – 我们正在定义我们自己的语言 – 我们把它称为“CakeLanguage”或者什么东西,并添加它的运算符。 这是因为∘
不是JS语法的一部分,JS语法不允许任何其他语言的操作符。
我们将为此使用两个开源项目:
- esprima需要JS代码并为其生成语法树。
- escodegen做了另一个方向,从语法树esprima spits中生成JS代码。
你已经密切关注你会知道我们不能直接使用esprima,因为我们会给它不明白的语法。
我们将添加一个#
运算符,它为x # y === 2x + y
提供了乐趣。 我们会给它多重性的优先级(因为运算符有运算符优先级)。
所以,当你得到你的Esprima.js副本 – 我们需要改变以下内容:
要FnExprTokens
– 这是expression式,我们需要添加#
所以它可以识别它。 之后,它会看起来如此:
FnExprTokens = ['(', '{', '[', 'in', 'typeof', 'instanceof', 'new', 'return', 'case', 'delete', 'throw', 'void', // assignment operators '=', '+=', '-=', '*=', '/=', '%=', '<<=', '>>=', '>>>=', '&=', '|=', '^=', ',', // binary/unary operators '+', '-', '*', '/', '%','#', '++', '--', '<<', '>>', '>>>', '&', '|', '^', '!', '~', '&&', '||', '?', ':', '===', '==', '>=', '<=', '<', '>', '!=', '!=='];
要scanPunctuator
我们将添加它和它的char代码作为可能的情况: case 0x23: // #
然后进行testing,所以它看起来像:
if ('<>=!+-*#%&|^/'.indexOf(ch1) >= 0) {
代替:
if ('<>=!+-*%&|^/'.indexOf(ch1) >= 0) {
然后到binaryPrecedence
让我们给它与多重性相同的优先级:
case '*': case '/': case '#': // put it elsewhere if you want to give it another precedence case '%': prec = 11; break;
而已! 我们刚刚扩展了语言语法来支持#
操作符。
我们还没有完成,我们需要把它转换回JS。
首先为我们的树定义一个简短的visitor
函数,recursion访问它的所有节点。
function visitor(tree,visit){ for(var i in tree){ visit(tree[i]); if(typeof tree[i] === "object" && tree[i] !== null){ visitor(tree[i],visit); } } }
这只是通过Esprima生成的树并访问它。 我们传递一个函数,并在每个节点上运行它。
现在,让我们来看看我们特殊的新算子:
visitor(syntax,function(el){ // for every node in the syntax if(el.type === "BinaryExpression"){ // if it's a binary expression if(el.operator === "#"){ // with the operator # el.type = "CallExpression"; // it is now a call expression el.callee = {name:"operator_sharp",type:"Identifier"}; // for the function operator_# el.arguments = [el.left, el.right]; // with the left and right side as arguments delete el.operator; // remove BinaryExpression properties delete el.left; delete el.right; } } });
所以简而言之:
var syntax = esprima.parse("5 # 5"); visitor(syntax,function(el){ // for every node in the syntax if(el.type === "BinaryExpression"){ // if it's a binary expression if(el.operator === "#"){ // with the operator # el.type = "CallExpression"; // it is now a call expression el.callee = {name:"operator_sharp",type:"Identifier"}; // for the function operator_# el.arguments = [el.left, el.right]; // with the left and right side as arguments delete el.operator; // remove BinaryExpression properties delete el.left; delete el.right; } } }); var asJS = escodegen.generate(syntax); // produces operator_sharp(5,5);
我们需要做的最后一件事是定义函数本身:
function operator_sharp(x,y){ return 2*x + y; }
并包含在我们的代码之上。
这里的所有都是它的! 如果你读到目前为止 – 你应该得到一个cookies:)
这里是GitHub上的代码,所以你可以玩它。
正如我在你的问题的评论中所说的,sweet.js 不支持中缀操作符 。 你可以自由地分发sweet.js并自己添加它,或者你只是SOL。
老实说,实现自定义中缀操作符还是不值得的。 Sweet.js是一个很好的支持工具,它是我所知道的唯一一个试图在JS中实现macros的工具。 使用自定义预处理器添加自定义中缀运算符可能不值得您拥有的收益。
也就是说,如果你只是为非专业的工作而努力,那就去做任何你想做的事吧。