去除数组重复项

前几天在QQ群里有人提出了一个关于动态插入DOM元素的问题,并由此引出了如何删除数组中重复项的讨论,为此我写了一个小函数来实现这个需求:

var pureMultil=function(arr){
  var obj={};
  var a = [];
  for(var i=0,l=arr.length;i < l;i++){
      if(!(arr[i] in obj)){
        a.push(arr[i]);
      }
      obj[arr[i]]="";
  }
  return a;
}

原理很简单:就是先把数组的项保存在一个obj字面量对象里,之后在循环中通过判断当前数组的项是否已然在obj字面对象里,并随着做不同的操作。

今天重新想到了这段代码,翻开来看看了,想到了操作符”&&”的运行机制,就利用它的特点(操作符”||”跟它正好相反):如果左边的判断为true就继续执行右边的操作。为此,修改了上面的实现方式:

var pureMultil=function (arr){
  var obj={};
  var a = [];
  for(var i=0,l=arr.length;i < l;i++){
	  !(arr[i] in obj) && a.push(arr[i]) && (obj[arr[i]]="");
  }
  return a;
}

通过测试,在同样的条件下,速度居然呈倍数提高~《测试用例1》,示例中另外的一种实现方式由hehe123提供,不过性能上欠佳。

============================================================================

2010-6-20 updates:

为了纠正Wait在评论中所提出的对数据类型的判断来正确的处理像这样的数组:[1,"1"]。修改了下代码:

var pureMultil=function (arr){
  var obj={},a = [],m,n;
  for(var i=0,l=arr.length;i < l;i++){
    m = arr[i];
	n = typeof m;
    (obj[n+m] !== m) && a.push(m) && (obj[n+m] = m);
  }
  return a;
}

或者使用obj的判断形式:

var pureMultil2=function (arr){
  var obj={},a = [],m,n;
  for(var i=0,l=arr.length;i < l;i++){
    m = arr[i];
	n = typeof m;
    !((n+m) in obj) && a.push(m) && (obj[n+m] = "");
  }
  return a;
}

代码中加入了对数据类型的判断,这在循环中可想而知在性能消耗上也更多了,情况不是很佳,衡量而行,或者另寻方案。《测试页面2

倒是如果使用普通的方式,倒显得性能最佳而且也无需字面量对象的创建和复杂的判断,虽然代码量上多了许多:《测试页面3

var include=function (arr,item){
  for(var i=0,l=arr.length;i < l;i++){
     if(arr[i] === item){
        return true;
	 }
  }
  return false;
}

var normalPureMultil=function (arr){
  var a=[];
  for(var i=0,l=arr.length;i < l;i++){
    if(!include(a,arr[i])){
       a.push(arr[i]);
	}
  }
  return a;
}

总结:对于小数组形式的去除重复项,可以考虑上面代码精简的方案来处理,这样可以节省文件的大小;而如果是大数组(上千数目)来说,而且文件大小还可以凑合的话,使用普通的方式,还是比较合适的。

对循环操作的几种优化

下面根据我平时编写代码和浏览其他库的源文件的时候发现的几种对循环操作的优化方法。循环操作可以说在我们平时的编程中是经常会遇到和使用的步骤,大家可能会习以为常的使用一种方式,比如:

//假设arr为一个现成的数组
for(var i=0;i<arr.length;i++){
//......
}

这种方式是我们最初接触到的原始的方式,这种方式的缺点就是每次循环的时候都需要计算数组的length属性,要知道这个操作在IE的低版本中是很低效率的。

下面我们来看一下另外几种循环方式对循环操作的优化。首先定义一个大数组,方便测试效果明显:

var arr=[];
for(var i=0;i<100000;i++)
    arr[i]=i;

第一种方式:先使用变量储存数组的length属性,这就解决了每次循环需要计算数组的length属性所使用的额外的操作。但是这种优化还不够明显。

for (var i = 0, l = arr.length; i < l; i++) {
      if (arr[i] === n)
            break;
}

第二种方式:使用while来循环遍历数组,并使用一个变量来存储数组的每一项并作为判断条件。这种方式比上面第一种方式又优化了许多。变量上面的大数组,基本都是用了0s。它的原理就在于使用变量来存储数组的某一项,并作为循环判断条件,这就少了for循环中的第三个参数设置的索引递增的操作。

var i = -1, c = 0;
while (c = arr[++i])
      if (c === n)
             break;

这种方式还有一种变体,如果是从后往前遍历数组,可以使用这样方式:

var i=arr.length;
while(i--){
    //....
}

第三种方式:还是使用for循环来优化,这种方式跟while的原理很相似,都是使用变量来存储数组的某一项并作为判断条件。这种方式的使用时间跟第二种方式基本持平。

for (var i = 0, e; e = arr[i++];)
      if (e === n)
           break;

空口无凭,实例作证!请浏览测试网页:TestLoop.html

对数组、对象进行深复制

大家曾否想过这样的问题:当你要对一个数组,或者对象进行复制的时候,怎么进行复制?
我们都知道,除了三种基本的数据类型外(Number,Boolean,String)的数据赋给另外一个变量的时候,这样该变量得到的不是这些数据的副本,而是一个指向该数据的引用,它的变化,会影响原来的数据对象。这可怎么得到这种数据对象的引用呢?
看下面的代码:

var arr1 = [ [1,2,3] , [1,2,3] , [1,2,3] ];
var arr2 = arr1;

//arr2的改变会使arr1也同时改变
arr2[0] = 'replaced';
// arr2 = [ 'replaced' , [1,2,3] , [1,2,3] ];
// arr1 = [ 'replaced' , [1,2,3] , [1,2,3] ];
// arr2 === arr1; // This is TRUE

那下面用我们平时的做法来重新声明一个数组,通过循环来赋值:

var theCopy = []; // An new empty array
for (var i = 0, len = arr1.length; i < len; i++) {
    theCopy[i] = arr1[i];
}

// &amp;quot;theCopy&amp;quot; = [ 'replaced' , [1,2,3] , [1,2,3] ]

但是大家注意到没有,arr1的第二和第三个元素还是一个数组,这样theCopy的第二第三个元素还是得到了arr1中相应位置的数组元素的引用,还是有关联。还没有实现深度赋值。接着看下面的代码:

function deepCopy(obj){
    //如果obj是数组
    if (Object.prototype.toString.call(obj) === '[object Array]') {
        var out = [], i = 0, len = obj.length;
        for (; i < len; i++) {
            //arguments.callee == deepCopy
            //当然可以用deepCopy代替arguments.callee
            out[i] = arguments.callee(obj[i]);
        }
        return out;
    }
    //如果是对象
    if (typeof obj === 'object') {
        var out = {}, i;
        for (i in obj) {
            out[i] = arguments.callee(obj[i]);
        }
        return out;
    }
    return obj;
}

上面的deepCopy才真正的实现了深度复制,通过递归的技巧来循环遍历数组或者对象的每一个属性。这样就能够深入到数组或者对象的“里面”,实现真正的深度复制。

有些人或许会问这个方法有什么实际使用价值,当然,这样做的人其实不多,但是当你需要使用一个已经存在的数组里的数据,而又不想因为操作这些数据的时候改变原来数组的元素内容,这个时候你就需要得到数组的副本,而不是引用了。