call、apply、bind的区别?如何实现?

221 阅读4分钟

什么情况需要改变this指向

下例,正常情況 say方法输出martin 但是我们把 say 放在setTimeout 方法中,在定时器中是作为回调函数来执行的,因此回到主栈执 行时是在全局执行上下文的环境中执行的,这时候this指向 window,所以输出Lucy 我们实际需要的是,this指向 obj对象,这时候就需要该改变 this指向了

var name = "lucy";
var obj = {
    name: "martin",
    say: function () {
        console.log(this.name);
    }
};

obj.say(); // martin this指向obj

setTimeout(obj.say,0); // lucy this指向window

// 改变this指向
setTimeout(obj.say.bind(obj),0); //martin this指向obj

共同点

  • 都是显式改变this的方法
  • 第一个参数都是this的指向
  • 第一个参数是null或者undefined时,默认指向window(浏览器)

区别

  • call方法接收两个参数,第一个参数是this的值,第二个参数是传递给函数的参数,以逗号分隔,参数一次性传入。改变this指向后原函数会立即执行,此方法只临时改变this指向一次
  • apply方法也接收两个参数,第一个参数是this的值(为null或者undefined时,默认指向window(浏览器)),第二个参数一个数组或类数组对象,改变this指向后原函数会立即执行,此方法只临时改变this指向一次
  • bind方法创建一个新的函数,当这个新函数被调用时,bind的第一个参数将作为它运行时的this,参数可分多次传入。改变this指向不会立即执行,而是返回一个永久改变this指向的函数
function fn(...args){ 
    console.log(this,args); 
} 

let obj = { myname:"xxx" } 

// apply 
fn.apply(obj,[1,2]); 

// call 
fn.call(obj, 1,2); 

// bind
const bindFn = fn.bind(obj); 
// bind不会理解执行需要执行一次 
bindFn(1,2) // this指向obj 

fn() // this指向window

实现一个call

  • 判断调用对象是否为函数
  • 判断传入上下文对象是否存在,如果不存在,则设置为 window
  • 处理传入的参数,截取第一个参数后的所有参数
  • 将函数作为上下文对象的一个属性
  • 使用上下文对象来调用这个方法,并保存返回结果
  • 删除刚才新增的属性
  • 返回结果;
Function.prototype.myCall = function(context) {
  // 判断调用对象是否为函数
  if (typeof this !== "function") {
    console.error("type error")
    return
  }
  // 判断 context 是否传入,如果未传入则设置为 window
  context = context || window;
 
  // 获取参数
  let args = [...arguments].slice(1), result = null;
 
  // 将调用函数设为对象的方法
  context.fn = this;
  // 调用方法
  result = context.fn(...args);
  // 将属性删除
  delete context.fn;
  return result;
}
var obj1 = {
    a: 1,
    getValue: function() {
        console.log('obj1', arguments)
        return this.a
    }
}
var obj2 = {
    a: 2,
    getValue: function() {
        console.log('obj2', arguments)
        return this.a
    }
}
console.log('call原版', obj1.getValue.call(obj2, 1, 2, 3))
console.log('call模拟板', obj1.getValue.myCall(obj2, 1, 2, 3))

执行结果:

image.png

实现一个apply

  • 判断调用对象是否为函数
  • 判断传入上下文对象是否存在,如果不存在,则设置为 window
  • 将函数作为上下文对象的一个属性
  • 判断参数值是否传入
  • 使用上下文对象来调用这个方法,并保存返回结果
  • 删除刚才新增的属性
  • 返回结果
Function.prototype.myApply = function(context) {
  // 判断调用对象是否为函数
  if (typeof this !== 'function') {
    console.error('type error')
    return
  }
  // 判断 context 是否传入,如果未传入则设置为 window
  context = context || window
  // 将待执行的函数作为上下文对象的一个属性
  context.fn = this
  // 调用方法
  let args = arguments[1]
  let result = ''
  if (args) {
    result = context.fn(...args)
  } else {
    result = context.fn()
  }         
  // 将属性删除
  delete context.fn;
  return result
}

实现一个bind

逻辑1:

  • 判断调用对象是否为函数
  • 判断传入上下文对象是否存在,如果不存在,则设置为 window
  • 将函数作为上下文对象的一个属性
  • 处理传入的参数,截取第一个参数后的所有参数
  • 创建一个函数返回
  • 在函数内部,使用上下文对象来调用这个方法,并返回结果
  • 删除刚才新增的属性
  • 返回结果

Function.prototype.myBind = function(context) {
  // 判断调用对象是否为函数
  if (typeof this !== 'function') {
    console.error('type error')
    return
  }
  // 判断 context 是否传入,如果未传入则设置为 window
  context = context || window
  // 将待执行的函数作为上下文对象的一个属性
  context.fn = this
  // 构造参数
  const args = [...arguments].slice(1)
 
  return function() {
    // 调用方法
    const result = context.fn(...args)
    delete context.fn
    return result
  }
}

逻辑2:

  • 判断调用对象是否为函数;
  • 保存当前函数的引用,获取其余传入参数值;
  • 创建一个函数返回;
  • 函数内部使用 apply 来绑定函数调用,需要判断函数作为构造函数的情况,这个时候需要传入当前函数的 this 给 apply 调用,其余情况都传入指定的上下文对象。
Function.prototype.myBind = function (context) {
    // 判断调用对象是否为函数
    if (typeof this !== "function") {
        throw new TypeError("Error");
    }

    // 获取参数
    const args = [...arguments].slice(1),
    fn = this;

    return function Fn() {
        // 根据调用方式,传入不同绑定值
        return fn.apply(this instanceof Fn ? new fn(...arguments) : context, args.concat(...arguments));
    }
}