手动实现自己的call、apply、bind方法

467 阅读3分钟

写在前面: 其实已经有很多优秀的文章去介绍前端相关知识,自己在学习过程中也是看了很多别人总结的知识点。决定开始自己写文章主要是为了对所学东西有个总结,希望后续慢慢也能起到分享交流的作用。

一、call、apply、bind的异同

在javascript中,这三个方法主要都是为了改变函数内部this指向的,作用大体相同,下面将依次介绍三个方法。

1. call()

语法: call(thisArgs, args1, args2,...),其中,thisArgs指的是函数运行时指向的this,arg1, arg2, ...指的是调用的参数;

返回值: 返回的是指定的this调用该函数以后的结果;

示例:

function say(name, hobby){
    console.log(this)
    console.log(`my name is ${name}, my age is ${this.age},my hobby is ${hobby}`)
}
let obj = {
    age: 18
}
say('shaoyan', 'js')         
say.call(obj, 'shaoyan', 'js')         

在控制台执行上面的代码,执行结果如下图:

可以看到,第一次执行say方法时, this指向window, 第二个console打印结果为:my name is shaoyan, my age is undefined,my hobby is js,由于window上没有定义age属性,所以打印出的age结果为undefined; 而第二次使用call方法,将函数的this指向转为obj这个对象,this指向obj

再来一个示例:

function Animal(name, age) {
  this.name = name;
  this.age = age;
}

function Cat(name, age) {
  Animal.call(this, name, age);
  this.category = '猫科';
}
let cat1 = new Cat('feta', 2);       //来一个2岁的猫
console.log(cat1)

同样的控制台上执行,

在一个子构造函数中,你可以通过调用父构造函数的 call 方法来实现继承。上面的例子中,使用 Cat 构造函数创建的对象实例会拥有在 Animal 构造函数中添加的 name 属性和 age 属性,但 category 属性是在各自的构造函数中定义的。

2. apply()

apply方法和call方法区别在于就是call()方法接受的是参数列表,而apply()方法接受的是一个参数数组。

语法: apply(thisArgs, [argsArray]),其中,thisArgs指的是函数运行时指向的this,[argsArray]传入的参数,数组或者类数组对象;

返回值: 返回的是指定的this调用该函数以后的结果;

示例:

// 实例和上面call方法一致,除了参数是通过数组的形式传入,不多做介绍
function say(name, hobby){
    console.log(this)
    console.log(`my name is ${name}, my age is ${this.age},my hobby is ${hobby}`)
}
let obj = {
    age: 18
}
say.apply(obj, ['shaoyan', 'js'])          // 打印结果和call也是一致的

3.bind()方法

语法: bind(thisArgs, args1, args2,...),其中,thisArgs指的是函数运行时指向的this,arg1, arg2, ...指的是调用的参数;

返回值: 返回一个原函数的拷贝,并拥有指定的 this 值和初始参数;

bind()方法和call方法很相似,区别在于call方法返回的时函数的调用结果,而bind方法仅仅返回一个函数,如果要调用的话需要去执行它;

function say(name, hobby){
    console.log(this)
    console.log(`my name is ${name}, my age is ${this.age},my hobby is ${hobby}`)
}
let obj = {
    age: 18
}
say.bind(obj, 'shaoyan', 'js')        
//并不会执行函数,在控制台上打印结果为:
//ƒ say(name, hobby){
//   console.log(this)
//   console.log(`my name is ${name}, my age is ${this.age},my hobby is ${hobby}`)
//}
say.bind(obj, 'shaoyan', 'js')()    //这样才会执行

4. 总结

来一个简短的总结

函数名 语法 参数方式 返回值
call call(thisArgs,args1,arg2,...) 参数列表 函数的执行结果
apply apply(thisArgs,[argsArray]) 参数数组 函数的执行结果
bind call(thisArgs,args1,arg2,...) 参数列表 函数

二、 手动实现一个call方法

话不多说,我们来自己手动实现一个call方法

Function.prototype.myCall = function(){
    //执行期上下文
    let content = [...arguments][0] || window
    content.fn = this
    let args = [...arguments].slice(1)
    return content.fn(...args)
}

写完以后还是用上面的例子来验证一下:

function say(name, hobby){
    console.log(this)
    console.log(`my name is ${name}, my age is ${this.age},my hobby is ${hobby}`)
}
let obj = {
    age: 18
}
say.myCall(obj, 'shaoyan', 'js')        

来控制台上打印一下:

第二个console打印出的结果是my name is shaoyan, my age is 18,my bobby is js,可以实现改变this指向的作用,是不是好开心,慢着,仔细看下打印出的this,按理说你应该给我打印出我定义的obj就行呀,你咋给我还加了个fn的属性方法呢?这不乱套了吗,那我们就对这个方法再进行一下修改

Function.prototype.myCall = function(){
    //执行期上下文
    let content = [...arguments][0] || window
    content.fn = this
    let args = [...arguments].slice(1)
    let result = content.fn(...args)
    delete content.fn    // 多了,我把你给删除掉
    return result
}

这下再验证一下,ok没有问题啦,这样就实现了一个自己的call方法

三、 手动实现apply方法

apply方法就简单了,基本是call方法一致,只要将参数方式改为数组就可以了,上代码

Function.prototype.myApply = function() {
    let content = [...arguments][0] || window
    content.fn = this
    let args = [...arguments][1]
    let result = content.fn(...args)
    delete content.fn    
    return result
}
// 测试也是ok的

四、手动实现bind方法

Function.prototype.myBind = function() {
    let args = [...arguments]
    let content = args.shift() || window
    let _self = this
    return function(){
        let newAgrs = [...arguments]
        _self.apply(content, args.concat(newAgrs))
    }
}
// bind方法借助apply方法去实现

至此,自己手动实现call、apply、bind方法就此完成。