Browsers support dynamic JavaScript evaluation through eval
or new Function
. This is very convenient for compiling small data-binding expressions provided as strings into JavaScript functions.
E.g.
var add2 = new Function('x', 'return x + 2');
var y = add2(5); //7
I would like to preprocess these expressions to support ES6 arrow function syntax without using babel or any other library with more than a few hundred lines of JavaScript.
var selectId = new Function('x', 'return x.map(a=>a.id)');
Unfortunately, this doesn't work even with the latest IE version.
The function should take a string and return another string. E.g.
resolveArrows('return x.map(a=>a.id)')
should return
'return x.map(function(a) { return a.id })'
Any ideas on how to implement such a thing?
As others have already explained that such a utility would be extremely fragile and can not be trusted with very complex code.
However for simple cases it's possible to implement this.
Following is the link to the Fat Arrow
function expansion.
https://github.com/ConsciousObserver/stackoverflow/blob/master/Es6FatArrowExpansion/fatArrowUtil.js
Import fatArrowUtil.js and call expandFatArrow(code)
on your code.
Following is sample usage
expandFatArrow("()=>'test me';");
And below is the result
(function (){return 'test me';}).bind(this)
Below is the output for your suggested test case
//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))');
Note: This utility uses bind() of Function to preserve the 'this' context. It doesn't try to compile your code, any errors in the original code would be present in expanded code.
Below is the working sample with tests and results.
//start of fat arrow utility
'use strict';
function expandFatArrow(code) {
var arrowHeadRegex = RegExp(/(\((?:\w+,)*\w+\)|\(\)|\w+)[\r\t ]*=>\s*/);
var arrowHeadMatch = arrowHeadRegex.exec(code);
if(arrowHeadMatch) {//if no match return as it is
var params = arrowHeadMatch[1];
if(params.charAt(0) !== "(") {
params = "(" + params + ")";
}
var index = arrowHeadMatch.index;
var startCode = code.substring(0, index);
var bodyAndNext = code.substring(index + arrowHeadMatch[0].length);
var curlyCount = 0;
var curlyPresent = false;
var singleLineBodyEnd = 0;
var bodyEnd = 0;
var openingQuote = null;
for(var i = 0; i < bodyAndNext.length; i++) {
var ch = bodyAndNext[i];
if(ch === '"' || ch === "'") {
openingQuote = ch;
i = skipQuotedString(bodyAndNext, openingQuote, i);
ch = bodyAndNext[i];
}
if(ch === '{'){
curlyPresent = true;
curlyCount++;
} else if(ch === '}') {
curlyCount--;
} else if(!curlyPresent) {
//any character other than { or }
singleLineBodyEnd = getSingeLineBodyEnd(bodyAndNext, i);
break;
}
if(curlyPresent && curlyCount === 0) {
bodyEnd = i;
break;
}
}
var body = null;
if(curlyPresent) {
if(curlyCount !== 0) {
throw Error("Could not match curly braces for function at : " + index);
}
body = bodyAndNext.substring(0, bodyEnd+1);
var restCode = bodyAndNext.substring(bodyEnd + 1);
var expandedFun = "(function " + params + body + ").bind(this)";
code = startCode + expandedFun + restCode;
} else {
if(singleLineBodyEnd <=0) {
throw Error("could not get function body at : " + index);
}
body = bodyAndNext.substring(0, singleLineBodyEnd+1);
restCode = bodyAndNext.substring(singleLineBodyEnd + 1);
expandedFun = "(function " + params + "{return " + body + "}).bind(this)";
code = startCode + expandedFun + restCode;
}
return expandFatArrow(code);//recursive call
}
return code;
}
function getSingeLineBodyEnd(bodyCode, startI) {
var braceCount = 0;
var openingQuote = null;
for(var i = startI; i < bodyCode.length; i++) {
var ch = bodyCode[i];
var lastCh = null;
if(ch === '"' || ch === "'") {
openingQuote = ch;
i = skipQuotedString(bodyCode, openingQuote, i);
ch = bodyCode[i];
}
if(i !== 0 && !bodyCode[i-1].match(/[\t\r ]/)) {
lastCh = bodyCode[i-1];
}
if(ch === '{' || ch === '(') {
braceCount++;
} else if(ch === '}' || ch === ')') {
braceCount--;
}
if(braceCount < 0 || (lastCh !== '.' && ch === '\n')) {
return i-1;
}
}
return bodyCode.length;
}
function skipQuotedString(bodyAndNext, openingQuote, i) {
var matchFound = false;//matching quote
var openingQuoteI = i;
i++;
for(; i < bodyAndNext.length; i++) {
var ch = bodyAndNext[i];
var lastCh = (i !== 0) ? bodyAndNext[i-1] : null;
if(ch !== openingQuote || (ch === openingQuote && lastCh === '\\' ) ) {
continue;//skip quoted string
} else if(ch === openingQuote) {//matched closing quote
matchFound = false;
break;
}
}
if(matchFound) {
throw new Error("Could not find closing quote for quote at : " + openingQuoteI);
}
return i;
}
//end of fat arrow utility
//validation of test cases
(function () {
var tests = document.querySelectorAll('.test');
var currentExpansionNode = null;
var currentLogNode = null;
for(var i = 0; i < tests.length; i++) {
var currentNode = tests[i];
addTitle("Test " + (i+1), currentNode);
createExpansionAndLogNode(currentNode);
var testCode = currentNode.innerText;
var expandedCode = expandFatArrow(testCode);
logDom(expandedCode, 'expanded');
eval(expandedCode);
};
function createExpansionAndLogNode(node) {
var expansionNode = document.createElement('pre');
expansionNode.classList.add('expanded');
currentExpansionNode = expansionNode;
var logNode = document.createElement('div');
logNode.classList.add('log');
currentLogNode = logNode;
appendAfter(node,expansionNode);
addTitle("Expansion Result", expansionNode);
appendAfter(expansionNode, logNode);
addTitle("Output", logNode);
}
function appendAfter(afterNode, newNode) {
afterNode.parentNode.insertBefore(newNode, afterNode.nextSibling);
}
//logs to expansion node or log node
function logDom(str, cssClass) {
console.log(str);
var node = null;
if(cssClass === 'expanded') {
node = currentExpansionNode;
} else {
node = currentLogNode;
}
var newNode = document.createElement("pre");
newNode.innerText = str;
node.appendChild(newNode);
}
function addTitle(title, onNode) {
var titleNode = document.createElement('h3');
titleNode.innerText = title;
onNode.parentNode.insertBefore(titleNode, onNode);
}
})();
pre {
padding: 5px;
}
* {
margin: 2px;
}
.test-unit{
border: 2px solid black;
padding: 5px;
}
.test{
border: 1px solid gray;
background-color: #eef;
margin-top: 5px;
}
.expanded{
border: 1px solid gray;
background-color: #ffe;
}
.log{
border: 1px solid gray;
background-color: #ddd;
}
.error {
border: 1px solid gray;
background-color: #fff;
color: red;
}
<html>
<head>
<link rel='stylesheet' href='style.css'>
</head>
<body>
<div class='test-unit'>
<pre class='test'>
//skip braces in string, with curly braces
var fun = ()=> {
return "test me {{{{{{} {{{}";
};
logDom( fun());
var fun1 = ()=> logDom('test1: ' + 'test me again{ { {}{{ }}}}}}}}}}}}}}');
fun1();
</pre>
</div>
<div class='test-unit'>
<pre class='test'>
var selectId = new Function('x', 'return x.map(a=>a.id)');;
var mappedArr = selectId([{id:'test'},{id:'test1'}]);
console.log("test2: " + JSON.stringify(mappedArr));
logDom("test2: " + JSON.stringify(mappedArr), 'log');
</pre>
</div>
<div class='test-unit'>
<pre class='test'>
//with surrounding code
var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9];
var es6OddNumbers = numbers.filter(number => number % 2);
logDom("test3 : " + es6OddNumbers, 'log');
</pre>
</div>
<div class='test-unit'>
<pre class='test'>
//standalone fat arrow
var square = x => x * x;
logDom("test4: " + square(10), 'log');
</pre>
</div>
<div class='test-unit'>
<pre class='test'>
//with mutiple parameters, single line
var add = (a, b) => a + b;
logDom("test5: " + add(3, 4), 'log');
</pre>
</div>
<div class='test-unit'>
<pre class='test'>
//test with surrounding like test1
var developers = [{name: 'Rob'}, {name: 'Jake'}];
var es6Output = developers.map(developer => developer.name);
logDom("test6: " + es6Output, 'log');
</pre>
</div>
<div class='test-unit'>
<pre class='test'>
//empty braces, returns undefined
logDom("test7: " + ( ()=>{} )(), 'log');
</pre>
</div>
<div class='test-unit'>
<pre class='test'>
//return empty object
logDom("test8: " + ( ()=>{return {}} )(), 'log');
</pre>
</div>
<div class='test-unit'>
<pre class='test'>
//working with the 'this' scope and multiline
function CounterES6() {
this.seconds = 0;
var intervalCounter = 0;
var intervalId = null;
intervalId = window.setInterval(() => {
this.seconds++;
logDom("test9: interval seconds: " + this.seconds, 'log');
if(++intervalCounter > 9) {
clearInterval(intervalId);
logDom("Clearing interval", 'log');
}
}, 1000);
}
var counterB = new CounterES6();
window.setTimeout(() => {
var seconds = counterB.seconds;
logDom("test9: timeout seconds: " +counterB.seconds, 'log');
}, 1200);
</pre>
</div>
</body>
</html>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With