手写bind方法

976 阅读4分钟

概念:bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。 bind的几个特性如下

  • 返回一个新的函数
  • 分段接收参数
  • 改变this指向
  • 返回的新函数可以使用new操作符

接下来我们一步一步来实现这几个特性

返回新函数

调用bind方法会返回一个新的函数,但是执行的代码依旧是原函数内部代码,只不过修改了this指向,我们可以写个栗子验证一下

function demo(){
    console.log('hello')
}
var child = demo.bind()
child() // hello

demo.bind()返回一个新的函数child,执行child后执行demo内部代码输出hello,所以验证无误,让我们开始实现这一步

Function.prototype.myBind = function(){
    var _t = this
    return function(){
	_t()  	
    }
}

分析一下上面的代码,第一步先获取this是为了获取原函数,然后return出一个新函数,函数内部执行原函数逻辑,把原来demo修改一下

function demo(){
    console.log('hello')
}
var child = demo.myBind()
child() // hello

执行child后输出hello,没毛病。接下来看看怎么分段接收参数。

分段接收参数

废话不多说,拓展一下原来的栗子

function demo(name, year){
    console.log(`hello ${name} ${year}`)
}
var child = demo.bind(null, 'world')
child(2021) // hello world 2021

第一个参数是新函数的this,所以暂时先跳过,后面再说,接着看这个例子,bind的时候第二个参数开始是参数列表,可以接收n个参数,执行新函数的时候也可以接收n参数列表,所以我们需要接收2次入参,并且按顺序拼接起来,传入新函数内

Function.prototype.myBind = function(){
    var _t = this
    var outArgs = Array.prototype.slice.call(arguments, 1) // 获取第二个参数开始的全部参数
    return function(){
        var innerArgs = Array.prototype.slice.call(arguments) // 获取全部参数
        _t.apply(null, outArgs.concat(innerArgs)) // 参数是数组,所以用apply调用  	
    }
}

把原来栗子里的bind改成myBind发现输出一致,所以这一步完工!进入下一步修改this指向

this指向

说到修改this指向,第一时间肯定想到call,apply,所以我们在原来实现上显示的接收一个参数context(也就是this),然后在执行apply的时候使用,所以这里也就不多说了,不懂的可以自己查一下,直接上修改后的实现

Function.prototype.myBind = function(context){
    var _t = this
    var outArgs = Array.prototype.slice.call(arguments, 1)
    return function(){
        var innerArgs = Array.prototype.slice.call(arguments)
        _t.apply(context, outArgs.concat(innerArgs)) 	
    }
}

再写个栗子验证一下

var obj = {
    month : 11
}
function demo(name, year){
    console.log(`hello ${name} ${year} ${this.month}`)
}
var child = demo.myBind(obj, 'world')
child(2021) // hello world 2021 11

demo中的this指向myBind里的第一个参数obj,所以this.month等于11,输出正确! 接下来到最难理解的new操作符

new操作符

使用new操作符会有以下2个特殊的情况

  • bind第一个参数this失效,this指向实例
  • 继承原函数的原型链

写个栗子验证一下这2种情况

var obj = {
    name: 'ts'
}
function demo(){
    this.age = 100
    console.log(this.name)
}
demo.prototype.sayAge = function(){
    console.log(this.age)
}
var child = demo.bind(obj)
var newChild = new child() // undefined
newChild.sayAge() // 100

从log来看没毛病。所以我们来实现吧~ 既然要让this失效,那么我们就需要知道什么时候使用new,什么时候是普通调用。可以根据new的特性,this指向实例来判断,那么怎么知道this指向的是实例呢? 先把实现写上再结合上面那个栗子一步步解释

Function.prototype.myBind = function(context){
    var _t = this
    var outArgs = Array.prototype.slice.call(arguments, 1)
    var fn = function(){
        var innerArgs = Array.prototype.slice.call(arguments)
        _t.apply(this instanceof fn ? this : context, outArgs.concat(innerArgs))
    }
    fn.prototype = this.prototype // 实现原型继承
    return fn
}

步解如下

  1. 函数fn内部this等于newChild(实例)
  2. 实例的原型是demo.prototype
  3. 函数fn外部的this指向demo
  4. 函数fn外部执行了fn.prototype = this.prototype(demo.prototype)
  5. 所以this instanceof fn可以理解为newChild instanceof demo.prototype
  6. 答案为true,那么就是new操作,false就是普通调用(普通调用this指向window)

这部分不理解的可以看我上一篇文章喔 然后老规矩,把上面的栗子里的bind改为myBind验证代码,log和bind一致,完工!

优化

之前使用fn.prototype = this.prototype的方式实现继承,多少有点粗糙,可能会引发一些问题,比如实例原型新增属性或者方法后,demo的原型也受到了修改

function demo(){
    this.age = 100
}
demo.prototype.sayAge = function(){
    console.log(this.age)
}
var child = demo.myBind()
var newChild = new child()
newChild.sayAge()
// 原型新增方法
newChild.__proto__.sayHello = function(){
    console.log('hello')
}
newChild.sayHello() // hello
// demo原型也存在这个方法
demo.prototype.sayHello() // hello

为了解决这种情况,我们可以使用原型式继承

Function.prototype.myBind = function(context){
    var _t = this
    var outArgs = Array.prototype.slice.call(arguments, 1)
    var Fpop = function() {} // 中转函数
    var fn = function(){
        var innerArgs = Array.prototype.slice.call(arguments)
        _t.apply(this instanceof Fpop ? this : context, outArgs.concat(innerArgs))
    }
    Fpop.prototype = this.prototype // 实现原型继承
    fn.prototype = new Fpop()
    return fn
}

收工!!! 原文链接:github.com/mqyqingfeng…