te')); return $arr; } /* 遍历用户所有主题 * @param $uid 用户ID * @param int $page 页数 * @param int $pagesize 每页记录条数 * @param bool $desc 排序方式 TRUE降序 FALSE升序 * @param string $key 返回的数组用那一列的值作为 key * @param array $col 查询哪些列 */ function thread_tid_find_by_uid($uid, $page = 1, $pagesize = 1000, $desc = TRUE, $key = 'tid', $col = array()) { if (empty($uid)) return array(); $orderby = TRUE == $desc ? -1 : 1; $arr = thread_tid__find($cond = array('uid' => $uid), array('tid' => $orderby), $page, $pagesize, $key, $col); return $arr; } // 遍历栏目下tid 支持数组 $fid = array(1,2,3) function thread_tid_find_by_fid($fid, $page = 1, $pagesize = 1000, $desc = TRUE) { if (empty($fid)) return array(); $orderby = TRUE == $desc ? -1 : 1; $arr = thread_tid__find($cond = array('fid' => $fid), array('tid' => $orderby), $page, $pagesize, 'tid', array('tid', 'verify_date')); return $arr; } function thread_tid_delete($tid) { if (empty($tid)) return FALSE; $r = thread_tid__delete(array('tid' => $tid)); return $r; } function thread_tid_count() { $n = thread_tid__count(); return $n; } // 统计用户主题数 大数量下严谨使用非主键统计 function thread_uid_count($uid) { $n = thread_tid__count(array('uid' => $uid)); return $n; } // 统计栏目主题数 大数量下严谨使用非主键统计 function thread_fid_count($fid) { $n = thread_tid__count(array('fid' => $fid)); return $n; } ?>javascript - Context-preserving eval - Stack Overflow
最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Context-preserving eval - Stack Overflow

programmeradmin2浏览0评论

We're building a small REPL that evaluates (with eval) javascript expressions as they are being entered by the user. Since the whole thing is event-driven, evaluation must take place in a separate function, but the context (that is, all declared variables and functions) must be preserved between the calls. I came up with the following solution:

function* _EVAL(s) {
    while (1) {
        try {
            s = yield eval(s)
        } catch(err) {
            s = yield err
        }
    }
}

let _eval = _EVAL()
_eval.next()

function evaluate(expr) {
    let result = _eval.next(expr).value
    if (result instanceof Error)
        console.log(expr, 'ERROR:', result.message)
    else
        console.log(expr, '===>', result)
}

evaluate('var ten = 10')
evaluate('function cube(x) { return x ** 3 }')
evaluate('ten + cube(3)')
evaluate('console.log("SIDE EFFECT")')
evaluate('let twenty = 20')
evaluate('twenty + 40') // PROBLEM

We're building a small REPL that evaluates (with eval) javascript expressions as they are being entered by the user. Since the whole thing is event-driven, evaluation must take place in a separate function, but the context (that is, all declared variables and functions) must be preserved between the calls. I came up with the following solution:

function* _EVAL(s) {
    while (1) {
        try {
            s = yield eval(s)
        } catch(err) {
            s = yield err
        }
    }
}

let _eval = _EVAL()
_eval.next()

function evaluate(expr) {
    let result = _eval.next(expr).value
    if (result instanceof Error)
        console.log(expr, 'ERROR:', result.message)
    else
        console.log(expr, '===>', result)
}

evaluate('var ten = 10')
evaluate('function cube(x) { return x ** 3 }')
evaluate('ten + cube(3)')
evaluate('console.log("SIDE EFFECT")')
evaluate('let twenty = 20')
evaluate('twenty + 40') // PROBLEM

As you can see it works fine with function-scoped variables (var and function), but fails on block scoped ones (let).

How can I write a context-preserving eval wrapper that would also preserve block-scoped variables?

The code runs in a browser, DOM and Workers are fully available.

It should be mentioned that the desired function must handle side effects properly, that is, each line of code, or, at least, each side effect, should be performed exactly once.

Links:

JavaScript: do all evaluations in one vm | https://vane.life/2016/04/03/eval-locally-with-persistent-context/

Share Improve this question edited May 4, 2021 at 7:29 georg asked Apr 29, 2021 at 18:11 georggeorg 215k56 gold badges322 silver badges398 bronze badges 10
  • If you're in a front-end environment, an alternative method is to append a <script> tag each time. But then you'll need some way to get the result of the final statement in a given input string back to the user somehow, which may require a full-fledged parser like Acorn. Vaguely plausible, but there's gotta be an easier way – CertainPerformance Commented Apr 29, 2021 at 18:47
  • You'll want to have a look at how the Chrome console is implemented. They employ several tricks, including TDZ avoidance. – Bergi Commented Apr 29, 2021 at 20:21
  • Would you be fine with the statements running in the global scope? Or: a global scope, like a web worker? – Bergi Commented Apr 29, 2021 at 20:22
  • @CertainPerformance: yes, this is in a browser – georg Commented Apr 30, 2021 at 6:55
  • 1 @georg Ah, that might not work for let, it's quite possible lexical variables are always constrained to the scope of the eval'd expression :-/ – Bergi Commented Apr 30, 2021 at 10:36
 |  Show 5 more ments

4 Answers 4

Reset to default 8 +200

The article you linked contains a crazy approach that actally works: during each eval() call, we create a new closure inside that eval scope and export it so that to we can use it evaluate the next statement.

var __EVAL = s => eval(`void (__EVAL = ${__EVAL.toString()}); ${s}`);

function evaluate(expr) {
    try {
        const result = __EVAL(expr);
        console.log(expr, '===>', result)
    } catch(err) {
        console.log(expr, 'ERROR:', err.message)
    }
}

evaluate('var ten = 10')
evaluate('function cube(x) { return x ** 3 }')
evaluate('ten + cube(3)')
evaluate('console.log("SIDE EFFECT")')
evaluate('let twenty = 20')
evaluate('twenty + 40') // NO PROBLEM :D

TL;DR

Here is the remended, best solution I came up with below, supporting all expressions including promise-based expressions like fetch(), making use of async/await and nesting evaluate() in the final then() of my fetch().

Note (also mentioned in full post below)
The result of the nested evaluate() expression is logged first. This is correct and to be expected as that nested expression runs within the fetch() that runs it. Once the entire fetch runs, it will return undefined just as a variable assignment would. For every other [non-remended] solution in my answer below, the title variable will be evaluated if and after the fetch() statement has been fully evaluated successfully. This is because we are either forcefully deferring the evaluation of the title variable by means of setTimeout() or a pre-processed then(), or by means of forced sequential loading in the "BONUS" solution at the bottom of this solution.

var __EVAL = s => eval(`void (__EVAL = ${__EVAL.toString()}); ${s}`);

async function evaluate(expr) {
  try {
    const result = await __EVAL(expr);
    console.log(expr, '===>', result)
  } catch (err) {
    console.log(expr, 'ERROR:', err.message)
  }
}

evaluate('var ten = 10')
evaluate('function cube(x) { return x ** 3 }')
evaluate('ten + cube(3)')
evaluate('let twenty = 20')
evaluate('twenty + 40')
evaluate('let title = ""')
evaluate('fetch("https://jsonplaceholder.typicode./todos/1").then(res => res.json()).then(obj => title = obj.title).then(() => evaluate("title"))')

The madness explained

A few other solutions came very close here, so I must give credit to both Bergi and Brandon McConnell— Bergi for his/her clever use of closures with eval() and Brandon for his ingenuity in using a "stepped" result.

The correct solution does exist, and it does work with promises. For ease of use, I did use Bergi's solution as a foundation for my own, and if you do select my answer, I will gladly split the reputation bonus evenly with them.

Simply by making the evaluate() function async/await, you allow it to work with promises. From here, you have to decide how you would like for it to run— either organically, where fetch() statements run asynchronously as they normally would, or synchronously and wait for any Promise to be settled before proceeding to the next evaluate() call.

In my solution here, I chose to go the organic route as this is how JavaScript does actually work natively. To force all promises to run before proceeding would be to circumvent the nature of JavaScript. For example, if you were using this code to build a JavaScript engine or piler, you would want the code to run the same with your engine as it would on the web for other users, so organic would be the wait to go.

BONUS

发布评论

评论列表(0)

  1. 暂无评论