数组方法的探究

71 阅读12分钟

关于数组的学习,Array类定义的方法用处最大,是学习的重点。在学习这些方法时,要记住其中有些方法会修改调用它们的数组,有些则不会。另外还有些方法会返回数组,有时候返回的是新数组,有时候返回的是被修改的数组的引用。

接下来会集中介绍几个相关的数组方法,并且会探索它们的原理。

  • 迭代器方法用于遍历数组元素,通常会对每个元素调用一次指定的函数。
  • 栈和队列方法用于在数组的开头或结尾添加或删除元素。
  • 子数组(切片)方法用于提取、删除、插入、填充和复制更大数组的连续区域。
  • 搜索和排序方法用于在数组中查找元素和对数组元素排序。

数组迭代器

这些方法都会接收一个函数作为第一个参数,并且对数组的每个元素(或某些元素)都调用一次这个函数。如果数组是稀疏的,则不会对不存在的元素调用。多数情况下,该函数调用时会收到3个参数,分别是数组元素的值、索引和数组本身。通常我们只需要这几个参数中的第一个,可以忽略后面两个。

多数迭代器方法还可以接收可选的第二个参数,该参数会成为第一个参数传入的函数内部的this值。

forEach()

forEach()方法 遍历数组的每个元素,并对每个元素调用一次指定的函数。

  • 搭建一个HTML结构,一个列表和一组JSON数据。
<ul>
    <li></li>
    <li></li>
    <li></li>
</ul>
<div id="data">
    [        {            "name": "张三",            "age": 18        },        {            "name": "李四",            "age": 19        },        {            "name": "王五",            "age": 20        }    ]
</div>
  • 使用forEach实现列表展示JSON数据。
<script>
    var data = JSON.parse(document.getElementById('data').innerHTML),
        oLi = document.getElementsByTagName('li');

    data.forEach(function(element, index, array){
        this[index].innerHTML = element.name;
    }, oLi);
</script>

我们可以探究一下forEach()的原理,它循环的过程以及如何修改this指向。

  • 注意第二个修改this指向的参数是可选的,所以使用实参列表arguments[1]去传,如果没有传入第二个参数则依旧指向window
  • call和apply都可以修改this指向,但是apply更适合传参。
Array.prototype.myForEach = function(fn){
    var arr = this,
        len = arr.length,
        self = arguments[1] || window;

    for(var i = 0; i < len; i++){
        // call和apply都可以修改this指向,但是apply更适合传参。
        fn.apply(self, [arr[i], i, arr]);
    }
}

var data = JSON.parse(document.getElementById('data').innerHTML),
    oLi = document.getElementsByTagName('li');

data.myForEach(function(element, index, array){
    this[index].innerHTML = element.name;
}, oLi);

filter()

filter()方法 返回一个新数组,该数组包含调用它的数组的子数组。传给该方法的函数应该是个断言函数,即返回为truefalse的函数,当函数返回true或返回值可以转换为true,则传给这个函数的元素就是最终返回的子数组的成员。

var arr = [1, 2, 3, 4, 5];
var newArr = arr.filter(function(elem){
    return elem > 3;
});
console.log(newArr); // [4, 5]

我们继续探究一下filter()的原理。

  • filter()是要返回一个新数组的,所以其中必定要新建一个空数组,用于存放子数组的成员。
  • 对于函数返回值的判断,可以用三目运算,如果是truepush对应的元素(注意push的元素是深拷贝的),如果是false就返回一个空字符串,什么也不做。
Array.prototype.myFilter = function(fn){
    var arr = this,
        len = arr.length,
        self = arguments[1] || window,
        newArr = [],
        item;

    for(var i = 0; i < len; i++){
        item = tools.deepClone(arr[i);
        fn.apply(self, [item, i, arr]) ? newArr.push(arr[i]) : '';
    }

    return newArr;
}

var arr = [1, 2, 3, 4, 5];
var newArr = arr.myFilter(function(elem){
    return elem > 3;
});
console.log(newArr); // [4, 5]

map()

map()方法 把调用它数组的每个元素分别传给指定的函数,返回这个函数的返回值构成的新数组,不会修改调用它的原数组。

var arr = [1, 2, 3];
var newArr = arr.map(function(element){
    return element * 10;
});
console.log(newArr); // [10, 20, 30]

我们继续探究一下map()的原理。

  • push的元素就是函数执行的返回值。
  • 注意push的元素是深拷贝的。
Array.prototype.myMap = function(fn){
    var arr = this,
        len = arr.length,
        self = arguments[1] || window,
        newArr = [],
        item;

    for(var i = 0; i < len; i++){
        item = tools.deepClone(arr[i]);
        newArr.push(fn.apply(self, [item, i, arr]));
    }

    return newArr;
}

var arr = [1, 2, 3];
var newArr = arr.myMap(function(element){
    return element * 10;
});
console.log(newArr); // [10, 20, 30]

every()与some()

every()和some()方法都是数组断言方法,即它们会对数组元素调用传入的断言函数,最后返回true和false。

every()方法 它在且在断言函数对数组的所有元素都返回true时才返回true,在断言函数第一次返回false时返回false,也就意味着第一次返回false时停止停止遍历数组。

var arr = [1, 2, 3, 4, 5];
arr.every(function(element){
    console.log('test'); // 'test'字符串打印了三次之后停止了遍历。
    return element < 3;
});

some()方法 只要数组元素中有一个让断言函数返回true它就会返回true,即第一次返回true时停止遍历数组。只有当断言函数对所有数组元素都返回false时它才会返回false

var arr = [1, 2, 3, 4, 5];
arr.some(function(element){
    console.log('test'); // 'test'字符串打印了一次之后停止了遍历。
    return element < 3;
});

重构every()some()方法,这里只写了every()的原理,some()其实是一样的,取反就行。

Array.prototype.myEvery = function(fn){
    var arr = this,
        len = arr.length,
        self = arguments[1] || window,
        bool = true;
    for(var i = 0; i < len; i++){
        if(!fn.apply(self, [arr[i], i, arr])){
            bool = false;
            break;
        }
    }
    return bool;
}

var arr = [1, 2, 3, 4, 5];
arr.myEvery(function(element){
    console.log('test'); // 'test'字符串打印了三次之后停止了遍历。
    return element < 3; 
});

reduce()与reduceRight()

reduce()与reduceRight()方法使用指定的函数归并数组元素,最终产生一个值。在函数编程中,归并是一个常见操作,有时也叫注入(inject)或折叠(fold)。

reduce()方法 接收两个参数,第一个参数是执行归并操作的函数。第二个参数是可选的,是传给归并函数的初始值。

  • 归并函数中有四个参数,第一个参数用于返回归并操作的累计结果,它的初始值是reduce()传入的第二个参数。归并函数的其它三个参数就和之前那些函数一样,值、索引和数组本身。
var initValue = [];
var arr = [1, 3, 3, 4, 8, 6, 7, 8, 9, 10];
var newArr = arr.reduce(function(prev, elem, index, arr){
    if(elem % 2 === 0){
        prev.push(elem);
    }
    return prev;
}, initValue);

console.log(newArr); // [4, 8, 6, 8, 10]
  • 如果没有指定初始值,那么reduce()调用时会使用数组的第一个元素作为初始值。
var arr = [1, 2, 3, 4, 5];
var res = arr.reduce(function(prev, elem){
    console.log(prev);
    // 1
    // 3
    // 6
    // 10
    return prev + elem;
});
console.log(res); // 15

再来看一个关于reduce()的案例,首先获得div标签中的字符串并通过split()方法分割成数组。再用该数组去调用reduce()方法,在函数体内再次对数组元素进行分割,每一次遍历都将其对应数组元素按=分割成两个子元素。prev的值是第二参数传入的一个空对象,这两个子元素分别作为空对象的属性名属性值,形成一组键值对。

<div id="J_cookieData">
    DSJLFSFLKSD=JDLAJDKSLAJLDKAJKLDALK;DSADA=313213123131;
    DKJLASDJKLSADAKLAS=DADKASKLA;DJSALDAK=1231231;
    DASJDLAKL=DASD;D1K2=L3KL321JLK;
    ASKDSA_DA=12313_2131_ 3131;KDADKAL=3;
    SDJKADKLSDLK312LK3=321331JKL2JLK3L;JDAKLD=1231M321M;
</div>
<script>
    var cookieDatas = document.getElementById('J_cookieData').innerHTML,
        cookieArr = cookieDatas.split(';');

    var cookieObj = cookieArr.reduce(function(prev, elem){
        var item = elem.split('=');
        prev[item[0]] = item[1];
        return prev;
    }, {});

    console.log(cookieObj);
</script>

重构reduce()方法。

Array.prototype.myReduce = function(fn){
    var arr = this,
        len = this.length,
        self = arguments[2] || window,
        init = arguments[1] || arr[0];

    for(var i = 0; i < len; i++){
        init = fn.apply(self, [init, arr[i], i, arr]);
    }
    return init;
}

var arr = [1, 3, 3, 4, 8, 6, 7, 8, 9, 10];
var newArr = arr.myReduce(function(prev, elem, index, arr){
    if(elem % 2 === 0){
        prev.push(elem);
    }
    return prev;
}, []);

console.log(newArr); // [4, 8, 6, 8, 10]

数组拼接

concat()方法 创建并返回一个新数组,新数组包含原数组中的元素以及传给concat()的参数,参数可以是数组形式。该方法常用于数组的拼接。

var arr1 = [1, 2, 3];
var arr2 = arr1.concat(4, 5);
console.log(arr2); // [1, 2, 3, 4, 5]
// 注意原数组并未改变
console.log(arr1); // [1, 2, 3]

栈和队列操作

push()方法 在数组列表末尾添加一个或多个新元素,并返回数组的新长度

var arr = [1, 2, 3];
console.log(arr.push(4)); // 4
console.log(arr); // [1, 2, 3, 4]

push()方法的原理是根据当前数组的长度决定元素的添加位置的。

var arr = [1, 2, 3];
Array.prototype.pushDemo = function(){
    for(var i = 0; i < arguments.length; i++){
        this[this.length] = arguments[i];
    }
    return this.length;
}

console.log(arr.pushDemo(4, 5, 6)); // 6
console.log(arr); // [1, 2, 3, 4, 5, 6]

pop()方法 删除数组最后一个元素,并返回被删除的值。

var arr = [1, 2, 3];
console.log(arr.pop()); // 3
console.log(arr); // [1, 2]

unshift()方法 从数组开头插入元素。

var arr = [1, 2, 3];
console.log(arr.unshift(4)); // 4
console.log(arr); // [4, 1, 2, 3]
  • splice()方法重写unshift()方法。
// 重写unshift方法
Array.prototype.unshiftDemo = function(){
    var len = arguments.length,
        arremp;
    for(var i = 0; i < len; i++){
        arremp = arguments[i];
        this.splice(i, 0, arremp);
    }
    return this.length;
}
var arr = [1, 2, 3, 4];
arr.unshiftDemo(5, 4, 3, 2, 1);
console.log(arr); // [5, 4, 3, 2, 1, 1, 2, 3, 4]
  • concat()方法重写unshift()方法。
// 重写unshift方法
Array.prototype.unshiftDemo2 = function(){
    var argArr = Array.prototype.slice.call(arguments);
    var newArr = argArr.concat(this);
    return newArr;
}
var arr = [1, 2, 3];
var newArr = arr.unshiftDemo2('a', 'b', 'c');
console.log(newArr); // ['a', 'b', 'c', 1, 2, 3]

shift()方法 从数组开头删除元素。

var arr = [1, 2, 3];
console.log(arr.shift()); // 1
console.log(arr); // [2, 3]

数组切片

数组定义了几个处理连续区域(数组“切片”)的方法。

slice()方法 返回一个数组的切片,它可以接收两个参数,分别用于指定要返回切片的起止位置,它不会修改原数组。

slice()传递一个参数时,返回的数组包含从起点开始直到数组末尾的所有元素。

var arr1 = ['a', 'b', 'c', 'd', 'e', 'f'];
var arr2 = arr1.slice(1);
console.log(arr2); // ['b', 'c', 'd', 'e', 'f']

slice()传递两个参数时,返回的数组包含第一个参数指定的元素,以及所有后续元素(但不包含第二个参数指定的元素),简单来说就是[start, end)这样的一个区间。

var arr1 = ['a', 'b', 'c', 'd', 'e', 'f'];
var arr2 = arr1.slice(1, 3);
console.log(arr2); // ['b', 'c']

splice()方法 可以从数组中删除元素或向数组中插入新元素。第一个参数指定插入或删除操作的起点位置。第二个参数指定删除(切割)的元素个数。如果省略第二个参数,从起点位置删除所有元素。该方法返回的是被删除元素的数组。如果没有删除元素则返回空数组。

它和slice()方法不同的是,它是直接对原数组进行修改。

var arr = [1, 2, 3, 4, 5, 6, 7, 8];
console.log(arr.splice(4)); // [5, 6, 7, 8]
console.log(arr); // [1, 2, 3, 4]

arr.splice(1, 2);
console.log(arr); // [1, 4]

splice()的第一个参数除了是正数,还可以设置为负数,比如最后一个元素的位置也可以用-1表示。

var arr = ['a', 'b', 'c', 'd'];
console.log(arr.splice(-1, 1)); // ['d']
  • 负数索引的原理如下:
var arr = ['a', 'b', 'c', 'd'];
function splice(arr, index){
    return index += index >= 0 ? 0 : arr.length;
}

console.log(arr[splice(arr, -1)]); // 'd'

splice()的前两个参数用于指定要删除哪些元素,而这两个参数后面还可以跟任意多个参数,表示在指定的位置插入到数组中的元素。并且删除的元素个数可以为0,也就意味着可以直接添加元素。

var arr = ['a', 'b', 'c', 'e'];
// 删除的元素个数为0,所以返回空数组。
console.log(arr.splice(3, 0, 'd')); // []
console.log(arr); // ['a', 'b', 'c', 'd', 'e']

数组索引

indexOf()和lastIndexOf()

indexOf()方法和lastindexOf()方法 从数组中搜索指定的值并返回第一个找到的元素的索引,如果没找到就返回-1indexOf()从前到后搜索数组,lastindexOf()从后往前搜索数组。

indexOf()方法和lastindexOf()方法还可以接收第二个参数,指定从数组的哪个位置开始搜索。

var arr = [1, 2, 3, 4];
console.log(arr.indexOf(3)); // 2
console.log(arr.indexOf(5)); // -1

数组排序

sort()方法 对数组元素就地排序并返回排序后的数组。在不传参调用时,按字母顺序(ASCII码)对数组元素排序(如有必要,临时把它们转换为字符串比较。)

var arr = [-1, -5, 8, 0, 2];
arr.sort();
console.log(arr); // [-1, -5, 0, 2, 8]

sort()方法传参调用可以对数组元素执行非字母顺序的排序,必须给传一个比较函数作为参数,这个函数中有两个参数。如果返回值小于0,则第一个参数在前面。如果返回值大于0,则第二个参数在前面。

var arr = [27, 49, 5, 7];

arr.sort(function(a, b){
    return a - b;
});
console.log(arr); // [5, 7, 27, 49]

// 倒序排列
arr.sort(function(a, b){
    return b - a;
});
console.log(arr); // [49, 27, 7, 5]

sort()方法也可以实现随机排序。

var arr = [1, 2, 3, 4, 5, 6];

arr.sort(function(a, b){
    // 创建一个0~1之间的随机数。
    var rand = Math.random();
    return rand - 0.5;
});

console.log(arr);

sort()方法实现按元素的字节数排序。

function getBytes(str){
    var sum = 0,
        len = str.length;
    for(var i = 0; i < len; i++){
        var char = str.charCodeAt(i);
        if(char > 255){
            sum += 2;
        }else{
            sum++;
        }
    }
    return sum;
}

var arr = ['apple', 'orange', 'hello' ];
arr.sort(function(a, b){
    return getBytes(a) - getBytes(b);
});
console.log(arr); // ['apple', 'hello', 'orange']

reverse()方法 对元素就地倒序排列,并返回倒序后的数组。

var arr = ['a', 'b', 'c'];
console.log(arr.reverse()); // ['c', 'b', 'a']
console.log(arr); // ['c', 'b', 'a']

数组与字符串转换

toString()方法 数组中的toString()方法是重写过的方法,把数组中的所有元素转换为字符串,然后把它们拼接起来并返回结果字符串,用逗号分隔。

var arr = [1, 2, 3];
console.log(arr.toString()); // '1', '2', '3'

join()方法 把数组中的所有元素转换为字符串,把它们拼接起来并返回结果字符串。它和toString()方法的区别是可以传递一个可选的字符串参数,用于分隔结果字符串中的元素。如果不指定分隔符,则默认使用逗号。

var arr = ['a', 'b', 'c', 'd'];
var str1 = arr.join();
console.log(str1); // a,b,c,d

var str2 = arr.join('-');
console.log(str2); // a-b-c-d

split()方法 将字符串按指定分隔符转换为数组,它有两个参数可以设置,第一个参数指定分隔符,第二个参数指定 切片长度。

var arr = ['a', 'b', 'c', 'd'];
var str1 = arr.join('-');
console.log(str1); // a-b-c-d

var arr1 = str1.split();
console.log(arr1); // ['a-b-c-d']
var arr2 = str1.split('-');
console.log(arr2); // ['a', 'b', 'c', 'd']

var arr3 = str1.split('-', 2);
console.log(arr3); // ['a', 'b']