理解 `this` 绑定的四种规则:默认绑定、隐式绑定、显式绑定、new 绑定

各位程序猿、攻城狮、代码界的艺术家们,晚上好!🌙

今天,咱们要一起深入探讨 JavaScript 中一个让人又爱又恨、捉摸不定的家伙——this。 哎呀,this,你可真是个磨人的小妖精!😈 多少英雄好汉,都曾败倒在你那似是而非的魔力之下。

别怕!今晚,我就要带大家揭开 this 的神秘面纱,保证让大家在今后的代码生涯中,与 this 谈笑风生,从此不再被它所困扰!

咱们今天要讲的,是 this 绑定的四大规则:默认绑定、隐式绑定、显式绑定、new 绑定。 听起来好像有点枯燥?别担心,我会尽量用最通俗易懂、最幽默风趣的语言,再加上一些实际的例子,让大家在轻松愉快的氛围中掌握这些知识点。

准备好了吗? 咱们开始吧!🚀

一、 this:代码界的百变星君

首先,我们要搞清楚 this 到底是个什么玩意儿? 简单来说,this 就是 JavaScript 函数执行时,自动生成的一个内部对象。 它指向的是函数执行时的上下文,也就是函数执行时所处的环境。

你可以把 this 想象成一位演员,他会根据不同的剧本(代码),扮演不同的角色(指向不同的对象)。 就像周星驰,可以演喜剧之王,也可以演唐伯虎,关键看他当时在演什么戏。🎭

this 的值,并不是在函数定义的时候就确定的,而是在函数被调用的时候才确定的。 这也是 this 让人感到困惑的原因之一。

二、 默认绑定:孤独的王者

默认绑定,顾名思义,是 this 绑定规则中最基本、也是最“孤独”的一种。 当我们在一个非严格模式下,直接调用一个函数,而没有任何其他规则适用时,this 就会默认绑定到全局对象

在浏览器环境中,全局对象就是 window 对象;在 Node.js 环境中,全局对象则是 global 对象。

举个例子:

function sayHello() {
  console.log("Hello, " + this.name);
}

name = "Global"; // 注意:非严格模式下,直接赋值给变量,会污染全局对象

sayHello(); // 输出:Hello, Global

在这个例子中,sayHello 函数被直接调用,没有任何其他规则适用,所以 this 默认绑定到了全局对象 window。 因此,this.name 实际上访问的是 window.name,也就是我们定义的全局变量 "Global"。

注意:严格模式下,默认绑定会将 this 绑定到 undefined。 这是为了避免意外地修改全局对象,提高代码的安全性。

"use strict";

function sayHello() {
  console.log("Hello, " + this.name); // 报错:Cannot read property 'name' of undefined
}

sayHello(); // 报错!

在严格模式下,this 绑定到了 undefined,所以访问 this.name 会导致错误。

总结一下:

规则 适用场景 this 指向
默认绑定 非严格模式下,直接调用函数,无其他规则适用 全局对象 (浏览器: window, Node.js: global)
默认绑定 严格模式下,直接调用函数,无其他规则适用 undefined

三、 隐式绑定:谁调用我,我就指向谁

隐式绑定,可以说是 this 绑定规则中最常见、也是最容易理解的一种。 当函数作为对象的方法被调用时,this 会隐式绑定到调用该方法的对象。 换句话说,谁调用了我,我就指向谁!

举个例子:

const person = {
  name: "Alice",
  sayHello: function() {
    console.log("Hello, " + this.name);
  }
};

person.sayHello(); // 输出:Hello, Alice

在这个例子中,sayHello 函数是 person 对象的方法。 当我们使用 person.sayHello() 调用该方法时,this 会隐式绑定到 person 对象。 因此,this.name 实际上访问的是 person.name,也就是 "Alice"。

再来一个更复杂的例子:

const obj1 = {
  name: "Obj1",
  obj2: {
    name: "Obj2",
    sayHello: function() {
      console.log("Hello, " + this.name);
    }
  }
};

obj1.obj2.sayHello(); // 输出:Hello, Obj2

在这个例子中,sayHello 函数是 obj2 对象的方法。 当我们使用 obj1.obj2.sayHello() 调用该方法时,this 会隐式绑定到 obj2 对象,而不是 obj1 对象。 记住,this 指向的是直接调用该方法的对象

隐式绑定有一个坑:隐式丢失

隐式丢失,指的是由于一些原因,导致隐式绑定失效,this 重新绑定到全局对象或 undefined 的情况。 这是初学者经常遇到的一个坑,需要特别注意。

1. 函数赋值给变量:

const person = {
  name: "Alice",
  sayHello: function() {
    console.log("Hello, " + this.name);
  }
};

const hello = person.sayHello; // 将 person.sayHello 赋值给变量 hello

hello(); // 输出:Hello, Global (非严格模式) 或 报错 (严格模式)

在这个例子中,我们将 person.sayHello 函数赋值给了变量 hello。 当我们直接调用 hello() 时,this 不再指向 person 对象,而是重新进行默认绑定,指向全局对象 (非严格模式) 或 undefined (严格模式)。 因为此时 hello() 仅仅是一个普通的函数调用,不再是对象的方法调用。

2. 回调函数:

const person = {
  name: "Alice",
  sayHello: function() {
    console.log("Hello, " + this.name);
  }
};

function doSomething(callback) {
  callback();
}

doSomething(person.sayHello); // 输出:Hello, Global (非严格模式) 或 报错 (严格模式)

在这个例子中,我们将 person.sayHello 函数作为回调函数传递给 doSomething 函数。 在 doSomething 函数内部调用 callback() 时,this 同样会发生隐式丢失,重新进行默认绑定。 因为 callback() 仅仅是一个普通的函数调用,不再是对象的方法调用。

如何解决隐式丢失问题?

解决隐式丢失问题,有几种常用的方法:

  • 使用 bind 方法 (显式绑定): 我们将在下一节介绍 bind 方法,它可以永久地将 this 绑定到指定的对象。

  • 使用箭头函数: 箭头函数没有自己的 this,它会继承外层作用域的 this。 这可以避免隐式丢失问题。

  • 使用 thatself 变量: 在方法内部,将 this 赋值给一个变量 (通常是 thatself),然后在回调函数中使用该变量。

总结一下:

规则 适用场景 this 指向 注意事项
隐式绑定 函数作为对象的方法被调用 调用该方法的对象 隐式丢失:函数赋值给变量、回调函数等情况可能导致 this 重新进行默认绑定。
解决方法:使用 bind 方法、箭头函数、thatself 变量等。

四、 显式绑定:指哪打哪,精准操控

显式绑定,顾名思义,就是我们可以明确地指定函数执行时 this 的值。 JavaScript 提供了三个方法来实现显式绑定:callapplybind

  • call 方法:

    call 方法允许我们调用一个函数,并指定该函数执行时 this 的值。 call 方法的第一个参数就是要绑定的 this 值,后面的参数是传递给函数的参数列表。

    const person = {
      name: "Alice"
    };
    
    function sayHello(greeting) {
      console.log(greeting + ", " + this.name);
    }
    
    sayHello.call(person, "Hello"); // 输出:Hello, Alice

    在这个例子中,我们使用 call 方法将 sayHello 函数的 this 绑定到 person 对象,并传递了 "Hello" 作为 greeting 参数。

  • apply 方法:

    apply 方法与 call 方法类似,也允许我们调用一个函数,并指定该函数执行时 this 的值。 不同的是,apply 方法的第二个参数是一个数组,数组中的元素将作为参数传递给函数。

    const person = {
      name: "Alice"
    };
    
    function sayHello(greeting, punctuation) {
      console.log(greeting + ", " + this.name + punctuation);
    }
    
    sayHello.apply(person, ["Hello", "!"]); // 输出:Hello, Alice!

    在这个例子中,我们使用 apply 方法将 sayHello 函数的 this 绑定到 person 对象,并传递了 ["Hello", "!"] 作为参数数组。

  • bind 方法:

    bind 方法与 callapply 方法不同,它不会立即调用函数,而是返回一个新的函数,该函数的 this 已经被永久地绑定到指定的对象。 我们可以稍后再调用这个新的函数。

    const person = {
      name: "Alice"
    };
    
    function sayHello(greeting) {
      console.log(greeting + ", " + this.name);
    }
    
    const helloAlice = sayHello.bind(person, "Hello"); // 返回一个新的函数,this 已经绑定到 person 对象
    
    helloAlice(); // 输出:Hello, Alice  (稍后调用)

    在这个例子中,我们使用 bind 方法将 sayHello 函数的 this 永久地绑定到 person 对象,并传递了 "Hello" 作为 greeting 参数。 bind 方法返回了一个新的函数 helloAlice,我们可以稍后再调用它。

    bind 方法的特点:

    • bind 方法返回的是一个新函数,而不是立即执行原函数。
    • bind 方法可以预先绑定参数,这些参数会在调用新函数时自动传递给原函数。
    • bind 方法的 this 绑定是永久的,即使使用 callapply 方法也无法修改。

总结一下:

规则 适用场景 this 指向 特点
显式绑定 使用 callapplybind 方法 指定的对象 callapply 方法会立即调用函数,bind 方法返回一个新的函数,this 绑定是永久的。
call 方法的参数是参数列表,apply 方法的参数是参数数组,bind 方法可以预先绑定参数。

五、 new 绑定:我是你的构造器

new 绑定,是 this 绑定规则中比较特殊的一种。 当我们使用 new 关键字调用一个函数时,JavaScript 会执行以下步骤:

  1. 创建一个新的空对象。
  2. 将该空对象的原型 (__proto__) 指向构造函数的 prototype 属性。
  3. 将该空对象作为 this 绑定到构造函数中。
  4. 如果构造函数没有显式地返回一个对象,则返回该新对象。

简单来说,new 关键字的作用就是:创建一个对象,并将 this 绑定到该对象。

举个例子:

function Person(name) {
  this.name = name;
  console.log("Person constructor called with name: " + this.name);
}

const alice = new Person("Alice"); // 输出:Person constructor called with name: Alice

console.log(alice.name); // 输出:Alice

在这个例子中,我们使用 new 关键字调用了 Person 函数。 new 关键字创建了一个新的空对象,并将该对象作为 this 绑定到 Person 函数中。 因此,this.name = name 实际上是在新对象上添加了一个 name 属性,并将其赋值为 "Alice"。 最后,new 关键字返回了该新对象,并将其赋值给变量 alice

new 绑定的优先级:

new 绑定的优先级高于隐式绑定和默认绑定。 也就是说,如果一个函数同时满足 new 绑定和隐式绑定或默认绑定,那么 this 会绑定到 new 关键字创建的新对象。

总结一下:

规则 适用场景 this 指向 特点
new 绑定 使用 new 关键字调用函数 new 关键字创建的新对象 创建一个新对象,将 this 绑定到该对象,并将该对象的原型指向构造函数的 prototype 属性。
优先级高于隐式绑定和默认绑定。

六、 优先级:谁更胜一筹?

现在,我们已经了解了 this 绑定的四大规则。 如果一个函数同时满足多个规则,那么 this 到底会绑定到哪个对象呢?

this 绑定的优先级如下:

  1. new 绑定
  2. 显式绑定
  3. 隐式绑定
  4. 默认绑定

也就是说,new 绑定的优先级最高,默认绑定的优先级最低。

举个例子:

const person = {
  name: "Alice",
  sayHello: function() {
    console.log("Hello, " + this.name);
  }
};

const helloAlice = person.sayHello.bind({ name: "Bob" }); // 显式绑定到 { name: "Bob" }

const charlie = new helloAlice(); // new 绑定

// 输出:Person constructor called with name: undefined
// 输出:Hello, Bob

在这个例子中,helloAlice 函数首先被显式绑定到 { name: "Bob" } 对象,然后又被 new 关键字调用。 由于 new 绑定的优先级高于显式绑定,所以 this 最终绑定到 new 关键字创建的新对象,而不是 { name: "Bob" } 对象。 但是,由于 bind 已经绑定了 "Hello" 参数,所以 helloAlice 函数在被 new 调用时,仍然会输出 "Hello, Bob"。

七、 总结:不再迷茫,掌控全局

今天,我们一起深入探讨了 JavaScript 中 this 绑定的四大规则:默认绑定、隐式绑定、显式绑定和 new 绑定。 希望通过今天的学习,大家能够彻底理解 this 的工作原理,不再被它所困扰。

记住,this 是一个动态的概念,它的值取决于函数被调用的方式。 掌握了 this 绑定的四大规则,你就能掌控全局,写出更加清晰、可维护的代码。

最后,送给大家一句话:理解 this,才能更好地理解 JavaScript。

祝大家编程愉快!💻 🎉

发表回复

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