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

JavaScript类的访问器属性应用

2023-06-206.7k 阅读

JavaScript类的访问器属性概述

在JavaScript中,类的访问器属性为我们提供了一种更灵活且强大的方式来控制对对象属性的访问。访问器属性包含获取器(getter)和设置器(setter),它们允许我们在获取或设置属性值时执行自定义的代码逻辑,而不仅仅是简单地获取或修改一个值。

从本质上讲,访问器属性不是数据属性,它们不直接存储值。相反,当读取访问器属性时,会调用获取器函数;当写入访问器属性时,会调用设置器函数。这种机制使得我们能够在属性访问的过程中实现诸如数据验证、计算派生值、执行副作用操作等功能。

基本语法

定义访问器属性的语法如下:

class MyClass {
    constructor() {
        this._privateValue = 0;
    }
    get myProperty() {
        return this._privateValue;
    }
    set myProperty(newValue) {
        if (typeof newValue === 'number') {
            this._privateValue = newValue;
        } else {
            throw new Error('Value must be a number');
        }
    }
}

在上述代码中,我们定义了一个MyClass类。其中,myProperty是一个访问器属性。get myProperty()是获取器函数,当我们读取myProperty属性时会调用它;set myProperty(newValue)是设置器函数,当我们为myProperty属性赋值时会调用它。注意,这里我们使用了一个下划线前缀_privateValue来表示这是一个内部使用的属性,虽然JavaScript本身并没有真正的私有属性概念,但这种约定可以提醒开发者该属性不应该在类外部直接访问。

获取器(Getter)的应用

计算派生值

获取器的一个常见应用场景是计算派生值。例如,假设我们有一个表示矩形的类,并且已经存储了矩形的宽度和高度属性。我们可以通过获取器来计算并返回矩形的面积。

class Rectangle {
    constructor(width, height) {
        this.width = width;
        this.height = height;
    }
    get area() {
        return this.width * this.height;
    }
}
const rect = new Rectangle(5, 10);
console.log(rect.area); 

在上述代码中,area是一个获取器属性。每次我们访问rect.area时,都会重新计算矩形的面积。这样,即使widthheight属性发生了变化,area始终能反映最新的计算结果。这种方式使得我们不需要显式地调用一个方法来计算面积,而是像访问普通属性一样方便。

隐藏内部实现细节

获取器还可以用于隐藏类的内部实现细节。例如,假设我们有一个类用于管理用户的密码。我们不希望外部代码直接访问密码的明文,但可能需要提供一种方式来检查密码是否符合特定的复杂度要求。

class User {
    constructor(password) {
        this._password = password;
    }
    get isPasswordStrong() {
        return this._password.length >= 8 && /[A-Z]/.test(this._password) && /[0-9]/.test(this._password);
    }
}
const user = new User('Abc12345');
console.log(user.isPasswordStrong); 

这里,isPasswordStrong是一个获取器属性。外部代码无法直接访问_password属性,但可以通过isPasswordStrong获取器来判断密码是否足够强。这样就隐藏了密码存储的具体细节,同时提供了一个有用的功能给外部代码使用。

懒加载数据

获取器在实现懒加载数据方面也非常有用。懒加载意味着只有在真正需要数据时才去加载它,而不是在对象创建时就加载所有数据,这对于提高性能和减少资源消耗非常有帮助。

class DataLoader {
    constructor() {
        this._data = null;
    }
    get data() {
        if (this._data === null) {
            // 模拟从服务器加载数据
            this._data = { key: 'value' };
        }
        return this._data;
    }
}
const loader = new DataLoader();
// 此时数据还未加载
console.log(loader.data); 
// 数据被加载并返回

在上述代码中,data是一个获取器属性。只有在第一次访问loader.data时,才会模拟从服务器加载数据并赋值给_data。后续访问data时,直接返回已经加载的数据,避免了重复加载。

设置器(Setter)的应用

数据验证

设置器最常见的应用之一是数据验证。当我们为对象的属性赋值时,需要确保赋的值符合一定的规则。例如,我们有一个表示年龄的属性,年龄应该是一个大于0且小于120的数字。

class Person {
    constructor() {
        this._age = 0;
    }
    set age(newAge) {
        if (typeof newAge === 'number' && newAge > 0 && newAge < 120) {
            this._age = newAge;
        } else {
            throw new Error('Invalid age value');
        }
    }
}
const person = new Person();
try {
    person.age = 30;
    console.log(person._age); 
    person.age = -5; 
} catch (error) {
    console.log(error.message); 
}

在上述代码中,age是一个设置器属性。当我们尝试为age赋值时,设置器函数会检查新值是否为有效的年龄。如果不符合要求,会抛出一个错误,从而防止无效数据被设置到对象中。

同步相关属性

设置器还可以用于同步相关属性。例如,假设我们有一个表示圆的类,同时存储了半径和直径属性。当半径发生变化时,直径也应该相应地更新。

class Circle {
    constructor(radius) {
        this._radius = radius;
        this._diameter = radius * 2;
    }
    set radius(newRadius) {
        if (typeof newRadius === 'number' && newRadius > 0) {
            this._radius = newRadius;
            this._diameter = newRadius * 2;
        } else {
            throw new Error('Invalid radius value');
        }
    }
    get diameter() {
        return this._diameter;
    }
}
const circle = new Circle(5);
console.log(circle.diameter); 
circle.radius = 10;
console.log(circle.diameter); 

在上述代码中,radius是一个设置器属性。当我们为radius赋值时,设置器函数不仅更新了_radius属性,还同步更新了_diameter属性,确保两个相关属性始终保持一致。

触发副作用操作

设置器可以在属性值改变时触发一些副作用操作。例如,当一个任务的状态发生改变时,我们可能需要记录日志。

class Task {
    constructor() {
        this._status = 'pending';
    }
    set status(newStatus) {
        const validStatuses = ['pending', 'in - progress', 'completed'];
        if (validStatuses.includes(newStatus)) {
            this._status = newStatus;
            console.log(`Task status changed to ${newStatus}`);
        } else {
            throw new Error('Invalid status value');
        }
    }
}
const task = new Task();
task.status = 'in - progress'; 

在上述代码中,status是一个设置器属性。当status的值被改变时,设置器函数会检查新值是否有效,并在值有效时记录状态改变的日志。

访问器属性与普通属性的区别

存储方式

普通属性直接在对象中存储数据值。例如:

const obj = {
    value: 10
};

这里value是一个普通属性,它在obj对象中直接存储了值10

而访问器属性并不直接存储值。如前面的MyClass类中的myProperty访问器属性:

class MyClass {
    constructor() {
        this._privateValue = 0;
    }
    get myProperty() {
        return this._privateValue;
    }
    set myProperty(newValue) {
        if (typeof newValue === 'number') {
            this._privateValue = newValue;
        } else {
            throw new Error('Value must be a number');
        }
    }
}

myProperty并不存储值,它通过获取器和设置器来间接访问和修改_privateValue

访问行为

当访问普通属性时,直接返回存储的值。例如:

const obj = {
    value: 10
};
console.log(obj.value); 

而访问访问器属性时,会调用获取器函数。例如:

const myObj = new MyClass();
console.log(myObj.myProperty); 

当为普通属性赋值时,直接修改存储的值:

const obj = {
    value: 10
};
obj.value = 20;
console.log(obj.value); 

为访问器属性赋值时,会调用设置器函数:

const myObj = new MyClass();
myObj.myProperty = 30;
console.log(myObj.myProperty); 

访问器属性在继承中的应用

重写访问器属性

在继承关系中,子类可以重写父类的访问器属性。例如,我们有一个父类Shape,它有一个获取面积的访问器属性area,子类Square继承自Shape并重写了area属性。

class Shape {
    constructor() {
        this._width = 0;
        this._height = 0;
    }
    get area() {
        return this._width * this._height;
    }
}
class Square extends Shape {
    constructor(side) {
        super();
        this._width = side;
        this._height = side;
    }
    get area() {
        return this._width * this._width;
    }
}
const square = new Square(5);
console.log(square.area); 

在上述代码中,Square类重写了Shape类的area访问器属性,以适应正方形面积的计算方式。

调用父类的访问器属性

子类在重写访问器属性时,有时也需要调用父类的访问器属性。例如,我们有一个父类Animal,它有一个表示健康状态的访问器属性health,子类Dog继承自Animal并在健康状态改变时增加一些额外的逻辑。

class Animal {
    constructor() {
        this._health = 100;
    }
    get health() {
        return this._health;
    }
    set health(newHealth) {
        if (typeof newHealth === 'number' && newHealth >= 0 && newHealth <= 100) {
            this._health = newHealth;
        } else {
            throw new Error('Invalid health value');
        }
    }
}
class Dog extends Animal {
    constructor() {
        super();
    }
    set health(newHealth) {
        super.health = newHealth;
        if (newHealth < 50) {
            console.log('The dog is not feeling well');
        }
    }
}
const dog = new Dog();
dog.health = 30; 

在上述代码中,Dog类的health设置器属性先调用了父类的health设置器来验证和设置健康值,然后根据健康值执行了额外的逻辑。

访问器属性与ES5的兼容性

在ES5中,虽然没有类的语法,但我们可以通过Object.defineProperty()方法来模拟访问器属性。例如:

const obj = {};
let _privateValue = 0;
Object.defineProperty(obj, 'myProperty', {
    get: function() {
        return _privateValue;
    },
    set: function(newValue) {
        if (typeof newValue === 'number') {
            _privateValue = newValue;
        } else {
            throw new Error('Value must be a number');
        }
    },
    enumerable: true,
    configurable: true
});
console.log(obj.myProperty); 
obj.myProperty = 10;
console.log(obj.myProperty); 

在上述代码中,通过Object.defineProperty()方法为obj对象定义了一个名为myProperty的访问器属性,其功能与ES6类中的访问器属性类似。enumerableconfigurable属性分别控制该属性是否可枚举和可配置。

最佳实践与注意事项

清晰的命名

在定义访问器属性时,应使用清晰的命名。获取器属性的命名应该反映它返回的值,设置器属性的命名应该反映它设置的属性。例如,get fullNameset fullName这样的命名就很直观,表明获取或设置的是完整名称。

避免副作用过重

虽然设置器可以触发副作用操作,但应避免副作用过于复杂或耗时。例如,在设置器中进行大量的网络请求或复杂的计算可能会导致性能问题,并且不符合设置器属性简洁、直观的设计初衷。

文档化

为访问器属性添加文档注释是非常重要的。这可以帮助其他开发者理解该属性的功能、输入输出要求以及可能的副作用。例如,使用JSDoc风格的注释:

/**
 * 获取用户的完整名称。
 * @returns {string} 用户的完整名称。
 */
get fullName() {
    return this.firstName + ' ' + this.lastName;
}

通过深入理解和合理应用JavaScript类的访问器属性,我们可以编写出更加健壮、灵活和易于维护的代码。无论是在处理数据验证、计算派生值,还是在管理对象状态和实现复杂业务逻辑方面,访问器属性都为我们提供了强大的工具。