分类
js

在任意上下文执行代码,`new Function` vs `eval()`

0. 需求及方向

我厂的 Navigator 扩展遇到一个需求:

  1. 动态生成一段 JS 在浏览器里运行
  2. 能重复生成、重复运行
  3. 能生成一段新代码,继续在上一次的环境里运行

其中(3)是新需求,(1)(2)已经比较完善地实现了。主要方案是使用 <script type="module">,这样加载的 JS 会自动执行,并且与全局环境相隔离,不会污染全局环境——这样就可以重复执行。

(3)的难点在于,上一段 JS 已经执行过了,环境也自然释放了,我要么把上一阶段 JS 的执行结果保存下来,要么把执行环境保存下来。前者需要对 Navlang 编译器做大幅度的修改;后者则有很大的实施难度。

经过研究和思考,我选择 保存环境 为攻关方向。即在上次执行结束前,在 window 上注册一个钩子函数,然后在后续追加执行的时候调用它,把后面的代码以函数的形式传进去,以便在上一次留下的环境里继续执行。

1. 问题:访问环境里的变量

添加钩子函数并不难,这样就行:

// 在上一段代码最后添加钩子函数
window.doNext = function (func) {
  func();
}

// 在新一段代码执行函数
window.doNext(function () {
  // 新生成的函数
});

这样做虽然看起来是在上一次的上下文环境中执行新的函数,但由于 JS 的闭包和作用域链特性,实际执行时,新函数并不能访问到上一次上下文的数据。换言之,目的并没有实现。

2. new Function() vs eval()

然后回到标题。所以我们必须在上一次的环境里重新构建函数,这并不困难,其实开发浏览器扩展时,因为要协调四、五个运行环境,所以把函数序列化传输,再重新构建执行非常常见。于是我直接操起 new Function(),然后失败了。

查阅 MDN,原来 new Function() 构造的函数,它的上下文会绑定在全局对象上(相当于 .call(null)),所以自然无法访问到上一次环境里的变量。

再看 eval()文档,似乎可行,于是换用之,果然有效。至此,问题解决。

3. 代码范例

function navlangExecuteFunction(func) {
  // 使用模版字符串构建异步函数
  eval(`(async function doNext() {
    // 捕获中间可能出现的问题
    try {
      // 执行真正的函数
      await (${func})();
      // 为兼容 node.js,mock 一个 process 来处理执行结束
      process.exit();
    } catch (e) {
       if (!e.message.startsWith('Exit code:')) {
         console.error(e);
         process.exit(1);
       }
    }
  })()`);
}
window.navlangExecuteFunction = navlangExecuteFunction;
const serializeFunction = (f) => {
  const serialized = f.toString();
  // Safari serializes async arrow functions with an invalid function keyword.
  // This needs to be removed in order for the function to be interpretable.
  const safariPrefix = 'async function ';
  if (serialized.startsWith(safariPrefix)) {
    const arrowIndex = serialized.indexOf('=>');
    const bracketIndex = serialized.indexOf('{');
    if (arrowIndex > -1 && (bracketIndex === -1 || arrowIndex < bracketIndex)) {
      return async ${serialized.slice(safariPrefix.length)};
    }
  }
  return serialized;
};

let nextStep = async function () {
  // 要执行的部分
}
nextStep = serializeFunction(nextStep);
navlangExecuteFunction(nextStep);

4. 可能带来的问题

MDN 明确建议大家 不要用 eval。理由如下:

  1. 不安全。因为 eval() 会在当前环境执行代码,意味着攻击者可能窃取任何当前环境的数据。
  2. 性能差。现代 JS 引擎会对代码进行大量优化,包括转成机器码等。eval() 会破坏这个过程,使得运行性能大大降低。

不过在我的场景下,这两个问题并不严重。一方面,被执行的追加代码都是由 Navlang 编译器生成,而不是任意第三方,它的安全性不会比其它的代码安全性低。另一方面,这个功能是帮助用户开发调试 Navlang 的,我们可以认为它大概率不会跑在性能敏感的环境里。

另外,对我厂的 Navigator 产品而言,这样的方案还会让 GC 变得比较难执行。不过一样从 便利开发 的角度出发,我觉得性价比完全 OK。

5. 总结

这个需求比较特殊,涉及到 JS 函数的很多性质,包括运行时优化的知识,还是蛮值得大家琢磨的。

另外就是所谓“尽信书不如无书”。eval() 的确存在一些问题,不能轻易使用,但是当需求摆在面前,经过充分的分析确认之后,我们该用还是要用。

分类
许愿

Hello,2021

2020 可能是史无前例的不受欢迎的一年,无论是微博、朋友圈、Twitter,极少看到有人怀念它的。我不禁为 2020 感到惋惜,明明不是它的罪过。

不过我还是要说,在我经历过的 36 个年头里,2020 年是体感速度最快的一年,我现在都还能回忆起年初的种种,好像一睁眼一闭眼,就 2021 了,还有很多想做的事情没做……好吧,还是总结一下吧。

回顾 2020

先说身体。去年 4 月 28 日,早上起床突然发现脚踝肿了,怀疑是痛风,于是去医院看诊。医生觉得不太像是痛风,还是扭伤的可能性大。但是检查结果血糖没控制好,骨外科的大夫说糖尿病患者可能会出现随机的关节痛。哎,果然还是走上了糖尿病患者的老路。

跟老婆商量了一下,正好疫情逐步得到控制,健身房也即将开放,于是我们办了健身卡开始健身。我也找医生换了新药控制食欲。开始效果很明显,食欲下降明显,几乎吃两口就饱了。体重下降很快,最低到 105+kg。可能是太过冒进,身体也出现一些不适,于是 8 月份去住了一周院调整。后来减了一些运动量,加了一些食量,目前感觉好多了,体重稳定在 106~108之间。

开始锻炼之后,心肺功能和肌肉都得到了不小的加强,基本学会自由泳。

总结一下:

  1. 终于有一年体重达标了,可喜可贺!
  2. 利拉鲁肽对我非常有效,不过半年过去,药效已经明显下降。
  3. 低血糖不好,控糖的目的一是血糖稳定,二是稳定在合适的范围内。
  4. 无氧运动加强肌肉,提升基础代谢;有氧运动减重

side project 部分就比较惨,书没进展,肉大师没捡起来,新接的视频也没录完……春节前一定要录完,加油。

博客大概写了 69 篇,还行吧,草稿箱里还有几篇没写完。访问量相比去年提升很多,坚持还是有用的。今年继续努力。

旅游彻底黄了,一年没出门。哦,不对,去了几次深圳……

终于下定决心把车换了,老车送回北京给表姐开,换了一辆亚洲龙(Avalon)代步。去年告别了很多人,包括 Avalon JS 框架的作者。新车挺好的,未来一时半会儿不会再换了。

NAS 的机器弄出来了,没顾上搞系统,今年继续吧。

2021 计划

  1. 坚持锻炼,把体重降到 105 kg
  2. 去年捡起来游泳,今年希望捡起来篮球
  3. 复活肉大师,把书写完
  4. 录完欠(签)下的教学视频
  5. 把前面设想的教学视频小语言做出来,然后再录 1~2 套视频
  6. 努力写博客,去年大概挣了 $20,希望今年够得上提款
  7. 旅游方面,如果国境线如果能开放,希望可以出去一次
  8. 感谢老板支持,去年年底终于把 Navlang + 扩展发出去了。那么今年要再努力一把:
    1. 上线 Chrome Web Store 和其它浏览器应用商店
    2. 能够收获 100+ 用户
    3. 完成付费使用功能
  9. 搞定一个 Side Project

去年身体状况从五月折腾到八月,期间想了很多,觉得还是要拼一拼,主要是不拼也未必能好。所以今年还要多努力!加油!

分类
js 技术

使用 Proxy 添加魔术属性/方法

最近在开发我厂的 QA 工具时,遇到一个问题。我需要模拟 Puppeteer 的所有方法,以便兼容原先的 JS 文件。Puppeteer 提供一个 .asElement() 方法,可以把函数执行结果转换成一个伪 DOM Element(如果函数返回的就是 DOM Element 的话),然后我们就可以在 Node.js 里调用原本属于 DOM 的方法,比如 .focus()。Pupputeer 会替我们完成映射和函数调用,并且返回结果。

对于大部分对象来说,我只要模拟对应的属性、方法,然后用自己的函数实现功能即可。但是 DOM Element 有上百个属性和方法,手工实现一遍实在太低效了。必须寻找其它途径。

好在我之前看过 Proxy 的介绍,赶紧翻出文档和书又复习两遍,就大概知道怎么做了。

Proxy 类如其名,可以“代理”对某个对象的访问。你可以把他理解成明星的经纪人。明星成名之前都是自己处理一切事务,有了经纪人之后,大部分事务就由经纪人负责,但仍然有一些事情需要明星自己处理。

Proxy 的用法很简单,实例化时,把要代理的对象传进去,定义一下代理方法就好。

const obj = { name: 'meathill' };
new Proxy(obj, {
  get(target, property) {
    // 如果对象中有要求的属性或方法,则返回
    if (property in target) {
      return target[property];
    }
    // 没有的话,进行其它处理
    return 'hello';
  }
});

obj.name // 'meathill'
obj.age // 'hello'
obj.sex // 'hello'

接下来,比如我们访问 obj.foo,那么代理就会生效,它会先检查 obj,如果这个对象上本来就有 foo 属性,就会返回;如果没有,则会调用我们定义的方法来处理。

如此一来,我们可以定义一个 VElement 类,这个类可以实现一些特殊方法,比如 .type(str) 输入,.click() 点击等;然后用 Proxy 代理其它方法和属性,让对象进入插件 Context 执行。


关于如何创建具有魔法属性/方法的类,请移步阅读 使用 Proxy 创建有魔术属性/方法的类


Proxy 还有其它方法也很有用,尤其是 get 对应的 set ,以后再介绍。大家可以自己抽空研究下。

参考

  • 阮一峰的 ES6
  • 《深入理解 ES6》

分类
公司

Welcome to Navlang

过去两天,我厂在杭州召开了 OpenResty Con 2018,很多 OpenResty 的社区的小伙伴分享了很多使用 OpenResty 的心得。春哥也分享了 OpenResty(包括商业公司 OpenResty Inc)接下来的计划和未来的方向。说实话我已经很长时间不参会也不关心会了,因为很多会营养太少,商业太过。不过我不得不说,我厂的会干货真的多。

回到主题,我也趁机搞了一个闪电演讲,介绍我厂的小语言之一——Navlang。遗憾的是,准备的不算充分,时间也比较短,所以介绍的不够清楚,导致好几个同学来问细节。

既然已经公开我以后也会分享一些实现细节和心得。感兴趣的同学可以先看 Slide:

开发这个语言的时候,我不太适应 Perl,也不太适应在春哥的要求下写代码,所以表现并不理想。如果有机会我还是希望能继续贡献 feature。

另外,受到 Navlang 的启发,我觉得可以搞一个 TechLang,用编程的方式录制视频教程。将来有机会搞一下。