Array.prototype.slice.call(arguments)中的技术细节

看到《JavaScript高级程序设计》中介绍了使用 Array.prototype.slice.call(arguments)将arguments对象转换为数组的方法,产生一些疑问:

  • 为什么需要将arguments转换为数组,它本身不是数组吗?
  • slice()方法有什么作用?
  • call()方法有什么作用?
  • 为什么是Array.prototype.slice()而不是Array.slice()?

查了一些资料,基本明白了以上问题,答案记录如下,js新手,如有错漏欢迎拍砖。

为什么需要将arguments转换为数组,它本身不是数组吗?

arguments是EMCAScript定义的关键字,指的是传入的参数,它一个Array-like对象,顾名思义,arguments对象类似数组,拥有数组的部分特性,如index,length。但它不具备数组的其他方法。

function foo(){
    consolelog(arguments);
    console.log(arguments[0]);
    console.log(arguments.length);
}

foo('a','b','c');
// ["a", "b", "c"]
// a
// 3

slice()方法有什么作用?

slice()方法把数组中一部分的浅复制存入一个新的数组对象中,并返回这个新的数组,它接受两个参数,起始位置和结束位置,拷贝区间为闭开区间(即不包含结束为止)。 如果只传入一个参数,则从该位置拷贝至末尾; 如果传入参数为负数,则指倒数第几个位置; 如果不传入参数或只传入一个0,则完整拷贝该数组;

var arr = [1,2,3,4]
var arr1 = arr.slice(1);    //arr1 = [2,3,4]
var arr2 = arr.slice(0,-2); //arr2 = [1,2]
var arr3 = arr.slice();     //arr3 = [1,2,3,4]

call()方法有什么作用?

call方法能指定调用某个函数的对象和参数。或者说将函数放到一个指定的作用域中执行,这样,this就指向了该作用域。call()方法接收两个参数,第一个是指定的对象,第二个是传入执行函数的参数列表。

window.color = 'red';
var obj = { color: 'blue'};

function sayColor(){
    console.log(this.color);
}

sayColor();             //red

sayColor.call();        //red
sayColor.call(window);  //red
sayColor.call(obj);     //blue

上例中,在全局作用域定义了一个sayColor函数,当在全局作用域调用时,this指向window对象;当call()没有传入指定作用域对象时,this也默认指向window对象;而当指定作用域对象为obj后,sayColor的this指向了obj。

所以,此时就能明白,在Array.prototype.slice.call(arguments)中,由于arguments不是数组,不具备slice()方法,因此通过call()方法使arguments调用Array.prototype.slice()方法从而将arguments类数组对象转换为数组对象。 那么就只剩下最后一个问题:

为什么是Array.prototype.slice()而不是Array.slice()?

先感受一下是否存在Array.prototype.slice()Array.slice()

2015-05-28_163140

可以看到,并没有Array.slice这个属性或者方法。为什么?这里,必须先明白什么是对象方法,什么是类方法,什么是原型方法。 JavaScript中构造函数有三种方式定义方法:

function Person(name){
    this.name = name;
    //对象方法
    this.sayName = function(){
        console.log('My name is ' + this.name);
    };
}

//类方法
Person.friends = function(){
    console.log('Bob is my friend');
};

//原型方法
Person.prototype.email = function(){
    console.log( this.name.toLowerCase() + '@email.com');
};

var person1 = new Person('Harper');
person1.sayName();      //My name is Harper
Person.friends();       //Bob is my friend
person1.email();        //harper@email.com

//以下需特别注意
person1.friends();              //抛错"person1.friends is not a function"
person.constructor.friends()    //Bob is my friend

以上就是定义构造函数的方法的三种方式。特别需要注意的是Person.friends:通过类方法定义类方法(属性)时,由于类方法(属性)是和类,也就是构造函数本身关联的,并没有和实例进行关联,因此无法通过实例直接访问该属性。如果需要通过实例对该方法进行访问的话,必须通过实例的constructor属性获取该实例的构造函数进行访问类方法。

接下来,解决下一个问题:

prototype是什么?

我们所创建的每一个函数都有一个prototype属性。这个属性是指针,指向某个对象,这个对象的作用是包含可以由特定类型的所有实例共享的属性和方法。这样,我们在某个构造函数的prototype上添加的方法和属性,就可以被该构造函数的所有实例所共享。 看一个例子:

function baseClass(){
    this.from = function(){
        console.log('From baseClass');
    };
}

function extendClass(){
}

extendClass.prototype = new baseClass();

var instance = new extendClass();
instance.from();    //From baseClass

首先定义了一个baseClass构造函数,然后定义了extendClass构造函数,我们将extendClass.prototype指向baseClass的实例,因此,当我们创建一个extendClass的实例时,就能够共享到baseClass的方法了。

prototype有个种特性

1.当实例与原型存在同名属性时,原型中的同名属性会被屏蔽,无法访问。

function baseClass()
{
    this.from = function()
    {
        console.log("From baseClass");
    };
}

function extendClass()
{
    this.from =function ()
    {
        console.log("From extendClass");
    };
}

extendClass.prototype = new baseClass();
var instance = new extendClass();

instance.from();    //From extendClass

instance.from = function(){
    console.log("From instance");
};

以上代码创建了两个构造函数bassClass和extendClass,其中,extendClass.prototype指向baseClass,它们都拥有同名方法from,之后创建了extendClass的实例instance,当访问instance.from()时,访问的是extendClass中的方法。作为拓展,可看以下例子:

function Person(){
}
Person.prototype.name = "Greg";
Person.prototype.age = 21;

var person1 = new Person();
var person2 = new Person();

person1.name = "Bob";
console.log(person1.name);  //Bob,来自实例
console.log(person2.name);  //Greg,来自原型

delete person1.name;
console.log(person1.name);  //Greg,来自原型

可以发现,当我们修改实例person1的name属性并没有生效,实质上person1.name = "Bob"是在该实例上创建了一个name属性,由于优先查找实例属性,因此将原型上的同名属性屏蔽了。 当使用delete操作符删除实例属性后,又可以访问到原型属性了。

2.原型的动态性,因为prototype存储的是一个指针,因此在原型上做的任何修改都能够立即从实例上反应出来。

var friend = new Person();

Person.prototype.sayHi = function(){
    console.log('Hi');
};
friend.sayHi();     //Hi

以上代码先创建了Person的一个实例,再在Person.prototype上添加一个方法,之前的实例依然能够访问到这个新方法。

如何使用extendClass的实例instance调用baseClass的from方法?

还记得之前讲到的call()方法吗?这里只要使用call()方法将baseClass的from方法指向instance即可。

extendClass.prototype = new baseClass();
var instance = new extendClass();


var baseinstance = new baseClass();
baseinstance.showMsg.call(instance);//From baseClass

好了,现在回到最初的问题:为什么是Array.prototype.slice()而不是Array.slice()? 因为slice()是在Array.prototype上定义的方法,而Array是一个对象或者说构造函数,定义在它原型上的方法只有Array的实例才能访问,或者通过Array.prototype进行直接访问。下面是一个例子:

function Person(name){
    this.name = name;
}
Person.prototype.age = 21;
Person.prototype.email = function(){
    console.log( this.name.toLowerCase() + '@email.com');
};

var person1 = new Person('Harper');
person1.name;                           //Harper
person1.age;                            //21
person1.email();                        //harper@email.com
Person.age;                             //undefine
Person.email();                         //抛错"Person.email is not a function"
Person.prototype.age;                   //21
Person.prototype.email.call(person1);   //harper@email.com

这里是不是和Array.prototype.slice.call(arguments)类似呢。