Array.prototype.slice.call(arguments)

今天在使用 Array.prototype.slice.call(arguments); 的时候,突然有一个疑问?为什么 slice() 方法可以将类数组对象/集合转换成一个新数组?

slice()介绍

我们知道,slice() 方法返回一个从开始到结束(不包括结束)选择的数组的一部分浅拷贝到一个新数组对象。且原始数组不会被修改。

基本语法为:

1
2
3
4
5
6
7
8
arr.slice();
// [0, end]

arr.slice(begin);
// [begin, end]

arr.slice(begin, end);
// [begin, end)

参数:

begin 可选:

  • 从该索引处开始提取原数组中的元素(从0开始)。
  • 如果该参数为负数,则表示从原数组中的倒数第几个元素开始提取,slice(-2)表示提取原数组中的倒数第二个元素到最后一个元素(包含最后一个元素)。
  • 如果省略 begin,则 slice 从索引 0 开始。

end 可选:

  • 在该索引处结束提取原数组元素(从0开始)。slice会提取原数组中索引从 begin 到 end 的所有元素(包含begin,但不包含end)。
  • slice(1,4) 提取原数组中的第二个元素开始直到第四个元素的所有元素 (索引为 1, 2, 3的元素)。
  • 如果该参数为负数, 则它表示在原数组中的倒数第几个元素结束抽取。 slice(-2,-1)表示抽取了原数组中的倒数第二个元素到最后一个元素(不包含最后一个元素,也就是只有倒数第二个元素)。
  • 如果 end 被省略,则slice 会一直提取到原数组末尾。
  • 如果 end 大于数组长度,slice 也会一直提取到原数组末尾。

返回值为一个含有提取元素的新数组

slice 不修改原数组,只会返回一个浅复制了原数组中的元素的一个新数组。原数组的元素会按照下述规则拷贝:

  • 如果该元素是个对象引用 (不是实际的对象),slice 会拷贝这个对象引用到新的数组里。两个对象引用都引用了同一个对象。如果被引用的对象发生改变,则新的和原来的数组中的这个元素也会发生改变。
  • 对于字符串、数字及布尔值来说(不是 String、Number 或者 Boolean 对象),slice 会拷贝这些值到新的数组里。在别的数组里修改这些字符串或数字或是布尔值,将不会影响另一个数组。

如果向两个数组任一中添加了新元素,则另一个不会受到影响。

slice() 转换类数组

slice 方法可以用来将一个类数组(Array-like)对象/集合转换成一个新数组。你只需将该方法绑定到这个对象上。 一个函数中的 arguments 就是一个类数组对象的例子:

1
2
3
4
5
function list() {
return Array.prototype.slice.call(arguments);
}

var list1 = list(1, 2, 3); // [1, 2, 3]

除了使用 Array.prototype.slice.call(arguments),你也可以简单的使用 [].slice.call(arguments) 来代替。另外,你可以使用 bind 来简化该过程:

1
2
3
4
5
6
7
8
var unboundSlice = Array.prototype.slice;
var slice = Function.prototype.call.bind(unboundSlice);

function list() {
return slice(arguments);
}

var list1 = list(1, 2, 3); // [1, 2, 3]

需要注意的是,IE9以下的IE浏览器的节点集合(因为ie8及之前版本的dom对象是以com对象的形式实现的,js对象与com对象不能进行转换):

1
2
3
4
5
var a={length:2,0:'first',1:'second'};
Array.prototype.slice.call(a);// ["first", "second"]

var a={length:2};
Array.prototype.slice.call(a);// [undefined, undefined]

针对于 slice() 方法的语法和参数规则,可能很难理解这句为什么能实现以上的转换功能,我就是一个。

首先,slice有两个用法,一个是String.slice,一个是Array.slice,第一个返回的是字符串,第二个返回的是数组,这里我们看第2个。

Array.prototype.slice.call(arguments) 能够将arguments转成数组,那么就是 arguments.toArray().slice();到这里,是不是就可以说 Array.prototype.slice.call(arguments) 的过程就是先将传入进来的第一个参数转为数组,再调用slice?

再看call的用法,如下例子:

1
2
3
4
5
6
var a = function(){
console.log(this); // 'littledu'
console.log(typeof this); // Object
console.log(this instanceof String); // true
}
a.call('littledu');

可以看出,call了后,就把当前函数推入所传参数的作用域中去了,不知道这样说对不对,但反正this就指向了所传进去的对象就肯定的了。

到这里,基本就差不多了,我们可以大胆猜一下slice的内部实现,如下:

1
2
3
4
5
6
7
8
9
Array.prototype.slice = function(start,end){
var result = new Array();
start = start || 0;
end = end || this.length; //this指向调用的对象,当用了call后,能够改变this的指向,也就是指向传进来的对象,这是关键
for(var i = start; i < end; i++){
result.push(this[i]);
}
return result;
}

我们再看看 MDN 上的 精简跨浏览器行为 描述:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/**
* Shim for "fixing" IE's lack of support (IE < 9) for applying slice
* on host objects like NamedNodeMap, NodeList, and HTMLCollection
* (technically, since host objects have been implementation-dependent,
* at least before ES2015, IE hasn't needed to work this way).
* Also works on strings, fixes IE < 9 to allow an explicit undefined
* for the 2nd argument (as in Firefox), and prevents errors when
* called on other DOM objects.
*/
(function () {
'use strict';
var _slice = Array.prototype.slice;

try {
// Can't be used with DOM elements in IE < 9
_slice.call(document.documentElement);
} catch (e) { // Fails in IE < 9
// This will work for genuine arrays, array-like objects,
// NamedNodeMap (attributes, entities, notations),
// NodeList (e.g., getElementsByTagName), HTMLCollection (e.g., childNodes),
// and will not fail on other DOM objects (as do DOM elements in IE < 9)
Array.prototype.slice = function(begin, end) {
// IE < 9 gets unhappy with an undefined end argument
end = (typeof end !== 'undefined') ? end : this.length;

// For native Array objects, we use the native slice function
if (Object.prototype.toString.call(this) === '[object Array]'){
return _slice.call(this, begin, end);
}

// For array like object we handle it ourselves.
var i, cloned = [],
size, len = this.length;

// Handle negative value for "begin"
var start = begin || 0;
start = (start >= 0) ? start : Math.max(0, len + start);

// Handle negative value for "end"
var upTo = (typeof end == 'number') ? Math.min(end, len) : len;
if (end < 0) {
upTo = len + end;
}

// Actual expected size of the slice
size = upTo - start;

if (size > 0) {
cloned = new Array(size);
if (this.charAt) {
for (i = 0; i < size; i++) {
cloned[i] = this.charAt(start + i);
}
} else {
for (i = 0; i < size; i++) {
cloned[i] = this[start + i];
}
}
}

return cloned;
};
}
}());

我们看看到第23行至24行的描述:

1
2
// IE < 9 gets unhappy with an undefined end argument
end = (typeof end !== 'undefined') ? end : this.length;

到这里,可以证明我们上面的猜测是对的,我相信,你应该和我一样加深了对 slice() 的理解,同时也解决了之前的疑惑了吧~



完~