ES6单例模式 vs 仅实例化一个类

98

我看到一些使用ES6类实现单例模式的代码,我想知道为什么要这样做,而不是在文件底部实例化该类并导出实例。这样做有什么负面影响吗?例如:

ES6 导出实例:

import Constants from '../constants';

class _API {
  constructor() {
    this.url = Constants.API_URL;
  }

  getCities() {
    return fetch(this.url, { method: 'get' })
      .then(response => response.json());
  }
}

const API = new _API();
export default API;

用法:

import API from './services/api-service'

使用以下单例模式有什么不同?使用其中一个的原因是什么?我更想知道我给出的第一个示例是否存在我不知道的问题。

单例模式:

import Constants from '../constants';

let instance = null;

class API {
  constructor() {

    if(!instance){
      instance = this;
    }

    this.url = Constants.API_URL;

    return instance;
  }

  getCities() {
    return fetch(this.url, { method: 'get' })
      .then(response => response.json());
  }
}

export default API;

使用方法:

import API from './services/api-service';

let api = new API()
5个回答

78

我不会推荐任何一个。这太过复杂了。如果你只需要一个对象,不要使用 class 语法!直接使用

import Constants from '../constants';

export default {
  url: Constants.API_URL,
  getCities() {
    return fetch(this.url, { method: 'get' }).then(response => response.json());
  }
};

:这是一个空段落标签。
import API from './services/api-service'

或者更简单的方式

import Constants from '../constants';

export const url = Constants.API_URL;
export function getCities() {
  return fetch(url, { method: 'get' }).then(response => response.json());
}

:这是一个空段落标签。
import * as API from './services/api-service'

3
这是在JavaScript中正确且惯用的做法。 - slebetman
27
请注意,JavaScript语言一直内置了单例模式,只是我们没有称其为单例模式,而是称其为对象字面量。因此,每当需要一个对象的单个实例时,JavaScript程序员会自动创建一个对象字面量。在JavaScript中,许多在其他语言中被称为“设计模式”的东西都是内置语法。 - slebetman
1
@CésarAlberca OP 没有使用依赖注入,所以我没有考虑这个。而且你也不需要一个 class,一个模块导入或工厂函数就足以使 fetch 可以被模拟。 - Bergi
3
@codewise 在回答中提到的链接解释了为什么在创建单例对象时应避免使用 class 语法。 - Bergi
1
@java-addict301,依赖注入不需要构造函数。工厂或部分应用同样有效。此外,问题是关于单例模式的,没有在构造函数中使用任何DI。在这种情况下,只需使用一个模块 - 现代单元测试框架甚至允许注入模块依赖项。 - Bergi
显示剩余5条评论

49

区别在于你是否想要测试东西。

假设你有一个名为api.spec.js的测试文件,并且你的API模块有一个依赖项,比如那些常量。

具体来说,两个版本中的构造函数都需要一个参数,即你的Constants导入。

因此,你的构造函数看起来像这样:

class API {
    constructor(constants) {
      this.API_URL = constants.API_URL;
    }
    ...
}



// single-instance method first
import API from './api';
describe('Single Instance', () => {
    it('should take Constants as parameter', () => {
        const mockConstants = {
            API_URL: "fake_url"
        }
        const api = new API(mockConstants); // all good, you provided mock here.
    });
});

现在,通过导出实例,就没有模拟了。

import API from './api';
describe('Singleton', () => {
    it('should let us mock the constants somehow', () => {
        const mockConstants = {
            API_URL: "fake_url"
        }
        // erm... now what?
    });
});

将实例化的对象导出后,您不能轻易和明智地更改其行为。


7
JavaScript 开发人员倾向于通过导入硬编码所有的依赖,原因不明。我认为更好的做法是通过构造函数传递依赖项,这样它可以 a) 进行测试,b) 可重用。 - Josh Stuart
2
我选择了这个答案,因为它确实回答了我的初始问题。谢谢。 - Aaron
// 嗯...现在怎么办? 为什么不呢? API.url = MockConstants.API_URL;对象具有所有这些实例属性/方法,无论类使用“this”访问。但当然,在单元测试中进行变异会引起其他问题。 - Shishir Arora
@ShishirArora 问题出在这里。你有一个测试,其中断言 API.url === 'example.com'。一切都好。然后有人在你的测试之前插入了 API.url === 'something else' - 你修改了整个测试套件的 API 对象,而不仅仅是一个单独的测试实例。现在你破坏了其他测试 - 即使你没有(潜在地)破坏代码本身。 - Zlatko
另外,如果您想在不仅仅是测试中实现此行为怎么办?比如,我想启动X个数据库驱动程序实例,并且每个实例连接到自己的数据库服务器?问题相同,但现在它不再是测试了。 - Zlatko
显示剩余2条评论

13

两种方式不同。 像下面这样导出一个类:

const APIobj = new _API();
export default APIobj;   //shortcut=> export new _API()

然后在多个文件中像下面这样导入将指向同一实例,并且这是创建单例模式的一种方式。

import APIobj from './services/api-service'

而直接导出类的另一种方式不是单例的,因为在我们导入的文件中需要使用 new 来创建类的实例,这将为每个新创建的实例创建一个单独的实例。

export default API;

引入类并实例化

import API from './services/api-service';
let api = new API()

2

使用单例模式的另一个原因是在某些框架(如Polymer 1.0)中,您无法使用export语法。
这就是为什么第二个选项(单例模式)对我更有用的原因。

希望这可以帮助您。


1
也许我有点迟到了,因为这个问题是在2018年写的,但是当搜索js单例类时,它仍然出现在结果页面的顶部,我认为即使其他方法可行,它仍然没有得到正确的答案。但不要创建一个类实例。这是我的创建JS单例类的方法:
class TestClass {
    static getInstance(dev = true) {
        if (!TestClass.instance) {
            console.log('Creating new instance');
            Object.defineProperty(TestClass, 'instance', {
                value: new TestClass(dev),
                writable : false,
                enumerable : true,
                configurable : false
            });
        } else {
            console.log('Instance already exist');
        }
        return TestClass.instance;
    }
    random;
    constructor() {
        this.random = Math.floor(Math.random() * 99999);
    }
}
const instance1 = TestClass.getInstance();
console.log(`The value of random var of instance1 is: ${instance1.random}`);
const instance2 = TestClass.getInstance();
console.log(`The value of random var of instance2 is: ${instance2.random}`);

这是执行此代码的结果。

Creating new instance
The value of random var of instance1 is: 14929
Instance already exist
The value of random var of instance2 is: 14929

希望这能对某人有所帮助


2
OP中的单例模式比这个更好。你的代码存在以下问题:a)构造函数仍然是公共的,任何人都可以调用它;b).instance是公共的,任何人都可以修改它(包括将其设置为null);c)getInstance(false)有时会返回一个开发实例,有时会返回一个生产实例,完全独立于传递的参数。这是一个非常典型的教育案例,说明为什么根本不应该使用单例模式。 - Bergi
是的,这是一个好的观点,但你可以简单地使用definePropriety解决这个问题,并使其(静态实例变量)不可变,这是一个例子: // static instance; //commented static getInstance(dev = true) { if (!TestClass.instance) { Object.defineProperty(TestClass, 'instance', { value: new TestClass(dev), writable : false, enumerable : true, configurable : false }); 完成后,您将无法再更改实例。 我会更新代码。 - khalid
但我不明白你所说的“c)getInstance(false)有时会返回开发实例,有时会返回生产实例”的意思。 - khalid
1
我的意思是,当调用 getInstance(false) 时,人们会期望它始终返回一个生产实例,不是吗?但事实并非如此,根据代码的其余部分,它也可能返回一个开发实例 - 真是毛骨悚然的“远程操作”。单例获取器不应该有参数。 - Bergi
你是对的,带参数的单例模式并不是真正的单例模式,但我这样做是为了教育目的,以展示即使尝试创建新实例,对象参数也不会改变,但我会更新代码。 - khalid

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接