JavaScript 中 this 关键字的绑定规则有哪些?请举例说明 call(), apply(), bind() 的区别及其应用场景。

咳咳,各位观众老爷,晚上好!我是你们的老朋友,代码界的段子手。今天咱们聊聊 JavaScript 里让人又爱又恨的 this 关键字。这玩意儿要是没搞明白,写出来的代码就跟薛定谔的猫似的,运行结果全靠猜,刺激!

咱们今天就来扒一扒 this 的底裤,让它在咱们面前一丝不挂,彻底臣服!

一、this 的绑定规则:一场权力游戏

this 就像个墙头草,它指向谁,取决于它被调用的方式。记住这句话,非常重要!

JavaScript 中 this 的绑定规则主要有以下几种:

  1. 默认绑定(Default Binding):

    • 规则: 在非严格模式下,如果 this 没有被其他规则绑定,它会默认绑定到全局对象。在浏览器中,全局对象通常是 window。在 Node.js 中,全局对象是 global。在严格模式下 ("use strict";),this 会绑定到 undefined
    • 应用场景: 这种情况通常发生在函数独立调用时。
    • 示例:
    function foo() {
      console.log(this); // 在浏览器中输出 Window 对象
    }
    
    foo();
    
    function bar() {
      "use strict";
      console.log(this); // 输出 undefined
    }
    
    bar();
  2. 隐式绑定(Implicit Binding):

    • 规则: 当函数作为对象的方法被调用时,this 会绑定到该对象。
    • 应用场景: 这是最常见的 this 绑定方式,尤其是在面向对象编程中。
    • 示例:
    const obj = {
      name: "张三",
      sayHello: function() {
        console.log("Hello, my name is " + this.name);
      }
    };
    
    obj.sayHello(); // 输出 "Hello, my name is 张三"

    注意: 如果方法链式调用,this 始终指向调用该方法的对象。

    const obj1 = {
      name: "李四",
      method: function() {
        console.log("obj1的this:" + this.name);
        return this;
      }
    };
    
    const obj2 = {
      name: "王五"
    };
    
    obj2.method = obj1.method;
    
    obj1.method().method(); //李四 李四  this指向obj1
    obj2.method().method(); //王五 王五 this指向obj2
  3. 显式绑定(Explicit Binding):call(), apply(), bind()

    • 规则: 通过 call(), apply(), bind() 方法,你可以明确地指定函数执行时 this 的值。
    • 应用场景: 当你需要控制 this 的指向,或者需要借用其他对象的方法时,可以使用显式绑定。
    • 示例: 稍后详细讲解。
  4. new 绑定(new Binding):

    • 规则: 当使用 new 关键字调用函数(构造函数)时,会发生以下事情:
      1. 创建一个新的空对象。
      2. 将这个新对象的 __proto__ 属性指向构造函数的 prototype 属性。
      3. this 绑定到这个新对象。
      4. 执行构造函数中的代码。
      5. 如果构造函数没有显式返回一个对象,则返回这个新对象。如果构造函数显式返回一个对象,则返回该对象。
    • 应用场景: 用于创建对象实例。
    • 示例:
    function Person(name, age) {
      this.name = name;
      this.age = age;
      this.sayHello = function() {
        console.log("Hello, my name is " + this.name + ", I am " + this.age + " years old.");
      };
    }
    
    const person1 = new Person("赵六", 30);
    person1.sayHello(); // 输出 "Hello, my name is 赵六, I am 30 years old."
    
    const person2 = new Person("钱七", 25);
    person2.sayHello(); // 输出 "Hello, my name is 钱七, I am 25 years old."
    
    // 构造函数显式返回对象的情况
    function Animal(name) {
        this.name = name;
        return {  // 显式返回一个对象
            nickname: "小" + name,
            sayName: function() {
                console.log("我的昵称是:" + this.nickname);
            }
        };
    }
    
    const animal1 = new Animal("老虎");
    animal1.sayName(); // 输出 "我的昵称是:小老虎"
    console.log(animal1.name); // undefined 因为Animal构造函数最终返回的是另一个对象,没有name属性
    

优先级:

这些绑定规则有优先级,当多个规则同时适用时,优先级高的规则会覆盖优先级低的规则。优先级从高到低依次为:

  1. new 绑定
  2. 显式绑定 (call(), apply(), bind())
  3. 隐式绑定
  4. 默认绑定

二、call(), apply(), bind():显式绑定的三剑客

这三个方法都是 Function.prototype 上的方法,也就是说,所有的函数都可以调用它们。它们的作用都是改变函数执行时的 this 指向。

方法 作用 参数 返回值 应用场景
call() 立即调用函数,并将 this 绑定到指定的对象。 第一个参数是要绑定的对象,后面的参数是函数的参数,依次传入。 函数的返回值。 借用方法: 当你需要使用一个对象的方法,但该对象本身没有这个方法时,可以使用 call() 来借用其他对象的方法。 继承: 在构造函数中,可以使用 call() 来调用父类的构造函数,实现继承。 * 改变 this 指向: 当你需要手动控制 this 的指向时,可以使用 call()
apply() 立即调用函数,并将 this 绑定到指定的对象。 第一个参数是要绑定的对象,第二个参数是一个数组,数组中的元素是函数的参数。 函数的返回值。 借用方法:call() 类似,可以借用其他对象的方法。 求数组最大值/最小值: 可以使用 apply()Math.max()Math.min()this 绑定到 null,并将数组作为参数传入,快速求出数组的最大值或最小值。 * 参数不确定时: 当函数的参数个数不确定时,可以使用 apply() 将参数放入数组中传入。
bind() 创建一个新的函数,该函数与原函数具有相同的函数体和作用域,但是 this 被永久绑定到指定的对象。 注意:bind() 不会立即执行函数,而是返回一个新的函数。 第一个参数是要绑定的对象,后面的参数是函数的参数,依次传入。 返回一个新的函数,该函数与原函数具有相同的函数体和作用域,但是 this 被永久绑定到指定的对象。 事件处理函数: 当你需要将一个函数作为事件处理函数,并且希望在事件处理函数中访问特定的对象时,可以使用 bind()this 绑定到该对象。 柯里化: 可以使用 bind() 来实现柯里化,即固定函数的部分参数,生成一个新的函数。 * 避免 this 丢失: 当你将一个对象的方法传递给其他函数时,可能会导致 this 丢失,可以使用 bind()this 绑定到该对象,避免 this 丢失。

2.1 call() 的用法

call() 方法接受一个 thisArg 参数,以及可选的参数列表。thisArg 指定了函数执行时 this 的值。如果 thisArgnullundefined,则 this 会绑定到全局对象(非严格模式)或 undefined(严格模式)。

function sayHello(greeting) {
  console.log(greeting + ", my name is " + this.name);
}

const person = {
  name: "张三"
};

sayHello.call(person, "你好"); // 输出 "你好, my name is 张三"
sayHello.call(null, "Hello"); //非严格模式下 输出 "Hello, my name is undefined" (浏览器环境)
sayHello.call(undefined, "Hi"); //非严格模式下 输出 "Hi, my name is undefined" (浏览器环境)

应用场景:

  • 借用方法:

    const obj1 = {
      name: "李四",
      age: 20
    };
    
    const obj2 = {
      name: "王五"
    };
    
    function showInfo() {
      console.log("Name: " + this.name + ", Age: " + this.age);
    }
    
    // obj2 没有 age 属性,但我们可以借用 obj1 的属性
    obj2.age = 25; // 先给obj2加一个age属性
    showInfo.call(obj2); // 输出 "Name: 王五, Age: 25"
  • 继承:

    function Animal(name) {
      this.name = name;
    }
    
    Animal.prototype.sayName = function() {
      console.log("I am an animal, my name is " + this.name);
    };
    
    function Dog(name, breed) {
      // 调用 Animal 构造函数,将 this 绑定到 Dog 实例
      Animal.call(this, name);
      this.breed = breed;
    }
    
    // 继承 Animal 的原型
    Dog.prototype = Object.create(Animal.prototype);
    Dog.prototype.constructor = Dog; // 修正 constructor 指向
    
    Dog.prototype.bark = function() {
      console.log("Woof!");
    };
    
    const dog = new Dog("旺财", "中华田园犬");
    dog.sayName(); // 输出 "I am an animal, my name is 旺财"
    dog.bark(); // 输出 "Woof!"

2.2 apply() 的用法

apply() 方法与 call() 类似,也接受一个 thisArg 参数,但它只接受一个参数,即一个包含函数参数的数组(或者类数组对象)。

function sayHello(greeting, punctuation) {
  console.log(greeting + ", my name is " + this.name + punctuation);
}

const person = {
  name: "赵六"
};

sayHello.apply(person, ["Hello", "!"]); // 输出 "Hello, my name is 赵六!"

应用场景:

  • 求数组最大值/最小值:

    const numbers = [5, 2, 8, 1, 9, 4];
    const max = Math.max.apply(null, numbers); // 将 Math.max 的 this 绑定到 null,并将 numbers 作为参数传入
    const min = Math.min.apply(null, numbers);
    console.log("Max:", max); // 输出 "Max: 9"
    console.log("Min:", min); // 输出 "Min: 1"
  • 参数不确定时:

    function sum() {
      let total = 0;
      for (let i = 0; i < arguments.length; i++) {
        total += arguments[i];
      }
      console.log("Sum:", total);
    }
    
    const nums = [1, 2, 3, 4, 5];
    sum.apply(null, nums); // 输出 "Sum: 15"

2.3 bind() 的用法

bind() 方法创建一个新的函数,当这个新的函数被调用时,它的 this 值会被设置成 bind() 传入的第一个参数。bind() 还可以接受额外的参数,这些参数会在调用新函数时作为参数传递给原函数。注意:bind() 不会立即执行函数,而是返回一个新的函数。

function sayHello(greeting) {
  console.log(greeting + ", my name is " + this.name);
}

const person = {
  name: "钱七"
};

const boundSayHello = sayHello.bind(person); // 创建一个新的函数,this 绑定到 person

boundSayHello("你好"); // 输出 "你好, my name is 钱七"

const boundSayHello2 = sayHello.bind(person, "Hi"); //预先绑定greeting参数

boundSayHello2(); //输出 "Hi, my name is 钱七"

应用场景:

  • 事件处理函数:

    <!DOCTYPE html>
    <html>
    <head>
      <title>Bind Example</title>
    </head>
    <body>
      <button id="myButton">Click Me</button>
      <script>
        const obj = {
          name: "周八",
          handleClick: function() {
            console.log("Button clicked by " + this.name);
          }
        };
    
        const button = document.getElementById("myButton");
        button.addEventListener("click", obj.handleClick.bind(obj)); // 将 this 绑定到 obj
      </script>
    </body>
    </html>

    在这个例子中,handleClick 方法需要访问 objname 属性。如果不使用 bind()this 在事件处理函数中会指向 button 元素,而不是 obj

  • 柯里化:

    function add(x, y) {
      return x + y;
    }
    
    const addFive = add.bind(null, 5); // 创建一个新的函数,x 固定为 5
    
    console.log(addFive(3)); // 输出 8
    console.log(addFive(10)); // 输出 15
  • 避免 this 丢失:

    const obj = {
      name: "吴九",
      greet: function() {
        console.log("Hello, my name is " + this.name);
      },
      delayedGreet: function() {
        setTimeout(this.greet.bind(this), 1000); // 将 this 绑定到 obj
      }
    };
    
    obj.delayedGreet(); // 1秒后输出 "Hello, my name is 吴九"

    如果不使用 bind()setTimeout 中的 this 会指向全局对象,导致 this.nameundefined

三、总结

this 的绑定规则是 JavaScript 中一个重要的概念,理解 this 的指向对于编写高质量的 JavaScript 代码至关重要。call(), apply(), bind() 是显式绑定 this 的三种方法,它们各有特点,适用于不同的场景。

希望今天的讲解能够帮助大家彻底理解 this 关键字,告别薛定谔的代码!如果觉得有用,记得点个赞,分享给你的小伙伴们!下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注