JS 的 this:一个你以为很深,但其实只是“迷路”了的指针

74 阅读5分钟

如果 JS 有朋友圈,this 一定是那个天天被骂、但大家又离不开的家伙。

“谁调用我,我就指向谁”“普通函数我就指向 window”“严格模式我直接罢工 undefined”“用 call/apply 我乖得像猫”“new 一下我就开始生孩子”。

this:我真的尽力了。

今天我们就来把 JS 里最令人困惑的 this,讲得像看段子一样轻松,却像源码一样靠谱。


🍿 一、为什么 JS 的 this 这么奇怪?(历史背锅现场)

首先你得知道一件事:

🧑‍💻 JavaScript 是 10 天发明的语言

Brendan Eich 当年使命是“把 Scheme 塞进浏览器”,结果为了兼容 Java 风格,又不得不加上一些看似“面向对象”的特性。

于是——

JS 需要一个像面向对象语言那样的 this,但它其实根本不是 OOP 语言。

更致命的是:

❗this 不是在编译期决定,而是在“你怎么调用函数”时决定

这就导致:

  • 词法作用域靠声明位置决定(编译期)
  • this 靠调用方式决定(运行期)

它是整个 JS 里唯一一个不遵守词法作用域规则的特例

你写在哪里不重要,你怎么叫它才重要。


🎯 二、作用域与自由变量:this 为什么不走“词法作用域”?

理解 this 前,你必须先弄懂 自由变量查找

function foo(){
  let myName = '极客时间';
  return bar.printName;
}

👇 printName 里的变量查找规则

JS 查变量走的是:

  1. 自己作用域里找
  2. 找外层词法作用域
  3. 再沿着作用域链查找

这叫 lexical scope(词法作用域)。

BUT——this 不在这个规则里。

它不是词法变量,不靠代码写在哪里决定。

它靠啥?

👇👇👇


🍕 三、this = “谁调用我,我就指向谁”

看下面经典例子:

var myName = '极客邦';

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

var fn = bar.printName;
fn();     // → ??? 
bar.printName();  // → ???

答案:

  • fn()'极客邦'(因为 this → window)
  • bar.printName()'time.geekbang.com'

为什么?

🎤 规则:函数的 this 由“调用位置”决定,不是由“定义位置”决定。

这就是 JS 最“反直觉”的地方。


🧨 四、this 指向规则大全

① 作为对象方法调用 → this 指向该对象

bar.printName(); // this → bar

② 普通函数调用 → this 指向全局(严格模式为 undefined)

function foo(){
  console.log(this); // strict: undefined
}

foo();

为什么早期要让它指向全局?

因为 JS 作者为了省事:函数必须有 this,总不能让它空着,于是干脆指向 window。

但这导致了:

  • 全局变量污染
  • window 上挂了无数奇怪属性
  • 技术债一路累到 ES6

③ call / apply / bind 手动绑定 this(至尊 VIP 通道)

function foo(){
  this.myName = '极客时间';
}

let obj = { myName: '极客邦' };

foo.call(obj);
console.log(obj.myName); // '极客时间'

call 和 apply 区别?

方法作用
call(this, arg1, arg2)逐个传参
apply(this, [args])数组传参

④ 构造函数(new) → this 指向新创建的实例

function CreateObj(){
  this.name = '极客时间';
}

let o = new CreateObj();
// this → o

其实 new 做了四件事:

  1. 创建一个对象 {}
  2. 让它继承原型
  3. 把 this 指向它
  4. 自动 return this

所以构造函数里的 this 毫无争议。


⑤ 事件监听里的 this → 事件绑定的元素

document.getElementById('link')
  .addEventListener('click', function(){
    console.log(this); // 当前 DOM
  });

🌈 六、为什么 this 经常“迷路”?(经典误区解析)

误区 1:方法赋值给变量 → this 就丢了

var foo = myObj.showThis;
foo(); // this → window

因为现在它变成“普通函数调用”了。


误区 2:回调函数里的 this 指向 window

setTimeout(myObj.showThis, 1000);

解决办法?

✔ 用 bind
✔ 或者箭头函数


误区 3:箭头函数没有自己的 this

箭头函数的 this → 词法作用域决定(外层的 this)

let obj = {
  name: '极客时间',
  say: () => {
    console.log(this); // window,不是 obj
  }
};

🚀 七、为什么没有 class 的时代 this 更“灾难”?

早期 JS 没有 class,所有面向对象场景都靠 this:

function Car(name){
  this.name = name;
}
  • 函数可作为构造器
  • 函数可作为普通函数
  • 函数可作为对象方法
  • 函数可作为事件回调

同一个函数,四种 this,谁受得了?

这也是 ES6 加 class、箭头函数 的原因:

class 让 this 行为更符合 OOP
箭头函数让回调里的 this 不再迷路


🌟 八、综合案例:你以为访问的是 time.geekbang.com,但 this 却带你去了 window

你的代码:

var myName = '极客邦';

var bar = {
  myName:'time.geekbang.com',
  printName:function(){
    console.log(myName);       // 自由变量:极客邦
    console.log(bar.myName);   // time.geekbang.com
    console.log(this);         // window(被 foo 返回后成为普通函数)
    console.log(this.myName);  // 极客邦
  }
};

function foo(){
  let myName = '极客时间';
  return bar.printName;
}

var print = foo();
print();

解析:

代码原因
console.log(myName)词法作用域 → 找到全局的 '极客邦'
console.log(bar.myName)显式访问对象
this → windowprint() 是普通函数调用
this.myName'极客邦'因为 window.myName = var myName

你以为你访问的是对象,结果 this 给你带偏了。


🎁 九、总结:理解 this,只需要记住一行话

this 的指向,永远由“调用位置”决定,而不是“函数在哪里定义”。

再补一条:
箭头函数没有 this,完全依赖外层作用域。

如果你能记住以下 5 条:

场景this 指向
普通函数window / undefined
对象方法该对象
构造函数 new实例对象
call/apply/bind指定对象
事件回调DOM 元素
箭头函数外层作用域的 this

恭喜你,你已经打通 JS this 的任督二脉。


🔥 十、最后:为什么 this 很重要?

因为它是:

  • OOP 里对象方法的灵魂
  • 构造函数背后实例机制的核心
  • 事件回调中指向 DOM 的方式
  • JS 早期“补丁式 OOP”的产物
  • 理解 class / 原型链 / 闭包的基础

懂 this,你就懂 JS 的半个世界。