在TypeScript中,有一种语法可以将字段声明为lazily-initialized吗?
就像在Scala中一样,例如:
Array.prototype.indexOf()
意味着字段初始值设定项仅在首次访问字段时才会运行。
答案 0 :(得分:12)
我会使用一个getter:
class Lazy {
private _f1;
get f1() {
return this._f1 || this._f1 = expensiveInitializationForF1();
}
}
是的,您可以使用装饰器来解决这个问题,但对于简单的情况,这可能会有点过分。
答案 1 :(得分:6)
我发现它不能在你自己的打字稿中使用@lazyInitialize
。所以你必须重写那个。我的装饰者,你只需要复制并使用它。在吸气剂上使用@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;