本系列一共七章,Github 地址请查阅这里,原文地址请查阅这里。javascript
这是编写一个前端框架系列的第三章,本章我将会阐述浏览器端不一样的代码求值的方法及其所产生的问题。我也将会介绍一个方法,它依赖于一些新颖或者少见的 JavaScript 功能。前端
eval()
函数用于对字符串形式的 JavaScript 代码进行求值。java
代码求值的最多见的解决方案即便用 eval()
函数。由 eval()
执行的代码可以访问闭包和全局做用域,这会致使被称为代码注入 code injection 的安全隐患,正所以让 eval()
成为 JavaScript 最臭名昭著的功能之一。git
虽然让人不爽,可是在某些状况下 eval()
是很是有用的。大多数的现代框架须要它的功能,可是由于上面提到的问题而不敢使用。结果,出现了许多在沙箱而非全局做用域中的字符串求值的替代方案。沙箱防止代码访问安全数据。通常状况下,它是一个简单的对象,这个对象会为求值代码替换掉全局的对象。github
替代 eval()
最多见的方式即为彻底重写 - 分两步走,包括解析和解释字符串。首先解析器建立一个抽象语法树(AST),而后解释器遍历语法树并在沙箱中解释为代码。浏览器
这是被最为普遍使用的方案,可是对于如此简单的事情被认为是牛刀小用。从零开始重写全部的东西而不是为 eval()
打补丁会致使易出不少的 bug, 而且它还要求频繁地修改以匹配语言的升级更新。缓存
NX 试图避免从新实现原生代码。代码求值是由一个使用了一些新或者冷门的 JavaScript 功能的小型库来处理的。安全
本节将会按部就班地介绍这些功能,而后由它们来介绍 nx-compile 是如何运行代码的。此库含有一个被称为 compileCode()
的库,运行方式相似如下代码:bash
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
函数。前端框架
函数构建器建立了一个新的函数对象。在 JavaScript 中,每一个函数都其实是一个函数对象。
Function
构造器是 eval()
的一个替代方案。new Function(...args, 'funcBody')
对传入的 'funcBody'
字符串进行求值,并返回执行这段代码的函数。它和 eval()
主要有两点区别:
function compileCode(src) {
return new Function(src)
}
复制代码
new Function()
在咱们的需求中是一个更好的替代 eval()
的方案。它有很好的性能和安全性,可是为使其可行须要屏蔽其对全局做用域的访问。
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 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
是一个著名的标记。一个著名的标记便是一个内置的 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]
}
复制代码
如今代码是安全的,可是它的性能仍然能够升级,由于它每次调用返回函数时都会建立一个新的代理。可使用缓存来避免,每次调用时,若沙箱对象相同,则可使用同一个 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
。
以上的 compileCode
例子是一个只有 19 行代码的可用的沙箱代码评估器。若是你想要看 nx-compile 库的完整源码,能够参见这里。
除了解释代码求值,本章的目标是为了展现如何利用新的 ES6 功能来改变现有的功能,而不是从新发明它们。我试图经过这些例子来展现 Proxies
和 Symbols
的全部功能。