打字稿:装饰器在 angular 项目和打字稿游乐场上的行为不同

Typescript: decorators behave differently on angular project and typescript playground

我需要在 angular 2.0.0-rc1 中将一个对象序列化为 json,当我发现 Typescript 的 private 根本不是私有的,并设置 属性不通过JSON.stringify.

输出

所以我着手装饰class:

//method decorator
function enumerable(value: boolean) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        descriptor.enumerable = value;
    };
}
//property decorator
function exclude(target: any, propertyKey: string): any {
    return { enumerable: false };
}
class MyClass {
    test: string = "test";
    @exclude
    testExclude: string = "should be excluded";
    @enumerable(true)
    get enumerated(): string {
        return "yes";
    }
    @enumerable(false)
    get nonEnumerated(): string {
        return "non enumerable"
    }
}

let x = new MyClass();
//1st
console.log(JSON.stringify(x));
//2nd
console.log(JSON.stringify(x, Object.keys(MyClass.prototype)));
//3rd
console.log(JSON.stringify(x, Object.keys(x).concat(Object.keys(MyClass.prototype))));//test 3

Typescript playground 上,这给出了

{"test":"test"}
{"enumerated":"yes"}
{"test":"test","enumerated":"yes"}

但是在我的项目 (angular 2.0.0-rc1) 上,这给出了

{"test":"test","testExclude":"should be excluded"}
{"enumerated":"yes"}
{"test":"test","testExclude":"should be excluded","enumerated":"yes"}

我真正想要的是 playground 的输出#3。

看了转译后的代码后, 唯一的区别是反射元数据的代码:

//snip ...

    __decorate([
        exclude, 
        __metadata('design:type', String)
    ], MyClass.prototype, "testExclude", void 0);
    __decorate([
        enumerable(true), 
        __metadata('design:type', String)
    ], MyClass.prototype, "enumerated", null);
    __decorate([
        enumerable(false), 
        __metadata('design:type', String)
    ], MyClass.prototype, "nonEnumerated", null);
    return MyClass;
}());

none 操场上 __metadata 行。

这里发生了什么?我怎样才能在我的项目中获得 playground 的第 3 名?

已修复(或者可能只是一种解决方法)。

请注意,在 playground 中,Reflect-metadata 不可用。 属性 装饰器可以 return 一个对象被赋值(或)到描述符来改变它的行为。在 angular 环境中,Reflect-metadata(特别是 Reflect.decorate())被用来装饰事物。

阅读 reflect-metadata doc, and this 后,显然无法更改 属性 装饰器上的 属性Descriptor,因为它与构造函数而不是原型相关联。解决方案(解决方法)是使用新描述符重新创建 属性。

function include(value: boolean) {
    return function (target: any, propertyKey: string): any {
        // Buffer the value
        var _val = target[propertyKey];
        // Delete property.
        if (delete target[propertyKey]) {
            // Create new property with getter and setter
            Object.defineProperty(target, propertyKey, {
                get: () => _val,
                set: (newVal) => _val = newVal,
                enumerable: value,
                configurable: true
            });
        }
    }
}

只需要工厂,所以我可以使用 @include(false) 而不是 @exclude

唯一的缺点是 属性 现在绑定到原型,因此正常的 JSON.stringify(instance) 不会序列化它。

关于这一点,我们可以进一步使通用装饰器在 属性 和方法中都可用,例如:

//method decorator
function excludeMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    descriptor.enumerable = false;
    return descriptor;
};
//property decorator
function excludeProperty(target: any, propertyKey: string): any {
    // Buffer the value
    var _val = target[propertyKey];
    // Delete property.
    if (delete target[propertyKey]) {
        // Create new property with getter and setter
        Object.defineProperty(target, propertyKey, {
            get: () => _val,
            set: (newVal) => _val = newVal,
            enumerable: false,
            configurable: true
        });
    }
}
function exclude(...args : any[]) {
    switch(args.length) {
        case 2:
            return excludeProperty.apply(this, args);
        case 3:
            if (typeof args[2] !== "number")
                return excludeMethod.apply(this, args);
        default:
            throw new Error("Decorators are not valid here!");
    }
}

所以现在我们可以这样使用它了:

class MyClass {
    test: string = "test";
    @exclude
    testExclude: string = "should be excluded";
    get enumerated(): string {
        return "yes";
    }
    @exclude
    get nonEnumerated(): string {
        return "non enumerable"
    }
    constructor() {}
}

let x = new MyClass();
//to serialize, we have to whitelist the instance and its prototype prop keys
console.log(JSON.stringify(x, Object.keys(x).concat(Object.keys(MyClass.prototype))));

到目前为止,我还没有找到更简洁的方法。

我掉进了兔子洞...

所以出于某种原因,将白名单添加到 JSON.stringify 以某种方式使其无法递归序列化嵌套对象:

class a {
    p1 = 1;
    p2 = 2;
}
class b {
    m1 = new a();
    m2 = "test";
    m3 = new Array<a>();
}
let i = new b();
i.m3.push(new a());
i.m3.push(new a());

JSON.stringify(i); 
// properly gives 
// {"m1":{"p1":1,"p2":2},"m2":"test","m3":[{"p1":1,"p2":2},{"p1":1,"p2":2}]}

JSON.stringify(i, Object.keys(i).concat(Object.keys(Object.getPrototypeOf(i))));
// nested class a doesn't get serialized 
// {"m1":{},"m2":"test","m3":[{},{}]}

如果你像我一样想在 TS 中隐藏私有变量并给它一个只读外观,那么把它放在那里 属性:

将它声明为一个简单的对象成员,然后在构造函数中修改它的属性描述符:

//Don't do this
class a {
    private _prop;
    get prop() { return _prop; }
}

//do this instead
class a {
    prop; //just define your public-facing property
    constructor() {
        let _prop; //internal variable here
        Object.defineProperty(this, "prop", { //now we modify the existing prop, 
            get: () =>  _prop, //closure to outside variable 
            //(no set here, it's readonly after all)
            enumerable: true, //make sure it's visible
            configurable: false //close up access
        }); 
    }
}

现在我们可以简单地使用 JSON.stringify(instance)。唯一的缺点是如果你有复杂的 getter/setter,请记住它在每个 instance/new.

中被调用

使用此模式和上面的 @exclude 装饰器,几乎可以解决我的用例。希望这对某人有所帮助..