我所理解的call、apply和bind

112 阅读4分钟

「这是我参与2022首次更文挑战的第5天,活动详情查看:2022首次更文挑战

call 和 apply

共同点

callapply都是改变this的指向,他们都是改变一个函数的执行上下文,将一个对象的方法交给另一个对象来执行(这点有奇用,后面介绍),并且立即执行函数。他们的调用者只能是一个函数,并且第一个参数要重新指向的新对象,如果为空,则会指向window。

不同点

它们的不同点主要体现在参数的区别上,一个是使用参数列表进行传参,一个是使用参数数组进行传参。

function test(a,b,c){}
let obj = {}
test.call(obj,1,2,3,4)
test.apply(obj,[1,2,3,4])

如上述代码所示,call函数传递的主要是参数列表,如果传递的是参数数组的话,则call会把数组当成第二个参数进行传递;反过来,如果apply传递的是参数列表的话,那么只有前面两个参数有效,后面的则会被忽略。apply的第二个参数是可以是一个数组也可以是一个类数组。

使用场景

call的妙用

对象的继承

function Father(name){
    this.name = name || 'father'
}
function Child (name){ 
    Father.call(this,name)
}

借用方法

很多时候我们可以在类数组上巧妙的使用上数组的方法

function f1(){
    const a = [].shift.call(arguments)
    console.log(a) // 1
    console.log([].slice.call(arguments)) // [2,3,4,5]
}
f1(1,2,3,4,5)

arguments本来是一个类数组,并没有数组的方法,但是可以通过数组来借用数组的方法。

apply的妙用

用来求取数组中最大的一项

let arr  = [955,22,55,1111,236,558,44]
console.log(Math.max.apply(null,arr)) // 1111

实现数组合并

let arr1 = [1, 2, 3];
let arr2 = [4, 5, 6];
[].push.apply(arr1,arr2)
console.log(arr1) // [1,2,3,4,5,6]

手写call和apply

Function.prototype._call = function(ctx,...arg){
    ctx = (ctx === undefined || ctx == null) ? window : ctx
    const fn = Symbol('fn')
    ctx[fn] = this
    const result = ctx[fn](...arg)
    delete ctx[fn]
    return result
}
Function.prototype._apply = function(ctx,arg){
    if(!Array.isArray(arg)){
        throw('the second arg is not a array')
    }
    ctx = (ctx === undefined || ctx == null) ? window : ctx
    const fn = Symbol('fn')
    ctx[fn] = this
    const result = ctx[fn](...arg)
    delete ctx[fn]
    return result
}

bind

bind() 方法会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。(来自于 MDN )

可以看出,bind函数会返回一个新函数,也就是说它不像callapply那样会立即调用函数,他是会返回一个新函数供以后调用。

const p = {
    name:'tom'
}
function getName(age){
    console.log(`I am ${this.name}.I ${age} years old`)
}
const newGetName = getName.bind(p,11)
newGetName(16)

手写bind

实现this指向改变

Function.prototype._bind = function(ctx,...arg){
    ctx = (ctx === undefined || ctx == null) ? window : ctx
    const fn = this
    return function(){
        return fn.apply(ctx,arg)
    }
}

传参的模拟实现

关于bind的参数改变是有一些疑惑的,在使用bind的时候可以传参,那么在使用bind之后返回的函数又是否可以传递参数呢?看看下面的例子吧!

const p = {
    name:'tom'
}
function getName(age){
    console.log(`I am ${this.name}.I ${age} years old`)
}
const newGetName = getName.bind(p,18)
newGetName(16) // I am tom.I 18 years old
const p = {
    name:'tom'
}
function getName(age){
    console.log(`I am ${this.name}.I ${age} years old`)
}
const newGetName = getName.bind(p)
newGetName(16) // I am tom.I 16 years old
const p = {
    name:'tom'
}
function getName(age,sex){
    console.log(`I am ${this.name}.I ${age} years old.${sex}`)
}
const newGetName = getName.bind(p,18)
newGetName(16) // I am tom.I 18 years old.16

仔细观察,不难发现,bind返回的函数是可以传值的,并且参数是跟在bind函数传的值后面的,那我们再对我们刚才的bind函数再做一次修改吧!

Function.prototype._bind = function(ctx,...arg){
    ctx = (ctx === undefined || ctx == null) ? window : ctx
    const fn = this
    return function(){
        arg = arg.concat([...arguments])
        return fn.apply(ctx,arg)
    }
}

哦,对了,bind返回的是一个函数,那么他肯定是可以被用作构造函数的,可以new出一个新对象的,这样子的话之前绑定的this就要舍弃,指向这个新新创建的对象。

var value = 2;

var foo = {
    value: 1
};

function bar(name, age) {
    this.habit = 'shopping';
    console.log(this.value);
    console.log(name);
    console.log(age);
}

bar.prototype.friend = 'kevin';

var bindFoo = bar.bind(foo, 'daisy');

var obj = new bindFoo('18');
// undefined
// daisy
// 18
console.log(obj.habit);
console.log(obj.friend);
// shopping
// kevin
console.log(bindFoo('18'))
// 1
// daisy
// 18

那么这里我们怎么实现这个功能呢?判断新函数的this是否是该函数构造出来的即可,这里可以使用instanceof来判断。

Function.prototype._bind = function(ctx,...arg){
    const fn = this
    const resFn = function(){
        arg = arg.concat([...arguments])
        return fn.apply(this instanceof resFn ? this : ctx,arg)
    }
    resFn.prototype = this.prototype
    return resFn
}