Giter Site home page Giter Site logo

web-frontend-magic's Issues

如何让 JS 调用空函数也能报错?

先来看个 JS 思考题 —— 如何获取闭包内 key 变量的值:

// 挑战目标:获取 key 的值
(function() {
  // 一个内部变量,外部无法获取
  var key = Math.random()

  console.log('[test] key:', key)

  // 一个内部函数
  function internal(x) {
    return x
  }

  // 对外暴露的函数
  apiX = function(x) {
    try {
      return internal(x)
    } catch (err) {
      return key
    }
  }
})()

// 你的代码写在此处:
// ...

2014-11-13 Hack小技巧:

2014-11-13
Hack小技巧:检测控制台是否被打开

演示:https : //www.etherdream.com/FunnyScript/console_detect/

这个对火狐没用,研究研究如何才能检测 火狐的 独立弹出 控制台,非独立弹出检测浏览器可视区域宽高就行了,

利用 JS 函数柯里化,实现高性能虚拟机(2)

前言

上一篇,我们讲解了如何将指令转换成函数,从而减少运行时开销。

然而光有转换还是不够的,如何将转换后的函数高效运行起来,才是真正的挑战。

下面开始我们的探索。。。

流程控制

计数器

传统虚拟机的流程控制,大多依靠程序计数器(program counter)实现,用以指向当前指令的位置。

计数器的方案,同样适用于我们的函数列表:

fn_list = [fn, fn, ......]
pc = 0

do {
  f = fn_list[pc++]
  f()
} while (......)

正常情况下,我们顺序执行列表中的函数;如果需要跳转,则可提供一些「能修改计数器」的指令,以实现分支、循环等效果。

计数器的原理很简单,但其效率并不高。因为每执行一条指令,都得访问数组、更新计数器、终止判断,从而产生数倍的性能开销。

那么,是否有更快的流程控制方案呢?

指令分离

既然我们要追求极致的性能,那只能牺牲一些灵活性。

冯诺伊曼结构的虚拟机,指令和数据是合为一体的,这显然非常灵活。例如,程序可以跳到指令区外,将动态数据当做指令运行;可以跳到指令中间,将半个指令当做新指令执行;甚至还可以自修改,运行时改变指令数据。

然而这并不常用。我们不妨抛弃这些小众特性,尝试将指令和数据分离,这样是否能玩出新花样?

柯里化

现在,指令始终是固定的。因此不妨将多个指令事先绑在一起,这样就能一次调用多个指令。

例如,我们捆绑这两个指令:

mod r3, r5, r7
xor r0, r0, r1

根据上一篇文章,它们先被转换成两个闭包函数:

fn_list[0] = OP2(mod, set_r3, get_r5, get_r7)
fn_list[1] = OP2(xor, set_r0, get_r0, get_r1)

现在我们需要另一个函数,用于两者的绑定。这时,柯里化又派上用场了:

function wrap2(f1, f2) {
  return function() {
    f1(); f2()
  }
}

我们将 fn_list 作为参数列表传给 wrap2,即可得到合二为一的结果:

f = wrap2.apply(null, fn_list)

之后,只需 f() 即可触发 f1()、f2(),从而实现一次调用两个指令!

模板

如果想绑定 3 个、5 个指令,又该如何实现?很简单,接着实现 wrap3、wrap5 即可。

function wrap5(f1, f2, f3, f4, f5) {
  return function() {
    f1(); f2(); f3(); f4(); f5()
  }
}

我们可提供多个版本,例如从 wrap2 直到 wrap16。这样 16 个指令以内的,即可直接绑定;超过 16 个,则可通过组合实现。

例如绑定 18 个指令,可以这样实现:

f = wrap16(x1, x2, ......, x15, wrap3(x16, x17, x18) )

因为绑定后的结果也是一个函数,所以能多层嵌套。

这样,我们就能使用链式结构,将任意多的指令绑在一起,从而实现一次调用、批量执行!

动态 vs 静态

也许你在想,为什么绑定的个数非得固定,而不动态获取呢。例如这样岂不更简单:

function wrapN() {
  const arr = arguments
  return function() {
    for (let i = 0; i < arr.length; i++)
      arr[i]()
  }
}

从做逻辑上说,这确实没问题。然而,动态对于优化是不利的,可能会导致性能降低。

我们写个简单的案例,观察动态的性能开销:

let a = 0
function inc() { a++ }

// 硬编码
console.time('t0')
for (let i = 0; i < 100000000; i++) {
  inc(); inc();
  inc(); inc();
}
console.timeEnd('t0')


// 固定个数
const f = wrap4(inc, inc, inc, inc)
console.time('t1')

for (let i = 0; i < 100000000; i++) {
  f()
}
console.timeEnd('t1')


// 动态个数
const f = wrapN(inc, inc, inc, inc)
console.time('t2')

for (let i = 0; i < 100000000; i++) {
  f()
}
console.timeEnd('t2')

在线运行:https://jsfiddle.net/jcktof8b/

由此可见,固定个数的 wrap4 耗时和硬编码相差无几,而动态个数的 wrapN 则要慢上 3 倍!

分支流程

固定分支

我们实现了顺序流程,现在来考虑分支流程。

由于没有计数器,因此像 goto 这样的跳转,显然不易实现了。不过一般情况下,我们很少会用 goto,尤其像 JS 本来就不支持,只能用 break、continue 等控制流程,跳到块头或块尾。

既然我们的目标是高性能,那不妨再牺牲一些灵活性,只提供固定的流程控制!

例如条件判断,我们通过预制的模板来实现:

function br_if(src, exp1, exp2) {
  return function() {
    src() ? exp1() : exp2()
  }
}

这样,提供条件源、分支 1、分支 2,即可生成一个「带判断功能」的闭包函数。

类似的,循环也可以这样实现。例如,一个固定次数的循环模板:

function loop(N, exp1) {
  return function() {
    for (let i = 0; i < N; i++) {
      exp1()
    }
  }
}

不过,怎样才能将现有的指令块,作为参数传给分支模板呢?

看来,我们得设计一种特殊的指令结构。

指令结构

既然程序没有 goto 这样的任意跳转,那平坦型的指令结构不再有意义,不如使用树结构:

01  add ...
02  loop 1000
03    sub ...
04    br_if r1
05      add ...
06      sub ...
07    else
08      mul ...
09      div ...

虚拟机预处理时,将 同级 的指令合在一起,作为 上级 流程模板的参数。

例如,上述 5~6 行的指令合成 f56 函数,8~9 行合成 f89

f56 = wrap2(f5, f6)
f89 = wrap2(f8, f9)

然后通过分支模板,生成 4~9 行的分支闭包:

f49 = br_if(get_r1, f56, f89)

由于该分支与第 3 行指令位于同一级,于是合成 f39

f39 = wrap2(f3, f49)

然后通过循环模板,生成 2~9 行的循环闭包:

f29 = loop(1000, f39)

由于该循环与第 1 行指令位于同一级,于是合成 f19

f19 = wrap2(f1, f29)

这就是最终的根节点。调用它,即可驱动整个程序!

小结

到此,顺序流程和分支流程已实现,这个「柯里化虚拟机」总算是图灵完备了。

既然我们的目标是高性能,那么其中显然还有不少值得推敲优化的地方。下一篇,我们继续探索。

简单演示:https://www.etherdream.com/FunnyScript/CurryVM/www/

这个 Demo 很不完善,现在已有很大变化~ 当然主要分享的是思路

利用 JS 函数柯里化,实现高性能虚拟机(1)

前言

元旦期间,相信大家都被微信小游戏刷屏了,各种玩法攻略铺天盖地。当得知这是用 JS 开发时,立马想把过去写的 JS 游戏都翻新一遍。于是打开小游戏的官网,查看开发文档。

然而遗憾的是,小游戏虽然是用 JS 写的,但它并非运行在浏览器环境里,因此和 HTML 并不兼容。此外,在官网文档上,还看见一条奇葩的规则:

...
2.与小程序一样,小游戏每次发布需要经过审核。我们在小程序和小游戏中都移除了动态执行代码的能力,包括以下调用方式:
(1)eval 函数
(2)setTimeout、setInterval 函数第一个参数传入代码字符串执行
(3)使用 Function 传入字符串构造函数
(4)使用 GeneratorFunction 传入字符串构造生成器函数
...

咋一看貌似有些道理,要是开放 eval 的话,代码可以从网络上更新,就能绕过官方审核了。

但是,开发者若真想动态执行,那根本就是拦不住的 —— 在代码里藏一个虚拟机不就可以了吗。之前我们探讨《使用 VM 壳混淆 JavaScript 代码》时,就讲解了 JS 虚拟机的概念,让程序根据不同的数据,执行不同的操作。虽然性能不高,但用于简单临时的场合,还是没问题的。

这时冒出个想法:要是能把性能优化得足够好的话,是不是可以让整个小游戏都由虚拟指令实现,这样程序只需发布一次就再也不用审核了呢?

于是开始探索,一个不用 eval、而是由 JS 自我驱动的图灵机,性能最高能达到多少。

有什么场合,必须使用 ES6 的 Reflect?

大家都知道 JS 非常灵活,很多原生 API 都可以被开发者重写。

然而有时我们希望调用的 API 是原生的(至少在我们程序运行后不再有变化)。常见的做法,就是在程序运行时将原始接口进行备份:

(function(Date_now) {
  function call_later() {
    Date_now();
  }
  // ...
})(Date.now);

这样,即使后续程序修改了 Date.now,我们程序内部的 Date_now 引用的仍是原始版本。

当然这个案例比较简单。下面思考一个更复杂的,如果换成 document.getElementById,又改如何实现?
也许你首先会想到这样:

(function(fn) {
  fn('id')
  // ...
})(document.getElementById);

但是 document.getElementByIdDate.now 不同,这个 API 并不是静态函数,它依赖 this。直接调用 fn 的话,就会抛出 Illegal invocation 错误。(当然很久以前的古老 IE 浏览器可以这么调用,这里不扯远)

当然你会说,把 document 也进行备份,然后通过 call/apply 就可以了:

(function(document, fn) {
  fn.apply(document, [...]);
  // ...
})(document, document.getElementById);

从工程角度来看,到此确实可以了。但从理论上说,此处的 apply 其实并不能保证 100% 调用就是原生 document.getElementById。换言之,执行 apply 是有可能存在副作用的!

仔细想想,所谓的 apply 其实就是 Function 类的一个方法而已,即 Function.prototype.apply。如果把它重写了,那么 fn.apply() 就是调用重写后的函数!

所以,调用 call/apply 是无法保证绝对可靠的。

那么,能否把原生的 apply 也备份起来呢?可以。但是为了调用原生的 apply,你仍得使用 apply,于是陷入一个死循环。。。

为了解决这个窘境,是时候派上 Reflect 了。Reflect 提供了一个 apply 方法,它比 Function.prototype.apply 更底层,所以不会受到 Function 重写的影响。

更好的是,Reflect 提供的函数都是静态的,如同之前提到的 Date.now 一样!

因此,我们只需备份 Reflect.apply 即可。 上述案例即可这样实现:

(function(apply, document, fn) {
  apply(fn, document, ...);
  // ...
})(Reflect.apply, document, document.getElementById);

演示:http://jsfiddle.net/9m2a7fts/1/

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.