【问题标题】:Typescript: decorators behave differently on angular project and typescript playgroundTypescript:装饰器在 Angular 项目和 typescript 操场上的行为不同
【发布时间】:2016-10-09 18:36:38
【问题描述】:

当我发现 Typescript 的私有根本不是私有的,并且 get set 属性没有通过 JSON.stringify 输出时,我需要在 angular 2.0.0-rc1 中将一个对象序列化为 json。

于是我开始装饰班级:

//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"}

但在我的项目(角度 2.0.0-rc1)中,这给出了

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

我真正想要的是游乐场的输出#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;
}());

操场上没有__metadata 行。

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

【问题讨论】:

    标签: angularjs json angular-decorator reflect-metadata


    【解决方案1】:

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

    请注意,在 Playground 中,反射元数据不可用。属性装饰器可以返回要分配(或)到描述符的对象以更改其行为。在 Angular 环境中,使用 Reflect-metadata(特别是 Reflect.decorate())来装饰事物。

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

    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))));
    

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

    【讨论】:

    • @KaranPatel 请注意我后面的回答,此装饰器不适用于嵌套对象。
    • 谢谢埃文..我提到了这个当前的答案而不是低于答案。但你可以做的一件事是,如果它不能正常工作,你可以删除下面的答案。
    【解决方案2】:

    我掉进了兔子洞……

    所以由于某种原因,将白名单添加到 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 中隐藏私有变量并给它一个只读外观属性:

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

    //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,请记住在每个实例/new 中都会调用它。

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

    【讨论】:

      猜你喜欢
      • 2016-03-27
      • 1970-01-01
      • 2015-08-19
      • 1970-01-01
      • 2019-07-29
      • 2017-11-15
      • 2018-06-08
      • 2018-11-19
      • 2022-01-17
      相关资源
      最近更新 更多