说明:本文只关注该函数的核心功能,不会过多处理边界情况(edge case)。
一、 原生 call 函数分析
call函数的基本使用这里不做赘述,想了解可以去看看MDN:
Function.prototype.call() - JavaScript | MDN (mozilla.org)
1.1 原生 call 函数的特性
- 原生call函数,可以在任何的函数上调用
- 能调用对应的函数
- 原生call函数语法:
function.call(thisArg, arg1, arg2, ...),分析可以得出第一个是需要绑定的this,后边是需要传递的参数 - 使用调用者提供的
this值和参数调用该函数的返回值。若该方法没有返回值,则返回undefined。
二、实现 call 函数
2.1 实现可以在任何函数上调用
对于可以在任何函数上调用,说明是共享的(继承),可以在Object.prototype或Function.prototype上新增函数即可实现(因为原型链关系,如下图所示 ps:自己理解画的,如有不妥之处还请纠正)。
这里我们在
Function.prototype上新增一个自己的call函数,我命名为_call(你随意~)。
Function.prototype._call = function(){
console.log('_call')
}
// 测试函数
function foo(){
console.log('foo执行了~')
}
// 测试一下,看能否调用
foo._call()
输出:
foo执行了~
第一步已经完成,我们已经实现了一个可以在任何函数上调用的_call方法,让我们继续!别停下!
2.2 能调用对应的函数
对于调用对应的函数,只要我们能拿到谁调用了_call,我们就能调用这这个调用者,那么怎么拿到谁调用了_call呢?
答案是:this。请看以下示例:
Function.prototype._call = function(){
// 看看this是什么?
console.log('this: ', this)
}
function foo() {
console.log('foo执行了~')
}
foo._call()
输出:
因为我们这里写的是普通函数,而普通函数会绑定this,foo._call()的时候this就指向foo了,由此可知,this确实指向了我们的调用者,那么我们只需要this(),即可调用我们的调用者,接下来改造我们的_call。
Function.prototype._call = function () {
const fn = this // 这里的 fn 指向调用者 foo
fn()
}
function foo() {
console.log('foo执行了~')
}
foo._call()
输出:
看!我们已经成功的调用了我们的调用者!
2.3 参数
接收参数的话只需要_call(thisArg,...args)即可。
2.3.1 参数之绑定 this
接收了,怎么改变调用者的this呢?
还记的刚刚我们是怎么拿到调用者的吗?
我们在自己的函数内部 通过this关键字拿到了调用者,那么我们现在是要改变某个函数的this(不通过 call、apply、bind),是不是可以逆推上面的逻辑呢?只要在某个需要绑定的this上调用我们的fn函数,那么这个fn函数的this是不是就指向了我们需要绑定的this呢?
接下来试一试:
Function.prototype._call = function (thisArg, ...args) {
// 1.获取需要执行的函数
const fn = this // 这里的 fn 指向调用者 foo
// 2.通过隐式绑定改变fn函数的this指向
thisArg.fn = fn
thisArg.fn()
delete thisArg.fn // 调用完了之后就可删除了
}
function foo() {
console.log('foo执行了~', this)
}
// { a: 'foo' } 是绑定的this
foo._call({ a: 'foo' })
输出:
可以发现打印的this确实指向了我们的{ a: 'foo' }对象,但是发现这个对象上多了一个fn方法?其实我们函数执行完了之后就已经删除了的,只是这里还在显示而已。
隐患:如果传入进来的这个this上边有fn方法的话,那么这里会产生覆盖问题(概率很小,但不排除),我们可以使用Symbol来创建一个独一无二的值,这样的话,就不会产生覆盖问题了。
用Symbol改写:
Function.prototype._call = function (thisArg, ...args) {
// 1.获取需要执行的函数
const fn = this // 这里的 fn 指向调用者 foo
// 2.通过隐式绑定改变fn函数的this指向
// thisArg.fn = fn
// thisArg.fn()
// delete thisArg.fn
const key = Symbol()
thisArg[key] = fn
thisArg[key]()
delete thisArg[key]
}
function foo() {
console.log('foo执行了~', this)
// 隔一会再打印看看还有没有~
setTimeout(() => {
console.log(this)
}, 1000)
}
// { a: 'foo' } 是绑定的this
foo._call({ a: 'foo' })
输出:
我们已经实现了改变函数的this指向,接下来看看传递其他参数应该怎么实现。
2.3.2 call方法的其他参数
在上面我们已经提到,可以通过_call(thisArg,...args)接收剩余参数,那么我们只需要在调用fn函数的时候,传递进去就可以了。
Function.prototype._call = function (thisArg, ...args) {
// 1.获取需要执行的函数
const fn = this // 这里的 fn 指向调用者 foo
// 2.通过隐式绑定改变fn函数的this指向
// thisArg.fn = fn
// thisArg.fn()
// delete thisArg.fn
const key = Symbol()
thisArg[key] = fn
thisArg[key](...args)
delete thisArg[key]
}
function foo(a, b, c, d) {
console.log('foo执行了~', this)
console.log('a, b, c, d: ', a, b, c, d)
}
// { a: 'foo' } 是绑定的this
foo._call({ a: 'foo' }, 1, 2, 3, 4)
输出:
其他参数的问题也已经解决了!
2.3.3 this参数的特殊处理
原生的call方法,在传入的第一个参数不是object的时候,是有特殊处理的,如下图所示:
原生call特性:
function foo() {
console.log('this: ', this)
}
foo.call()
foo.call(null)
foo.call(undefined)
foo.call('str')
foo.call(123)
foo.call({})
foo.call([])
输出:
我们这里也来一一实现一下:
// 封装一个函数,用于判断一个值是否是 object 或 funciton,排除null(因为null不能添加任何属性,但是typeof null === 'object')
function isObject(value) {
const type = typeof value
return value !== null && (type === 'object' || type === 'function')
}
Function.prototype._call = function (thisArg, ...args) {
// 1.获取需要执行的函数
const fn = this // 这里的 fn 指向调用者 foo
// 2.判断 this 是否是对象
if (!isObject(thisArg)) {
// 不是对象
const type = typeof thisArg
// null、undefined、或者为空的时候
if (thisArg === null || type === 'undefined') {
thisArg = window
} else if (type === 'string' || type === 'number') {
// 小技巧,Object函数,可以将一些基本数据类型包装成对象类型
// Object(123) => Number {123}, Object('str') => String {'str'}
thisArg = Object(thisArg)
}
}
// 3.通过隐式绑定改变fn函数的this指向
// thisArg.fn = fn
// thisArg.fn()
// delete thisArg.fn
const s1 = Symbol()
thisArg[s1] = fn
thisArg[s1](...args)
delete thisArg[s1]
}
function foo() {
console.log('this: ', this)
}
// 测试
foo._call()
foo._call(null)
foo._call(undefined)
foo._call('str')
foo._call(123)
foo._call({})
foo._call([])
输出:
对照上图,可以发现,我们已经实现了call函数的核心功能。
2.4 返回值
这个就很简单了,接收一下再return就可以了。
function isObject(value) {
const type = typeof value
return value !== null && (type === 'object' || type === 'function')
}
Function.prototype._call = function (thisArg, ...args) {
// 1.获取需要执行的函数
const fn = this // 这里的 fn 指向调用者 foo
// 2.判断 this 是否是对象
if (!isObject(thisArg)) {
const type = typeof thisArg
if (thisArg === null || type === 'undefined') {
thisArg = window
} else if (type === 'string' || type === 'number') {
thisArg = Object(thisArg)
}
}
// 3.通过隐式绑定改变fn函数的this指向
const s1 = Symbol()
thisArg[s1] = fn
const res = thisArg[s1](...args)
delete thisArg[s1]
return res
}
function foo(a, b, c, d) {
console.log('this: ', this)
console.log('a, b, c, d: ', a, b, c, d)
return a + b + c + d
}
const result = foo._call({}, 1, 2, 3, 4)
console.log('result: ', result)
输出:
三、最终实现
整理一下我们函数,附上完整版
// 工具函数
function isObject(value) {
const type = typeof value
return value !== null && (type === 'object' || type === 'function')
}
Function.prototype._call = function (thisArg, ...args) {
// 1.获取需要执行的函数
const fn = this
// 2.判断 this 是否是对象
if (!isObject(thisArg)) {
const type = typeof thisArg
if (thisArg === null || type === 'undefined') {
thisArg = window
} else if (type === 'string' || type === 'number') {
thisArg = Object(thisArg)
}
}
// 3.通过隐式绑定改变fn函数的this指向
const s1 = Symbol()
thisArg[s1] = fn
const res = thisArg[s1](...args)
delete thisArg[s1]
// 4.返回值
return res
}