各位程序猿、攻城狮、代码界的艺术家们,晚上好!🌙
今天,咱们要一起深入探讨 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。 这可以避免隐式丢失问题。 -
使用
that或self变量: 在方法内部,将this赋值给一个变量 (通常是that或self),然后在回调函数中使用该变量。
总结一下:
| 规则 | 适用场景 | this 指向 |
注意事项 |
|---|---|---|---|
| 隐式绑定 | 函数作为对象的方法被调用 | 调用该方法的对象 | 隐式丢失:函数赋值给变量、回调函数等情况可能导致 this 重新进行默认绑定。 |
解决方法:使用 bind 方法、箭头函数、that 或 self 变量等。 |
四、 显式绑定:指哪打哪,精准操控
显式绑定,顾名思义,就是我们可以明确地指定函数执行时 this 的值。 JavaScript 提供了三个方法来实现显式绑定:call、apply 和 bind。
-
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方法与call和apply方法不同,它不会立即调用函数,而是返回一个新的函数,该函数的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绑定是永久的,即使使用call或apply方法也无法修改。
总结一下:
| 规则 | 适用场景 | this 指向 |
特点 |
|---|---|---|---|
| 显式绑定 | 使用 call、apply 或 bind 方法 |
指定的对象 | call 和 apply 方法会立即调用函数,bind 方法返回一个新的函数,this 绑定是永久的。 |
call 方法的参数是参数列表,apply 方法的参数是参数数组,bind 方法可以预先绑定参数。 |
五、 new 绑定:我是你的构造器
new 绑定,是 this 绑定规则中比较特殊的一种。 当我们使用 new 关键字调用一个函数时,JavaScript 会执行以下步骤:
- 创建一个新的空对象。
- 将该空对象的原型 (
__proto__) 指向构造函数的prototype属性。 - 将该空对象作为
this绑定到构造函数中。 - 如果构造函数没有显式地返回一个对象,则返回该新对象。
简单来说,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 绑定的优先级如下:
new绑定- 显式绑定
- 隐式绑定
- 默认绑定
也就是说,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。
祝大家编程愉快!💻 🎉