和这个问题相似的一个问题是:为什么isArray
在Array
上而不在Array.prototype
上。即通常调用isArray
方法是使用Array.isArray()
,而不是Array.prototype.isArray()
。
方法放在原型对象上(即Array.prototype
上)大家可以理解,以前在实现继承的时候,就使用过:
function Person() {
this.name = 'fan';
}
// 方法放在原型对象上,达到共享方法、节省存储空间的效果
Person.prototype.sayHello = function () {
console.log('hello');
};
先说明为什么方法可以放在构造函数上
把方法放在构造函数上很少使用,这就是问题的关键,实际上构造函数也是一个对象,它是Function
构造函数的一个实例,既然是对象,那就可以有自己的属性和方法,当然就可以把方法放在构造函数上了,比如sayHello
这个方法,可以把它放在Person
构造函数(也是Person
对象)上,当作Person的一个成员。每个JavaScript函数实际上都是一个Function
实例。
Function
构造函数自己也是一个对象,但它没有自己的属性和方法,它也有自己的原型对象,用来放一些属性和方法,比如常用的call
、bind
、apply
方法实际上就是放在Function.prototype
上的,所以,只要是函数,就能通过原型链访问到这些方法,因为JS函数是Function
实例对象,会指向Function.prototype
。
通过以下代码就可以看到一个JS函数是Function实例,并且上面有自己定义的方法:
接下来说明把方法放在原型对象上和放在构造函数上有什么区别
由于原型链的关系,凡是放在原型对象上的方法,都可以通过实例直接调用该方法。通俗地讲,就是一个实例只能访问到原型链上(各级原型对象上)的属性和方法,比如一个数组:
let arr = [1, 2, 3, 4, 5];
// 可以直接用arr调用slice方法
let arrCopy1 = arr.slice();
// 也可以用Array.prototype.slice.call调用
let arrCopy2 = Array.prototype.slice.call(arr);
但是,只要这个方法放在构造函数上,比如from
方法,它就不能被实例直接调用,实例访问不到它,如下:
let arr = [1, 2, 3];
// 错误的调用
let arrCopy = arr.from(); // TypeError: arr.from is not a function
// 正确的调用
let arrCopy = Array.from(arr);
最后是为什么要把方法放在不同的地方
比如Array.isArray
,它是判断一个变量是否是一个数组,如果把它放在Array.prototype上,根据前面讲的特性,那么只有当一个变量已经是数组的情况下,才能调用isArray
,也就是说,一个变量arr
,如果是数组,调用arr.isArray()
就是true
,如果不是数组,那它根本就没办法调用isArray
。况且,变量可能是各种各样的值,比如基本类型的number
、string
、undefined
、null
。null
是没有办法调用任何函数的。所以,isArray
方法必须放在Array构造函数上,每次调用它都用Array.isArray()
。
不过,如果使用call
强行用实例直接调用原型对象上的方法,也可以实现,但不提倡:
function Person() {}
Person.prototype.myIsArray = Array.isArray;
let person = new Person();
// 第一个参数是null,因为call是把函数内的this绑定为第一个参数,Array.isArray方法是判断参数是否为数组
console.log(person.myIsArray.call(null, [1, 2]));
// 经过测试,可以得到正确的结果,判断一个东西是否是数组
通过Array.isArray()
,可以知道其他内置对象的方法放在原型对象上还是在构造函数上,原因都大同小异。
我个人认为from
方法之所以放在Array构造函数上是因为不是所有数据类型都能转成数组的,所以在其函数体中,一定有对类型的判断,比如Array.from(null)
就会报TypeError: Cannot convert undefined or null to object
,显然,放在原型对象上就不合适了(null没法调用任何方法,而from
方法必须对不合适的数据类型进行处理)。
对于slice
,它可以切割数组,也可以切割字符串,还可以把类数组对象转成数组。对于其他数据类型的变量,是不允许切的,所以根本就不允许其他数据类型的变量调用slice
。