JS之手写bind原理

由于最近的工作比较忙,所以导致自己也很长一段时间没写博客了,另一方面则是暂时不知道有啥可写的,不过在最近我看到了一篇关于手写bind原理的文章,和自己之前简单手写的bind完全不是一个level,想着一方面这也是面试经常会问到的一个问题,于是我深挖了一下这部分的知识点,带着大家一起学习一下:

不知道大家一开始听到手写bind函数是不是和我一样的不屑,这不是有手就行(doge)?然后直接写了类似下面的代码:

//大致步骤基本是:
//1、在原型函数上定义一个bind函数,并接受一个要绑定的对象参数
//2、通过apply将函数绑定在传过来的对象参数上
//3、返回这个绑定后的新函数
function bind(fn, obj, ...arr) {
	return fn.apply(obj, arr)
}

如果只是普通的面试这样基本没问题了,但是各位毕竟作为一名优秀的开发人员,这样还远远不够,因为我们还要考虑原型、函数柯里化和new方法,所以带着这几个问题继续将我们写的代码进行优化:

1、绑定在原型上的方法
众所周知,我们在调用某个内置的api时,例如xxx.bind,由于 function xxx 的原型链 指向的是 Function.prototype , 因此我们在调用 xxx.bind 的时候,调用的是 Function.prototype 上的方法,所以我们直接在函数原型链上构建一个我们的_bind方法,Function.prototype._bind = function() {}

2、改变this的指向
这可以说是 bind 最核心的特性了,就是改变 this 的指向,并且返回一个函数。而改变 this , 我们可以通过已知的 apply 和 call 来实现,这里我们就暂且使用 apply 来进行模拟。首先通过 self 来保存当前 this,也就是传入的函数。因为我们知道 this 具有 隐式绑定的规则(摘自 《你不知道的JavaScript(上)》2.2.2)

Function.prototype._bind = function(thisObj) {
//这里的this指向的就是调用_bind的myname函数
   const self = this
   return function() { 
       self.apply(thisObj)
    }
}

var obj = {a:1}
function myname() {
  console.log(this.a)	//输出a的值为1
}
myname._bind(obj)()	

3、支持柯里化
首先我们要清除函数柯里化的作用有:
① 接收比较固定的参数,其他的参数由返回的函数接收使用,提高参数的复用能力,提高函数的适用性。
② 延迟执行。(用于被要求求值的时候,再一次性求值)
为了帮助新手便于理解什么是柯里化,这里我举一个具体的例子:

function fn(x) {
	return function (y) {
		return x + y
	}
}
var fn1 = fn(1)
fn1(2) 

从上面的这个例子不难发现,柯里化使用了闭包,当我们执行 fn1 的时候,函数内使用了外层函数的 x, 从而形成了闭包。
而我们的 bind 函数也是类似,我们通过先获取当前外部函数的 arguments ,并且去除了绑定的对象,保存成变量 args,最后 return 的方法,再一次获取当前函数传进来的 arguments参数, 最终用 finalArgs 进行了一次合并。(可能看这段话有点难以理解,那看完下面代码你便会恍然大悟)

Function.prototype._bind = function(thisObj) {
   const self = this
    // 将第一个绑定的对象参数去除,留下其他参数
    const args = [...arguments].slice(1)
    return function (){
        // 考虑二次调用继续传参的情况
        const finalArgs = [...args, ...arguments]
        self.apply(thisObj, finalArgs)
    }
}

 var obj = { i: 1} 
 function myFun(a, b, c) {
     console.log(this.i + a + b + c)
 }
 var myFun1 = myFun._bind(obj, 1, 2)
 myFun1(3)

4、考虑new的调用
我们的方法,通过 bind 绑定之后,依然是可以通过 new 来进行实例化的, new 的优先级会高于 bind(摘自 《你不知道的JavaScript(上)》2.3 优先级)。根据书中提出的关于判断this总结的几条规则是:
① 函数是否在new中调用(new绑定)?如果是的话this绑定的是新创建的对象
② 函数是否通过call/apply绑定 ( 显示绑定 ) ? 如果是的话this绑定的就是指定对象
③ 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话this绑定的是那个上下文对象
④ 如果都不是的话,使用默认绑定,严格模式下绑定到undefined,否则绑定到全局对象下(即window)
举个例子帮大家理解一下第一点:

function foo(something){
     this.a = something
 }
 var obj1 = {}
 var bar = foo.bind(obj1)
 bar(2)
 console.log(obj1.a);    //2

 // 我们会发现new bar(3)并没有像我们预计的那样把obj1.a修改为3,相反new修改了硬绑定到obj1的this指向
 // 因为使用了new绑定,我们得到了一个名字为baz的新对象,并且它的值为3
 var baz = new bar(3)
 console.log(obj1.a);    //2
 console.log(baz.a);     //3

理解以后我们用原生的bind和我们的_bind来做对比。

原生bind:

var obj = { i: 1}
function myFun(a, b, c) {
    // 此处用new方法,this指向的是当前函数 myFun,i的值为undefined
    console.log(this.i + a + b + c)
}
var myFun1 = myFun.bind(obj, 1, 2)
new myFun1(3); // NAN

_bind:

var obj = { i: 1}
function myFun(a, b, c) {
    console.log(this.i + a + b + c)
}
// 此处调用_bind方法,this指向的是 obj,i的值为1
var myFun1 = myFun._bind(obj, 1, 2)
new myFun1(3);	//7

所以我们对_bind函数进行修改,判断是否为new调用的,这里用到new.target,正好是用来检测构造方法是否是通过 new 运算符来被调用的。所以我们对_bind函数进行改造,改造后的_bind如下:

Function.prototype._bind = function(thisObj) {
    const self = this
    const args = [...arguments].slice(1)
    return function () {
        const finalArgs = [...args, ...arguments];
        //判断是否为new调用,如果是的话,则不能让构造函数成功绑定上thisObj,所以这里绑定的this是一个空的对象,this = {}
        //则result的结果是undefined,最终加出来的结果也就是NaN
        if(new.target !== undefined) {
            var result = self.apply(this, finalArgs)
            if(result instanceof Object) {return reuslt}
            return this
        }else{
            self.apply(thisObj, finalArgs)
        }
    }
}

5、 保留函数原型

Function.prototype._bind = function (thisObj) {
  // 判断是否为函数调用
if (typeof this !== 'function' || Object.prototype.toString.call(this) !== '[object Function]') {
   throw new TypeError(this + ' must be a function')
 }
   const self = this
   const args = [...arguments].slice(1)
   var bound = function () {
       var finalArgs = [...args, ...arguments]
       // new.target 用来检测是否是被 new 调用
       if (new.target !== undefined) {
           // 说明是用new来调用的
           var result = self.apply(this, finalArgs)
           if (result instanceof Object) {return result}
           return this; 
       } else {
           return self.apply(thisArg, finalArgs)
        } 
   }
   if (self.prototype) {
       // 为什么使用了 Object.create? 因为我们要防止,bound.prototype 的修改而导致self.prototype 被修改。
       // 不要写成 bound.prototype = self.prototype; 这样可能会导致原函数的原型被修改。 
       bound.prototype = Object.create(self.prototype); 
       bound.prototype.constructor = self; 
   }
   return bound
}

参考文章:https://baijiahao.baidu.com/s?id=1700147115251557647&wfr=spider&for=pc
以上就是一个比较完整的 bind 实现了,如果你想了解更多细节的实践,可以查看(也是 MDN 推荐的)
https://github.com/Raynos/function-bind

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值