MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

JavaScript类的私有字段与私有方法

2024-08-145.2k 阅读

JavaScript 类的私有字段

在传统的 JavaScript 编程中,实现类的私有字段并非易事。JavaScript 没有像一些其他编程语言(如 Java、C++)那样原生支持私有成员的语法。在 ES2015(ES6)引入类的语法之前,开发者通常使用闭包和立即执行函数表达式(IIFE)来模拟私有变量。

传统方式模拟私有字段

function Person(name) {
    let privateName = name;

    this.getPrivateName = function() {
        return privateName;
    };
}

let person = new Person('John');
console.log(person.getPrivateName()); 
// 输出: John
// 这里尝试直接访问 privateName 会报错,因为它是闭包内的局部变量
// console.log(person.privateName); 

在上述代码中,privateName 变量通过闭包被封装在 Person 函数内部,外部代码无法直接访问,只能通过 getPrivateName 方法来间接获取其值,从而模拟了私有字段的效果。

ES2022 之前的尝试与局限

ES2015 引入了类的语法,但当时并没有直接支持私有字段。开发者尝试通过约定俗成的方式来表示私有字段,比如在字段名前加下划线。

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

let person = new Person('Jane');
console.log(person.getName()); 
// 输出: Jane
// 虽然 _name 表示私有字段,但仍可从外部访问
console.log(person._name); 

这种方式只是一种约定,并没有真正实现私有性,外部代码仍然可以直接访问 _name 字段。

ES2022 正式引入私有字段

ES2022 正式为 JavaScript 类带来了真正的私有字段支持。私有字段的名称以 # 开头。

class Person {
    #name;
    constructor(name) {
        this.#name = name;
    }
    getName() {
        return this.#name;
    }
}

let person = new Person('Bob');
console.log(person.getName()); 
// 输出: Bob
// 尝试从外部访问 #name 会报错
// console.log(person.#name); 

在上述代码中,#name 是一个私有字段,只能在类的内部被访问和修改。如果在类的外部尝试访问 person.#name,会导致语法错误。

私有字段的特性

  1. 唯一性:每个私有字段在其所在的类中是唯一的。即使不同类中私有字段名相同,它们也是相互独立的。
class ClassA {
    #field;
    constructor() {
        this.#field = 'A';
    }
}

class ClassB {
    #field;
    constructor() {
        this.#field = 'B';
    }
}

let a = new ClassA();
let b = new ClassB();
// 两个 #field 是不同的,相互不影响
  1. 不能通过对象字面量访问:私有字段不能通过对象字面量的方式访问,只能在类的方法中访问。
class Example {
    #privateField = 'value';
}

let obj = new Example();
// 以下操作会报错
// let value = obj['#privateField']; 
  1. 不参与属性枚举:私有字段不会出现在 for...in 循环、Object.keys()Object.getOwnPropertyNames() 等操作中。
class Product {
    #price;
    constructor(price) {
        this.#price = price;
    }
}

let product = new Product(100);
let keys = Object.keys(product);
// keys 为空数组,因为 #price 不参与属性枚举
console.log(keys); 

私有字段与继承

当涉及到继承时,子类无法直接访问父类的私有字段。

class Animal {
    #species;
    constructor(species) {
        this.#species = species;
    }
    getSpecies() {
        return this.#species;
    }
}

class Dog extends Animal {
    constructor() {
        super('Dog');
        // 尝试在子类中访问父类的 #species 会报错
        // console.log(this.#species); 
    }
}

let dog = new Dog();
console.log(dog.getSpecies()); 
// 输出: Dog,通过父类的公有方法访问私有字段

在上述代码中,Dog 类继承自 Animal 类,但 Dog 类不能直接访问 Animal 类的 #species 私有字段,只能通过 Animal 类提供的公有方法 getSpecies 来间接获取。

JavaScript 类的私有方法

与私有字段类似,JavaScript 在 ES2022 之前没有原生的私有方法支持,开发者同样需要通过各种技巧来模拟。

传统方式模拟私有方法

function MathUtils() {
    function privateAdd(a, b) {
        return a + b;
    }

    this.publicSum = function(numbers) {
        let sum = 0;
        for (let num of numbers) {
            sum = privateAdd(sum, num);
        }
        return sum;
    };
}

let math = new MathUtils();
let result = math.publicSum([1, 2, 3]);
console.log(result); 
// 输出: 6
// 尝试从外部调用 privateAdd 会报错
// console.log(privateAdd(1, 2)); 

在上述代码中,privateAdd 函数通过闭包封装在 MathUtils 函数内部,外部无法直接调用,只有 publicSum 公有方法可以调用它,从而模拟了私有方法。

ES2022 引入的私有方法

ES2022 引入了私有方法,其名称同样以 # 开头。

class MathOperations {
    #add(a, b) {
        return a + b;
    }
    sum(numbers) {
        let sum = 0;
        for (let num of numbers) {
            sum = this.#add(sum, num);
        }
        return sum;
    }
}

let operations = new MathOperations();
let sumResult = operations.sum([1, 2, 3]);
console.log(sumResult); 
// 输出: 6
// 尝试从外部调用 #add 会报错
// console.log(operations.#add(1, 2)); 

在这个例子中,#add 是一个私有方法,只能在 MathOperations 类的内部被调用。如果在类的外部尝试调用 operations.#add(1, 2),会导致语法错误。

私有方法的特性

  1. 只能在类内部调用:私有方法只能在类的其他方法中调用,外部代码无法直接访问。
class Printer {
    #printMessage(message) {
        console.log('Private message:', message);
    }
    publicPrint() {
        this.#printMessage('Hello from public method');
    }
}

let printer = new Printer();
printer.publicPrint(); 
// 输出: Private message: Hello from public method
// 尝试从外部调用 #printMessage 会报错
// printer.#printMessage('Outside call'); 
  1. 不影响类的原型:私有方法不会出现在类的原型上,不会被继承的子类意外访问或重写。
class BaseClass {
    #privateMethod() {
        return 'Base private method';
    }
    publicMethod() {
        return this.#privateMethod();
    }
}

class SubClass extends BaseClass {
    // 这里不会意外重写父类的 #privateMethod
    publicMethod() {
        return super.publicMethod() +'from SubClass';
    }
}

let sub = new SubClass();
console.log(sub.publicMethod()); 
// 输出: Base private method from SubClass
  1. 增强封装性:私有方法有助于将类的内部实现细节隐藏起来,只暴露必要的公有接口给外部使用,提高代码的安全性和可维护性。

私有方法与继承

如同私有字段,子类不能直接调用父类的私有方法。

class Shape {
    #calculateArea() {
        return 0;
    }
    getArea() {
        return this.#calculateArea();
    }
}

class Rectangle extends Shape {
    constructor(width, height) {
        super();
        this.width = width;
        this.height = height;
    }
    // 不能在子类中直接调用父类的 #calculateArea
    // 必须重写 getArea 方法来实现自己的逻辑
    getArea() {
        return this.width * this.height;
    }
}

let rectangle = new Rectangle(5, 10);
console.log(rectangle.getArea()); 
// 输出: 50

在上述代码中,Rectangle 类继承自 Shape 类,但不能直接调用 Shape 类的 #calculateArea 私有方法。Rectangle 类需要重写 getArea 公有方法来实现自己的面积计算逻辑。

私有字段与私有方法的应用场景

数据封装与保护

在开发模块或类库时,常常需要保护内部数据不被外部随意修改,以确保数据的完整性和一致性。

class BankAccount {
    #balance;
    constructor(initialBalance) {
        this.#balance = initialBalance;
    }
    deposit(amount) {
        if (amount > 0) {
            this.#balance += amount;
            return true;
        }
        return false;
    }
    withdraw(amount) {
        if (amount > 0 && amount <= this.#balance) {
            this.#balance -= amount;
            return true;
        }
        return false;
    }
    getBalance() {
        return this.#balance;
    }
}

let account = new BankAccount(1000);
account.deposit(500);
console.log(account.getBalance()); 
// 输出: 1500
account.withdraw(200);
console.log(account.getBalance()); 
// 输出: 1300
// 尝试直接修改 #balance 会报错,保护了账户余额数据
// account.#balance = -100; 

BankAccount 类中,#balance 私有字段确保了账户余额不能被外部直接修改,只能通过 depositwithdraw 方法来操作,保证了数据的安全性。

隐藏实现细节

当实现一个复杂的算法或功能时,可能有一些辅助方法不希望暴露给外部使用,这些方法可以定义为私有方法。

class SearchEngine {
    #index = [];
    #addDocumentToIndex(document) {
        this.#index.push(document);
    }
    #searchInIndex(query) {
        let results = [];
        for (let doc of this.#index) {
            if (doc.includes(query)) {
                results.push(doc);
            }
        }
        return results;
    }
    addDocument(document) {
        this.#addDocumentToIndex(document);
    }
    search(query) {
        return this.#searchInIndex(query);
    }
}

let engine = new SearchEngine();
engine.addDocument('JavaScript is awesome');
engine.addDocument('Learn JavaScript');
let searchResults = engine.search('JavaScript');
console.log(searchResults); 
// 输出: ['JavaScript is awesome', 'Learn JavaScript']
// 外部不需要知道 #addDocumentToIndex 和 #searchInIndex 的实现细节
// 只需要使用 addDocument 和 search 公有方法

SearchEngine 类中,#addDocumentToIndex#searchInIndex 私有方法负责内部的索引添加和搜索逻辑,外部只需要使用 addDocumentsearch 公有方法,隐藏了内部实现细节,提高了代码的可维护性。

避免命名冲突

在大型项目中,不同的模块或类可能会使用相同的方法名或字段名。私有字段和私有方法可以有效避免这种命名冲突。

class ModuleA {
    #data;
    #processData() {
        // 处理逻辑
    }
    publicMethod() {
        this.#processData();
    }
}

class ModuleB {
    #data;
    #processData() {
        // 不同的处理逻辑
    }
    publicMethod() {
        this.#processData();
    }
}

let a = new ModuleA();
let b = new ModuleB();
// 虽然 ModuleA 和 ModuleB 都有 #data 和 #processData
// 但它们是相互独立的,不会产生命名冲突

在上述代码中,ModuleAModuleB 类都有自己的私有字段 #data 和私有方法 #processData,由于私有成员的唯一性,它们不会相互干扰,避免了命名冲突。

私有字段与私有方法的注意事项

兼容性问题

虽然 ES2022 引入了私有字段和私有方法,但并非所有的 JavaScript 运行环境都支持。在实际项目中,特别是需要支持旧版本浏览器或运行时的情况下,可能需要使用转译工具(如 Babel)将代码转换为兼容的版本。 例如,使用 Babel 配置文件(.babelrc):

{
    "presets": ["@babel/preset - env"]
}

然后通过 Babel 命令将包含私有字段和私有方法的代码进行转译,以确保在目标环境中能够正常运行。

调试与测试

由于私有字段和私有方法不能直接从外部访问,调试和测试可能会变得稍微复杂一些。在调试时,通常需要在类的公有方法中添加日志输出或使用调试工具来查看内部状态。

class DebugClass {
    #privateValue;
    constructor(value) {
        this.#privateValue = value;
    }
    publicMethod() {
        console.log('Inside publicMethod, #privateValue:', this.#privateValue);
        // 其他逻辑
    }
}

let debugObj = new DebugClass(42);
debugObj.publicMethod(); 
// 在控制台输出: Inside publicMethod, #privateValue: 42

在测试方面,需要通过公有接口来间接验证私有成员的状态和行为。例如,对于一个包含私有字段和私有方法的类,可以编写测试用例来测试公有方法的输出是否符合预期,从而间接验证私有部分的正确性。

与反射 API 的关系

JavaScript 的反射 API(如 Reflect 对象)提供了一些操作对象内部属性和方法的能力,但私有字段和私有方法不受反射 API 的影响。

class ReflectExample {
    #privateField;
    constructor() {
        this.#privateField = 'private value';
    }
}

let example = new ReflectExample();
// 使用反射 API 无法访问私有字段
let keys = Reflect.ownKeys(example);
console.log(keys); 
// 不会包含 #privateField

在上述代码中,Reflect.ownKeys 方法不会返回私有字段 #privateField,这进一步体现了私有成员的封闭性。

通过深入了解 JavaScript 类的私有字段与私有方法,开发者能够更好地实现数据封装、隐藏实现细节、避免命名冲突,提升代码的安全性、可维护性和可扩展性。同时,注意它们在兼容性、调试测试以及与反射 API 关系等方面的问题,有助于在实际项目中更有效地应用这些特性。