【硬核教程】从入门到入土!彻底吃透 JavaScript 中 this 关键字这一篇就够了

一、前言

在 JavaScript 的世界里,this 关键字堪称最让人 "爱恨交织" 的特性。对于新手来说,它像一团迷雾,常常让人摸不着头脑,甚至写出各种 "魔幻" 的 bug;但对于老手而言,它又是灵活操控对象上下文的利器。本文将用通俗易懂的语言和丰富的示例,带你从零基础吃透 this,让你不仅能看懂,更能灵活运用!

二、为什么需要 this?先看一个没有 this 的糟糕世界

假设我们要实现一个 "打招呼" 的功能,不使用 this 时可能会这样写:

javascript 复制代码
function greet() {
  console.log("Hello, " + name); // 依赖全局变量name
}

const name = "小明";
greet(); // 输出:Hello, 小明

// 当需要问候"小红"时,不得不重新定义函数
const name = "小红";
greet(); // 输出:Hello, 小红(但这会污染全局变量!)

问题显而易见:函数强依赖全局变量,无法复用,且容易引发命名冲突。

this 的救赎:动态绑定上下文

使用 this 后,同一个函数可以适配不同对象:

javascript 复制代码
function greet() {
  console.log("Hello, " + this.name); // this指向调用时的对象
}

const person1 = { name: "小明", greet };
const person2 = { name: "小红", greet };

person1.greet(); // 输出:Hello, 小明
person2.greet(); // 输出:Hello, 小红

核心价值:this 让函数不再依赖固定变量,而是在调用时动态绑定到 "当前对象",极大提升了代码复用性。

三、破除迷思:关于 this 的常见错误认知

在学习 this 之前,必须先破除两个最常见的误解。

误解一:this 指向函数自身

错误代码示例

scss 复制代码
function foo() {
  this.count++; // 误以为this指向函数foo
}
foo.count = 0; // 给函数添加属性count

for (let i = 0; i < 3; i++) {
  foo(); // 每次调用foo时,this指向全局对象(浏览器中是window)
}

console.log(foo.count); // 输出:0(全局变量count被修改,而非函数的count属性)

真相

默认情况下,独立调用函数时,this 指向全局对象(浏览器中是 window,严格模式下为 undefined)。若想让 this 指向函数自身,需显式绑定:

scss 复制代码
foo.call(foo); // 强制this指向foo函数

误解二:this 指向函数的作用域

错误代码示例

scss 复制代码
function foo() {
  const a = 2; // 作用域内的变量a
  bar(); // 调用bar函数
}

function bar() {
  console.log(this.a); // 输出:undefined(this不查找作用域链)
}

foo();

真相
this 的绑定与作用域链完全无关!作用域链是变量查找机制(如查找 a),而 this 是函数调用时的上下文绑定,两者是独立的概念。

四、this 绑定的四大核心规则

this 的绑定规则共有四条,掌握它们就能破解 99% 的 this 谜题。

规则一:默认绑定(独立函数调用)

适用场景:直接调用函数,没有任何对象 "拥有" 该函数。

scss 复制代码
function sayHi() {
  console.log(this); // 浏览器中输出window,Node.js中输出global
}

sayHi(); // 独立调用,触发默认绑定

严格模式注意

scss 复制代码
function strictTest() {
  'use strict'; // 开启严格模式
  console.log(this); // 输出:undefined
}
strictTest();

规则二:隐式绑定(通过对象调用)

适用场景 :当函数作为对象的方法被调用时,this 指向该对象。

javascript 复制代码
const user = {
  name: "Alice",
  sayHello() {
    console.log("Hello, " + this.name); // this指向user对象
  }
};

user.sayHello(); // 输出:Hello, Alice

隐式丢失陷阱:当函数被单独提取出来调用时,会丢失绑定!

php 复制代码
const fn = user.sayHello; // 提取函数
fn(); // 输出:Hello, undefined(this指向全局对象)

规则三:显式绑定(强制指定 this

通过 callapplybind 三种方法强制指定 this 的指向。

javascript 复制代码
function introduce(lang) {
  console.log(`我会说${lang},我叫${this.name}`);
}

const engineer = { name: "Bob" };

// 立即调用并指定this
introduce.call(engineer, "JavaScript"); // 输出:我会说JavaScript,我叫Bob
introduce.apply(engineer, ["Python"]); // 输出:我会说Python,我叫Bob

// 永久绑定(返回新函数)
const boundFn = introduce.bind(engineer);
boundFn("Java"); // 输出:我会说Java,我叫Bob

区别对比

方法 参数传递方式 返回值
call 逗号分隔参数(如 call(obj, arg1, arg2) 函数执行结果
apply 数组传递参数(如 apply(obj, [arg1, arg2]) 函数执行结果
bind call,但返回新函数 新函数(永久绑定 this

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

当使用 new 关键字调用函数时,会创建一个新对象,且 this 指向该对象。

javascript 复制代码
function Car(brand) {
  this.brand = brand; // this指向新创建的对象
}

const bmw = new Car("BMW");
console.log(bmw.brand); // 输出:BMW

new 的四个步骤

  1. 创建一个空对象 const newObj = {};
  2. 绑定原型链 newObj.__proto__ = Car.prototype;
  3. 绑定 this const result = Car.call(newObj, brand);
  4. 返回对象 return result || newObj;

额外补充:构造函数返回值对 this 的影响

通常情况下,构造函数默认返回新创建的对象(即 this 指向的对象)。但如果构造函数显式返回一个对象,则 new 调用返回的将是这个显式返回的对象,而不是 this 指向的对象。如果显式返回的不是对象(如基本类型),则忽略返回值,仍然返回 this 指向的对象。

javascript 复制代码
function Person(name) {
  this.name = name;
  // 显式返回一个对象
  return { message: 'This is a different object' };
}

const person = new Person('John');
console.log(person); // 输出:{ message: 'This is a different object' }

function Animal(type) {
  this.type = type;
  // 显式返回一个基本类型
  return 123;
}

const animal = new Animal('Dog');
console.log(animal.type); // 输出:Dog

五、优先级排序:当规则发生冲突时

四条规则的优先级从高到低为:new 绑定 > 显式绑定 > 隐式绑定 > 默认绑定

实战验证

ini 复制代码
function whoAmI() {
  console.log(this.name);
}

const obj = { name: "Obj", whoAmI };
const bound = whoAmI.bind(obj); // 显式绑定

// 场景1:new绑定 vs 显式绑定
const instance = new bound(); // 输出:undefined(new绑定优先级更高,创建新对象,未初始化name)

// 场景2:隐式绑定 vs 默认绑定
const fn = obj.whoAmI;
fn(); // 输出:undefined(隐式丢失,触发默认绑定)

六、特殊场景与例外情况

6.1 被忽略的 this:传入 nullundefined

当显式绑定时传入 nullundefined,会被忽略,应用默认绑定:

javascript 复制代码
function logThis() {
  console.log(this);
}

logThis.call(null); // 浏览器中输出window(默认绑定)
logThis.call(undefined); // 同上

安全提示 :避免传入 null 作为 this,可能意外污染全局对象。推荐用一个空对象替代:

javascript 复制代码
const safeObj = Object.create(null); // 空对象,无原型链污染风险
logThis.call(safeObj); // 输出:{}

6.2 间接引用:赋值操作导致的绑定丢失

javascript 复制代码
const obj = {
  msg: "Hello",
  say() {
    console.log(this.msg);
  }
};

// 间接引用:将方法赋值给变量
const fn = obj.say;
fn(); // 输出:undefined(this指向全局)

原理:赋值操作会导致函数脱离原对象,调用时触发默认绑定。

6.3 回调函数中的 this

在事件监听器、定时器等回调函数中,this 的指向往往会让人困惑。

事件监听器中的 this

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

ini 复制代码
const button = document.createElement('button');
button.textContent = 'Click me';
button.addEventListener('click', function() {
  console.log(this.textContent); // 输出:Click me
});
document.body.appendChild(button);

定时器中的 this

setTimeoutsetInterval 的回调函数中,this 默认指向全局对象(在浏览器中是 window)。

javascript 复制代码
function Timer() {
  this.time = 0;
  setInterval(function() {
    this.time++; // 这里的this指向window
    console.log(this.time); // 输出NaN
  }, 1000);
}

const timer = new Timer();

为了解决这个问题,可以使用箭头函数或者提前保存 this 的引用。

ini 复制代码
function Timer() {
  this.time = 0;
  const self = this;
  setInterval(function() {
    self.time++;
    console.log(self.time);
  }, 1000);
}

const timer = new Timer();

七、箭头函数:颠覆传统的词法 this

7.1 箭头函数的特殊机制

箭头函数不使用传统的 this 绑定规则,而是继承外层作用域的 this(词法作用域绑定)。

javascript 复制代码
const obj = {
  name: "传统函数",
  traditional() {
    console.log(this.name); // 输出:传统函数(隐式绑定)
    setTimeout(function() {
      console.log(this.name); // 输出:undefined(默认绑定,因为setTimeout的回调是独立函数)
    }, 100);
  },
  arrow: () => {
    console.log(this.name); // 输出:undefined(箭头函数继承外层的this,这里外层是全局作用域)
  }
};

obj.traditional(); // 传统函数 undefined
obj.arrow(); // undefined(全局作用域中没有name变量)

7.2 经典应用:解决回调函数 this 丢失

在类组件中,传统函数需要手动绑定 this,而箭头函数自动继承外层 this

arduino 复制代码
class Button {
  constructor(text) {
    this.text = text;
    // 传统写法:需要手动绑定
    // this.handleClick = this.handleClick.bind(this);
  }

  // 箭头函数写法:自动绑定this
  handleClick = () => {
    console.log(this.text); // 正确输出按钮文本
  }
}

const btn = new Button("点击我");
document.addEventListener("click", btn.handleClick); // 点击时输出:点击我

7.3 箭头函数不能使用 arguments 对象

箭头函数没有自己的 arguments 对象,它会继承外层作用域的 arguments 对象。

scss 复制代码
function outer() {
  const arrow = () => {
    console.log(arguments[0]); // 继承outer函数的arguments
  };
  arrow();
}

outer(123); // 输出:123

八、this 判断流程图:一看就懂的决策指南

九、实战测试:从代码中理解 this

测试题

javascript 复制代码
var name = "全局"; // 注意:var声明的变量在全局作用域中会成为window的属性

const obj = {
  name: "对象",
  foo: function() {
    console.log(this.name); // 场景1
    const inner = () => { // 箭头函数,继承外层this
      console.log(this.name); // 场景2
    };
    inner();
  },
  bar: () => { // 箭头函数,外层是全局作用域
    console.log(this.name); // 场景3
  }
};

function test(fn) {
  fn(); // 场景4
}

// 调用
obj.foo(); // 输出?
obj.bar(); // 输出?
test(obj.foo); // 输出?

答案解析

  1. 场景 1obj.foo() 是隐式绑定,this 指向 obj,输出 对象
  2. 场景 2 :箭头函数 inner 继承外层 foothis(即 obj),输出 对象
  3. 场景 3bar 是箭头函数,外层作用域是全局,this 指向 window,输出 全局
  4. 场景 4test(obj.foo) 中,obj.foo 被提取为独立函数,触发默认绑定,输出 全局

十、this需要警惕之处

1. 避免 this 丢失的三大法宝

  • 使用箭头函数:适用于回调函数、类方法。

  • 提前绑定 :用 bind 永久绑定 this

kotlin 复制代码
class Timer {
  constructor() {
    this.time = 0;
    setInterval(this.tick.bind(this), 1000); // 提前绑定this
  }
  tick() {
    this.time++;
  }
}
  • 使用 thatself 中转(传统写法):
javascript 复制代码
function outer() {
  const that = this; // 缓存this
  function inner() {
    console.log(that.value); // 使用that
  }
}

2. 警惕全局作用域的 this

  • 避免用 var 声明变量,改用 let/const,减少全局污染。
  • 严格模式下,全局作用域的函数调用 thisundefined,避免意外赋值。

3. DOM 事件中的 this

javascript 复制代码
// 错误:this指向DOM元素
element.addEventListener("click", function() {
  this.style.color = "red"; // 正确,但需注意this是DOM元素
});

// 正确:显式绑定
element.addEventListener("click", function() {
  this.style.color = "red"; // 这里的this是DOM元素,若需访问外部对象,需提前绑定
}.bind(obj));

十一、总结:this 的核心本质

  • this 不是静态的 :它的指向在函数调用时确定,取决于调用方式。

  • 四大规则是核心 :默认、隐式、显式、new 绑定,优先级依次升高。

  • 箭头函数是特例 :它不绑定 this,而是继承外层作用域的 this,适合处理回调。

理解 this 的关键在于:永远不要在函数定义时猜测 this 的指向,而是看函数如何被调用。多写代码、多调试,逐步建立 "调用时绑定" 的思维模型,你将彻底掌控这个 "魔法关键字"!

相关推荐
天天扭码9 分钟前
前端进阶 | 面试必考—— JavaScript手写定时器
前端·javascript·面试
梦雨生生25 分钟前
拖拉拽效果加点击事件
前端·javascript·css
前端Hardy27 分钟前
HTML&CSS:全网最全的代码时钟效果
javascript·css·html
前端Hardy32 分钟前
HTML&CSS:看这里,动态背景卡片效果
javascript·css·html
前端Hardy32 分钟前
第2课:变量与数据类型——JS的“记忆盒子”
前端·javascript
前端Hardy34 分钟前
第1课:初识JavaScript——让你的网页“动”起来!
javascript
冴羽1 小时前
SvelteKit 最新中文文档教程(23)—— CLI 使用指南
前端·javascript·svelte
jstart千语1 小时前
【SpringBoot】HttpServletRequest获取使用及失效问题(包含@Async异步执行方案)
java·前端·spring boot·后端·spring
徐小夕1 小时前
花了2个月时间,写了一款3D可视化编辑器3D-Tony
前端·javascript·react.js
凕雨1 小时前
Cesium学习笔记——dem/tif地形的分块与加载
前端·javascript·笔记·学习·arcgis·vue