Encapsulate a myCall
function to realize call
the function of the function.
myCall
The function receives multiple parameters. The first parameter is fn
the function to be executed, the second parameter is the pointer context
that needs to be explicitly changed this
, and the subsequent parameters are fn
all parameters.
const person = {
userName: "zhangsan",
};
function fn() {
return this.userName;
}
myCall(fn, person); // 执行函数 fn,返回 'zhangsan'
// fn 传参数的情况
const obj = {
count: 10,
};
function fn(x, y, z) {
return this.count + x + y + z;
}
myCall(fn, obj, 1, 2, 3); // 执行函数 fn,返回 16
Solution Analysis
Before trying to implement call
the function, let's review its usage again. call
The function is to change this
the pointer. The code is as follows:
var userName = "xxx";
const person = {
userName: "zhangsan",
};
function fn() {
console.log(this.userName);
}
fn.call(); // 直接调用,this 指向 window,输出 'xxx'
fn.call(person); // 用 call,this 指向 person,输出 'zhangsan'
call
It is Function.prototype
the method written above, and what we want to achieve in this section myCall
is to pass the function as a parameter. The two are just different in the calling form, and the principle is the same.
Let's try to implement this
the function of explicitly changing the pointer, call the function in the object, and this
point to this object, so what we need to do is:
- Attach the function
fn
to the object to point tocontext
. - Execute
context.fn
, after executing the deletecontext
functionfn
, avoid polluting the properties of the incoming object.
The code is implemented as follows:
function myCall(fn, context) {
// 把函数 fn 挂载到对象 context 上。
context.fn = fn;
// 执行 context.fn
context.fn();
// 执行完了删除 context 上的 fn 函数,避免对传入对象的属性造成污染。
delete context.fn;
}
have a test:
var userName = "xxx";
const person = {
userName: "zhangsan",
};
function fn() {
return this.userName;
}
myCall(fn, person); // 输出 'zhangsan'
myCall(fn, window); // 输出 'xxx'
It can be seen that with only three lines of code, we have realized call
the core function of the function.
However, there are some other details that need to be dealt with, such as:
- To handle
context
the case of not passing a value, pass a default valuewindow
. fn
The parameters of the processing functionfn
are carried in when the function is executed.- Obtain
fn
the return value generated by the execution function, and finally return this return value.
The final implementation code is as follows:
// 要处理 context 不传值的情况,传一个默认值 window。
function myCall(fn, context = window) {
context.fn = fn;
// 处理函数 fn 的参数,执行 fn 函数时把参数携带进去。
const args = [...arguments].slice(2);
// 获取执行函数 fn 产生的返回值。
const res = context.fn(...args);
delete context.fn;
// 最终返回这个返回值
return res;
}
const obj = {
count: 10,
};
function fn(x, y, z) {
console.log(this.count + x + y + z);
}
myCall(fn, obj, 1, 2, 3); // 执行函数 fn,输出 16
In this way, we have realized call
the function that the function should have. The native call
function is Function.prototype
the method written above. We also try to implement a function on the prototype of the function myCall
. It only needs a little modification. The code is implemented as follows:
// 写到函数的原型上,就不需要把要执行的函数当作参数传递进去
Function.prototype.myCall = function (context = window) {
// 这里的 this 就是这个要执行的函数
context.fn = this;
// 参数少了一个,slice(2) 改为 slice(1)
const args = [...arguments].slice(1);
const res = context.fn(...args);
delete context.fn;
return res;
};
Handle edge cases
The function implemented on the function prototype above myCall
still has room for optimization. There are some edge cases that may cause errors, such as pointing the object to point to a primitive value. The code is as follows:
fn.myCall(0); // Uncaught TypeError: context.fn is not a function
At this time, you need to refer to how the original call
function solves this problem. Let's print it out and have a look:
var userName = "xxx";
const person = {
userName: "zhangsan",
};
function fn(type) {
console.log(type, "->", this.userName);
}
fn.call(0, "number");
fn.call(1n, "bigint");
fn.call(false, "boolean");
fn.call("123", "string");
fn.call(undefined, "undefined");
fn.call(null, "null");
const a = Symbol("a");
fn.call(a, "symbol");
fn.call([], "引用类型");
As you can see, undefined
and null
pointed to window
, both the original type and the reference type undefined
.
In fact, it is because the original type points to the corresponding packaging type, and the reference type points to this reference type. The reason why the output values are all is because there are no attributes undefined
on these objects .userName
Modify our myCall
function to achieve compatibility with the original type, the code is as follows:
Function.prototype.myCall = function (context = window) {
if (context === null || context === undefined) {
context = window; // undefined 和 null 指向 window
} else {
context = Object(context); // 原始类型就包装一下
}
context.fn = this;
const args = [...arguments].slice(1);
const res = context.fn(...args);
delete context.fn;
return res;
};
There is another edge case, assuming that there is an fn
attribute on the object, and the following call is executed, the attribute on the object fn
will be deleted, the code is as follows:
const person = {
userName: "zhangsan",
fn: 123,
};
function fn() {
console.log(this.userName);
}
fn.myCall(person);
console.log(person.fn); // 输出 undefined,本来应该输出 123
Because the original attribute on the object has the same name fn
as myCall
the temporarily defined attribute inside the function .fn
Do you still remember Symbol
the function? It can Symbol
be used to prevent object attribute name conflicts. Continue to modify myCall
the function. The code is implemented as follows:
Function.prototype.myCall = function (context = window) {
if (context === null || context === undefined) {
context = window;
} else {
context = Object(context);
}
const fn = Symbol("fn"); // 用 symbol 处理一下
context[fn] = this;
const args = [...arguments].slice(1);
const res = context[fn](...args);
delete context[fn];
return res;
};
call usage scenario
call
There are many usage scenarios, and all the usage call
scenarios of calls are to explicitly change this
the pointer, and call
the problems that can be solved can also be apply
solved by using it, because they are only different in the form of parameter passing. Let's take a look at call
the four commonly used usage scenarios.
1. Accurately judge a data type
It can be used to accurately judge the type of a data Object.prototype.toString.call(xxx)
.
Call this method to uniformly return a formatted [object Xxx]
string to represent the object.
// 引用类型
console.log(Object.prototype.toString.call({})); // '[object Object]'
console.log(Object.prototype.toString.call(function () {})); // "[object Function]'
console.log(Object.prototype.toString.call(/123/g)); // '[object RegExp]'
console.log(Object.prototype.toString.call(new Date())); // '[object Date]'
console.log(Object.prototype.toString.call(new Error())); // '[object Error]'
console.log(Object.prototype.toString.call([])); // '[object Array]'
console.log(Object.prototype.toString.call(new Map())); // '[object Map]'
console.log(Object.prototype.toString.call(new Set())); // '[object Set]'
console.log(Object.prototype.toString.call(new WeakMap())); // '[object WeakMap]'
console.log(Object.prototype.toString.call(new WeakSet())); // '[object WeakSet]'
// 原始类型
console.log(Object.prototype.toString.call(1)); // '[object Number]'
console.log(Object.prototype.toString.call("abc")); // '[object String]'
console.log(Object.prototype.toString.call(true)); // '[object Boolean]'
console.log(Object.prototype.toString.call(1n)); // '[object BigInt]'
console.log(Object.prototype.toString.call(null)); // '[object Null]'
console.log(Object.prototype.toString.call(undefined)); // '[object Undefined]'
console.log(Object.prototype.toString.call(Symbol("a"))); // '[object Symbol]'
The need to call here call
is to explicitly change this
the pointer to our target variable.
If we do not change this
the pointer to our target variable xxx
, this
it will always point to the caller Object.prototype
, that is, the prototype object. If we call toString
the method on the prototype object, the result will always be the same [object Object]
, as shown in the following code:
2. Pseudo-array to array
Pseudo-array to array can be used before es6 Array.prototype.slice.call(xxx)
.
function add() {
const args = Array.prototype.slice.call(arguments);
// 也可以这么写 const args = [].slice.call(arguments)
args.push(1); // 可以使用数组上的方法了
}
add(1, 2, 3);
The principle is the same as accurately judging a data type. If you do not change this
the pointer to the target pseudo-array, this
it will always point to the called one Array.prototype
, and it will not take effect.
// 从 slice 方法原理理解为什么要调用 call
Array.prototype.slice = function (start, end) {
const res = [];
start = start || 0;
end = end || this.length;
for (let i = start; i < end; i++) {
res.push(this[i]); // 这里的 this 就是伪数组,所以要调用 call
}
return res;
};
3. ES5 implements inheritance
In a child constructor, you can call
implement inheritance by calling methods of the parent constructor.
function Person(name) {
this.name = name;
}
function Student(name, grade) {
Person.call(this, name);
this.grade = grade;
}
const p1 = new Person("zhangsan");
const s1 = new Student("zhangsan", 100);
In the above code example, the constructor will have the properties in Student
the constructor , and the properties are its own.Person
name
grade
Student
If the code here is replaced by ES6, it is equivalent to the following code:
class Person {
constructor(name) {
this.name = name;
}
}
class Student extends Person {
constructor(name, grade) {
super(name);
this.grade = grade;
}
}
const p1 = new Person("zhangsan");
const s1 = new Student("zhangsan", 100);
Regarding inheritance, it is enough to master the implementation of ES6, and it is enough to understand the ES5, because now everyone basically uses the ES6 way of writing.
4. Handle the callback function this loss problem
Execute the following code, the callback function will cause this
loss.
const obj = {
userName: "zhangsan",
sayName() {
console.log(this.userName);
},
};
obj.sayName(); // 输出 'zhangsan'
function fn(callback) {
if (typeof callback === "function") {
callback();
}
}
fn(obj.sayName); // 输出 undefined
The reason for this phenomenon is that when the callback function is executed, this
the pointer is already there window
, so it is output undefined
.
You can use to call
change this
the pointing, the code is as follows:
const obj = {
userName: "zhangsan",
sayName() {
console.log(this.userName);
},
};
obj.sayName(); // 输出 'zhangsan'
function fn(callback, context) {
// 定义一个 context 参数,可以把上下文传进去
if (typeof callback === "function") {
callback.call(context); // 显式改变 this 值,指向传入的 context
}
}
fn(obj.sayName, obj); // 输出 'zhangsan'