blog icon indicating copy to clipboard operation
blog copied to clipboard

重学js-数组

Open Pasoul opened this issue 5 years ago • 8 comments

前言

数组是值的有序集合。数组每个元素所处的位置以数字表示,称为索引。数组元素索引不一定要连续的,它们之间可以有空缺,每一个JavaScript数组都有一个length属性。针对非稀疏数组,该属性就是数组元素的个数,针对稀疏数组,length比所有元素的索引要大。

JavaScript数组是JavaScript对象的特殊形式,数组的索引实际上碰巧是整数的属性名差不多。通常,数组的实现是经过优化的,用数字索引来访问数组一般来说比访问常规属性的对象要快很多。

数组继承自Array.prototype中的属性,它定义了一套丰富的数组操作方法,大多数这些方法对‘类数组’也有效。

Pasoul avatar Jun 10 '19 08:06 Pasoul

创建数组

两种创建数组的方法:

数组直接量:

var empty = []; // 没有元素的数组
var arr = [,,]; // 数组有两个元素,都是undefined
var count = [1,,3]; // 数组有3个元素,中间的那个元素值为undefined

数组直接量的值不一定要是常量,也可以是任意的表达式:

var base = 1234;
var table = [vase, base+1, base+2];

如果省略数组直接量中的某个值,省略的元素将被赋予undefined值:

var count = [1,,3]; // 数组长度为3,中间的元素为undefined

数组直接量的语法允许可选的逗号结尾,故[,,]只有两个元素而非三个。

调用构造函数Array()

有三种方式调用构造函数

  1. 调用时没有参数:

var a = new Array();

  1. 调用时指定数组长度:

var a = new Array(10); 该技术创建指定长度的数组。当预先知道所需元素个数时,这种形式的Array()构造函数可以分配一个数组空间。注意,数组中没有存储值,甚至数组的索引属性“0”,“1”等还未定义。

  1. 显示指定两个或多个数组元素或者一个非数值元素:

var a = new Array(1,2,3,4,'test');

Pasoul avatar Jun 10 '19 09:06 Pasoul

数组元素的读和写

使用[ ]操作符来访问和谐数组的一个元素。请记住,数组是对象的特殊形式,使用方括号访问数组元素就像使用方括号访问对象的属性一样。JavaScript将指定的数字索引值转换成字符串--索引值1变成'1',然后将其作为属性名来使用。关于索引值从数字转换成字符串没什么特别之处:对常规对象也可以这么做:

o = {}
o{1} = 'one'

清晰的区分数组的索引和对象的属性名是非常重要的。所有的索引都是属性名,但只有0 ~ 2^23 - 2之间的整数属性名才是索引。所有的数组都是对象,可以为其创建任意名字的属性。

注意,可以使用负数和非整数来索引数组,这种情况下,数值转换为字符串,字符串作为属性名来用。

a[-1.23] = true; // 创建一个名为-1.23的属性
a["1000"] = 0; // 这是数组的第1001个元素,和a[1000]相等
a[1.000] // 和a[1]相等

事实上数组索引仅仅是对象属性名的一种特殊类型,这意味着JavaScript数组没有越界的概念。当试图查询对象中不存在的属性时,不会报错,只会返回undefined.

Pasoul avatar Jun 11 '19 04:06 Pasoul

稀疏数组

稀疏数组就是包含从0开始的不连续索引的数组,length属性值大于元素的个数。虽然绝大多数JavaScript数组不是稀疏数组,但是了解稀疏数组是了解JavaScript数组真实本质的一部分。

var  a = new Array(5); // 数组没有元素,但是length是5
a = [];
a[1000] = 0; // 数组只有一个元素,但是length是1001

delete操作符也可以产生稀疏数组。

足够稀疏的数组通常实现上比稠密的数组更慢、内存利用率更高,在这样的数组中查找元素的时间与常规对象属性的查找时间一样长。

注意,在数组直接量中省略值时不会创建稀疏数组。省略的元素在数组中是存在的,其值是undefined。这和数组元素根本不存在是有一些微妙的区别的,可以用in操作符检测两者的区别

var a1 = [,]; // 此数组没有元素,长度是1
var a2  = [undefined]; // 该数组根本没有元素
0 in a1; // false: a1在索引0出没有元素
0 in a2; // true: a2在索引0处有一个值为undefined的元素

Pasoul avatar Jun 11 '19 07:06 Pasoul

使用delete操作符删除数组元素

可以像删除对象属性一样使用delete运算符来删除数组元素,需要注意的是,delete操作符不影响数组的长度,也不会将元素从高索引处移下来填充已删除元素的空白, 这个数组会变成稀疏数组。

a = [1,2,3];
delete a[1]; // a在索引为1的位置不再有元素
1 in a; // false: 数组索引1并未在数组中定义
a.length // 3:delete操作符不影响数组长度

Pasoul avatar Jun 11 '19 08:06 Pasoul

数组遍历

使用for循环遍历数组元素是最常见的方法:

var arr = [1,2,3];
for (var i = 0; i < arr.length; i++) {
 // ...
}

在嵌套循环或者其他性能非常重要的上下文中,可以看到这种基本的数组遍历需要优化,数组的长度都应该只查询一次,而非每次循环都要查询

var arr = [1,2,3];
for (var i = 0, len = arr.length; i < len; i++) {
 // ...
}

如果处理的数组是稀疏的,想要排除不存在的元素:

for (var i = 0, len = arr.length; i < len; i++) {
 if (!(i in arr)) continue;
 // ...
}

还可以通过for/in处理稀疏数组,循环每次将一个可枚举的属性名(包括数组索引)赋值给循环变量,不存在的索引不会遍历到:

var array = [1, , 3];
for (var i in array) {
        console.log(i); // 0 2
}     

注意,for/in循环能够枚举继承的属性名,如添加到Array.prototype中的方法,由于这个原因,在数组上不应该使用for/in循环

Array.prototype.customMethods = function() {};
var array = [1, 2];
for (var i in array) {
      console.log(i); // 依次打印:0,1,customMethods
}

定义在数组上的属性也会被遍历出来:

var array = [1, 2];
array["customProps"] = "customProps";
for (var i in array) {
        console.log(i); // 依次打印:0,1,customProps
}

ECMAScript规范允许for/in循环以不同的顺序遍历对象的属性,通常数组的遍历时升序的,但不能保证一定是这样。如果我们依赖于遍历的顺序,最好不要用for/in循环

Pasoul avatar Jun 11 '19 09:06 Pasoul

数组方法

ECMAScript 3中的数组方法

ECMAScript 3Array.prototype中定义了一些很有用的操作数组的函数,如join()reverse()sort()concat()slice()splice()push()pop()unshift()shift()toString()toLocaleString()

这里toString()join()方法很类似,都是将数组输出为字符串,区别是join()可以指定数组元素分隔符,它们都有一个方便的功能,结合split()方法将多维数组扁平化为一维数组:

[1, [2,3]].join().split() // [1,2,3]

ECMAScript 5中的数组方法

ECMAScript 5定义了9个新的数组方法,其中大多数方法的第一个参数接收一个函数,并且对数组的每个元素(或一些元素)调用一次该函数。如果是稀疏数组,对不存在的元素不调用传递的函数。大多数情况下,调用的函数使用三个参数:数组元素、元素的索引和数组本身。

ECMAScript 5的数组方法都不会去修改原始数组。

  1. forEach()方法从头到尾遍历数组,然后为每个元素调用指定的函数。注意,forEach()方法无法在所有元素都传递给调用的函数之前终止遍历,也就是说,没有像for循环中使用的相应的break语句,如果要提前终止,比如把forEach()方法放在一个try块中,并能抛出一个异常:
try {
      [1, 2, 3].forEach(v => {
        if (v == 2) {
          throw new Error('error')
        }
        console.log(v)
      })
} catch(e) {}
  1. map方法和forEach方法类似,区别是会返回一个操作后的数组,它并不会改变原数组。如果是稀疏数组,返回的也是相同形式的稀疏数组:它拥有相同的长度,相同的缺失元素

b = [1, 2, 3].map(v => v * 2); // [2, 4, 6]

  1. filter方法返回的数组元素是调用的数组的子集,传递的函数是用于逻辑判定的,该函数返回truefalse。如果为true,则传递给判定函数的元素就是这个子集的成员。

注意:filter会跳过稀疏数组中缺少的元素,它的返回数组总是稠密的,为了压缩稠密数组的空缺,代码如下:

    var arr = new Array(100);
    arr.push(101);
    console.log(arr)
    var result = arr.filter(v => {
      return true
    });
    console.log(result)

image

  1. everysome

everysome方法是数组的逻辑判定:它们对数组元素应用指定的函数进行判定,返回truefalseevery方法:当且仅当针对数组中的所有元素调用判定函数都返回true,它才返回true

a = [1,2,3,4,5];
a.every(function(x) { return x < 10; }) // true, 所有的值都 < 10
a.every(function(x) { return x % 2 === 0; }) // false,不是所有的值都是偶数

some方法:当数组中至少有一个元素调用判定函数返回true,它就返回true;并且当且仅当数组中所有元素调用判定函数都返回false,它才返回false

a = [1,2,3,4,5];
a.some(function(x) { return x % 2 === 0 }) // true,a中含有偶数
a.some(isNaN) // false,a中不含有非数组元素
  1. reducereduceRight

reducereduceRight方法使用指定的函数将数组元素进行组合,生成单个值。这在函数式编程中是常见的操作,也可以称为“注入”和“折叠”。 举例说明它是怎么工作的:

  var a = [1,2,3,4,5];
   // 数组求和
   var sum = a.reduce(function(x, y) {
      return x + y
   }, 0);
   // 数组求积
   var product =  a.reduce(function(x, y) {
      return x * y;
   }, 1);
   // 求最大值
   var max = a.reduce(function(x, y) {
      return x > y ? x : y;
   })
   console.log(sum, product, max); // 16 120 5

reduce()需要两个参数,第一个是执行化简操作的函数,化简函数的任务就是用某种方法把两个值组合或化简为一个值,并返回化简后的值。上述例子中,函数通过加法、乘法或取最大值的方法组合两个值。第二个参数(可选)是传递函数的一个初始值。

reduce()使用的函数和forEach()map()使用的函数不同。比较熟悉的是,数组元素、元素的索引、数组本身将作为2-4个参数传递给函数。第一个参数是到目前为止的化简操作累积的结果。第一次调用函数时,第一个参数是一个初始值,它就是传递给reduce()的第二个参数。在接下来的调用中,这个值就是上一次化简函数的返回值。在第一个例子中,第一次调用化简函数时的参数是0和1,将两者相加并返回1,再次调用时的参数是1和2,它返回3,然后它计算3 + 3 = 6,6 + 4 = 10,最后计算10 + 5 = 15,最后的值是15,reduce()返回这个值。

可能你注意到,上面第三个例子中reduce()只有一个参数:它没有指定初始值,当不指定初始值时,reduce()将使用数组的第一个元素作为其初始值,这意味着第一次调用化简函数就使用了第一个和第二个元素作为其第一个和第二个参数。

在空数组上,不带初始值参数调用reduce()将导致类型错误异常,如果调用它的时候只有一个值——数组只有一个元素并且没有指定初始值,或者空数组指定了一个初始值,reduce()只是简单的返回那个值而不会调用化简参数。 举例说明:

var result = [].reduce(function(x, y) {
      console.log('这里不会调用');
      return x+y
}, 1)
// 这里抛出类型异常:Uncaught TypeError: Reduce of empty array with no initial value
var result = [].reduce(function(x, y) {
      return x+y
})

reduceRight()reduce()工作用例一样,区别是reduceRight()按照数组的索引从右到左处理数组。

  1. indexOf()和lastIndexOf()

indexOf()lastIndexOf()搜索数组中具有给定值的元素,返回找到第一个元素的索引,如果没有找到就返回-1,indexOf()从头至尾搜索,而lastIndexOf()则反向搜索

Pasoul avatar Jun 11 '19 11:06 Pasoul

数组类型

ECMAScript5中,可以使用Array.isArray()函数来做这件事情:

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

但是,在ECMAScript5之前,要区分数组和非数组对象缺非常困难,typeof操作符只会简单的返回object(并且除了函数以外的所有对象都是如此),instanceof操作符只能用于简单的情形

[] instanceof Array // => true
{} instanceof Array // => false

使用instanceof的问题是在Web浏览器中有可能存在多个窗口或者窗体(iframe),每个窗口都有自己的JavaScript环境,有自己的全局对象,并且每个全局对象有自己的一组构造函数,因此一个窗体中的对象不可能是另外窗体中的构造函数的实例,举个例子来证明instanceof操作符不能视为一个可靠的数组检测方法:

<!DOCTYPE html>
<html>
<head>
</head>
<body>
  <iframe name="childframe" src="Flight.html"></iframe>
  <script type="text/javascript">
    window.onload = function () {
        //航班
        var airplanes = ["MU", "CA", "CZ"];
        // 将航班数组传递到iframe窗口的方法中
        var result = window.frames[0].flight.SearchFlight(airplanes);
    };
  </script>
  </body>
</html>
<!DOCTYPE html>
<html>
<head>
</head>
<body>
   <script type="text/javascript">
      var flight = (function () {
        return {
              SearchFlight: function(arr) {
                  // 在flight窗口中检测传递过来的数据是不是数组类型
                  var result = arr instanceof Array;
                  alert(result); // => false               
              }
        };

    })();
   </script>
   </body>
</html>

很惊讶的发现instanceof不能判断我们传递过来的数组,其实不同窗口的window对象是相对独立的,Array也只是window上挂载的一个属性,即A窗口的数组不可能是B窗口的数组构造函数的实例。

解决方案是检查对象的类属性,对数组而言该属性的值总是Array,因此在ECMAScript3中,isArray()函数的代码可以这么写:

function isArray(arr) {
        return typeof arr === 'object' && Object.prototype.toString.call(arr) === '[object Array]';
}

实际上这也是ES5Array.isArray()函数所做的事情。

Pasoul avatar Aug 12 '19 09:08 Pasoul

类数组对象

我们已经看到,数组中有一些特性是其他对象所没有的:

  • 当有新的元素添加时,自动更新length属性
  • 设置length为一个较小的值,将截断数组
  • Array.prototype中继承一些有用的方法
  • 其类属性为Array

这些特性让数组和常规的对象有明显的区别,但是它们不能定义数组的本质特性。一种常常完全合理的看法把拥有一个数值length属性和对应非负整数属性的对象看做一种类型的数组。

常见的如arguments和一些DOM方法(如document.getElementByTagName())都是类数组对象。

**注意:**类数组对象没有继承自Array.prototype,那就不能在它们上面直接调用数组方法。尽管如此,可以间接地使用Function.call方法调用:

var a = {'0':'a', '1':'b' ,'2':'c', length:3} // 类数字对象
Array.prototype.join.call(a, '+') // a+b+c

Pasoul avatar Aug 13 '19 03:08 Pasoul