bind的那些事

最近面头条的时候,要求自己手动实现一个bind函数,然后又问了bind还能干嘛。这里就围绕这两个好好写一下。

首先bind的文档说明: (链接:传送门

bind()方法创建一个新的函数, 当被调用时,将其this关键字设置为提供的值,在调用新函数时,在任何提供之前提供一个给定的参数序列。
fun.bind(thisArg[, arg1[, arg2[, ...]]])
参数

  • thisArg
    当绑定函数被调用时,该参数会作为原函数运行时的 this 指向。当使用new 操作符调用绑定函数时,该参数无效。
  • arg1, arg2, ...
    当绑定函数被调用时,这些参数将置于实参之前传递给被绑定的方法。
  • 返回值
    返回由指定的this值和初始化参数改造的原函数拷贝

bind() 函数会创建(返回)一个新函数(称为绑定函数),新函数与被调函数(绑定函数的目标函数)具有相同的函数体(在 ECMAScript 5 规范中内置的call属性)。当新函数被调用时 this 值绑定到 bind() 的第一个参数,该参数不能被重写。绑定函数被调用时,bind() 也接受预设的参数提供给原函数。(注意这句话是重点,也就是说bind支持了这个功能)

一般新手分不清于call,apply的关系,call/apply一般会伴随这函数的调用的,而bind只是返回了带新this的函数,将在下次调用。一般用法是调用bind后赋给一个变量,支持调用的时候继续传参。

我们一般快速实现一个bind:

/*

函数体内的this,就是需要绑定this的实例函数,或者说是原函数。最后我们使用apply来进行参数(context)绑定,并返回。
同时,将第一个参数(context)以外的其他参数,作为提供给原函数的预设参数,这也是基本的柯里化基础。(应该是偏函数)

*/
Function.prototype.bind =  Function.prototype.bind || function(context) {
    var self = this;
    arg = Array.prototype.slice.call(arguments,1);
    return function() {
        return self.apply(context,arr);
    }
}

但是这个实现有个问题,我们将参数限定了arguments.slice(1),我们返回的绑定函数中,如果想实现预设传参,上个代码就不能满足了。
eg:

function sum(a, b) {
    console.log(a + b)
}

var sum2 = sum.bind(null,2);   // 固定参数a, 值为2
sum2(4)                        // 传入参数b, 值为4, 结果为6 重在我们在调用函数的时候可以预设传参

那么我们将上个bind的实现更完善一下:

Function.prototype.bind =  Function.prototype.bind || function(context) {
    var self = this;
    var args = Array.prototype.slice.call(arguments,1);
    return function() {
        var innerArgs = Array.prototype.slice.call(arguments);
        var FinalArgs = args.concat(innerArgs);
        return self.apply(context,FinalArgs);
    }
}

这样就是实现了偏函数功能,在一些资料里是说“柯里化”,我觉得还是有点区别的,共同特点是实现了参数复用。

关于柯里化和偏函数举个小例子就知道了:
假设有一个Add(x,y,z)函数,接收x,y,z三个参数,返回x+y+z

  • 偏函数

AddBySeven =Otherbind(Add, 7);
AddBySeven(5, 10); // returns 22;
这是偏函数,固定了你函数的某一个或几个参数,返回一个新的函数,接收剩下的参数, 参数个数可能是1个,也可能是2个,甚至更多。

  • 柯里化:把一个有n个参数的函数变成n个只有1个参数的函数

curryAdd = Curry(Add);
AddBySeven = curryAdd(7);
AddBySeven(5)(10); // returns 22
// curryAdd(7)(5)(10)

Add = (x, y, z) => x + y + z
变成了CurryAdd = x => y => z => x + y + z

很多资料有的叫柯里化有的叫偏函数,这点我觉得还是读者自己判断把。

到这里可能大家觉得上面的实现已经完美了,但是JS的坑是补不完的,问题又来了!
看过文档的就知道,在文档上介绍bind时还说了这点:

一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器。提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。

那么如果bind返回的函数当做构造函数调用的时候,我们就需要在内部重新构造原型链了。所以更兼容的写法来了:

Function.prototype.bind =  Function.prototype.bind || function(context) {
	if(typeof this !== 'function') {
        throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
    }
    var self = this;
    var args = Array.prototype.slice.call(arguments,1);
    var Fun = {};
    Fun.prototype = this.prototype;//继承原来函数
    var Comb = function(){
        var innerArgs = Array.prototype.slice.call(arguments);
        var FinalArgs = args.concat(innerArgs);
        return self.apply(this instanceof Fun ? this : context || this ,FinalArgs);
    }
    Comb.prototype = new Fun();
    return Comb;
    }

这里的点:

  1. 第一个this是第一次调用bind的函数,也必须是函数,所以在之前就做了一个容错判断。

  2. 如果最后我们是以new 调用bind返回的函数,即当做构造函数调用,那么这里的this就是Comb的实例,这时候因为Fun继承了之前调用的函数,Comb又new了Fun,Comb即是Fun的派生类,因此 this instanceof fNOP === true,这时候无视 bind 效果,因此 this 该是什么还是什么。模仿了原本bind的feature,如果这个条件判断失败,则使用context 去替换 this。如果context没传,那么this该是什么就是什么。

但这里再想想面试官当时的的问题,"bind还能干嘛",其实这个bind就是改变上下文还能干嘛,其实面试管的意思是利用bind的这个feature可以干嘛

把bind本身的作用讲讲,在把上面bind本身的偏函数功能(允许第一次传参不完全,后面调用可以继续传参),自己实现的偏函数,作为new 构造函数来调用这些讲讲,就够了。

最后再加一个bind的小功能把,平常我们转换伪数组,通常是使用:

var slice = Array.prototype.slice;

// ...

slice.call(arguments);//arguments是一个伪数组

如果我们用bind把对象参数绑定到call上返回给slice,每次就不用调用call了,而且还不影响原函数的this:

var combSlice = Array.prototype.slice;
var slice = Function.prototype.call.bind(combSlice);

// ...

slice(arguments);//slice是一个新函数

好了,bind的这些事到此结束,欢迎在下方交流评论。

posted @ 2018-03-27 23:17  Lawliet__zmz  阅读(166)  评论(0编辑  收藏  举报