call() 和apply()
call()方法调用一个函数,其具有一个指定的this值和分别提供的参数(参数的列表)
call()和apply()的区别在于,call()方法接受的是若干个参数的列表,而apply()方法接受的是一个包含多个参数的数组
var func = function(arg1,arg2){
.....
};
func.call(this,arg1,arg2); // 使用call,参数列表
func.apply(this,[arg1,arg2]); // 使用apply,参数数组
- 使用场景
下面列举一些常用用法:
- 合并两个数组
let p1 = ['pars','pot'];
let p2 = ['cel','beet'];
// 将第二个数组融合进第一个数组
// 相当于p1.push('cel','beet');
Array.prototype.push.apply(p1,p2);
// 4
p1;
// ['pars','pot','cel','beet']
当第二个数组(如示例中的p2)太大时不要使用这个方法来合并数组,因为一个函数能够接受的参数个数时有限制的。不同的引擎有不同的限制,JS核心限制在65535,有些引擎会抛出异常,有些不抛出异常但会丢失多余参数。
解决方法: 将参数数组切块后循环传入目标方法
function concatArray(arr1, arr2) {
var QUANTUM = 32768;
for (var i = 0, len = arr2.length; i < len; i += QUANTUM) {
Array.prototype.push.apply(
arr1,
arr2.slice(i, Math.min(i + QUANTUM, len) )
);
}
return arr1;
}
// 验证代码
var arr1 = [-3, -2, -1];
var arr2 = [];
for(var i = 0; i < 1000000; i++) {
arr2.push(i);
}
Array.prototype.push.apply(arr1, arr2);
// Uncaught RangeError: Maximum call stack size exceeded
concatArray(arr1, arr2);
// (1000003) [-3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]
2.获取数组中最大值和最小值
let nums = [5,456,120,-217];
Math.max.apply(Math,nums); // 456
Math.max.call(Math,5,456,120,-217); //456
// ES6
Math.max.call(Math,...nums); // 456
为什么要这么用呢,因为数组nums本身没有max方法,但是Math有,所以这里就是借助call/apply使用Math.max方法。
3.验证是否是数组
function isArray(obj) {
return Object.prototype.toString.call(obj) === '[object Array]';
}
isArray([1,2,3]);
// true
// 直接使用toString()
[1, 2, 3].toString(); // "1,2,3"
"123".toString(); // "123"
123.toString(); // SyntaxError: Invalid or unexpected token
Number(123).toString(); // "123"
Object(123).toString(); // "123"
可以通过toString()来获取每个对象的类型,但是不同对象的toString()有不同的实现,所以通过Object.prototype.toString()来检测,需要以call() / apply() 的形式来调用,传递要检查的对象作为第一个参数。
另一个验证是否是数组的方法
let toStr = Function.prototype.call.bind(Object.prototype.toString);
function isArray(obj) {
return toStr(obj) === '[object Array]';
}
isArray([1,2,3]);
// true
// 使用改造后的toStr
toStr([1,2,3]); // "[object Array]"
toStr('123'); // "[object String]"
toStr(123); // "[object Number]"
toStr(Obejct(123)); // "[object Number]"
上面方法首先使用Function.prototype.call函数指定一个 this 值,然后.bind返回一个新的函数,始终将Object.prototype.toString设置为传入参数。等价于Object.prototype.toString.call()。
这个有一个前提toString()方法没有被覆盖
Object.prototype.toSting = function() {
return '';
}
isArray([1,2,3]);
// false
- 类数组对象(Array-like Object)使用数组方法
var domNodes = document.getElementsByTagName('*');
domNodes.unshift('h1');
// TypeError: domNodes.unshift is not a function
var domNodeArrays = Array.prototype.slice.call(domNodes);
domNodeArrays.unshift('h1'); // 505 不同环境下数据不同
// (505) ["h1", html.gr__hujiang_com, head, meta, ...]
类数组对象有下面两个特性
- 具有:指向对象元素的数字索引下标和length属性
- 不具有:比如 push 、shift 、forEach 以及 indexOf等数组对象具有的方法 要说明的是,类数组对象是一个对象 。JS中存在一种名为类数组的对象结构,比如 arguments对象,还有DOM API返回的NodeList对象都属于类数组对象、类数组对象不能使用 push/pop/shift/unshift等方法,通过 Array.prototype.slice.call转换成真正的数组,就可以使用 Array 下所有方法。
类数组对象转数组的其他方法:
// 上面代码等同于
var arr = [].slice.call(arguments);
ES6:
let arr = Array.from(arguments);
let arr = [...arguments];
Array.from()可以将两类对象转为真正的数组:类数组对象和可遍历对象(包括ES6新增的数据结构Set和Map)。
PS扩展一: 为什么通过Array.prototype.slice.call()就可以把类数组对象转换成数组? 因为slice将类数组对象通过下标操作放进了新的Array里面。
- 调用父构造函数实现继承
function SuperType() {
this.color = ['red','green','blue'];
}
function SubType() {
// 核心代码,继承自SuperType
SuperType.call(this);
}
let instance1 = new SubType();
instance1.color.push('black');
console.log(instance1.color);
// ['red','green','blue','black']
let instance2 = new SubType();
console.log(instance2.color);
// ['red','green','blue']
在子构造函数中,通过调用父构造函数的call方法来实现继承,于是SubType的每个实例都会将SuperType中的属性复制一份。
缺点:
- 只能继承父类的实例属性和方法,不能继承原型属性/方法
- 无法实现复用,每个子类都有父类实例函数的副本,影响性能
call的模拟实现
简单的例子:
var val = 1;
var obj = {
val:1
}
function foo() {
console.log(this.val);
}
foo.call(obj); // 1
通过上面的介绍我们知道,call()主要有以下两点
- call()改变了this的指向
- 函数foo执行了
call和apply模拟实现汇总
call的模拟实现
Function.prototype.call = function (context) {
context = context ? Object(context) : window;
var fn = Symbol(); // added
context[fn] = this; // changed
let args = [...arguments].slice(1);
let result = context[fn](...args); // changed
delete context[fn]; // changed
return result;
}
apply的模拟实现
Function.prototype.apply = function (context, arr) {
context = context ? Object(context) : window;
context.fn = this;
let result;
if (!arr) {
result = context.fn();
} else {
result = context.fn(...arr);
}
delete context.fn
return result;
}