一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第2天,点击查看活动详情。
本题难度:⭐ ⭐ ⭐
答:
面试过关版本
Function.prototype.myCall = function (context = window) {
context.fn = this
const args = [...arguments].slice(1)
const res = context.fn(...args)
delete context.fn
return res
}
尽可能完善的版本
Function.prototype.myCall = function (context) {
if (context === null || context === undefined) {
context = window
} else {
context = Object(context)
}
const fn = Symbol('fn')
context[fn] = this
const args = [...arguments].slice(1)
let result = context[fn](...args)
delete context[fn]
return result
}
要读懂本文,需掌握下面的前置知识:
一步一步实现手写 call
在手写之前,先明白 call 函数的功能,是为了改变 this 的指向,如下例所示:
var lastName = 'xxx'
const person = {
lastName: 'lin'
}
function fn () {
console.log(this.lastName)
}
fn.call(person) // 用call this 指向 person,输出 'lin'
fn.call() // 直接调用,this指向 window,输出 'xxx'
实现 this 的显式指向
call 是写到 Function.prototype 上的方法,我们可以自己实现一个 myCall,也写到原型链上。
Function.prototype.myCall = function(obj) {
obj.fn = this // 这里的 this 就是调用 myCall 的函数,把函数赋值给 obj.fn,这样执行 obj.fn() 时,函数内的 this 就指向了 obj
obj.fn() // 执行 obj上面的 fn 方法
delete obj.fn // 执行完了,fn 方法就没用了,删除掉
}
测试一下:
var lastName = 'xxx'
const person = {
lastName: 'lin'
}
function fn () {
console.log(this.lastName)
}
fn.myCall(person) // 输出 'lin'
fn.myCall(window) // 输出 'xxx'
这样就实现了 this 显式指向传入的对象。
兼容不传值的情况
但是现在还有问题,不传入要指向的对象的话会报错:
fn.myCall() // Uncaught TypeError: Cannot set properties of undefined (setting 'fn')
我们改造一下 myCall,兼容不传入上下文的情况,上下文不传入就默认指向 window,顺便改一下参数名字,不叫 obj,叫 context 更合适一点:
Function.prototype.myCall = function(context = window) {
context.fn = this
context.fn()
delete context.fn
}
测试一下:
var lastName = 'xxx'
function fn () {
console.log(this.lastName)
}
fn.myCall() // 'xxx'
这样就兼容了不传值的情况。
实现参数传递
但是现在还有问题,参数没有携带进去,执行下面的代码,arguments 是空的。
const person = {
lastName: 'lin'
}
function fn () {
console.log('this.lastName :>> ', this.lastName)
console.log('[...arguments] :>> ', [...arguments])
}
fn.myCall(person, 1, 2, 3, 4)
我们改造一下,执行 fn 函数时把参数携带进去。
Function.prototype.myCall = function(context = window) {
context.fn = this
const args = [...arguments].slice(1) // 获取除了第一个参数之外后面的所有参数
context.fn(...args) // 执行的时候把参数传递进去
delete context.fn
}
还是刚才的代码,测试结果如下:
获取返回值
但是现在还有问题,执行这个函数时的返回值没有返回,比如返回所有参数相加的和,结果是 undefiend。
function fn () {
return [...arguments].reduce((acc, cur) => acc + cur)
}
console.log(fn.myCall(person, 1, 2, 3, 4)) // undefined
继续改造 myCall 函数,
Function.prototype.myCall = function(context = window) {
context.fn = this
const args = [...arguments].slice(1)
const res = context.fn(...args) // 用一个变量 res 来接收函数返回值
delete context.fn
return res // 返回 res
}
测试一下:
function fn () {
return [...arguments].reduce((acc, cur) => acc + cur)
}
console.log(fn.myCall(person, 1, 2, 3, 4)) // 10
至此,一个面试过关版本的手写 call 函数,就实现完了。
拓展:尽可能完善的版本
尽可能完善的版本,想办法解决一些边缘情况。
兼容原始类型
什么是边缘情况,比如,把要指向的对象指向一个原始值,就报错了:
fn.myCall(0)
这时,就需要参考一下原生的 call 函数是如何解决了的,我们打印出来看一下:
var lastName = 'xxx'
const person = {
lastName: 'lin'
}
function fn (type) {
console.log(type, '->', this.lastName)
}
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,是因为这些对象上都没有 lastName 属性。
改造一下我们的 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
}
symbol 处理对象的属性,防止属性重名
假设对象上本来就有一个 fn 属性,执行下面的调用,对象上的 fn 属性会被删除。
const person = {
lastName: 'lin',
fn: 123
}
function fn () {
console.log(this.lastName)
}
fn.myCall(person)
console.log('person :>> ', person);
因为对象上本来的 fn 属性和 myCall 函数内部临时定义的fn 属性重名了。
可以用 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
}
这样在处理的过程中,fn 就不会因为重名被覆盖了,如下图所示:
至此,一个尽可能完善版本的 myCall ,终于写完了。
其实用 Math.random() 来生成一个随机的属性,也可以解决属性重名问题,这里就不再赘述了。
结尾
阿林水平有限,文中如果有错误或表达不当的地方,非常欢迎在评论区指出,感谢~
如果我的文章对你有帮助,你的👍就是对我的最大支持^_^
你也可以关注《前端每日一问》这个专栏,防止失联哦~
我是阿林,输出洞见技术,再会!
上一篇:
「前端每日一问(38)」列举一些call、apply、bind 的使用场景
下一篇: