《侦探手册:破解 JavaScript 中 this 的“身份”谜案》

179 阅读6分钟

侦探手册:破解 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谜题时,拿出你的侦探手册:

  1. 是箭头函数吗? → 是:查它的排练记录(定义时的环境)
  2. 有new合同吗? → 有:新建的团队就是this
  3. 有call/apply/bind吗? → 有:合同指定的场地就是this
  4. 有对象邀请吗? → 有:邀请方就是this
  5. 以上都没有? → 默认街头表演(全局或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);
        
        箭头节目();
        传统节目();
    }
};

演出季.启动();

侦探报告

  1. ✅ 启动方:2024春季演出季(隐式合同)
  2. ✅ 箭头节目在:2024春季演出季(记得排练环境)
  3. ❌ 传统节目在:街头(没有邀请人)
  4. ❌ 延时传统节目:街头(setTimeout直接调用)
  5. ✅ 延时箭头节目: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()();

欢迎在评论区分享你的分析! 💬