侦探手册:破解 JavaScript 中 this 的"身份"谜案
重要通告 📢
为了教学直观,本文使用了中文变量名,但在实际开发中 危险动作,请勿模仿!!!
1. 核心原理:舞台决定角色 🎭
在JavaScript的江湖中,有一个让无数开发者又爱又恨的角色——this。它像一位善变的演员,在不同的舞台上扮演不同的角色;又像一个精明的变色龙,随着环境变化而改变自己的颜色。
想象这样一个场景:
const 演员 = { 名字: "小明", 表演: function() { console.log(`大家好,我是${this.名字}`); } }; 演员.表演(); // "大家好,我是小明" const 另一个舞台 = 演员.表演; 另一个舞台(); // "大家好,我是undefined"(或者指向window)同一个函数,两次表演,两个完全不同的结果!
这就是this的魔力——它不关心"你是谁",只关心"谁在调用你"。
1.1 什么是"舞台"?——调用位置的重要性
this的指向不是由函数定义决定的,而是由函数被调用时的位置和方式决定的。就像一个演员在不同的剧场会有不同的表现。
当JavaScript引擎调用一个函数时,它会创建一个执行上下文,这个上下文就是函数的"表演舞台",记录了:
- 演出时间(何时调用)
- 演出方式(如何调用)
- 观众(传入的参数)
- 主角
this的指向
// 让我们追踪一个函数的多个"演出邀请"
function 自我介绍() {
console.log("本次演出主办方:", this.主办方 || "无名剧场");
}
const 北京剧院 = {
主办方: "北京大剧院",
节目: 自我介绍
};
const 上海剧场 = {
主办方: "上海艺术中心",
邀请演出: 自我介绍
};
// 不同的邀请方式,不同的结果
自我介绍(); // 直接邀请 → "本次演出主办方:无名剧场"
北京剧院.节目(); // 北京剧院邀请 → "本次演出主办方:北京大剧院"
上海剧场.邀请演出(); // 上海剧场邀请 → "本次演出主办方:上海艺术中心"
1.2 追踪调用栈:找出真正的"邀请人"
当函数嵌套调用时,我们需要追踪完整的调用栈来找到最终决定this指向的那个"邀请人":
function 开场白() {
console.log("【开场】主办方:", this.剧场名 || "街头表演");
中场节目();
}
function 中场节目() {
console.log("【中场】主办方:", this.剧场名 || "街头表演");
压轴戏();
}
function 压轴戏() {
console.log("【压轴】主办方:", this.剧场名 || "街头表演");
}
const 国家大剧院 = {
剧场名: "国家大剧院",
演出安排: 开场白
};
国家大剧院.演出安排();
// 输出结果:
// 【开场】主办方:国家大剧院
// 【中场】主办方:街头表演
// 【压轴】主办方:街头表演
为什么结果不同?
因为只有开场白是国家大剧院直接邀请的,而中场节目和压轴戏是"自己跑上台"的,没有明确的邀请人。
2. 破除迷信:两个常见误区 ❌
在深入探索之前,我们先破除两个关于this的常见迷信:
2.1 误区一:this是函数自己的名片 ❌
function 自报家门() {
console.log("我的名字是:", this.name);
console.log("我真的是我自己吗?", this === 自报家门);
}
自报家门.name = "张三";
自报家门();
// 我的名字是:undefined
// 我真的是我自己吗?false
真相:this不是函数的名片,而是演出邀请函上的主办方名称。
2.2 误区二:this能看到函数的后台 ❌
function 后台密谈() {
const 秘密台词 = "这是后台秘密";
function 上台表演() {
console.log("我能看到秘密吗?", this.秘密台词); // undefined
}
上台表演();
}
后台密谈();
真相:this只关心舞台前的邀请人,不关心后台的秘密。
3. 四大绑定法则:this的表演合同 📜
现在,让我们看看this如何根据不同的"演出合同"来确定自己的角色。
3.1 法则一:默认合同(Default Binding)
没有明确邀请人,就默认在街头表演
function 街头艺人() {
console.log("我在哪里表演?", this === window ? "街头(全局)" : "专业剧场");
}
街头艺人(); // "我在哪里表演? 街头(全局)"
3.1.1 严格模式下的特殊规定:
"use strict";
function 严格艺人() {
console.log("严格模式下我在哪?", this); // undefined
}
严格艺人(); // undefined(连街头都不让表演了)
3.2 法则二:隐式合同(Implicit Binding)
谁邀请我,我就为谁表演
function 表演节目() {
console.log(`我在${this.剧场名}为您表演`);
}
const 歌剧院 = {
剧场名: "巴黎歌剧院",
今晚节目: 表演节目
};
歌剧院.今晚节目(); // "我在巴黎歌剧院为您表演"
3.2.1 多层邀请:听从最直接的邀请人
const 文化部 = {
名称: "国家文化部",
下属单位: {
名称: "北京艺术团",
安排演出: 表演节目
}
};
文化部.下属单位.安排演出(); // "我在北京艺术团为您表演"
为什么不是文化部?
因为最后的直接邀请是"下属单位",this总是听从最后一个发出邀请的人。
3.2.2 合同丢失的陷阱
陷阱一:转介绍失效
const 原邀请 = 歌剧院.今晚节目;
原邀请(); // "我在undefined为您表演,哈哈哈"
为什么失效?
因为原邀请只是拿到了演出内容,失去了原邀请方的身份。
陷阱二:延期演出问题
setTimeout(歌剧院.今晚节目, 1000);
// 1秒后输出:"我在undefined为您表演"
模拟setTimeout的内部机制:
// 想象setTimeout是这样工作的:
function 简易setTimeout(演出内容, 等待时间) {
// 等待...
// 时间到!
演出内容(); // 直接调用,没有邀请方!
}
3.3 法则三:显式合同(Explicit Binding)
强制指定演出场地
function 巡回演出(开场词) {
console.log(`${开场词},我现在在${this.城市}演出`);
}
const 场地A = { 城市: "纽约" };
const 场地B = { 城市: "伦敦" };
// call:逐个传递参数
巡回演出.call(场地A, "女士们先生们"); // "女士们先生们,我现在在纽约演出"
// apply:数组传递参数
巡回演出.apply(场地B, ["Ladies and Gentlemen"]); // "Ladies and Gentlemen,我现在在伦敦演出"
3.3.1 call与apply方法
两者的区别主要在参数传递方式:
call():参数逐个传递apply():参数以数组形式传递
3.3.2 终身合同:bind绑定
const 纽约专属演出 = 巡回演出.bind(场地A, "欢迎来到");
纽约专属演出(); // "欢迎来到,我现在在纽约演出"
setTimeout(纽约专属演出, 2000); // 2秒后依然是纽约
bind的工作原理:
// bind就像签了一份终身合同
function 模拟Bind(演出内容, 固定场地, ...固定台词) {
return function(...现场台词) {
// 无论何时何地演出,都必须在这个场地
return 演出内容.apply(固定场地, [...固定台词, ...现场台词]);
};
}
3.4 法则四:新建合同(New Binding)
创建全新的演出团队
function 创建剧团(剧团名) {
// new调用时发生的魔法:
// 1. 创建新团队 {}
// 2. this指向这个新团队
this.名称 = 剧团名;
this.介绍 = function() {
console.log(`我们是${this.名称}剧团`);
};
// 3. 自动返回这个新团队
}
const 太阳剧团 = new 创建剧团("太阳马戏团");
太阳剧团.介绍(); // "我们是太阳马戏团剧团"
4. 合同优先级大赛 🏆
当多个合同冲突时,this会听从优先级最高的:
new合同 > 显式合同 > 隐式合同 > 默认合同
function 合同测试() {
console.log("我听从:", this.老板);
}
const 甲方 = { 老板: "隐式老板", 测试: 合同测试 };
const 乙方 = { 老板: "显式老板" };
// 隐式合同
甲方.测试(); // "我听从:隐式老板"
// 显式合同更强
甲方.测试.call(乙方); // "我听从:显式老板"
// new合同最强
const 全新团队 = new 甲方.测试();
console.log(全新团队.老板); // undefined(全新团队,没有老板)
5. 特殊演员:箭头函数 🏹
箭头函数是个另类演员,它不关心演出时的邀请人,只记得自己排练时的环境。
const 传统剧团 = {
名称: "传统艺术团",
排练: function() {
console.log("传统方法排练:", this.名称);
const 箭头演员 = () => {
console.log("箭头演员记得:", this.名称);
};
const 普通演员 = function() {
console.log("普通演员看现在:", this?.名称 || "无主");
};
箭头演员(); // 记得排练时的环境
普通演员(); // 看现在的邀请人
}
};
传统剧团.排练();
6. 实战侦探手册 🔍
遇到this谜题时,拿出你的侦探手册:
- 是箭头函数吗? → 是:查它的排练记录(定义时的环境)
- 有new合同吗? → 有:新建的团队就是this
- 有call/apply/bind吗? → 有:合同指定的场地就是this
- 有对象邀请吗? → 有:邀请方就是this
- 以上都没有? → 默认街头表演(全局或undefined)
7. 综合实战演练
const 演出季 = {
名称: "2024春季演出季",
启动: function() {
console.log("1. 启动方:", this.名称);
const 箭头节目 = () => {
console.log("2. 箭头节目在:", this.名称);
};
function 传统节目() {
console.log("3. 传统节目在:", this?.名称 || "街头");
}
setTimeout(function() {
console.log("4. 延时传统节目:", this?.名称 || "街头");
}, 10);
setTimeout(() => {
console.log("5. 延时箭头节目:", this.名称);
}, 10);
箭头节目();
传统节目();
}
};
演出季.启动();
侦探报告:
- ✅ 启动方:2024春季演出季(隐式合同)
- ✅ 箭头节目在:2024春季演出季(记得排练环境)
- ❌ 传统节目在:街头(没有邀请人)
- ❌ 延时传统节目:街头(setTimeout直接调用)
- ✅ 延时箭头节目:2024春季演出季(记得排练环境)
8. 最终真相:this的完整法则 📜
| 合同类型 | 签约方式 | this指向 | 核心原则 |
|---|---|---|---|
| 默认合同 | 直接调用 | 全局/undefined | 没有邀请人,街头表演 |
| 隐式合同 | 对象.方法() | 调用对象 | 谁邀请,为谁演 |
| 显式合同 | call/apply/bind | 指定对象 | 合同说了算 |
| 新建合同 | new 函数() | 新对象 | 创建新团队 |
| 特殊演员 | 箭头函数 | 定义时环境 | 记得排练时 |
9. 结语:掌握this,掌握JavaScript的舞台
this并不神秘,它只是一个严格遵守演出合同的演员。理解了它的四种合同和一种特殊情况,你就能:
- ✅ 预判
this在任何场景下的表现 - ✅ 避免常见的
this陷阱 - ✅ 写出更加清晰可靠的代码
- ✅ 在面试中游刃有余地回答相关问题
记住,this的黄金法则依然是:对于普通函数,this指向最后调用它的对象,但务必记住四个例外情况。
现在,你已经是一名合格的this侦探了!
带上你的知识,去JavaScript的世界里解决更多的this谜题吧!🎉
思考题:你能解释下面代码的输出结果吗?
const obj = {
value: 'obj',
createArrow: () => {
console.log(this.value);
},
createFunction: function() {
return () => {
console.log(this.value);
};
}
};
obj.createArrow();
obj.createFunction()();
欢迎在评论区分享你的分析! 💬