替代方案
NX 试图避免重新实现原生代码。代码求值是由一个使用了一些新或者冷门的 JavaScript 功能的小型库来处理的。
本节将会循序渐进地介绍这些功能,然后由它们来介绍 nx-compile 是如何运行代码的。此库含有一个被称为 compileCode()
的库,运行方式类似以下代码:
const code = compileCode('return num1 + num2')
// this logs 17 to the console
console.log(code({num1: 10, num2: 7}))
const globalNum = 12
const otherCode = compileCode('return globalNum')
// global scope access is prevented
// this logs undefined to the console
console.log(otherCode({num1: 2, num2: 3}))
在本章末尾,我们将会以少于 20 行的代码来实现 compileCode
函数。
new Function()
函数构建器创建了一个新的函数对象。在 JavaScript 中,每个函数都实际上是一个函数对象。
Function
构造器是 eval()
的一个替代方案。new Function(...args, 'funcBody')
对传入的 'funcBody'
字符串进行求值,并返回执行这段代码的函数。它和 eval()
主要有两点区别:
- 它只会对传入的代码求值一次。调用返回的函数会直接运行代码,而不会重新求值。
- 它不能访问本地闭包变量,但是仍然可以访问全局作用域。
function compileCode(src) {
return new Function(src)
}
new Function()
在我们的需求中是一个更好的替代 eval()
的方案。它有很好的性能和安全性,但是为使其可行需要屏蔽其对全局作用域的访问。
With 关键字
with 声明为一个声明语句拓展了作用域链
with
是 JavaScript 一个冷门的关键字。它允许一个半沙箱的运行环境。with
代码块中的代码会首先试图从传入的沙箱对象获得变量,但是如果没找到,则会在闭包和全局作用域中寻找。闭包作用域的访问可以用 new Function()
来避免,所以我们只需要处理全局作用域。
function compileCode(src) {
src = 'with (sandbox) {' + src + '}'
return new Function('sandbox', src)
}
with
内部使用 in
运算符。在块中访问每个变量,都会使用variable in sandbox
条件进行判断。若条件为真,则从沙箱对象中读取变量。否则,它会在全局作用域中寻找变量。通过欺骗 with
可以让variable in sandbox
一直返回真,我们可以防止它访问全局作用域。
ES6 代理
代理对象用于定义基本操作的自定义行为,如属性查找或赋值。
一个 ES6 proxy
封装一个对象并定义陷阱函数,这些函数可以拦截对该对象的基本操作。当操作发生的时候,陷阱函数会被调用。通过在Proxy
中包装沙箱对象并定义一个 has
陷阱,我们可以重写 in
运算符的默认行为。
function compileCode(src) {
src ='with (sandbox) {' + src + '}
const code = new Function('sandbox', src)
return function(sandbox) {
const sandboxProxy = new Proxy(sandbox, {has})
return code(sandboxProxy)
}
}
// this trap intercepts 'in' operations on sandboxProxy
function has(target, key) {
return true
}
以上代码欺骗了 with
代码块。variable in sandbox
求值将会一直是 true 值,因为 has
陷阱函数会一直返回 true。with
代码块将永远都不会尝试访问全局对象。
Symbol.unscopables
标记是一个唯一和不可变的数据类型,可以被用作对象属性的一个标识符。
Symbol.unscopables
是一个著名的标记。一个著名的标记即是一个内置的 JavaScript Symbol
,它可以用来代表内部语言行为。例如,著名的标记可以被用作添加或者覆写遍历或者基本类型转换。
Symbol.unscopables 著名标记用来指定一个对象自身和继承的属性的值,这些属性被排除在
with
所绑定的环境之外。
Symbol.unscopables
定义了一个对象的 unscopable
(不可限定)属性。在with
语句中,不能从Sandbox对象中检索Unscopable属性,而是直接从闭包或全局作用域检索属性。Symbol.unscopables
是一个不常用的功能。你可以在本页上阅读它被引入的原因。
我们可以通过在沙箱的 Proxy
属性中定义一个 get
陷阱来解决以上的问题,这可以拦截 Symbol.unscopables
检索,并且一直返回未定义。这将会欺骗 with
块的代码认为我们的沙箱对象没有 unscopable 属性。
function compileCode(src) {
src = 'with(sandbox) {' + src + '}'
const code = new Function('sandbox', src)
return function(sandbox) {
const sandboxProxy = new Proxy(sandbox, {has, get})
return code(sandboxProxy)
}
}
function has(target, key) {
return true
}
function get(target, key) {
if (key === Symbol.unscopables) return undefined
return target[key]
}
WeakMaps 用于缓存
现在代码是安全的,但是它的性能仍然可以升级,因为它每次调用返回函数时都会创建一个新的代理。可以使用缓存来避免,每次调用时,若沙箱对象相同,则可以使用同一个 Proxy
对象。
一个代理属于一个沙箱对象,所以我们可以简单地把代理添加到沙箱对象中作为一个属性。然而,这将会对外暴露我们的实现细节,并且如果不可变的沙箱对象被 Object.freeze()
函数冻结了,这就行不通了。在这种情况下,使用 WeakMap
是一个更好的替代方案。
WeakMap 对象是一个键/值对的集合,其中键是弱引用。键必须是对象,而值可以是任意值。
一个 WeakMap
可以用来为对象添加数据,而不用直接用属性来扩展数据。我们可以使用 WeakMaps
来间接地为沙箱对象添加缓存代理。
const sandboxProxies = new WeakMap()
function compileCode (src) {
src = 'with (sandbox) {' + src + '}'
const code = new Function('sandbox', src)
return function(sandbox) {
if (!sandboxProxies.has(sandbox)) {
const sandboxProxy = new Proxy(sandbox, {has, get})
sandboxProxies.set(sandbox, sandboxProxy)
}
return code(sandboxProxies.get(sandbox))
}
}
function has(target, key) {
return true
}
function get(target, key) {
if (key === Symbol.unscopables) return undefined
return target[key]
}
这样,每个沙箱对象只能创建一个Proxy
。