由于最近的工作比较忙,所以导致自己也很长一段时间没写博客了,另一方面则是暂时不知道有啥可写的,不过在最近我看到了一篇关于手写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