深入理解 JavaScript 中的 Getter 和 Setter:从原理到实战

在日常的 JavaScript 开发中,我们经常需要处理对象的属性。通常,我们会直接通过点 notation(如 obj.name)来读取或修改这些属性。然而,随着应用逻辑变得复杂,直接访问属性往往会带来潜在的风险,比如我们无法在赋值时进行数据验证,也无法在读取时动态计算返回值。

这就是 ECMAScript 5 (ES5) 引入 Getter(获取器)Setter(设置器) 的原因。它们是两种特殊的方法,允许我们以更安全、更灵活的方式控制对对象属性的访问和修改。在这篇文章中,我们将深入探讨 Getter 和 Setter 的工作原理,学习如何在对象和类中定义它们,并通过丰富的实战案例来掌握这一强大的语言特性。

什么是 Getter 和 Setter?

简单来说,Getter 和 Setter 为我们提供了一种“伪属性”访问方式。从代码的使用者角度来看,他们就像是在操作普通的对象属性(例如 obj.name),但在内部,JavaScript 实际上是在调用特定的函数来处理这些操作。

  • Getter: 用于获取属性值。它让你能够在返回数据之前执行逻辑(如计算、格式化或访问权限检查)。
  • Setter: 用于设置属性值。它让你能够在接受新值之前执行逻辑(如数据验证、类型检查或触发相关的副作用,如更新 UI)。

使用这两个特性,我们可以实现所谓的数据封装,这是面向对象编程(OOP)的核心原则之一。

JavaScript Getter:智能获取数据

Getter 是一种用于绑定到对象属性的特殊方法。当你尝试读取该属性时,Getter 方法会自动被调用。

定义 Getter

在对象字面量或类中,我们可以使用 get 关键字后跟一个函数名来定义 Getter。注意,这个函数名将成为对外暴露的属性名。

让我们看一个基础的例子:

class Person {
    constructor(name) {
        // 使用下划线前缀约定表示这是一个“私有”或“受保护”的属性
        this._name = name;
    }

    // 定义 getter
    get name() {
        console.log("正在执行 Getter...");
        return this._name;
    }
}

const p1 = new Person("Anjali");
// 注意:这里没有括号,就像访问普通属性一样
console.log(p1.name);

输出:

正在执行 Getter...
Anjali

#### 在这个例子中:

  • 我们定义了一个名为 name 的 getter。
  • 当我们访问 INLINECODEaf8086f3 时,并没有直接读取 INLINECODE951e0297,而是触发了 get name() 方法。
  • 这允许我们在返回数据之前插入额外的逻辑(比如上面的日志,或者复杂的转换逻辑)。

Getter 与普通函数的区别

你可能会问:“为什么不直接写一个叫 getName() 的函数?” 这是一个好问题。Getter 提供了一种更优雅、更符合直觉的语法。

特性

Getter (get)

普通函数 :—

:—

:— 调用方式

像属性一样访问 (INLINECODE559f83e1)

必须显式调用 (INLINECODE7f9d5180) 代码可读性

看起来像是在读取数据,语义更清晰

明显是在执行操作,需要括号 封装性

隐藏了它是方法的实现细节,更像数据

暴露了它是一个方法的本质

最佳实践: 当你的操作仅仅是“返回一个基于对象状态的值”时,使用 Getter;当操作涉及复杂计算或需要参数时,使用普通函数。

JavaScript Setter:掌控数据修改

如果说 Getter 是数据的守门人,那么 Setter 就是数据的过滤器。 Setter 允许我们在修改属性值时执行验证逻辑,防止无效数据进入我们的对象。

定义 Setter

Setter 使用 set 关键字定义。它必须接受一个参数(即要设置的新值)。

class Person {
    constructor(name) {
        this._name = name;
    }

    // 定义 getter
    get name() {
        return this._name;
    }

    // 定义 setter
    set name(newName) {
        // 在这里添加验证逻辑
        if (newName.length < 2) {
            console.log("错误:名字太短了");
            return;
        }
        this._name = newName;
    }
}

const p2 = new Person("Alice");
console.log(p2.name); // 输出: Alice

p2.name = "Bob"; // 使用 setter 修改值
console.log(p2.name); // 输出: Bob

p2.name = "X"; // 尝试设置无效值
console.log(p2.name); // 输出: Bob (值未变)

输出:

Alice
Bob
错误:名字太短了
Bob

#### 在这个例子中:

  • 我们通过 p2.name = "Bob" 这种看似赋值的方式调用了 setter。
  • Setter 检查了新名字的长度。如果不符合要求,它拒绝更新 _name 属性。
  • 这种机制极大地增强了代码的健壮性。

实战场景:Getters 与 Setters 的强大组合

现在我们已经掌握了基础,让我们通过几个更贴近实际开发的场景来看看它们如何协同工作。

场景一:在对象字面量中使用 Getters 和 Setters

除了在类中,我们也可以直接在普通对象字面量中使用它们。这在处理数据转换时非常有用。

const user = {
    firstName: "Anurag",
    lastName: "Das",

    // Getter:动态计算全名
    get fullName() {
        return `${this.firstName} ${this.lastName}`;
    },

    // Setter:解析全名并更新对应的属性
    set fullName(nameStr) {
        const parts = nameStr.split(" ");
        if (parts.length !== 2) {
            console.log("请输入格式为 ‘名 姓‘ 的完整名称");
            return;
        }
        this.firstName = parts[0];
        this.lastName = parts[1];
    }
};

console.log(user.fullName); // 读取:输出 "Anurag Das"

// 写入:自动拆分字符串并更新 firstName 和 lastName
user.fullName = "Rahul Dravid";
console.log(user.firstName); // 输出: Rahul
console.log(user.lastName); // 输出: Dravid

分析: 这里 INLINECODE32d519a1 表面上是一个属性,但实际上它是 INLINECODE6c20b3b8 和 lastName 的衍生品。这种模式让数据接口更加简洁。

场景二:保护计算属性(防止逻辑错误)

有时候,我们并不希望某些属性被外部随意修改。例如,一个矩形的“面积”是由“宽”和“高”计算得来的,直接设置面积是不合理的。

class Rectangle {
    constructor(width, height) {
        this.width = width;
        this.height = height;
    }

    // 面积是通过计算得出的,所以只需要 Getter
    get area() {
        return this.width * this.height;
    }

    // 我们可以定义一个 Setter 来拦截对 area 的赋值行为
    set area(value) {
        // 这是一个只读属性,或者我们可以抛出错误
        console.log("警告:面积是由宽和高计算得出的,不能直接设置!");
    }
}

const rect = new Rectangle(10, 5);
console.log(rect.area); // 输出: 50

rect.area = 1000; // 尝试强制修改
// 输出: 警告:面积是由宽和高计算得出的,不能直接设置!

场景三:利用私有字段实现真正的封装

随着 JavaScript 的进化,ES2022 正式引入了私有字段(以 # 开头)。结合 Getter 和 Setter,我们可以构建出真正安全的数据模型。

class BankAccount {
    // #balance 是私有字段,外部无法直接访问
    #balance;

    constructor(initialBalance) {
        this.#balance = initialBalance;
    }

    // 提供读取余额的接口
    get balance() {
        return this.#balance;
    }

    // 控制余额的修改
    set balance(amount) {
        if (amount < 0) {
            console.log("错误:余额不能为负数!交易拒绝。");
        } else {
            this.#balance = amount;
            console.log(`交易成功。当前余额:${this.#balance}`);
        }
    }

    deposit(amount) {
        this.balance += amount; // 内部复用 setter
    }
}

const myAccount = new BankAccount(1000);
console.log(myAccount.balance); // 输出: 1000

myAccount.balance = -500; // 输出错误信息
console.log(myAccount.balance); // 依然是 1000

myAccount.deposit(200); // 输出成功信息
console.log(myAccount.balance); // 1200

关键点: 即使有人尝试 myAccount.#balance = 0,代码也会报错,从而保证了数据的安全性。

场景四:旧式方法 —— Object.defineProperty()

在 ES6 类出现之前,或者当我们无法修改对象原始定义时,可以使用 Object.defineProperty() 来动态添加 Getter 和 Setter。这在处理第三方库的对象或维护旧代码时非常有用。

const product = {
    price: 100
};

// 动态添加 ‘tax‘ 属性
Object.defineProperty(product, ‘tax‘, {
    get: function() {
        // 假设税率是 10%
        return this.price * 0.10;
    },
    set: function(value) {
        // 如果设置了 tax,我们反推 price
        // 这只是一个演示逻辑,实际上这种反向计算可能不常见
        console.log(`尝试设置税额为 ${value},这通常是不允许的自动计算属性。`);
    }
});

console.log(product.price);  // 100
console.log(product.tax);    // 10

常见陷阱与性能建议

虽然 Getter 和 Setter 很强大,但在使用时也有一些需要注意的地方。

1. 性能考量

Getter 的每次访问都会执行函数。如果 Getter 内部包含非常复杂的计算(例如遍历一个巨大的数组),而该属性又被频繁读取(例如在一个循环中每秒读取 60 次用于渲染),它可能会成为性能瓶颈。

解决方案: 如果计算代价高昂,考虑使用缓存机制,或者仅在数据变更时重新计算并存储结果,Getter 仅返回缓存值。

2. 语义误区

不要滥用 Getter。如果获取数据的操作涉及到明显的副作用(例如发送 API 请求或者修改 DOM),那么它不应该是一个 Getter,而应该是一个普通方法。开发者通常期望访问属性是一个廉价的、无副作用的操作。

错误的示例:

// 不好的做法:Getter 触发了网络请求
class UserProfile {
    get userData() {
        return fetch(‘/api/user‘); // 别这样做!
    }
}

总结

在这篇文章中,我们探索了 JavaScript 中 Getter 和 Setter 的方方面面。我们从基本的语法开始,了解了它们如何通过 INLINECODE270ec99b 和 INLINECODE65c8b500 关键字工作,并通过对比普通函数发现了它们在代码可读性上的优势。

我们深入了几个实战案例,从简单的姓名格式化到严格的银行账户余额验证,再到利用 Object.defineProperty 进行动态属性配置。这些工具让我们能够构建出更安全、更模块化且易于维护的代码。

作为开发者,你应该遵循以下最佳实践:

  • 始终使用 Setter 进行数据验证:不要让无效状态进入你的对象。
  • 结合私有字段:使用 # 前缀保护你的内部数据,只通过 Getter/Setter 暴露必要的接口。
  • 保持 Getter 的轻量级:避免在 Getter 中执行重计算或副作用操作。

掌握了 Getter 和 Setter,你就掌握了 JavaScript 面向对象编程中关于封装的关键一环。在你的下一个项目中,试着重构一段代码,利用这些特性来提升代码质量吧!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。如需转载,请注明文章出处豆丁博客和来源网址。https://shluqu.cn/29853.html
点赞
0.00 平均评分 (0% 分数) - 0