JavaScript之call和apply原理解析及模拟实现

JavaScript之call和apply原理解析及模拟实现

call() 和 apply()的区别在于,call()方法接受的是若干个参数的列表,而apply()方法接受的是一个包含多个参数的数组

var func = function(arg1, arg2) {
     ...
};

func.call(this, arg1, arg2); // 使用 call,参数列表
func.apply(this, [arg1, arg2]) // 使用 apply,参数数组

call() 和 apply()的常用用法

1.合并数组

var a = ['apple', 'banana'];
var b = ['orange', 'garde'];

// 将第二个数组融合进第一个数组
// 相当于 a.push('orange', 'garde');
Array.prototype.push.apply(a, b);
// 4

vegetables;
// ['apple', 'banana', 'orange', 'garde']

当第二个数组太大时,不要使用这个方法来合并数组,因为一个函数能够接受的参数个数是有限制的。不同的引擎有不同的限制,JS核心限制在 65535,有些引擎会抛出异常,有些不抛出异常但丢失多余参数。

我们可以用将参数数组切块后循环传入目标方法,代码如下:

function concatOfArray(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

concatOfArray(arr1, arr2);
// 结果:(1000003) [-3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]

2、获取数组中的最大值,最小值

var numbers = [5, 458 , 120 , -215 ]; 
Math.max.apply(Math, numbers);   //458    
Math.max.call(Math, 5, 458 , 120 , -215); //458

// ES6中写法
Math.max.call(Math, ...numbers); // 458

数组 numbers 本身没有 max方法,这里借助 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() 的形式来调用,传递要检查的对象作为第一个参数。

var 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(Object(123)); // "[object Number]"

上面方法首先使用 Function.prototype.call函数指定一个 this 值,然后 .bind 返回一个新的函数,始终将 Object.prototype.toString 设置为传入参数。其实等价于 Object.prototype.toString.call()

注意:前提是toString()方法没有被覆盖

Object.prototype.toString = function() {
    return '';
}
isArray([1, 2, 3]);
// false

4.调用父构造函数实现继承

function  SuperType(){
    this.color=["red", "green", "blue"];
}
function  SubType(){
    // 核心代码,继承自SuperType
    SuperType.call(this);
}

var instance1 = new SubType();
instance1.color.push("black");
console.log(instance1.color);
// ["red", "green", "blue", "black"]

var instance2 = new SubType();
console.log(instance2.color);
// ["red", "green", "blue"]

在子构造函数中,通过调用父构造函数的call方法来实现继承。
缺点:

  • 只能继承父类的实例属性和方法,不能继承原型属性/方法
  • 无法实现复用,每个子类都有父类实例函数的副本,影响性能

call的模拟实现

var value = 1;
var foo = {
    value: 1
};

function bar() {
    console.log(this.value);
}

bar.call(foo); // 1

call()主要有以下两点

  • call()改变了this的指向
  • 函数 bar 执行了

模拟实现第一步

如果在调用call()的时候把函数 bar()添加到foo()对象中,即如下

var foo = {
    value: 1,
    bar: function() {
        console.log(this.value);
    }
};

foo.bar(); // 1

上面的代码改变了this的指向并且执行了函数bar。
这种方法给foo额外添加了一个属性
解决方法很简单,用 delete 删掉就好了。
实现下面3步就可以模拟实现了。

  • 将函数设置为对象的属性:foo.fn = bar
  • 执行函数:foo.fn()
  • 删除函数:delete foo.fn
// 第一版
Function.prototype.call2 = function(context) {
    // 首先要获取调用call的函数,用this可以获取
    context.fn = this; 		// foo.fn = bar
    context.fn();			// foo.fn()
    delete context.fn;		// delete foo.fn
}

// 测试一下
var foo = {
    value: 1
};

function bar() {
    console.log(this.value);
}

bar.call2(foo); // 1

模拟实现第二步

第一版代码函数 bar 不能接收参数,我们可以从 arguments中获取参数,取出第二个到最后一个参数放到数组中。
抛弃第一个数组是因为第一个参数是 this。

//第二版
var args = [];
for(var i = 1, len = arguments.length; i < len; i++) {
    args.push('arguments[' + i + ']');
}

参数数组搞定了,接下来要做的就是执行函数 context.fn()。

context.fn( args.join(',') ); 

上面直接调用肯定不行,args.join(’,’)会返回一个字符串,并不会执行。

这边采用 eval方法来实现,拼成一个函数。

eval('context.fn(' + args +')')

上面代码中args 会自动调用 args.toString() 方法,因为'context.fn(' + args +')'本质上是字符串拼接,会自动调用toString()方法,如下代码:

var args = ["a1", "b2", "c3"];
console.log(args);
// ["a1", "b2", "c3"]

console.log(args.toString());
// a1,b2,c3

console.log("" + args);
// a1,b2,c3

第二版的代码如下:

// 第二版
Function.prototype.call2 = function(context) {
    context.fn = this;
    var args = [];
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    }
    eval('context.fn(' + args +')');
    delete context.fn;
}

// 测试一下
var foo = {
    value: 1
};

function bar(name, age) {
    console.log(name)
    console.log(age)
    console.log(this.value);
}

bar.call2(foo, 'kevin', 18); 
// kevin
// 18
// 1

模拟实现第三步

还需要注意的几点如下:

  • this 参数可以传 null 或者 undefined,此时 this 指向 window
  • this 参数可以传基本类型数据,原生的 call 会自动用 Object() 转换
  • 函数是可以有返回值的

第三版代码如下

// 第三版
Function.prototype.call2 = function (context) {
    context = context ? Object(context) : window; // 实现细节 1 和 2
    context.fn = this;

    var args = [];
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    }

    var result = eval('context.fn(' + args +')');

    delete context.fn
    return result; // 实现细节 2
}

// 测试一下
var value = 2;

var obj = {
    value: 1
}

function bar(name, age) {
    console.log(this.value);
    return {
        value: this.value,
        name: name,
        age: age
    }
}

function foo() {
    console.log(this);
}

bar.call2(null); // 2
foo.call2(123); // Number {123, fn: ƒ}

bar.call2(obj, 'kevin', 18);
// 1
// {
//    value: 1,
//    name: 'kevin',
//    age: 18
// }

call和apply模拟实现汇总

call的模拟实现
Function.prototype.call = function (context) {
    context = context ? Object(context) : window; 
    context.fn = this;

    var args = [];
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    }
    var result = eval('context.fn(' + args +')');

    delete context.fn
    return result;
}

ES6:

Function.prototype.call = function (context) {
  context = context ? Object(context) : window; 
  context.fn = this;

  let args = [...arguments].slice(1);
  let result = context.fn(...args);

  delete context.fn
  return result;
}
apply的模拟实现
Function.prototype.apply = function (context, arr) {
    context = context ? Object(context) : window; 
    context.fn = this;

    var result;
    // 判断是否存在第二个参数
    if (!arr) {
        result = context.fn();
    } else {
        var args = [];
        for (var i = 0, len = arr.length; i < len; i++) {
            args.push('arr[' + i + ']');
        }
        result = eval('context.fn(' + args + ')');
    }

    delete context.fn
    return result;
}

ES6:

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;
}

参考于https://juejin.im/post/5c060585e51d45480061b05f

发布了102 篇原创文章 · 获赞 252 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/low666/article/details/104919657