如果 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 查变量走的是:
- 自己作用域里找
- 找外层词法作用域
- 再沿着作用域链查找
这叫 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 做了四件事:
- 创建一个对象
{} - 让它继承原型
- 把 this 指向它
- 自动 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 → window | print() 是普通函数调用 |
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 的半个世界。