数组

数组是值的有序集合,数组中每个值称为元素,元素在数组中的位置称为索引。JavaScript中的数组是一种特殊的对象:

  • 类属性class attributeArray

  • 新元素添加到数组后,自动更新自身的length属性

  • Array.prototype对象中继承方法

  • 设置length属性值小于元素个数时,会截断数组

数组的特点
  • JavaScript中,数组的元素可以是任意JavaScript值,并且同一个数组中不同元素值的类型可以不同

  • 数组的索引是基于0的32位数值,最大的索引为(2^32 - 1) - 1

  • 数组是动态的,可以根据需要增长或缩短

  • 数组的操作方法大部分定义在Array.prototype原型对象上,使所有的数组对象都可以使用

  • 数组的实现经过了优化,用索引访问元素的效率高于直接访问常规对象的属性

  • 数组可能是稀疏或者稠密的。


1 数组的创建与访问

1.1 创建数组

有两种方法可以创建数组:数组字面量[1, 2, 3]new Array(1, 2, 3)推荐使用数组字面量创建数组,简洁易懂。

对象字面量创建的数组,其元素可以是表达式、对象或其他数组。如果省略数组中字面量的值,数组会变为稀疏数组。

var empty = [];   //  <==> var  empty = new Array();数组字面量的写法更简洁
var primes = [2, 3, 5, 7, 11];
var misc = [1.1, true, 'a'];    //数组中元素可以是任意类型值

var base = 1024;
var table = [base, base + 1, base + 2, base + 3];  // ==> [ 1024, 1025, 1026, 1027 ]

var b = [[1, {a: "1", b: 2}], [true, {x: 2, y:3}]];

通过Array()构造函数也可以创建数组,但是使用比较繁琐,根据传入参数个数的不同,分为三种情况:

  • 不传入参数:var a = new Array()a数组与[]相同

  • 传入一个非负整数:var a = new Array(10);创建一个存放10个数组元素的空间,但是此时没有定义索引属性和存储值,此时的数组是稀疏的

  • 传入2个或多个数组元素或者一个非数值元素:var c = new Array('1', 2, true, "text");,创建的数组['1', 2, true, "text"]

稀疏数组与稠密数组

根据数组是否有不连续的数组,将其分为稀疏数组和稠密数组

稀疏数组的length属性大于元素的个数,在实现上比稠密数组更慢,消耗内存空间更大,查找元素的效率与常规对象相似。

要避免使用稀疏数组,其查找效率很低。稀疏数组空缺的部分没有值和索引,因为不能用for-in循环遍历出空缺的部分,for-in循环可以遍历对象的可枚举属性(自身和继承)

var undefs = [, , ];   //数组字面量允许逗号结尾,是稀疏数组,没有元素,使用for-in循环不会输出任何内容
var count = [1, , 3];   //稀疏数组,使用for-in循环只输出[1, 3]
var arr = new Array(10);   //稀疏数组,没有赋值和索引

a = new Array(5);   //数组没有元素,a.length为5,稀疏
a[1000] = 0;        //a.length为1001,索引大于元素个数,稀疏

a = [, , , ];  //是稀疏数组,0 in a;  => false
a = [1, , 3];   //稀疏数组,长度为3
a = [,];        //数组没有元素,长度是1

总结:数组创建尽量使用字面量方法,如果数组的索引不连续,则数组是稀疏数组,查询效率低,内存消耗高

1.2 数组的访问

数组是特殊的对象,可以利用对象属性的访问方式.[],因为数组元素的索引是非负数的字符串,所以需要使用[]访问,而数组的属性(如length则可以用.访问)。

利用[]可以读写数组的元素:

var a = ["Hello"];
var value = a[0];        //读取第一个元素
a[1] = 3.14;         //写入第二个元素

数组是特殊的对象

  • 所有的数组都是对象,但对象不一定是数组;

  • 数组对象的属性名在0~2^32-2之间的是索引,可以为数组创建任意属性,只有当属性是索引时,才更新其length属性

  • 使用负整数、与整数等的浮点数作为属性时,会将其转化为索引

    a[-1.23] = true;   //创建一个名为"-1.23"的属性
    a[-3] = false;   //创建一个名为"-3"的属性
    a["1000"] = 0;     //数组的第1001个元素为值为0
    a[2.000];  //与a[2]相同

注:JS中没有越界的概念,因为是将数组作为对象来处理,对于不存在的下标,将其当做普通属性添加到数组对象上。

总结:使用[]访问数组元素,使用.访问数组属性,如.length

1.3 数组的长度

对于稠密数组,其length属性代表元素的个数,索引从0开始,最大值是length - 1。如果是稀疏数组,其length属性大于元素的个数。数组的长度保证大于每个元素的索引值

[].length;   //==>0,数组没有元素
[1, 2, 3].length;   //==>3,最大索引为2,length为3
  • 为数组元素赋值,如果其索引i大于等于现有数组长度,length的属性值将设置为i+1

  • 如果设置的lengthn小于数组长度时,会将索引大于n的元素全部删除。

  • 在ES5中,可以通过Object.defineProperty()设置数组的length属性的描述符descripter{writable: false},使其变为只读属性,设置之后不能添加或者删除数组元素

    a = [1, 2, 3, 4, 5];
    a.length = 3;             //数组变为[1, 2, 3]
    a.length = 0;            //删除所有数组元素
    
    a = [1, 2, 3];
    //注意设置为只读后,不能在为数组添加元素,除非修改writable为true
    Object.defineProperty(a, 'length', {writable: false});
    a.length = 2;    //a.length不会改变
    a.length;      //==>3

2 数组的操作

Array.prototype对象中定义许多关于数组操作的方法。

2.1 数组元素的添加与删除

push()unshift()方法
  • push()方法:在数组末尾插入一个或多个元素,与pop()方法相对应,返回插入元素后数组的新长度

  • unshift()方法:在数组的首部插入一个或多个元素,与shift()方法相对应,返回插入元素后的数组新长度。注意一次插入多个元素和多次插入一个元素的效果是不同的

pop()shift()方法
  • pop()方法:在数组末尾删除一个元素,返回删除的元素;

  • shift()方法:在数组首部删除一个元素,返回删除的元素;

    var a = [];
    a.push('one');  // ==> ['one'],在末尾添加一个元素
    a.push('two', 'zero');  // ==> ['one', 'two', 'zero'],在末尾添加两个个元素
    
    a.pop();      // ==>"zero",a变为['one', 'two']
    a.unshift('four'); // ==> a变为['four', 'one', 'two']
    a.shift();   // ==> 删除"four"

注:会修改调用该方法的数组

splice()方法
  • splice(start, num, insert1, insert2,...)方法是插入、删除和替换数组元素的通用方法。

  • 参数start:插入或删除的起始位置

  • 参数num:需要删除元素的个数,如果不指定会将起始位置后的所有元素删除

  • insertinsert2...:需要插入的元素

    var a = [1, 2, 3, 4, 5, 6, 7];
    a.splice(5);   //==>[6, 7],a变为[1, 2, 3, 4, 5]
    a.splice(2, 2);   //==>[3, 4],a变为[1, 2, 5]
    a.splice(-1, 1);   //==>[5],a变为[1, 2]
    
    a.splice(1, 0, 'a', false, 0);   // ==>[],没有删除元素,a变为[1, "a", false, 0, 2]
    a.splice(2, 2, ['x', 'y']);     // ==>[false, 0],a变为[1, "a", ['x', 'y'], 2]

注:会修改调用该方法的数组

2.2 数组的遍历

使用for循环是遍历数组的常见方法,可以配合部分if语句过滤部分元素。

for(var i=0, len=arr.length; i<len; i++) {
  if(!a[i]) {continue;}    //过滤掉null、undefined和不存在的元素
  if(a[i] === undefined) {continue;}     //过滤掉undefined和不存在的元素
  if(!(i in arr)) {continue;}      //in操作符可以检测对象是否含有某个属性,数组没有索引时,返回false,可以过滤掉不存在的元素
}

对于稀疏数组,可以使用for-in循环历来过滤掉不存在的索引。for-in循环会访问继承的可枚举属性,利用hasOwnProperty()方法过滤掉非自身属性。

for(var i in p) {
  if(!p.hasOwnProperty(i)) {continue;}  //跳过继承的属性
}
forEach()方法

由于for-in循环本身是未遍历对象而设计,如果数组有其余可枚举属性,需要专门过滤。ES5定义forEach()方法来遍历数组。

forEach(fn)方法接收一个函数作为参数,将每个元素分别调用该函数。forEach()可以给fn传递三个参数,fn(value, index, array)

  • value:数组元素

  • index:数组元素对应的索引

  • array:数组本身

    var a = [1, 2, 3, 4, 5];
    //计算数组元素的和
    var sum = 0;
    a.forEach(function(value) {   //只传递数组元素一个参数
      sum += value;
    });
    console.log(sum);   //  ==> 15
    //将数组每个元素加2
    a.forEach(function (value, index, array) {
      array[index] = value + 2;
    });
    console.log(a);   // ==>[3, 4, 5, 6, 7]

forEach()方法不能在所有元素都传递给fn函数调用前终止遍历(不能使用break跳出循环),如果要提前终止循环,需要将forEach()方法放在try块中,如果forEach()调用的函数fn能抛出foreach.break异常,遍历提前终止。

function foreach(value, index, array) {
  try {
    a.forEach(index, array);
  } catch(e) {
    if(e === foreach.break) {
      return;
    } else {
      throw e;
    }
  }
}

2.3 多维数组

JS并不支持真正的多维数组,可以利用数组的数组来进行模拟,使用[][]访问即可。

var table = new Array(10);   //表格10行
var len=table.length;
for(let i=0; i<len; i++) {
  table[i] = new Array(10);   //10列
}
//初始化表格
for(let row=0; row<len; row++) {
  for(let col=0; col<table[row].length; col++) {
    table[row][col] = row * col;
  }
}
var product = table[5][7];     //  ==> 35

3 数组的方法

Array.prototype中定义了与数组操作相关的方法,主要分为ES3和ES5两个部分

3.1 ES3中的数组操作方法

Array.prototype.join():不修改原字符串

Array.prototype.join(seperator)方法将数组中所有元素先转化为字符串,再利用参数seperator传入的分隔符将元素转换成的字符串拼接起来,最后返回该字符串。默认使用,分隔。String.prototype.split()方法的逆向操作

var a = [1, 2, 3];
a.join();   // ==> "1,2,3"
a.join(" ");   // ==> "1 2 3"
a.join("");   // ==> "123"
"1,2,3".split(",").join();  //==> "1,2,3"
Array.prototype.reverse():会修改原数组

将数组中的元素顺序颠倒,返回逆序后的数组

var a = [1, 2, 3];
a.reverse.join();   //"3,2,1",并且a变为[3, 2, 1],所以注意使用副本来操作
Array.prototype.sort():会修改原数组

将数组中元素按照指定的顺序后返回,默认按照字母表的顺序怕排序。undefined值被排到最后,要使用其他方式排序,需要传入一个比较函数fn(a, b),依据两个参数在排序好的数组中的先后顺序:

  • 如果第一个参数在第二个参数之前,函数返回值小于0

  • 如果第一个参数在第二个参数值后,函数返回值大于0

  • 如果两个参数相等,函数返回值等于0

    var arr = ['banana', 'cherry', 'apple'];
    arr.sort();
    arr.join(",");   //==> "apple,banana,cherry",默认按照字母表顺序
    
    var a = [1, 2, 3, 4];
    a.sort(function(a, b) {
      return a - b;    //如果目标是升序,a在b的前面,函数返回值小于0即可
    });
    a.sort(function(a, b) {
      return b - a ;   //如果目标是降序,a在b的后面,函数返回值大于0即可
    });
    
    //不区分大小写升序排列
    var arr = ['ant', 'Dog', 'Bug', 'cat'];
    
    arr.sort(function(first, second) {
      first = first.toLowerCase();    //将字符串全部转化为小写
      second = second.toLowerCase();
      if(first < second) {       //如果正确顺序是first在second前,则返回-1
        return -1;
      } else if (first > second) {
        return 1;
      } else {
        return 0;
      }
    });
Array.prototype.concat():不修改原数组

用于拼接原始数组与传入的参数,组成一个新的数组并返回。如果传入的参数是数组,会连接最外层的数组元素

var a = [1, 2, 3];
a.concat(4, 5);   // ==> [1, 2, 3, 4, 5]
a.concat([4, 5]);  //  ==> [1, 2, 3, 4, 5]
a.concat(4, [5, [6, 7]]);  //==> [1, 2, 3, 4, 5, [6, 7]]  
// 将数组[5, [6, 7]]最外层拆开,但是保留内部
Array.prototype.slice():不改变原数组

返回指定数组的一个片段或数组:

  • 通过两个参数指定切割的起始位置与结束位置(不包含第二个位置的元素)

  • 如果只有一个参数,返回从开始位置到数组结束的所有元素

  • 参数接收负数,-1表示最后一个元素,-2表示倒数第二个元素

    a = [1, 2, 3, 4, 5];
    a.slice(0, 3);   // ==> [1, 2, 3]
    a.slice(3);   // ==> [4, 5]
    a.slice(1, -1);   // ==> [2, 3, 4]
toString()toLocalString()

先将数组的每个元素转化为字符串,再利用逗号将其拼接为一个字符串,其中toLocaleString()方法区别在于转换DateNumber类型的数字、日期和时间时,根据时区、数字来转化。

  • 转换结果与不传入参数的join()方法类似

[1, 2, 3].toString();   // ==> "1,2,3"
['a', [1, 'b']].toString();  // ==> "a,1,b"

3.2 ES5中操作数组的方法

ES5中定义了9个方法来遍历forEache()、映射map()、过滤filter()、检测every() some()、简化reduce() reduceRight()和搜索indexOf() lastIndexOf()数组。九种方法不会修改调用它的数组,但是传入的函数会。

  • 九种方法基本都属于过程抽象,第一个参数基本都是一个函数,对数组的每个元素调用该函数

Array.prototype.map()

map(fn)只有一个参数,map()方法传递三个参数给fn(value, index, array),根据需要选择。其调用方式与forEach()相似,但是传递给map(fn)的函数fn有返回值。

  • map()返回的是新数组,不修改原数组

  • 如果是稀疏数组,返回稀疏素组;具有相同的长度与缺失元素

    var a = [1, 2, 3];
    a.map(function(value) {
      return value * value;     //map()传入的函数必须要有返回值,将返回值作为返回数组的元素的值
    });          //==> [1, 4, 9],原数组a还是[1, 2, 3]不变
Array.prototype.filter()

filter()方法返回的数组是调用数组的一个子集。通过传递的函数进行逻辑判断,如果返回值是true,当前元素被添加到返回的子集中;如果返回false,则过滤掉该元素;

  • filter()会跳过稀疏数组中缺失的元素,返回的总是稠密数组

  • arr.filter(function() {return true;}); 会过滤掉所有缺失的元素;

  • arr.filter(funtion(x) {return (x !== undefined && x !=null)}):过滤掉undefinednull

    var a = [5, 4, 3, 2, 1];
    a.filter(function(value) {
      return value < 3;
    });        // ==> [2, 1]
    a.filter(function(value, index) {
      return index % 2 === 0;  //过滤掉索引为奇数的元素
    });     // ==> [5, 3, 1],不会改变原数组
Array.prototype.every()Array.prototype.some()

every()some()用于对数组进行逻辑判断,返回true或`false;每个元素应用传入的函数进行判断:

  • every()方法:当且仅当数组中所有元素调用函数的判断结果为true时,才返回true

  • some()方法:至少有一个元素调用函数的判断结果为true时,返回true

  • every()some()在确定返回truefalse后会停止遍历数组。类似&&||的短路特性

  • 在空数组上调用,根据惯例,every()返回truesome()返回false

    var a = [5, 4, 3, 2, 1];
    a.every(function(value) {
      return value < 10;
    });      //  ==>true,所有的值都小于10
    a.every(function(value) {
      return value % 2 === 0;
    });    // ==> false,不是所有元素都是偶数
    
    a.some(function(value) {
      return value % 2 === 0;
    });    // ==> true,数组a中有偶数
    a.some(isNaN);   //  ==> false,a中所有元素都不是非数值元素
Array.prototype.reduce()Array.prototype.reduceRight()

reduce()reduceRight():使用指定函数将数组中的元素进行组合,生成单个值。在函数式编程中常用,称为注入和折叠。ruduceRight()的工作原理与reduce()相同,只是从右侧开始索引

reduce(fn, init)有两个参数:

  • fn是执行简化操作的函数;

  • init是传递给简化函数的初始值,如果不指定,默认为数组的第一个元素

reduce()传递给化简函数fn(init, value, index array)四个参数:

  • init:第一次调用函数时,initreduce()传入的初始值,如果reduce()没有指定第二个参数,默认使用数组的第一个元素;以后的调用中,其值等于化简函数的返回值

  • valueindexarray:分别是数组元素、元素索引和数组本身。

    var a = [1, 2, 3, 4, 5];
    a.reduce(function(init, value) {
      return init + value;
    }, 0);    //初始值init是0,返回累加的结果==> 15
    //初始值为1,如果不指定,默认为数组的第一个元素
    a.reduce(function(init, value) {return init * value;}, 1);  //  ==>120
    a.reduce(function(init, value) {return (init > value)? init: value;});
    
    var a = [2, 3, 4];
    //计算2^(3^4),乘方操作的优先顺序是从右到左。
    a.reduceRight(function(init, value) {return Math.pow(value, init);});

a.reduce(function(init, value) {return init + value;}, 0); 的执行过程:

  1. 化简函数第一次调用时,reduce()方法将0传递给函数的init参数,将数组的第一个元素传递给value

  2. 执行化简函数,将返回值赋予init,再次继续下次调用

Array.prototype.indexOf()Array.prototype.lastIndexOf()

搜索整个数组中指定值,返回第一个匹配值的索引,如果没有找到返回-1indexOf()从头到尾查找、lastIndexOf()从尾到头查找。

indexOf()lastIndexOf()的参数没有函数,第一个参数是查找的值;第二个参数可选,指定开始查找的索引位置

//查找数组中所有出现的value,返回一个包含匹配值索引的数组
function findall(arr, value) {
  var results = [];
  var len = a.length;
  var index = 0;         //开始查找的索引

  while(index < len) {   //arr.indexOf(arr, index)中第二个参数是为了过滤掉之前已经查找过的数组元素
    index = arr.indexOf(value, index);    //避免重复的同时,加快了搜索效率
    if(index === -1) {break;}      //如果未找到,便退出;因为forEach()退出循环不方便,所以使用while
    results.push(index);
    index += 1;
  }
}

4 类数组对象

4.1 数组类型

判断一个未知的对象是否为数组,可以使用ES5提供的Array.isArray()方法,定义在Array()构造器函数上,没有定义在Array.prototype原型上。

Array.isArray([]);    //  ==>true
Array.isArray({});    //  ==>false

因为typeofinstanceof在判断Array类型时比较繁复,所以使用同一的Array.isArray()来判断。isArray()方法的可能实现:

//Function.isArray || function(o)用来判断程序中是否有同名的isArray函数,确保不会重写原有函数。
var isArray = Function.isArray || function(o) {
  return (typeof o === 'object') && (Object.prototype.toString.call(o) === '[object Array]');
};

4.2 类数组对象

JS中数组的部分特性是其余对象没有的:

  1. 新元素添加到数组后,自动更新length属性

  2. 设置length为较小值时,截断数组

  3. Array.prototype中继承属性

  4. 类属性为Array

这4点并不是数组的本质。但是可以将一个用于length属性(值为数值),其余属性为非负整数(索引)的对象看做类数组。由于数组的方法通用性很强,可以使用数组的方法来操作类数组对象,只是类数组对象并未继承Array.prototype对象,所以需要Function.call()来间接调用。

var a = {'0': 'a', '1': 'b', '2': 'c', 'length': 3};   //类数组对象
Array.prototype.join.call(a, ',');      // ==> 'a,b,c'
Array.prototype.slice.call(a, 0);     //  ==> ['a', 'b', 'c']真正数组的副本
Array.prototype.map.call(a, function(value) {
  return value.toUpperCase();
});      //  ==> ['A', 'B', 'C']

函数的参数arguments对象和document.getElementsByTagName()获取的对象都是类数组对象。检测一个对象是否是类数组对象:

//判断o是否为类数组对象
//字符串和函数有length属性,使用typeof将其排除
//在DOM中,文本节点有length属性,要额外调用o.nodeType !== 3排除
function isArrayLike(o) {
  if(o && typeof o === 'object' &&    //非null和undefined,并且是对象
      isFinite(o.length) &&
      o.length >= 0 &&
      o.length == Math.floor(o.length) &&   //o.length是整数
      o.length < Math.pow(2, 32)) {   //数组索引的上限是2^32-1
    retun true;
  } else {
    return false;
  }
}
4.3 作为数组的字符串

在ES5中,字符串的行为类似于只读的数组,可以使用charAt()访问单个字符,也可以通过[]访问单个字符。

var s = 'test';
s.charAt(0);   // ==> 't'
s[1];   // ==> 'e'

使用[]的索引形式访问字符串,可以简化charAt()的调用。因为字符串用于非负的整数length属性,每个字符的索引是非负整数,可以将字符串看做类数组对象,使用数组的方法来操作字符串。

  • 调用的方法不能改变原字符串,splice()sort()reverse()pop()push()shift()unshift()方法不能使用,使用数组方法修改字符串会导致错误,但是没有错误提示

var s = "javascript";
Array.prototype.join.call(s, " ");  // ==>"j a v a s c r i p t"

总结

  • 使用对象字面量方式创建数组

  • 数组元素的读写都可以通过[]

  • 对于稀疏数组,其length属性大于元素的个数

  • ES5之后,单纯的数组遍历可以使用forEach(),对于需要判断跳出循环遍历的,可以使用for循环替代、或使用try-catch

  • 数组的操作方法都定义在Array.prototype原型对象上,可以对数组进行:forEach()map()every()some()filter()reduce()reduceRinght()indexOf()lastIndexOf()splice()sort()reverse()

  • 类数组对象指有非负整数的length值,并且索引为非负整数的对象,类数组对象可以使用数组的方法来操作,使用Function.call()来间接调用即可


Kyxy
316 声望10 粉丝