JavaScript 中的 this:一个“看人下菜碟”的调皮指针

59 阅读5分钟

你有没有觉得,JavaScript 的 this 就像一个职场老油条?
——谁叫它,它就听谁的;没人管,它就躺平装死(或者干脆报错)。

今天,我们就来扒一扒这个 JS 世界里最“见风使舵”的关键字:this
它不是变量,不是函数,甚至不是个正经对象——但它偏偏无处不在,还总在你最意想不到的时候给你整出点“惊喜”。


🧠 this 是什么?

简单说:this 是一个指针,指向当前函数的调用者

但问题来了—— “谁调用了我?” 这个问题的答案,直接决定了 this 指向谁。
JS 不像其他语言(比如 Java 或 C++),它的 this 不是在写代码时就定死的,而是在运行时根据调用方式动态决定的!

这就导致了一个经典场景:

var bar = {
  myName: 'time.geekbang.com',
  printName: function () {
    console.log(this.myName);
  }
};

bar.printName(); // ✅ 输出 "time.geekbang.com"

let fn = bar.printName;
fn(); // ❌ 输出 undefined(严格模式)

同一个函数,换个姿势调用,this 就变了脸!
这哪是编程语言?分明是川剧变脸!


🔍 this 的五种常见命运

1️⃣ 作为对象的方法被调用 → this 指向该对象

obj.method(); // this === obj

这是最“正常”的情况,符合直觉:方法属于谁,this 就是谁。


2️⃣ 作为普通函数被调用 → this 指向全局对象(或 undefined

function foo() { console.log(this); }
foo(); 
// 非严格模式:this === window(浏览器)
// 严格模式:this === undefined 💥

早期 JS 设计者偷了个懒:“反正总得有个 this,那就指向全局吧!”
结果导致无数 bug —— 函数里不小心用了 this.xxx,结果污染了 window

后来有了 严格模式('use strict') ,终于让这种“裸奔调用”直接报错,算是亡羊补牢。

📌 小知识:var 声明的变量会自动挂到 window 上,let/const 不会。所以:

var myName = '极客邦';
console.log(window.myName); // '极客邦'
let yourName = '掘金';
console.log(window.yourName); // undefined

3️⃣ 用 call / apply / bind 强制指定 → this 听你的!

fn.call(obj);   // this === obj
fn.apply(obj);  // this === obj
const bound = fn.bind(obj); bound(); // this === obj

这是 JS 给你的一把“尚方宝剑”——你可以强行让 this 指向任何对象。
手动绑定,天下我有!


4️⃣ 作为构造函数被调用(new)→ this 指向新创建的实例

function Person(name) {
  this.name = name; // this 指向新对象
}
const p = new Person('小明');

这时候 this 就像个刚出生的婴儿,等着你给它赋值。
注意:如果构造函数 return 一个对象,this 就白忙活了!


5️⃣ 事件处理函数 → this 指向触发事件的 DOM 元素

<button onclick="handleClick()">点我</button>
<script>
function handleClick() {
  console.log(this); // <button> 元素!
}
</script>

在浏览器中,事件回调的 this 自动绑定到目标元素,非常贴心(但也容易让人混淆)。


🤯 自由变量 vs this:别搞混了!

很多人以为 this.myName 和直接写 myName 是一回事——大错特错!

  • myName 是自由变量,遵循词法作用域(Lexical Scope) ,编译阶段就确定了查找路径。
  • this.myName 是动态绑定,运行时才决定从哪个对象上取值。

举个栗子🌰:

var myName = '极客邦'; // 全局变量,挂到 window 上
var bar = {
  myName: 'time.geekbang.com',
  printName: function () {
    console.log(myName);      // ✅ 词法作用域 → '极客邦'
    console.log(this.myName); // ✅ 动态绑定 → 取决于谁调用!
  }
};

如果你把 printName 赋值给一个变量再调用:

let fn = bar.printName;
fn();

此时函数是以普通函数形式被调用的。在非严格模式下,this 会指向全局对象(浏览器中是 window)。

而由于之前使用了 var myName = '极客邦' ,这个变量会被自动挂载到 window 上,即:

window.myName === '极客邦'; // true

所以:

  • console.log(myName)
    → 查找的是词法作用域中的自由变量,结果是 '极客邦'(这是确定的、可预测的)。
  • console.log(this.myName)
    → 实际等价于 window.myName,结果也是 '极客邦'但这只是因为巧合:恰好有一个同名的全局变量被 var 声明了

⚠️ 关键区别在于

  • 第一个 '极客邦' 来自作用域链(静态绑定,代码结构决定);
  • 第二个 '极客邦' 来自全局对象的属性(动态绑定,运行时调用方式决定)。

如果换成 let myName = '极客邦',或者在严格模式下执行,或者在 Node.js 环境中,结果就会完全不同 —— 第二个可能变成 undefined 或直接报错。


💡 正确结论:

不要因为两个输出“看起来一样”,就认为它们是等价的。
这种“巧合一致”是脆弱的、不可靠的,一旦环境或声明方式改变,程序就会出错。
永远不要依赖 this 在全局调用时能“碰巧”访问到你想要的变量。

💡 所以:不要依赖全局变量和 this 的“巧合匹配” ,那是 bug 的温床!


🛑 为什么说 this 是个“不好的设计”?

正如你上传的 readme.md 中所说:

“JS 做了一个不好的设计:this 由函数的调用方式决定……JS 作者忘了处理情况,偷懒直接让 this 指向全局。”

确实,this 的行为违背了函数式编程的纯粹性——同一个函数,输入相同,输出却可能不同(因为 this 变了)。

现代 JS 已经提供了更好的替代方案:

  • 使用 箭头函数(没有自己的 this,继承外层)
  • 用 闭包 捕获变量
  • 用 模块化 避免全局污染
  • 用 class 明确方法归属(虽然 class 底层还是函数,但语义更清晰)

✅ 总结:如何驯服 this 这匹野马?

  1. 永远开启严格模式'use strict',避免 this 意外指向 window
  2. 明确调用方式:知道你是用 obj.fn() 还是 fn()
  3. 需要固定 this?用 bind
  4. 不确定 this 是谁?打印出来看!  console.log(this)
  5. 能不用 this 就不用:用参数传值、用闭包、用模块,代码更可靠。

🎉 最后彩蛋

:如果 this 会说话,它第一句会说什么?
:“Who the hell called me?

所以,下次写 JS 时,请对 this 多一分敬畏,少一分放纵。
毕竟——你永远不知道,下一个调用你函数的人,是不是想让你的 this 指向 null