手写call方法改变this指向

本文介绍了如何封装一个名为myCall的函数,模拟JavaScript的call功能,包括处理this的指向、函数参数以及各种边缘情况。文章还探讨了call的使用场景,如精准判断数据类型、伪数组转数组、ES5实现继承和处理回调函数中的this丢失问题。最后,展示了如何在函数原型上添加myCall方法,并处理原始类型的兼容性问题。
摘要由CSDN通过智能技术生成

封装一个 myCall 函数,用来实现 call 函数的功能。

myCall 函数接收多个参数,第一个参数是 fn,代表要执行的函数,第二个参数是 context,代表需要显式改变的 this 指向,之后的参数都是 fn 的参数。

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

题解分析

在尝试去实现 call 函数之前,我们再来回顾一下它的用法,call 函数是为了改变 this 的指向,代码如下:

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 是写到 Function.prototype 上的方法,而本节我们要实现的 myCall 是把函数当作参数传递进去,两者只是调用形式不同,原理都是一样的。

我们尝试来实现一下显式改变 this 指向的功能,调用对象中的函数,this 指向为这个对象,所以我们需要做的操作是:

  • 把函数 fn 挂载到要指向的对象 context 上。
  • 执行 context.fn,执行完了删除 context 上的 fn 函数,避免对传入对象的属性造成污染。

代码实现如下:

function myCall(fn, context) {
  // 把函数 fn 挂载到对象 context 上。
  context.fn = fn;

  // 执行 context.fn
  context.fn();

  // 执行完了删除 context 上的 fn 函数,避免对传入对象的属性造成污染。
  delete context.fn;
}

测试一下:

var userName = "xxx";
const person = {
  userName: "zhangsan",
};

function fn() {
  return this.userName;
}

myCall(fn, person); // 输出 'zhangsan'
myCall(fn, window); // 输出 'xxx'

可以看到,仅仅三行代码,我们就实现了 call 函数的核心功能。

不过这里面有一些其他细节需要处理,比如:

  • 要处理 context 不传值的情况,传一个默认值 window
  • 处理函数 fn 的参数,执行 fn 函数时把参数携带进去。
  • 获取执行函数 fn 产生的返回值,最终返回这个返回值。

最终实现代码如下:

// 要处理 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

这样我们就实现了 call 函数该有的功能,原生的 call 函数是写到 Function.prototype 上的方法,我们也尝试在函数的原型上实现一个 myCall 函数,只需稍加改造即可,代码实现如下:

// 写到函数的原型上,就不需要把要执行的函数当作参数传递进去
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;
};

处理边缘情况

上文在函数原型上实现的 myCall 函数,还有优化的空间,有一些边缘的情况,可能会导致报错,比如把要指向的对象指向一个原始值,代码如下:

​fn.myCall(0); // Uncaught TypeError: context.fn is not a function

​

这时,就需要参考一下原生的 call 函数是如何解决的这个问题,我们打印出来看一下:

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([], "引用类型");

可以看到,undefined 和 null 指向了 window,原始类型和引用类型都是 undefined

其实是因为,原始类型指向对应的包装类型,引用类型就指向这个引用类型,之所以输出值都是 undefined,是因为这些对象上都没有 userName 属性。

改造一下我们的 myCall 函数,实现原始类型的兼容,代码如下:

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

还有另外一种边缘情况,假设对象上本来就有一个 fn 属性,执行下面的调用,对象上的 fn 属性会被删除,代码如下:

const person = {
  userName: "zhangsan",
  fn: 123,
};

function fn() {
  console.log(this.userName);
}

fn.myCall(person);

console.log(person.fn); // 输出 undefined,本来应该输出 123

因为对象上本来的 fn 属性和 myCall 函数内部临时定义的 fn 属性重名了。

还记得 Symbol 的作用吗,可以用 Symbol 来防止对象属性名冲突问题,继续改造 myCall 函数,代码实现如下:

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 使用场景

call 的使用场景非常多,所有调用 call 的使用场景都是为了显式地改变 this 的指向,能用 call 解决的问题也能用 apply 解决,因为它们俩只是传参形式不同。下面一起来看 call 常用的四个使用场景。

1.精准判断一个数据类型

精准地判断一个数据的类型,可以用到 Object.prototype.toString.call(xxx)

调用该方法,统一返回格式 [object Xxx] 的字符串,用来表示该对象。

// 引用类型
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]'

这里需要调用 call 就是为了显式地改变 this 指向为我们的目标变量。

如果不改变 this 指向为我们的目标变量 xxxthis 将永远指向调用的 Object.prototype,也就是原型对象,对原型对象调用 toString 方法,结果永远都是 [object Object],如下代码所示:

2.伪数组转数组

伪数组转数组,在 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);

原理同精准判断一个数据类型相同,如果不改变 this 指向为目标伪数组,this 将永远指向调用的 Array.prototype,就不会生效。

// 从 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 实现继承

在一个子构造函数中,你可以通过调用父构造函数的 call 方法来实现继承。

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);

上面的代码示例中,构造函数 Student 中会拥有构造函数 Person 中的 name 属性,grade 属性是 Student 自己的。

这里的代码如果换成 ES6 的,就等价于下面的代码:

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);

关于继承,掌握 ES6 的实现方式就好,ES5 的做了解即可,因为现在大家基本上都用 ES6 的写法了。

4.处理回调函数 this 丢失问题

执行下面的代码,回调函数会导致 this 丢失。

const obj = {
  userName: "zhangsan",
  sayName() {
    console.log(this.userName);
  },
};

obj.sayName(); // 输出 'zhangsan'

function fn(callback) {
  if (typeof callback === "function") {
    callback();
  }
}

fn(obj.sayName); // 输出 undefined

导致这样现象的原因是回调函数执行的时候 this 指向已经是 window 了,所以输出 undefined

可以使用 call 改变 this 指向,代码如下:

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'
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Leviash

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值