JavaScript手撕bind方法?
Posted Crushdada
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JavaScript手撕bind方法?相关的知识,希望对你有一定的参考价值。
首先,它是函数的一个方法,我们需要将其--
1.挂载到Function的原型链上
Function.prototype.mybind =...
//这样,所有继承自Function的函数就能够使用.操作符来访问mybind了!
//PS:因为JS原型式继承
然后,让我们先看看原生JS的bind方法有哪些行为--
2.调用函数时改变this指向
让调用该方法的函数的this指向传入的第一个参数
我们可以借助apply方法实现
Function.prototype.mybind = function (context) {
this.apply(context);
};
let obj = {
name: "Crushdada",
};
let fn = function (params) {
console.log(this.name);
};
fn.mybind(obj); //Crushdada
3.返回一个匿名的绑定函数
注意两点:
- 由于我们返回了一个绑定函数(匿名函数),则在调用时需要在调用语句后面再加一个圆括号
与此同时,由于匿名函数中的this指向window/global,我们需要使用箭头函数或者手动保存一下指向mybind中指向调用者fn的this
- 此处使用箭头函数
Function.prototype.mybind = function (context) { return () => this.apply(context); }; let obj = { name: "Crushdada", }; let fn = function (params) { console.log(this.name); }; fn.mybind(obj)(); //Crushdada
4.支持柯里化传递参数
个人理解:相比“允许传入参数”这种说法,形容为“传递参数”更贴切,bind方法作为一个中间方法,会代收参数后再传递给它返回的匿名绑定函数,其返回一个匿名函数这一点,天然支持柯里化(可能是ES6引入它的初衷之一),因为这样就允许我们在调用bind时传入一部分参数,在调用其绑定函数时再传入剩下的参数。然后它会在接收完第二次传参后再apply执行调用bind的那个方法
实现柯里化的逻辑很简单,仅仅需要在mybind中接收一次参数,然后在绑定函数中接收一次参数,并将二者拼接后一起传给mybind的调用方法使用即可
下面,实现传参&柯里化!
若使用的是普通函数,要处理参数,由于arguments为类数组,slice为Array方法,故先在原型链上调用然后call一下
- 第一个参数为this新的指向,不是属性,故slice掉它
使用箭头函数能极大简化代码
下面我们改亿点点细节!- 使用箭头函数(Array Function)没有arguments属性,因此使用rest运算符替代处理
- 在拼接args和bindArgs时使用扩展运算符替代concat
不得不说ES6引入的rest运算符、扩展运算符在处理参数这一点上提供了极大的便利
Function.prototype.mybind = function (context, ...args) { return (...bindArgs) => { //拼接柯里化的两次传参 let all_args = [...args, ...bindArgs]; //执行调用bind方法的那个函数 let call_fn = this.apply(context, all_args); return call_fn; }; }; let person = { name: "Crushdada", }; let getInfo = function (like, fav) { let info = `${this.name} likes ${like},but his favorite is ${fav}`; return info; }; //anonymous_bind:mybind返回的那个匿名的绑定函数 let anonymous_bind = getInfo.mybind(person, "南瓜子豆腐"); let info = anonymous_bind("皂角仁甜菜"); //执行绑定函数 console.log(info); //Crushdada likes 南瓜子豆腐,but his favorite is 皂角仁甜菜
箭头函数不能作为构造函数!
需要用普通函数重写mybind
写到支持柯里化这一步,bind方法还是可以使用箭头函数实现的,而且比普通函数更加简洁
但是想要继续完善它的的行为,就不能用继续用Arrow Function了,因为箭头函数不能被new!,要是尝试去new它会报错:
anonymous_bind is not a constructor
笔者也是写到这才想起箭头函数这个机制的。那么下面我们需要用普通函数重写mybind
不过也很简单,只需要手动保存一下this即可。就不再贴出改动后的代码了。直接看下一步
5.支持new绑定函数
bind的一个隐式行为:
- 它返回的绑定函数允许被new 关键字调用,但是,实际被作为构造器的是调用bind的那个函数!!!
且new调用时传入的参数照常被传递给调用函数。
逻辑
实现这一步的逻辑也较为简单,我们类比一下和一般调用new时的区别--
- new一个普通函数:按理来说生成的实例对象的构造函数就是那个普通函数
- new一个绑定函数:生成的实例对象的构造函数是调用bind的那个函数
主要需要我们写的逻辑有:
- 判断是否是new调用
让getInfo函数中的this指向--new中创建的实例对象obj
- 就是把getInfo函数里的this换成obj,以使obj获取到其中的属性
- 可以借助apply方法
判断getInfo函数是否返回一个对象,若是,则返回该对象,否则返回new生成的obj
至于为什么这么写,就需要你先弄懂new关键字实现的机制了,我的笔记链接附在文末
下面,实现它!Function.prototype.mybind= function (context, ...args) { let self = this; return function (...bindArgs) { //拼接柯里化的两次传参 let all_args = [...args, ...bindArgs]; // new.target 用来检测是否是被 new 调用 if (new.target !== undefined) { // 让调用mybind的那个函数的this指向new中创建的空对象 var result = self.apply(this, all_args); // 判断调用mybind方法的那个实际的构造函数是否返回对象,没有返回对象就返回new生成的实例对象obj return result instanceof Object ? result : this; } //如果不是 new 就原来的逻辑 //执行调用bind方法的那个函数 let call_fn = self.apply(context, all_args); return call_fn; }; }; let person = { name: "Crushdada", }; let getInfo = function (like, fav) { this.dear = "Bravetata"; let info = `${this.name} likes ${like},but his favorite is ${fav}`; return info; }; //anonymous_bind:mybind返回的那个匿名的绑定函数 let anonymous_bind = getInfo.mybind(person, "南瓜子豆腐"); let obj = new anonymous_bind("皂角仁甜菜"); //执行绑定函数 console.log(obj); //{ dear: \'Bravetata\' } console.log(obj.name); //undefined
解释一下以上代码:
第一个逻辑
- new内部有类似这样一条语句:Con.apply(obj, args)
- 其中Con是new 的那个构造函数,obj是最后要返回的实例对象
- 当我们new上面mybind中return的那个绑定函数时
- Con就是该绑定函数
- 当Con.apply(obj, args)执行,
- 调用绑定函数并将其中的this换成obj
- 然后程序就进入到了该绑定函数中--
- 判断确实是new调用的
- 执行self.apply(this, all_args);
- 这条语句就相当于getInfo.apply(obj, all_args)
- 这样就达成我们的目的了!--让getInfo成为new生成的实例对象的实际构造器
第二个逻辑
- new关键字会判断构造函数本身会不会返回一个对象
- 如果会,则直接返回这个对象当做实例,否则正常是返回那个new生成的obj当做实例对象
- 那么--我们在第一个逻辑里已经调用了实际构造器--getInfo
接下来我们直接判断一下调用的结果,即它是否return一个对象,然后return给new做最终的return即可
此外:可以看到,当new mybind返回的绑定函数时,obj没有获取到person.name属性,为undefined。也就是说--
此时,bind改变this指向的行为会失效
看个栗子,这样清楚一点
var value = 2;
var foo = {
value: 1
};
function bar(name, age) {
this.habit = \'shopping\';
console.log(this.value);
console.log(name);
console.log(age);
}
bar.prototype.friend = \'kevin\';
var bindFoo = bar.bind(foo, \'daisy\');
var obj = new bindFoo(\'18\');
// undefined
// daisy
// 18
console.log(obj.habit);
console.log(obj.friend);
// shopping
// kevin
尽管在全局和 foo 中都声明了 value 值,最后依然返回了 undefind,说明绑定的this 失效了,
这是为什么呢?
如果大家了解 new 的模拟实现,就会知道了--
new是JS模拟面向对象的一个关键字,它的目的之一是实现继承,它要去继承构造函数(类)之中的属性,那么new关键字是怎样去实现的呢?它在内部应用了类似这样一条语句:
Con.apply(obj, args) //Con是new 的那个构造函数
new 关键字会先声明一个空对象obj,然后将构造函数的this指向这个对象
这样做会发生什么--
- 如果构造函数中设置了一些属性,如:this.name = xx;
- 那么就相当于将this换成了obj,变成:obj.name = xx;
- obj就继承到了构造函数的属性!!
- obj就是最后会返回的实例对象
详见:《JS中new操作符做了什么?》--Crushdada\'s Notes
让我们回到为什么this会失效这一问题上
了解完new关键字的相关实现,我们已经得到答案了--
new完绑定函数后,绑定函数内部的this 已经指向了 obj,而obj中没有value这个属性,当然就返回undefined了
6.支持原型链继承
实际上这一步是对绑定函数内重写new方法的一个补充--
因为new方法本来就支持原型链继承
逻辑
那么我们只需要--
让new的实例对象obj的原型指向实际构造器getInfo的prototype即可
Object.setPrototypeOf(this, self.prototype);
规范化/严谨性
可以为mybind方法加上一个判断,调用者必须是一个函数,否则抛出TypeError--
if (typeof this !== \'function\' || Object.prototype.toString.call(this) !== \'[object Function]\') {
throw new TypeError(this + \' must be a function\');
}
一个疑问?
我们模拟实现bind方法,终归是通过apply实现的。而它源码是如何实现的,对于我来说就像一个黑盒。也就是说:不用apply,它是如何实现?
7.究极版本--不借助apply实现
百度的各种版本大都借助apply实现的,不过很幸运在思否找到了答案--JS bind方法如何实现?
答者给出的替代apply的方法很简单:
- 调用bind方法的那个函数--即要改变this指向的那个函数:caller
- 要让caller的this指向:context
那么我们只需要--
将caller作为一个对象方法挂载到context上:context.callerFn = caller
- 上面那句代码中,属性名"callerFn"是自定义的
这样,当执行该句时,相当于context调用了caller函数,那么caller函数中的this自然就指向其调用者context了。
以上,就替代了apply在本例中的核心功能--调用函数同时改变this指向
此外,为提高代码性能,用完callerFn后就删掉它
context.__INTERNAL_SECRETS = func
try {
return context.__INTERNAL_SECRETS(...args)
} finally {
delete context.__INTERNAL_SECRETS
}
将apply替换为以上代码,就得到最终版了
最终版
FFunction.prototype.mybind = function (context, ...args) {
if (
typeof this !== "function" ||
Object.prototype.toString.call(this) !== "[object Function]"
) {
throw new TypeError(this + " must be a function");
}
let self = this; //这里的this和self即:调用mybind的方法--fn()
context.caller2 = self;
return function (...bindArgs) {
let all_args = [...args, ...bindArgs];
//new调用时,this被换成new方法最后要返回的实例对象obj
if (new.target !== undefined) {
try {
this.caller = self;
var result = this.caller(...all_args);
} finally {
delete this.caller;
}
Object.setPrototypeOf(this, self.prototype);
return result instanceof Object ? result : this;
}
//当不是new调用时,this指向global/window(因为匿名函数返回后由全局调用)
try {
var final_res = context.caller2(...all_args);
} finally {
delete context.caller2;
}
return final_res; //调用mybind的那个函数[可能]有返回
};
};
其他知识点:
Array.prototype.slice.call()
- 接收一个字符串或有length属性的对象
该方法能够将有length属性的对象或字符串转换为数组
因此像是arguments对象这样拥有length属性的类数组就可以使用该方法转换为真正的数组
JS中,只有String和Array拥有.slice方法,对象没有。let slice = (arrlike) => Array.prototype.slice.call(arrlike); var b = "123456"; let arr = slice(b); console.log(arr); // ["1", "2", "3", "4", "5", "6"]
arr.slice()方法
返回一个新的数组对象,这一对象是一个由
begin
和end
决定的原数组的浅拷贝(包括begin
,不包括end
)。- 接收的参数--
begin、end
是数组index - 原始数组不会被改变。
const animals = [\'ant\', \'bison\', \'camel\', \'duck\', \'elephant\']; console.log(animals.slice(2)); // expected output: Array ["camel", "duck", "elephant"] console.log(animals.slice(2, 4)); // expected output: Array ["camel", "duck"]
逻辑或“||”
- 接收的参数--
a || b :
- 若运算符前面的值为false,则返回后面的值
若true,返回前面的值
js中逻辑值为false的6种情况
当一个函数拥有形参,但调用时没有传实参时,形参是undefined,会被按false处理
function name(params) {
console.log(params); //undefined
}
name();
console.log(undefined == false); //false
console.log(undefined || "undefined was reated as false");
//undefined was reated as false
会被逻辑或运算符当做false处理的总共6个--
0、null、""、false、undefined 或者 NaN
参考:
javascript深入之bind的模拟实现--掘金
js 实现 bind 的这五层,你在第几层?--蓝色的秋风 | 思否
《JS中new操作符做了什么?》--Crushdada\'s Notes
以上是关于JavaScript手撕bind方法?的主要内容,如果未能解决你的问题,请参考以下文章
手撕JavaScript call apply bind 函数
JavaScript手撕前端面试题:手写Object.create | 手写Function.call | 手写Function.bind
前端开发必备技能 —— 数据结构 && 算法 && 手撕JavaScript/ES6