概念:bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。 bind的几个特性如下
- 返回一个新的函数
- 分段接收参数
- 改变this指向
- 返回的新函数可以使用new操作符
接下来我们一步一步来实现这几个特性
返回新函数
调用bind方法会返回一个新的函数,但是执行的代码依旧是原函数内部代码,只不过修改了this指向,我们可以写个栗子验证一下
function demo(){
console.log('hello')
}
var child = demo.bind()
child() // hello
demo.bind()返回一个新的函数child,执行child后执行demo内部代码输出hello,所以验证无误,让我们开始实现这一步
Function.prototype.myBind = function(){
var _t = this
return function(){
_t()
}
}
分析一下上面的代码,第一步先获取this是为了获取原函数,然后return出一个新函数,函数内部执行原函数逻辑,把原来demo修改一下
function demo(){
console.log('hello')
}
var child = demo.myBind()
child() // hello
执行child后输出hello,没毛病。接下来看看怎么分段接收参数。
分段接收参数
废话不多说,拓展一下原来的栗子
function demo(name, year){
console.log(`hello ${name} ${year}`)
}
var child = demo.bind(null, 'world')
child(2021) // hello world 2021
第一个参数是新函数的this,所以暂时先跳过,后面再说,接着看这个例子,bind的时候第二个参数开始是参数列表,可以接收n个参数,执行新函数的时候也可以接收n参数列表,所以我们需要接收2次入参,并且按顺序拼接起来,传入新函数内
Function.prototype.myBind = function(){
var _t = this
var outArgs = Array.prototype.slice.call(arguments, 1) // 获取第二个参数开始的全部参数
return function(){
var innerArgs = Array.prototype.slice.call(arguments) // 获取全部参数
_t.apply(null, outArgs.concat(innerArgs)) // 参数是数组,所以用apply调用
}
}
把原来栗子里的bind改成myBind发现输出一致,所以这一步完工!进入下一步修改this指向
this指向
说到修改this指向,第一时间肯定想到call,apply,所以我们在原来实现上显示的接收一个参数context(也就是this),然后在执行apply的时候使用,所以这里也就不多说了,不懂的可以自己查一下,直接上修改后的实现
Function.prototype.myBind = function(context){
var _t = this
var outArgs = Array.prototype.slice.call(arguments, 1)
return function(){
var innerArgs = Array.prototype.slice.call(arguments)
_t.apply(context, outArgs.concat(innerArgs))
}
}
再写个栗子验证一下
var obj = {
month : 11
}
function demo(name, year){
console.log(`hello ${name} ${year} ${this.month}`)
}
var child = demo.myBind(obj, 'world')
child(2021) // hello world 2021 11
demo中的this指向myBind里的第一个参数obj,所以this.month等于11,输出正确! 接下来到最难理解的new操作符
new操作符
使用new操作符会有以下2个特殊的情况
- bind第一个参数this失效,this指向实例
- 继承原函数的原型链
写个栗子验证一下这2种情况
var obj = {
name: 'ts'
}
function demo(){
this.age = 100
console.log(this.name)
}
demo.prototype.sayAge = function(){
console.log(this.age)
}
var child = demo.bind(obj)
var newChild = new child() // undefined
newChild.sayAge() // 100
从log来看没毛病。所以我们来实现吧~ 既然要让this失效,那么我们就需要知道什么时候使用new,什么时候是普通调用。可以根据new的特性,this指向实例来判断,那么怎么知道this指向的是实例呢? 先把实现写上再结合上面那个栗子一步步解释
Function.prototype.myBind = function(context){
var _t = this
var outArgs = Array.prototype.slice.call(arguments, 1)
var fn = function(){
var innerArgs = Array.prototype.slice.call(arguments)
_t.apply(this instanceof fn ? this : context, outArgs.concat(innerArgs))
}
fn.prototype = this.prototype // 实现原型继承
return fn
}
步解如下
- 函数fn内部this等于newChild(实例)
- 实例的原型是demo.prototype
- 函数fn外部的this指向demo
- 函数fn外部执行了fn.prototype = this.prototype(demo.prototype)
- 所以this instanceof fn可以理解为newChild instanceof demo.prototype
- 答案为true,那么就是new操作,false就是普通调用(普通调用this指向window)
这部分不理解的可以看我上一篇文章喔 然后老规矩,把上面的栗子里的bind改为myBind验证代码,log和bind一致,完工!
优化
之前使用fn.prototype = this.prototype的方式实现继承,多少有点粗糙,可能会引发一些问题,比如实例原型新增属性或者方法后,demo的原型也受到了修改
function demo(){
this.age = 100
}
demo.prototype.sayAge = function(){
console.log(this.age)
}
var child = demo.myBind()
var newChild = new child()
newChild.sayAge()
// 原型新增方法
newChild.__proto__.sayHello = function(){
console.log('hello')
}
newChild.sayHello() // hello
// demo原型也存在这个方法
demo.prototype.sayHello() // hello
为了解决这种情况,我们可以使用原型式继承
Function.prototype.myBind = function(context){
var _t = this
var outArgs = Array.prototype.slice.call(arguments, 1)
var Fpop = function() {} // 中转函数
var fn = function(){
var innerArgs = Array.prototype.slice.call(arguments)
_t.apply(this instanceof Fpop ? this : context, outArgs.concat(innerArgs))
}
Fpop.prototype = this.prototype // 实现原型继承
fn.prototype = new Fpop()
return fn
}
收工!!! 原文链接:github.com/mqyqingfeng…