JavaScript数组所有API全解密
<p>数组是一种非常重要的数据类型,它语法简单、灵活、高效。 在多数编程语言中,数组都充当着至关重要的角色,以至于很难想象没有数组的编程语言会是什么模样。特别是JavaScript,它天生的灵活性,又进一步发挥了数组的特长,丰富了数组的使用场景。可以毫不夸张地说,不深入地了解数组,不足以写JavaScript。</p> <p>截止ES7规范,数组共包含 <strong>33</strong> 个标准的API方法和一个非标准的API方法,使用场景和使用方案纷繁复杂,其中有不少浅坑、深坑、甚至神坑。下面将从 Array 构造器及ES6新特性开始,逐步帮助你掌握数组。</p> <p>声明:以下未特别标明的方法均为ES5已实现的方法。</p> <h2>Array构造器</h2> <p>Array 构造器用于创建一个新的数组。通常,我们推荐使用对象字面量创建数组,这是一个好习惯,但是总有对象字面量乏力的时候,比如说,我想创建一个长度为8的空数组。请比较如下两种方式:</p> <pre> <code class="language-javascript">// 使用Array构造器 var a = Array(8); // => [undefined × 8] // 使用对象字面量 var b = []; b.length = 8; // => [undefined × 8]</code></pre> <p><img src="https://simg.open-open.com/show/2fdbf6ce9b3ea5e40342b2ba68c65b88.png"></p> <p>Array 构造器明显要简洁一些,当然你也许会说,对象字面量也不错啊,那么我保持沉默。</p> <p>如上,我使用了 Array(8) 而不是 new Array(8) ,这会有影响吗?实际上,并没有影响,这得益于 Array 构造器内部对 this 指针的判断, <a href="/misc/goto?guid=4959751565080274601" rel="nofollow,noindex">ELS5_HTML规范</a> 是这么说的:</p> <p>When Array is called as a function rather than as a constructor, it creates and initialises a new Array object. Thus the function call Array(…) is equivalent to the object creation expression new Array(…) with the same arguments.</p> <p>从规范来看,浏览器内部大致做了如下类似的实现:</p> <pre> <code class="language-javascript">function Array(){ // 如果this不是Array的实例,那就重新new一个实例 if(!(this instanceof arguments.callee)){ return new arguments.callee(); } }</code></pre> <p><img src="https://simg.open-open.com/show/1945379e5c33bf357a436d68dfb01f93.png"></p> <p>上面,我似乎跳过了对 Array 构造器语法的介绍,没事,接下来我补上。</p> <p>Array 构造器根据参数长度的不同,有如下两种不同的处理:</p> <ul> <li>new Array(arg1, arg2,…) ,参数长度为 0 或长度大于等于 2 时,传入的参数将按照顺序依次成为新数组的第 0 至 N 项(参数长度为 0 时,返回空数组)。</li> <li>new Array(len) ,当 len 不是数值时,处理同上,返回一个只包含 len 元素一项的数组;当 len 为数值时,根据如下 <a href="/misc/goto?guid=4959751565172068426" rel="nofollow,noindex">规范</a> , len 最大不能超过 32 位无符号整型,即需要小于 2 的 32 次方( len 最大为 Math.pow(2,32) -1或-1>>>0) ,否则将抛出 <strong>RangeError</strong> 。</li> </ul> <p>If the argument len is a Number and ToUint32(len) is equal to len, then the length property of the newly constructed object is set to ToUint32(len) . If the argument len is a Number and ToUint32(len) is not equal to len , a <strong>RangeError</strong> exception is thrown.</p> <p>以上,请注意 Array 构造器对于单个数值参数的特殊处理,如果仅仅需要使用数组包裹若干参数,不妨使用 Array.of ,具体请移步下一节。</p> <h2>ES6新增的构造函数方法</h2> <p>鉴于数组的常用性,ES6专门扩展了数组构造器 Array ,新增 2 个方法: Array.of() 、 Array.from() 。下面展开来聊。</p> <h3>Array.of</h3> <p>Array.of() 用于将参数依次转化为数组中的一项,然后返回这个新数组,而不管这个参数是数字还是其它。它基本上与 Array 构造器功能一致,唯一的区别就在单个数字参数的处理上。如下:</p> <pre> <code class="language-javascript">Array.of(8.0); // => [8] Array(8.0); // => [undefined × 8]</code></pre> <p><img src="https://simg.open-open.com/show/3d3121af6b0c163942681e48079df280.png"></p> <p>参数为多个,或单个参数不是数字时, Array.of() 与 Array 构造器等同。</p> <pre> <code class="language-javascript">Array.of(8.0, 5); // => [8, 5] Array(8.0, 5); // => [8, 5] Array.of('8'); // => ["8"] Array('8'); // => ["8"]</code></pre> <p><img src="https://simg.open-open.com/show/1b9c0254f96ba5f55ffcadb730b37ba2.png"></p> <p>因此,若是需要使用数组包裹元素,推荐优先使用 Array.of() 方法。</p> <p>即使其他版本浏览器不支持也不必担心,由于 Array.of() 与 Array 构造器的这种高度相似性,实现一个polyfill十分简单。如下:</p> <pre> <code class="language-javascript">if (!Array.of){ Array.of = function(){ return Array.prototype.slice.call(arguments); }; }</code></pre> <h3>Array.from</h3> <pre> <code class="language-javascript">// 语法: Array.from(arrayLike[, processingFn[, thisArg]])</code></pre> <p>Array.from() 的设计初衷是快速便捷的基于其他对象创建新数组,准确来说就是从一个类似数组的可迭代对象创建一个新的数组实例,说人话就是,只要一个对象有迭代器, Array.from() 就能把它变成一个数组(当然,是返回新的数组,不改变原对象)。</p> <p>从语法上看, Array.from() 拥有 3 个形参,第一个为类似数组的对象,必选。第二个为加工函数,新生成的数组会经过该函数的加工再返回。第三个为 this 作用域,表示加工函数执行时 this 的值。后两个参数都是可选的。我们来看看用法。</p> <pre> <code class="language-javascript">var obj = {0: 'a', 1: 'b', 2:'c', length: 3}; Array.from(obj, function(value, index){ console.log(value, index, this, arguments.length); return value.repeat(3); //必须指定返回值,否则返回undefined }, obj);</code></pre> <p>执行结果如下:</p> <p><img src="https://simg.open-open.com/show/5f52fe2c697b55ded1f2ce6c522d0317.png"></p> <p>可以看到加工函数的 this 作用域被 obj 对象取代,也可以看到加工函数默认拥有两个形参,分别为迭代器当前元素的值和其索引。</p> <p>注意,一旦使用加工函数,必须明确指定返回值,否则将隐式返回 undefined ,最终生成的数组也会变成一个只包含若干个 undefined 元素的空数组。</p> <p>实际上,如果不需要指定 this ,加工函数完全可以是一个箭头函数。上述代码可以简化如下:</p> <pre> <code class="language-javascript">Array.from(obj, (value) => value.repeat(3));</code></pre> <p>除了上述 obj 对象以外,拥有迭代器的对象还包括这些: String , Set , Map , arguments 等, Array.from() 统统可以处理。如下所示:</p> <pre> <code class="language-javascript">// String Array.from('abc'); // => ["a", "b", "c"] // Set Array.from(new Set(['abc', 'def'])); // => ["abc", "def"] // Map Array.from(new Map([[1, 'abc'], [2, 'def']])); // => [[1, 'abc'], [2, 'def']] // 天生的类数组对象arguments function fn(){ return Array.from(arguments); } fn(1, 2, 3); // => [1, 2, 3]</code></pre> <p><img src="https://simg.open-open.com/show/6fa0eeb13f7ce0e7c47185297cc79d24.png"></p> <p>到这你可能以为 Array.from() 就讲完了,实际上还有一个重要的扩展场景必须提下。比如说生成一个从 0 到指定数字的新数组, Array.from() 就可以轻易的做到。</p> <pre> <code class="language-javascript">Array.from({length: 10}, (v, i) => i); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]</code></pre> <p><img src="https://simg.open-open.com/show/048cad91955d67b779c4bd6c1623f065.png"></p> <p>后面我们将会看到,利用数组的 keys 方法实现上述功能,可能还要简单一些。</p> <h3>Array.isArray</h3> <p>顾名思义, Array.isArray 用来判断一个变量是否数组类型。JS的弱类型机制导致判断变量类型是初级前端开发者面试时的必考题,一般我都会将其作为考察候选人第一题,然后基于此展开。在ES5提供该方法之前,我们至少有如下5种方式去判断一个值是否数组:</p> <pre> <code class="language-javascript">var a = []; // 1.基于instanceof a instanceof Array; // 2.基于constructor a.constructor === Array; // 3.基于Object.prototype.isPrototypeOf Array.prototype.isPrototypeOf(a); // 4.基于getPrototypeOf Object.getPrototypeOf(a) === Array.prototype; // 5.基于Object.prototype.toString Object.prototype.toString.apply(a) === '[object Array]';</code></pre> <p>以上,除了 Object.prototype.toString 外,其它方法都不能正确判断变量的类型。</p> <p>要知道,代码的运行环境十分复杂,一个变量可能使用浑身解数去迷惑它的创造者。且看:</p> <pre> <code class="language-javascript">var a = { __proto__: Array.prototype }; // 分别在控制台试运行以下代码 // 1.基于instanceof a instanceof Array; // => true // 2.基于constructor a.constructor === Array; // => true // 3.基于Object.prototype.isPrototypeOf Array.prototype.isPrototypeOf(a); // => true // 4.基于getPrototypeOf Object.getPrototypeOf(a) === Array.prototype; // => true</code></pre> <p>以上, 4 种方法将全部返回 true ,为什么呢?我们只是手动指定了某个对象的 __proto__ 属性为 Array.prototype ,便导致了该对象继承了 Array 对象,这种毫不负责任的继承方式,使得基于继承的判断方案瞬间土崩瓦解。</p> <p>不仅如此,我们还知道, Array 是堆数据,变量指向的只是它的引用地址,因此每个页面的 Array 对象引用的地址都是不一样的。 iframe 中声明的数组,它的构造函数是 iframe 中的 Array 对象。如果在 iframe 声明了一个数组 x ,将其赋值给父页面的变量 y ,那么在父页面使用 y instanceof Array ,结果一定是 false 的。而最后一种返回的是字符串,不会存在引用问题。实际上,多页面或系统之间的交互只有字符串能够畅行无阻。</p> <p>鉴于上述的两点原因,故笔者推荐使用最后一种方法去撩面试官(别提是我说的),如果你还不信,这里恰好有篇文章跟我持有相同的观点: <a href="/misc/goto?guid=4959751565254594878" rel="nofollow,noindex">Determining with absolute accuracy whether or not a JavaScript object is an array</a> 。</p> <p>相反,使用 Array.isArray 则非常简单,如下:</p> <pre> <code class="language-javascript">Array.isArray([]); // => true Array.isArray({0: 'a', length: 1}); // => false</code></pre> <p>实际上,通过 Object.prototype.toString 去判断一个值的类型,也是各大主流库的标准。因此 Array.isArray 的polyfill通常长这样:</p> <pre> <code class="language-javascript">if (!Array.isArray){ Array.isArray = function(arg){ return Object.prototype.toString.call(arg) === '[object Array]'; }; }</code></pre> <h2>数组遍历</h2> <p>ES6对数组的增强不止是体现在API上,还包括语法糖。比如说 for ... of ,它就是借鉴其它语言而成的语法糖,这种基于原数组使用 for ... of 生成新数组的语法糖,叫做 <strong>数组遍历</strong> 。 数组遍历最初起早在ES6的草案中,但在第27版(2014年8月)中被移除,目前只有Firefox v30+支持,遍历有风险,使用需谨慎。 所幸如今这些语言都还支持遍历:CoffeeScript、Python、Haskell、Clojure,我们可以从中一窥端倪。这里我们以Python的 for ... in 遍历打个比方:</p> <pre> <code class="language-javascript"># python for in 推导 a = [1, 2, 3, 4] print [i * i for i in a if i == 3] # [9]</code></pre> <p>如下是SpiderMonkey引擎(Firefox)之前基于ES4规范实现的数组推导(与Python的推导十分相似):</p> <pre> <code class="language-javascript">[i * i for (i of a)] // => [1, 4, 9, 16]</code></pre> <p>ES6中数组有关的 for ... of 在ES4的基础上进一步演化, for 关键字居首, in 在中间,最后才是运算表达式。如下:</p> <pre> <code class="language-javascript">[for (i of [1, 2, 3, 4]) i * i] // => [1, 4, 9, 16]</code></pre> <p>同Python的示例,ES6中数组有关的 for ... of 也可以使用 if 语句:</p> <pre> <code class="language-javascript">// 单个if [for (i of [1, 2, 3, 4]) if (i == 3) i * i] // => [9] // 甚至是多个if [for (i of [1, 2, 3, 4]) if (i > 2) if (i < 4) i * i] // => [9]</code></pre> <p>更为强大的是,ES6数组推导还允许多重 for ... of 。</p> <pre> <code class="language-javascript">[for (i of [1, 2, 3]) for (j of [10, 100]) i * j] // => [10, 100, 20, 200, 30, 300]</code></pre> <p>甚至,数组推导还能够嵌入另一个数组推导中。</p> <pre> <code class="language-javascript">[for (i of [1, 2, 3]) [for (j of [10, 100]) i * j] ] // => [[10, 100], [20, 200], [30, 300]]</code></pre> <p>对于上述两个表达式,前者和后者唯一的区别,就在于后者的第二个推导是先返回数组,然后与外部的推导再进行一次运算。</p> <p>除了多个数组推导嵌套外,ES6的数组推导还会为每次迭代分配一个新的作用域(目前Firefox也没有为每次迭代创建新的作用域):</p> <pre> <code class="language-javascript">// ES6规范 [for (x of [0, 1, 2]) () => x][0]() // 0 // Firefox运行 [for (x of [0, 1, 2]) () => x][0]() // 2</code></pre> <p>通过上面的实例,我们看到使用数组推导来创建新数组比 forEach , map , filter 等遍历方法更加简洁,只是非常可惜,它不是标准规范。</p> <p>ES6不仅新增了对 Array 构造器相关API,还新增了 8 个原型的方法。接下来我会在原型方法的介绍中穿插着ES6相关方法的讲解,请耐心往下读。</p> <h2>原型</h2> <p>继承的常识告诉我们,JS中所有的数组方法均来自于 Array.prototype ,和其他构造函数一样,你可以通过扩展 Array 的 prototype 属性上的方法来给所有数组实例增加方法。</p> <p>值得一说的是, Array.prototype 本身就是一个数组。</p> <p><img src="https://simg.open-open.com/show/47e85255cc8aa84bdd0bbfa4bb377127.png"></p> <pre> <code class="language-javascript">Array.isArray(Array.prototype); // => true console.log(Array.prototype.length); // => 0</code></pre> <p>以下方法可以进一步验证:</p> <pre> <code class="language-javascript">console.log([].__proto__.length); // => 0 console.log([].__proto__); // => [Symbol(Symbol.unscopables): Object]</code></pre> <p><img src="https://simg.open-open.com/show/2df372cf74e74242ad2cddf53d90b4a2.png"></p> <p>有关 Symbol(Symbol.unscopables) 的知识,这里不做详述,具体请移步后续章节。</p> <h2>方法</h2> <p>数组原型提供的方法非常之多,主要分为三种,一种是会改变自身值的,一种是不会改变自身值的,另外一种是遍历方法。</p> <p>由于 Array.prototype 的某些属性被设置为 [[DontEnum]] ,因此不能用一般的方法进行遍历,我们可以通过如下方式获取 Array.prototype 的所有方法:</p> <pre> <code class="language-javascript">Object.getOwnPropertyNames(Array.prototype);</code></pre> <p><img src="https://simg.open-open.com/show/ccd832956478fc277cb8a4bd31dfb407.png"></p> <h3>改变自身值的方法(9个)</h3> <p>基于ES6,改变自身值的方法一共有 9 个,分别为 pop() 、 push() 、 reverse() 、 shift() 、 sort() 、 splice() 、 unshift() ,以及两个ES6新增的方法 copyWithin() 和 fill() 。</p> <p>对于能改变自身值的数组方法,日常开发中需要特别注意,尽量避免在循环遍历中去改变原数组的项。接下来,我们一起来深入地了解这些方法。</p> <p>pop()</p> <p>pop() 方法删除一个数组中的最后的一个元素,并且返回这个元素。如果是栈的话,这个过程就是栈顶弹出。</p> <pre> <code class="language-javascript">var array = ["cat", "dog", "cow", "chicken", "mouse"]; var item = array.pop(); console.log(array); // => ["cat", "dog", "cow", "chicken"] console.log(item); // => mouse</code></pre> <p><img src="https://simg.open-open.com/show/22c094da321e1fca021caf076e8d50d9.png"></p> <p>由于设计上的巧妙, pop() 方法可以应用在类数组对象上,即 <strong>鸭式辨型</strong> 。 如下:</p> <pre> <code class="language-javascript">var o = { 0:"cat", 1:"dog", 2:"cow", 3:"chicken", 4:"mouse", length:5 } var item = Array.prototype.pop.call(o); console.log(o); // => Object {0: "cat", 1: "dog", 2: "cow", 3: "chicken", length: 4} console.log(item); // => mouse</code></pre> <p><img src="https://simg.open-open.com/show/663f412e2fdece1acf165e107a139523.png"></p> <p>但如果类数组对象不具有 length 属性,那么该对象将被创建 length 属性, length 值为 0 。如下:</p> <pre> <code class="language-javascript">var o = { 0:"cat", 1:"dog", 2:"cow", 3:"chicken", 4:"mouse" } var item = Array.prototype.pop.call(o); console.log(o); // => Object {0: "cat", 1: "dog", 2: "cow", 3: "chicken", 4: "mouse", length: 0} console.log(item); // => undefined</code></pre> <p><img src="https://simg.open-open.com/show/036d1f4ddc2357adf0568b509a2aca89.png"></p> <p>push()</p> <p>push() 方法添加一个或者多个元素到数组末尾,并且返回数组新的长度。如果是栈的话,这个过程就是栈顶压入。</p> <pre> <code class="language-javascript">var array = ["football", "basketball", "volleyball", "Table tennis", "badminton"]; var i = array.push("golfball"); console.log(array); // => ["football", "basketball", "volleyball", "Table tennis", "badminton", "golfball"] console.log(i); // => 6</code></pre> <p><img src="https://simg.open-open.com/show/a9d458aa00054b10044b7da7bd126ae3.png"></p> <p>同 pop() 方法一样, push() 方法也可以应用到类数组对象上,如果 length 不能被转成一个数值或者不存在 length 属性时,则插入的元素索引为 0 ,且 length 属性不存在时,将会创建它。</p> <pre> <code class="language-javascript">var o = { 0:"football", 1:"basketball" }; var i = Array.prototype.push.call(o, "golfball"); console.log(o); // => Object {0: "golfball", 1: "basketball", length: 1} console.log(i); // => 1</code></pre> <p><img src="https://simg.open-open.com/show/58dde84d31706bd02e348e0a23a4bc6a.png"></p> <p>实际上, push() 方法是根据 length 属性来决定从哪里开始插入给定的值。</p> <pre> <code class="language-javascript">var o = { 0:"football", 1:"basketball", length:1 }; var i = Array.prototype.push.call(o,"golfball"); console.log(o); // => Object {0: "football", 1: "golfball", length: 2} console.log(i); // => 2</code></pre> <p><img src="https://simg.open-open.com/show/dee2dcc5d55bbc3be2304619e00eacfc.png"></p> <p>利用 push() 根据 length 属性插入元素这个特点,可以实现数组的合并,如下:</p> <pre> <code class="language-javascript">var array = ["football", "basketball"]; var array2 = ["volleyball", "golfball"]; var i = Array.prototype.push.apply(array,array2); console.log(array); // => ["football", "basketball", "volleyball", "golfball"] console.log(i); // => 4</code></pre> <p><img src="https://simg.open-open.com/show/96403da6c9ae930cad8397c150961bd0.png"></p> <p>reverse()</p> <p>reverse() 方法颠倒数组中元素的位置,第一个会成为最后一个,最后一个会成为第一个,该方法返回对数组的引用。</p> <pre> <code class="language-javascript">var array = [1,2,3,4,5]; var array2 = array.reverse(); console.log(array); // => [5,4,3,2,1] console.log(array2===array); // => true</code></pre> <p><img src="https://simg.open-open.com/show/a25f46eb3d864a254ab3d76b99c325e5.png"></p> <p>同上, reverse() 也是 <strong>鸭式辨型</strong> 的受益者,颠倒元素的范围受 length 属性制约。如下:</p> <pre> <code class="language-javascript">var o = { 0:"a", 1:"b", 2:"c", length:2 }; var o2 = Array.prototype.reverse.call(o); console.log(o); // => Object {0: "b", 1: "a", 2: "c", length: 2} console.log(o === o2); // => true</code></pre> <p><img src="https://simg.open-open.com/show/4fd648eeeeee6f909f7d49a4b29266fa.png"></p> <p>如果 length 属性小于 2 或者 length 属性不为数值,那么原类数组对象将没有变化。即使 length 属性不存在,该对象也不会去创建 length 属性。特别的是,当 length 属性较大时,类数组对象的 <strong>『索引』</strong> 会尽可能的向 length 看齐。如下:</p> <pre> <code class="language-javascript">var o = { 0:"a", 1:"b", 2:"c", length:100 }; var o2 = Array.prototype.reverse.call(o); console.log(o); // => Object {97: "c", 98: "b", 99: "a", length: 100} console.log(o === o2); // => true</code></pre> <p><img src="https://simg.open-open.com/show/63717645909db75b7a87a6c4ff6c98cd.png"></p> <p>shift()</p> <p>shift() 方法删除数组的第一个元素,并返回这个元素。如果是栈的话,这个过程就是栈底弹出。</p> <pre> <code class="language-javascript">var array = [1,2,3,4,5]; var item = array.shift(); console.log(array); // => [2,3,4,5] console.log(item); // => 1</code></pre> <p><img src="https://simg.open-open.com/show/307582a885f58ff32b0f738b6b2e308c.png"></p> <p>同样受益于 <strong>鸭式辨型</strong> ,对于类数组对象, shift() 仍然能够处理。如下:</p> <pre> <code class="language-javascript">var o = { 0:"a", 1:"b", 2:"c", length:3 }; var item = Array.prototype.shift.call(o); console.log(o); // => Object {0: "b", 1: "c", length: 2} console.log(item); // => a</code></pre> <p><img src="https://simg.open-open.com/show/a5d2dbd4f289621b99775d1858201228.png"></p> <p>如果类数组对象 length 属性不存在,将添加 length 属性,并初始化为 0 。如下:</p> <pre> <code class="language-javascript">var o = { 0:"a", 1:"b", 2:"c" }; var item = Array.prototype.shift.call(o); console.log(o); // => Object {0: "a", 1: "b", 2:"c" length: 0} console.log(item); // => undefined</code></pre> <p><img src="https://simg.open-open.com/show/fa2d65a53404e726a5206e8d4d116ab2.png"></p> <p>sort()</p> <p>sort() 方法对数组元素进行排序,并返回这个数组。 sort() 方法比较复杂,这里我将多花些篇幅来讲这块。</p> <pre> <code class="language-javascript">// 语法: arr.sort([comparefn])</code></pre> <p>comparefn 是可选的,如果省略,数组元素将按照各自转换为字符串的Unicode(万国码)位点顺序排序,例如 "Boy" 将排到 "apple" 之前。当对数字排序的时候, 25 将会排到 8 之前,因为转换为字符串后, ”25” 将比 ”8” 靠前。例如:</p> <pre> <code class="language-javascript">var array = ["apple","Boy","Cat","dog"]; var array2 = array.sort(); console.log(array); // => ["Boy", "Cat", "apple", "dog"] console.log(array2 == array); // => true array = [10, 1, 3, 20]; var array3 = array.sort(); console.log(array3); // => [1, 10, 20, 3]</code></pre> <p><img src="https://simg.open-open.com/show/c44ee5365dd473b6ccaa754637b31b73.png"></p> <p>如果指明了 comparefn ,数组将按照调用该函数的返回值来排序。若 a 和 b 是两个将要比较的元素:</p> <ul> <li>若 comparefn(a, b) < 0 ,那么 a 将排到 b 前面;</li> <li>若 comparefn(a, b) = 0 ,那么 a 和 b 相对位置不变;</li> <li>若 comparefn(a, b) > 0 ,那么 a , b 将调换位置;</li> </ul> <p>如果数组元素为数字,则排序函数 comparefn 格式如下所示:</p> <pre> <code class="language-javascript">function compare(a, b){ return a - b; }</code></pre> <p>如果数组元素为非ASCII字符的字符串(如包含类似 e 、 é 、 è 、 a 、 ä 或中文字符等非英文字符的字符串),则需要使用 String.localeCompare 。下面这个函数将排到正确的顺序。</p> <pre> <code class="language-javascript">var array = ['互','联','网','改','变','世','界']; var array2 = array.sort(); var array = ['互','联','网','改','变','世','界']; // 重新赋值,避免干扰array2 var array3 = array.sort(function (a, b) { return a.localeCompare(b); }); console.log(array2); console.log(array3);</code></pre> <p><img src="https://simg.open-open.com/show/933914cbfd93fff3ab2cb881b51e3f68.png"></p> <p>这个示例中使用 String.localeCompare 跑出来的效果并无差异(使用的是Chrome浏览器),和原作者提供的示例有差别,特意这里提示一下。根据 <a href="/misc/goto?guid=4959751565341126212" rel="nofollow,noindex"> String.prototype.localeCompare() - JavaScript | MDN 说明 </a> , localeCompare 方法是和操作系统 locale 值相关。 换过一个语言来做示例,这个示例的代码来自于《 <a href="/misc/goto?guid=4959751566311075885" rel="nofollow,noindex">排列含音节字母的字符串</a> 》。</p> <p>有两种方法可以解决这个问题,由ECMAScript国际化API提供的 localeCompare 和 Intl.Collator 。</p> <p>使用 localeCompare() :</p> <pre> <code class="language-javascript">['único','árbol', 'cosas', 'fútbol'].sort(function (a, b) { return a.localeCompare(b); }); // => ["árbol", "cosas", "fútbol", "único"] ['Woche', 'wöchentlich', 'wäre', 'Wann'].sort(function (a, b) { return a.localeCompare(b); }); // => ["Wann", "wäre", "Woche", "wöchentlich"]</code></pre> <p><img src="https://simg.open-open.com/show/35e0ace029de3ae9b6253cad655abf2c.png"></p> <p>使用 Intl.Collator() :</p> <pre> <code class="language-javascript">['único','árbol', 'cosas', 'fútbol'].sort(Intl.Collator().compare); // => ["árbol", "cosas", "fútbol", "único"] ['Woche', 'wöchentlich', 'wäre', 'Wann'].sort(Intl.Collator().compare); // => ["Wann", "wäre", "Woche", "wöchentlich"]</code></pre> <p><img src="https://simg.open-open.com/show/4352e68f7ba2e048c7ee1920871e5ce2.png"></p> <ul> <li>两个方法都可以自定义区域位置。</li> <li>根据Firefox,当比较大数量的字符串时,使用Intl.Collator更快。</li> </ul> <p>所以当你处理一个由非英语组成的字符串数组时,记得使用此方法来避免排序出现意外。</p> <p>同上, sort() 一样受益于鸭式辨型,比如:</p> <pre> <code class="language-javascript">var o = {0:'互',1:'联',2:'网',3:'改',4:'变',5:'世',6:'界',length:7}; Array.prototype.sort.call(o,function(a, b){ return a.localeCompare(b); }); console.log(o);</code></pre> <p><img src="https://simg.open-open.com/show/a48b3d0da1d22978c3383334adb1f131.png"></p> <p>注意:使用 sort() 的鸭式辨型特性时,若类数组对象不具有 length 属性,它并不会进行排序,也不会为其添加 length 属性。</p> <pre> <code class="language-javascript">var o = {0:'互',1:'联',2:'网',3:'改',4:'变',5:'世',6:'界'}; Array.prototype.sort.call(o,function(a, b){ return a.localeCompare(b); }); console.log(o);</code></pre> <p><img src="https://simg.open-open.com/show/16de2722a4a0d4feece4b1c33c6b8a65.png"></p> <p>comparefn 如果需要对数组元素多次转换以实现排序,那么使用 map() 辅助排序将是个不错的选择。基本思想就是将数组中的每个元素实际比较的值取出来,排序后再将数组恢复。</p> <pre> <code class="language-javascript">// 需要被排序的数组 var array = ['dog', 'Cat', 'Boy', 'apple']; // 对需要排序的数字和位置的临时存储 var mapped = array.map(function(el, i) { return { index: i, value: el.toLowerCase() }; }) // 按照多个值排序数组 mapped.sort(function(a, b) { return +(a.value > b.value) || +(a.value === b.value) - 1; }); // 根据索引得到排序的结果 var result = mapped.map(function(el){ return array[el.index]; }); console.log(result); // ["apple", "Boy", "Cat", "dog"]</code></pre> <p><img src="https://simg.open-open.com/show/3756f5d30ea75a040b54e564c0e2cf29.png"></p> <p>实际上,ECMAscript规范中并未规定具体的 sort() 算法,这就势必导致各个浏览器不尽相同的 sort() 算法,请看 sort() 方法在Chrome浏览器下表现:</p> <pre> <code class="language-javascript">var array = [ { n: "a", v: 1 }, { n: "b", v: 1 }, { n: "c", v: 1 }, { n: "d", v: 1 }, { n: "e", v: 1 }, { n: "f", v: 1 }, { n: "g", v: 1 }, { n: "h", v: 1 }, { n: "i", v: 1 }, { n: "j", v: 1 }, { n: "k", v: 1 } ]; array.sort(function (a, b) { return a.v - b.v; }); for (var i = 0,len = array.length; i < len; i++) { console.log(array[i].n); // => f a c d e b g h i j k }</code></pre> <p>由于 v 值相等, array 数组排序前后应该不变,然而Chrome却表现异常,而其他浏览器(如IE 或 Firefox)表现正常。</p> <p>这是因为v8引擎为了高效排序(采用了不稳定排序)。即数组长度超过 10 条时,会调用另一种排序方法(快速排序);而 10 条及以下采用的是插入排序,此时结果将是稳定的,如下:</p> <pre> <code class="language-javascript">var array = [ { n: "a", v: 1 }, { n: "b", v: 1 }, { n: "c", v: 1 }, { n: "d", v: 1 }, { n: "e", v: 1 }, { n: "f", v: 1 }, { n: "g", v: 1 }, { n: "h", v: 1 }, { n: "i", v: 1 }, { n: "j", v: 1 } ]; array.sort(function (a, b) { return a.v - b.v; }); for (var i = 0,len = array.length; i < len; i++) { console.log(array[i].n); // => a b c d e f g h i j }</code></pre> <p>从 a 到 j 刚好 10 条数据。</p> <p>那么我们该如何规避Chrome浏览器的这种”bug”呢?其实很简单,只需略动手脚,改变排序方法的返回值即可,如下:</p> <pre> <code class="language-javascript">array.sort(function (a, b) { return a.v - b.v || array.indexOf(a)-array.indexOf(b); });</code></pre> <p>使用数组的 sort() 方法需要注意一点:各浏览器的针对 sort() 方法内部算法实现不尽相同,排序函数尽量只返回 -1 、 0 、 1 三种不同的值,不要尝试返回 true 或 false 等其它数值,因为可能导致不可靠的排序结果。</p> <p>sort() 方法传入的排序函数如果返回布尔值会导致什么样的结果呢?</p> <p>以下是常见的浏览器以及脚本引擎:</p> <table> <thead> <tr> <th>浏览器名称</th> <th>ECMAScript引擎</th> </tr> </thead> <tbody> <tr> <td>IE6~IE8</td> <td>JScript</td> </tr> <tr> <td>IE9~I10</td> <td>Chakra</td> </tr> <tr> <td>Firefox</td> <td>SpiderMonkey, IonMonkey, TraceMonkey</td> </tr> <tr> <td>Chrome</td> <td>V8</td> </tr> <tr> <td>Safari</td> <td>JavaScriptCore(SquirrelFish Extreme)</td> </tr> <tr> <td>Opera</td> <td>Carakan</td> </tr> </tbody> </table> <p>分析以下代码,预期将数组元素进行升序排序:</p> <pre> <code class="language-javascript">var array = [7, 6, 5, 4, 3, 2, 1, 0, 8, 9]; var comparefn = function (x, y) { return x > y; }; array.sort(comparefn);</code></pre> <p>代码中, comparefn 函数返回值为 bool 类型,并非为规范规定的 -1 、 0 、 1 值。那么执行此代码,各 JS 脚本引擎实现情况如何?</p> <table> <thead> <tr> <th>ECMAScript引擎</th> <th>输出结果</th> <th>是否符合预期</th> </tr> </thead> <tbody> <tr> <td>JScript</td> <td>[2, 3, 5, 1, 4, 6, 7, 0, 8, 9]</td> <td>否</td> </tr> <tr> <td>Carakan</td> <td>[0, 1, 3, 8, 2, 4, 9, 5, 6, 7]</td> <td>否</td> </tr> <tr> <td>Chakra & JavaScriptCore</td> <td>[7, 6, 5, 4, 3, 2, 1, 0, 8, 9]</td> <td>否</td> </tr> <tr> <td>SpiderMonkey</td> <td>[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]</td> <td>是</td> </tr> <tr> <td>V8</td> <td>[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]</td> <td>是</td> </tr> </tbody> </table> <p>根据表中数据可见,当数组内元素个数小于等于 10 时,现象如下:</p> <ul> <li>JScript & Carakan 排序结果有误</li> <li>Chakra & JavaScriptCore 看起来没有进行排序</li> <li>SpiderMonkey 返回了预期的正确结果</li> <li>V8 暂时看起来排序正确</li> </ul> <p>将数组元素扩大至 11 位,现象如下:</p> <pre> <code class="language-javascript">var array = [7, 6, 5, 4, 3, 2, 1, 0, 10, 9, 8]; var comparefn = function (x, y) { return x > y; }; array.sort(comparefn);</code></pre> <table> <thead> <tr> <th>ECMAScript引擎</th> <th>输出结果</th> <th>是否符合预期</th> </tr> </thead> <tbody> <tr> <td>JScript</td> <td>[2, 3, 5, 1, 4, 6, 7, 0, 8, 9, 10]</td> <td>否</td> </tr> <tr> <td>Carakan</td> <td>[0, 1, 3, 8, 2, 4, 9, 5, 10, 6, 7]</td> <td>否</td> </tr> <tr> <td>Chakra & JavaScriptCore</td> <td>[7, 6, 5, 4, 3, 2, 1, 0, 10, 8, 9]</td> <td>否</td> </tr> <tr> <td>IonMonkey</td> <td>[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]</td> <td>是</td> </tr> <tr> <td>V8</td> <td>[5, 0, 1, 2, 3, 4, 6, 7, 8, 9, 10]</td> <td>否</td> </tr> </tbody> </table> <p>根据表中数据可见,当数组内元素个数大于 10 时:</p> <ul> <li>JScript & Carakan 排序结果有误</li> <li>Chakra & JavaScriptCore 看起来没有进行排序</li> <li>SpiderMonkey 返回了预期的正确结果</li> <li>V8 排序结果由正确转为不正确</li> </ul> <p>splice()</p> <p>splice() 方法用新元素替换旧元素的方式来修改数组。它是一个常用的方法,复杂的数组操作场景通常都会有它的身影,特别是需要维持原数组引用时,就地删除或者新增元素, splice() 是最适合的。</p> <pre> <code class="language-javascript">// 语法: arr.splice(start,deleteCount[, item1[, item2[, …]]])</code></pre> <ul> <li>start 指定从哪一位开始修改内容。如果超过了数组长度,则从数组末尾开始添加内容;如果是负值,则其指定的索引位置等同于 length + start ( length 为数组的长度),表示从数组末尾开始的第 -start 位。</li> <li>deleteCount 指定要删除的元素个数,若等于 0 ,则不删除。这种情况下,至少应该添加一位新元素,若大于 start 之后的元素总和,则 start 及之后的元素都将被删除。</li> <li>itemN 指定新增的元素,如果缺省,则该方法只删除数组元素。</li> <li>返回值 由原数组中被删除元素组成的数组,如果没有删除,则返回一个空数组。</li> </ul> <p>下面来举栗子说明:</p> <pre> <code class="language-javascript">var array = ["apple","boy"]; var splices = array.splice(1,1); console.log(array); // => ["apple"] console.log(splices); // => ["boy"] ,可见是从数组下标为1的元素开始删除,并且删除一个元素,由于itemN缺省,故此时该方法只删除元素 array = ["apple","boy"]; splices = array.splice(2,1,"cat"); console.log(array); // => ["apple", "boy", "cat"] console.log(splices); // => [], 可见由于start超过数组长度,此时从数组末尾开始添加元素,并且原数组不会发生删除行为 array = ["apple","boy"]; splices = array.splice(-2,1,"cat"); console.log(array); // => ["cat", "boy"] console.log(splices); // => ["apple"], 可见当start为负值时,是从数组末尾开始的第-start位开始删除,删除一个元素,并且从此处插入了一个元素 array = ["apple","boy"]; splices = array.splice(-3,1,"cat"); console.log(array); // => ["cat", "boy"] console.log(splices); // => ["apple"], 可见即使-start超出数组长度,数组默认从首位开始删除 array = ["apple","boy"]; splices = array.splice(0,3,"cat"); console.log(array); // => ["cat"] console.log(splices); // => ["apple", "boy"], 可见当deleteCount大于数组start之后的元素总和时,start及之后的元素都将被删除</code></pre> <p><img src="https://simg.open-open.com/show/9cd646ba27727b929a69ab7feb48a7f7.png"></p> <p>同上, splice() 一样受益于鸭式辨型, 比如:</p> <pre> <code class="language-javascript">var o = { 0:"apple", 1:"boy", length:2 }; var splices = Array.prototype.splice.call(o,1,1); console.log(o); // => Object {0: "apple", length: 1}, 可见对象o删除了一个属性,并且length-1 console.log(splices); // => ["boy"]</code></pre> <p><img src="https://simg.open-open.com/show/077323f1453947ef0c2d7c02dbefe5a4.png"></p> <p>注意:如果类数组对象没有 length 属性, splice() 将为该类数组对象添加 length 属性,并初始化为 0 。</p> <p>如果需要删除数组中一个已存在的元素,可参考如下:</p> <pre> <code class="language-javascript">var array = ['a','b','c']; array.splice(array.indexOf('b'),1);</code></pre> <p><img src="https://simg.open-open.com/show/edae8c5bf5a44fd0886abf788e7a9745.png"></p> <p>unshift()</p> <p>unshift() 方法用于在数组开始处插入一些元素(就像是栈底插入),并返回数组新的长度。</p> <pre> <code class="language-javascript">var array = ["red", "green", "blue"]; var length = array.unshift("yellow"); console.log(array); // => ["yellow", "red", "green", "blue"] console.log(length); // => 4</code></pre> <p><img src="https://simg.open-open.com/show/df1f5000b01a29510e122227cfb72ac4.png"></p> <p>如果给 unshift() 方法传入一个数组呢?</p> <pre> <code class="language-javascript">var array = ["red", "green", "blue"]; var length = array.unshift(["yellow"]); console.log(array); // => [["yellow"], "red", "green", "blue"] console.log(length); // => 4, 可见数组也能成功插入</code></pre> <p><img src="https://simg.open-open.com/show/47ee4053d92c7e7c608dac335f73ada0.png"></p> <p>同上, unshift() 也受益于鸭式辨型,呈上栗子:</p> <pre> <code class="language-javascript">var o = { 0:"red", 1:"green", 2:"blue", length:3 }; var length = Array.prototype.unshift.call(o,"gray"); console.log(o); // => Object {0: "gray", 1: "red", 2: "green", 3: "blue", length: 4} console.log(length); // => 4</code></pre> <p><img src="https://simg.open-open.com/show/11e602a9eadbe4f043bde4165cb8b7ee.png"></p> <p>注意:如果类数组对象不指定 length 属性,则返回结果是这样的 Object {0: "gray", 1: "green", 2: "blue", length: 1} , shift() 会认为数组长度为 0 ,此时将从对象下标为 0 的位置开始插入,相应位置属性将被替换,此时初始化类数组对象的 length 属性为插入元素个数。</p> <p>copyWithin()</p> <p>copyWithin() 方法基于ECMAScript 2015(ES6)规范,用于数组内元素之间的替换,即替换元素和被替换元素均是数组内的元素。</p> <pre> <code class="language-javascript">// 语法: arr.copyWithin(target, start[, end = this.length])</code></pre> <p>taget 指定被替换元素的索引, start 指定替换元素起始的索引, end 可选,指的是替换元素结束位置的索引。如果 start 为负,则其指定的索引位置等同于 length + start , length 为数组的长度。 end 也是如此。</p> <pre> <code class="language-javascript">var array = [1,2,3,4,5]; var array2 = array.copyWithin(0,3); console.log(array===array2,array2); // => true [4, 5, 3, 4, 5] var array = [1,2,3,4,5]; console.log(array.copyWithin(0,3,4)); // => [4, 2, 3, 4, 5] var array = [1,2,3,4,5]; console.log(array.copyWithin(0,-2,-1)); // => [4, 2, 3, 4, 5]</code></pre> <p><img src="https://simg.open-open.com/show/6be3fd911a956525378c7b0346325a2e.png"></p> <p>同上, copyWithin() 一样受益于鸭式辨型,例如:</p> <pre> <code class="language-javascript">var o = {0:1, 1:2, 2:3, 3:4, 4:5,length:5} var o2 = Array.prototype.copyWithin.call(o,0,3); console.log(o === o2,o2);</code></pre> <p><img src="https://simg.open-open.com/show/414419bcbd68ffdd63848f24f44cc9af.png"></p> <p>fill()</p> <p>fill() 方法基于ECMAScript 2015(ES6)规范,它同样用于数组元素替换,但与 copyWithin() 略有不同,它主要用于将数组指定区间内的元素替换为某个值。</p> <pre> <code class="language-javascript">// 语法: arr.fill(value, start[, end = this.length])</code></pre> <p>value 指定被替换的值, start 指定替换元素起始的索引, end 可选,指的是替换元素结束位置的索引。如果 start 为负,则其指定的索引位置等同于 length + start , length 为数组的长度。 end 也是如此。</p> <pre> <code class="language-javascript">var array = [1,2,3,4,5]; var array2 = array.fill(10,0,3); console.log(array === array2,array2); // => true [10, 10, 10, 4, 5], 可见数组区间[0,3]的元素全部替换为10</code></pre> <p><img src="https://simg.open-open.com/show/9cae9e47fd71d703076e269d3357c0b0.png"></p> <p>同上, fill() 一样受益于鸭式辨型,例如:</p> <pre> <code class="language-javascript">var o = { 0:1, 1:2, 2:3, 3:4, 4:5, length:5 } var o2 = Array.prototype.fill.call(o,10,0,2); console.log(o === o2, o2);</code></pre> <p><img src="https://simg.open-open.com/show/61aac5b8c16dc45bc920b4924aa821d1.png"></p> <h3>不会改变自身的方法</h3> <p>基于ES7,不会改变自身的方法一共有 9 个,分别为 concat() 、 join() 、 slice() 、 toString() 、 toLocateString() 、 indexOf() 、 lastIndexOf() 、未标准的 toSource() 以及ES7新增的方法 includes() 。</p> <p>concat()</p> <p>concat() 方法将传入的数组或者元素与原数组合并,组成一个新的数组并返回。</p> <pre> <code class="language-javascript">var array = [1, 2, 3]; var array2 = array.concat(4,[5,6],[7,8,9]); console.log(array2); // => [1, 2, 3, 4, 5, 6, 7, 8, 9] console.log(array); // => [1, 2, 3], 可见原数组并未被修改</code></pre> <p><img src="https://simg.open-open.com/show/65577c36b0f62a8760e5a1c48f8f3e65.png"></p> <p>若 concat() 方法中不传入参数,那么将基于原数组浅复制生成一个一模一样的新数组(指向新的地址空间)。</p> <pre> <code class="language-javascript">var array = [{a: 1}]; var array3 = array.concat(); console.log(array3); // => [{a: 1}] console.log(array3 === array); // => false console.log(array[0] === array3[0]); // => true,新旧数组第一个元素依旧共用一个同一个对象的引用</code></pre> <p><img src="https://simg.open-open.com/show/c176549519b645bfd66824148374c418.png"></p> <p>同上, concat() 一样受益于鸭式辨型,但其效果可能达不到我们的期望,如下:</p> <pre> <code class="language-javascript">var o = {0:"a", 1:"b", 2:"c",length:3}; var o2 = Array.prototype.concat.call(o,'d',{3:'e',4:'f',length:2},['g','h','i']); console.log(o2); // => [{0:"a", 1:"b", 2:"c", length:3}, 'd', {3:'e', 4:'f', length:2}, 'g', 'h', 'i']</code></pre> <p><img src="https://simg.open-open.com/show/d0651a6a148040dbb1fc5f760fc0f81c.png"></p> <p>可见,类数组对象合并后返回的是依然是数组,并不是我们期望的对象。</p> <p>join()</p> <p>join() 方法将数组中的所有元素连接成一个字符串。</p> <pre> <code class="language-javascript">var array = ['We', 'are', 'Chinese']; console.log(array.join()); // => "We,are,Chinese" console.log(array.join('+')); // => "We+are+Chinese" console.log(array.join('')); // => "WeareChinese"</code></pre> <p>同上, join() 一样受益于鸭式辨型,如下:</p> <pre> <code class="language-javascript">var o = { 0:"We", 1:"are", 2:"Chinese", length:3 }; console.log(Array.prototype.join.call(o,'+')); // => "We+are+Chinese" console.log(Array.prototype.join.call('abc')); // => "a,b,c"</code></pre> <p>slice()</p> <p>slice() 方法将数组中一部分元素浅复制存入新的数组对象,并且返回这个数组对象。</p> <pre> <code class="language-javascript">// 语法: arr.slice([start[, end]])</code></pre> <p>参数 start 指定复制开始位置的索引, end 如果有值则表示复制结束位置的索引(不包括此位置)。</p> <p>如果 start 的值为负数,假如数组长度为 length ,则表示从 length + start 的位置开始复制,此时参数 end 如果有值,只能是比 start 大的负数,否则将返回空数组。</p> <p>slice() 方法参数为空时,同 concat() 方法一样,都是浅复制生成一个新数组。</p> <pre> <code class="language-javascript">var array = ["one", "two", "three","four", "five"]; console.log(array.slice()); // => ["one", "two", "three","four", "five"] console.log(array.slice(2,3)); // => ["three"]</code></pre> <p><img src="https://simg.open-open.com/show/bec03afff671a9c93004bc7eca7f9850.png"></p> <p>浅复制是指当对象的被复制时,只是复制了对象的引用,指向的依然是同一个对象。下面来说明 slice() 为什么是浅复制。</p> <pre> <code class="language-javascript">var array = [{color:"yellow"}, 2, 3]; var array2 = array.slice(0,1); console.log(array2); // => [{color:"yellow"}] array[0]["color"] = "blue"; console.log(array2); // => [{color:"bule"}]</code></pre> <p><img src="https://simg.open-open.com/show/df6901206bedb98a6d19d9224d90a5e8.png"></p> <p>由于 slice() 是浅复制,复制到的对象只是一个引用,改变原数组 array 的值, array2 也随之改变。同时,稍微利用下 slice() 方法第一个参数为负数时的特性,我们可以非常方便的拿到数组的最后一项元素,如下:</p> <pre> <code class="language-javascript">console.log([1,2,3].slice(-1)); // => [3]</code></pre> <p>同上, slice() 一样受益于鸭式辨型。如下:</p> <pre> <code class="language-javascript">var o = { 0:{"color":"yellow"}, 1:2, 2:3, length:3 }; var o2 = Array.prototype.slice.call(o,0,1); console.log(o2); // => [{color:"yellow"}]</code></pre> <p>鉴于IE9以下版本对于该方法支持性并不是很好,如需更好的支持低版本IE浏览器,请参考 <a href="/misc/goto?guid=4959751566402440735" rel="nofollow,noindex">polyfill</a> 。</p> <p>toString()</p> <p>toString() 方法返回数组的字符串形式,该字符串由数组中的每个元素的 toString() 返回值经调用 join() 方法连接(由逗号隔开)组成。</p> <pre> <code class="language-javascript">var array = ['Jan', 'Feb', 'Mar', 'Apr']; var str = array.toString(); console.log(str); // => Jan,Feb,Mar,Apr</code></pre> <p>当数组直接和字符串作连接操作时,将会自动调用其 toString() 方法。</p> <pre> <code class="language-javascript">var str = ['Jan', 'Feb', 'Mar', 'Apr'] + ',May'; console.log(str); // => "Jan,Feb,Mar,Apr,May" // 下面我们来试试鸭式辨型 var o = {0:'Jan', 1:'Feb', 2:'Mar', length:3}; var o2 = Array.prototype.toString.call(o); console.log(o2); // => [object Object] console.log(o.toString()==o2); // => true</code></pre> <p>可见, Array.prototype.toString() 方法处理类数组对象时,跟类数组对象直接调用 Object.prototype.toString() 方法结果完全一致,说好的鸭式辨型呢?</p> <p>根据ES5语义, toString() 方法是通用的,可被用于任何对象。如果对象有一个 join() 方法,将会被调用,其返回值将被返回,没有则调用 Object.prototype.toString() ,为此,我们给o对象添加一个 join() 方法。如下:</p> <pre> <code class="language-javascript">var o = { 0:'Jan', 1:'Feb', 2:'Mar', length:3, join:function(){ return Array.prototype.join.call(this); } }; console.log(Array.prototype.toString.call(o)); // => "Jan,Feb,Mar"</code></pre> <p>toLocaleString()</p> <p>toLocaleString() 类似 toString() 的变型,该字符串由数组中的每个元素的 toLocaleString() 返回值经调用 join() 方法连接(由逗号隔开)组成。</p> <pre> <code class="language-javascript">// 语法: arr.toLocaleString()</code></pre> <p>数组中的元素将调用各自的 toLocaleString() 方法:</p> <ul> <li><strong>Object</strong> : <a href="/misc/goto?guid=4959751566489006124" rel="nofollow,noindex"> Object.prototype.toLocaleString() </a></li> <li><strong>Number</strong> : <a href="/misc/goto?guid=4959751566569497758" rel="nofollow,noindex"> Number.prototype.toLocaleString() </a></li> <li><strong>Date</strong> : <a href="/misc/goto?guid=4959751566653063863" rel="nofollow,noindex"> Date.prototype.toLocaleString() </a></li> </ul> <p>来看个简单示例:</p> <pre> <code class="language-javascript">var array= [{name:'zz'}, 123, "abc", new Date()]; var str = array.toLocaleString(); console.log(str); // => [object Object],123,abc,2016/1/5 下午1:06:23</code></pre> <p>其鸭式辨型的写法也同 toString() 保持一致,如下:</p> <pre> <code class="language-javascript">var o = { 0:123, 1:'abc', 2:new Date(), length:3, join:function(){ return Array.prototype.join.call(this); } }; console.log(Array.prototype.toLocaleString.call(o)); // => 123,abc,2016/1/5 下午1:16:50</code></pre> <p>indexOf()</p> <p>indexOf() 方法用于查找元素在数组中第一次出现时的索引,如果没有,则返回 -1 。</p> <pre> <code class="language-javascript">// 语法: arr.indexOf(element, fromIndex=0)</code></pre> <p>element 为需要查找的元素。 fromIndex 为开始查找的位置,缺省默认为 0 。如果超出数组长度,则返回 -1 。如果为负值,假设数组长度为 length ,则从数组的第 length + fromIndex 项开始往数组末尾查找,如果 length + fromIndex < 0 则整个数组都会被查找。</p> <p>indexOf() 使用严格相等(即使用 === 去匹配数组中的元素)。</p> <pre> <code class="language-javascript">var array = ['abc', 'def', 'ghi','123']; console.log(array.indexOf('def')); // => 1 console.log(array.indexOf('def',-1)); // => -1 此时表示从最后一个元素往后查找,因此查找失败返回-1 console.log(array.indexOf('def',-4)); // => 1 由于4大于数组长度,此时将查找整个数组,因此返回1 console.log(array.indexOf(123)); // => -1, 由于是严格匹配,因此并不会匹配到字符串'123'</code></pre> <p>得益于鸭式辨型, indexOf() 可以处理类数组对象。如下:</p> <pre> <code class="language-javascript">var o = { 0:'abc', 1:'def', 2:'ghi', length:3 }; console.log(Array.prototype.indexOf.call(o,'ghi',-4)); // => 2</code></pre> <p>然而该方法并不支持IE9以下版本,如需更好的支持低版本IE浏览器(IE6~8), 请参考 <a href="/misc/goto?guid=4959751566739236474" rel="nofollow,noindex">Polyfill</a> 。</p> <p>lastIndexOf()</p> <p>lastIndexOf() 方法用于查找元素在数组中最后一次出现时的索引,如果没有,则返回 -1 。并且它是 indexOf() 的逆向查找,即从数组最后一个往前查找。</p> <pre> <code class="language-javascript">// 语法: arr.lastIndexOf(element, fromIndex=length-1)</code></pre> <p>element 为需要查找的元素。 fromIndex 为开始查找的位置,缺省默认为数组长度 length - 1 。如果超出数组长度,由于是逆向查找,则查找整个数组。如果为负值,则从数组的第 length + fromIndex 项开始往数组开头查找,如果 length + fromIndex < 0 则数组不会被查找。</p> <p>同 indexOf() 一样, lastIndexOf() 也是严格匹配数组元素。</p> <p>举例请参考 indexOf() ,不再详述,兼容低版本IE浏览器(IE6~8),请参考 <a href="/misc/goto?guid=4959751566821296377" rel="nofollow,noindex">Polyfill</a> 。</p> <p>includes()</p> <p>includes() 方法基于ECMAScript 2016(ES7)规范,它用来判断当前数组是否包含某个指定的值,如果是,则返回 true ,否则返回 false 。</p> <pre> <code class="language-javascript">// 语法: arr.includes(element, fromIndex=0)</code></pre> <p>element 为需要查找的元素。 fromIndex 表示从该索引位置开始查找 element ,缺省为 0 ,它是正向查找,即从索引处往数组末尾查找。</p> <pre> <code class="language-javascript">var array = [-0, 1, 2]; console.log(array.includes(+0)); // => true console.log(array.includes(1)); // => true console.log(array.includes(2,-4)); // => true</code></pre> <p>以上, includes() 似乎忽略了 -0 与 +0 的区别,这不是问题,因为JavaScript一直以来都是不区分 -0 和 +0 的。</p> <p>你可能会问,既然有了 indexOf() 方法,为什么又造一个 includes() 方法, arr.indexOf(x) > -1 不就等于 arr.includes(x) ?看起来是的,几乎所有的时候它们都等同,唯一的区别就是 includes() 能够发现 NaN ,而 indexOf() 不能。</p> <pre> <code class="language-javascript">var array = [NaN]; console.log(array.includes(NaN)); // => true console.log(array.indexOf(NaN)>-1); // => false</code></pre> <p>该方法同样受益于鸭式辨型。如下:</p> <pre> <code class="language-javascript">var o = { 0:'a', 1:'b', 2:'c', length:3 }; var bool = Array.prototype.includes.call(o, 'a'); console.log(bool); // => true</code></pre> <p>该方法只有在Chrome 47、opera 34、Safari 9版本及其更高版本中才被实现。如需支持其他浏览器,请参考 <a href="/misc/goto?guid=4959751566904987382" rel="nofollow,noindex">Polyfill</a> 。</p> <p>toSource()</p> <p>toSource() 方法是非标准的,该方法返回数组的源代码,目前只有 Firefox 实现了它。</p> <pre> <code class="language-javascript">var array = ['a', 'b', 'c']; console.log(array.toSource()); // => ["a", "b", "c"] // 测试鸭式辨型 var o = { 0:'a', 1:'b', 2:'c', length:3 }; console.log(Array.prototype.toSource.call(o)); // => ["a","b","c"]</code></pre> <h3>遍历方法</h3> <p>基于ES6,不会改变自身的方法一共有 12 个,分别为 forEach() 、 every() 、 some() 、 filter() 、 map() 、 reduce() 、 reduceRight() 以及ES6新增的方法 entries() 、 find() 、 findIndex() 、 keys() 、 values() 。</p> <p>forEach()</p> <p>forEach() 方法指定数组的每项元素都执行一次传入的函数,返回值为 undefined 。</p> <pre> <code class="language-javascript">// 语法: arr.forEach(fn, thisArg)</code></pre> <p>fn 表示在数组每一项上执行的函数,接受三个参数:</p> <ul> <li>value 当前正在被处理的元素的值</li> <li>index 当前元素的数组索引</li> <li>array 数组本身</li> </ul> <p>thisArg 可选,用来当做 fn 函数内的 this 对象。</p> <p>forEach() 将为数组中每一项执行一次 fn 函数,那些已删除,新增或者从未赋值的项将被跳过(但不包括值为 undefined 的项)。</p> <p>遍历过程中, fn 会被传入上述三个参数。</p> <pre> <code class="language-javascript">var array = [1, 3, 5]; var obj = {name:'cc'}; var sReturn = array.forEach(function(value, index, array){ array[index] = value * value; console.log(this.name); // => cc被打印了三次 },obj); console.log(array); // => [1, 9, 25], 可见原数组改变了 console.log(sReturn); // => undefined, 可见返回值为undefined</code></pre> <p><img src="https://simg.open-open.com/show/630d3959065553483f7345d2c77fe7d7.png"></p> <p>得益于鸭式辨型,虽然 forEach() 不能直接遍历对象,但它可以通过 call() 方式遍历类数组对象。如下:</p> <pre> <code class="language-javascript">var o = { 0:1, 1:3, 2:5, length:3 }; Array.prototype.forEach.call(o,function(value, index, obj){ console.log(value,index,obj); obj[index] = value * value; },o); // => 1 0 Object {0: 1, 1: 3, 2: 5, length: 3} // => 3 1 Object {0: 1, 1: 3, 2: 5, length: 3} // => 5 2 Object {0: 1, 1: 9, 2: 5, length: 3} console.log(o); // => Object {0: 1, 1: 9, 2: 25, length: 3}</code></pre> <p><img src="https://simg.open-open.com/show/e92ac68d4d37c52e2da44769b056803a.png"></p> <p>参考前面的文章 <a href="/misc/goto?guid=4959751567003644268" rel="nofollow,noindex">详解JS遍历</a> 中 forEach() 的讲解,我们知道, forEach() 无法直接退出循环,只能使用 return 来达到 for 循环中 continue 的效果,并且 forEach() 不能在低版本IE(6~8)中使用,兼容写法请参考 <a href="/misc/goto?guid=4959751567083129127" rel="nofollow,noindex">Polyfill</a> 。</p> <p>every()</p> <p>every() 方法使用传入的函数测试所有元素,只要其中有一个函数返回值为 false ,那么该方法的结果为 false ;如果全部返回 true ,那么该方法的结果才为 true 。因此 every() 方法存在如下规律:</p> <ul> <li>若需检测数组中存在元素大于 100 (即 one > 100 ),那么我们需要在传入的函数中构造 “false” 返回值 (即返回 item <= 100 ),同时整个方法结果为 false 才表示数组存在元素满足条件;(简单理解为:若是单项判断,可用 one false ===> false )</li> <li>若需检测数组中是否所有元素都大于 100 (即 all > 100 )那么我们需要在传入的函数中构造 “true” 返回值 (即返回 item > 100 ),同时整个方法结果为 true 才表示数组所有元素均满足条件。(简单理解为:若是全部判断,可用 all true ===> true )</li> </ul> <p>语法同上述 forEach() ,具体还可以参考 <a href="/misc/goto?guid=4959751567170378252" rel="nofollow,noindex">详解JS遍历</a> 中 every() 的讲解。</p> <p>以下是鸭式辨型的写法:</p> <pre> <code class="language-javascript">var o = {0:10, 1:8, 2:25, length:3}; var bool = Array.prototype.every.call(o,function(value, index, obj){ return value >= 8; },o); console.log(bool); // => true</code></pre> <p>every() 一样不能在低版本IE(6~8)中使用,兼容写法请参考 <a href="/misc/goto?guid=4959751567257066755" rel="nofollow,noindex">Polyfill</a> 。</p> <p>some()</p> <p>some() 方法刚好同 every() 方法相反, some() 测试数组元素时,只要有一个函数返回值为 true ,则该方法返回 true ,若全部返回 false ,则该方法返回 false 。 some() 方法存在如下规律:</p> <ul> <li>若需检测数组中存在元素大于 100 (即 one > 100 ),那么我们需要在传入的函数中构造 “true” 返回值 (即返回 item > 100 ),同时整个方法结果为 true 才表示数组存在元素满足条件;(简单理解为:若是单项判断,可用 one true ===> true )</li> <li>若需检测数组中是否所有元素都大于 100 (即 all > 100 ),那么我们需要在传入的函数中构造 “false” 返回值 (即返回 item <= 100 ),同时整个方法结果为 false 才表示数组所有元素均满足条件。(简单理解为:若是全部判断,可用 all false ===> false )</li> </ul> <p>你注意到没有, some() 方法与 includes() 方法有着异曲同工之妙,他们都是探测数组中是否拥有满足条件的元素,一旦找到,便返回 true 。多观察和总结这种微妙的关联关系,能够帮助我们深入理解它们的原理。</p> <p>some() 的鸭式辨型写法可以参照 every() ,同样它也不能在低版本IE(6~8)中使用,兼容写法请参考 <a href="/misc/goto?guid=4959751567347834368" rel="nofollow,noindex">Polyfill</a> 。</p> <p>filter()</p> <p>filter() 方法使用传入的函数测试所有元素,并返回所有通过测试的元素组成的新数组。它就好比一个过滤器,筛掉不符合条件的元素。</p> <pre> <code class="language-javascript">var array = [18, 9, 10, 35, 80]; var array2 = array.filter(function(value, index, array){ return value > 20; }); console.log(array2); // => [35, 80]</code></pre> <p>filter() 一样支持鸭式辨型,具体请参考 every() 方法鸭式辨型写法。其在低版本IE(6~8)的兼容写法请参考 <a href="/misc/goto?guid=4959751567428963584" rel="nofollow,noindex">Polyfill</a> 。</p> <p>map()</p> <p>map() 方法遍历数组,使用传入函数处理每个元素,并返回函数的返回值组成的新数组。</p> <pre> <code class="language-javascript">// 语法: arr.map(fn, thisArg)</code></pre> <p>参数介绍同 forEach() 方法的参数介绍。</p> <p>具体用法请参考 <a href="/misc/goto?guid=4959751567513965873" rel="nofollow,noindex">详解JS遍历</a> 中 map() 的讲解。</p> <p>map() 一样支持鸭式辨型, 具体请参考 every() 方法鸭式辨型写法。</p> <p>其在低版本IE(6~8)的兼容写法请参考 <a href="/misc/goto?guid=4959751567595381127" rel="nofollow,noindex">Polyfill</a> 。</p> <p>reduce()</p> <p>reduce() 方法接收一个方法作为累加器,数组中的每个值(从左至右) 开始合并,最终为一个值。</p> <pre> <code class="language-javascript">// 语法: arr.reduce(fn, initialValue)</code></pre> <p>fn 表示在数组每一项上执行的函数,接受四个参数:</p> <ul> <li>previousValue 上一次调用回调返回的值,或者是提供的初始值</li> <li>value 数组中当前被处理元素的值</li> <li>index 当前元素在数组中的索引</li> <li>array 数组自身</li> </ul> <p>initialValue 指定第一次调用 fn 的第一个参数。</p> <p>当 fn 第一次执行时:</p> <ul> <li>如果 initialValue 在调用 reduce() 时被提供,那么第一个 previousValue 将等于 initialValue ,此时 item 等于数组中的第一个值;</li> <li>如果 initialValue 未被提供,那么 previousVaule 等于数组中的第一个值, item 等于数组中的第二个值。此时如果数组为空,那么将抛出 <strong> TypeError </strong> 。</li> <li>如果数组仅有一个元素,并且没有提供 initialValue ,或提供了 initialValue 但数组为空,那么 fn 不会被执行,数组的唯一值将被返回。</li> </ul> <p>看个简单示例:</p> <pre> <code class="language-javascript">var array = [1, 2, 3, 4]; var s = array.reduce(function(previousValue, value, index, array){ return previousValue * value; },1); console.log(s); // => 24 // ES6写法更加简洁 array.reduce((p, v) => p * v); // => 24</code></pre> <p>以上回调被调用4次,每次的参数和返回见下表:</p> <table> <thead> <tr> <th>callback</th> <th>previousValue</th> <th>currentValue</th> <th>index</th> <th>array</th> <th>retrun value</th> </tr> </thead> <tbody> <tr> <td>第1次</td> <td>1</td> <td>1</td> <td>1</td> <td>[1, 2, 3, 4]</td> <td>1</td> </tr> <tr> <td>第2次</td> <td>1</td> <td>2</td> <td>2</td> <td>[1, 2, 3, 4]</td> <td>2</td> </tr> <tr> <td>第3次</td> <td>2</td> <td>3</td> <td>3</td> <td>[1, 2, 3, 4]</td> <td>6</td> </tr> <tr> <td>第4次</td> <td>6</td> <td>4</td> <td>4</td> <td>[1, 2, 3, 4]</td> <td>24</td> </tr> </tbody> </table> <p>reduce() 一样支持鸭式辨型,具体请参考 every() 方法鸭式辨型写法。</p> <p>其在低版本IE(6~8)的兼容写法请参考 <a href="/misc/goto?guid=4959751567679769419" rel="nofollow,noindex">Polyfill</a> 。</p> <p>reduceRight()</p> <p>reduceRight() 方法接收一个方法作为累加器,数组中的每个值(从右至左)开始合并,最终为一个值。除了与 reduce() 执行方向相反外,其他完全与其一致,请参考上述 reduce() 方法介绍。</p> <p>其在低版本IE(6~8)的兼容写法请参考 <a href="/misc/goto?guid=4959751567759934266" rel="nofollow,noindex">Polyfill</a> 。</p> <p>entries()</p> <p>entries() 方法基于ECMAScript 2015(ES6)规范,返回一个数组迭代器对象,该对象包含数组中每个索引的键值对。</p> <pre> <code class="language-javascript">var array = ["a", "b", "c"]; var iterator = array.entries(); console.log(iterator.next().value); // => [0, "a"] console.log(iterator.next().value); // => [1, "b"] console.log(iterator.next().value); // => [2, "c"] console.log(iterator.next().value); // => undefined, 迭代器处于数组末尾时, 再迭代就会返回undefined</code></pre> <p><img src="https://simg.open-open.com/show/a89898224d8ba03bb77fc36d11c53501.png"></p> <p>很明显, entries() 也受益于鸭式辨型,如下:</p> <pre> <code class="language-javascript">var o = { 0:"a", 1:"b", 2:"c", length:3 }; var iterator = Array.prototype.entries.call(o); console.log(iterator.next().value); // => [0, "a"] console.log(iterator.next().value); // => [1, "b"] console.log(iterator.next().value); // => [2, "c"]</code></pre> <p>find() & findIndex()</p> <p>find() 方法基于ECMAScript 2015(ES6)规范,返回数组中第一个满足条件的元素(如果有的话), 如果没有,则返回 undefined 。</p> <p>findIndex() 方法也基于ECMAScript 2015(ES6)规范,它返回数组中第一个满足条件的元素的索引(如果有的话)否则返回 -1 。</p> <pre> <code class="language-javascript">// 语法: arr.find(fn, thisArg),arr.findIndex(fn, thisArg)</code></pre> <p>我们发现它们的语法与 forEach() 等十分相似,其实不光语法, find() (或 findIndex() )在参数及其使用注意事项上,均与 forEach() 一致。因此此处将略去 find() (或 findIndex() )的参数介绍。下面我们来看个例子:</p> <pre> <code class="language-javascript">var array = [1, 3, 5, 7, 8, 9, 10]; function f(value, index, array){ return value%2==0; // 返回偶数 } function f2(value, index, array){ return value > 20; // 返回大于20的数 } console.log(array.find(f)); // => 8 console.log(array.find(f2)); // => undefined console.log(array.findIndex(f)); // => 4 console.log(array.findIndex(f2)); // => -1</code></pre> <p>由于其鸭式辨型写法也与 forEach() 方法一致,故此处略去。</p> <p>keys()</p> <p>keys() 方法基于ECMAScript 2015(ES6)规范,返回一个数组索引的迭代器。</p> <pre> <code class="language-javascript">var array = ["abc", "xyz"]; var iterator = array.keys(); console.log(iterator.next()); // => Object {value: 0, done: false} console.log(iterator.next()); // => Object {value: 1, done: false} console.log(iterator.next()); // => Object {value: undefined, done: false}</code></pre> <p><img src="https://simg.open-open.com/show/7b28def6d6c41100ad2cf6e88a52f879.png"></p> <p>索引迭代器会包含那些没有对应元素的索引,如下:</p> <pre> <code class="language-javascript">var array = ["abc", , "xyz"]; var sparseKeys = Object.keys(array); var denseKeys = [...array.keys()]; console.log(sparseKeys); // => ["0", "2"] console.log(denseKeys); // => [0, 1, 2]</code></pre> <p>其鸭式辨型写法请参考上述 entries() 方法。</p> <p>前面我们用 Array.from() 生成一个从 0 到指定数字的新数组,利用 keys() 也很容易实现。</p> <pre> <code class="language-javascript">[...Array(10).keys()]; // => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] [...new Array(10).keys()]; // => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]</code></pre> <p>由于 Array 的特性, new Array 和 Array 对单个数字的处理相同,因此以上两种均可行。</p> <p>values()</p> <p>values() 方法基于ECMAScript 2015(ES6)规范,返回一个数组迭代器对象,该对象包含数组中每个索引的值。其用法基本与上述 entries() 方法一致。</p> <p>遗憾的是,现在没有浏览器实现了该方法,因此下面将就着看看吧。</p> <pre> <code class="language-javascript">var array = ["abc", "xyz"]; var iterator = array.values(); console.log(iterator.next().value); // => abc console.log(iterator.next().value); // => xyz</code></pre> <p>采用在线的 <a href="/misc/goto?guid=4958871204393151840" rel="nofollow,noindex">Babel工具</a> ,可以打印出结果。</p> <p>Symbol.iterator()</p> <p>该方法基于ECMAScript 2015(ES6)规范,同 values() 方法功能相同。</p> <pre> <code class="language-javascript">var array = ["abc", "xyz"]; var iterator = array[Symbol.iterator](); console.log(iterator.next().value); // => abc console.log(iterator.next().value); // => xyz</code></pre> <p>其鸭式辨型写法请参考上述 entries() 方法。</p> <h2>小结</h2> <p>以上, Array.prototype 的各方法基本介绍完毕,这些方法之间存在很多共性。比如:</p> <ul> <li>所有插入元素的方法, 比如 push() 、 unshift() ,一律返回数组新的长度;</li> <li>所有删除元素的方法,比如 pop() 、 shift() 、 splice() 一律返回删除的元素,或者返回删除的多个元素组成的数组;</li> <li>部分遍历方法,比如 forEach() 、 every() 、 some() 、 filter() 、 map() 、 find() 、 findIndex() ,它们都包含 function(value,index,array){} 和 thisArg 这样两个形参。</li> </ul> <p>Array.prototype 的所有方法均具有鸭式辨型这种神奇的特性。它们不止可以用来处理数组对象,还可以处理类数组对象。</p> <p>例如 JavaScript 中一个纯天然的类数组对象字符串( String ),像 join() 方法(不改变当前对象自身)就完全适用,可惜的是 Array.prototype 中很多方法均会去试图修改当前对象的 length 属性,比如说 pop() 、 push() 、 shift() , unshift() 方法,操作 String 对象时,由于 String 对象的长度本身不可更改,这将导致抛出 <strong> TypeError </strong> 错误。</p> <p>还记得么, Array.prototype 本身就是一个数组,并且它的长度为 0 。</p> <p>后续章节我们将继续探索Array的一些事情。感谢您的阅读!</p> <p> </p> <p>来自:<a href="/misc/goto?guid=4959751567869818329" rel="nofollow,noindex">http://louiszhai.github.io/2017/04/28/array/</a></p> <p> </p>
本文由用户 RYPJulius 自行上传分享,仅供网友学习交流。所有权归原作者,若您的权利被侵害,请联系管理员。
转载本站原创文章,请注明出处,并保留原始链接、图片水印。
本站是一个以用户分享为主的开源技术平台,欢迎各类分享!