整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

JS中的重载-如何实现一个类似这样的功能,我也想玩玩

JS中的重载-如何实现一个类似这样的功能,我也想玩玩

数重载

在其他语言中,我们一般都听说过重载的概念,对于一个方法,如果我们期望传入不同的参数类型,或者传入不同的参数个数,甚至传入参数的顺序不一样,而去执行不同的处理代码,以返回相应的结果,那么他们的方法名是可以相同的,而且它们不会产生冲突,这就是所谓的重载。

这是因为像java等这样的强类型语言,他们具有类型约束和方法签名,这样就使得他们能够根据调用时传入参数的情况来决定使用哪一个同名方法来处理需要的操作。

是在说我吗?

但是js属于弱类型语言,也就是说它不会规定参数必须传入哪种类型,定义的方法也不具有签名的功能,而且在定义多个同名方法时,类似于css里面的层叠样式表的效果,后定义的同名方法会覆盖前面的方法,还有一个就是函数具有提升的特性,这也就使得它无法实现重载的功能。

怎么办呢?

其实js中也确实不需要重载的功能,因为没有类型的约束,在一个方法里面就可以做很多自由的发挥,不但能满足需要重载的需求,而且还能玩一些花样。

不过话又说回来,没有了约束,就容易犯错,都用一个方法体来处理所有情况,就会容易出乱子,使得我们需要使用一堆的类似if-else的语句来做到这一点。

那么我们能不能在现有js运行方式的基础上,借鉴其他语言对于重载的运用,来绕个弯子变量来实现一下属于js自己的重载方式呢?

试一试

我们今天只讨论在js中变相实现重载的方式,而不讨论它的意义与实际应用场景,我们通过一个简单的例子,来支撑这个讨论,其他的交由你们来自由发挥。

让我们开始吧

js重载的方式

在js中主要有以下几种实现重载的方式:

  1. 使用剩余参数的形式来接收可变的参数,并通过一些判断手段来处理不同情况的逻辑。
  2. 使用arguments的形式,来动态判断需要执行的操作
  3. 使用proxy的形式来拦截函数的行为,以达到控制不同的逻辑分支。

前两种方式比较相似,思路一样,只是实现手段有所不同,用户需要自己写判断逻辑。第三种方式结合proxy来隐藏实现细节,让用户只关注各自的分工。但它们都是在参数动态判断方面做文章。

前两种方式的优缺点:

优点:可以直接定义函数或使用表达式

缺点:函数名不能相同,需要写较多的判断分支

第三种方式的优缺点:

优点:可以不用写各种参数形式的分支

缺点:只能使用表达式定义函数

由于前两种网上已经有很多的实现思路与方案,因此这里不再进行探讨,其中有很多奇妙的实现,可以做到在js中使用重载的思想。

所以在此我们只讨论第三种方案,我们下面来看一下它的思路是什么,是否满足重载的需求,它是如何实现的,以及它能满足我们什么样的需求?

这是什么呢?

需求假设

我们现在有这样一个场景和需求:

自己开了一家服装店,由于生意火爆,我们想要答谢新老顾客,现在推出了一个活动,全场无论任何服装,只要买一件就直接打95折,只要买两件就全部打9折,只要买三件就全部打85折,只要买四件及以上,就全部打8折。

如果用代码来实现,其实就是给方法中传入一个两个三个四个参数的问题,因此我们自然而然的就想到了使用重载来实现这个需求。

接下来我们就试着自己实现一个这样的功能,看看可不可以创建一个赋能方法来使某个业务处理函数具有重载的能力。

思路分析

要生成这样一个赋能方法,我们需要有对函数改造的能力,在创建业务处理函数的时候,最好能够改变它的默认行为,在执行的时候也能够对它进行拦截以做一些额外的操作。

那么我们很自然而然的就想到了使用Proxy,先生成一个Proxy函数,然后在给它设置属性的时候,我们进行拦截,把赋值操作中的value缓存起来,以备将来调用的时候做分支处理,根据参数的个数与类型来控制需要执行的业务逻辑代码。它真的能做到吗?我们来看一下下面的一步步代码实现。

实现需求

function Overload(defaultCall) {
  let func=defaultCall || new Function()
  func.overloadCached=[]
  return new Proxy(func, {
    set(target, prop, value) {
      if(prop==='load') {
        target.overloadCached.push(value)
      }
    },
    apply(target, thisArg, argumentsList) {
      for(let i=target.overloadCached.length - 1; i > -1; i--) {
        if(argumentsList.length===target.overloadCached[i].length || (argumentsList.length > target.overloadCached[i].length)) {
          return target.overloadCached[i].apply(thisArg, argumentsList)
        }
      }
      return target.apply(thisArg, argumentsList)
    }
  })
}
let sum=Overload()
sum.load=function (a) {
  return a * 0.95;
}
sum.load=function (a, b) {
  return (a + b) * 0.9;
}
sum.load=function (a, b, c) {
  return (a + b + c) * 0.85;
}
sum.load=function (a, b, c, d, ...arg) {
  return (arg.concat(a,b,c,d).reduce((total, cur)=> {return total + cur},0)) * 0.8;
}
console.log(sum(200));
console.log(sum(200, 300));
console.log(sum(180, 280, 190));
console.log(sum(270, 260, 310, 240));
console.log(sum(180, 220, 240, 210, 190));
//输出:190,450,552.5,864,832

可以看到,我们实现了一个Overload函数,用来返回一个Proxy,通过它去load不同的方法来实现对同名方法的重载,调用的时候只需要一个方法名即可,Proxy中我们对set(即设置该Proxy的值的操作)和apply(即执行该Proxy的操作)两种操作进行了拦截,用到了一个叫做overloadCached的属性来缓存我们的处理函数,在调用函数的时候,我们使用从后往前遍历的方式,来达到后定义优先生效的原则。

通过打印结果我们知道,它已经满足了我们的需求假设。

默认处理

从上面的代码中我们发现,Overload函数可以传入一个叫做defaultCall的参数,它是用来处理默认操作的,也就是说如果后面定义的所有方法都不能够处理的时候,将使用该默认函数进行处理,如果没有定义该函数,那么调用sum时如果没有满足的执行函数,将会返回undefined。

现在我们给它传入一个默认的处理函数,那么上面的需求将可以写成这样:

function Overload(defaultCall) {
  let func=defaultCall || new Function()
  func.overloadCached=[]
  return new Proxy(func, {
    set(target, prop, value) {
      if(prop==='load') {
        target.overloadCached.push(value)
      }
    },
    apply(target, thisArg, argumentsList) {
      for(let i=target.overloadCached.length - 1; i > -1; i--) {
        //注意这里的变化
        if(argumentsList.length===target.overloadCached[i].length) {
          return target.overloadCached[i].apply(thisArg, argumentsList)
        }
      }
      return target.apply(thisArg, argumentsList)
    }
  })
}
let sum=Overload(function () {
  return ([].__proto__.reduce.call(arguments, (total, cur)=> {return total + cur},0)) * 0.8;
})
sum.load=function (a) {
  return a * 0.95;
}
sum.load=function (a, b) {
  return (a + b) * 0.9;
}
sum.load=function (a, b, c) {
  return (a + b + c) * 0.85;
}
console.log(sum(200));
console.log(sum(200, 300));
console.log(sum(180, 280, 190));
console.log(sum(270, 260, 310, 240));
console.log(sum(180, 220, 240, 210, 190));
//输出:190,450,552.5,864,832

我们注意Overload函数的变化,现在依然满足上面的需求。

处理兼容

由于我们把四个参数即以上的处理函数改为通过传入默认函数的方式来实现,因此我们修改了Overload方法,这显然是不合理的,因为这样不设置默认函数的时候会出问题,因此我们做一个兼容处理,修改之后就变成了这样:

function Overload(defaultCall) {
  let func=defaultCall || new Function()
  func.overloadCached=[]
  return new Proxy(func, {
    set(target, prop, value) {
      if(prop==='load') {
        let str=value.toString()
        let m1=str.match(/\(.+?\)/)
        if(m1 && m1[0].indexOf("...") !=-1) {
          value.rest=true
        }
        target.overloadCached.push(value)
      }
    },
    apply(target, thisArg, argumentsList) {
      for(let i=target.overloadCached.length - 1; i > -1; i--) {
        if((argumentsList.length===target.overloadCached[i].length) || (target.overloadCached[i].rest && argumentsList.length > target.overloadCached[i].length)) {
          return target.overloadCached[i].apply(thisArg, argumentsList)
        }
      }
      return target.apply(thisArg, argumentsList)
    }
  })
}
//输出:190,450,552.5,864,832

现在使用这个Overload函数就已经能够处理上面的这两种情况了。我们设定了一个rest属性来给方法打上了一个标识。

需求延伸

如果我们现在在上面的需求基础上,又想要对金额做一些处理,比如希望能够加上$、¥、等前缀,来区分不同的币种。

这个时候我们需要增加新的重载函数,而加了币种之后的函数可能与现有的函数参数个数相同(比如sum('$', 220, 240)和sum(270, 260, 310)),这就造成了误判,那么我们能不能再做一个类型区分呢?

应该是可以的,但是我们必须约定一种格式,比如下面这种形式,我们需要在获取Proxy属性的时候(这里就用到了拦截获取Proxy属性的操作),将类型进行缓存,以便将来时候的时候来做类型的判断:

//我们约定了10种类型
//n→number
//s→string
//b→boolean
//o→object
//a→array
//d→date
//S→Symbol
//r→regexp
//B→bigint
//f→function
function Overload(defaultCall) {
  let func=defaultCall || new Function()
  func.overloadCached=[]
  func.modifier=[]
  return new Proxy(func, {
    get(target, property, receiver) {
      if(property !=='load') {
        target.modifier.push(property)
      }
      return receiver
    },
    set(target, prop, value) {
      if(['n','s','b','o','a','d','S','r','B','f'].includes(prop)) {
        target.modifier.push(prop)
      }
      if(prop==='load' || target.modifier.length !==0) {
        let str=value.toString()
        let m1=str.match(/\(.+?\)/)
        if(m1 && m1[0].indexOf("...") !=-1) {
          value.rest=true
        }
        value.modifier=target.modifier
        target.overloadCached.push(value)
        target.modifier=[]
      }
    },
    apply(target, thisArg, argumentsList) {
      for(let i=target.overloadCached.length - 1; i > -1; i--) {
        if((argumentsList.length===target.overloadCached[i].length) || (target.overloadCached[i].rest && argumentsList.length > target.overloadCached[i].length)) {
          if(target.overloadCached[i].modifier.length !==0){
            let ty={
              '[object Number]': ['n'],
              '[object String]': ['s'],
              '[object Boolean]': ['b'],
              '[object Object]': ['o'],
              '[object Array]': ['a'],
              '[object Date]': ['d'],
              '[object Symbol]': ['S'],
              '[object Regexp]': ['r'],
              '[object BigInt]': ['B'],
              '[object Function]': ['f'],
            }
            if(target.overloadCached[i].modifier.some((m, j)=> {
              return !ty[({}).__proto__.toString.call(argumentsList[j])].includes(m)
            })) {
              continue
            }
          }
          return target.overloadCached[i].apply(thisArg, argumentsList)
        }
      }
      return target.apply(thisArg, argumentsList)
    }
  })
}
let sum=Overload()
sum.load.n=function (a) {
  return a * 0.95;
}
sum.load.n.n=function (a, b) {
  return (a + b) * 0.9;
}
sum.load.n.n.n=function (a, b, c) {
  return (a + b + c) * 0.85;
}
sum.load.s.n.n=function (a, b, c) {
  return a + (b + c) * 0.85;
}
sum.load.n.n.n.n=function (a, b, c, d, ...arg) {
  return (arg.concat(a,b,c,d).reduce((total, cur)=> {return total + cur},0)) * 0.8;
}
sum.load.s.n.n.n=function (a, b, c, d, ...arg) {
  return a + (arg.concat(b,c,d).reduce((total, cur)=> {return total + cur},0)) * 0.8;
}
console.log(sum(200));
console.log(sum(200, 300));
console.log(sum(260, 310, 240));
console.log(sum('', 280, 190));
console.log(sum(180, 220, 240, 210, 190));
console.log(sum('$', 220, 240, 210, 190));
//输出:190,450,688.5,399.5,832,$688

我们现在已经加上了类型判断,通过传入的参数类型与个数的不同,能够相应的去执行对应的函数,其实参数的顺序一个道理,也是支持的。

类型扩展

上面的类型约定我们可能看起来怪怪的,而且比较难以理解,因此我们可以扩展一下类型约定的表示方式,改造后的Overload函数如下:

function Overload(defaultCall) {
  let func=defaultCall || new Function()
  func.overloadCached=[]
  func.modifier=[]
  return new Proxy(func, {
    get(target, property, receiver) {
      if(property !=='load') {
        if(property.indexOf(',') !==-1) {
          property.split(',').map(item=> {
            target.modifier.push(item)
          })
        }else{
          property.split('').map(item=> {
            target.modifier.push(item)
          })
        }
      }
      return receiver
    },
    set(target, prop, value) {
      let modi=null
      if(prop.indexOf(',') !==-1) {
        modi=prop.split(',')
      }else{
        modi=prop.split('')
      }
      if(modi.every(p=> {
        return ['n','s','b','o','a','d','S','r','B','f','number','string','boolean','object','array','date','Symbol','regexp','bigint','function'].includes(p)
      })) {
        modi.map(item=> {
          target.modifier.push(item)
        })
      }
      if(prop==='load' || target.modifier.length !==0) {
        let str=value.toString()
        let m1=str.match(/\(.+?\)/)
        if(m1 && m1[0].indexOf("...") !=-1) {
          value.rest=true
        }
        value.modifier=target.modifier
        target.overloadCached.push(value)
        target.modifier=[]
      }
    },
    apply(target, thisArg, argumentsList) {
      for(let i=target.overloadCached.length - 1; i > -1; i--) {
        if((argumentsList.length===target.overloadCached[i].length) || (target.overloadCached[i].rest && argumentsList.length > target.overloadCached[i].length)) {
          if(target.overloadCached[i].modifier.length !==0){
            let ty={
              '[object Number]': ['n','number'],
              '[object String]': ['s','string'],
              '[object Boolean]': ['b','boolean'],
              '[object Object]': ['o','object'],
              '[object Array]': ['a','array'],
              '[object Date]': ['d','date'],
              '[object Symbol]': ['S','Symbol'],
              '[object Regexp]': ['r','regexp'],
              '[object BigInt]': ['B','bigint'],
              '[object Function]': ['f','function'],
            }
            if(target.overloadCached[i].modifier.some((m, j)=> {
              return !ty[({}).__proto__.toString.call(argumentsList[j])].includes(m)
            })) {
              continue
            }
          }
          return target.overloadCached[i].apply(thisArg, argumentsList)
        }
      }
      return target.apply(thisArg, argumentsList)
    }
  })
}

这样我们就可以支持一下几种类型约定的书写形式:

sum.load.s.n.n=function (a, b, c) {
  return a + (b + c) * 0.85;
}
sum.load['snn']=function (a, b, c) {
  return a + (b + c) * 0.85;
}
sum.load.snn=function (a, b, c) {
  return a + (b + c) * 0.85;
}
//对于全称不能够写成.(点)的形式
sum.load['string,number,number']=function (a, b, c) {
  return a + (b + c) * 0.85;
}
//这四种形式的任意一种对于console.log(sum('$', 280, 190));
//都会输出:$399.5

到此为止,我们已经能够支持参数的个数、类型、顺序的不同,会执行不同的处理函数,满足了重载的基本需求,完成了我们在最开始的需求假设的实现。

结语

目前这种方式只能支持函数表达式的方式来进行重载,这里只是给大家提供一个自定义实现重载的方式,结合自己的业务场景,小伙伴们可以自由发挥,其实目前js的既有方式能满足我们需要重载的场景,而不需要额外设计重载的代码。

具体这种方式的优劣,大家可以自行判断,并且可以根据这种思路重新设计一下实现的手段。

谢谢

、JavaScript

1.JavaScript语言

JavaScript是ECMAScript的实现,由ECMA 39(欧洲计算机制造商协会39号技术委员会)负责制定ECMAScript标准。

ECMAScript发展史:

时间

版本

说明

1997年7月

ES1.0 发布

当年7月,ECMA262 标准出台

1998年6月

ES2.0 发布

该版本修改完全符合ISO/IEC 16262国际标准。

1999年12月

ES3.0 发布

成为 JavaScript 的通行标准,得到了广泛支持

2007年10月

ES4.0草案发布

各大厂商意见分歧,该方案未通过

2008年7月

发布ES3.1,并改名为ECMAScript 5

废除ECMAScript 4.0,所以4.0版本不存在

2009年12月

ESt 5.0 正式发布


2011年6月

ES5.1 发布

该版本成为了 ISO 国际标准(ISO/IEC 16262:2011)

2013年12月

ES6 草案发布


2015年6月

ES6 正式发布,并且更名为“ECMAScript 2015”

TC39委员会决定每年发布一个ECMAScript 的版本

2.JavaScript引擎

JavaScript引擎是指用于处理以及执行JavaScript脚本的虚拟机。

常见的JavaScript引擎:


??

我们描述了一个名为 Jalangi 的工具框架,用于 JavaScript 程序的动态分析和符号执行测试。该框架是用 JavaScript 编写的,允许对 JavaScript 进行各种重载动态分析。Jalangi 包含两个关键技术:1)选择性录制回放,这是一种能够录制并准确地回放用户选择的程序部分的技术;2)阴影值和阴影执行,可以轻松实现重量级动态分析,例如,condicolic 测试和污点跟踪。Jalangi 通过对源代码注入进行检测,这使得它可以实现跨平台移植。根据 Apache 2.0 许可,可以从https://github.com/SRA-SiliconValley/jalangi获得Jalangi。我们在SunSpider基准套件和五个Web应用程序上对Jalangi的评估表明,Jalangi在录制过程中的平均速度降低了26倍,在重放和分析过程中的速度平均降低了30倍。与类似工具(如PIN和针对x86二进制文件的Valgrind)报告的类似情况相比,速度的降低是可比的。

关键词

JavaScript;动态分析;符号执行测试

一 导言

JavaScript 是编写客户端 web 应用程序的首选语言,并且在编写移动应用程序(例如用于 Tizen OS 和 iOS)、桌面应用程序(如 Windows 8 和 Gnome 桌面应用程序)和服务器端应用程序(如 node.js)时越来越流行。但是,针对 JavaScript 应用程序的分析、测试和调试的工具较少。我们已经开发了一个简单而强大的框架,称为 Jalangi,用于为 JavaScript 编写重载动态分析。本文简要介绍了该框架及其使用场景。该框架提供了一些有用的抽象和 API,大大简化了 JavaScript 动态分析的实现。关于 Jalangi 背后的技术的详细描述可以在[6]中找到。

Jalangi 可以在任何浏览器或 node.js 上工作。我们通过选择性的源代码检测来实现浏览器的独立性。即使某些源文件没有检测,Jalangi 也可以运行。对 Jalangi 的分析分两个阶段进行。在第一阶段中,在用户选择的平台(如 Android 上运行的 mobile chrome)上执行并录制一个插入指令的 JavaScript 应用程序。在第二阶段中,录制的数据用于在桌面环境中执行用户指定的动态分析。

Jalangi 允许通过支持阴影值和阴影执行轻松实现动态分析。阴影值使我们能够将阴影值与程序中使用的任何值相关联。在 Jalangi 中,我们使用阴影值和执行实现了几个动态分析:1)共同语言测试,2)纯符号执行,3)跟踪 null 和 undefined 的来源,4)检测可能的类型不一致,5)简单的对象分配分析器,6)简单的动态污染分析。

二 技术细节

我们提供了 Jalangi 的技术细节概要。有关更详细的技术讨论,请参阅[6]。用户标识由 Jalangi 检测的 web 应用程序的一个子集,以便录制和回放。在录制阶段,将在用户选择的平台上执行生成的检测代码。即使使用了用户代码的子集,整个应用程序也会在录制阶段执行,包括已插入和未插入 JavaScript 代码以及本机代码。但是,在回放阶段,Jalangi 仅回放已检测的代码。Jalangi 具有在用户平台上完整执行 JavaScript 应用程序的功能,使录制的执行可以在开发笔记本电脑/台式机 JavaScript 引擎上进行调试,以进行调试,包括移动浏览器和基于 node.js 的系统,或具有嵌入式 JavaScript 引擎的集成开发系统。这种方法还支持使用支持阴影执行的底层阴影值实现动态分析。

通过录制执行期间从内存加载的每个值,并在回放期间在相关内存加载期间重新使用它们,可以有效地提供录制和回放。这种方法虽然听起来不错,但也存在一些挑战,例如:(1)如何有效地录制函数和对象?(2) 如何在未检测本机函数(例如,JavaScript 事件 dispather)调用检测函数时提供回放?通过提供间接记录(其中唯一的数字标识符与每个对象和功能相关联)以及记录这些标识符的值来解决第一个问题。第二个问题通过显式录制和调用已检测功能来解决,这些功能又从未插入代码中调用,或者由于 JavaScript 事件处理程序调度而执行。

此外,我们还观察到,回放期间的内存负载值可以通过执行检测代码来计算,而无需录制所有内存负载的值。通过仅记录必要的内存负载,这用于提高 Jalangi 的效率。为了确定是否必须记录内存负载的值,Jalangi 在记录阶段跟踪影子内存,该影子内存将在执行实际代码时随实际内存一起更新。 执行本机和未执行的代码不会对影子内存进行更新。为了确保在重放阶段可以使用正确的值,只有在记录阶段在内存位置存储的值存在差异时(例如,如果在内存位置的值之间存在差异),Jalangi 才会存储内存负载的值。 实际加载的内存及其关联值存储在影子内存中)。

在 Jalangi 中,在回放阶段执行中使用的任何值都可以替换为带注释的值,该值可以为实际使用的值提供附加信息。例如,支持污染分析所需的额外污染信息,或者可能是与符号执行中的实际值相关的信息,这些信息可以以符号表达式的形式提供。Jalangi 使用 concolvalue 类型的对象来表示带注释的值。

三 动态分析

在 Jalangi,我们进行了以下动态分析:

  • 符号执行测试:符号执行测试沿着具体的执行路径执行符号执行,生成表示输入值约束的逻辑公式,并求解约束以生成新的测试输入,这些新的测试输入将沿着以前未探索的路径执行程序。Jalangi 中的符号执行测试支持对整数,字符串和对象类型的约束以及新颖的类型约束。我们引入类型约束来处理 JavaScript 的动态特性,对于程序的不同可行执行路径,输入变量的类型可以不同。
  • 纯符号执行:纯符号执行象征性地执行程序,从不为了回溯而重新启动程序。它在执行分支语句之前检查状态,执行一个分支,然后使用检查点状态回溯以探索另一个分支。对于小程序,纯符号执行避免了由于重复重新启动而造成的时间浪费。
  • 跟踪空值和未定义值的来源:此分析录制空值和未定义值来源的源代码位置,并在由于空值或未定义值而发生错误时报告位置。每当由于这些文本(例如访问空值的字段)而出现错误时,就会向用户报告文本的阴影值。这样的报告有助于程序员轻松识别空值的来源。
  • 检测可能的类型不一致:动态分析检查在给定程序位置创建的对象是否可以采用多个不一致的类型。它计算在程序中的每个定义位置创建的对象和函数值的类型。如果在程序位置定义的对象或函数值在执行期间被观察到具有多个类型,则分析报告程序位置和观察到的类型。有时,这种类型的不一致可能会指向程序中的潜在错误。我们已经在两个 SunSpider 基准测试程序中注意到了这样的问题。
  • 简单对象分配探查器:此动态分析录制在给定分配站点创建的对象数量以及访问对象的频率。它报告在给定分配站点创建的对象是只读的还是常量。它还报告对象创建时间和对象的最新访问时间之间的最大和平均差异。如果分配站点创建的常量对象太多,则可能导致内存效率低下。我们在基准测试套件中的一个 web 应用程序中发现了这样一个问题。
  • 动态污染分析:动态污染分析是一种信息流分析形式,它检查信息是否可以从一组特定的内存位置(称为源)流向另一组内存位置(称为汇)。我们在 Jalangi 中实现了一种简单的动态污染分析形式。在分析中,我们将任何对象的任何字段的读取(以前未由检测源编写)视为污染源。我们将任何可能改变程序控制流的内存位置的读取都视为接收器。我们附加污染信息与实际值的阴影值。

四 实施

Jalangi 可在https://github.com/SRA SiliconValley/Jalangi 上获得。我们已经用 JavaScript 实现了 Jalangi。

Jalangi 通过检测 JavaScript 代码进行操作。表 3 显示了在表 1 中插入代码后获得的代码。在检测期间,Jalangi 从 Jalangi 库插入各种回调函数。回调函数列在表 2 中。这些函数在 JavaScript 中包装了各种操作。Jalangi 的选择性录制重放引擎是通过定义这些回调函数来实现的。

Jalangi 将工具库公开为函数工具代码。这也使我们能够动态地测试在运行时创建和计算的任何代码。例如,我们将对 eval(s)的任何调用修改为 eval(instrumentCode(s))。

五 Jalangi 的表现

我们在 JavaScript-SunSpider(http://www.webkit.org/perf/SunSpider/SunSpider.html)基准套件中的26个程序和使用HTML5/JavaScript为Tizen操作系统编写的5个web应用程序上运行了Jalangi的录制回放。(https://developer.tizen.org/下载/示例web应用程序)。表4显示了与录制阶段和三个动态分析相关的开销:无分析(用null表示)、跟踪空和未定义的来源(用track表示)和污染分析(用taint表示)。我们还报告为每个基准程序录制的值的数量。实验是在配备2.3 GHz Intel Core i7 和 8 GB RAM 的笔记本电脑上进行的,并在 Chrome 25 上运行了网络应用,并在 node.js 0.8.14 上执行了重放。

我们没有衡量 web 应用程序的增长速度,因为它们大多是交互式应用程序。对于 SunSpider 基准套件,在记录阶段,我们观察到平均速度降低了 26 倍,最低为 1.5 倍,最高为 93 倍。 在重播阶段进行空分析时,我们观察到平均速度降低了 30 倍,最小值为 1.5 倍,最大值为 93 倍。 跟踪分析显示,平均速度降低了 32.75 倍,最小值为 1.5 倍,最大值为 96 倍。

5.1 JALANGI 动态分析检测到的问题

Jalangi 可能的类型不一致检查器发现,SunSpider 基准测试套件的 3d-cube.js 中的 CreateP 函数主要用作构造函数,但在某个位置它被称为函数。作为函数调用的结果,程序在全局对象中创建一个不必要的 V 字段。我们认为此调用可能是编程错误。

Jalangi 的对象分配分析器注意到附件游戏 webapp 中的 getValue(place,_board)方法创建了一个常量对象数千次。我们相信,通过在方法之外提升常数对象,可以避免这种不必要的常数对象的创建。

六 相关工作

据我们所知,Jalangi 是第一个 JavaScript 动态分析框架。很少有工具可以执行 JavaScript 程序的录制回放。JSBench 使用录制回放机制创建 JavaScript 基准。Mugshot 捕获所有事件,以确定地重放 web 应用程序的执行。Ripley 在服务器端副本上复制客户端 JavaScript 程序的执行。

七 结论

Jalangi 已经处理了 JavaScript 的各种具有挑战性的细节。由于可以处理 JavaScript 的所有令人担忧的问题,因此可以轻松地在 Jalangi 框架中实施动态分析。我们期望 Jalangi 将有助于未来 JavaScript 动态分析的研究。

致谢

本文由南京大学软件学院 2020 级硕士李彤宇转述翻译 感谢国家重点研发计划(2018YFB1003900)和国家自然科学基金(61832009,61932012)支持!