好的,我们正式进入 JavaScript 的另一个深水区,这也是面向对象编程的基石:this
关键字。
this
是 JavaScript 中最强大、最灵活,也最容易引起困惑的概念之一。但别担心,它的行为遵循一套清晰的规则。一旦你理解了这些规则,就能完全驾驭它。
this
的指向 - 解开JS中最微妙的谜题核心法则: 在学习 this
之前,请先记住这条黄金法则:
this
的值取决于函数被调用时的“执行上下文”(即函数是如何被调动的),而不是函数被定义时的位置。
学习目标:
this
(隐式绑定)。this
(默认绑定),以及 this
“丢失”的常见场景。this
的特殊行为(词法绑定)。️ 任务:
这个练习非常特别。你的任务不是编写代码,而是预测下面代码片段中每一行 console.log
的输出结果。
请在运行代码之前,仔细阅读代码和注释,然后根据你对 this
规则的理解,在心中或纸上写下你认为的答案。这个“预测”的过程本身就是最有价值的学习环节。
预测代码 (08-this-keyword.js
):
创建文件 08-this-keyword.js
,将以下代码复制进去。
// 在全局作用域下,严格模式(模块默认就是)下 this 是 undefined
// 非严格模式下是 window 对象。我们假设在模块环境中。
console.log("全局 this:", this);
const user = {
name: "Alex",
skills: ['React', 'Node', 'CSS'],
// 场景1: 作为对象的方法被调用
greet() {
console.log(`你好,我是 ${this.name}。`);
},
// 场景2: 箭头函数作为对象的方法
showSkills: () => {
// 箭头函数没有自己的 this,它会捕获其定义时所在作用域的 this
console.log(`我的技能: ${this.name} 知道吗?`, this);
},
// 场景3: 方法内部的普通函数
describe() {
console.log(`${this.name} 在描述自己...`);
function innerFunction() {
// 这是一个独立的函数调用,它的 this 指向什么?
console.log(`在 innerFunction 中,this.name 是 ${this.name}`, this);
}
innerFunction();
}
};
// --- 开始预测 ---
console.log("--- 调用 1: 方法调用 ---");
// 问: 下面这行会输出什么?
user.greet();
console.log("\n--- 调用 2: '丢失 this' 的情况 ---");
// 我们把 user.greet 方法赋值给一个独立的变量
const standaloneGreet = user.greet;
// 问: 现在这样调用,会输出什么?为什么?
standaloneGreet();
console.log("\n--- 调用 3: 箭头函数的 this ---");
// 问: 调用箭头函数方法,会输出什么?
user.showSkills();
console.log("\n--- 调用 4: 嵌套函数的 this ---");
// 问: 调用 describe 方法后,其内部的 innerFunction 会输出什么?
user.describe();
请写下你对4次调用的预测结果:
user.greet()
):standaloneGreet()
):user.showSkills()
):user.describe()
内部的 innerFunction
):这是一个纯粹的“概念检查”练习。挑战在于应用我们刚刚讨论的规则。不要急着运行代码!当你预测完毕,准备好对答案并看详细解析时,就告诉我。
好的,我们来核对答案并深入解析其中的奥秘。这是一个坎,迈过去之后,你对 JavaScript 的理解会上升一个全新的层次。
全局 this: undefined
--- 调用 1: 方法调用 ---
你好,我是 Alex。
--- 调用 2: '丢失 this' 的情况 ---
TypeError: Cannot read properties of undefined (reading 'name')
--- 调用 3: 箭头函数的 this ---
我的技能: undefined 知道吗? undefined
--- 调用 4: 嵌套函数的 this ---
Alex 在描述自己...
TypeError: Cannot read properties of undefined (reading 'name')
你的预测和这个结果相符吗?无论是否相符,我们一起来看看到底是为什么。
user.greet()
你好,我是 Alex。
user.
这个点),this
就被“绑定”到了这个点前面的对象上。所以,在 greet
函数内部,this
就是 user
对象。因此 this.name
就是 'Alex'
。standaloneGreet()
TypeError: Cannot read properties of undefined (reading 'name')
this
最常见的“陷阱”。当我们执行 const standaloneGreet = user.greet;
时,我们只是把 greet
函数本身赋值给了 standaloneGreet
这个变量,它与 user
对象的关联就此断开了。standaloneGreet()
时,它是一个独立的、没有任何上下文对象的函数调用。在这种情况下,默认绑定规则生效:
this
会被设为 undefined
。this.name
就变成了 undefined.name
,从而抛出 TypeError
。user.showSkills()
我的技能: undefined 知道吗? undefined
this
(Lexical this
)this
规则里的一个“异类”。它没有自己的 this
。它会像普通变量一样,捕获其定义时所在作用域的 this
。
showSkills: () => { ... }
这个箭头函数是在全局(或模块顶层)作用域中定义的(作为 user
对象字面量的一部分)。this
是 undefined
。showSkills
内部的 this
就永远是 undefined
,无论它将来如何被调用。因此 this.name
就是 undefined.name
,结果为 undefined
。user.describe()
Alex 在描述自己...
TypeError: Cannot read properties of undefined (reading 'name')
user.describe()
的调用符合隐式绑定规则,所以 describe
方法内部的 this
是 user
对象。因此第一个 console.log
成功输出了 Alex 在描述自己...
。describe
方法内部,innerFunction()
是作为一个独立的、没有任何上下文的函数被调用的。这和“调用2”的情况完全一样!因此,innerFunction
内部的 this
遵循默认绑定规则,值为 undefined
,从而在访问 this.name
时抛出 TypeError
。this
?既然我们知道了问题所在,那么如何解决“调用2”和“调用4”中的问题呢?
使用 .bind()
(显式绑定)
.bind()
方法可以创建一个新函数,并永久地将 this
绑定到指定的对象上。
// 修正调用2
const standaloneGreet = user.greet.bind(user);
standaloneGreet(); // 输出: 你好,我是 Alex。
使用箭头函数 (利用词法this
)
在 ES6 之后,解决嵌套函数 this
问题的最佳方法就是使用箭头函数,因为它能自动捕获我们想要的 this
。
// 修正调用4
describe() {
console.log(`${this.name} 在描述自己...`);
const innerFunction = () => { // 改为箭头函数!
// 它会自动捕获外层 describe 方法的 this,也就是 user 对象
console.log(`在 innerFunction 中,this.name 是 ${this.name}`);
}
innerFunction();
}
// user.describe() 会输出:
// Alex 在描述自己...
// 在 innerFunction 中,this.name 是 Alex
this
的指向是一个纯粹的规则问题,记住“函数是如何被调用的”这个核心,就能解决大部分问题。
我们已经攻克了 this
这个难关。下一个练习,我们将学习 class
语法,它是 ES6 提供的、用于创建对象的“语法糖”,能帮助我们更优雅地组织代码和处理 this
,让你更接近传统的面向对象编程。
准备好后,我们就可以开始了!