2016-04-05 15 views
4

Browser unterstützen dynamische JavaScript-Auswertung durch eval oder new Function. Dies ist sehr praktisch zum Kompilieren kleiner Datenbindungsausdrücke, die als Strings in JavaScript-Funktionen bereitgestellt werden.Pfeilfunktion eval preprocessor

z.

var add2 = new Function('x', 'return x + 2'); 
var y = add2(5); //7 

Ich mag diese Ausdrücke Vorprozess zu ES6 Pfeil Funktion Syntax zu unterstützen, ohne babel zu verwenden oder jede andere Bibliothek mit mehr als ein paar hundert Zeilen JavaScript.

var selectId = new Function('x', 'return x.map(a=>a.id)'); 

Leider funktioniert das nicht auch mit der neuesten IE-Version.

Die Funktion sollte eine Zeichenfolge und eine andere Zeichenfolge zurückgeben. Z.B.

resolveArrows('return x.map(a=>a.id)') 

sollte zurückkehren

'return x.map(function(a) { return a.id })'

Irgendwelche Ideen auf, wie man eine solche Sache zu implementieren?

+0

Sie wollen einen Parser schreiben? – SparK

+0

@Andy das ist JavaScript Fettpfeil Lambda-Notation (Argument ist 'a', Rückgabewert ist' a.id'). Daher das 'a => a.id', welches ein Funktionsargument für' x.map() 'ist. – TheHansinator

+0

Sie können nicht erwarten, dass dies auf Browsern funktioniert, die keine Pfeilfunktionen unterstützen. Sie benötigen einen Parser, wie Babel oder Traceur. – Oriol

Antwort

3

Wie andere bereits erklärt haben, wäre ein solches Hilfsprogramm äußerst fragil und kann nicht mit sehr komplexem Code vertraut werden.

In einfachen Fällen ist es jedoch möglich, dies zu implementieren. Im Folgenden finden Sie den Link zur Funktionserweiterung Fat Arrow.

https://github.com/ConsciousObserver/stackoverflow/blob/master/Es6FatArrowExpansion/fatArrowUtil.js

Import fatArrowUtil.js und expandFatArrow(code) auf Ihrem Code aufrufen.

Es folgt Verwendungsbeispiel

expandFatArrow("()=>'test me';"); 

Und unten ist das Ergebnis

(function(){return 'test me';}).bind(this) 

Im Folgenden wird der Ausgang für Ihre vorgeschlagene Testfall

//actual 
var selectId = new Function('x', 'return x.map(a=>a.id)'); 
//after expansion 
var selectId = new Function('x', 'return x.map((function (a){return a.id}).bind(this))'); 

Hinweis: Dieses Dienstprogramm bind verwendet() der Funktion, um den 'diesen' Kontext zu bewahren. Es wird nicht versucht, den Code zu kompilieren, da Fehler im ursprünglichen Code in erweitertem Code vorhanden wären.

Unten ist die funktionierende Probe mit Tests und Ergebnissen.

//start of fat arrow utility 
 
'use strict'; 
 
function expandFatArrow(code) { 
 
\t var arrowHeadRegex = RegExp(/(\((?:\w+,)*\w+\)|\(\)|\w+)[\r\t ]*=>\s*/); 
 
\t var arrowHeadMatch = arrowHeadRegex.exec(code); 
 
\t 
 
\t if(arrowHeadMatch) {//if no match return as it is 
 
\t \t var params = arrowHeadMatch[1]; 
 
\t \t if(params.charAt(0) !== "(") { 
 
\t \t \t params = "(" + params + ")"; 
 
\t \t } 
 
\t \t var index = arrowHeadMatch.index; 
 
\t \t var startCode = code.substring(0, index); 
 
\t \t 
 
\t \t var bodyAndNext = code.substring(index + arrowHeadMatch[0].length); 
 
\t \t 
 
\t \t var curlyCount = 0; 
 
\t \t var curlyPresent = false; 
 
\t \t var singleLineBodyEnd = 0; 
 
\t \t var bodyEnd = 0; 
 
\t \t var openingQuote = null; 
 
\t \t 
 
\t \t for(var i = 0; i < bodyAndNext.length; i++) { 
 
\t \t \t var ch = bodyAndNext[i]; 
 
\t \t \t if(ch === '"' || ch === "'") { 
 
\t \t \t \t openingQuote = ch; 
 
\t \t \t \t i = skipQuotedString(bodyAndNext, openingQuote, i); 
 
\t \t \t \t ch = bodyAndNext[i]; 
 
\t \t \t } 
 
\t \t \t 
 
\t \t \t if(ch === '{'){ 
 
\t \t \t \t curlyPresent = true; 
 
\t \t \t \t curlyCount++; 
 
\t \t \t } else if(ch === '}') { 
 
\t \t \t \t \t curlyCount--; 
 
\t \t \t } else if(!curlyPresent) { 
 
\t \t \t \t //any character other than { or } 
 
\t \t \t \t singleLineBodyEnd = getSingeLineBodyEnd(bodyAndNext, i); 
 
\t \t \t \t break; 
 
\t \t \t } 
 
\t \t \t if(curlyPresent && curlyCount === 0) { 
 
\t \t \t \t bodyEnd = i; 
 
\t \t \t \t break; 
 
\t \t \t } 
 
\t \t } 
 
\t \t var body = null; 
 
\t \t if(curlyPresent) { 
 
\t \t \t if(curlyCount !== 0) { 
 
\t \t \t \t throw Error("Could not match curly braces for function at : " + index); 
 
\t \t \t } 
 
\t \t \t body = bodyAndNext.substring(0, bodyEnd+1); 
 
\t \t \t 
 
\t \t \t var restCode = bodyAndNext.substring(bodyEnd + 1); 
 
\t \t \t var expandedFun = "(function " + params + body + ").bind(this)"; 
 
\t \t \t code = startCode + expandedFun + restCode; 
 
\t \t } else { 
 
\t \t \t if(singleLineBodyEnd <=0) { 
 
\t \t \t \t throw Error("could not get function body at : " + index); 
 
\t \t \t } 
 
\t \t \t 
 
\t \t \t body = bodyAndNext.substring(0, singleLineBodyEnd+1); 
 
\t \t \t 
 
\t \t \t restCode = bodyAndNext.substring(singleLineBodyEnd + 1); 
 
\t \t \t expandedFun = "(function " + params + "{return " + body + "}).bind(this)"; 
 
\t \t \t code = startCode + expandedFun + restCode; 
 
\t \t } 
 

 
\t \t return expandFatArrow(code);//recursive call 
 
\t } 
 
\t return code; 
 
} 
 
function getSingeLineBodyEnd(bodyCode, startI) { 
 
\t var braceCount = 0; 
 
\t var openingQuote = null; 
 
\t 
 
\t for(var i = startI; i < bodyCode.length; i++) { 
 
\t \t var ch = bodyCode[i]; 
 
\t \t var lastCh = null; 
 
\t \t if(ch === '"' || ch === "'") { 
 
\t \t \t openingQuote = ch; 
 
\t \t \t i = skipQuotedString(bodyCode, openingQuote, i); 
 
\t \t \t ch = bodyCode[i]; 
 
\t \t } 
 
\t \t 
 
\t \t if(i !== 0 && !bodyCode[i-1].match(/[\t\r ]/)) { 
 
\t \t \t lastCh = bodyCode[i-1]; 
 
\t \t } 
 

 
\t \t if(ch === '{' || ch === '(') { 
 
\t \t \t braceCount++; 
 
\t \t } else if(ch === '}' || ch === ')') { 
 
\t \t \t braceCount--; 
 
\t \t } 
 
\t \t 
 
\t \t if(braceCount < 0 || (lastCh !== '.' && ch === '\n')) { 
 
\t \t \t return i-1; 
 
\t \t } 
 
\t } 
 
\t 
 
\t return bodyCode.length; 
 
} 
 
function skipQuotedString(bodyAndNext, openingQuote, i) { 
 
\t var matchFound = false;//matching quote 
 
\t var openingQuoteI = i; 
 
\t i++; 
 
\t for(; i < bodyAndNext.length; i++) { 
 
\t \t var ch = bodyAndNext[i]; 
 
\t \t var lastCh = (i !== 0) ? bodyAndNext[i-1] : null; 
 
\t \t 
 
\t \t if(ch !== openingQuote || (ch === openingQuote && lastCh === '\\')) { 
 
\t \t \t continue;//skip quoted string 
 
\t \t } else if(ch === openingQuote) {//matched closing quote 
 
\t \t \t matchFound = false; 
 
\t \t \t break; 
 
\t \t } 
 
\t } 
 
\t if(matchFound) { 
 
\t \t throw new Error("Could not find closing quote for quote at : " + openingQuoteI); 
 
\t } 
 
\t return i; 
 
} 
 
//end of fat arrow utility 
 

 
//validation of test cases 
 
(function() { 
 
\t var tests = document.querySelectorAll('.test'); 
 
\t var currentExpansionNode = null; 
 
\t var currentLogNode = null; 
 
\t for(var i = 0; i < tests.length; i++) { 
 
\t \t var currentNode = tests[i]; 
 
\t \t addTitle("Test " + (i+1), currentNode); 
 
\t \t createExpansionAndLogNode(currentNode); 
 
\t \t 
 
\t \t var testCode = currentNode.innerText; 
 
\t \t var expandedCode = expandFatArrow(testCode); 
 

 
\t \t logDom(expandedCode, 'expanded'); 
 
\t \t 
 
\t \t eval(expandedCode); 
 
\t \t 
 
\t }; 
 
\t function createExpansionAndLogNode(node) { 
 
\t \t var expansionNode = document.createElement('pre'); 
 
\t \t expansionNode.classList.add('expanded'); 
 
\t \t currentExpansionNode = expansionNode; 
 
\t \t 
 
\t \t var logNode = document.createElement('div'); 
 
\t \t logNode.classList.add('log'); 
 
\t \t currentLogNode = logNode; 
 
\t \t 
 
\t \t appendAfter(node,expansionNode); 
 
\t \t addTitle("Expansion Result", expansionNode); 
 
\t \t appendAfter(expansionNode, logNode); 
 
\t \t addTitle("Output", logNode); 
 
\t } 
 
\t function appendAfter(afterNode, newNode) { 
 
\t \t afterNode.parentNode.insertBefore(newNode, afterNode.nextSibling); 
 
\t } 
 

 
\t //logs to expansion node or log node 
 
\t function logDom(str, cssClass) { 
 
\t \t console.log(str); 
 
\t \t var node = null; 
 
\t \t if(cssClass === 'expanded') { 
 
\t \t \t node = currentExpansionNode; 
 
\t \t } else { 
 
\t \t \t node = currentLogNode; 
 
\t \t } 
 
\t \t 
 
\t \t var newNode = document.createElement("pre"); 
 
\t \t 
 
\t \t newNode.innerText = str; 
 
\t \t node.appendChild(newNode); 
 
\t } 
 
\t function addTitle(title, onNode) { 
 
\t \t var titleNode = document.createElement('h3'); 
 
\t \t titleNode.innerText = title; 
 
\t \t onNode.parentNode.insertBefore(titleNode, onNode); 
 
\t } 
 
})();
pre { 
 
\t padding: 5px; 
 
} 
 
* { 
 
\t margin: 2px; 
 
} 
 
.test-unit{ 
 
\t border: 2px solid black; 
 
\t padding: 5px; 
 
} 
 
.test{ 
 
\t border: 1px solid gray; 
 
\t background-color: #eef; 
 
\t margin-top: 5px; 
 
} 
 
.expanded{ 
 
\t border: 1px solid gray; 
 
\t background-color: #ffe; 
 
} 
 
.log{ 
 
\t border: 1px solid gray; 
 
\t background-color: #ddd; 
 
} 
 
.error { 
 
\t border: 1px solid gray; 
 
\t background-color: #fff; 
 
\t color: red; 
 
}
<html> 
 
\t <head> 
 
\t \t <link rel='stylesheet' href='style.css'> 
 
\t </head> 
 
\t <body> 
 
<div class='test-unit'> 
 
<pre class='test'> 
 
\t //skip braces in string, with curly braces 
 
\t var fun =()=> { 
 
\t \t return "test me {{{{{{} {{{}"; 
 
\t }; 
 
\t logDom(fun()); 
 
\t var fun1 =()=> logDom('test me again{ { {}{{ }}}}}}}}}}}}}}'); 
 
\t fun1(); 
 
</pre> 
 
</div> 
 

 
<div class='test-unit'> 
 
<pre class='test'> 
 
\t var selectId = new Function('x', 'return x.map(a=>a.id)');; 
 
\t var mappedArr = selectId([{id:'test'},{id:'test1'}]); 
 
\t console.log("test0: " + JSON.stringify(mappedArr)); 
 
\t logDom("test0: " + JSON.stringify(mappedArr), 'log'); 
 
</pre> 
 
</div> 
 

 
<div class='test-unit'> 
 
<pre class='test'> 
 
\t //with surrounding code 
 
\t var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9]; 
 
\t var es6OddNumbers = numbers.filter(number => number % 2); 
 
\t logDom("test1 : " + es6OddNumbers, 'log'); 
 
</pre> 
 
</div> 
 

 
<div class='test-unit'> 
 
<pre class='test'> 
 
\t //standalone fat arrow 
 
\t var square = x => x * x; 
 
\t logDom("test2: " + square(10), 'log'); 
 
</pre> 
 
</div> 
 

 
<div class='test-unit'> 
 
<pre class='test'> 
 
\t //with mutiple parameters, single line 
 
\t var add = (a, b) => a + b; 
 
\t logDom("test3: " + add(3, 4), 'log'); 
 
</pre> 
 
</div> 
 

 
<div class='test-unit'> 
 
<pre class='test'> 
 
\t //test with surrounding like test1 
 
\t var developers = [{name: 'Rob'}, {name: 'Jake'}]; 
 
\t var es6Output = developers.map(developer => developer.name); 
 
\t logDom("test4: " + es6Output, 'log'); 
 
</pre> 
 
</div> 
 

 
<div class='test-unit'> 
 
<pre class='test'> 
 
\t //empty braces, returns undefined 
 
\t logDom("test5: " + (()=>{})(), 'log'); 
 
</pre> 
 
</div> 
 

 
<div class='test-unit'> 
 
<pre class='test'> 
 
\t //return empty object 
 
\t logDom("test6: " + (()=>{return {}})(), 'log'); 
 
</pre> 
 
</div> 
 

 
<div class='test-unit'> 
 
<pre class='test'> 
 
\t //working with the 'this' scope and multiline 
 
\t function CounterES6() { 
 
\t this.seconds = 0; 
 
\t var intervalCounter = 0; 
 
\t var intervalId = null; 
 
\t intervalId = window.setInterval(() => { 
 
\t \t \t this.seconds++; 
 
\t \t \t logDom("test7: interval seconds: " + this.seconds, 'log'); 
 
\t \t \t if(++intervalCounter > 9) { 
 
\t \t \t \t clearInterval(intervalId); 
 
\t \t \t \t logDom("Clearing interval", 'log'); 
 
\t \t \t } 
 
\t \t }, 1000); 
 
\t } 
 

 
\t var counterB = new CounterES6(); 
 
\t window.setTimeout(() => { 
 
\t \t var seconds = counterB.seconds; 
 
\t \t logDom("test7: timeout seconds: " +counterB.seconds, 'log'); 
 
\t }, 1200); 
 
</pre> 
 
</div> 
 
\t \t 
 
\t </body> 
 
</html>

+1

Kühl. Es klappt. Genau darum habe ich gebeten. Ich sehe nur ein potentielles Problem und das ist, wenn Klammern in Strings gefunden werden. Ich denke, dass die Klammerzählung innerhalb von Strings deaktiviert werden sollte, aber das ist etwas, was ich selbst hacken kann. – Marko

+0

Sie haben Recht, ich habe nicht daran gedacht. Ich werde versuchen, es zu ändern. – 11thdimension

+0

@Marko Der Code wurde aktualisiert, um geschweifte Klammern in den Strings in Anführungszeichen zu behandeln. – 11thdimension