this 指向大揭秘:为什么你的方法总在“认错爹”?🌟

42 阅读6分钟

🌟深入理解 JavaScript 中的 this:从设计缺陷到最佳实践

在 JavaScript 的世界里,this 是一个既基础又容易让人困惑的概念。它不像其他语言那样遵循固定的规则,而是由函数的调用方式决定——这一设计被不少人认为是 JavaScript 早期的一个“历史包袱”。本文将结合你提供的学习笔记和代码示例,系统梳理 this 的行为机制、常见陷阱以及如何优雅地应对。


🔍 一、自由变量与词法作用域(Lexical Scope)

在深入 this 之前,我们先明确一个关键概念:自由变量的查找依赖于词法作用域

  • 词法作用域(Lexical Scope) :指的是变量的作用域是在代码编写时就确定的,而不是运行时。
  • 自由变量:在函数内部使用,但既不是参数也不是局部变量,而是来自外层作用域的变量。
let myName = "极客邦";
function foo() {
    console.log(myName); // 自由变量,从外层作用域查找
}

这种查找机制是静态的,编译阶段就决定了变量的查找路径(通过作用域链)。而 this 却是个例外!

💡 关键区别

  • 变量查找 → 编译期(词法作用域)
  • this 指向 → 运行期(调用方式决定)

⚠️ 二、JavaScript 中 this 的“不良设计”

正如笔记中所说,this 的设计确实存在争议:

“JS 做了一个不好的设计:this 由函数的调用方式决定。”

为什么说这是个问题?

早期 JavaScript 没有 class,为了模拟面向对象编程(OOP),开发者依赖函数和 this 来访问对象属性。理想情况下,方法内部的 this 应该自动指向所属对象。但现实是:

  • 如果把方法赋值给一个变量再调用this 就会丢失!
  • 在非严格模式下,this 默认指向全局对象(如浏览器中的 window),导致意外污染。

示例分析

来看你提供的代码片段:

var bar = {
    myName: "time.geekbang.com",
    printName: function () {
        console.log(myName);        // ❌ 自由变量,查全局 → "极客邦"
        console.log(bar.myName);    // ✅ 显式访问对象属性
        console.log(this);          // 取决于调用方式!
        console.log(this.myName);   // 可能 undefined!
    }
};

var myName = "极客邦"; // 挂载到 window.myName
var _printName = foo(); // foo 返回 bar.printName 函数引用
_printName(); // 普通函数调用!

_printName() 被当作普通函数调用时:

  • this 指向 window(非严格模式)
  • this.myName 实际是 window.myName → 输出 "极客邦"
  • myName 作为自由变量,也从全局作用域找到 "极客邦"

结果:你本想打印 "time.geekbang.com",却得到了 "极客邦"!😱

📌 这就是 this 丢失的经典场景


🧠 三、this 的五种绑定规则

JavaScript 中 this 的指向不是在函数定义时确定的,而是在函数被调用时根据调用方式动态决定的。为了系统掌握 this,我们必须理解它的五种绑定规则,并牢记它们的优先级顺序

优先级从高到低
1. new 绑定2. 显式绑定3. 隐式绑定4. 默认绑定5. 箭头函数(无独立 this)

下面我们逐一详解,并配以可运行的完整代码。


1️⃣ new 绑定(构造函数调用)

当函数通过 new 关键字调用时,会创建一个新对象,并将 this 绑定到这个新对象上。

function User(name) {
    this.name = name;
    this.greet = function() {
        console.log(`Hello, I'm ${this.name}`);
    };
}

const alice = new User("Alice");
alice.greet(); // 输出: Hello, I'm Alice
console.log(alice instanceof User); // true

关键点

  • new 调用时,JS 引擎会:

    1. 创建一个空对象;
    2. 将该对象的原型链接到构造函数的 prototype
    3. 将 this 指向这个新对象;
    4. 执行构造函数体;
    5. 如果构造函数没有显式返回对象,则自动返回 this
  • 此时 this 永远指向新创建的实例。


2️⃣ 显式绑定(call / apply / bind

开发者可以主动指定 this 的值,使用 Function.prototype 上的方法:

const person = {
    name: "Bob"
};

function introduce() {
    console.log(`My name is ${this.name}`);
}

// call:传入参数列表
introduce.call(person); // 输出: My name is Bob

// apply:传入参数数组
introduce.apply(person); // 同样输出: My name is Bob

// bind:返回一个新函数,this 被永久绑定
const boundIntroduce = introduce.bind(person);
boundIntroduce(); // 输出: My name is Bob

关键点

  • call 和 apply 立即执行函数;
  • bind 不执行,而是返回一个 this 已绑定的新函数(常用于事件回调、setTimeout 等场景);
  • 显式绑定的优先级高于隐式和默认绑定。

💡 示例:修复 this 丢失问题

const print = bar.printName.bind(bar);
print(); // 即使作为普通函数调用,this 仍指向 bar

3️⃣ 隐式绑定(作为对象方法调用)

当函数作为对象的属性(方法)被调用时,this 指向该对象。

const car = {
    brand: "Tesla",
    start: function() {
        console.log(`${this.brand} is starting...`);
    }
};

car.start(); // 输出: Tesla is starting...

⚠️ 但要注意“隐式丢失”问题

const startCar = car.start; // 仅复制函数引用
startCar(); // ❌ this 指向全局(非严格模式)或 undefined(严格模式)
// 输出: undefined is starting... (严格模式下报错)

关键点

  • 只有直接通过对象调用obj.method())才算隐式绑定;
  • 一旦函数被赋值给变量、作为参数传递,就变成普通函数调用,触发默认绑定。

4️⃣ 默认绑定(普通函数调用)

当函数独立调用(不通过对象、不使用 new、未显式绑定),则触发默认绑定。

var globalName = "Global"; // 注意:var 会挂载到 window

function sayName() {
    console.log(this.globalName);
}

sayName(); // 非严格模式 → 输出: Global(this === window)

但在严格模式下:

'use strict';

function sayNameStrict() {
    console.log(this); // undefined
    // console.log(this.globalName); // TypeError!
}

sayNameStrict(); // 报错:Cannot read property 'globalName' of undefined

关键点

  • 非严格模式:this → 全局对象(浏览器中是 window)
  • 严格模式:this → undefined
  • 这也是为什么推荐始终使用 'use strict' —— 避免意外污染全局。

5️⃣ 箭头函数(没有自己的 this

箭头函数不绑定自己的 this,而是继承外层作用域的 this(词法 this)。

const team = {
    members: ["Alice", "Bob"],
    leader: "Carol",
    
    report: function() {
        // 普通函数:this 指向 team
        this.members.forEach(function(member) {
            // ❌ 普通回调函数:this 指向全局(非严格模式)
            console.log(`${member} reports to ${this.leader}`); // this.leader → undefined
        });
    },
    
    reportFixed: function() {
        // ✅ 使用箭头函数:this 继承自 reportFixed 的 this(即 team)
        this.members.forEach((member) => {
            console.log(`${member} reports to ${this.leader}`); // 正确输出
        });
    }
};

team.report();      // 输出: Alice reports to undefined
team.reportFixed(); // 输出: Alice reports to Carol, Bob reports to Carol

关键点

  • 箭头函数的 this 在定义时就确定了,无法被 call/apply/bind 改变;
  • 适合用在回调函数、定时器、数组方法等需要保留外层 this 的场景;
  • 不要在对象方法或构造函数中使用箭头函数(会失去动态 this 能力)。

🔁 优先级总结(实战验证)

function foo() {
    console.log(this.name);
}

const obj1 = { name: "Obj1", fn: foo };
const obj2 = { name: "Obj2" };

// 测试:显式绑定 vs 隐式绑定
obj1.fn.call(obj2); // 输出: Obj2 → 显式绑定优先级更高!

// 测试:new 绑定最高
function Bar() {
    this.name = "New Instance";
}
Bar.prototype.fn = foo;

const instance = new Bar();
instance.fn();          // 输出: New Instance(隐式绑定)
instance.fn.call(obj2); // 输出: Obj2(显式绑定)
new (instance.fn.bind(obj2))(); // 即使 bind 了,new 仍创建新对象 → 输出: undefined(因为 bind 后的函数没有设置 this.name)

📌 记住口诀
“New 最强,Call/Apply/Bind 紧随其后,对象方法第三,普通函数垫底,箭头函数另辟蹊径。”


通过以上五种绑定规则的完整剖析,相信你已经能够自信地判断任何场景下 this 的指向了!接下来,只需在实践中多加注意调用方式,就能彻底告别 this 的困惑 😊

🛡️ 四、如何避免 this 陷阱?

✅ 使用严格模式

'use strict';
// 普通函数调用时 this 为 undefined,避免意外污染

✅ 避免将方法赋值给变量

如果必须传递方法,使用 .bind()

const safePrint = bar.printName.bind(bar);
safePrint(); // this 正确指向 bar

✅ 用箭头函数处理回调(谨慎!)

箭头函数没有自己的 this,适合用在不需要动态 this 的场景:

class Timer {
    constructor() {
        this.seconds = 0;
        setInterval(() => {
            this.seconds++; // ✅ 箭头函数继承 class 的 this
        }, 1000);
    }
}

⚠️ 但不要在对象方法中滥用箭头函数,否则会失去 this 指向对象的能力!

✅ 优先使用 let/const 而非 var

  • var 声明的变量会挂载到 window,加剧 this 混乱
  • let/const 不会污染全局对象,更安全

🎯 五、回到最初的问题:如何拿到 time.geekbang.com

你问:“如果要拿到 time.geekbang.com 怎么办?”

正确做法:

// 方案1:始终通过对象调用
bar.printName(); // this → bar → 输出正确

// 方案2:显式绑定
_printName.call(bar);

// 方案3:在函数内部避免依赖 this,直接用 bar.myName(不推荐,耦合强)

// 方案4:改写为箭头函数?不行!因为箭头函数无法通过 this 访问 bar

最佳实践:确保方法总是以 obj.method() 形式调用,或使用 .bind() 固定上下文。


🧩 六、执行上下文与 this

每次函数调用都会创建一个执行上下文(Execution Context) ,包含:

  • 变量环境(Variable Environment)
  • 词法环境(Lexical Environment)
  • this 绑定

this 的值就是在进入执行上下文时,根据调用点(call-site) 动态确定的,与函数定义位置无关。

🔍 小技巧:判断 this 时,永远问自己——“这个函数是谁调用的?


🎉 结语

this 虽然源于早期设计的妥协,但只要掌握其动态绑定机制五种规则,就能化繁为简。记住:

🔸 变量查找看定义(词法作用域)
🔸 this 指向看调用(动态绑定)

配合严格模式、bind、箭头函数等现代工具,你完全可以写出清晰、健壮的代码。随着 ES6+ 类语法的普及,this 的使用也更加规范。

希望这篇博客帮你彻底理清 this 的来龙去脉!🚀