JS浅谈:bind,apply和call简介以及实现

149 阅读4分钟

MDN官方文档

bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。

call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。

apply() 方法调用一个具有给定this值的函数,以及以一个数组(或类数组对象)的形式提供的参数。

注意:call()方法的作用和 apply() 方法类似,区别就是call()方法接受的是参数列表,而apply()方法接受的是一个参数数组。

个人概述

这几个函数的共同点就在于,它们改变了函数的作用域,也就是this的指向。那什么是改变作用域呢? 打个比方。
同样的一件女装,若是穿在漂亮小姐姐的身上,你可能会肾上腺素狂飙。
那么,如果穿在我身上呢?没错,穿在一个大汉的身上。 你会为我而芳心暗动吗?

说笑而已,不要当真,我们来看一个直观的例子吧。

Demo

const obj1 = { 
    num: 1,
    handle: function(num1, num2) {
           return this.num + this.num1 + this.num2;
    }
};
const obj2 = { 
    num: 2,
    handle: function(num1, num2) {
           return this.num + this.num1 + this.num2;
    }
};
obj1.handle(1,2); // 1+1+2 = 4
obj2.handle(1,2); // 1+1+2 = 5

不难发现,两个对象中的handle方法在逻辑上是一模一样的,唯一不同的就是,调用它们的对象不同,致使它们的this指向不同,在obj1中,this.num = 1,在obj2中,this.num = 2。

明明是一样的代码,却需要写两次,哪怕CV工程师能忍,简洁爱好者都忍不了。

别急,这时候就轮到三位少侠上场了。

const handle = function(num1,num2) {
    return this.num + num1 + num2;
}

const obj1 = { num:1 }
const obj2 = { num:2 }

console.log(handle.bind(obj1,1,2)()) // 4
console.log(handle.bind(obj2,1,2)()) // 5

console.log(handle.apply(obj1,[1,2]))// 4
console.log(handle.apply(obj2,[1,2])) // 5

console.log(handle.call(obj1,1,2)) // 4
console.log(handle.call(obj2,1,2)) // 5

就如我们之前所的,这三位少侠可以改变函数的作用域,this会指向函数内的第一个参数。

三兄弟的殊途同归

这三个函数是十分相似的,但是也有一些细节上的区分,观察上面的官方介绍以及我们的Demo不难发现。

与apply,call不同的是,bind返回的是被改变了作用域的函数。这也就是为什么上面的代码中,handle在bind之后还需要进行调用。

与bind,call不同的是,apply在接收了作为函数作用域的对象后,接下来的参数需要以数组形式传入,而不是一个一个传进去。但最终效果是一样的。

基于ES6简易实现

const fn = function(num1,num2) {
    return this.num + num1 + num2;
}

const obj = { num:1 }
const obj2 = { num:2 }

Function.prototype.selfCall = function(context) {
    //指定作用域
    context = context || window;
    //获取参数
    let args = [...arguments].splice(1);
    //将方法挂载到这个函数上,函数作用域自然就是在对象内了,然后进行调用
    context.handle = this;
    let res = context.handle(...args)
    //获取到结果之后,就没必要在对象上保存这个方法了。
    delete context.handle;
    //返回我们获得的最终值。
    return res;
}

Function.prototype.selfApply = function(context) {
    //指定作用域
    context = context || window;
    //获取参数
    let args = arguments[1];
    //将方法挂载到这个函数上,函数作用域自然就是在对象内了,然后进行调用
    context.handle = this;
    let res = context.handle(...args)
    //获取到结果之后,就没必要在对象上保存这个方法了。
    delete context.handle;
    //返回我们获得的最终值。
    return res;
}

Function.prototype.selfBind = function(context) {

    //获取函数本体
    let self = this;
    //指定作用域
    context = context || window;
    //获取参数
    let args = [...arguments].splice(1);
    //这里引用我们前面自己写的call函数
    return function(){
        return self.selfCall(context,...args)
    }
}

console.log(fn.selfBind(obj,1,2)()) // 4
console.log(fn.selfBind(obj2,1,2)()) // 5
console.log(fn.selfApply(obj,[1,2]))// 4
console.log(fn.selfApply(obj2,[1,2])) // 5
console.log(fn.selfCall(obj,1,2)) // 4
console.log(fn.selfCall(obj2,1,2)) // 5js

该说的都写在注释里了,耐下心看一下吧~

题外话

既然都基于ES6了,为什么不用箭头函数去酷酷地实现呢?
千万别干这种事!

let attr = "Hello";

const fn = () => {
    console.log(this.attr)
}

const obj = {
    attr: "World!"
}

fn() // window环境下:Hello node环境下:undefined

fn.call(obj) // undefined

箭头函数的作用域在它声明时就已经是确定了的,不能再改变,不要踩坑咯。

第一次写文,还是能感觉到各方面的不足的,但是我实在想去睡觉了啊!就这样吧,祝安好!