手写call,apply和bind(分析三者的用法与区别)

906 阅读7分钟

它们有什么用及区别?

在阐述它们如何使用之前,我们有必要整理清楚this的用法,简单的说thisJavaScript语言的一个关键字,它是函数运行时,在函数体内部自动生成的一个对象,只能在函数体内部使用。

那么问题又来了,this的值是什么呢?

因为this是在函数运行时,函数内部自动生成的一个对象,那么接下来我们通过函数来对this进行分析。 首先JavaScript中的函数可以分为两类:

  • 常规函数:函数声明式,函数表达式,构造函数
  • 箭头函数:(ES6引入使用)

接下来分别分析this在这些函数中究竟是什么?

理解常规函数中的this

1.纯粹的函数调用

function test(name) {
    console.log(name)
    console.log(this)
}
test('Jerry')  //调用函数

以上函数调用的方式是非常常见的,然而这只是一种简写的形式,完整的写法应该如下:

function test(name) {
    console.log(name)
    console.log(this)
}
test.call(undefined, 'Tom')

这里面便出现了我们将要学习的call,先不讨论它的作用,我们继续讨论this的用处,call方法接受的第一个参数就是this,但是我们这里是undefined,按照规定,如果你传的contextnull 或者 undefined,那么 window对象就是默认的context(严格模式下默认contextundefined)。

2.对象中函数的调用

const obj = {
    name: 'Jerry',
    greet: function() {
        console.log(this.name)
    }
}
obj.greet()  //第一种调用方法
obj.greet.call(obj) //第二种调用方法

从上面的例子中,我们发现这次call方法的第一个参数为obj,此时说明函数greet内部的this指向了obj对象,这显而易见便知call方法的作用是改变this的指向,又因为上面两种调用方式结果一样可知函数的this指向可以理解为谁调用便指向谁。

3.构造函数中的this

每个构造函数在new之后都会返回一个对象,这个对象就是this,也就是context上下文。

理解箭头函数中的this

在使用箭头函数的时候,箭头函数会默认绑定外层的this值,所以在箭头函数中this的值和外层的this是一样的。因为箭头函数没有this,所以需要通过查找作用域链来确定this的值。

这就意味着如果箭头函数被非箭头函数包含, this绑定的就是最近一层非箭头函数的 this

注意:多层对象件套里面的this是和最外层保持一致的。

因为今天的重点是讲解callapplybind的用法及实现,然而箭头函数是没有这些方法的,所以箭头函数的使用仅限于此。

首先说明callapply是ES5中的语法,bind是ES6新引入的,它们三者的相似之处为:

  • 都是用来改变函数的this对象的指向
  • 第一个参数都是this要指向的对象
  • 都可以利用后续参数进行传参

不同之处使用一个例子进行说明:

const personOne = {
    name: "张三",
    age: 12,
    say: function () {
        console.log(this.name + ',' + this.age);
    }
}

const personTwo = {
    name: "李四",
    age: 24
}

personOne.say();    //张三,12

对于以上的结果,我们应该都非常清楚,那么问题来了,如果我们想要知道personTwo对象的信息如何实现呢?

分别使用callapply以及bind方法实现,并从中得到它们三者的区别:

personOne.say.call(personTwo);       //李四,24
personOne.say.apply(personTwo);      //李四,24
personOne.say.bind(personTwo);       //没有输出任何东西

修改以上代码对比可知:callapply都是对函数的直接调用,而bind方法返回的仍然是一个函数,因此我们需要执行它才会有结果。

personOne.say.call(personTwo);       //李四,24
personOne.say.apply(personTwo);      //李四,24
personOne.say.bind(personTwo)();       //李四,24

接着继续讨论其余参数

const personOne = {
    name: "张三",
    age: 12,
    say: function (gender, phone) {
        console.log(this.name + ',' + this.age + ',' + gender + ',' + phone);
    }
}

const personTwo = {
    name: "李四",
    age: 24
}

personOne.say("女", "123");

这个例子的区别于上面的即为say函数需要传递参数,我们分别使用这三种方法实现传递参数:

personOne.say.call(personTwo, "女", "123");       //李四,24,女,123
personOne.say.apply(personTwo, ["女", "123"]);    //李四,24,女,123
personOne.say.bind(personTwo, "女", "123")();     //李四,24,女,123

显而易见的区别callbind除了第一个参数外,之后的参数均为一一传递,而apply除了第一个参数外,只有一个参数即为一个数组,数组中的每一项为函数需要的参数。

说明它们的用法以及区别之后,我们就要自己尝试着剖析它的原理,自己书写这三个方法啦~~~~~

call实现

在知道了它的使用即原理之后,想必直接看实现方法应该也可以理解的,那么先上代码:

Function.prototype.myCall = function (obj) {
    const object = obj || window; //如果第一个参数为空则默认指向window对象
    let args = [...arguments].slice(1); //存放参数的数组
    object.func = this;
    const result =  object.func (...args);
    delete object.func; //记住最后要删除掉临时添加的方法,否则obj就无缘无故多了个fn
    return result;
}

代码非常简短,一步步进行说明解释:

因为call方法是每一个函数都拥有的,所以我们需要在Function.prototype上定义myCall,传递的参数obj即为call方法的第一个参数,说明this的指向,如果没有该参数,则指向默认为window对象。

args为一个存放除第一个参数以外的其余参数的数组(arguments为函数中接收到的多有参数,[...arguments]可以将arguments类数组转换为真正的数组,详细讲解可以查看ES6语法)。

解释object.func=this之前,我们先使用示例使用一下自己定义的myCall函数:

const personOne = {
    name: "张三",
    age: 12,
    say: function (gender, phone) {
        console.log(this.name + ',' + this.age + ',' + gender + ',' + phone);
    }
}

const personTwo = {
    name: "李四",
    age: 24
}

Function.prototype.myCall = function (obj) {
    const object = obj || window; //如果第一个参数为空则默认指向window对象
    let args = [...arguments].slice(1); //存放参数的数组
    object.func = this;
    const result =  object.func (...args);
    delete object.func; //记住最后要删除掉临时添加的方法,否则object就无缘无故多了个func
    return result;
}

personOne.say.myCall(personTwo,"女",18333669807);   //李四,24,女,18333669807

根据示例,我们进行解释,myCall里面的this指的是personOne.say这个方法(因为myCall是一个方法,上面所说的,谁调用它,它的this便指向谁),object.func=this相当于给object这个对象克隆了一个personOne.say方法,让object在调用这个方法,相当于object.personOne.say,达到了call的效果。(记住最后要删除掉临时添加的方法,否则object就无缘无故多了个func

object.func (...args)里面的参数即为传入的其余参数。

apply实现

通过上面的分析,想必大家应该已经基本明白了它是如何实现的了,那么接下来实现apply就非常简单了,因为两者的区别主要就是参数的传递方式不同,和上面一样,先直接看一下代码:

Function.prototype.myApply = function (obj) {
    const object = obj || window; //如果第一个参数为空则默认指向window对象
    if (arguments.length > 1) {
        var args = arguments[1]; //存放参数的数组
    } else {
        var args = []; //存放参数的数组
    }
    object.func = this;
    const result = object.func(...args);
    delete object.func; //记住最后要删除掉临时添加的方法,否则obj就无缘无故多了个fn
    return result;
}

personOne.say.myApply(personTwo, ["女", 24]);

主要区别就是获取参数不同,因为apply的带二个参数为数组,数组中包含函数需要的各项参数值,其余内容实现myCall相同,此处就不在做解释。

bind实现

话不多说,依旧是先上代码

Function.prototype.myBind = function (obj) {
    const object = obj || window; //如果第一个参数为空则默认指向window对象
    let self = this;
    let args = [...arguments].slice(1); //存放参数的数组

    return function () {
        let newArgs = [...arguments]
        return self.apply(object, args.concat(newArgs))
    }
}

personOne.say.myBind(personTwo, "女", 24)();

前面的知识不重复说,return function是因为bind返回的是一个函数,并且这个函数不会执行,需要我们再次调用,那么当我们调用的时候,我们依旧可以对这个函数进行传递参数,即为支持柯里化形式传参,所以需要在返回的函数中声明一个空的数组接收调用bind函数返回的函数时传递的参数,之后对两次的参数使用concat()方法进行连接,调用ES5中的apply方法。

到此为止,这三种方法的使用,区别以及实现已经都讲述完了,希望这篇文章对大家有所帮助~~