bind、call、apply的原理和使用场景

141 阅读3分钟

bind、call、apply的使用

call:立即执行,可传入个参数,第一个参数为需绑定的this上下文,后面则为需传入的参数 call(this, arg1, arg2, ....)
apply:立即执行,传入两个参数,第一个参数为需绑定的this上下文,第二个参数为需传入的参数组 apply(this, arrArgs)
bind: 不立即执行函数,传入参数同call方法 bind(this, arg1, arg2, ...)

bind

使用场景: 不调用函数,又想改变函数内部this指向,比如定时器之类的问题

// 原理
Function.prototype.myBind = function(context = globalThis, ...args) {
    const fn = this;
    const newFunc = function() {
        const newArgs = args.concat(...arguments)
        if (this instanceof newFunc) {
            // new 调用
            fn.apply(this, newArgs);
        } else {
            // 普通函数调用
            fn.apply(context, newArgs);
        }

        // fn.apply(this instanceOf newFunc ? this: context, newArgs)
    }
    newFunc.prototype = Object.create(fn.prototype)
    return newFunc
}

// 常见应用场景
setTimeout(function() {
    console.log("aa")
}.bind(this), 2000)

Tip: 多次绑定bind()是无效的。更深层次的原因,bind()的实现相当于使用函数在内部包了一个call / apply,第二次bind()相当于再包住第一次bind(),故第二次及以后的bind是无法生效的
第一次 bind 绑定的对象是固定的,也就是后面通过 bind 或者 call 再次绑定的时候,就无法修改这个 this 了

call

使用场景:主要用于继承
用来代替另一个对象调用一个方法(本身无该方法,才选择调用),call方法可以将一个函数的上下文从初始的对象转成obj指向的新对象,如果没有提供obj对象,那么Global对象被指做obj

function Product(name, price) {
	this.name = name
	this.price = price
}

function Food(name, price) {
	// 调用Product的call方法并将当前作用域对象this传入替换掉Product内部的this作用域对象
	Product.call(this, name, price)
	this.category = 'food'
}

let tempFood = new Food('cheese', 5)

Function.prototype.myCall = function(context = globalThis, ...args) {
	// 关键步骤,在context上调用方法,触发this绑定context,使用Symbol防止原有属性的覆盖
    const key = Symbol("key");
    // 绑定调用函数(.call之前的方法即this,前面提到过调用call方法会调用一遍自身,所以要存下来)
    context[key] = this;
    // 执行调用函数,记录拿取返回值
    const res = context[key](...args);
    // 销毁调用函数,以免作用域污染
    delete context[key];
    return res;
}

// 常见应用场景
// 继承
function A(name, sex) {
    this.name = name;
    this.sex = sex;
}

function B() {
    A.myCall(this, "jack", "nv")
    this.age = 12
}

B.prototype = Object.create(A.prototype)
B.prototype.constructor = B
let b = new B();
console.log(b.name, b.sex)

// 验证对象类型
Object.prototype.toString.call(obj)

// 类数组对象使用数组方法
Array.prototype.slice.call(document.getElementsByTagName("*"))

apply

使用场景:经常跟数组有关系,比如借助Math内置对象实现数组最大最小值问题

Function.prototype.myApply = function(context = globalThis, ...args) {
    const key = Symbol("key")
    context[key] = this;
    let res;
    if (args[0]) {
        res = context[key](...args[0])
    } else {
        res = context[key]()
    }
    delete context[key];
    return res;
} 

// 常见应用场景
// 1. 获取数组中的最大/小值
let arr = [1, 2, 3, 4]
let res = Math.Max.apply(null, arr)
console.log(res)  // 4

// 2. 数组之间拼接
var arr1 = [12, "foo", {name: "Joe"}, -2548];
var arr2 = ["Doe", 555, 100];
Array.prototype.push.apply(arr1, arr2)
/* arr1 = [12, "foo", {name: "Joe"}, -2548, "Doe", 555, 100] */

...

当传入对象为数组时,若想使用call方法,可结合展开运算符 call(this, ...args)