手动实现一个call
作者:青鸾
call
JS中,有一些很神奇的存在,比如我们今天要讲到的call,它可以改变this指向,而且用法简单,预支类似的还有bind、apply,三者的区别在于,bind的返回值是一个函数体,需要手动执行。call与apply基本一样,区别在于apply最多只接收两个参数,第二个参数是一个数组,其实在使用call和apply的时候,对我们来说无非就是是否要三个点,call和apply会立刻执行,无需再次执行。 今天我们来简单的去实现一个自己的call方法。
使用call的最简单案例
let obj = { a: 1 }
function fn() {
console.log(this.a)
}
fn.call(obj)
fn中并没有a属性,我们使用call将this指向obj对象,这样this.a便有值了。 这是一个最基本call的使用,我们需要注意两点: 1、函数call之后被执行了 2、使用call的函数this指向到了call的第一个参数
第一步很容易,第二步如何实现呢,其实很简单,我们直接把这个方法添加为call参数的属性,比如上面的obj和fn,我们这样,即可实现类似第二步的效果,简单来说就是加属性
let obj = {
a: 1,
fn: function() {
console.log(this.a)
}
}
考虑到这两点,我们便可以先开始实现一个简单的自定义call方法
Function.prototype.call2 = function(v) {
// 此处的v便是我们上文中的obj参数
v.fn = this
// 此处的this便是调用call的函数
v.fn()
// 此处的fn名称是任意的,我们加了一个属性名为fn的属性并且赋值进行了调用
delete v.fn
// 调用完成后删掉添加的属性fn
}
我们再来试一下
let obj = { a: 1 }
function fn() {
console.log(this.a)
}
Function.prototype.call2 = function(v) {
v.fn = this
v.fn()
delete v.fn
}
fn.call2(obj)
// 1
好耶!第一步很顺利,接下来我们就该考虑call独有的传参了,call接收参数的个数是不限的,为此我们可以使用函数的arguments属性,这是一个只能在函数内部访问的属性,是一个有长度的类数组对象,虽然只能在函数内部访问,不过足够用了。
为了通俗易懂,我们直接将上文的例子稍加变动,如下:
let obj = { a: 1 }
function fn(x, y) {
console.log(x)
console.log(y)
console.log(this.a)
}
fn.call(obj, 3, 2)
// 3
// 2
// 1
call的结果为3、2、1,这时候就需要对我们初版的call进行变动了。
Function.prototype.call2 = function() {
const args = []
// 此处我们加入了对第一个参数为null的判断
const v = arguments[0] === null ? window : arguments[0]
for(var i = 1; i < arguments.length; i++) {
args.push(arguments[i])
}
// 此处需要注意,即使v是Array类型,依然可以直接给其添加属性
v.fn = this
// ...args便是除了第一个参数之外的剩余参数,剩余运算符是ES6的新特性之一
v.fn(...args)
delete v.fn
}
这时候我们再输出,
fn.call2(obj, 3, 2)
// 3
// 2
// 1
发现与call一样了,但我们又想到了,我们上述的函数fn,都是没有return没有返回值的,那么有返回值的又该如何处理呢,其实很简单,加入return就可以了
Function.prototype.call2 = function() {
const args = []
const v = arguments[0] === null ? window : arguments[0]
for(var i = 1; i < arguments.length; i++) {
args.push(arguments[i])
}
v.fn = this
// 此处我们使用一个变量记录函数执行的返回值,然后将其返回
let res = v.fn(...args)
delete v.fn
return res
}
这样就可以有返回值了,可能有小伙伴会对v.fn这块有疑问,觉得如果是数组或者函数怎么办,其实这些都不会有任何影响,我们简单尝试一个数组
var arr = [1];
function fn() {
console.log(this[0])
}
fn.call(arr)
// 1
这里其实要涉及到一些JS原生部分,就不多做介绍了,简而言之,我们给数组添加的属性,我们是可以像对象那样去访问的。 不考虑极端情况,这个call我们已经基本完成了,但是笔者拿着写好的自定义的call方法在尝试的时候,发现了一个问题,在将Math.max.call更换为Math.max.call2时,出现了异常。
Math.max.call(...[1,2,3])
// 3
Math.max.call(null, ...[1,2,3])
我们写的call方法其实没有考虑到第一种情况——call的第一个参数是基本数据类型,比如1,'str',这种,使用官方提供的call我们可以看到有一步包装,这个笔者暂时还没想到怎么实现,各位有兴趣的也可以想一下,具体如下:
function fn() { console.log(this) }
fn.call(1)
// Number {1}
fn.call('str')
// String {'str'}
贴上最后的完整代码,最后一步暂时使用了穷举,还没有想到好的办法
Function.prototype.call2 = function () {
const args = []
var v
if (typeof arguments[0] === 'object') {
v = arguments[0] === null ? window : arguments[0]
for (var i = 1; i < arguments.length; i++) {
args.push(arguments[i])
}
// 此处需要注意,即使v是Array类型,依然可以直接给其添加属性
v.fn = this
let res = v.fn(...args)
delete v.fn
if (res !== undefined) {
return res
}
} else {
v = arguments[0] === undefined ? window : arguments[0]
switch (typeof v) {
case 'number':
v = new Number(v)
break;
case 'string':
v = new String(v)
case 'boolean':
v = new Boolean(v)
default:
break;
}
for (var i = 1; i < arguments.length; i++) {
args.push(arguments[i])
}
// 此处需要注意,即使v是Array类型,依然可以直接给其添加属性
v.fn = this
let res = v.fn(...args)
delete v.fn
if (res !== undefined) {
return res
}
}
}