前端必会的手写实现面试题——bind

前言

  我在另一篇文章中介绍了call/apply的手写实现,还没有看的小伙伴可以先看了call/apply再来看bind的实现,因为他们之中有相似之处,但是bind的实现会更加难一些,循序渐进更能加深理解:
前端必会的手写实现面试题——call/apply

实现bind方法

一句话简单介绍call/apply:

bind() 方法会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的this,之后的一序列参数将会在传递的实参前传入作为它的参数。(这里我直接引用MDN的专业描述)

我们通俗点来解释就是,bind和call/apply一样可以修改函数的this指向,但是bind不会立即执行,而是返回一个绑定了新this的函数,待需要的时候再执行即可。

这里我们就不演示bind的基本使用了,毕竟大家肯定是对bind的使用很熟悉了,才会开始琢磨自己实现它对吧。

在实现bind的功能之前,我们先对它的特点做一个总结:

  1. 必须是函数才能调用bind
  2. 可以修改函数this指向
  3. 返回一个绑定了指定this的新函数,在下面例子中我们用bindFun表示
  4. 支持传参且支持函数柯里化
  5. 返回的bindFun还能当作构造函数,new出新的实例
  6. 返回的bindFun中的this无法再次修改,使用call/apply也不行

可能会有朋友对 函数柯里化 不熟悉,这里给大家简单解释一下:
  所谓函数柯里化其实是在函数调用时只传递一部分参数进行调用,函数会返回一个新函数去处理剩下的参数,这么说可能会有点绕,给大家提供一个例子参考一下

function foo(x, y) {
    return function(y) {
        console.log(x + y)
    }
}

let f = foo(1)
f(2)  // 3
foo(1)(2)  //3

好啦说完了前置知识,我们就开始一一实现上面总结的特点吧。

这里我再提一下,bind的实现有些部分是和call/apply重复的,那我就不浪费篇幅重新讲一遍了,没有看过call/apply实现的小伙伴们可以先看我这篇文章哦:
前端必会的手写实现面试题——call/apply



实现第一步骤:初步实现

  1. 必须是函数才能调用bind
  2. 可以修改函数this指向
  3. 返回一个绑定了指定this的新函数,在下面例子中我们用bindFun
    表示

首先第一条我们可以在开头判断调用this的是不是函数,不是的话就抛出一个错误。
第二条修改this指向,为了代码简洁我们可以使用call/apply来修改。
第三条我们只需要return一个设定好this指向的函数(bindFun)出去就好

Function.prototype.bind2 = function (context) {
    if (typeof this !== 'function') { //必须是函数才能调用bind
        throw new Error('Function.prototype.bind - what is trying to be bound is not callable')
    }
    let f = this    //先获取要执行的函数
    let bindFun =  function () {
        f.apply(context)  //使用apply修改this的指向s
    }
    return bindFun
}

let obj = {
    name: 'roger'
};
function show() {
    console.log(`名字(obj的属性):${this.name}`)
}

let bindFun = show.bind2(obj);
bindFun()   //名字(obj的属性):roger

需要注意的是,我们使用let f = this提前保存了调用bind的函数,因为如果不提前保存,在执行到bindFun内部的时候this会指向window(不明白this为什么会指向window的小伙伴需要再补一补this相关知识哦)。

我们第一步骤就已经实现了前三条特点,前三条是比较容易实现的,如果还有小伙伴看不太懂,就说明还没有吃透call/apply的实现原理,可以回到我前面的文章巩固一下,因为接下来的实现会比前面的稍微难一些。



我们继续吧。

实现第二步骤:传参和函数柯里化

这一步骤只要大家了解了函数柯里化是个什么东西,难度就会大大的降低,我们可以在调用bind2的时候传入一部分参数,后面在调用返回bindFun的时候补全剩余参数,原理知道了,那我们来实践一下试试:

Function.prototype.bind2 = function (context) {
    if (typeof this !== 'function') { //必须是函数才能调用bind
        throw new Error('Function.prototype.bind - what is trying to be bound is not callable')
    }
    let f = this    //先获取要执行的函数
    let agrs = [...arguments].slice(1) //第一次取参数(调用bind2时传入的参数)
    let bindFun =  function () {
        let agrs2 = [...arguments]  //第二次取参(调用bindFun时传入的参数)
        let Allagrs = agrs.concat(agrs2)    //合并两组参数(要注意一下顺序)
        f.apply(context, Allagrs)  //使用apply修改this的指向s
    }
    return bindFun
}

let obj = {
    name: 'roger'
};
function show(age, address) {
    console.log(`名字(obj的属性):${this.name}`)
    console.log(age)
    console.log(address)
}

let bindFun = show.bind2(obj, 18);
bindFun('深圳')   //名字(obj的属性):roger; 18; 深圳

实现了一遍之后是不是就觉得和函数柯里化并没有想象中那么高深莫测?到这里其实我们就已经实现了bind的绝大部分基本功能了,大家切忌光看不练,看到这里一定要打开编辑器尝试coding,看看能不能不看教程的情况下自己写出来,认真思考,重复练习才是学习的重中之重。




好啦,相信大家已经展握上面的知识点了,下面我们开始讲解bind实现最难的部分,也就是第5点:返回的bindFun还能当作构造函数,new出新的实例。

绑定函数也可以使用 new 运算符构造,它会表现为目标函数已经被构建完毕了似的。提供的 this 值会被忽略,但前置参数仍会提供给模拟函数。(这里引用MDN的专业描述)

我们再通俗一点的解释:通过bind返回的bindFun函数可以和构造函数一样,通过new构造出新的实例对象,在这个过程中,this的指向会指向到实例中(这里需要大家对构造函数和原型有了解)。



为了方便大家理解,我还是先演示一遍bind的使用,再来写实现部分:

let obj = {
    name: 'roger'
}

function show(age, address) {
    this.name = this.name
    this.age = age
    this.address = address
    this.profession = '前端开发工程师'
}
show.prototype.hobby = '看电影'

let bindFun = show.bind(obj, 18)
let person = new bindFun('深圳')

console.log(person) //{name: undefined, age: 18, address: "深圳", profession: "前端开发工程师"}
console.log(person.hobby)   //看电影

相信大家看完就悟了,其实就是把bindFun当作一个构造函数,可以new出任意个实例对象嘛对吧。


当然眼尖的朋友会疑惑为什么name: undefined,明明this指向的是obj对不对,我给大家解释一下:
  我们知道,bind会把bindFun的this指向obj对不对,紧接着使用new操作符构造新实例person,我们会发现之前绑定好this丢失了(实际上已经指向了实例person),这就是为什么name: undefined


根据以上的知识,我们知道:

  • bind返回的bindFun有两种使用方式:
    • 直接使用
    • 当作构造函数来实例化出新的实例对象
  • 两种使用方式的区别
    • 直接使用的时候this永远指向指定的位置(例子中的obj)且无法修改this指向
    • 当作构造函数使用的时候this指向新的实例对象

好了,知道有两种使用方式且知道他们的区别,那我们就要开始完善我们的bind2了:



实现第三步骤:让bindFun有构造函数的功能

  到了这一步,我们只要在bindFun是通过new调用的时候把this指向实例自身,在普通调用的时候把this指向obj就万事大吉了,那我们怎么判断到底是new调用还是普通调用?
  我们知道(不知道的就评论留言提问哈),判断一个对象是否是某个构造函数的实例,我们可以用instanceof。我们用一个小栗子试一下:

function Fn() {
    this.name = 'roger'
};
var person = new Fn();
console.log(person instanceof Fn)  //true

这个例子里面person是由构造函数Fn实例化出来的,所以person instanceof Fn结果为true。那么同理,如果bindFun是通过New调用,那生成的实例使用instanceof判断是可以的。那么,我们判断结果为true,就把新this(指向实例化对象)传入bindFun,如果为false,就按旧执行,使用调用bind时指定的this(obj),我们来试一下:

Function.prototype.bind2 = function (context) {
    if (typeof this !== 'function') { //必须是函数才能调用bind
        throw new Error('Function.prototype.bind - what is trying to be bound is not callable')
    }
    let f = this    //先获取要执行的函数
    let agrs = [...arguments].slice(1) //第一次取参数(调用bind2时传入的参数)
    let bindFun =  function () {
        let agrs2 = [...arguments]  //第二次取参(调用bindFun时传入的参数)
        let Allagrs = agrs.concat(agrs2)    //合并两组参数(要注意一下顺序)
        f.apply(this instanceof f ? this : context, Allagrs)  //判断是new调用还是普通调用,且使用apply修改this的指向
    }
    bindFun.prototype = f.prototype //原型链继承
    return bindFun
}

let obj = {
    name: 'roger'
};
function show(age, address) {
    this.name = this.name
    this.age = age
    this.address = address
    this.profession = '前端开发工程师'
}
show.prototype.hobby = '看电影'

let bindFun = show.bind2(obj, 18);
let person = new bindFun('深圳')
console.log(person)  //{name: undefined, age: 18, address: "深圳", profession: "前端开发工程师"}
console.log(person.hobby)   //看电影
console.log(bindFun.prototype.constructor === show)  //true

这样执行结果就和我们之前使用bind演示的一样了,这里有两个需要注意的地方:

  1. 可能有人会好奇为什么this instanceof f而不是this instanceof bindFun,这是因为我们从始至终要执行的函数都是调用bind的那个函数也就是f,大家要记得这一点。
  2. 有朋友可能会对bindFun.prototype = f.prototype陌生,这里涉及到原型链继承的知识点,修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值。如果不加这个,那结果就会不一样了,因为继承不到绑定函数(show)的属性了,大家可以动手试一试。

如果有朋友对原型链这方面不熟悉的话可以留言哦,我会给你解答,后面也会专门写一篇《原型和原型链》。




其实到这里还有没有结束啊朋友们,还有一个缺陷
  熟悉构造函数和原型的朋友们应该知道,实例可以继承构造函数的实例属性/方法和原型属性/方法,且实例可以随便修改实例属性/方法(独立性),不会影响到其他的实例和构造函数本身,但是我们修改原型属性/方法的时候就会影响到全部实例和构造函数的原型对象,为了大家容易理解,我还是举个栗子:

function Fn() {
    this.name = 'roger'
    this.sayThis = function () {
        console.log(this)
    }
};
Fn.prototype.age = 18

let person1 = new Fn();
let person2 = new Fn();
person1.name = 'lee'
person1.__proto__.age = 20

console.log(person1.name) //lee
console.log(person2.name) //roger

console.log(person1.age) //20
console.log(person2.age) //20

这样就直观很多了吧,那这样就会带来一个问题,实例修改了原型属性/方法。就会影响到绑定函数(show)的 prototype,所以我们需要通过一个空函数来进行中转。

实现第四步骤:最终一步,使用空函数进行中转

Function.prototype.bind2 = function (context) {
    if (typeof this !== 'function') { //必须是函数才能调用bind
        throw new Error('Function.prototype.bind - what is trying to be bound is not callable')
    }
    let f = this    //先获取要执行的函数
    let agrs = [...arguments].slice(1) //第一次取参数(调用bind2时传入的参数)
    let f2 = function(){}   //创建空函数来中转

    let bindFun =  function () {
        let agrs2 = [...arguments]  //第二次取参(调用bindFun时传入的参数)
        let Allagrs = agrs.concat(agrs2)    //合并两组参数(要注意一下顺序)
        f.apply(this instanceof f ? this : context, Allagrs)  //判断是new调用还是普通调用,且使用apply修改this的指向
    }
    f2.prototype = f.prototype
    bindFun.prototype = new f2() //原型链继承
    return bindFun
}

如果是第一次接触这种利用空函数做原型链的中转的小伙伴可能会有点懵,我简单解释一下,实际上就是我们不希望通过new调用bindFun而生成出来的实例能够通过__proto__修改到绑定函数(show)的原型属性/方法,所以在他们的原型链中间放置了一个空白的函数,让原型链多了一层。当然,其实通过__proto__.__proto__还是可以修改的到show的原型的,这是关于原型和原型链的知识,不熟悉的小伙伴可要抓紧时间进修了呀。

总结

总结一下这篇的内容,实现bind的重点就在于以下几点:

  1. 必须是函数才能调用bind
  2. 可以修改函数this指向
  3. 返回一个绑定了指定this的新函数,在下面例子中我们用bindFun表示
  4. 支持传参且支持函数柯里化
  5. 返回的bindFun还能当作构造函数,new出新的实例
  6. 返回的bindFun中的this无法再次修改,使用call/apply也不行

大家看完之后一定要动手自己写,教程只是起到引导作用,看完不等于学到手,看完之后自己动手琢磨着写出来才是真的展握该知识点,大家奥利给。
  终于写完了,真是不容易呀。因为bind的实现相对于call/apply来说会复杂一些,为了让大家能看明白一点我也来来回回做了各种的测试,修修补补写了两天才发出来。我也是技术博客新人,喜欢分享便不辞劳累,不够严谨之处也望大家指出来我好改正,祝大家年年涨薪。

  • 5
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值