getter知多少

249 阅读4分钟

从想判断getter说起

如下一段代码示例:

class A {
   foo = 'foo';
   get bar() {
       return this.foo;
   }
}
var a = new A();
// How to judge 'a.bar' is getter?
function isGetProp(obj, propKey) {
    // TODO: how
}
isGetProp(a, 'bar') === true;
isGetProp(a, 'foo') === false;

如何判断class A的实例a中的bar属性是否是一个getter只读属性?

为什么要判断属性是否为getter?

这个问题的提出是来自之前的一篇分享《如何优雅地创建嵌套对象并赋初值》中的实现过程,还没看过且有兴趣的同学后面可以去看看。

前情提要:

class FormVO extends BaseModel {
    // 外部传入字段(=来自api或其他组件)
    firstName = '';
    lastName = '';
    
    // 内部处理逻辑(根据前端展示逻辑自定义)
    @computed
    get fullName() {
        return this.firstName + ' ' + this.lastName;
    };
}
// 使用FormVO.create
var formData = FormVO.create({
    firstName: 'Michael',
    lastName: 'Jordan'
});
// 代替下面这种方式
// new FormVO();

BaseModel.create方法简要逻辑如下:

class BaseModel {
    static create(props) {
        const instance = new this();
        // 逻辑示意,实际上 分配属性时 还有更多处理
        Object.assign(instance, props);
    }
}

回到上面FormVO的定义,由于FormVO中fullName是一个getter,所以如果出现下面的赋值不会生效,如果fullName是被mobx的@computed的装饰了的话,还会抛出异常,中断程序运行。

FormVO.create({
    fullName: 'xxx',
});

因为传入的初始值是来自第三方(比如接口api返回),会有风险,所以create方法中处理初始值时需要判断当前实例上的属性是否是getter或被mobx的@computed的装饰了的getter。后者可以通过mobx提供了isComputedProp来判断, 所以今天想讨论的是如何判断前者?

我尝试找js中是否提供原生判断方法可以直接判断,Object对象上也没找到直接可用的。

网上查了一圈,也没找到可用的。

那就需要自己实现,但要实现,首先得了解它是个什么东东?

接下来看看对象getter究竟是什么:

getter究竟是什么?是函数还是属性?

其实getter定义的是一种伪属性(调用方式与普通属性类型),但执行过程又与函数相似(只是没有入参),MDN文档中把它归类在函数项下,貌似介于属性和函数之间。其实在vue中大家并不陌生,computed叫计算属性或动态属性。react中class组件和mobx状态管理中也比较常见。

定义getter的三种方式:

字面量创建

var a = {
    foo: 'foo',
    get bar() {
        return this.foo;
    }
}
console.log('instance a is %o:\n', a);

image.png

defineProperty创建

var a = {
    foo: 'foo'
};
Object.defineProperty(a, 'bar', {
    get() {
        return this.foo;
    }
});
console.log('--- instance a is:\n %o', a);

image.png

class创建

class A {
   foo = 'foo';
   get bar() {
       return this.foo;
   }
}
var a = new A();
console.log('instance a is %o', a);
// 通过class创建的对象中动态属性是定义在其原型对象上的, 所以如下情况需要注意!!!
// b中没有bar属性
const b = {
    ...a
}
// rest中也没有bar属性
const {foo, ...rest} = a;

image.png

对比可以看出,通过class创建的对象中动态属性是定义在其原型对象上的,与上面两种定义方式有所区别。在解构赋值时尤其需要注意!!!

getter伪属性和普通属性的区别:

除了最直观的是否可写(其实不太严谨,因为定义相应的setter后也可写),还有什么区别吗?

可以通过Object.getOwnPropertyDescriptor获取属性的描述看出区别,这也是我们要实现判断逻辑用到的主要方法。

image.png

综上分析就可以给出文初提出的判断对象中属性是否为getter判断方法实现,如下:

function isGetterProp(instance, key) {
    if (key in instance) {
        let p = instance;
        let propDesc;
        while (p && !propDesc) {
            propDesc = Object.getOwnPropertyDescriptor(p, key);
            p = Object.getPrototypeOf(p);
        }
        // only getter
        return !!propDesc && (propDesc.get && !propDesc.set);
    }
    return false;
}

拓展思考

用好getter,能大大减少需要维护的状态字段的数量,状态字段的依赖流转尽可能自动化,能用一个状态字段表述的绝对不用两个,状态越少,逻辑可读性及可维护性就越强。再配合mobx的computed等功能,能解决getter每次访问都需要重新计算的潜在性能问题。

说完getter,与之相对的setter其实也比较有用,只是平时大家好像用得不多。

比如在以下场景中,配合使用setter,会比较方便。

// Model层 定义数据结构和一些内聚的处理逻辑
import moment from 'moment';
class VO {
   // case1: 时间戳和字符串的转换
   startDate = 1658985847640;
   get startDateStr() {
       return moment(startDate).format('YYYY-MM-DD HH:mm:ss');
   }
   set startDateStr(val) {
       this.startDate = moment(val).valueOf();
   }
   
   // case2: 做一些校验 提示。
   _value = 0; // 强类型语言中会将此定义为私有属性(private),ts中也可以
   get value() {
       return this._value;
   }
   set value(val) {
       if (val > 100) {
           alert('输入的值不能超过100哦😯!')
       } else {
           this._value = val;
       }
   }
   // ...
   
   // post api params model
   get DTO() {
       const params = {
           startDate: this.startDate,
           value: this.value,
           // ...
       } 
       return params;
   }
}
const vo = new VO();

// View层 展示
//...
<div>{{vo.startDateStr}}</div>
//...
<input value={vo.value} />
//...

// Controller层
// 切换日期
vo.startDateStr = '2022-07-27 14:20:00';
// 输入value
vo.value = 120;
// 提交数据
postApi('/post', {data: vo.DTO});

出于面向对象的思想,利用好getter和setter, model层可以处理很多只与内部字段相关的数据转换操作,使逻辑更为内聚。

附录:

参考: developer.mozilla.org/en-US/docs/…