JavaScript实现手写call/apply/bind的示例代码

还记得之前面试得物的时候,上来就是一道手写bind,当时咱也不知道啥情况,也没准备什么手写的题目,就这样轻轻松松的挂了

现在前端行业那么的卷,面试的时候让你手写的个什么东西是非常常见的。下面是我总结的3道手写题。希望对你有帮助。

call

call的作用是啥

我们首先看一个案例

let foo = {
 value: 1
}

function bar() {
 console.log(this.value)
}

bar.call(foo) //1

可以总结两个点:

  • call改变了bar的this指向,指向了foo
  • bar被执行了 那我们是不是可以理解为是这样的情况呢
let foo = {
 value: 1,
 bar: function() {
 console.log(this.value)
 }
}
foo.bar()

我们可以看到这个时候this就指向了foo,但是多了一个属性,那再把这个属性删掉就是咯。所以我们的思路可以是这样的:

  • 将函数设置成foo的属性
  • 执行这个函数
  • 删除这个函数 暂时可以先写成这样
Function.prototype.myCall = function (context) {
 context.fn = this
 context.fn()
 delete context.fn
}

现在我们再回到最初的案例,然后加上参数

let foo = {
 value: 1
}

function bar(name, age) {
 console.log(this.value)
 console.log(name)
 console.log(age)
}

bar.call(foo, "mick", 18)
// 1
// mick
// 18

那我们就可以把call去除第一个参数,然后剩下的参数在执行的时候添加进去就好了

Function.prototype.myCall = function (context) {
 context.fn = this
 const args = [...arguments].slice(1)
 context.fn(...args)
 delete context.fn
}

我们修改下案例

var value = 1
function bar() {
 console.log(this.value)
}

bar.call(null) // 1

当绑定的this指向为null的时候,则认识指向了window

let foo = {
 value: 1
}

function bar(name, age) {
 return {
 value: this.value,
 name,
 age
 }
}

console.log(bar.call(foo, "mick", 18))
// { value: 1, name: 'mick', age: 18 }

如果函数有返回值,我们实现的call不能仅仅是执行了,也要有返回值。

Function.prototype.myCall = function (context) {
 context = context || window
 context.fn = this
 const args = [...arguments].slice(1)
 const res = context.fn(...args)
 delete context.fn
 return res
}

这样就实现了一个call

总结

  • 将函数设置成要指向的那个this的属性
  • 执行这个函数
  • 删除这个属性
  • 考虑参数问题
  • 考虑this为null的情况
  • 考虑下返回值

apply

apply和call差不多,只是入参不一样,apply的参数是数组

Function.prototype.myApply = function (context, arr) {
 context = context || window
 context.fn = this
 var res
 if (!arr) {
 res = context.fn()
 } else {
 res = context.fn(...arr)
 }
 delete context.fn
 return res
}

bind

MDN上解释的bind为:bind()  方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。 我们可以得出两个点:

  • 返回一个新的函数
  • 可以传入参数

先看下案例:

var foo = {
 value: 1
}

function bar(name, age) {
 console.log(this.value)
 console.log(name)
 console.log(age)
}

var bindFoo = bar.bind(foo, "mick")

bindFoo(18)
// 1
// mick
// 18

可以看到 bind的参数和返回的bindFoo的参数是合并的,而改变this可以利用apply来实现

Function.prototype.myBind = function (context) {
 const self = this
 const args = [...arguments].slice(1)
 return function () {
 const bindArgs = [...arguments].slice()
 return self.apply(context, args.concat(bindArgs))
 }
}

然而bind还有一个特点。在MDN上这样说到:绑定函数自动适应于使用new操作符去构造一个由目标函数创建的新实例。当一个绑定函数是用来构建一个值的,原来提供的 this 就会被忽略。不过提供的参数列表仍然会插入到构造函数调用时的参数列表之前。

所以当执行bind被返回的那个函数被当做构造函数的时候,bind绑定的this值就会失效。 我们看下案例:

var value = 2

var foo = {
 value: 1
}

function bar(name, age) {
 this.hobby = "studying"
 console.log(this.value)
 console.log(name)
 console.log(age)
}

bar.prototype.friend = "randy"

var bindFoo = bar.bind(foo, "mick")

var obj = new bindFoo(18)
// undefined
// mick
// 18

console.log(obj.hobby)
console.log(obj.friend)
// studying
// randy

可以看出this失效了,所以我们要完善一下

Function.prototype.myBind = function (context) {
 const self = this
 const args = [...arguments].slice(1)
 var fBound = function () {
 const bindArgs = [...arguments].slice()
 // 用apply 实现this的绑定
 return self.apply(
 this instanceof fBound ? this : context,
 args.concat(bindArgs)
 )
 }

 fBound.prototype = this.prototype

 return fBound
}

首先加了this instanceof fBound 这个主要是为了判断fBound是不是被当做构造函数使用的,如果是,那么将绑定函数的this指向该实例。fBound.prototype = this.prototype修改fBound的prototype是为了绑定函数的prototype,实例就可以继承绑定函数原型中的值了。这也是为什么new bindFoo的实例能够访问bar原型的属性。

优化

fBound.prototype = this.prototype,当我直接修改fBound的prototype的时候,也会直接修改绑定函数bar的prototype。这时候我们就需要一个空函数来中转:

Function.prototype.myBind = function (context) {
 const self = this
 const args = [...arguments].slice(1)

 var fNOP = function () {}
 var fBound = function () {
 const bindArgs = [...arguments].slice()
 // 用apply 实现this的绑定
 return self.apply(
 this instanceof fNOP ? this : context,
 args.concat(bindArgs)
 )
 }

 fNOP.prototype = this.prototype
 fBound.prototype = new fNOP()

 return fBound
}

总结

  • 执行bind返回一个新的函数
  • bind的参数和返回新的函数的参数会拼接,bind的参数优先级更高
  • 如果返回的函数当做构造函数使用的时候,this会失效
  • 修改原型的值需要一个中转优化
作者:mick

%s 个评论

要回复文章请先登录注册