Analysis and Simulation of call and apply principle of JavaScript

Analysis and Simulation of call and apply principle of JavaScript

The difference between call() and apply() is that the call() method accepts a list of several parameters, while the apply() method accepts an array of multiple parameters

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

func.call(this, arg1, arg2); // Using call, parameter list
func.apply(this, [arg1, arg2]) // Using apply, parameter array

Common usage of call() and apply()

1. Merge array

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

// Merge the second array into the first
// Equivalent to A. push ('Orange ',' garde ');
Array.prototype.push.apply(a, b);
// 4

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

When the second array is too large, do not use this method to merge arrays, because there is a limit to the number of parameters a function can accept. Different engines have different restrictions. JS core is limited to 65535. Some engines will throw exceptions, some will not throw exceptions but lose extra parameters.

We can cut the parameter array into blocks and loop it to the target method. The code is as follows:

function concatOfArray(arr1, arr2) {
    var QUANTUM = 32768;
   // Use the circular cutting array to merge
    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;
}

// Verification
var arr1 = [-3, -2, -1];
var arr2 = [];
for(var i = 0; i < 1000000; i++) {
    arr2.push(i);
}

//Array.prototype.push.apply(arr1, arr2);
//Error: Uncaught RangeError: Maximum call stack size exceeded

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

2. Get the maximum and minimum values in the array

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

// Writing in ES6
Math.max.call(Math, ...numbers); // 458

The array numbers does not have the max method itself, so use the Math.max method with call / apply.

3. Verify whether it is an array

function isArray(obj){ 
    return Object.prototype.toString.call(obj) === '[object Array]';
}
isArray([1, 2, 3]);
// true

// Use toString() directly
[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"

You can get the type of each object through toString(), but different objects have different implementations of toString(), so to detect through Object.prototype.toString(), you need to call it in the form of call() / apply(), passing the object to be checked as the first parameter.

var toStr = Function.prototype.call.bind(Object.prototype.toString);
function isArray(obj){ 
    return toStr(obj) === '[object Array]';
}
isArray([1, 2, 3]);
// true

// Using modified toStr
toStr([1, 2, 3]); 	// "[object Array]"
toStr("123"); 		// "[object String]"
toStr(123); 		// "[object Number]"
toStr(Object(123)); // "[object Number]"

The above method first uses the Function.prototype.call function to specify a value of this, and then. bind returns a new function, always setting Object.prototype.toString as the incoming parameter. In fact, it is equivalent to Object.prototype.toString.call().

Note: if toString() method is not overridden

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

4. Call the parent constructor to implement inheritance

function  SuperType(){
    this.color=["red", "green", "blue"];
}
function  SubType(){
    // Core code, inherited from 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"]

In the child constructor, inheritance is implemented by calling the call method of the parent constructor.
Disadvantages:

  • Only instance properties and methods of the parent class can be inherited, not prototype properties / methods
  • Reuse cannot be realized. Each subclass has a copy of the parent instance function, which affects performance

Analog implementation of call

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

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

bar.call(foo); // 1

call() mainly has the following two points

  • call() changes the direction of this
  • Function bar executed

The first step of Simulation Implementation

If you add the function bar() to the foo() object when you call(), it is as follows

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

foo.bar(); // 1

The above code changes the direction of this and executes the function bar.
This method adds an additional attribute to foo
The solution is simple, just delete it.
Realize the following three steps to simulate the implementation.

  • Set the function to the object's property: foo.fn = bar
  • Execution function: foo.fn()
  • Delete function: delete foo.fn
// First edition
Function.prototype.call2 = function(context) {
    // First, get the function calling the call. You can get it with this
    context.fn = this; 		// foo.fn = bar
    context.fn();			// foo.fn()
    delete context.fn;		// delete foo.fn
}

// Test it.
var foo = {
    value: 1
};

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

bar.call2(foo); // 1

Simulation implementation step 2

The first version of the code function bar can't receive parameters. We can get parameters from arguments and take the second to last parameters and put them into the array.
The first array is discarded because the first parameter is this.

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

The parameter array is finished. The next thing to do is to execute the function context.fn().

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

The above direct call is definitely not allowed. args.join(',') will return a string and will not execute.

Here we use the eval method to implement and put together a function.

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

In the above code, args will automatically call the args.toString() method, because 'context.fn(' + args + ') is essentially string splicing, it will automatically call the toString() method, as follows:

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

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

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

The code of the second edition is as follows:

// The second edition
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;
}

// Test it.
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

The third step of Simulation Implementation

The following points need to be noted:

  • This parameter can be null or undefined. At this time, this points to window
  • this parameter can be used to transfer basic type data. The native call will be automatically converted with Object()
  • Functions can have return values

The code of the third edition is as follows

// The Third Edition
Function.prototype.call2 = function (context) {
    context = context ? Object(context) : window; // Implementation details 1 and 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; // Implementation details 2
}

// Test it.
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 and apply simulation implementation summary

Analog implementation of 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;
}
The simulation implementation of apply
Function.prototype.apply = function (context, arr) {
    context = context ? Object(context) : window; 
    context.fn = this;

    var result;
    // Determine whether the second parameter exists
    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;
}

Refer to https://juejin.im/post/5c060585e51d45480061b05f

Published 90 original articles, won praise 198, visited 10000+
Private letter follow

Tags: Javascript Attribute

Posted on Tue, 17 Mar 2020 02:26:32 -0400 by gareh