Синглтон на javascript

Вся красота и гибкость джаваскрипта раскрывается как раз на подобных задачах: реализация функционала, который не заложен в языковые конструкции, но широко применяется в других языках программирования.

Синглтон по определению — объект, который может присутствовать в системе только в единственном числе.

Самое простое, что можно сделать — создать объект, и назвать его условно синглтоном. То есть непрограммными средствами решаем задачу.

window.singletone = {
	// singletone body
};

Такой подход порождает больше вопросов и проблем, чем решает: нет никакой инкапсуляции данных, ссылку на объект необходимо явно прокидывать во все области видимости, где подразумевается его использование. Да и сама форма записи, в случае большого количества методов, сложна для чтения.

Вопрос с инкапусяцией можно решить с помощью замыкания:

var singletone = (function() {
	var private;

	function private_method () {}
	
	return {
		public : function () {
			return private_method(private);
		}
	}
}());

Уже немного лучше. Данные инкапсулированы, но строить хоть какую систему наследования с подобным подходом сложно. Для упрощения процесса наследования, будем работать с функцией – конструктором. Эта функция должна возвращать ссылку на один и тот же объект. Самое простое, что можно сделать во вновь сложившейся ситуации, это сконструировать объект и всегда возвращать ссылку на него

var Singletone;

Singletone = (function () {
	var instance;

	instance = {};
	return function () {
		return instance;
	}
}());

Этот код больше похож на правду: во-первых мы инкапсулировали данные, во-вторых программно ограничили количество инстансов синглтона до одного. Возможность использования конструктора как с new так и без. Код вполне рабочий, и подходящий для использования в продакшене. Но в нем еще достаточно минусов: объект конструируется в любом случае, даже если конструктор не был ни разу вызван, невозможность использования цепочки прототипов, некорректный конструктор объекта, возвращаемого Singletone.

var singletone,
	another_singletone;

singletone = new Singletone();
another_singletone = Singletone();

console.log(singletone === another_singletone); // true
console.log(another_singletone.constructor === Singletone); // false
console.log(another_singletone.__proto__ === Singletone.prototype); // false

Конечно мы можем вручную установить свойство constructor, __proto__. Правда такой код потеряет в кроссбраузерности и изящности (все равно продолжит работать, что намного важнее красоты). Я же в своих изысканиях ищу максимально изящный вариант.

Судя по предъявляемым требованиям, конструктор синглтона должен создавать и запоминать объект при первом вызове, и возвращать этот объект при последующих. Вспомним, что объект функции это объект, и будем хранить в нем инстанс сконструированного объекта.

function Singletone () {
	if (Singletone.instance) {
		return Singletone.instance
	}
	Singletone.instance = this;
}

Вот она, красота, javascript. Если конструктор явно не возвращает объект, то возвращается конструируемый объект. При первом вызове new Singletone в свойство Singletone.instance запишется конструируемый объект, а при последующих будет возвращаться ссылка на него.

Этот код хорош по нескольким причинам: объект не создается, если он не был сконструирован (в отличии от предыдущих вариантов), объект можно расширять с помощью Singletone.prototype, так-же проверять instanceOf Singletone. С другой стороны, эту функцию необходимо всегда вызывать с ключевым словом new. Не то, чтобы я был противником использования new или сокрытия этого механизма от программиста, использующего мой синглтон, но создание возможности вызова конструктора как с new, так и без, позволит избежать ряда вопросов про использование синглтона. Так же в этом варианте функция выступает в качестве хранилища инстанса объекта. Такой подход имеет право на жизнь, но он слишком прямолинеен и показывает детали реализации наружу, делая код чуть уязвимее.

Возможно я бы не придирался так к коду (кстати, в вики пример реализации синглтона именно в виде, представленном выше), если не бы не знал лучшей реализации.

Мы же пойдем дальше, и создадим реализацию, которая:

  • инкапсулирует данные
  • не конструирует объект, если не конструктор не был вызван
  • позволяет использовать все инструменты прототипного наследования (constructor, Singletone.prototype, instanceOf)
  • может вызываться как с new, так и без
var Singletone = (function () {
	var instance;

	return function Construct_singletone () {
		if (instance) {
			return instance;
		}
		if (this && this.constructor === Construct_singletone) {
			instance = this;
		} else {
			return new Construct_singletone();
		}
	}
}());

Разберем что происходит в возвращаемой функции.
Если есть instance, то возвращаем его. Если функция используется с new или без, все равно мы вернем ссылку на объект, сконструированный этой функцией.

Если функция вызвана с new, то this ссылается на объект, конструируемый этой функцией. This так же может быть определен, если функция вызвана в нестрогом режиме (this === window), или если функция вызвана как метод объекта (this === этот_объект). Второй случай аннигилируется проверкой конструктора у this.

Если this не определен (вызов в строгом), или конструктор у this не текущая функция, возвращаем результат текущей функции с new.

Имеем на выходе пуленепробиваемый конструктор синглтона, который можно где угодно хранить и как угодно вызывать. Такую красоту нельзя не прихватить в тулбокс.

Share
Send
8 comments
Илья

Было приятно увидеть знакомого автора в в топе выдачи, спасибо за статью!
п.с. и спасибо за MMA/2014 :)

Дмитрий Подгорный

Рад быть полезным.

Павел

Благодарю, статья мне помогла разобраться с этим синглтоном:)

Dmitry Efimenko

Что вы скажите о таком решении?
https://code.google.com/p/jslibs/wiki/JavascriptTips#Singleton_pattern

Дмитрий Подгорный

Главный минус на мой взгляд в том, что решение не будет работать в строгом режиме (речь идет именно о строгом режиме в том файле, где объявляется MySingletonClass)
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/arguments/callee

В остальном подход мало чем отличается от описанного в статье подхода, когда в свойстве конструктора сохраняется ссылка на инстанс синглтона.

Сергей

function S () {
if (!S.self) {
S.self = this;
}

return self;
}

$a = new S();
$b = new S();

console.log($a == $b) // true

Дмитрий Подгорный

Среди примеров в статье этот тоже рассмотрен

undefinedIsAFuntion

Здравствуйте, Дмитрий. Уже несколько дней пытаюсь понять один момент из кода.
В примерах вроде
var singletone = (function() {
var private;

function private_method () {}

return {
public : function () {
return private_method(private);
}
}
}());
зачем скобки вокруг функции?
a = function у нас ведь уже является функциональным выражением, т.е.
a = function() {}(); – корректно отрабатывает.
В чём здесь секрет?

Дмитрий Подгорный

Здравствуйте.

Технически скобки вокруг функции можно было бы и опустить. Я оставляю их ради двух вещей:

  1. Начиная читать строку вида var a = (function () { становится понятно однозначно, что я не записываю функцию в переменную, а что-то еще делаю с самой функцией. Например вызываю ее, или вызываю метод .bind. Таким образом я позволяю сделать читающему более уверенные предположения о том что происходит в коде без необходимости полностью прочитать код функции.
  1. Ради постоянства записи “все самовызывающиеся функции обрамлены скобками”.

Кстати, если есть больше вопросов по javascript, буду рад ответить на них на форуме http://forum.jscourse.com.

undefinedIsAFunction

А, вон оно что. Честно говоря, я подозревал что-то подобное. Спасибо за ответ )

Быдло кодер

че очки не носишь?

Дмитрий

Здравствуйте,

Большое спасибо за статью!

Я новичок в JS. Подскажите, пожалуйста, как в такой реализации синглтона объявлять и использовать публичные и приватные методы/атрибуты?

Спасибо!

Дмитрий Подгорный

Вот пример где есть методы (inc, dec) и они изменяют значение (total) в самом синглтоне. Обрати внимание // initialization stage комментарий. Это место где будут создаваться свойства при первом конструировании экземпляра синглтона.

var Singletone = (function () {
	var instance;

	function Construct_singletone () {
		if (instance) {
			return instance;
		}
		if (this && this.constructor === Construct_singletone) {
			instance = this;
			// initialization stage
			this.total = 0
		} else {
			return new Construct_singletone();
		}
	}

	Construct_singletone.prototype.inc = function () {
		this.total += 1;
	}

	Construct_singletone.prototype.dec = function () {
		this.total -= 1;
	}

	return Construct_singletone
}());

const a = new Singletone()
a.inc()
a.inc()
console.log(a.total) // 2
const b = Singletone()
console.log(b.total) // 2
console.log(a === b) // true
Popular