简体   繁体   English

为Node CommonJS模块创建不可变的类

[英]Creating immutable class for Node CommonJS module

I'm creating a basic class in a Node project, and I want to test it using Jest. 我正在Node项目中创建一个基本类,我想使用Jest对其进行测试。 I'm getting an error that is implying the use of 'strict' mode in the test, which I want to avoid/fix 我收到一个错误,暗示要在测试中使用“严格”模式,我想避免/修复该错误


~/lib/LogRecord.js 〜/ lib / LogRecord.js

let LogRecord = require('../lib/logRecord');

describe('Test LogRecord functionality', () => {
    test('LogRecord constructor', () => {
        let timestamp = Date.now();
        let logRecord = new LogRecord('INFO', 'Test Message', timestamp);
        expect(logRecord.level).toBe('INFO');
        expect(logRecord.message).toBe('Test Message');
        expect(logRecord.timestamp).toBe(timestamp);
    });
    test('LogRecord is read-only', () => {
        let timestamp = Date.now();
        let logRecord = new LogRecord('INFO', 'Test Message', timestamp);

        logRecord.level = 'WARN'
        logRecord.message = 'New Message'
        logRecord.timestamp = Date.now();

        expect(logRecord.level).toBe('INFO');
        expect(logRecord.message).toBe('Test Message');
        expect(logRecord.timestamp).toBe(timestamp);
    });
});

I'm testing it with this: 我正在对此进行测试:

Test LogRecord functionality › LogRecord constructor

TypeError: Cannot set property level of #<LogRecord> which has only a getter

  1 | module.exports = class LogRecord {
  2 |     constructor(level, message, timestamp) {
> 3 |         this.level = level;
    |         ^
  4 |         this.message = message;
  5 |         this.timestamp = timestamp ? timestamp : Date.now();
  6 |     }

  at new LogRecord (lib/logRecord.js:3:9)
  at Object.test (test/logRecord.test.js:6:25)

When I run npm test I get the following error on both of the LogRecord tests: 当我运行npm test ,两个LogRecord测试都出现以下错误:

const data = new WeakMap();

let levelKey = {id:'level'};
let messageKey = {id:'message'};
let timestampKey = {id:'timestamp'};

module.exports = class LogRecord {
    constructor(level, message, timestamp) {
        data.set(levelKey, level);
        data.set(messageKey, message);
        data.set(timestampKey, timestamp ? timestamp : Date.now());
    }

    get level () {
        return data.get(levelKey)
    }

    get message () {
        return data.get(messageKey)
    }

    get timestamp () {
        return data.get(timestampKey)
    }
}

Edit - Working class 编辑-工人阶级

 const data = new WeakMap(); let levelKey = {id:'level'}; let messageKey = {id:'message'}; let timestampKey = {id:'timestamp'}; module.exports = class LogRecord { constructor(level, message, timestamp) { data.set(levelKey, level); data.set(messageKey, message); data.set(timestampKey, timestamp ? timestamp : Date.now()); } get level () { return data.get(levelKey) } get message () { return data.get(messageKey) } get timestamp () { return data.get(timestampKey) } } 

Testing is about making sure that your code does what you think it does. 测试是要确保您的代码能够实现您认为的功能。 Consider the following class: 考虑以下类别:

class Foo {
  constructor (bar) {
    this._bar = bar;
  }

  get bar () {
    return this._bar;
  }
}

Here bar is read-only , there is no way to set the bar property: 这里的bar只读的 ,无法设置bar属性:

let foo = new Foo('a foo');
foo.bar; // 'a foo'
foo.bar = 'am not'; // TypeError!

The modules question isn't really relevant: as Logar linked in the comments class bodies are always strict mode irregardless. 模块问题并不是真正相关的:因为注释类主体中链接的Logar 始终是严格模式,无论如何。

So if you want a property to be read only, you don't need to worry about writing it. 因此,如果您希望属性是只读的,则不必担心编写它。 Workflow might look something like this: 工作流程可能如下所示:

  1. Write empty class Foo class Foo {} and construct an instance foo = new Foo() 编写空类Foo class Foo {}并构造一个实例foo = new Foo()
  2. Write test that checks for bar which fails because we have an empty class 编写测试以检查因为我们有一个空的类而导致bar失败的测试
  3. Add constructor parameter and getter 添加构造函数参数和getter
  4. Check that test now passes 检查测试现在通过了
  5. Add test to ensure that trying to set bar throws expected error* 添加测试以确保尝试设置栏会引发预期错误*

If you don't want read-only properties you can just add a setter: 如果您不希望使用只读属性,则可以添加一个setter:

class Foo {
  constructor (bar) {
    this._bar = bar;
  }

  get bar () {
    return this._bar;
  }

  set bar (value) {
    this._bar = value;
  }
}

In which case you'd add a test that sets bar and the reads the altered value back out. 在这种情况下,您将添加一个测试,该测试会设置bar并读取更改后的值。

* You might be wondering why this test is here when this behavior is guaranteed by the spec and I would argue that the test is necessary since someone could (transparently to the callers) refactor the class to be an old-school constructor and create a vector for bugs: *您可能想知道为什么规范会保证这种行为时为什么要进行此测试,并且我认为该测试是必需的,因为有人(对于调用者而言)可以将该类重构为老式构造函数并创建一个向量错误:

// post refactor Foo
const Foo = function Foo(bar) {
  this.bar = bar; // danger! now writable!
};

Hopefully this sort of thing would be caught by a knowledgable reviewer, but I'd write the test anyways. 希望有经验的审阅者会发现这种情况,但是我还是会编写测试。

Update 更新资料

If what you want is a guaranteed read-only property that you set in the constructor, here is a recipe for such: 如果您想要的是在构造函数中设置的有保证的只读属性,则可以使用以下方法:

const data = new WeakMap();

module.exports = class Foo () {
  constructor (bar) {
    data.set(this, bar);
  }

  get bar () {
    return data.get(this);
  }
};

Because data is not exported no outside code can change it. 因为没有导出data ,所以没有外部代码可以更改它。 Attempting to set the bar property of an instance will throw. 尝试设置实例的bar属性将引发异常。 This is a bit more complicated that just defining an underscore property with getters and setters, but if it's what you want, well... I know this pattern because I've used it. 只是使用getter和setter定义下划线属性,这有点复杂,但是如果您要使用它,那么……我知道这种模式,因为我已经使用过它。

Update 2 更新2

You only create one weakmap per module , not per class or instance. 您只能为每个模块 (而不是每个类或实例)创建一个weakmap。 The weakmap stores a unique data entry keyed to individual instances (ie this ): 弱映射存储一个唯一的数据条目,该条目键入到各个实例(即this ):

const data = new WeakMap();

module.exports = {
  Foo: class Foo () {
    constructor (bar) {
      data.set(this, bar);
    }

    get bar () {
      return data.get(this);
    }
  },

  Bar: class Bar () {
    constructor (prop1, prop2) {

      // for multiple props we'll store an object...
      data.set(this, { prop2, prop1 });
    }

    get prop1 () {
      // ...and retrieve it and access it's props to return
      return data.get(this).prop1;
    }

    get prop2 () {
      return data.get(this).prop2;
    }
  }
};

Note that setting the props with a getter but no setter will still throw... 请注意,用吸气剂设置道具,但不会有设置器会抛出...

// in someotherfile.js
const { Foo } = require('path/to/file/with/foo.js');
const foo = new Foo('imma foo');
foo.bar; // 'imma foo'
foo.bar = 'no not really'; // TypeError!

// you can still set random properties that don't have a getter:
foo.baz = 'I do not throw';
foo.baz; // 'I do not throw'

If you want your properties to be read only after object initialization, you can use Object.freeze in the constructor, and remove your getters : 如果希望仅在对象初始化之后读取属性,则可以在构造函数中使用Object.freeze ,并删除您的getter:

class LogRecord {
    constructor(level, message, timestamp) {
        this.level = level;
        this.message = message;
        this.timestamp = timestamp ? timestamp : Date.now();
        Object.freeze(this);
    }
}

But this will freeze all of your object's properties. 但这将冻结对象的所有属性。 You won't be able to modify, remove, or add any after that. 之后,您将无法修改,删除或添加任何内容。 Didn't dive too deep into this so it may have some flaws as well 没有深入研究这一点,因此它也可能存在一些缺陷

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM