手写call、apply、bind、new

649 阅读4分钟

call、apply、bind的使用方法及区别

作用:这三个函数的作用都是用来改变this的指向

call使用方法

fn.call(thisArg, arg1, arg2, arg3 ...)

function fn1() {
    console.log(this)
}
const obj = { a: 1 }
fn1.call(obj, 1, 2, 3, 4) // {a: 1}

apply的用法

fn.apply(thisArg, [arg1, arg2, arg3...])

function fn1() {
    console.log(this)
}
const obj = { a: 1 }
fn1.apply(obj, [1, 2, 3, 4]) // {a: 1}

bind的用法

fn.bind(thisArg, arg1, arg2, arg3)

function fn1() {
    console.log(this)
}
const obj = { a: 1 }
fn1.bind(obj, 1, 2, 3, 4) // 不会打印任何东西。

乍一看,三者的用法几乎一致,需要注意的是bind函数,调用之后并没有执行fn1

先看call和apply, 它们仅有的区别是传参的不同,call接收的是参数逐一传入,apply接收的是一个参数组成的数组

接下来,来实现自己的call和apply函数,通过用法发现需要实现两点:

  • 改变执行函数的this指向为第一个参数
  • 执行原函数

第二点好实现,来看第一点,通常情况下,当函数作为对象的方法调用时,this就指向这个对象,可以通过这个特点来实现自己的call函数

实现call

// 因为需要所有函数都可以调用,所以需要写在Function的原型上
Function.prototype.myCall(context) {
	// 判断context是否存在,不存在设置为window
    context = context ? Object(context) : window
    // 处理参数
    const args = [...arguments].slice(1)
    // 要将this指向改为context,需要用context来调用
    context.fn = this // 这里的this是原函数
    const result = context.fn(...args) // 执行原函数,此时因为是context调用,因此函数中的this指向了context
    delete context.fn
    return result
}

实现apply

实现自己的apply函数只需要修改传参方式即可

// 因为需要所有函数都可以调用,所以需要写在Function的原型上
Function.prototype.myCall(context) {
	// 判断context是否存在,不存在设置为window
    context = context ? Object(context) : window
    // 要将this指向改为context,需要用context来调用
    context.fn = this // 这里的this是原函数
    
    let result
    // 处理参数
   if (arguments[1]) {
   	result = context.fn(...arguments[1])
   } else {
   	result = context.fn()
   }
    delete context.fn
    return result
}

实现bind

接下来实现bind函数,bind与前两者较大不同是它创建了一个新的函数,并且它第一个参数被指定为这个新函数的this

  • 返回一个新函数
  • 第一个参数被指定为新函数的this
  • 其余参数被当作新函数的参数
// 第一版
Function.prototype.myBind(context) {
	const args = [...arguments].slice(1)
	const that = this
	return function() {
    	that.apply(context, args.concat(...arguments))
    }
}

到这里已经实现了基本的bind功能,但是根据MDN对bind的介绍:

调用绑定函数时作为 this 参数传递给目标函数的值。 如果使用new运算符构造绑定函数,则忽略该值。当使用 bind 在 setTimeout 中创建一个函数(作为回调提供)时,作为 thisArg 传递的任何原始值都将转换为 object。如果 bind 函数的参数列表为空,或者thisArg是null或undefined,执行作用域的 this 将被视为新函数的 thisArg。 通过new运算符操作bind生成的函数时,bind绑定的this会失效,此时的this会指向new生成的实例 ,接下来了解一下new运算符

new的用法

function Animal(type) {
	this.type = type
    this.age = age
}
const animal1 = new Animal('猫', 2)
const animal2 = new Animal('狗', 2)

当执行new Animal()的时候,做了以下几件事情

  • 创建一个空对象
  • 改变this指向到这个空对象,并且让这个对象能访问到构造函数原型上的属性
  • 执行构造函数,此时的this指向了新创建的对象

模拟生成new函数

function _new() {
	// 创建一个新对象
    let obj = {}
    // 获取传入的构造函数
    let constructor = [].shift.call(arguments)
    // 让这个对象能访问到构造函数原型上的属性
    obj.__proto__ = constructor.prototype
    // 改变this指向,并执行构造函数
    constructor.apply(obj, aruguments) // 这里的arguments已经经过截取处理
    // 返回创建的实例对象
    return obj
}

回到上面的问题,我们来改造一下

// 第二版
Function.prototype.myBind(context) {
	const args = [...arguments].slice(1)
	const that = this
	return function resFn () {
    	// 判断这里面的this是否是构造函数的实例,如果是说明是用了new运算符
    	that.apply(this instanceof resFn ? this : context , args.concat(...arguments))
    }
}

到这里解决了this绑定的问题,this指向了new出来的实例,但是发现这个实例和原函数已经没什么关系了,也就是访问不到原函数原型上的属性,继续改造

// 第三版
Function.prototype.myBind(context) {
	const args = [...arguments].slice(1)
	const that = this
    const Fn = new Function() // 为了避免直接修改到原型,采用中转函数
	function resFn () {
    	// 判断这里面的this是否是构造函数的实例,如果是说明是用了new运算符
    	that.apply(this instanceof resFn ? this : context , args.concat(...arguments))
    }
    Fn.prototype = this.prototype
    resFn.prototype = new Fn()
    return resFn
}

总结

  • call、apply、bind的作用都是用来改变this指向
  • call和apply的区别在于传参方式不同,call的参数是每一项依次传入,apply是作为数组整体传入
  • 调用call和apply会执行函数,bind会返回一个新的函数
  • 关键思路在于“通过对象调用函数,this就指向了这个对象”