Learning Javascript to simulate the implementation of call,apply

Keywords: Front-end Javascript less

Preface

This article is 1630 words, it takes about 8 minutes to read.

All in all: in this paper, we simulate and implement a relatively perfect call and apply method from scratch by raising problems and then solving them

text

Introduction to call and apply

First, I will introduce two methods, call and apply, which are both mounted on the function prototype, so all functions can call these two methods.

Note: the function of call() method is similar to that of apply(). The difference is that call() method accepts parameter list, while apply() method accepts an array of parameters.

Example:

function foo(b = 0) {
	console.log(this.a + b);
}
const obj1 = {
	a: 1
};
const obj2 = {
	a: 2
};
foo.call(obj1, 1); // 2
foo.call(obj2, 2); // 4
foo.apply(obj1, [1]); // 2
foo.apply(obj2, [2]); // 4

For students unfamiliar with this, you can asynchronize first: Understand this in Javascript . To sum up, this of Javascript functions points to the caller, and whoever calls this points to the caller. If no one calls this function, it points to undefined in strict mode and window s in non strict mode.

So call and apply are essentially used to change this value of the called function. As mentioned above, call and apply are only different in parameters. If call is implemented by simulation, then apply is only the difference in parameter processing. In other words, call and apply do two things:

  1. Change this value of the called function;
  2. Transfer parameter call;

###Change this

Now the problem of analog implementation of call and apply is transferred to another problem, that is, how to change this value of a function. It is very simple:

function foo(b = 0) {
	console.log(this.a + b);
}
const obj1 = {
	a: 1,
  foo: foo
};
const obj2 = {
	a: 2,
  foo: foo
};
obj1.foo(1);
obj2.foo(2);

That is to say, we assign this method to the object, and then the object calls this function. It's very simple to change this step of a function. First, assign this function to the object this points to, then the object calls this function, and delete the function from the object after execution. The steps are as follows:

obj.foo = foo;
obj.foo();
delete obj.foo;

With ideas, we can implement the first version of call method:

Function.prototype.call2 = function(context) {
  context = context || {};
  context[this.name] = this;
  context[this.name]();
  delete context[this.name];
}

this.name is the name of the function declaration, but it is not necessary to correspond to the function name. We can use any key:

Function.prototype.call2 = function(context) {
  context = context || {};
  context.func = this;
  context.func();
  delete context.func;
}

Use the new call to call the above function:

foo.call2(obj1); // 1
foo.call2(obj2); // 2

OK, the problem of this is solved. Next is the problem of parameter transfer:

Biography

The arguments in the function are stored in a class array object, arguments. So we can get the parameters from arguments to call2:

Function.prototype.call2 = function(context) {
  context = context || {};
  var params = [];
 	for (var i = 1; i < arguments.length; i++) {
    params[i - 1] = arguments[i];
	}
  context.func = this;
  context.func();
  delete context.func;
}

At this point, the question arises. How can params be passed to func? The easy way to think of is to use the extended operator of ES6:

Function.prototype.call2 = function(context) {
  context = context || {};
  var params = [];
 	for (var i = 1; i < arguments.length; i++) {
    params[i - 1] = arguments[i];
	}
  context.func = this;
  context.func(...params);
  delete context.func;
}

Look at our example:

foo.call2(obj1, 1); // 2
foo.call2(obj2, 2); // 4

Another implementation is to use the less commonly used Eval function, that is, we splice the parameters into a string and pass it to the eval function for execution,

The eval() function evaluates a string and executes the JavaScript code in it.

Take a look at our second implementation:

Function.prototype.call2 = function(context) {
  context = context || {};
  var params = [];
 	for (var i = 1; i < arguments.length; i++) {
    params[i - 1] = arguments[i];
	}
  // Note that this here refers to the called function
  context.func = this;
  eval('context.func(' + params.join(",") + ')');
  delete context.func;
}

Other

call and apply also have two other important features. They can return function execution results normally. When accepting null or undefined as parameters, point this to window. Then we can implement these two features and add necessary judgment prompts. This is our third version implementation:

Function.prototype.call2 = function(context) {
  context = context || window;
  var params = [];
  // i is initialized to 1 here to skip the context parameter
 	for (var i = 1; i < arguments.length; i++) {
    params[i - 1] = arguments[i];
	}
  // Note that this here refers to the called function
  context.func = this;
  var res = eval('context.func(' + params.join(",") + ')');
  delete context.func;
  return res;
}

Then we call the following test:

foo.call2(obj1, 1); // 2

foo.call(2, 1); // NaN
foo.call2(2, 1); // context.func is not a function

As we found above, after changing the object to the number 2, the original call returned NaN, but our call 2 reported an error, indicating that there is a problem with our direct context = context || window. There is also an internal type judgment. After solving this problem, our fourth version is implemented as follows:

Function.prototype.call2 = function(context) {  
  if (context === null || context === undefined) {
		context = window;
  } else {
		context = Object(context) || context;
  }
  var params = [];
  // i is initialized to 1 here to skip the context parameter
 	for (var i = 1; i < arguments.length; i++) {
    params[i - 1] = arguments[i];
	}
  // Note that this here refers to the called function
  context.func = this;
  var res = eval('context.func(' + params.join(",") + ')');
  delete context.func;
  return res;
}

This is our final code, which can be compatible from ES3 to ES6. At this time:

foo.call(2, 1); // NaN
foo.call2(2, 1); // NaN

Simulate the implementation of apply

apply and call are only parameter differences. Rewrite call2:

Function.prototype.apply2 = function(context, arr) {
  if (context === null || context === undefined) {
		context = window;
  } else {
		context = Object(context) || context;
  }
  // Note that this here refers to the called function
  context.func = this;
  arr =  arr || [];
  var res = eval('context.func(' + arr.join(",") + ')');
  delete context.func;
  return res;
}

The above is our final implementation. At present, another problem is the problem of context.func, so that the context we passed in can't use func string as method name.

conclusion

Our implementation process has solved the following problems:

  1. Change this of the called function;
  2. Pass the parameter to the called function;
  3. Return the result of the called function. When the first parameter is null or undefined, this of the called function points to window;
  4. Solve the problem of type judgment;

Above.

Limited ability, general level, welcome to errata, thank you very much.

Subscribe to more articles, official account, advanced learning, reply to "666", and get a package of front-end technology books.

Posted by tron00 on Sun, 01 Mar 2020 04:14:47 -0800