(十五) call()方法及其实现

1. call()方法

MDN给出的解释为: call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。

function.call(thisArg, arg1, arg2, ...) , 其接受的参数如下:

  • 第一个参数是 thisArg, 即:在 function 函数运行时使用的 this
  • arg1, arg2, ... 为指定的参数列表

也就是: 让 function执行, 让function中的this指向thisArg

示例

var obj = {
  name: '猫',
  age: 13,
}

function showInfo() {
  console.log('我的个人信息是: ', this.name + this.age);
}

showInfo()		// 我的个人信息是: undefined
showInfo.call(obj)		// 我的个人信息是: 猫13

个人对于call()方法的理解是: 它将showInfo(也就是函数)暂时的作为了obj的属性

showInfo.call(obj)	
// 就相当于
var obj = {
  name: '猫',
  age: 13,
  showInfo: function(){
  	console.log('我的个人信息是: ', this.name + this.age);
  }
}

obj.showInfo()
//只不过这个过程是一次性的

2. 利用原生js模拟call()方法

具体的思路就是: 根据上面对于call()方法的理解

  • 将function作为obj的属性
  • 让obj调用这个函数
  • 调用完将这个属性删除
  • 除了这些, 我们还应该考虑到像函数的返回值, 参数列表, thisArg的类型等问题
// 定义在Function的原型上
Function.prototype.myCall = function (context) {
  // 1. 将function作为obj的属性
  context.fn = this
  // 2. 让obj调用这个函数
  context.fn()
  // 3. 调用完删除这个属性
  delete context.fn
}

var obj = {
  name: '猫',
  age: 13,
}

function showInfo() {
  console.log('我的个人信息是: ', this.name + this.age);
}

showInfo.myCall(obj)		// 我的个人信息是: 猫13

解释一下: context.fn = this

这里主要是this指向以及原型链的问题, 我们将 myCall 这个函数当作了Function.prototype的属性, Function.prototype又是所有函数的顶级构造函数, 为什么这么说 ? 因为对象的__proto__指向其构造函数的prototype, 而函数也是对象, 所以Function.__proto__ = Function.prototype

既然mycalFunction.prototype上的属性, 那么所有的函数都会共享这个属性, 我们刚刚也说了.函数也是对象, showInfo.myCall() 是不是相当于一个对象在调用内部的方法, 那么当函数作为对象的方法被调用时, 谁调用, this就指向谁

我们可以在myCall内部来打印看一下是不是这样

果然, myCall内部的this是指向其调用的function, 这也就解释了为什么可以通过context.fn = this来完成 将function作为obj的属性这一步

接下来解决参数与返回值的问题

// 定义在Function的原型上
Function.prototype.myCall = function (context) {
  // 1. 将function作为obj的属性
  context.fn = this
  // 4. 参数与返回值问题
  let args = [], res
  for (let i = 1, l = arguments.length; i < l; i++) {
    args.push(arguments[i])
  }
  // 2. 让obj调用这个函数, 并传递参数,利用...扩展运算符
  res = context.fn(...args)
  // 3. 调用完删除这个属性
  delete context.fn

  // 5. 将参数返回出去
  return res
}

var obj = {
  name: '猫',
  age: 13,
}

function showInfo(num1, num2) {
  console.log('我的个人信息是: ', this.name + this.age);

  return num1 + num2
}

var a = showInfo.myCall(obj, 10, 20)		// 我的个人信息是: 猫13
console.log(a);		// 30

最后就是对于thisArg类型的问题, 对于像基本类型以及其中两个特殊的类型null和undefined的处理

终极实现方案

Function.prototype.myCall = function (context) {

  // 6.判断 context 的类型
  if (typeof context !== 'object') {
    // 如果不是对象类型, 就创建一个空对象
    context = Object.create(null)
  } else {
    // 如果是null或不传则返回window, 否则返回其本身
    context = context || window
  }

  // 1. 将function作为obj的属性
  context.fn = this
  // 4. 参数与返回值问题
  let args = [], res
  for (let i = 1, l = arguments.length; i < l; i++) {
    args.push(arguments[i])
  }
  // 2. 让obj调用这个函数, 并传递参数
  res = context.fn(...args)
  // 3. 调用完删除这个属性
  delete context.fn

  // 5. 将参数返回除去
  return res
}

var obj = {
  name: '猫',
  age: 13,
}

function showInfo(num1, num2) {
  console.log('我的个人信息是: ', this.name + this.age);

  return num1 + num2
}

var a = showInfo.myCall(obj, 10, 20)		// 我的个人信息是: 猫13
console.log(a);		// 30

showInfo.myCall(100)		// 我的个人信息是:  NaN

3. 关于call()的经典面试题

function fn1() { console.log(1) }
function fn2() { console.log(2) }

fn1.call(fn2)
fn1.call.call(fn2)

Function.prototype.call(fn1)
Function.prototype.call.call(fn1)

我们先把实现的myCall()方法拿过来, 对于该题目用不到的代码进行移除掉了, 以免混淆

还有一句重要的话: 当函数作为对象的方法被调用时, 谁调用, this就指向谁 哪里不明白就来看这一句话

Function.prototype.call = function (context) {
  if (typeof context !== 'object') {
    context = Object.create(null)
  } else {
    context = context || window
  }
  
  // 核心代码
  context.fn = this
  context.fn()

}

好, 接下来我们就以自己实现的call()方法来对上面的题目进行分析, 所有的分析都是在call()方法内部进行的

fn1.call(fn2):

  • context = fn2
  • context.fn = this => fn2.fn = fn1
  • context.fn() => fn2.fn() => fn1() // 最后fn1执行, 输出 1

fn1.call.call(fn2):

  • 首先, 要明白是哪个函数在执行, 谁调用的
  • 很明显, 是粉色部分fn1.call.call(fn2) 在执行, fn1.call.call(fn2) 在调用
  • context = fn2
  • context.fn = this => fn2.fn = fn1.call
  • context.fn() => fn2.fn() => fn2.fn1.call()
    • 此时会再次执行fn1.call(), 调用者是 fn2
    • 因为这次没有传this指向, 第一个参数为空, 会走else判断
    • context = window
    • context.fn = this => window.fn = fn2
    • context.fn() => window.fn2() // 最终fn2执行, 输出2

Function.prototype.call(fn1):

  • context = fn1
  • context.fn = this => fn1.fn = Function.prototype
  • context.fn() => fn1.fn() => fn1.Function.prototype()
    • 此时执行 fn1.Function.prototype() 调用者是fn1 this指向fn1
    • 执行Function.prototype() // 它是一个空的顶级构造函数, 最终什么也不会输出

Function.prototype.call.call(fn1):

  • context = fn1
  • context.fn = this => fn1.fn = Function.prototype.call
  • context.fn() => fn1.fn() => fn1.Function.prototype.call()
    • Function.prototype.call()执行, 调用者是fn1 this指向fn1
    • context = window
    • context.fn = this => window.fn = fn1
    • context.fn() => window.fn() => windown.fn1() // 最终fn1执行, 输出 1

总结:

  • 当只有一个call时, 执行call左边调用它的函数

  • 当有两个或两个以上的call时, 执行call的参数函数

posted @ 2021-07-29 21:18  只猫  阅读(1078)  评论(0编辑  收藏  举报