一步步手摸手带你用JavaScript手写call函数

前言:

在js引擎中,内置函数apply、call、bind是由c++实现的。

这里我们通过js来模拟实现,

主要练习函数、this、调用关系,不过度考虑一些边界情况。(edge case

前置知识:了解this的绑定规则(new绑定、显式绑定、隐式绑定、默认绑定)

摸索思路:

1、通过在Function.prototype(所有函数对象的原型对象)上定义新方法的方式让所有函数实例可以访问和调用新方法。

javascript 复制代码
Function.prototype.hycall = function () {
  console.log("调用了");
};
function foo() {
  console.log("foo", this);
}
function sum(n1, n2) {
  console.log("sum", n1,n2,this);
  return n1 + n2;
}
foo.call(); // foo window
console.log(foo.call()); // undefined
foo.call("abc"); // foo String {abc}
console.log(foo.call("abc")); // undefined
foo.call(123); // foo Number {123}
foo.call({ name: "zhangsan" }); // foo {name: "zhangsan"}
foo.call(null); // foo window
foo.call(undefined); // foo window
foo.call(true); // foo Boolean {true}
sum.call(); // sum undefined undefined window
sum.call(123, 1, 2); // sum 1 2 window
console.log(sum.call(123, 1, 2)); // 3
foo.hycall(); // 调用了
sum.hycall(); // 调用了

2、分析内置的call方法

通过代码我们发现call主要做了以下事情

1)执行调用它的函数(示例中的foo和sum函数)

2)可以改变this的指向,并且this最终是指向对象(第一个传入的参数会被绑定this

3)可以传入多个参数

4)最后有一个返回结果,回调函数的返回值或undefined

代码实现:

1、执行调用它的函数

对应第一点的实现,首先我们要拿到这个函数,通过之前对this绑定规则的学习我们可以知道,形如foo.hycall这样的调用方式符合this的隐式绑定规则,this指向foo,因此在hycall中通过this可以拿到我们的函数 (示例中的foo和sum函数,并且进行调用。对我们自己的hycall进行如下改造

javascript 复制代码
Function.prototype.hycall = function () {
  // 拿到函数
  var fn = this
  // 调用函数
  // 独立函数调用,符合this的默认绑定规则,this指向window
  fn() 
};
foo.hycall(); // foo window
sum.hycall(); // sum window

可以发现,foo.hycall()现在的执行结果和foo.call()一样,都是window

2、改变this的指向,并且this最终是指向对象(第一个传入的参数会被绑定this

对于要改变this的指向,

首先,同样的我们可以利用this的隐式绑定规则,例如xx.fn()这样的调用方式,改变this的指向为xx

javascript 复制代码
Function.prototype.hycall = function (thisArg) { // 参数任意取名,这里取为thisArg
  var fn = this
  thisArg.fn = fn
  // 隐式调用,this指向thisArg
  thisArg.fn()
};
foo.call({name:"aa"}) // foo {name: 'aa'} 
foo.hycall({name:"aa"}) // foo {name: 'aa', fn: ƒ}

此时执行foo.hycall({name:"aa"}),我们发现,打印的结果比call多了一个fn属性,因为我们通过thisArg.fn = fn使得thisArg多了一个fn属性。可以使用delete进行完善(浏览器打印可能看起来还有fn属性,显示问题,展开就没了)

javascript 复制代码
Function.prototype.hycall = function (thisArg) {
  var fn = this;
  thisArg.fn = fn;
  thisArg.fn();
  delete thisArg.fn; // 使用call打印的this没有fn这个属性,所以这里删除
};

至此,我们完成了做到了改变this的指向。

还有一点,我们知道,执行foo.call(参数),不论参数传的是null、undefined、字符串、数字、对象、Boolean甚至什么都不出传,最终this打印都是一个对象。this打印出来最终是指向的是一个对象

显然目前我们的hycall做不到这一点。

可以使用JavaScript内置的Object,通过Object(thisArg)的方式,将参数转化为对象

javascript 复制代码
Function.prototype.hycall = function (thisArg) {
  var fn = this;
  // 对thiArg进行类型转换(防止传入的是非对象类型,如数字、字符串),没传入则设置为window
  // thisArg = thisArg ? Object(thisArg) : window; // 这句代码在thisArg传入数字0会有问题
  thisArg = (thisArg !== null && thisArg !== undefined) ? Object(thisArg) : window;
  thisArg.fn = fn;
  thisArg.fn(); 
  delete thisArg.fn; 
};

至此,我们做到了改变this指向,并且指向的是一个对象。

3、可以传入多个参数

例如sum.call("abc", 2, 3),call是可以拿到后面两个参数并且传入sum函数的

现在我们hycall也要做到这一点,涉及到一个es6的知识点,剩余参数

javascript 复制代码
function test(...args) {
  console.log(args);
}
test() // []
test(1, 2, 3); // [1, 2, 3]
test(1, 2, 3, 4, 5, 6, 7, 8, 9); // [1, 2, 3, 4, 5, 6, 7, 8, 9]

...args这样的参数形式,称为剩余参数,函数里可以拿到所有传入的参数组成的一个数组。

那么我们的hycall可以完善为:

javascript 复制代码
Function.prototype.hycall = function (thisArg, ...args) {
  var fn = this;
  thisArg = (thisArg !== null && thisArg !== undefined) ? Object(thisArg) : window;
  thisArg.fn = fn;
  thisArg.fn(...args); // 这里...args的含义是把args里面的参数展开,...是展开运算符
  delete thisArg.fn;
};
sum.hycall("abc",3,8) // sum 3 8 String{"abc"}
console.log(sum.hycall("abc",3,8)) // undefined

4、返回结果

我们知道console.log(sum.hycall("abc",3,8))此时打印的结果是undefined,

而console.log(sum.call("abc", 3, 8))的结果是11

因此,我们只要把thisArg.fn(...args)的执行结果返回就行了。

javascript 复制代码
Function.prototype.hycall = function (thisArg, ...agrs) {
  var fn = this;
  thisArg = (thisArg !== null && thisArg !== undefined) ? Object(thisArg) : window;
  thisArg.fn = fn;
  var result = thisArg.fn(...agrs);
  return result;
}

至此我们使用JavaScript模拟实现了call。

完整注释版

javascript 复制代码
Function.prototype.hycall = function (thisArg, ...agrs) {
  // ...agrs是剩余参数
  // 1、拿到函数
  var fn = this;
  // 2、对thiArg进行类型转换(防止传入的是非对象类型,如数字、字符串),没传入则设置为window
  thisArg = (thisArg !== null && thisArg !== undefined) ? Object(thisArg) : window;
  // 3、将函数作为属性增加到thisArg上,同时通过隐式调用改变this指向
  thisArg.fn = fn;
  var result = thisArg.fn(...agrs); // 这里...args的含义是把agrs里面的参数展开,...是展开运算符
  delete thisArg.fn;
  // 4、返回结果
  return result;
};

拓展

这里抛出一个边界问题:

如果传入的thisArg本身有一个属性fn,要求不能被覆盖掉,该怎么修改代码?

相关推荐
兑生5 小时前
力扣面试150 快乐数 循环链表找环 链表抽象 哈希
leetcode·链表·面试
ThisIsClark9 小时前
【后端面试总结】mysql的group by怎么用
mysql·面试·职场和发展
Good Note14 小时前
Golang笔记——常用库context和runtime
开发语言·redis·笔记·后端·面试·golang·春招
四七伵18 小时前
127.0.0.1 与 localhost:需要知道的区别与用法
后端·面试
fly spider18 小时前
多线程-线程池的使用
java·面试·线程池·多线程·juc
言之。1 天前
【架构面试】三、高可用高性能架构设计
面试·职场和发展·架构
边城梦溪1 天前
《STL基础之vector、list、deque》
数据结构·c++·面试·stl
HEU_firejef2 天前
面试经典150题——图
面试·职场和发展
小菜鸟博士2 天前
手撕Diffusion系列 - 第九期 - 改进为Stable Diffusion(原理介绍)
人工智能·深度学习·学习·算法·面试
Dr.勿忘2 天前
C#常考随笔1:const和readonly有什么区别?
开发语言·前端·unity·面试·c#·游戏引擎