JavaScript 的 this 为什么总让你困惑?一文带你彻底搞懂this

623 阅读9分钟

前言

你是否曾在 JavaScript 开发中被this搞得晕头转向?
嵌套回调里this突然指向全局?
方法传递后this神秘丢失?
构造函数与箭头函数的this规则总记混?

本文将从语言设计原理出发,通过 20 + 手写代码示例,带你精准预判this指向,告别 “玄学调式”,写出更健壮的 JS 代码!

一、为什么需要 this ?

在 JavaScript 的函数设计中,this 的存在源于一个核心需求:让函数在调用时能自动关联到它所属的对象环境

设想一个简单的对象方法场景:如果没有 this,我们需要将对象本身显式传递给方法,如:

const user = {
  name: "Alice",
  greet: function (self) {
    console.log("Hello, " + self.name);
  }
};
user.greet(user); // 显式传递对象实例

这种写法不仅写起来麻烦(每次调用都要手动传对象),还会让对象的内部细节暴露出来(比如必须把整个对象传给方法),破坏了代码的 “封装性”(就像把房子的钥匙随便交给外人)。

而 this 的出现就像给函数装了一个 “自动导航仪”:它作为一个隐藏的参数,在函数调用时会自动找到当前所属的对象,让方法可以直接使用对象的属性,无需再手动传递。

我们通过一段代码来看看this如何让函数灵活适配不同对象:

function identify() {
    return this.name.toUpperCase();
}

function speak() {
    var greeting = "Hello, I'm " + identify.call(this);
    console.log(greeting);
}

var me = { name: "Kyle" };
var you = { name: "Reader" };

identify.call(me); // KYLE
identify.call(you); // READER

speak.call(me); // Hello, I'm KYLE
speak.call(you); // Hello, I'm READER

这里的关键是:同一个函数identify,通过显式传递this(如call(me)),可以在不同对象上复用

二、this 的本质

在 JavaScript 里,this的指向不是写代码时就固定好的,而是在函数被调用的那一刻才动态确定的
你可以把this想象成一个 “临时演员”—— 它具体代表谁,完全取决于函数是怎么被调用的

这一特性是理解 this 的关键,也是最容易混淆的地方。

const obj = {
  value: 42,
  method: function () {
    console.log(this.value); // 当通过 obj.method() 调用时,this 指向 obj
  }
};

const func = obj.method; // 将方法赋值给变量,切断与 obj 的直接关联
func(); // 这里的 this 指向全局对象(浏览器环境中是 window,严格模式下是 undefined)

关键对比

  • obj.method() :调用位置有对象前缀,隐式绑定obj
  • func() :独立调用,触发默认绑定

在第一个调用场景中,obj.method() 的调用位置包含对象前缀,this 隐式绑定到 obj;而当方法被赋值给变量 func 并独立调用时,this 遵循默认绑定规则,指向全局环境(非严格模式)。这说明 this 的绑定完全由调用方式决定。

三、五大绑定规则

1. 默认绑定

当函数以独立方式调用(即没有对象前缀、new 关键字或显式绑定)时,this 遵循默认绑定:

  • 非严格模式this 指向全局对象(浏览器中的 window,Node.js 中的 global)。
  • 严格模式this 被绑定为 undefined,避免意外污染全局环境。

ES6 的模块(<script type="module">)和类(class)内部默认启用严格模式,但普通脚本(传统 <script> 或非模块代码)默认仍是非严格模式

// 非严格模式下的默认绑定
function foo() {
  console.log(this.a); 
}
var a = 2; 
foo(); //2

// 严格模式下的默认绑定
function strictFoo() {
  "use strict"; 
  console.log(this.a); 
}
strictFoo();// TypeError: Cannot read properties of undefined (reading 'a')

虽然this的绑定规则完全取决于调用位置,但是只有foo()运行在非严格模式下时,默认绑定才能绑定到全局对象;在严格模式下调用foo()则不影响默认绑定

function foo() {
    console.log(this.a);
}
var a = 2;
(function () {
    "use strict"
    foo(); //2
})()

2. 隐式绑定

当函数作为对象的方法被调用时(即通过 . 或 [] 访问),this 会隐式绑定到该对象,此时调用位置的对象前缀是关键。

const obj = {
  a: 2,
  foo: function () {
    console.log(this.a); 
  }
};
obj.foo(); 

但是注意,对象属性链中只有上一层或者最后一层在调用位置中起作用。这句话是什么意思?就是对象方法的调用是属于就近原则

function foo() {
    console.log(this.a);
}
const obj2 = {
    a: 2,
    foo: foo
};
const obj1 = {
    a: 3,
    obj2: obj2
}
obj1.obj2.foo(); //2

代码分析

  • 在 obj2.foo() 中,foo 是 obj2 的方法,因此 this 指向 obj2
  • obj1 的存在不影响绑定,因为调用链的最终调用者是 obj2
  • this 指向 obj2,obj2.a 的值为 2,因此输出 2。

但是隐式绑定有一个最常见的问题就是被隐式绑定的函数会丢失绑定对象,也就是说会应用默认绑定,从而把this绑定到全局对象或者undefied上(取决于是否是严格模式)。

场景 1:函数赋值导致上下文丢失

function foo() {
    console.log(this.a);
}
var obj = {
    a: 2,
    foo: foo
};
var bar = obj.foo; 
var a = "global"; 

bar(); // "global"

场景 2:回调函数传递导致的绑定丢失

function foo() {
    console.log(this.a);
}
function doFoo(fn) {
    fn();  // 调用位置
}
var obj = {
    a: 2,
    foo: foo
};
var a = "global";

doFoo(obj.foo); // "global"

问题分析

obj.foo引用 → 通过参数传递 → fn变量 → 直接调用()
         隐式丢失发生在这里

问题出现了,那么该怎么样解决?通过显示绑定或者箭头函数,下文中会提到的。

3. 显式绑定(bind、apply和call)

通过 bind()apply() 和 call(),我们可以显式指定函数调用时的 this 指向,这三种方法的优先级高于隐式绑定和默认绑定。

// call/apply 
function foo(x, y) {
  console.log(this.a, x, y);
}
const obj = { a: 2 };
foo.call(obj, 3, 4); // 输出 2 3 4,参数逐个传递
foo.apply(obj, [3, 4]); // 同上,参数通过数组传递

// bind 创建绑定函数
const boundFoo = foo.bind(obj, 3); 
boundFoo(4); // 2 3 4,绑定永久生效,无法被后续调用覆盖
boundFoo.call(window, 5, 6); // 2 3 5,bind 的硬绑定优先级最高
foo.bind(obj,4) //4

关于bind()、apply()和call():

  • bind() 创建一个新函数,并永久绑定该函数的 this 值(硬绑定),后续调用无法再通过 call/apply/bind 修改 this,但不影响原函数
var obj1 = {
    x: 1
};
var obj2 = {
    x: 2
};
var obj3 = {
    x: 3
};
var x = 4;
function foo() {
    console.log(this.x);
}
var foo1 = foo.bind(obj1);
foo1(); // 1
foo1.bind(obj2)() // 1
foo.bind(obj3)() // 3
  • apply() 和 call() 的第一个参数都是 this,区别在于通过 apply 调用时实参是放到数组中的,而通过 call 调用时实参是逗号分隔的

4. new 绑定:构造函数的实例上下文创建

当函数通过 new 关键字调用时,会创建一个新的对象实例,this 绑定到该实例,构造函数的返回值规则如下:

  • 若未显式返回对象,默认返回 this(新实例);
  • 若显式返回对象,则忽略 this,返回该对象。
function Person(name) {
  this.name = name; 
}
const alice = new Person("Alice");
console.log(alice.name); // "Alice"

// 显式返回对象时的 this 
function Car() {
  this.wheels = 4;
  return { wheels: 2 }; // 显式返回新对象
}
const bike = new Car();
console.log(bike.wheels); // 2

5. 箭头函数

ES6 引入的箭头函数(() => {})是 this 绑定的特例:它没有自己的 this,而是继承外层词法作用域的 this 值,这在回调函数中能有效避免上下文丢失。

箭头函数有三大核心特点

  1. 无独立 this(继承外层词法作用域)
  2. 不可作为构造函数
  3. 无 arguments 对象
const obj = {
  a: 2,
  traditional: function () {
    setTimeout(function () {
      console.log(this.a); 
    }, 100);
  },
  arrow: function () {
    setTimeout(() => {
      console.log(this.a); 
    }, 100);
  }
};
obj.traditional(); // undefined
obj.arrow(); // 2

// 箭头函数不能作为构造函数
const Foo = () => {
  this.a = 2; 
};
new Foo(); // TypeError: Foo is not a constructor

代码分析

  • obj.traditional() 调用时,this 指向 obj(隐式绑定)
  • traditional方法中,setTimeout 中的匿名函数是一个普通函数,因此this指向window,但是window.a未定义,输出undefined
  • arrow方法中,箭头函数 () => { ... } 无独立 this,它继承外层作用域的 this 值,它的外层作用域是arrowarrow 是 obj 的方法,其 this 已绑定到 obj,因此箭头函数中的 this 也指向 obj,obj.a的值为2

四、绑定规则优先级

优先级顺序

当多个绑定规则可能同时适用时,优先级从高到低依次为:

  1. 箭头函数(词法继承)
  2. new 绑定
  3. 显式绑定(bind > call/apply)
  4. 隐式绑定
  5. 默认绑定

优先级验证案例

案例 1:new 绑定高于显式绑定

function foo(something) {
    this.a = something;
}

var obj1 = {};
var bar = foo.bind(obj1);
bar(2);
console.log(obj1.a); // 2

var baz = new bar(3);
console.log(obj1.a); // 2 
console.log(baz.a); // 3

案例 2:显式绑定覆盖隐式绑定

var obj1 = {
    a: 2,
    foo: foo
};
var obj2 = { a: 3 };

obj1.foo.call(obj2); // 3

箭头函数的特殊地位

箭头函数不参与上述优先级排序,因其 this 是词法作用域继承的,而非通过调用规则绑定。它的绑定优先级取决于外层作用域的绑定规则,例如:

function foo() {
    return () => {
        console.log(this.a);
    };
}
var obj1 = { a: 2 };
var obj2 = { a: 3 };

var bar = foo.call(obj1);
bar.call(obj2); // 2,不是3!

我们通过几个典型案例来测试对this绑定规则的掌握程度,每个例子都暗藏关键知识点,建议先自己思考输出结果,再对照自己分析的对不对:

// 1.箭头函数
var obj = {
    a: 1,
    fn: () => console.log(this.a),
    fn2: function() {
        return () => console.log(this.a);
    }
}
obj.fn(); // undefined
obj.fn2()(); // 1

// 2.new绑定
function Foo(a) {
    this.a = a;
}
var instance = new Foo(2);
console.log(instance.a); // 2

// 3.bind/apply/call
function bar() { console.log(this.a) }
var ctx = { a: 3 };
bar = bar.bind(ctx);
bar(); // 3

// 4.obj.调用
var obj = { a:4, bar };
obj.bar(); // 3

// 5.直接调用
var a = 5;
bar(); // 3
var barCopy = obj.bar;
barCopy(); // 3

五、总结

理解 this 机制需要把握两个核心:

  1. 动态性:this绑定取决于调用方式,而非定义位置
  2. 规则优先级:箭头函数 > new > 显式绑定(bind > call/apply) > 隐式绑定 > 默认绑定

希望本文能让你对 this 不再困惑!如果你在开发中遇到 this 相关的 “诡异问题”,比如:

  • 为什么嵌套箭头函数的 this 突然指向全局?
  • 为什么 setTimeout 里的 this 总是不对?
    欢迎在评论区留言,我们一起拆解 “玄学”,把 this 拿捏得明明白白!😊

学习建议:遇到 this 问题时,先标注函数的调用方式(有没有 obj./new/bind 等),再按优先级规则一步步推导,比凭感觉猜更可靠!

参考文献

  1. Kyle Simpson. (2015). 你不知道的JavaScript(上卷)  [M]. 第1版. 北京: 人民邮电出版社.

  2. MDN Web Docs. (2023). this - JavaScript [EB/OL].
    developer.mozilla.org/zh-CN/docs/…

  3. JS 中 this 指向问题相信我,只要记住本文的 7️⃣ 步口诀,就能彻底掌握 JS 中的 this 指向。 先念口诀 - 掘金