【硬核教程】从入门到入土!彻底吃透 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 的指向,而是看函数如何被调用。多写代码、多调试,逐步建立 "调用时绑定" 的思维模型,你将彻底掌控这个 "魔法关键字"!

相关推荐
万少25 分钟前
第五款 HarmonyOS 上架作品 奇趣故事匣 来了
前端·harmonyos·客户端
OpenGL31 分钟前
Android targetSdkVersion升级至35(Android15)相关问题
前端
rzl021 小时前
java web5(黑马)
java·开发语言·前端
Amy.Wang1 小时前
前端如何实现电子签名
前端·javascript·html5
海天胜景1 小时前
vue3 el-table 行筛选 设置为单选
javascript·vue.js·elementui
今天又在摸鱼1 小时前
Vue3-组件化-Vue核心思想之一
前端·javascript·vue.js
蓝婷儿1 小时前
每天一个前端小知识 Day 21 - 浏览器兼容性与 Polyfill 策略
前端
百锦再1 小时前
Vue中对象赋值问题:对象引用被保留,仅部分属性被覆盖
前端·javascript·vue.js·vue·web·reactive·ref
jingling5551 小时前
面试版-前端开发核心知识
开发语言·前端·javascript·vue.js·面试·前端框架
拾光拾趣录1 小时前
CSS 深入解析:提升网页样式技巧与常见问题解决方案
前端·css