我从对REST服务器的AJAX调用中接收到一个JSON对象。这个对象的属性名与我的TypeScript类相匹配(这是这个问题的后续)。

初始化它的最佳方法是什么?我不认为这将工作,因为类(& JSON对象)的成员是对象的列表和成员是类,而这些类的成员是列表和/或类。

但我更喜欢一种方法,查找成员名和分配他们,创建列表和实例化类的需要,所以我不必为每个类中的每个成员写显式代码(有很多!)


当前回答

我的方法略有不同。我没有将属性复制到新的实例中,我只是改变了现有pojo的原型(在旧的浏览器上可能不太好用)。每个类负责提供一个setprototype方法来设置任何子对象的原型,这些子对象反过来提供它们自己的setprototype方法。

(我也使用_Type属性来获取未知对象的类名,但在这里可以忽略)

class ParentClass
{
    public ID?: Guid;
    public Child?: ChildClass;
    public ListOfChildren?: ChildClass[];

    /**
     * Set the prototypes of all objects in the graph.
     * Used for recursive prototype assignment on a graph via ObjectUtils.SetPrototypeOf.
     * @param pojo Plain object received from API/JSON to be given the class prototype.
     */
    private static SetPrototypes(pojo: ParentClass): void
    {
        ObjectUtils.SetPrototypeOf(pojo.Child, ChildClass);
        ObjectUtils.SetPrototypeOfAll(pojo.ListOfChildren, ChildClass);
    }
}

class ChildClass
{
    public ID?: Guid;
    public GrandChild?: GrandChildClass;

    /**
     * Set the prototypes of all objects in the graph.
     * Used for recursive prototype assignment on a graph via ObjectUtils.SetPrototypeOf.
     * @param pojo Plain object received from API/JSON to be given the class prototype.
     */
    private static SetPrototypes(pojo: ChildClass): void
    {
        ObjectUtils.SetPrototypeOf(pojo.GrandChild, GrandChildClass);
    }
}

下面是ObjectUtils.ts:

/**
 * ClassType lets us specify arguments as class variables.
 * (where ClassType == window[ClassName])
 */
type ClassType = { new(...args: any[]): any; };

/**
 * The name of a class as opposed to the class itself.
 * (where ClassType == window[ClassName])
 */
type ClassName = string & {};

abstract class ObjectUtils
{
/**
 * Set the prototype of an object to the specified class.
 *
 * Does nothing if source or type are null.
 * Throws an exception if type is not a known class type.
 *
 * If type has the SetPrototypes method then that is called on the source
 * to perform recursive prototype assignment on an object graph.
 *
 * SetPrototypes is declared private on types because it should only be called
 * by this method. It does not (and must not) set the prototype of the object
 * itself - only the protoypes of child properties, otherwise it would cause a
 * loop. Thus a public method would be misleading and not useful on its own.
 * 
 * https://stackoverflow.com/questions/9959727/proto-vs-prototype-in-javascript
 */
public static SetPrototypeOf(source: any, type: ClassType | ClassName): any
{
    let classType = (typeof type === "string") ? window[type] : type;

    if (!source || !classType)
    {
        return source;
    }

    // Guard/contract utility
    ExGuard.IsValid(classType.prototype, "type", <any>type);

    if ((<any>Object).setPrototypeOf)
    {
        (<any>Object).setPrototypeOf(source, classType.prototype);
    }
    else if (source.__proto__)
    {
        source.__proto__ = classType.prototype.__proto__;
    }

    if (typeof classType["SetPrototypes"] === "function")
    {
        classType["SetPrototypes"](source);
    }

    return source;
}

/**
 * Set the prototype of a list of objects to the specified class.
 * 
 * Throws an exception if type is not a known class type.
 */
public static SetPrototypeOfAll(source: any[], type: ClassType): void
{
    if (!source)
    {
        return;
    }

    for (var i = 0; i < source.length; i++)
    {
        this.SetPrototypeOf(source[i], type);
    }
}
}

用法:

let pojo = SomePlainOldJavascriptObjectReceivedViaAjax;

let parentObject = ObjectUtils.SetPrototypeOf(pojo, ParentClass);

// parentObject is now a proper ParentClass instance

其他回答

JQuery .extend为你做这个:

var mytsobject = new mytsobject();

var newObj = {a:1,b:2};

$.extend(mytsobject, newObj); //mytsobject will now contain a & b

对于简单的对象,我喜欢这个方法:

class Person {
  constructor(
    public id: String, 
    public name: String, 
    public title: String) {};

  static deserialize(input:any): Person {
    return new Person(input.id, input.name, input.title);
  }
}

var person = Person.deserialize({id: 'P123', name: 'Bob', title: 'Mr'});

利用在构造函数中定义属性的能力可以使其简洁。

这将为您提供一个类型化对象(与所有使用object的答案相比)。赋值或一些变量,给你一个对象),不需要外部库或装饰器。

我的方法略有不同。我没有将属性复制到新的实例中,我只是改变了现有pojo的原型(在旧的浏览器上可能不太好用)。每个类负责提供一个setprototype方法来设置任何子对象的原型,这些子对象反过来提供它们自己的setprototype方法。

(我也使用_Type属性来获取未知对象的类名,但在这里可以忽略)

class ParentClass
{
    public ID?: Guid;
    public Child?: ChildClass;
    public ListOfChildren?: ChildClass[];

    /**
     * Set the prototypes of all objects in the graph.
     * Used for recursive prototype assignment on a graph via ObjectUtils.SetPrototypeOf.
     * @param pojo Plain object received from API/JSON to be given the class prototype.
     */
    private static SetPrototypes(pojo: ParentClass): void
    {
        ObjectUtils.SetPrototypeOf(pojo.Child, ChildClass);
        ObjectUtils.SetPrototypeOfAll(pojo.ListOfChildren, ChildClass);
    }
}

class ChildClass
{
    public ID?: Guid;
    public GrandChild?: GrandChildClass;

    /**
     * Set the prototypes of all objects in the graph.
     * Used for recursive prototype assignment on a graph via ObjectUtils.SetPrototypeOf.
     * @param pojo Plain object received from API/JSON to be given the class prototype.
     */
    private static SetPrototypes(pojo: ChildClass): void
    {
        ObjectUtils.SetPrototypeOf(pojo.GrandChild, GrandChildClass);
    }
}

下面是ObjectUtils.ts:

/**
 * ClassType lets us specify arguments as class variables.
 * (where ClassType == window[ClassName])
 */
type ClassType = { new(...args: any[]): any; };

/**
 * The name of a class as opposed to the class itself.
 * (where ClassType == window[ClassName])
 */
type ClassName = string & {};

abstract class ObjectUtils
{
/**
 * Set the prototype of an object to the specified class.
 *
 * Does nothing if source or type are null.
 * Throws an exception if type is not a known class type.
 *
 * If type has the SetPrototypes method then that is called on the source
 * to perform recursive prototype assignment on an object graph.
 *
 * SetPrototypes is declared private on types because it should only be called
 * by this method. It does not (and must not) set the prototype of the object
 * itself - only the protoypes of child properties, otherwise it would cause a
 * loop. Thus a public method would be misleading and not useful on its own.
 * 
 * https://stackoverflow.com/questions/9959727/proto-vs-prototype-in-javascript
 */
public static SetPrototypeOf(source: any, type: ClassType | ClassName): any
{
    let classType = (typeof type === "string") ? window[type] : type;

    if (!source || !classType)
    {
        return source;
    }

    // Guard/contract utility
    ExGuard.IsValid(classType.prototype, "type", <any>type);

    if ((<any>Object).setPrototypeOf)
    {
        (<any>Object).setPrototypeOf(source, classType.prototype);
    }
    else if (source.__proto__)
    {
        source.__proto__ = classType.prototype.__proto__;
    }

    if (typeof classType["SetPrototypes"] === "function")
    {
        classType["SetPrototypes"](source);
    }

    return source;
}

/**
 * Set the prototype of a list of objects to the specified class.
 * 
 * Throws an exception if type is not a known class type.
 */
public static SetPrototypeOfAll(source: any[], type: ClassType): void
{
    if (!source)
    {
        return;
    }

    for (var i = 0; i < source.length; i++)
    {
        this.SetPrototypeOf(source[i], type);
    }
}
}

用法:

let pojo = SomePlainOldJavascriptObjectReceivedViaAjax;

let parentObject = ObjectUtils.SetPrototypeOf(pojo, ParentClass);

// parentObject is now a proper ParentClass instance

这是一些快速的镜头,展示了几种不同的方法。他们绝不是“完整的”,作为一个免责声明,我不认为这样做是一个好主意。此外,代码不是太干净,因为我只是键入它在一起相当快。

另外需要注意的是:当然,可反序列化的类需要有默认构造函数,就像我所知道的所有其他语言中的反序列化一样。当然,如果你调用一个不带参数的非默认构造函数,Javascript不会抱怨,但是类最好为此做好准备(另外,它不是真正的“typescript方式”)。

选项#1:根本没有运行时信息

这种方法的问题主要在于任何成员的名称必须与其类匹配。它自动地限制你每个类只能有一个相同类型的成员,并打破了一些良好实践的规则。我强烈反对这样做,但这里只列出它,因为这是我写这个答案时的第一份“草稿”(这也是为什么名字是“Foo”等)。

module Environment {
    export class Sub {
        id: number;
    }

    export class Foo {
        baz: number;
        Sub: Sub;
    }
}

function deserialize(json, environment, clazz) {
    var instance = new clazz();
    for(var prop in json) {
        if(!json.hasOwnProperty(prop)) {
            continue;
        }

        if(typeof json[prop] === 'object') {
            instance[prop] = deserialize(json[prop], environment, environment[prop]);
        } else {
            instance[prop] = json[prop];
        }
    }

    return instance;
}

var json = {
    baz: 42,
    Sub: {
        id: 1337
    }
};

var instance = deserialize(json, Environment, Environment.Foo);
console.log(instance);

选项#2:name属性

为了解决选项1中的问题,我们需要了解JSON对象中节点的类型。问题是在Typescript中,这些东西是编译时构造,我们在运行时需要它们——但运行时对象在设置它们之前根本不知道它们的属性。

一种方法是让类知道它们的名称。不过,JSON中也需要这个属性。实际上,你只需要在json中使用它:

module Environment {
    export class Member {
        private __name__ = "Member";
        id: number;
    }

    export class ExampleClass {
        private __name__ = "ExampleClass";

        mainId: number;
        firstMember: Member;
        secondMember: Member;
    }
}

function deserialize(json, environment) {
    var instance = new environment[json.__name__]();
    for(var prop in json) {
        if(!json.hasOwnProperty(prop)) {
            continue;
        }

        if(typeof json[prop] === 'object') {
            instance[prop] = deserialize(json[prop], environment);
        } else {
            instance[prop] = json[prop];
        }
    }

    return instance;
}

var json = {
    __name__: "ExampleClass",
    mainId: 42,
    firstMember: {
        __name__: "Member",
        id: 1337
    },
    secondMember: {
        __name__: "Member",
        id: -1
    }
};

var instance = deserialize(json, Environment);
console.log(instance);

选项#3:显式声明成员类型

如上所述,类成员的类型信息在运行时不可用——除非我们使它可用。我们只需要对非原始成员这样做,就可以了:

interface Deserializable {
    getTypes(): Object;
}

class Member implements Deserializable {
    id: number;

    getTypes() {
        // since the only member, id, is primitive, we don't need to
        // return anything here
        return {};
    }
}

class ExampleClass implements Deserializable {
    mainId: number;
    firstMember: Member;
    secondMember: Member;

    getTypes() {
        return {
            // this is the duplication so that we have
            // run-time type information :/
            firstMember: Member,
            secondMember: Member
        };
    }
}

function deserialize(json, clazz) {
    var instance = new clazz(),
        types = instance.getTypes();

    for(var prop in json) {
        if(!json.hasOwnProperty(prop)) {
            continue;
        }

        if(typeof json[prop] === 'object') {
            instance[prop] = deserialize(json[prop], types[prop]);
        } else {
            instance[prop] = json[prop];
        }
    }

    return instance;
}

var json = {
    mainId: 42,
    firstMember: {
        id: 1337
    },
    secondMember: {
        id: -1
    }
};

var instance = deserialize(json, ExampleClass);
console.log(instance);

选项4:冗长但简洁的方式

2016年3月1日更新:正如@GameAlchemist在评论(想法,实现)中指出的那样,在Typescript 1.7中,下面描述的解决方案可以使用类/属性装饰器以更好的方式编写。

串行化总是一个问题,在我看来,最好的方法不是最短的方法。在所有选项中,这是我更喜欢的选项,因为类的作者可以完全控制反序列化对象的状态。如果要我猜的话,我会说所有其他的选择,迟早都会给你带来麻烦(除非Javascript有一个原生的方法来处理这个问题)。

实际上,下面的例子并没有充分体现灵活性。它实际上只是复制了类的结构。不过,在这里您必须记住的区别是,类可以完全控制使用任何类型的JSON来控制整个类的状态(您可以计算东西等)。

interface Serializable<T> {
    deserialize(input: Object): T;
}

class Member implements Serializable<Member> {
    id: number;

    deserialize(input) {
        this.id = input.id;
        return this;
    }
}

class ExampleClass implements Serializable<ExampleClass> {
    mainId: number;
    firstMember: Member;
    secondMember: Member;

    deserialize(input) {
        this.mainId = input.mainId;

        this.firstMember = new Member().deserialize(input.firstMember);
        this.secondMember = new Member().deserialize(input.secondMember);

        return this;
    }
}

var json = {
    mainId: 42,
    firstMember: {
        id: 1337
    },
    secondMember: {
        id: -1
    }
};

var instance = new ExampleClass().deserialize(json);
console.log(instance);

另一种选择是使用工厂

export class A {

    id: number;

    date: Date;

    bId: number;
    readonly b: B;
}

export class B {

    id: number;
}

export class AFactory {

    constructor(
        private readonly createB: BFactory
    ) { }

    create(data: any): A {

        const createB = this.createB.create;

        return Object.assign(new A(),
            data,
            {
                get b(): B {

                    return createB({ id: data.bId });
                },
                date: new Date(data.date)
            });
    }
}

export class BFactory {

    create(data: any): B {

        return Object.assign(new B(), data);
    }
}

https://github.com/MrAntix/ts-deserialize

像这样使用

import { A, B, AFactory, BFactory } from "./deserialize";

// create a factory, simplified by DI
const aFactory = new AFactory(new BFactory());

// get an anon js object like you'd get from the http call
const data = { bId: 1, date: '2017-1-1' };

// create a real model from the anon js object
const a = aFactory.create(data);

// confirm instances e.g. dates are Dates 
console.log('a.date is instanceof Date', a.date instanceof Date);
console.log('a.b is instanceof B', a.b instanceof B);

保持类简单 工厂可灵活注射