call 、bind、apply以及this的理解

108 阅读12分钟

call 、bind、apply的理解

他们这几个函数都是改变this指代问题的要想说清楚他们三个函数就有必要先来讲一下this的指代问题了

this问题

先念口诀:箭头函数、new、bind、apply 和 call、欧比届点(obj.)、直接调用、不在函数里。

按照口诀的顺序,只要满足前面某个场景,就可以确定 this 指向了

1. 箭头函数

箭头函数排在第一个是因为它的 this 不会被改变,所以只要当前函数是箭头函数,那么就不用再看其他规则了

箭头函数的 this 是在创建它时外层 this 的指向。这里的重点有两个:

  1. 创建箭头函数时,就已经确定了它的 this 指向。
  2. 箭头函数内的 this 指向外层的 this

所以要知道箭头函数的 this 就得先知道外层 this 的指向,需要继续在外层应用七步口诀。

2. new

当使用 new 关键字调用函数时,函数中的 this 一定是 JS 创建的新对象。

读者可能会有疑问,“如果使用 new 关键调用箭头函数,是不是箭头函数的 this 就会被修改呢?”。

我们在控制台试一下。

func = () => {}
new func() // throw error

从运行结果中可以看出,箭头函数不能当做构造函数,所以不能与 new 一起执行。

3. bind

易错点

  1. 多次 bind 时只认第一次 bind 的值
function func() {
  console.log(this)
}

func.bind(1).bind(2)() // 1
  1. 箭头函数中 this 不会被修改
func = () => {
  // 这里 this 指向取决于外层 this,参考口诀 7 「不在函数里」
  console.log(this)
}

func.bind(1)() // Window,口诀 1 优先
  1. bind 与 new
function func() {
  console.log(this, this.__proto__ === func.prototype)
}

boundFunc = func.bind(1)
new boundFunc() // Object true,口诀 2 优先

4. apply 和 call

apply()call() 的第一个参数都是 this,区别在于通过 apply 调用时实参是放到数组中的,而通过 call 调用时实参是逗号分隔的。

易错点

  1. 箭头函数中 this 不会被修改
func = () => {
  // 这里 this 指向取决于外层 this,参考口诀 7 「不在函数里」
  console.log(this)
}

func.apply(1) // Window,口诀 1 优先

2.bind 函数中 this 不会被修改

function func() {
  console.log(this)
}

boundFunc = func.bind(1)
boundFunc.apply(2) // 1,口诀 3 优先

5. 欧比届点(obj.)

function func() {
  console.log(this.x)
}

obj = { x: 1 }
obj.func = func
obj.func() // 1

6. 直接调用

在函数不满足前面的场景,被直接调用时,this 将指向全局对象。在浏览器环境中全局对象是 Window,在 Node.js 环境中是 Global。

先来个简单的例子。

function func() {
  console.log(this)
}

func() // Window

来一个复杂的例子,外层的outerFunc就起个迷惑目的。

function outerFunc() {
  console.log(this) // { x: 1 }

  function func() {
    console.log(this) // Window
  }

  func()
}

outerFunc.bind({ x: 1 })()

7. 不在函数里

不在函数中的场景,可分为浏览器的 <script /> 标签里,或 Node.js 的模块文件里。

  1. <script /> 标签里,this 指向 Window。
  2. 在 Node.js 的模块文件里,this 指向 Module 的默认导出对象,也就是 module.exports

练习

既然学会了口诀咱们来练习一下

  1. 问题一:在以下代码中,this 指向什么?

    function Outer() {
      this.value = 42;
      this.innerFunction = function() {
        return () => {
          console.log(this.value);//口诀1 箭头函数
        };
      };
    }
    
    const outerInstance = new Outer();//口诀2 new
    const nestedArrow = outerInstance.innerFunction();
    nestedArrow();
    
  2. 问题二:在以下代码中,this 指向什么?

    const obj1 = {
      prop: 'I am obj1',
      getProp: function() {
        return function() {
          console.log(this.prop);
        }.bind(this);//口诀3
      }
    };
    
    const obj2 = {
      prop: 'I am obj2'
    };
    
    const boundFunction = obj1.getProp();//口诀五
    boundFunction.call(obj2);//口诀4
    
  3. 问题三:在以下代码中,this 指向什么?

    function MyClass() {
      this.value = 42;
      this.getValue = function() {
        return this.value;
      };
    }
    
    const myInstance = new MyClass();//口诀2
    const myFunction = myInstance.getValue;
    console.log(myFunction());
    
  4. 问题四:在以下代码中,this 指向什么?

    const myObject = {
      value: 'I am myObject',
      myMethod: function() {
        setTimeout(() => {
          console.log(this.value);//口诀1
        }, 1000);
      }
    };
    //口诀5
    myObject.myMethod.call({ value: 'I am a different object' });//口诀4
    
  5. 问题五:在以下代码中,this 指向什么?

    const outerFunction = function() {
      this.value = 'I am outer';
      return () => {
        console.log(this.value);//口诀1
      };
    };
    
    const innerFunction = outerFunction.call({ value: 'I am a different object' });//口诀4
    innerFunction();
    

答案

  1. 问题一:在该代码中,this 确实指向 42。通过使用箭头函数,this 继承自外部作用域的 this,因此在执行 nestedArrow 时,this 指向 outerInstance 对象。
  2. 问题二:在该代码中,this 确实指向 I am obj1。通过使用 bind(this) 绑定了外部的 this,因此在执行 boundFunction.call(obj2) 时,this 指向 obj1 对象。
  3. 问题三:在该代码中,this 确实指向 42。通过使用 new MyClass() 创建实例时,this 指向新创建的对象。
  4. 问题四:在该代码中,this 确实指向 'I am a different object'。虽然 myObject.myMethod 使用了箭头函数,但是在 call 中显式传递了一个新的对象,因此 this 指向传递的对象。
  5. 问题五:在该代码中,this 确实指向 'I am a different object'。虽然 innerFunction 是在 outerFunction 中创建的,但在 outerFunction.call({ value: 'I am a different object' }) 中显式传递了一个新的对象,因此 this 指向传递的对象。

为了方便大家更深入的理解this指向问题从网上找了一些面试题来与大家分享。

题目一

var name = "window"; // window.name = "window"

var person = {
  name: "person",
  sayName: function () {
    console.log(this.name);
  }
};

function sayName() {
  var sss = person.sayName;
  sss(); // window 很明显是独立函数调用,没有与任何对象关联
  person.sayName(); // person 隐式绑定,与person关联
  (person.sayName)(); // person 同上(加括号只是代表这是一个整体)
  // console.log((b = person.sayName)); // 这里实际上就是sayName这个函数
  (b = person.sayName)(); // 间接函数引用,是独立函数调用, 输出 window
}

sayName();

问题一:很多人会问var sss = person.sayName; sss();为什么sss函数定义之后在直接调用就不属于person对象?

解析:在 var sss = person.sayName; sss(); 这一步中,sss 只是保存了对 person.sayName 函数的引用,而调用 sss() 时,它实际上是在全局作用域中被调用的,因此 this 指向全局对象(window),而不是 person 对象。

这是因为在 JavaScript 中,函数的调用方式影响了 this 的绑定。在独立函数调用时,this 通常指向全局对象。在对象方法调用时,this 会隐式绑定到该对象。这是 JavaScript 中函数调用和 this 绑定的基本规则。

问题二:很多人会问(b = person.sayName)();这个地方我明明调用了person.sayName为什么结果还是window

解析:使用括号是为了明确表示对 b 赋值的同时进行函数调用。如果你想要在没有括号的情况下进行函数调用,其实等价于b = person.sayName; b();这样的话就又回到第一个问题相当于全局独立调用所以为window

题目二

var name = 'window'
var person1 = {
  name: 'person1',
  foo1: function () {
    console.log(this.name)
  },
  foo2: () => console.log(this.name),
  foo3: function () {
    return function () {
      console.log(this.name)
    }
  },
  foo4: function () {
    return () => {
      console.log(this.name)
    }
  }
}

var person2 = { name: 'person2' }

// 隐式绑定person1对象
person1.foo1(); // person1

// 显式绑定person2
person1.foo1.call(person2);  // person2

// 箭头函数不适用任何规则, 向上层作用域中找this
person1.foo2(); // window
person1.foo2.call(person2); // window

// person1.foo3()返回了一个函数,然后独立调用
person1.foo3()(); // window

// person1.foo3.call(person2) 返回的是一个函数,然后独立调用
person1.foo3.call(person2)(); // window

// person1.foo3()返回一个函数,然后显式绑定到 person2
person1.foo3().call(person2); // person2

// person1.foo4()返回一个箭头函数,往上层作用域找,找到foo4中绑定的this,是 person1
person1.foo4()(); // person1
// foo4显示绑定到 person2
person1.foo4.call(person2)(); // person2
// person1.foo4()返回箭头函数,往上层作用域找
person1.foo4().call(person2); // person1

问题一:在person1.foo2(); person1.foo2.call(person2);这个代码中为什么运行结果都是window 为什么call指向没有用?

解析:正如口诀一所说当前函数是箭头函数,那么就不用再看其他规则了 但是你要注意的的是那一种类型的箭头函数,是一个函数返回一个箭头函数,还是一个直接是一个箭头函数这二者可是有区别的嘞。如果直接是箭头函数那就是符合口诀一就不需要考虑其他规则,但是如果是函数返回箭头函数然后在调用这个函数就不符合口诀一的规则了

问题二:person1.foo3()(); 为什么结果是window?

解释:在 person1.foo3()(); 中,首先调用了 person1.foo3()它返回了一个普通的函数。然后,对返回的函数进行了再次调用。

关键点在于第二次调用返回的函数时,**它是在全局作用域中独立调用的。**虽然第一次调用 person1.foo3() 时,该函数内部的 this 是由调用方式决定的,但是第二次调用并没有涉及任何对象,因此 this 会默认指向全局对象(在浏览器环境中通常是 window)。

问题三:person1.foo3.call(person2)();为什么他也是返回window?

解析:在 person1.foo3.call(person2)(); 中,虽然你使用了 call 方法,但是关键点在于 foo3() 返回的函数是一个普通函数,而这个函数在第二次调用时并没有使用 call 来显式指定 this

题目三

var name = 'window'
function Person (name) {
  this.name = name

  this.foo1 = function () {
    console.log(this.name)
  },

  this.foo2 = () => console.log(this.name),

  this.foo3 = function () {
    return function () {
      console.log(this.name)
    }
  },

  this.foo4 = function () {
    return () => {
      console.log(this.name)
    }
  }
}
var person1 = new Person('person1')
var person2 = new Person('person2')

person1.foo1() // person1 隐式绑定
person1.foo1.call(person2) // person2 显式绑定

person1.foo2() // person1 隐式绑定
// 箭头函数不适用显式绑定规则,直接向上层作用域找
person1.foo2.call(person2) // person1 

// person1.foo3() 返回一个函数,在全局调用
person1.foo3()() // window
person1.foo3.call(person2)() // window // 同理
// person1.foo3() 返回的函数使用 .call 显式绑定 person2
person1.foo3().call(person2) //person2 

// person1.foo4() 返回一个箭头函数,再调用,向上层作用域找
person1.foo4()() // person1

// person1.foo4.call(person2) 返回箭头函数,并且foo4显式绑定this为person2
// 再调用这个箭头函数,向上找就找到 foo4 的this 为person2
person1.foo4.call(person2)() // person2 

// person1.foo4() 返回箭头函数,不适用显式绑定,向上找到 person1
// 注意这里跟上面的区别,这里的foo4调用不是.call调用的,而是.foo4()这样调用的
// call是来调用箭头函数的,而箭头函数不适用显式绑定,向上找到的是person1
person1.foo4().call(person2) // person1

题目四

var name = 'window'
function Person (name) {
  this.name = name
  this.obj = {
    name: 'obj',
    foo1: function () {
      return function () {
        console.log(this.name)
      }
    },
    foo2: function () {
      return () => {
        console.log(this.name)
      }
    }
  }
}
var person1 = new Person('person1')
var person2 = new Person('person2')

// person1.obj.foo1()返回一个函数,在全局中调用
person1.obj.foo1()() // window
// person1.obj.foo1.call(person2) 返回一个函数 在全局中调用
person1.obj.foo1.call(person2)() // window
// person1.obj.foo1() 返回一个函数,显式绑定person2
person1.obj.foo1().call(person2) // person2


// 箭头函数调用,向上找到 foo2 中的this是obj
person1.obj.foo2()() // obj
// foo2调用的时候显式绑定person2,箭头函数向上层找到的就是person2
person1.obj.foo2.call(person2)() // person2
// 箭头函数不适用 显式绑定,向上找找到 obj
person1.obj.foo2().call(person2) // obj

call、bind、 apply

说完this的指代问题了咱们就回归正题说一下call、bind、以及apply这三个函数吧。

callbindapply 是 JavaScript 中用于改变函数执行上下文(this 值)的方法。它们都是在函数对象上的方法,可以用来显式地设置函数执行时的 this 值。

三个函数的使用

  1. call 方法

    • 语法:function.call(thisArg, arg1, arg2, ...)
    • call 方法接受一个参数 thisArg,用于指定函数执行时的 this 值。
    • 后续参数是传递给函数的参数列表。
    function greet(name) {
      console.log(`Hello, ${name}! My name is ${this.name}.`);
    }
    
    const person = { name: 'John' };
    
    greet.call(person, 'Alice');
    

    在这个例子中,call 方法将 greet 函数的执行上下文设置为 person 对象,所以在函数内部的 this.name 就是 person 对象的 name 属性。

  2. apply 方法

    • 语法:function.apply(thisArg, [argsArray])
    • apply 方法也接受一个参数 thisArg,用于指定函数执行时的 this 值。
    • 第二个参数是一个数组,包含传递给函数的参数列表。
    function greet(name) {
      console.log(`Hello, ${name}! My name is ${this.name}.`);
    }
    
    const person = { name: 'John' };
    
    greet.apply(person, ['Alice']);
    

    call 不同,apply 接受参数数组而不是单独列举参数。

  3. bind 方法

    • 语法:function.bind(thisArg[, arg1[, arg2[, ...]]])
    • bind 方法会创建一个新函数,该函数的 this 值会被永久设置为指定的 thisArg
    • 可以在 bind 中传递额外的参数,这些参数将被绑定到新函数的参数列表的开始位置。
    function greet(name) {
      console.log(`Hello, ${name}! My name is ${this.name}.`);
    }
    
    const person = { name: 'John' };
    
    const greetPerson = greet.bind(person);
    greetPerson('Alice');
    

    在这个例子中,bind 方法创建了一个新的函数 greetPerson,其 this 值永久绑定到 person 对象。当调用 greetPerson 时,this.name 将始终引用 person 对象的 name 属性。

区别和联系

应用场景

  1. call()经常做继承
  2. apply()经常跟数组有关系,比如借助于数学对象实现数组最大值最小值
  3. bind()不调用函数,但是还想改变this指向,比如改变定时器内部的this指向
call()apply()bind()
相同点改变函数this指向改变函数this指向改变函数this指向
是否调用函数
传递参数逗号,隔开数组形式[]逗号,隔开
应用场景继承与数组有关不想调用函数

手撕三个函数的实现

1. call 实现

  • 将第一个参数作为call函数内部临时对象obj
  • obj一个属性fn,成为实际执行函数,并将this关键字指向这个属性
  • 执行这个函数,并拿到返回值
  • 删除函数属性
  • 返回函数执行的结果
Function.prototype.myCall = function(context, ...args) {
    // 确保提供了上下文,否则使用全局对象
    const targetContext = context ? Object(context) : global;

    // 在目标上下文对象上添加当前函数作为属性
    targetContext.tempFunction = this;

    // 调用这个函数并传入参数,获取结果
    const result = targetContext.tempFunction(...args);

    // 清理上下文对象,删除临时添加的函数属性
    delete targetContext.tempFunction;

    // 返回函数调用的结果
    return result;
};

2. apply的实现

apply第二个参数是以数组形式传递的,所以基本步骤与call一致,不同的是函数执行的时候需要进行判断是否传入了第二个参数。如果有,将其传入并执行;若没有,直接执行。

// 实现apply
Function.prototype.myApply = function (obj, arr) {
    // 判断上下文
    const newObj = obj ? Object(obj) : global;
    // 将函数设置为对象的属性
    newObj.fn = this;
    // 执行这个函数,并拿到返回值
    let res;
    if (arr) {
        res = newObj.fn(...arr);
    } else {
        res = newObj.fn();
    }
    // 删除这个函数属性
    delete newObj.fn;
    // 返回值
    return res;
};

3. bind 实现

// 实现bind
Function.prototype.MyBind = function (context) {
    // 调用的方法本身
    const self = this;
    // 类数组->真数组
    const args = Array.prototype.slice.call(arguments, 1);
    // 中转函数
    const temp = function () {};
    const fn = function () {
        // 将新函数执行时的参数arguments数组化,然后与绑定时的参数合并
        const newArgs = Array.prototype.slice.call(arguments);
        // 如果被new调用,this应该是fn的实例
        return self.apply(this instanceof fn ? this : context || global, args.concat(newArgs));
    };
    // 中转原型链
    temp.prototype = self.prototype;
    fn.prototype = new temp();
    return fn;
};