前言
js中有不少比较难以理解的概念,比如 js原型 和 继承 。我曾经很早的时候就看过js原型方面的知识,并在当时写了一篇 博客 作为记录,很显然当时的我只是死记硬背。最近我利用空闲的时间将一些相对比较深入的js概念和用法重新学习,并新建了一个专栏 深入javascript 用于记录和分享。以下来介绍如何手动模拟实现一个 call
函数:
原生 call 表现形式
var foo = 'windowFoo'
const obj = {
foo: 'objFoo',
getName: function (a, b) {
console.log(this.foo, a, b)
}
}
const obj2 = { foo: 'obj2Foo' }
obj.getName(1, 2) // objFoo 1, 2
obj.getName.call(obj2, 1, 2) // obj2Foo 1, 2
obj.getName.call(null, 1, 2) // windowFoo 1, 2
obj.getName.call(undefined, 1, 2) // windowFoo 1, 2
obj.getName.call(false, 1, 2) // undefined 1, 2
如何你对于 call
函数用法不太了解,建议先看以下我的这篇博客 深入 JavaScript 之 call函数 用法 ,在了解它的用法和行为后再来模拟它的实现会更得心应手。
根据上述的代码,我们可以得到它的表现形式如下:
- 改变一个函数内部的
this
指向 - 第一个参数为其他的特定值时,
this
指向会转成不同的值(这里指代null
、undefined
等值) - 以参数列表的形式将后续参数传给函数
将上面的三种表现形式实现,便几乎实现了一个 call
函数的模拟了,下面我们来一步一步实现。
改变一个函数内部的 this
指向
首先我们需要知道的是 call
函数是 Function.prototype
上的方法,于是我们可以这样写:
const obj = {
name: 'objName',
getThis: function () {
console.log(this)
}
}
const obj2 = { name: 'obj2Name', }
obj.getThis() // { name: 'objName', getThis: f () }
Function.prototype.call2 = function (context) {
context.fn = this
context.fn()
delete context.fn
}
obj.getThis.call2(obj2) // { name: 'obj2Name' }
context.fn = this
这里的 this
就是 call2
函数的调用者 getThis
方法,通过赋值给 context.fn
,于是 getThis
函数 内部 this
值 被改为了 fn 的调用者 。以上代码非常简单、易于理解,实现第二个表现形式就更简单了。
第一个参数为其他值时,this
指向会转成不同的值
当第一个参数是 null
和 undefined
时,函数内部的 this
值会转为 window
。当它是其他的值时,它的 this
表现形式类似于一个空对象,其实加个判断就行了。为了更贴近原生表现形式,我这里是这样写的:
Function.prototype.call2 = function (context) {
function getContext(target) {
return (target === null || target === undefined) ? window : Object(target)
}
context = getContext(context)
context.fn = this
context.fn()
delete context.fn
}
以上几乎 100% 模拟了原生表现,如下:
obj.getThis.call2(null) // window
obj.getThis.call2(undefined) // window
obj.getThis.call2(true) // Boolean(true)
obj.getThis.call(null) // window
obj.getThis.call(undefined) // window
obj.getThis.call(true) // Boolean(true)
以参数列表的形式将后续参数传给函数
采用 ES6 的话就非常简单:
Function.prototype.call2 = function (context) {
// 获取取 context 之后的参数
const args = []
for (let i = 1; i < arguments.length; i++) {
args.push(arguments[i])
}
function getContext(target) {
return (target === null || target === undefined) ? window : Object(target)
}
context = getContext(context)
context.fn = this
const result = context.fn(...args)
delete context.fn
// 用于返回函数的返回值
return result
}
本篇实现主要参考 该博客 实现的,这位大佬通过 eval 执行一个拼接参数后的函数字符串 来实现以上功能,如果你们感兴趣可以去他博客看看,我这边为了偷懒就直接用 ES6 了😴。
完整代码
var name = 'windowName'
const obj = {
name: 'objName',
getName: function (a, b) {
console.log(this.name, a, b)
return a + b
}
}
const obj2 = { name: 'obj2Name' }
Function.prototype.call2 = function (context) {
// 获取取 context 之后的参数
const args = []
for (let i = 1; i < arguments.length; i++) {
args.push(arguments[i])
}
function getContext(target) {
return (target === null || target === undefined) ? window : Object(target)
}
context = getContext(context)
context.fn = this
const result = context.fn(...args)
// 执行完毕删除 fn,避免给 context 对象增加额外属性
delete context.fn
// 用于返回函数的返回值
return result
}
const num = obj.getName.call2(obj2, 1, 2) // obj2Name 1 2
console.log(num) // 3
obj.getName.call2(null, 1, 2) // windowName 1 2
obj.getName.call2(undefined, 1, 2) // windowName 1 2
obj.getName.call2(true, 1, 2) // undefined 1 2
其实还是很简单的,大多数看似复杂的事物只要有序地拆分为若干个小块后,实现起来也是水到渠成~
注意:请在浏览器环境下执行以上代码,不然打印可能会不一致