定义函数和对象如下:
let obj = {
'a': 'a in obj'
}
window.a = 'a in window'
function fn (msg) {
console.log(this.a, msg)
}
fn('raw')
手写 call
原生的call 方法有两个特征:
- 改变this的指向
- 通过多个参数的形式传递参数
那我们手写myCall方法模拟call,如何实现这两个特征呢?
- 我们知道函数中this指向调用这个函数的对象,那我们可以将需要改变this的函数,暂时挂载到一个对象上,在通过对象.方法的形式调用这个函数,就可以改变函数执行时this指向了
- 传递参数,可以巧妙地使用
剩余运算符 和 展开运算符
…`,不然的话,就需要使用arguments和eval等,还要拼接字符串,挺麻烦的
直接上代码吧:
Function.prototype.myCall = function (...args) {
// 通过剩余运算符拿到 myCall的全部参数,第一个参数是新的this指向,赋值给t
const t = args.shift()
// t 添加一个临时的属性 _fn 为this,这里的this就是fn.myCall(obj) 的 fn
t._fn = this
// 通过t 调用这个 函数,使用展开运算符... 向函数传递参数
t._fn(...args)
// 注意要删除掉这个临时属性
delete t ._fn
}
调用 call 和 myCall
fn.call(obj, 'call') // a in obj call
fn.myCall(obj, 'myCall') // a in obj myCall
发现自己写的myCall方法没毛病
需要注意的是,obj中不能有 _fn属性,所以还需要判断obj中是不是有这个属性,要生成一个obj中没有的属代替’_fn’。这里我就忽略了
手写apply
前面手写了call,apply和call其实是几乎一样的,只是apply以数组的形式传递参数,也就是apply方法的第二个参数应该是个数组(或类数组)
Function.prototype.myApply = function (...args) {
if (! (args[1] instanceof Object)) {
throw new TypeError('CreateListFromArrayLike called on non-object')
}
const t = args[0]
t._fn = this
t._fn(...Array.from(args[1]))
delete t._fn
}
测试一下:
fn.apply(obj, {0: 'apply', length: 1}) // a in obj apply
fn.myApply(obj, {0: 'myApply', length: 1}) // a in obj myApply
fn.apply(obj, ['apply']) // a in obj apply
fn.myApply(obj, ['myApply']) // a in obj myApply
手写bind
bind方法和前面的不一样,它不会立即执行函数,而是返回一个新函数
- 返回一个新函数,这个新函数不管谁调用它,this都指向设定的那个对象
- 实现柯里化,bind的第2个以后的参数为向函数传递的参数,新函数调用时传递的参数与bind方法中传递的参数拼接在一起
实现代码如下:
Function.prototype.myBind = function (...args) {
// 用剩余操作符... 获取myBind的参数,第一个参数时新指定的this
const t = args.shift()
// myBind应该返回一个函数
return (...newArgs) => {
// 返回的函数内部应该调用原来的函数,这里的this由于时箭头函数,在函数定义的地方找上级作用域,也就是myBind 内部的this,就是 xxx.myBind 的xxx那个函数
// myBind的参数args 和 新生成的函数调用时候传递的参数应该concat到一起
this.apply(t, args.concat(newArgs))
// 如果这里使用刚刚自己手写的myApply,那就完全摆脱了对原生方法的依赖了哦
// this.myApply(t, args.concat(newArgs))
}
}
测试一下:
let fn1 = fn.bind(obj)
fn1('bind') // a in obj bind
let fn2 = fn.myBind(obj)
fn2('myBind') // a in obj myBind
let obj1 = {
a: 'hhh',
fn1: fn1,
fn2: fn2
}
obj1.fn1('bind fn called by obj1') // a in obj bind fn called by obj1
obj1.fn2('myBind fn called by obj1') // a in obj myBind fn called by obj1
// 这里可以看到即使obj1.fn2() 也不会改变this指向为obj1 因为在myBind中(myApply中)函数是直接被t调用的,this的指向永远是离自己最近一层的对象--t
测试通过
总结:
- 手写apply和call:通过为对象添加临时方法,并使用对象.方法调用,实现this的改变;apply是数组传参,call是多个参数传递,通过…运算符获取和传递参数
- 手写bind,返回一个新函数,新函数内部通过apply或call改变this指向并调用原函数;通过myBind的参数和新函数的参数concat到一起,新函数内部调用原函数时传递所有的参数,从而实现柯里化