Web 技术研究所

我一直坚信着,Web 将会成为未来应用程序的主流

API设计慎用访问器属性

  在封装不需要兼容低版本浏览器的库时可以直接使用ES5的特性,给封装的对象定义getter和setter这些访问器属性。虽然这些东西很好用,但是访问器属性往往会被用户认为是一种比普通方法高效的东西,如果把低效的方法定义成访问器属性可能会带来一些性能问题。
  这个问题我是在封装线性代数相关的库时想到的。比如矩阵求逆或行列式求值之类的操作就不适合定义成访问器属性,因为他们的计算量比较大,属于慢操作。甚至我都不想把向量求模长的计算定义成访问器属性,虽然它只有一个求和运算和一个平方根运算。
  其实也不是完全不能作为访问器属性来定义,如果为其实现了一套完善的缓存机制的话也许就可以。还是拿矩阵求逆举例好了。如果我的程序监控了矩阵的变化,而在矩阵有任何变化后清除掉先前求逆的缓存数据。当用户访问矩阵的逆时如果有缓存则直接返回缓存,否则才去计算。这样通常就不会有性能问题,只是监控矩阵的变化这个操作可能会有一点性能开销。也许矩阵这种变化不会太大的东西这么实现不成问题,也许存在一些很难监控其变化的东西,所以并不是所有情况都能用缓存来解决的。
  也许大家对这种算法类的封装不感冒吧,那我就举个其它例子。比如我要实现一个不用支持对象类型作为key的Set,那么length要怎么设计?如果把它用访问器属性设计,每次访问时去统计元素数量的话就会变成这样
<script src="http://www.web-tinker.com/share/performance.js"></script>
<button>for(var i=0;i&lt;m.length;i++);</button>
<script>
function MySet(){
  var data={__proto__:null};
  this.setItem=function(key,value){data[key]=value;};
  this.getItem=function(key){return data[key];};
  Object.defineProperty(this,"length",{
    get:function(){ // 时间复杂度 O(n)
      var count=0;
      for(var i in data)count++;
      return count;
    }
  });
};
var m=new MySet;
//写入测试数据
for(var i=0;i<1E3;i++)m.setItem(i,"ite"+i+1);
</script>
  这么直接遍历会直接卡成狗!首先遍历的时间复杂的O(n)和获取length属性的O(n)嵌套起来变成了O(n²)的复杂度。

  如果实现对这个length访问器属性的缓存的话结果就不同了(其实这个例子可以不用实现缓存,直接动态维护length,实际上Live的HTMLCollection之类的集合都是动态维护的)。不过这里为了演示缓存,就使用了缓存的做法。
<script src="http://www.web-tinker.com/share/performance.js"></script>
<button>for(var i=0;i&lt;m.length;i++);</button>
<script>
function MySet(){
  var data={__proto__:null},length;
  this.setItem=function(key,value){
    data[key]=value;
    length=void 0; //当数据有变化时清除length
  };
  this.getItem=function(key){return data[key];};
  Object.defineProperty(this,"length",{
    get:function(){
      //如果存在length就直接使用,否则才计算
      if(length!==void 0)return length;
      length=0;
      for(var i in data)length++;
      return length;
    }
  });
};
var m=new MySet;
//写入测试数据
for(var i=0;i<1E3;i++)m.setItem(i,"ite"+i+1);
</script>

  其实对于数据结构复杂的东西,使用缓存也未必容易,而且性能具体能提高多少也和数据本身的结构有关。上面这个例子比较典型,所以性能得到了很大提升。但别忘了它在写入数据的代码中多了一行,不能忽略它的开销。对于一些很难优化的情况,我的建议是别用访问器属性,直接将接口定义成方法。比如上面的例子,如果实例不实现length属性,只提供一个getLength方法,那么用户也不会直接将这个方法放进循环里(当然不排除一些只学过三天编程的用户,这时候也真没办法),至少这样会潜在地告诉别人“我是个慢操作”。
网名:
3.80.32.*
电子邮箱:
仅用于接收通知
提交 悄悄的告诉你,Ctrl+Enter 可以提交哦
神奇海螺
[查看全部主题]
各类Web技术问题讨论区
发起新主题
本模块采用即时聊天邮件通知的模式
让钛合金F5成为历史吧!
次碳酸钴的技术博客,文章原创,转载请保留原文链接 ^_^