JS: call,apply和bind解析|刷题打卡

153 阅读4分钟

一、是什么?有什么用?

applycallbind这3个都是函数原型上的方法:

作用是改变函数执行时的上下文,也就是改变函数运行时的this指向。

那什么情况下需要改变this的指向呢?

举个例子:

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

obj.say(); // inner
setTimeout(obj.say, 0); // outer

从例子中可以看出,正常调用情况下输出inner,但是如果把say方法放在setTimeout的方法中,在定时器中是作为回调函数来执行的,因此执行时是在全局上下文的环境中执行的,这时候this指向window全局变量,所以window.name就是global

二、有什么区别?

1.区别

  • apply接收2个参数,第一个参数是this的指向,第二个参数是函数接收的参数,必须以数组的形式传入;改变this指向后原函数会立即执行,并且此方法只是临时改变this指向一次。
function fn(...args) {
    console.log(this, args);
}
var obj = {
    name: "Tom"
}

fn.apply(obj, [1, 2]); // {name: "Tom"} [1, 2]

fn(1, 2) // Window [1, 2]

当第一个参数为nullundefined时,在浏览器环境中默认指向window全局对象。

fn.apply(null,[1,2]); // this指向window
fn.apply(undefined,[1,2]); // this指向window
  • call方法的第一个参数也是this的指向,后面传入的是一个参数列表。跟apply一样,改变this指向后原函数会立即执行,且此方法只是临时改变this指向一次
function fn(...args){
    console.log(this,args);
}
var obj = {
    name: "Tom"
}

fn.call(obj,1,2); // {name: "Tom"} [1, 2]
fn(1,2) // Window [1, 2]

当第一个参数为nullundefined时,在浏览器环境中默认指向window全局对象。

fn.call(null,[1,2]); // this指向window
fn.call(undefined,[1,2]); // this指向window
  • bind方法第一个参数也是this的指向,后面传入的也是参数列表,但是这个参数列表可以分多次传入。但是bind改变this指向后不会立即执行,而是返回一个永久的this指向的新函数。
function fn(...args){
    console.log(this,args);
}
var obj = {
    name: "Tom"
}

var bindFn = fn.bind(obj); // this 也会变成传入的obj ,bind不是立即执行的

bindFn(1,2) // {name: "Tom"} [1, 2]

fn(1,2) // Window [1, 2]

2.总结

从上面几个例子可以看出,applycallbind三者的区别在于:

  • 三者都可以改变函数的this指向

  • 三者第一个参数都是this要指向的对象,如果没有这个参数或者参数为nullundefined,则默认指向全局window对象。

  • 三者都可以传参,但是apply是数组,而call是用逗号分割参数列表,且callapply是一次性传入参数,而bind可以分多次传入

  • bind是返回绑定this之后的函数,applycall是立即执行。

三、自己模拟实现

首先上测试代码:

// 测试代码
function test() {
  function fn(...args){
    console.log(this,args);
  }
  var obj = {
      name: "Tom"
  }
  
  fn.myBind(obj)(1, 2);    // 期望返回 {name: "Tom"} [1, 2]
  fn.myBind(obj, 1, 2)();    // 期望返回 {name: "Tom"} [1, 2]
  fn.myApply(obj, [1, 2]); // 期望返回 {name: "Tom"} [1, 2]
  fn.myCall(obj, 1, 2);    // 期望返回 {name: "Tom"} [1, 2]
}

test();

关于args的写法可以参考:juejin.cn/post/693575…

1.实现bind

// 首先要考虑传递参数的方式:

// 方式一:只在bind中传递函数参数
fn.bind(obj,1,2)()

// 方式二:在bind中传递函数参数,也在返回函数中传递参数
fn.bind(obj,1)(2)

// 实现代码
// 现代浏览器都有bind函数 我们声明mybind避免覆盖bind
Function.prototype.myBind = function(context, ...args) {
  // context 是上下文对象 , args 是传入的参数
  if(typeof this !=== 'function') {
    throw new Error('Error!');
  }
  const fn = this;
  return (...fnArgs) => {
    // fnArgs是返回函数的参数
    return this.apply(context, args.concat(fnArgs));
  }
}

2.实现apply

// 考虑3个问题:
// 1:改变this指向
// 通过给上下文对象赋值一个临时函数,执行这个函数时this就会指向当前正在执行的对象。
// 2: 传参
// 通过 ES6 的spread运算符简单实现
// 3: 返回值
Function.prototype.myApply = function(context, arr) {
    context = context ? Object(context) : window;
    context.fn = this;
    var result;
    // 根据传入的第二个参数判断
    if (arr) {
        result = context.fn(...arr);
    } else {
        result = context.fn();
    }
    delete context.fn;
    return result;
}

3.实现 call

// 考虑3个问题:
// 1:改变this指向
// 通过给上下文对象赋值一个临时函数,执行这个函数时this就会指向当前正在执行的对象。
// 2: 传参
// 通过 ES6 的spread运算符简单实现
// 3: 返回值
Function.prototype.myCall = function(context) {
    context = context ? Object(context) : window;
    context.fn = this;
    // 利用拓展运算符直接将arguments转为数组
    var args = [...arguments].slice(1);
    const res = context.fn(...args);
    delete context.fn;
    return res;
}