Call & Apply & Bind的用法、区别和实现

1,056 阅读4分钟

函数原型链中的 apply,call 和 bind 方法是 JavaScript 中相当重要的概念,它们都是用来改变函数中的this指向。那关于它们的用法、区别和内部实现,你都知道吗?

call

函数基于原型链找到Function.prototype.call方法,并且把它执行,在方法执行时完成了一些功能:

  • 让当前函数执行;
  • 把函数中的this指向改为第一个传递的实参;
  • 把传递给call其余的实参当作参数传递给当前函数;

如果一个参数都没有,非严格模式下是让函数中的this指向window,严格模式下指向的是Undefined.

window.name = 'window';
let obj = {name:'张三'};
let fn = function(){
    console.log(this.name);
}

fn();  //=> this:window    输出'window'  严格模式=> undefined
//obj.fn();  //报错 obj.fn is not a function =>  obj中没有fn属性

需求:如何让fn执行时this指向obj呢?

obj.fn = fn;
obj.fn();
delete obj.fn;   
//先给obj增加fn属性,然后再删除

fn.call(obj);   //this:obj   输出'张三'
fn.call();  //this:window   严格:undefined
fn.call(null);  //this:window  严格:undefined

因此,如果第一个参数传递的是null / undefined,或者不传,非严格模式下this指向window,严格模式下传递的是谁this就是谁,不传this是undefined.

如果给fn加参数后,传递参数是什么意思?

function fn(a,b){
    ...
}
    
fn.call(obj);  //a和b均为undefined
fn.call(obj,1,2);  //this:obj   a:1  b:2
fn.call(1,2);   //this:1   a:2  b:undefined

那么如何实现一个内置call方法呢?

/*
 * @params:
 *	context:最后要改变的函数中的this指向 
 *  args:传递给函数的实参信息
 */
function call(context,...args){
    //context为null或者undefined为window,否则传了什么就是什么
    context = context == null ? window : context;
 
    //context必须为对象
    let contextType = typeof context;
    if(!/^(function|object)$/i.test(contextType)){
    	context = Object(context);
    }
    
    let result,
        key = Symbol('key');
    context[key] = this;   //把函数作为对象的某个成员值(成员名唯一,防止修改原始对象的值)
    result = context[key](...args);  //基于对象[成员]方式把函数执行,此时函数中的this就是对象
    delete context[key];  //设置的成员用完后删除掉
    return result;  //把函数的返回值作为call方法执行的结果返回
}

apply

和call方法一样,都是把函数执行,并且改变里面的this关键字,唯一的区别就是传递给函数的参数方式不同;

call和apply的区别:

  • call是一个个传参;
  • apply是按照数组传参;

通过实例来对比一下区别:

let obj = {name:'张三'};
let fn = function(a,b){
    console.log(this.name);
}
//让fn方法执行,且方法中this指向obj,并且传递参数10和20

//使用call
fn.call(obj,10,20);

//使用apply
fn.apply(obj,[10,20]);

bind

和call / apply 一样,也是用来改变函数中的this关键字的,只不过基于bind改变this,当前方法并没有被执行,类似于预先改变this。

let obj = {name:'张三'};
let fn = function(a,b){
    console.log(this.name);
}

document.body.onclick=fn;  //事件触发时,fn中的this->body

如果希望点击body时,让fn中的this指向obj应该怎样去实现?

document.body.onclick = fn.call(obj);  

是不是很多人觉得应该用call来改变指向,这么写是错误的,错误的,错误的!!!这样处理,不是把fn绑定给事件,而是把fn执行后的结果绑定给了事件。

正确方法:

//给事件绑定了匿名函数
document.body.onclick = function(){
    //this -> body
    fn.call(obj);  
}

//那么如何绑定fn呢?

//使用bind
document.body.onclick = fn.bind(obj);

由此可见,bind的优势在于:预先把fn中的this修改为obj,此时fn并没有执行,当点击事件触发才会执行fn,而call和apply都是在改变this的同时立即把方法执行。

注:在IE6~8中不支持bind方法

既然存在兼容性,那么就来自己动手实现一个bind方法吧!

/*
 * @params:
 *	context:最后要改变的函数中的this指向 
 *  args:传递给函数的实参信息
 */
function bind(context,...params){
    let _this = this;
    return function anonymous(...args){
        //args -> 可能传递的事件对象等信息
        _this.call(context,...params.concat(args));
    }
}

call & apply & bind区别

call / apply :立即执行函数并且修改里面的this;

bind:利用柯理化思想,预先把 “需要处理的函数/改变的this/传递的参数” 等信息存储在闭包中,后期达到条件(事件触发/定时器等),先执行返回的匿名函数,在执行匿名函数的过程中再去改变this等 => this和参数的预处理