🔥 JavaScript this 指向全解析:面试必考的 5 大绑定规则
📌 目标读者: 有基础 JS 语法经验、准备面试或想夯实基础的初中级前端开发者 ⏱️ 预计阅读: 10-15 分钟
this是 JavaScript 中最令人困惑的概念之一。它的值不是在函数声明时确定的,而是在函数调用时动态绑定的。本文将结合 MDN 权威文档和实际踩坑经验,带你彻底搞懂this的指向规则。
一、先看一个经典面试题
let name = "梅西";
let obj = {
name: "姆巴佩",
say: function () {
console.log(this.name);
},
};
const fn = obj.say;
fn(); // 输出什么?
obj.say(); // 输出什么?
很多人会脱口而出:都输出 "姆巴佩"。但实际结果是:
fn()→"梅西"(普通函数调用,this 指向 window)obj.say()→"姆巴佩"(作为对象方法调用,this 指向 obj)
核心结论:this 的值取决于函数如何被调用,而不是函数如何被定义。
二、前置知识:什么是严格模式?
文中多次提到"严格模式",这里先简单说明:
// 方式一:脚本顶部开启,整个文件生效
"use strict";
// 方式二:函数内部开启,仅该函数生效
function fn() {
"use strict";
}
严格模式的主要差异:普通函数调用时 this 为 undefined(非严格模式下是 window),变量必须声明后使用等。
💡 ES6 的
class语法内部自动处于严格模式,无需手动声明。这就是为什么 class 方法中this丢失时会直接报错。
三、this 的五大绑定规则
规则一:默认绑定 —— 普通函数调用
当函数直接调用(不带任何前缀),在非严格模式下 this 指向 window,严格模式下为 undefined。
function greet() {
console.log(this);
}
greet(); // 非严格模式 → window,严格模式 → undefined
⚠️ 注意:
var在全局作用域下声明的变量会挂载到window上,造成全局污染。函数内部的var不会。推荐统一使用let/const。
// 全局作用域下
var name = "张三"; // window.name = "张三" ❌ 污染全局
let age = 18; // 不会挂载到 window ✅
规则二:隐式绑定 —— 作为对象的方法调用(含事件处理)
当函数作为对象的方法被调用时,this 指向调用该方法的对象。
const obj = {
name: "zwq",
say: function () {
console.log(this);
console.log(`我是${this.name}`);
},
};
obj.say(); // this → obj,输出 "我是zwq"
但是! 如果把方法赋值给变量再调用,this 绑定就丢失了:
const fn = obj.say; // 赋值后脱离了对象,this 绑定丢失
fn(); // this → window,输出 "我是undefined"
原型链上的方法也遵循同样的规则 —— this 指向实际调用它的对象,而不是定义它的对象:
const parent = {
greet() {
return this;
},
};
// 推荐使用 Object.create 而非 __proto__
const child = Object.create(parent);
child.name = "child";
console.log(child.greet()); // this 是 child,不是 parent
🖱️ 事件处理函数中的 this
在 DOM 事件处理函数中,this 等同于 e.currentTarget,即绑定事件监听器的元素,而不一定是实际触发事件的元素(e.target)。
document.querySelector(".link").addEventListener("click", function (e) {
console.log(this); // 指向绑定监听器的元素
console.log(this === e.currentTarget); // true(始终相等)
console.log(this === e.target); // 不一定!事件冒泡时可能不同
e.preventDefault();
});
💡 面试高频点:
this===e.currentTarget(绑定监听器的元素),但this不一定等于e.target(实际触发事件的元素)。当存在事件冒泡时两者不同。
规则三:显式绑定 —— call / apply / bind
手动指定 this 的指向,三种方式各有特点:
const obj2 = { name: "天天" };
const obj = {
name: "zwq",
speak: function (a, b) {
console.log(`${this.name}: ${a} ${b}`);
},
};
// ✅ call —— 立即执行,参数逐个传递
obj.speak.call(obj2, "hello", "world"); // "天天: hello world"
// ✅ apply —— 立即执行,参数以数组传递
obj.speak.apply(obj2, ["hello", "world"]); // "天天: hello world"
// ✅ bind —— 不立即执行,返回一个新函数,this 被永久绑定
// bind 不仅绑定 this,后续调用时的参数也会透传
const fn = obj.speak.bind(obj2);
fn("hello", "world"); // "天天: hello world"
三者对比:
| 方法 | 是否立即执行 | 参数传递方式 | 返回值 |
|---|---|---|---|
call | ✅ 是 | 逐个传递 | 函数返回值 |
apply | ✅ 是 | 数组传递 | 函数返回值 |
bind | ❌ 否 | 逐个传递 | 新函数 |
💡 实际应用场景: 在事件处理函数中,我们通常需要异步执行回调,此时
call和apply不适用(它们会立即执行),而bind返回新函数但不立即执行,正好满足需求。
const oForm = document.querySelector(".add-items");
function addItem(e) {
console.log(this); // bind 后,this 指向 obj 而非 DOM 元素
e.preventDefault();
}
// 用 bind 手动指定 this(注意:bind 会透传参数,e 会自动传入)
oForm.addEventListener("submit", addItem.bind(obj));
⚠️ 踩坑提醒: 我在实际项目中遇到过一个 bug——把事件处理函数
bind之后,想在removeEventListener时取消监听,发现取消不掉。因为bind返回的是一个新函数,引用地址不同。解决办法是把 bind 后的函数存起来:
const handler = addItem.bind(obj);
oForm.addEventListener("submit", handler);
// 之后要移除时
oForm.removeEventListener("submit", handler); // ✅ 必须用同一个引用
规则四:new 绑定 —— 构造函数调用
使用 new 关键字调用函数时,this 指向新创建的实例对象。
function Person(name) {
this.name = name;
}
const p = new Person("zwq");
console.log(p.name); // "zwq" —— this 指向新实例 p
⚠️ 如果构造函数显式返回一个对象,
this的绑定会被覆盖:
function Person(name) {
this.name = name;
return { name: "被覆盖了" }; // 返回对象会替换新实例
}
const p = new Person("zwq");
console.log(p.name); // "被覆盖了"
四、箭头函数:this 的"终结者"
箭头函数没有自己的 this。它会捕获外层作用域的 this 值,而且无法被 call / apply / bind 改变,也不能用 new 调用。
let name = "梅西";
const obj = {
name: "姆巴佩",
say: function () {
// 普通函数:this 指向 obj
setTimeout(() => {
// 箭头函数:this 继承外层 say 的 this → obj
console.log(this.name); // "姆巴佩"
}, 1000);
},
};
obj.say();
对比一下如果用普通函数:
setTimeout(function () {
console.log(this.name); // "梅西" —— 普通函数 this 指向 window
}, 1000);
箭头函数的 this 绑定验证:
const globalObject = this;
const foo = () => this;
console.log(foo() === globalObject); // true
// call / apply / bind 都无法改变箭头函数的 this
console.log(foo.call({ name: "obj" }) === globalObject); // true
console.log(foo.bind({ name: "obj" })() === globalObject); // true
// new 也不能用于箭头函数
new foo(); // ❌ TypeError: foo is not a constructor
⚠️ 环境差异: 上面代码在浏览器全局脚本中,
this是window;在 ES Module(<script type="module">)或 Node.js 严格模式下,顶层this是undefined。结论依然成立,只是值不同。
💡 最佳实践: 在回调函数中优先使用箭头函数,可以避免
this指向丢失的问题,无需手动bind。
五、类中的 this
实例方法 vs 静态方法
class C {
instanceField = this; // 实例上下文 → 指向实例
static staticField = this; // 静态上下文 → 指向类本身
}
const c = new C();
console.log(c.instanceField === c); // true
console.log(C.staticField === C); // true
类方法的 this 丢失问题
和对象方法一样,类方法被单独调用时 this 也会丢失。因为 class 内部自动处于严格模式,this 是 undefined,访问属性时直接报错:
class Car {
sayHi() {
console.log(`Hello from ${this.name}`); // 访问 undefined.name → 报错!
}
get name() {
return "Ferrari";
}
}
const car = new Car();
const sayHi = car.sayHi;
sayHi(); // ❌ TypeError: Cannot read properties of undefined (reading 'name')
💡 如果方法只是
console.log(this)而不访问属性,不会报错,只会输出undefined。报错的本质原因是undefined.name。
解决方案一:在构造函数中 bind
class Car {
constructor() {
this.sayHi = this.sayHi.bind(this);
}
sayHi() {
console.log(`Hello from ${this.name}`);
}
}
const car = new Car();
const bird = { name: "Tweety" };
bird.sayHi = car.sayHi;
bird.sayHi(); // "Hello from Ferrari" —— this 被永久绑定到 car 实例
解决方案二(推荐):类字段 + 箭头函数
class Car {
// 箭头函数作为类字段,this 在实例化时自动绑定
sayHi = () => {
console.log(`Hello from ${this.name}`);
};
get name() {
return "Ferrari";
}
}
const car = new Car();
const bird = { name: "Tweety" };
bird.sayHi = car.sayHi;
bird.sayHi(); // "Hello from Ferrari" —— 始终绑定到 car 实例
💡 React 开发者注意: 类组件中绑定事件处理函数,推荐使用类字段箭头函数写法,比在 constructor 中 bind 更简洁,也是目前最主流的写法。
派生类必须先调用 super()
class Base {}
class Bad extends Base {
constructor() {
console.log(this); // ❌ ReferenceError!
}
}
class Good extends Base {
constructor() {
super(); // 先调用 super(),才能使用 this
console.log(this); // ✅
}
}
六、this 规则优先级
当多条规则同时适用时,按以下优先级判断(从高到低):
new 绑定 > 显式绑定 (call/apply/bind) > 隐式绑定(含事件处理) > 默认绑定
判断流程:
函数是怎么调用的?
│
├─ 用了 new? → this 指向新实例
├─ 用了 call/apply/bind?→ this 指向手动指定的对象
├─ 是 obj.fn() 形式? → this 指向 obj(事件处理函数归此类,this 等于 e.currentTarget)
├─ 是箭头函数? → this 继承外层作用域(不可改变)
└─ 都不是? → this 指向 window(严格模式 undefined)
验证:bind 只能生效一次
function f() {
return this.a;
}
const g = f.bind({ a: "azerty" });
console.log(g()); // "azerty"
const h = g.bind({ a: "yoo" }); // 二次 bind 无效!
console.log(h()); // "azerty"(仍然是第一次 bind 的值)
七、总结速查表
| 调用方式 | this 指向 | 示例 |
|---|---|---|
| 普通函数调用 | window(严格模式 undefined) | fn() |
| 对象方法调用 | 调用该方法的对象 | obj.fn() |
| 事件处理函数 | e.currentTarget(绑定监听器的元素) | el.addEventListener(...) |
call / apply | 手动指定的对象 | fn.call(obj) |
bind | 手动指定的对象(永久绑定) | fn.bind(obj)() |
new 调用 | 新创建的实例 | new Fn() |
| 箭头函数 | 外层作用域的 this(不可改变) | () => {} |
| 类字段箭头函数 | 实例(实例化时绑定) | fn = () => {} |
八、进阶方向
本文覆盖了 this 的核心场景,以下主题值得进一步探索:
- getter / setter 中的 this —— 指向访问属性的对象
- Proxy 中的 this —— 代理对象的方法调用时 this 指向 proxy
- Reflect.apply() —— 更现代的显式绑定方式
- 手写 call / apply / bind —— 面试高频手写题
📚 参考资料:
记住一个核心原则:this 在运行时确定,不在声明时确定。看函数怎么调用,就知道 this 是谁!
👋 如果这篇文章对你有帮助,欢迎点赞 👍、收藏 ⭐、关注支持一下!有问题或补充欢迎在评论区交流~
标签:
JavaScript·前端面试·this 指向·前端基础