Preprocessor function eval

Browsers support dynamic JavaScript evaluation via evalor new Function. This is very convenient for compiling small data binding expressions provided as strings in JavaScript functions.

eg.

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

I would like to preprocess these expressions to support the syntax of the ES6 arrow function without using babel or any other library with more than a few hundred JavaScript lines.

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

Unfortunately, this does not work even with the latest version of IE.

The function should take a string and return another string. For instance.

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

must return

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

Any ideas on how to implement such a thing?

+4
source share
2 answers

, .

. Fat Arrow.

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

fatArrowUtil.js expandFatArrow(code) .

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

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

//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))');

. bind() 'this'. , .

.

//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('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("test0: " + JSON.stringify(mappedArr));
	logDom("test0: " + 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("test1 : " + es6OddNumbers, 'log');
</pre>
</div>

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

<div class='test-unit'>
<pre class='test'>
	//with mutiple parameters, single line
	var add = (a, b) => a + b;
	logDom("test3: " + 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("test4: " + es6Output, 'log');
</pre>
</div>

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

<div class='test-unit'>
<pre class='test'>
	//return empty object
	logDom("test6: " + ( ()=>{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("test7: 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("test7:   timeout seconds: " +counterB.seconds, 'log');
	}, 1200);
</pre>
</div>
		
	</body>
</html>
Hide result
+3

, , . , , -, eval, anno 2019, ! Chrome & Edge, .

:

var lambda = '(a, b) => a + b';
var fun = eval(lambda);
var sum = fun(40, 2);

document.write('Sum: ${sum}');
Hide result
-1

All Articles