DEV Community

satish-rajnale
satish-rajnale

Posted on

Question 4: In-depth analysis of call and apply principles, usage scenarios and implementation

Call() and apply()

The call() method calls a function that has a specified this Value and separately supplied parameters (List of parameters)。

 //The difference is thatcall()The method accepted isa list of several parameters,andapply()The method accepted isAn array of multiple parameters

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

func.call(this, arg1, arg2); / / Use call, parameter list
func.apply(this, [arg1, arg2]) / / Use apply, parameter array
Enter fullscreen mode Exit fullscreen mode

Scenes to be used:

Here are some common usages

1. merge two arrays
 var vegetables = ['parsnip', 'potato'];
var moreVegs = ['celery', 'beetroot'];

// merge the second array into the first array
// is equivalent to vegetables.push('celery', 'beetroot');
Array.prototype.push.apply(vegetables, moreVegs);
// 4

vegetables;
// ['parsnip', 'potato', 'celery', 'beetroot']
Enter fullscreen mode Exit fullscreen mode

When the second array (as in the example moreVegs) Don't use this method to merge arrays when they are too large, becauseThe number of parameters a function can accept is limited. Different engines have different limits. The JS core is limited to 65535. Some engines throw exceptions, some don't throw exceptions but lose redundant parameters.

How to solve it? The method isLoop the parameter array into a target method after it is diced
 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;
}

// verification code
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, ...]
Enter fullscreen mode Exit fullscreen mode
2. merge two arrays
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
Enter fullscreen mode Exit fullscreen mode
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"
Enter fullscreen mode Exit fullscreen mode

Another verify Array Metthod

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

// Use the modified toStr
toStr([1, 2, 3]);   // "[object Array]"
toStr("123");       // "[object String]"
toStr(123);         // "[object Number]"
toStr(Object(123)); // "[object Number]"
Enter fullscreen mode Exit fullscreen mode
  • In the above method first Function.prototype.call(Function) specifies a this Value, then .bind Return a new function that will always be Object.prototype.toString Set to pass in parameters. Actually equivalent to Object.prototype.toString.call()
4. the class array object (Array-like Object) using the array method
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 data is different in different environments
// (505) ["h1", html.gr__hujiang_com, head, meta, ...] 
Copy code
Enter fullscreen mode Exit fullscreen mode

Class array objects have the following two properties

  1. with: a numeric index subscript to the object element and length Attributes
  2. does not have: pushshiftforEach as well as indexOf Methods such as array objects

To be explained, the class array object is a Object. There is a name in JS Class array-Object structure, such as arguments Objects, as well as returned by the DOM API NodeList Objects belong to class array objects, class array objects cannot be used push/pop/shift/unshift Array method, pass Array.prototype.slice.call to convert to a real array and you can use the methods.

Class array object to arrayOther methods:
// The above code is equivalent to
var arr = [].slice.call(arguments)

ES6:
let arr = Array.from(arguments);
let arr = [...arguments];
Copy code
Enter fullscreen mode Exit fullscreen mode

Array.from() You can turn two types of objects into real arrays: Class array Object and Traversable(iterable) objects (including ES6's new data structures Set and Map).

Question : Why do you have class array objects? Or why the class array object is what solves the problem?

JavaScript typed arrays are an array-like Object And provides a mechanism for accessing raw binary data. Array-stored objects can be dynamically increased and decreased, and any JavaScript value can be stored. The JavaScript engine does some internal optimizations so that operations on arrays can be fast. However, as web applications become more powerful, especially with new additions such as audio and video editing, access to raw data from WebSockets, etc., it's clear that sometimes using JavaScript code can quickly and easily pass typed arrays. It would be very helpful to manipulate the raw binary data.

In a word, you can manipulate complex data faster.

5. call the parent constructor to achieve 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"]
Copy code
Enter fullscreen mode Exit fullscreen mode

Disadvantages: add a throttle valve to the operation function.

  • Can only inherit the parent class Instance Properties and methods, cannot inherit prototype properties/methods
  • Unable to implement reuse, each subclass has a copy of the parent class instance function, affecting performance
Call simulation implementation

The following content refers to JavaScript deep call and apply simulation implementation

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

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

bar.call(foo); // 1
Copy code
Enter fullscreen mode Exit fullscreen mode

Through the above introduction we know that call() There are two main points

  1. call() Changed the direction of this
  2. function bar Executed
Simulation implementation first step
var foo = {
    value: 1,
    bar: function() {
        console.log(this.value);
    }
};

foo.bar(); // 1
Copy code
Enter fullscreen mode Exit fullscreen mode

This change can be implemented: change the pointer to this and execute the function bar. But writing this has side effects, that is, giving foo Added an additional attribute, how to solve it?
The solution is simple, use delete Just delete it.

So as long as the following 3 steps are implemented, the simulation can be implemented.

  1. Set the function to the properties of the object: foo.fn = bar
  2. the execution function: foo.fn()
  3. delete the function: delete foo.fn
  4. The code is implemented as follows:
// first edition
Function.prototype.call2 = function(context) {
    // First get the function that calls call, use this to get
    context.fn = this;      // foo.fn = bar
    context.fn();           // foo.fn()
    delete context.fn;      // delete foo.fn
}

// have a test
var foo = {
    value: 1
};

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

bar.call2(foo); // 1
Copy code
Enter fullscreen mode Exit fullscreen mode

perfect!

Simulation implementation of the second step

The first version has a problem, that is the function bar Can't receive parameters, so we can get from arguments Get the parameters, take the second to the last parameter and put it in the array, why should we discard the first parameter, because the first parameter is this

The method of converting an array object into an array has already been introduced above, but this is done using the ES3 scheme.

Anti shake idea:

  • I first set a delay time (timer), in this time, if you operate five times,
  • I will clear the previous four operations (clear timer triggered function), do not let the previous four operations to execute.
  • When the delay time is up, you can perform your fifth operation.

var args = [];
for(var i = 1, len = arguments.length; i < len; i++) {
    args.push('arguments[' + i + ']');
}
Copy code
Enter fullscreen mode Exit fullscreen mode

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

context.fn( args.join(',') ); // That does not work
Copy code
Enter fullscreen mode Exit fullscreen mode

The above direct call will definitely not work,args.join(',')Will return a string and will not execute.
Adopted here eval The method is implemented to form a function.

eval('context.fn(' + args +')')
Copy code
Enter fullscreen mode Exit fullscreen mode

In the above code args Will be called automatically args.toString() Method because'context.fn(' + args +')' Essentially string concatenation, it will be called automatically toString() Method, the following code:

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

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

console.log("" + args);
// a1,b2,c3
Copy code
Enter fullscreen mode Exit fullscreen mode

So the second version is implemented, the code is as follows:

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

// have a test
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
Copy code
Enter fullscreen mode Exit fullscreen mode
Simulation implementation of the third step

There are 2 more details to note:

  1. this parameter can be passednull OrundefinedAt this time this points to the window
  2. this parameter can pass basic type data, the original call will automatically use Object () conversion
  3. the function can have a return value
// 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
}

// have a test
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
// }
Copy code
Enter fullscreen mode Exit fullscreen mode

Call and apply simulation implementation summary

Call simulation implementation

ES3:

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;
}
Copy code
Enter fullscreen mode Exit fullscreen mode

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;
}
Copy code
Enter fullscreen mode Exit fullscreen mode
Analog implementation of apply

ES3

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

    var result;
    / / Determine whether there is a second parameter
    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;
}
Copy code
Enter fullscreen mode Exit fullscreen mode

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;
}
Copy code
Enter fullscreen mode Exit fullscreen mode

Top comments (0)