各位程序猿、攻城狮、代码界的艺术家们,晚上好!🌙
今天,咱们要一起深入探讨 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。
祝大家编程愉快!💻 🎉