(十五) 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
既然mycal
是Function.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的参数函数