一般我们代码几乎不会使用到eval, 但凡需要使用到eval的地方,都会代而使用构造器Function生成函数实例的方式,因为eval is evileval可以在全局作域下执行代码,也可以在局部作用域(间接调用eval)下执行代码。而使用构造器Function生成函数实例的方式,可以确保我们的代码是在全局作用域下执行。

依赖Function构造函数,我们可以实现自己的“eval”。这里我将实现的“eval”命名为$eval,以示区分。当然,$eval实现的功能和原生eval功能不尽相同。$eval可以让我们在指定作用域下执行代码。同时,这里还额外将$eval方法定义到Object.prototype中以适用不同场合。

下面是$evalObject.prototype.$eval的代码实现。

void function (global) {
  /**
   * Execute javascript code within specific scope
   * @param  {Function|String} fn Scoped function or expression
   * @param  {Object} imports     An object which defines required local variables
   * @return {Function}           Function that can be invoked to run code in specific scope
   */
  function scopedRunner(fn, imports) {
    var rfunc = /^function\s+(?:[^(]*)\(([^)]*)\)\s*{([^]*)}$/;
    var found = String(fn).match(rfunc) || [,,'return (' + fn + ');'];
    var args = found[1] || '';
    var stmt = found[2] || '';

    var body = Object.keys(imports || {}).reduce(function (ret, key, idx) {
      return ret + (idx ? ', ' : 'var ') + key + ' = $scope["' + key + '"]';
    }, '') + '; return function (' + args + ') {' + stmt + '};';

    return Function('$scope', body).call(null, imports);
  }

  // define `global.$eval`
  Object.defineProperty(global, '$eval', {
    value: function () {
      return scopedRunner.apply(null, arguments)();
    }
  });

  // define `Object.prototype.$eval`
  Object.defineProperty(Object.prototype, '$eval', {
    value: function (expr) {
      return global.$eval(expr, this);
    }
  });
}(function () { return this; }());

以下是一些关于$evalObject.prototype.$eval的使用例子。

  • e.g 1
// define some global variables
x = 10;
y = 30;

void function () {
  // define some local variables
  var a = 6, b = 7;

  $eval(function () {
    // Uncaught ReferenceError: a is not defined
    // console.log(a + b);
  });

  // Uncaught ReferenceError: a is not defined
  // console.log($eval('a + b'));

  console.log($eval('y / x')); //=> 3
}();
  • e.g 2
void function () {
  var obj = { a: 6, b: 7 };
  var result;

  result = $eval(function () {
    return a * b;
  }, obj);
  console.log(result); //=> 42

  result = $eval('a * b', obj);
  console.log(result); //=> 42

  result = obj.$eval('a * b');
  console.log(result); //=> 42
}();
  • e.g 3
void function () {
  var raws = ['42', '"42"', 'a * b', '[a, b]', '{ x: a, y: b }'];
  var out = raws.map(eval.$eval.bind({ a: 6, b: 7 }));

  //=> [42, '42', 42, [6, 7], { x: 6, y: 7 }]
  console.log(out);
}();
  • e.g 4
x = 10;
y = 30;

void function () {
  var obj = {
    x: 6,
    y: 7,
    times: function () {
      return this.x * this.y;
    },
    sum: function (x, y) {
      return x + y;
    }
  };

  console.log(
    $eval('$scope', obj) === obj,                        //=> true
    $eval(function () { return $scope; }, obj) === obj,  //=> true
    obj.$eval('$scope') === obj,                         //=> true
    obj.$eval(function () { return $scope; }) === obj    //=> true
  );

  console.log(
    obj.$eval('times()'),             //=> 300
    obj.$eval('$scope.times()'),      //=> 42
    obj.$eval('times.call($scope)')   //=> 42
  );

  obj.$eval(function () {
    console.log(times());             //=> 300
    console.log($scope.times());      //=> 42
    console.log(times.call($scope));  //=> 42
  });

  console.log(
    obj.$eval('sum(x,y)'),                          //=> 13
    obj.$eval(function () { return sum(x, y); }),   //=> 13
    $eval('sum(x,y)', obj),                         //=> 13
    $eval(function () { return sum(x, y); }, obj)   //=> 13
  );
}();

最后,$eval is not eval, $eval is not evil

有人叫她“熟食铺子”,因为只有熟食店会把那许多颜色暖热的肉公开陈列;
又有人叫她“真理”,因为据说“真理是赤裸裸的”;
而鲍小姐并未一丝不挂,所以他们修正为“局部的真理”。
                         《围城》 — 钱钟书

/**
 * Execute javascript code within specific scope
 * @param  {Function} fn      scoped function
 * @param  {Object}   imports An object defines required local variables
 * @return {Function}         A function that can trigger to run code in specific scope
 */
function scopedRunner(fn, imports) {
  var rfunc = /^function\s+(?:[^(]*)\(([^)]*)\)\s*{([^]*)}$/;
  var found = String(fn).match(rfunc) || [,,'return ' + fn];
  var args = found[1] || '';
  var stmt = found[2] || '';

  var body = Object.keys(imports || {}).reduce(function (ret, key, idx) {
    return ret + (idx ? ', ' : 'var ') + key + ' = __locals__["' + key + '"]';
  }, '') + '; return function (' + args + ') {' + stmt + '};';

  return Function('__locals__', body).call(null, imports);
}

The following are some examples.

// e.g 1
void function () {
   scopedRunner(
     function (m, n) {
       var result = (x + y) + (m + n);
       console.log('The meaning of life is:', result);  //=> 42
     },
     { x: 10, y: 1 }
   ).call(null, 30, 1);
}();
// e.g 2
void function () {
  var result = scopedRunner(
    'times(a, b)',
    { a: 6, b: 7, times: (x, y) => x * y }
  )();

  console.log('The meaning of life is:', result);      //=> 42
}();
// e.g 3
Object.prototype.__eval = function (expr) { return scopedRunner(expr, this)(); };

void function () {
  var obj = { a: 6, b: 7, times: (x, y) => x * y };
  var result = obj.__eval('times(a, b)');

  console.log('The meaning of life is:', result);      //=> 42
}();
// e.g 4
var take = (scope => scopedRunner(scope.eval, scope.with)());

void function () {
  var a = 6, b = 7, times = (a, b) => a * b;

  // take result out from evaluated value within specific scope
  var result = take({
    with: { a, b, times },
    eval: 'times(a, b)'
  });

  console.log('The meaning of life is:', result);      //=> 42
}();
// e.g 5
var invoke = scopedRunner((fn, ...args) => fn.apply(null, args))();

void function () {
  var result = invoke((a, b) => a + b, 6, 7);
  console.log('The meaning of life is:', result);      //=> 13
}();

Often times, we need to traverse some hierarchy structure, javascript object is a typical one. Say we’ve got an object called obj defined below.

var obj = {
    ninja: {
        robot: {},
        pizza: function () {}
    },
    foo: {
        bar: {
            baz: [],
            qux: ''
        }
    },
    eggs: {
        spam: []
    }
};

Now, I want to collect all keys defined in obj, commonly we have two choice to traverse obj keys: DFS(depth-first search) and BFS(breadth-first search).

Most times we will use DFS to do traverse, cause it is more intuitive than BFS, just use recursion, and the work is done, see the code below:

function dfs(o) {
    var out = [];

    function _dfs(o) {
        isPlainObject(o) && Object.keys(o).forEach(function (key) {
            out.push(key);
            _dfs(o[key]);
        });
    }

    _dfs(o);

    return out;
}


function isPlainObject(target) {
  return Object.prototype.toString.call(target) === '[object Object]';
}

// output: ["ninja", "robot", "pizza", "foo", "bar", "baz", "qux", "eggs", "spam"]
dfs(obj);

Sometimes, maybe BFS is our choice, it is also very simple to implement, the basic idea is to use FIFO(First in first out) queue strategy, see the code below to have a taste.

function bfs(o) {
    var out = [];
    var objs = [o];

    while (objs.length) {
        var obj = objs.shift();
        var keys = Object.keys(obj);
        out.push.apply(out, keys);

        keys.forEach(function (key) {
            if (isPlainObject(obj[key])) {
                objs.push(obj[key]);
            }
        });
    }

    return out;
}

function isPlainObject(target) {
  return Object.prototype.toString.call(target) === '[object Object]';
}

// output: ["ninja", "foo", "eggs", "robot", "pizza", "bar", "spam", "baz", "qux"]
bfs(obj);

标准的 markdown 中代码块转成 HTML 后都是静态用于展示的, 这是符合一般预期的.

但是对于前端开发者来说经常希望写在 markdown 中的代码可以直接在浏览器中执行, 这样可以省去一些可能的重复工作, 并且往往可以增强表现力.

对于 hexo, 这里定制了其 marked 解析插件: https://github.com/justan/hexo-renderer-marked

使用

_config.yml 中可以配置是否实时预览 HTMLJS 文件:

marked:
  htmldemo: true
  jsdemo: true

开启之后 markdown 中的 HTMLJS 代码就会输入在文章中, 当访问时就会立即执行.