深度解析call 和apply 原理,使用场景及实现

693 阅读1分钟

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,参数数组
  • 使用场景

下面列举一些常用用法:

  1. 合并两个数组
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
  1. 类数组对象(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里面。

  1. 调用父构造函数实现继承
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()主要有以下两点

  1. call()改变了this的指向
  2. 函数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;
}