将字段声明为懒惰

时间:2017-03-16 21:59:26

标签: typescript lazy-initialization

在TypeScript中,有一种语法可以将字段声明为lazily-initialized吗?

就像在Scala中一样,例如:

Array.prototype.indexOf()

意味着字段初始值设定项仅在首次访问字段时才会运行。

4 个答案:

答案 0 :(得分:12)

我会使用一个getter:

class Lazy {
  private _f1;

  get f1() {
    return this._f1 || this._f1 = expensiveInitializationForF1();
  }

}

是的,您可以使用装饰器来解决这个问题,但对于简单的情况,这可能会有点过分。

答案 1 :(得分:6)

我发现它不能在你自己的打字稿中使用@lazyInitialize。所以你必须重写那个。我的装饰者,你只需要复制并使用它。在吸气剂上使用@lazy而不是属性。

@Lazy

const {defineProperty, getPrototypeOf}=Object;
export default function lazy(target, name, {get:initializer, enumerable, configurable, set:setter}: PropertyDescriptor={}): any {
    const {constructor}=target;
    if (initializer === undefined) {
        throw `@lazy can't be set as a property \`${name}\` on ${constructor.name} class, using a getter instead!`;
    }
    if (setter) {
        throw `@lazy can't be annotated with get ${name}() existing a setter on ${constructor.name} class!`;
    }

    function set(that, value) {
        if (value === undefined) {
            value = that;
            that = this;
        }
        defineProperty(that, name, {
            enumerable: enumerable,
            configurable: configurable,
            value: value
        });
        return value;
    }

    return {
        get(){
            if (this === target) {
                return initializer;
            }
            //note:subclass.prototype.foo when foo exists in superclass nor subclass,this will be called
            if (this.constructor !== constructor && getPrototypeOf(this).constructor === constructor) {
                return initializer;
            }
            return set(this, initializer.call(this));
        },
        set
    };
}

测试

describe("@lazy", () => {
    class Foo {
        @lazy get value() {
            return new String("bar");
        }

        @lazy
        get fail(): string {
            throw new Error("never be initialized!");
        }

        @lazy get ref() {
            return this;
        }
    }


    it("initializing once", () => {
        let foo = new Foo();

        expect(foo.value).toEqual("bar");
        expect(foo.value).toBe(foo.value);
    });

    it("could be set @lazy fields", () => {
        //you must to set object to any
        //because typescript will infer it by static ways
        let foo: any = new Foo();
        foo.value = "foo";

        expect(foo.value).toEqual("foo");
    });

    it("can't annotated with fields", () => {
        const lazyOnProperty = () => {
            class Bar {
                @lazy bar: string = "bar";
            }
        };

        expect(lazyOnProperty).toThrowError(/@lazy can't be set as a property `bar` on Bar class/);
    });

    it("get initializer via prototype", () => {
        expect(typeof Foo.prototype.value).toBe("function");
    });

    it("calling initializer will be create an instance at a time", () => {
        let initializer: any = Foo.prototype.value;

        expect(initializer.call(this)).toEqual("bar");
        expect(initializer.call(this)).not.toBe(initializer.call(this));
    });

    it("ref this correctly", () => {
        let foo = new Foo();
        let ref: any = Foo.prototype.ref;

        expect(this).not.toBe(foo);
        expect(foo.ref).toBe(foo);
        expect(ref.call(this)).toBe(this);
    });

    it("discard the initializer if set fields with other value", () => {
        let foo: any = new Foo();
        foo.fail = "failed";

        expect(foo.fail).toBe("failed");
    });

    it("inherit @lazy field correctly", () => {
        class Bar extends Foo {
        }

        const assertInitializerTo = it => {
            let initializer: any = Bar.prototype.ref;
            let initializer2: any = Foo.prototype.ref;
            expect(typeof initializer).toBe("function");
            expect(initializer.call(it)).toBe(it);
            expect(initializer2.call(it)).toBe(it);
        };

        assertInitializerTo(this);
        let bar = new Bar();
        assertInitializerTo({});
        expect(bar.value).toEqual("bar");
        expect(bar.value).toBe(bar.value);
        expect(bar.ref).toBe(bar);
        assertInitializerTo(this);
    });


    it("overriding @lazy field to discard super.initializer", () => {
        class Bar extends Foo {
            get fail() {
                return "error";
            };
        }

        let bar = new Bar();

        expect(bar.fail).toBe("error");
    });

    it("calling super @lazy fields", () => {
        let calls = 0;
        class Bar extends Foo {
            get ref(): any {
                calls++;
                //todo:a typescript bug:should be call `super.ref` getter  instead of super.ref() correctly in typescript,but it can't
                return (<any>super["ref"]).call(this);
            };
        }

        let bar = new Bar();

        expect(bar.ref).toBe(bar);
        expect(calls).toBe(1);
    });

    it("throws errors if @lazy a property with setter", () => {
        const lazyPropertyWithinSetter = () => {
            class Bar{
                @lazy
                get bar(){return "bar";}
                set bar(value){}
            }
        };


        expect(lazyPropertyWithinSetter).toThrow(/@lazy can't be annotated with get bar\(\) existing a setter on Bar class/);

    });
});

答案 2 :(得分:1)

现代版本,类别:

class Lazy<T> {
  private #f: T | undefined;
  constructor(#init: () => T) {}
  public get f(): T {
    return this.#f1 ??= this.#init();
  }
}

现代版本,内联:

let value;
// …
use_by_ref(value ??= lazy_init());

带有代理的可能的将来版本当前不起作用,因为从构造函数返回不同的值通常被视为未定义的行为。

class Lazy<T> {
  constructor(private init: { [K in keyof T]: () => T[K] }) {
    let obj = Object.fromEntries(Object.keys(init).map(k => [k, undefined])) as unknown as { [K in keyof T]: undefined | T[K] };
    Object.seal(obj);
    return new Proxy(obj, this);
  }
  get<K extends keyof T>(t: T, k: K): T[K] {
    return t[k] ??= this.init[k];
  }
}

可以简化更多内容。

答案 3 :(得分:1)

我正在使用这样的东西:

export interface ILazyInitializer<T> {(): T}

export class Lazy<T> {
private instance: T | null = null;
private initializer: ILazyInitializer<T>;

 constructor(initializer: ILazyInitializer<T>) {
     this.initializer = initializer;
 }

 public get value(): T {
     if (this.instance == null) {
         this.instance = this.initializer();
     }

     return this.instance;
 }
}



let myObject: Lazy<MyObject>;
myObject = new Lazy(() => <MyObject>new MyObject("value1", "value2"));
const someString = myObject.value.getProp;