🔥 JavaScript this 指向全解析:面试必考的 5 大绑定规则

🔥 JavaScript this 指向全解析:面试必考的 5 大绑定规则

📌 目标读者: 有基础 JS 语法经验、准备面试或想夯实基础的初中级前端开发者 ⏱️ 预计阅读: 10-15 分钟

this 是 JavaScript 中最令人困惑的概念之一。它的值不是在函数声明时确定的,而是在函数调用时 动态绑定的。本文将结合 MDN 权威文档和实际踩坑经验,带你彻底搞懂 this 的指向规则。


一、先看一个经典面试题

js 复制代码
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 的值取决于函数如何被调用,而不是函数如何被定义。


二、前置知识:什么是严格模式?

文中多次提到"严格模式",这里先简单说明:

js 复制代码
// 方式一:脚本顶部开启,整个文件生效
"use strict";

// 方式二:函数内部开启,仅该函数生效
function fn() {
  "use strict";
}

严格模式的主要差异:普通函数调用时 thisundefined(非严格模式下是 window),变量必须声明后使用等。

💡 ES6 的 class 语法内部自动处于严格模式 ,无需手动声明。这就是为什么 class 方法中 this 丢失时会直接报错。


三、this 的五大绑定规则

规则一:默认绑定 ------ 普通函数调用

当函数直接调用(不带任何前缀),在非严格模式下 this 指向 window,严格模式下为 undefined

js 复制代码
function greet() {
  console.log(this);
}

greet(); // 非严格模式 → window,严格模式 → undefined

⚠️ 注意: var全局作用域 下声明的变量会挂载到 window 上,造成全局污染。函数内部的 var 不会。推荐统一使用 let / const

js 复制代码
// 全局作用域下
var name = "张三";  // window.name = "张三" ❌ 污染全局
let age = 18;       // 不会挂载到 window ✅

规则二:隐式绑定 ------ 作为对象的方法调用(含事件处理)

当函数作为对象的方法被调用时,this 指向调用该方法的对象

js 复制代码
const obj = {
  name: "zwq",
  say: function () {
    console.log(this);
    console.log(`我是${this.name}`);
  },
};

obj.say(); // this → obj,输出 "我是zwq"

但是! 如果把方法赋值给变量再调用,this 绑定就丢失了:

js 复制代码
const fn = obj.say; // 赋值后脱离了对象,this 绑定丢失
fn(); // this → window,输出 "我是undefined"

原型链上的方法也遵循同样的规则 ------ this 指向实际调用它的对象,而不是定义它的对象:

js 复制代码
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)。

js 复制代码
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 的指向,三种方式各有特点:

js 复制代码
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 ❌ 否 逐个传递 新函数

💡 实际应用场景: 在事件处理函数中,我们通常需要异步执行回调,此时 callapply 不适用(它们会立即执行),而 bind 返回新函数但不立即执行,正好满足需求。

js 复制代码
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 后的函数存起来:

js 复制代码
const handler = addItem.bind(obj);
oForm.addEventListener("submit", handler);
// 之后要移除时
oForm.removeEventListener("submit", handler); // ✅ 必须用同一个引用

规则四:new 绑定 ------ 构造函数调用

使用 new 关键字调用函数时,this 指向新创建的实例对象。

js 复制代码
function Person(name) {
  this.name = name;
}

const p = new Person("zwq");
console.log(p.name); // "zwq" ------ this 指向新实例 p

⚠️ 如果构造函数显式返回一个对象,this 的绑定会被覆盖:

js 复制代码
function Person(name) {
  this.name = name;
  return { name: "被覆盖了" }; // 返回对象会替换新实例
}

const p = new Person("zwq");
console.log(p.name); // "被覆盖了"

四、箭头函数:this 的"终结者"

箭头函数没有自己的 this 。它会捕获外层作用域的 this 值,而且无法被 call / apply / bind 改变,也不能用 new 调用

js 复制代码
let name = "梅西";
const obj = {
  name: "姆巴佩",
  say: function () {
    // 普通函数:this 指向 obj
    setTimeout(() => {
      // 箭头函数:this 继承外层 say 的 this → obj
      console.log(this.name); // "姆巴佩"
    }, 1000);
  },
};

obj.say();

对比一下如果用普通函数:

js 复制代码
setTimeout(function () {
  console.log(this.name); // "梅西" ------ 普通函数 this 指向 window
}, 1000);

箭头函数的 this 绑定验证:

js 复制代码
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

⚠️ 环境差异: 上面代码在浏览器全局脚本中,thiswindow;在 ES Module(<script type="module">)或 Node.js 严格模式下,顶层 thisundefined。结论依然成立,只是值不同。
💡 最佳实践: 在回调函数中优先使用箭头函数,可以避免 this 指向丢失的问题,无需手动 bind


五、类中的 this

实例方法 vs 静态方法

js 复制代码
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 内部自动处于严格模式,thisundefined,访问属性时直接报错:

js 复制代码
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

js 复制代码
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 实例

解决方案二(推荐):类字段 + 箭头函数

js 复制代码
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()

js 复制代码
class Base {}

class Bad extends Base {
  constructor() {
    console.log(this); // ❌ ReferenceError!
  }
}

class Good extends Base {
  constructor() {
    super(); // 先调用 super(),才能使用 this
    console.log(this); // ✅
  }
}

六、this 规则优先级

当多条规则同时适用时,按以下优先级判断(从高到低):

sql 复制代码
new 绑定  >  显式绑定 (call/apply/bind)  >  隐式绑定(含事件处理)  >  默认绑定

判断流程:

kotlin 复制代码
函数是怎么调用的?
│
├─ 用了 new?           → this 指向新实例
├─ 用了 call/apply/bind?→ this 指向手动指定的对象
├─ 是 obj.fn() 形式?   → this 指向 obj(事件处理函数归此类,this 等于 e.currentTarget)
├─ 是箭头函数?         → this 继承外层作用域(不可改变)
└─ 都不是?             → this 指向 window(严格模式 undefined)

验证:bind 只能生效一次

js 复制代码
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 指向 · 前端基础

相关推荐
Csvn2 小时前
TypeScript 类型守卫的 4 个实战级别:从 `filter(Boolean)` 到自定义类型谓词
前端
帅次2 小时前
Android 高级工程师面试:Kotlin 语法基础 近1年高频追问 22 题
android·面试·kotlin·扩展函数·空安全
骑士雄师2 小时前
langchain:推荐切片策略
服务器·前端·langchain
云浪2 小时前
拆开 WAV,看见声音——JavaScript 二进制数组实战全解析
前端·javascript·ecmascript 6
_瑞3 小时前
深入理解灵动岛
前端·ios·app
想你依然心痛3 小时前
嵌入式日志系统:分级日志、环形缓冲区与远程输出——运行时调试、非侵入
android·前端·javascript
这是个栗子3 小时前
【Vue代码分析】 import.meta.env.DEV:现代前端工程中的环境变量
前端·javascript·vue.js·环境变量
像牛奶却不是牛奶3 小时前
记录几个前端实用小函数
前端·javascript
IDIOT___IDIOT3 小时前
Windows 解锁后 Chrome 弹出白屏的排查与修复
前端·chrome