call、apply、bind 实现

1. call 实现

首先我们要知道系统的 call 方法主要实现了什么

  • 执行了函数
  • 改变了 this 的指向

先看系统的call方法

function foo() {
console.log("foo函数被执行", this);
}

function sum(num1, num2) {
console.log("sum函数被执行", this);
return num1 + num2
}

// 1. 传入对象
foo.call({})
// 2. 传入null/undefined
foo.call(null) // this自动替换为指向全局对象
// 3. 传入其他,字符串,数字,布尔值等等
foo.call("abc") // this 指向相应的对象

let res = sum.call("123", 10,20)
console.log(res) // 30

img0328_01.png

接下来我们开始实现自己的call(主要是实现思路,没有把所有的边缘条件考虑完全,但是基本都有)

1.1 让函数执行起来

Function.prototype.mycall = function() {
let fn = this // this就是我们想要执行的函数 foo,把它保存到 fn
// 接着让函数执行起来
fn() // 但是这里是独立函数调用,this指向window
}

foo.mycall() // 这里相当于mycall隐式绑定了foo,因此 mycall中的this就是foo

2.2 显式绑定this

现在我们要绑定我们指定的this

  • 先看第一种:foo.mycall({name: 'hello'}),绑定一个对象
Function.prototype.mycall = function(thisArg) {
let fn = this // this就是我们想要执行的函数 foo,把它保存到 fn
// 接着调用这个函数
// 我们的目的是让函数执行的时候绑定thisArg这个对象执行即thisArg.fn()(隐式绑定)
// 但是现在 thisArg没有 fn 这个属性呀,怎么调用呢
// 所以我们可以给thisArg添加一个属性 fn, 值就是我们想要执行的函数
thisArg.fn = fn
// 然后再调用
thisArg.fn()
// 但是这样我们不就让thisArg多出来一个属性fn了吗?
// 没关系,函数执行完 删掉就好
delete thisArg.fn
}
  • 如果我们传入的不是对象呢?那就不能给thisArg添加属性啦

    foo.mycall("123")

Function.prototype.mycall = function(thisArg) {
let fn = this
// 所以要把 thisArg 转成对象类型(Object构造函数可以将给定的值包装为一个新对象)
thisArg = Object(thisArg)
thisArg.fn = fn
thisArg.fn()
delete thisArg.fn
}
  • 如果传入的是 null / undefined 呢
Function.prototype.mycall = function(thisArg) {
let fn = this
// 如果thisArg传入的是 undefined / null, 应该让它指向全局对象
// 所以我们要做一个判断
thisArg = (thisArg !== null && thisArg !== undefined) ? Object(thisArg) : window
thisArg.fn = fn
thisArg.fn()
delete thisArg.fn
}

2.3 接下来要考虑参数了

// rest运算符
// ...args 会把我们传入的参数列表组合到一个数组 [num1,num2]
// ...args 也可以展开数组, 相当于对数组的一个遍历
Function.prototype.mycall = function(thisArg, ...args) {
let fn = this
thisArg = (thisArg !== null && thisArg !== undefined) ? Object(thisArg) : window
thisArg.fn = fn
// 把参数传入函数,并用 result 接收函数执行完的结果(例子中就是sum函数执行完的结果)
let result = thisArg.fn(...args) // ...[num1, num2] --> num1, num2
delete thisArg.fn
// 最后把结果返回出去
return result
}

到此,基本的call就已经实现了

检验一下叭

foo.mycall({name: 'hello'})
foo.mycall("123")
foo.mycall(undefined)
foo.mycall(null)

sum.mycall({name: "hello"}, 10, 20)
let res1 = sum.mycall(123, 10, 20)
console.log(res1);

image0328_02.png style="zoom:70%;"

2. apply 实现

跟 call 类似,只不过参数的处理有不同

Function.prototype.myapply = function(thisArg, argsArray) {
let fn = this

thisArg = (thisArg !== null && thisArg !== undefined) ? Object(thisArg) : window

thisArg.fn = fn
// 没有传argArray,那么argsArray默认为 undefined,那么就不能使用展开运算符了
// 所以如果没有传入参数, 那么把它初始化为 [], 空数组也是可以展开的 ...[]
argsArray = argsArray || []
let result = thisArg.fn(...argsArray)

delete thisArg.fn
return result
}

3. bind 实现

bind 需要我们返回一个新的函数,并且调用 bind 的时候不需要执行函数

Function.prototype.mybind = function(thisArg, ...args) {

var fn = this

thisArg = (thisArg !== null && thisArg !== undefined) ? Object(thisArg) : window

function newFn() {
thisArg.fn = fn
let result = thisArg.fn(...args)
delete thisArg.fn
// 返回结果
return result
}

// bind 需要返回一个新的函数 你也可以直接 return function() {...}
return newFn
}

大体上也差不多,但我们可以就下面这种情况改进一下

function sum2(num1, num2, num3, num4) {
console.log("sum2函数被执行", this);
return num1 + num2 + num3 + num4
}

let newSum2 = sum2.mybind("abc", 10,20) // 绑定的时候传入了两个参数
console.log(newSum2(30,40)); // 使用新返回的函数的时候再传入剩余的参数

这种情况,我们就需要把两次传入的参数合并起来,再调用

Function.prototype.mybind = function(thisArg, ...args) {
var fn = this

thisArg = (thisArg !== null && thisArg !== undefined) ? Object(thisArg) : window

function newFn(...newArgs) {
// 合并传入的参数
let allArgs = [...args, ...newArgs]

thisArg.fn = fn
let result = thisArg.fn(...allArgs)
delete thisArg.fn
// 返回结果
return result
}
return newFn
}

到此,基本的bind也实现了

最后说明一下,实现的思路是这样,但是方法不唯一的,可能还有一些边边角角没有考虑到的话,可以自己添加进去