首先,它是函数的一个办法,咱们须要将其--
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// 18console.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'); }
最终后果
Function.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; return function (...bindArgs) { let all_args = [...args, ...bindArgs]; if (new.target !== undefined) { var result = self.apply(this, all_args); Object.setPrototypeOf(this, self.prototype); return result instanceof Object ? result : this; } let call_fn = self.apply(context, all_args); return call_fn; };};
其余知识点:
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); //falseconsole.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