The handwritten call method changes the direction of this

Encapsulate a  myCall function to realize  call the function of the function.

myCall The function receives multiple parameters. The first parameter is  fnthe function to be executed, the second parameter is  the pointer contextthat 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 to  context .
  • Execute  context.fn, after executing the delete  context function  fn , 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 value  window.
  • fn The parameters of  the processing function  fn 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 undefinedon 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.PersonnamegradeStudent

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'

Guess you like

Origin blog.csdn.net/qq_51588894/article/details/130010141