【JavaScript】🎯JavaScript 的 "环境特工":this 指向的五大行动准则

110 阅读6分钟

🔍 1 前言

在 JavaScript 语言中,“一切皆对象”的特性深入人心,甚至其运行环境本身也是对象。这意味着 所有函数都依托于某个对象运行,而 this 所代表的,正是函数运行时所处的对象(即其运行环境)。

从本质上理解,this 的概念相当直观。然而,JavaScript 的动态语言特性带来了核心挑战:函数的运行环境会随着代码的执行过程而动态改变。这种动态性直接导致了 this 的指向也随之实时变化,使得我们无法在代码编写阶段就确定它最终指向哪个对象。正因如此,this 成为了 JavaScript 开发中一个广为人知的难点。

本文将深入剖析 this 的由来、核心指向规则,以及绑定 this 的具体方法。

🎯 2 this 是什么

2.1 概念

如前所述,this 会随函数的运行环境动态变化。但无论环境如何变换,this 始终指向一个对象。简而言之,this 代表当前属性或方法所属的环境对象,其指向会随着当前环境对象的变更而变更

2.2 本质

如需回顾引用数据类型的存储方式,请参考这篇文章:【JavaScript】🔍 变量变形记:JavaScript 数据类型的 8 种分身术!

var A = { a: 123 };

this 的出现与内存的数据结构密切相关。在讨论引用数据类型时提到,当我们将一个对象赋值给变量 A 时,JS 引擎会先在堆内存中创建该对象,然后将对象的引用地址赋值给变量 A。访问 A.a 时,需要先通过变量 A 获取地址,再根据地址找到原始对象并返回其 a 属性。

对象的属性也可以是函数:

var A = {
    a: function() {}
};

此时,A.a 存储的是另一个地址(指向函数)。JavaScript 允许函数在其内部访问当前环境的变量,而函数的运行环境又会动态变化。因此,需要一种机制告知函数其当前的运行环境——这就是 this

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

fn();       // -> 1 (非严格模式,this 指向全局对象)
obj.fn();   // -> 2 (this 指向 obj)

⚖️ 3 this 的指向规则

  1. 独立函数调用:

    • 严格模式下 ('use strict'): this 绑定为 undefined
    • 非严格模式下: this 绑定为全局对象 (globalThis,浏览器中为 window,Node.js 中为 global)。
    const o1 = {
        text: 'o1',
        fn: function() {
            return this.text;
        }
    };
    
    const o3 = {
        text: 'o3',
        fn: function() {
            var fn = o1.fn; // 将 o1.fn 方法的引用赋值给变量 fn
            return fn();    // 独立调用 fn(), this 丢失 o1 的绑定
        }
    };
    
    console.log(o1.fn()); // -> 'o1' (通过 o1 调用,this 指向 o1)
    console.log(o3.fn()); // -> undefined (独立调用 fn(), 非严格模式 this 指向全局,全局无 text 属性)
    
  2. 构造函数调用 (使用 new 关键字): this 指向新创建的实例对象(这与 new 运算符的内部机制有关)。

    如需回顾 new 的原理,请参考:【JavaScript】✨ JavaScript 对象 & 包装对象:魔法世界大冒险!

  3. 显式绑定 (使用 call, apply, bind 方法): 函数体内的 this 会被明确指定为传入的第一个参数所指向的对象(详见下文 4.2 节)。

  4. 隐式绑定 (通过上下文对象调用): 当函数作为一个对象的属性被调用时(obj.fn()),函数体内的 this 会被绑定到该对象 (obj) 上。

    const o1 = {
        text: 'o1',
        fn: function() {
            return this.text;
        }
    };
    
    const o2 = {
        text: 'o2',
        fn: function() {
            return o1.fn(); // 虽然是在 o2 的方法里调用,但实际执行的是 o1.fn()
        }
    };
    
    console.log(o1.fn()); // -> 'o1' (this 指向 o1)
    console.log(o2.fn()); // -> 'o1' (o1.fn() 被独立调用,规则1,非严格模式 this 指向全局或 undefined)
    // 注意:o2.fn() 内部的 o1.fn() 是独立调用,而非通过 o2 的上下文调用。
    
  5. 箭头函数: 箭头函数本身没有自己的 this。它的 this 继承自定义它时所处的父级作用域(词法作用域),且在函数生命周期内固定不变。箭头函数不能用作构造函数。

    // 箭头函数示例
    var a = 1;
    var obj = {
        a: 2,
        bar: function() {        // bar 是普通函数,this 指向调用它的对象 obj
            const baz = () => {  // 箭头函数 baz 在 bar 内部定义,继承 bar 的 this (指向 obj)
                console.log(this.a);
            };
            baz(); // 调用 baz
        },
    };
    obj.bar(); // -> 2 (箭头函数 baz 的 this 继承自 bar 的 this, 即 obj)
    

🛠️ 4 绑定 this 的方法

🔗 4.1 隐式绑定

当函数作为某个对象的属性被调用时(即拥有上下文对象),隐式绑定规则会自动将函数内部的 this 绑定到该上下文对象上。

// 隐式绑定示例
var a = 1; // 全局变量 a
function foo() {
    console.log(this.a);
}
var obj = {
    a: 2,   // 对象属性 a
    foo: foo // 引用全局函数 foo
};
obj.foo(); // -> 2
// 解释:foo 作为 obj 的属性被调用 (obj.foo()),因此 foo 内部的 this 指向 obj,访问 obj.a 得到 2

可以将 obj.foo() 理解为:在 obj 的地址空间中找到 foo 函数并执行它,因此 foo 执行时的环境对象是 objthis 自然指向 obj

🔧 4.2 显式绑定

JavaScript 提供了三个内置方法 (call, apply, bind),允许我们显式地设置函数调用时的 this 值。

🪄 4.2.1 Function.prototype.call(thisArg, ...args)

  • 立即调用目标函数。
  • 第一个参数 thisArg:指定函数内 this 应指向的对象。
  • 后续参数:以逗号分隔的形式直接传递给目标函数。

🧩 4.2.2 Function.prototype.apply(thisArg, [argsArray])

  • 立即调用目标函数。
  • 第一个参数 thisArg:指定函数内 this 应指向的对象。
  • 第二个参数 argsArray:一个数组(或类数组对象),包含传递给目标函数的参数。

🎭 4.2.3 Function.prototype.bind(thisArg, ...args)

  • 不会立即调用目标函数,而是返回一个新的函数
  • 第一个参数 thisArg:指定新函数内部永久绑定this 值。
  • 后续参数(可选):在绑定时可以预先传入部分参数(柯里化),剩余参数可以在新函数被调用时再传入。
  • 常用于需要延迟执行或需要固定 this 值的场景(例如事件处理函数)。
const person = {
    name: "张三",
    age: 30,
};

function introduce(city, hobby) {
    console.log(
        `大家好,我是${this.name},今年${this.age}岁,来自${city},喜欢${hobby}。`
    );
}

// 1. 使用 call() 方法
console.log("=== call() 演示 ===");
introduce.call(person, "北京", "游泳"); // -> "大家好,我是张三,今年30岁,来自北京,喜欢游泳。"

// 2. 使用 apply() 方法
console.log("\n=== apply() 演示 ===");
introduce.apply(person, ["上海", "读书"]); // -> "大家好,我是张三,今年30岁,来自上海,喜欢读书。"

// 3. 使用 bind() 方法
console.log("\n=== bind() 演示 ===");
const partiallyBound = introduce.bind(person, "深圳"); // 绑定 this 为 person, 预先传入 city='深圳'
partiallyBound("旅行"); // 调用新函数,传入 hobby='旅行' -> "大家好,我是张三,今年30岁,来自深圳,喜欢旅行。"

// 额外演示:绑定到不同对象
const anotherPerson = {
    name: "李四",
    age: 25,
};

console.log("\n=== 绑定到不同对象 ===");
introduce.call(anotherPerson, "杭州", "爬山"); // -> "大家好,我是李四,今年25岁,来自杭州,喜欢爬山。"