深入理解 JavaScript 中的 this:从设计缺陷到最佳实践(完整复习版)

在 JavaScript 开发中,this 是一个既基础又容易让人困惑的核心概念。它不像其他语言那样固定指向当前对象,而是由函数的调用方式决定 。这种灵活性带来了强大的能力,也埋下了不少"坑"。本文系统梳理 this 的行为规则、常见误区,并结合底层机制与实战代码,帮助你彻底掌握这一关键知识点。


一、为什么 this 如此特殊?

1.1 自由变量 vs this:作用域与执行上下文的区别 ✅⭐️

JavaScript 中有两种变量查找机制:

  • 自由变量(Lexical Scope) :在编译阶段确定,沿着词法作用域链向上查找。
  • this 绑定 :在运行时 根据函数调用方式动态决定,属于执行上下文的一部分。

关键区别

自由变量看"写在哪"(声明位置),this 看"怎么调"(调用方式)。

javascript 复制代码
js
编辑
var myName = '极客邦'; // 全局变量,挂载到 window

var bar = {
  myName: 'time.geekbang.com',
  printName: function () {
    console.log(myName);        // 自由变量 → '极客邦'(全局)
    console.log(this.myName);   // this 绑定 → 取决于调用方式
  }
};

💡 面试重要性 :⭐️⭐️⭐️⭐️

大厂常考:作用域链 vs 执行上下文、闭包与 this 的区别。

补充:什么是自由变量?

自由变量(Free Variable) 是指在当前作用域中未被声明,但被引用的变量。JavaScript 引擎会在词法作用域链中向上查找其定义。

javascript 复制代码
js
编辑
function outer() {
  var x = 10;
  return function inner() {
    console.log(x); // x 是 inner 的自由变量
  };
}
  • xinner 中未声明,但在外层 outer 中存在 → 是自由变量;
  • 查找发生在编译期(静态),与运行时无关;
  • 这就是闭包的基础:内部函数保留对外层自由变量的引用。

🔍 对比 this

  • 自由变量:静态绑定(词法作用域)→ 编译期确定;
  • this:动态绑定(执行上下文)→ 运行期确定。

二、this 的五种绑定规则(按优先级排序)✅⭐️⭐️⭐️⭐️⭐️

2.1 默认绑定:普通函数调用

当函数作为独立函数被调用时:

  • 非严格模式:this 指向全局对象(浏览器中是 window
  • 严格模式:thisundefined
scss 复制代码
js
编辑
function foo() {
  console.log(this); // 非严格 → window;严格 → undefined
}
foo(); // 普通调用

⚠️ 问题:这是 JS 的"历史包袱",易导致意外污染全局变量。


2.2 隐式绑定:作为对象方法调用

当函数通过对象属性调用时,this 指向该对象:

javascript 复制代码
js
编辑
var myObj = {
  name: '极客时间',
  showThis: function () {
    console.log(this.name); // '极客时间'
  }
};

myObj.showThis(); // this → myObj

但注意"隐式丢失"问题:

ini 复制代码
js
编辑
var foo = myObj.showThis;
foo(); // this → window(非严格)或 undefined(严格)

💡 原因foo 是普通函数引用,调用时已脱离对象上下文。
💡 面试重要性 :⭐️⭐️⭐️⭐️

高频考点:方法解构后 this 丢失、如何修复。


2.3 显式绑定:call / apply / bind ✅⭐️⭐️⭐️⭐️⭐️

通过这些方法可以强制指定 this

ini 复制代码
js
编辑
function updateInfo() {
  this.myName = '极客时间';
  this.age = 18;
}

let bar = { myName: '极客邦' };
updateInfo.call(bar); // 或 apply(bar)
console.log(bar); // { myName: '极客时间', age: 18 }

核心原理:apply(bar)thisbar 指向同一内存地址

  • this.xxx = ... 的操作,等价于直接操作 bar.xxx
  • 属性存在 → 覆盖值;不存在 → 新增属性
示例验证:
javascript 复制代码
js
编辑
let bar = {
  myName: '极客邦',
  text1: 1
};

function foo() {
  this.myName = '极客时间';     // 覆盖
  this.text2 = 2;               // 新增
  this.fn = () => console.log('新增函数');
}

foo.apply(bar);

console.log(bar);
// {
//   myName: '极客时间',
//   text1: 1,
//   text2: 2,
//   fn: [Function]
// }

🔍 本质 :对象是引用类型,apply 只是让 this 成为 bar 的替身。

引用类型同样适用:
ini 复制代码
js
编辑
let bar = { info: { age: 18 } };
function foo() {
  this.info = { gender: 'male' }; // 覆盖整个引用
  this.list = [1, 2, 3];          // 新增
}
foo.apply(bar);

💡 面试重要性 :⭐️⭐️⭐️⭐️⭐️

必考:call/apply/bind 区别、手写 bind、apply 如何影响对象属性。


2.4 构造函数调用:new 关键字 ✅⭐️⭐️⭐️⭐️⭐️

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

底层四步流程(引擎自动完成):

  1. 创建临时空对象var tempObj = {}(隐式,不可见)

  2. 绑定原型链tempObj.__proto__ = Constructor.prototype

  3. 执行构造函数,绑定 thisConstructor.call(tempObj)

  4. 返回对象

    • 若无 return 或 return 基本类型 → 返回 tempObj
    • 若 return 对象类型 → 返回该对象
示例代码:
javascript 复制代码
js
编辑
function CreateObj() {
  console.log(this);        // 输出临时对象
  this.name = '极客时间';
}
var myObj = new CreateObj(); // myObj = { name: '极客时间' }
手动模拟 new
ini 复制代码
js
编辑
function myNew(constructor) {
  var tempObj = {};
  tempObj.__proto__ = constructor.prototype;
  constructor.call(tempObj);
  return tempObj;
}

💡 关键细节

  • 临时对象是引擎内部创建的,你无法直接访问;
  • __proto__ 是非标准属性,规范中为 [[Prototype]]
  • 构造函数不应返回基本类型(会被忽略)。
    💡 面试重要性 :⭐️⭐️⭐️⭐️⭐️

高频大题:手写 new、解释 new 的执行过程、原型链构建。


2.5 事件处理函数中的 this

在 DOM 事件监听器中,this 指向触发事件的元素

javascript 复制代码
js
编辑
document.getElementById('link').addEventListener('click', function() {
  console.log(this); // <a id="link">...</a>
});

✅ 浏览器环境特殊约定,符合直觉。
💡 面试重要性 :⭐️⭐️

前端岗可能涉及,但不如前几项核心。


三、箭头函数的 this:词法绑定,永不改变 ✅⭐️⭐️⭐️⭐️⭐️

箭头函数的 this 是 JavaScript 中极易混淆的知识点,核心要抓住:箭头函数没有自己的 this ------ 它的 this 完全继承自「定义时的外层词法作用域」,且永久固定、无法修改

3.1 核心规则(一句话记住)

箭头函数的 this = 定义箭头函数时,其外层最近的非箭头函数 / 全局作用域的 this;且一旦确定,无论以何种方式调用(普通调用、方法调用、call/apply/bind),都不会改变。

3.2 关键特性(和普通函数的核心差异)

特性 箭头函数 普通函数
this 绑定时机 定义时绑定(词法作用域) 执行时绑定(调用方式决定)
this 能否修改 不能call/apply/bind 无效) (调用方式 / 绑定可改变)
自身是否有 this (继承外层) (执行时动态绑定)
构造函数调用(new 不支持(报错) 支持this 指向新实例)

💡 本质 :箭头函数的 this静态的 ,普通函数的 this动态的


3.3 实战场景(结合代码理解)

场景 1:箭头函数定义在全局作用域 → this 继承全局 window

ini 复制代码
js
编辑
const arrowFn = () => {
    console.log(this); // window(浏览器)
};

arrowFn(); // 普通调用 → this 仍为 window
const obj = { fn: arrowFn };
obj.fn(); // 方法调用 → this 还是 window(不会指向 obj)
arrowFn.call({ name: '测试' }); // call 绑定 → this 依然是 window

结论 :全局定义的箭头函数,this 永久指向 window,调用方式不影响。


场景 2:箭头函数定义在普通函数内部 → 继承外层函数的 this

这是箭头函数最常用的场景(解决普通函数内层 this 指向混乱的问题):

javascript 复制代码
js
编辑
const obj = {
    name: '极客时间',
    outerFn: function() {
        console.log('外层普通函数 this:', this); // obj(方法调用)
        
        // 箭头函数定义在 outerFn 内部,继承 outerFn 的 this
        const arrowFn = () => {
            console.log('箭头函数 this:', this); // obj(继承外层)
        };
        arrowFn(); // 普通调用 → this 仍为 obj
        arrowFn.call(window); // call 尝试改 this → 无效,还是 obj
    }
};
obj.outerFn();

执行结果

kotlin 复制代码
text
编辑
外层普通函数 this: {name: '极客时间', outerFn: ƒ}
箭头函数 this: {name: '极客时间', outerFn: ƒ}

结论 :箭头函数完美解决了 "普通函数内层函数 this 指向全局" 的问题。

🔍 关键澄清

箭头函数的 this 不是指向"外层函数本身" ,而是指向 "外层函数执行时的 this"
outerFn 作为函数对象确实没有 .name 属性,但 outerFn 执行时的 thisobj,而 obj.name = '极客时间',所以箭头函数能正确访问。


场景 3:箭头函数定义在对象属性 → 警惕 "外层是全局"

易错点:对象的属性不是 "作用域",箭头函数定义在对象里,外层作用域仍是全局:

javascript 复制代码
js
编辑
const obj = {
    name: '极客时间',
    arrowFn: () => {
        console.log(this); // window(外层是全局作用域)
    }
};
obj.arrowFn(); // 方法调用 → this 还是 window

误区 :不要以为箭头函数作为对象方法,this 就指向对象 ------ 对象本身不形成作用域,箭头函数的外层仍是全局。

💡 正确做法:对象方法应使用普通函数。


场景 4:箭头函数不能作为构造函数(new 调用报错)

ini 复制代码
js
编辑
const ArrowConstructor = () => {
    this.name = '极客时间';
};
// 报错:ArrowConstructor is not a constructor
new ArrowConstructor();

原因 :箭头函数没有自己的 this,也没有 prototype,无法作为构造函数。


场景 5:箭头函数在回调函数中的应用(解决 this 丢失)

经典场景 :定时器 / 事件回调中保留外层 this

javascript 复制代码
js
编辑
const obj = {
    name: '极客时间',
    delayLog: function() {
        // 普通函数回调:this 指向 window
        setTimeout(function() {
            console.log('普通函数回调 this:', this.name); // undefined
        }, 100);
        
        // 箭头函数回调:继承外层 delayLog 的 this(obj)
        setTimeout(() => {
            console.log('箭头函数回调 this:', this.name); // 极客时间
        }, 100);
    }
};
obj.delayLog();

结论 :箭头函数是解决回调函数 this 丢失的最优方案(无需手动 bind 或存 that = this)。

🔍 原理拆解

  1. obj.delayLog()delayLog 是普通函数,作为方法调用 → this = obj
  2. 箭头函数定义在 delayLog 内部 → 继承 delayLog 执行时的 this(即 obj);
  3. 即使 setTimeout 在 100ms 后调用,箭头函数的 this 依然锁定为 obj

3.4 避坑指南(常见错误)

错误用法 正确做法
把箭头函数作为对象的方法(想让 this 指向对象)→ 实际指向全局 对象方法用普通函数
call/apply/bind 改变箭头函数的 this → 完全无效 不要尝试修改箭头函数的 this
用箭头函数作为构造函数 → 直接报错 构造函数必须用普通函数
在需要动态 this 的场景使用箭头函数 动态 this → 普通函数;保留外层 this → 箭头函数

使用原则

  • 箭头函数适合 :回调函数、内层函数,需要保留外层 this 的场景;
  • 普通函数适合 :对象方法、构造函数,需要动态 this 的场景。

四、隐式丢失 vs 显式绑定:实战对比 ✅⭐️⭐️⭐️⭐️

4.1 隐式丢失示例

ini 复制代码
js
编辑
var bar = {
  myName: 'time.geekbang.com',
  printName: function () {
    console.log(this.myName);
  }
};

let _printName = bar.printName;
_printName(); // this → window / undefined ❌

4.2 显式绑定修复

ini 复制代码
js
编辑
let boundPrintName = bar.printName.bind(bar);
boundPrintName(); // ✅ 正确输出

4.3 对比总结

场景 this 指向 结果
bar.printName() bar
f = bar.printName; f() window / undefined
f.bind(bar)() bar

💡 经验法则 :只要函数可能被提取单独调用,就必须提前绑定 this
💡 面试重要性 :⭐️⭐️⭐️⭐️

React 类组件中常见问题,大厂必问。


五、如何正确拿到 bar.myName?方案对比

方案 优点 缺点
直接调用 bar.printName() 简单 无法传递引用
箭头函数 避免 this 问题 紧耦合,失去通用性
.bind(bar) 安全可靠 需额外步骤
闭包保存 self 兼容旧环境 代码冗余

推荐 :使用 .bind()


六、关于 varlet/const 在全局作用域的行为差异(重要澄清)✅⭐️⭐️⭐️⭐️

🚨 常见误解纠正
let 不挂载 window 不是因为 TDZ ,而是因为 ES6 引入了词法绑定模型

6.1 核心结论

特性 var 全局变量 let/const 全局变量
自动挂载 window ✅ 是 ❌ 否(词法绑定)
暂时性死区(TDZ) ❌ 无 ✅ 有
块级作用域 ❌ 无 ✅ 有

6.2 原理说明

  • var:全局变量 ≡ window 属性(历史遗留)
  • let/const:存储在「全局词法环境」的声明式记录中,与 window 解耦
ini 复制代码
js
编辑
var a = 1;
let b = 2;
console.log(window.a); // 1
console.log(window.b); // undefined

💡 面试重要性 :⭐️⭐️⭐️⭐️

大厂常考:全局变量存储机制、TDZ 与挂载无关。


七、严格模式:规避 this 的陷阱 ✅⭐️⭐️

'use strict' 下:

  • 普通函数调用 this === undefined
  • 避免意外创建全局变量
javascript 复制代码
js
编辑
'use strict';
function bad() {
  this.x = 1; // TypeError
}

建议:所有现代项目启用严格模式。


八、总结要点(面试速记版)✅⭐️⭐️⭐️⭐️⭐️

绑定方式 触发条件 this 指向 优先级
默认绑定 独立函数调用 window / undefined 最低
隐式绑定 obj.method() obj
显式绑定 call/apply/bind 指定对象
new 绑定 new Fn() 新实例 最高
箭头函数 定义时继承 外层非箭头函数的 this 静态,不受调用影响

🔔 口诀
谁调用,this 就是谁;没谁调,默认全局走;new 出来,实例有;call/apply/bind,我说了算;箭头函数,看定义,外层 this 永不变!


九、高频面试题清单(按重要性打星)⭐️⭐️⭐️⭐️⭐️

问题 重要性 考察点
解释 this 的绑定规则及优先级 ⭐️⭐️⭐️⭐️⭐️ 基础核心
手写 bind / new ⭐️⭐️⭐️⭐️⭐️ 实现能力
为什么 let 不挂载 window ⭐️⭐️⭐️⭐️ 作用域模型理解
方法解构后 this 丢失如何解决? ⭐️⭐️⭐️⭐️ 实战经验
apply(obj) 对 obj 属性的影响? ⭐️⭐️⭐️⭐️ 引用类型理解
箭头函数的 this 为何不能被 call 修改? ⭐️⭐️⭐️⭐️⭐️ 词法绑定 vs 动态绑定
箭头函数能否作为对象方法?为什么? ⭐️⭐️⭐️⭐️ 作用域与 this 理解
箭头函数与普通函数 this 差异? ⭐️⭐️⭐️ 语法特性
严格模式对 this 的影响? ⭐️⭐️ 安全编程意识

🌟 结语
this 并非"玄学",而是 JavaScript 执行模型的核心部分。掌握其规则,不仅能避开陷阱,还能灵活构建对象系统。理解底层机制(如 new 创建临时对象、apply 绑定引用、箭头函数词法绑定)是大厂面试脱颖而出的关键

同时,自由变量(词法作用域)与 this(动态绑定)的对比,是区分初级与高级开发者的重要分水岭。

相关推荐
刻刻帝的海角31 分钟前
基于UniApp与Vue3语法糖的跨平台待办事项应用开发实践
javascript·vue.js·uni-app
ByteCraze35 分钟前
系统性整理组件传参14种方式
前端·javascript·vue.js
大杯咖啡36 分钟前
基于 Vue3 (tsx语法)的动态表单深度实践-只看这一篇就够了
前端·javascript·vue.js
izx88839 分钟前
JavaScript 中 `this` 的真相:由调用方式决定的动态指针
javascript
前端缘梦41 分钟前
JavaScript核心机制:执行栈、作用域与this指向完全解析
前端·javascript·面试
春卷同学1 小时前
拼图游戏 - Electron for 鸿蒙PC项目实战案例
javascript·electron·harmonyos
皮蛋瘦肉粥_1211 小时前
pink老师-jsAPIS-day1
javascript·apis
Youyzq1 小时前
react 元素触底hooks封装
前端·javascript·react.js
灵犀坠1 小时前
前端面试&项目实战核心知识点总结(Vue3+Pinia+UniApp+Axios)
前端·javascript·css·面试·职场和发展·uni-app·html