Pull to refresh

Exploring JavaScript Symbols. Использование символов

Reading time5 min
Views12K
Это вторая статья из серии про символы и их использование в JavaScript. Предыдущая часть: «Symbol — новый тип данных в JavaScript».

С появлением символов объект Object был расширен одним методом, который позволяет получить все символы объекта:

	var role = Symbol('role');
	var score = Symbol('score');
	var id = 100;
	var name = 'Moderator';

	var user = { id: id, name: name };

	user[role] = 'admin';
	user[score] = 50000;

	Object.getOwnPropertySymbols( user ); // [Symbol(role), Symbol(score)]

Наличие этого метода лишает нас возможности создавать по-настоящему приватные свойства.

Вот, например, как можно получить роль пользователя:

	var userSymbols = Object.getOwnPropertySymbols( user );
	var roleSymbol = userSymbols[0];

	user[ roleSymbol ]; // 'admin'

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

Хочу отметить также, что спецификация ECMAScript 6 добавила новый объект Reflect который дает возможность рефлексии (или отражения) в JavaScript. И один из методов объекта Reflect позволяет получить все свойства, которые объявленные и через строки, и через символы.

	var properties = Reflect.ownKeys( user );

	console.log( user ); // ['id', 'name', Symbol(role), Symbol(score)]

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

	var Project = (function () {

		var projectData = Symbol('project');
		var projectStatus = Symbol('status');
		var _getTitle = Symbol('title');

		function Project( data, status ) {
			this[ projectData ] = data;
			this[ projectStatus ] = status;
		}

		Project.prototype.getProjectTitle = function () {
			return this[ _getTitle ]();
		};

		Project.prototype[ _getTitle ] = function () {
			return this[ projectData ].name + ' (' + this[ projectData ].description + ')';
		};

		Project.prototype.getStatus = function () {
			return this[ projectStatus ];
		};

		Project.prototype.changeStatus = function ( status ) {
			this[ projectStatus ] = status;
		};

		return Project;

	}());

	var project = new Project({ name: 'Application', description: 'favorite project' }, 'open');

	console.log( project.getStatus() );        // 'open'

	project.changeStatus('finished');

	console.log( project.getStatus() );        // 'finished'
	console.log( project.getProjectTitle() );  // 'Application (favorite project)'

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

Но все же основной причиной добавления символов является не необходимость создания приватных свойств (такая возможность, кстати, также была предложена — private name objects, но она по ряде причин не вошла в текущую версию спецификации). Основная проблема, которую будут решать символы, это создание уникальных свойств. Разработчики библиотек с помощью символов могут добавлять свойства которые гарантированно будут уникальными, а это значит, что не будут конфликтовать с другими свойствами (они не переопределят свойства, которые добавлены другими модулями, также пользовательский код не сможет случайно переопределить данные свойства).

	var Request = (function () {

		var requestState = Symbol('state');
		var states = {
			NOT_INITIALIZED: Symbol(),
			RECEIVED: Symbol(),
			PROCESSING: Symbol(),
			FINISHED: Symbol()
		};

		function Request() {
			this[ requestState ] = states.NOT_INITIALIZED;
		}

		Request.prototype.getStates = function () {
			return states;
		};

		Request.prototype.close = function () {
			this[ requestState ] = states.FINISHED;
		};

		Request.prototype.changeState = function ( state ) {
			this[ requestState ] = state;
		};

		return Request;

	}())

    var request = new Request();
    var handledState = Symbol('state');

    request[ handledState ] = false;

    // code

    request[ handledState ] = true;
    request.close();

Код выше демонстрирует ситуацию, когда у нас есть сторонний модуль, который предоставляет сущность Request. Этот модуль определяет состояние requestState, требуемое для работы модуля, используя символ. Разработчик также вводит свое состояние (например состояние, которое отображает или запрос обработан), также используя символ. Такой способ объявления свойств через символы дает нам гарантию того, что не будет переопределено поведение работы модуля.

В этом примере также предоставлен еще один способ использования символов. Объект states можно рассматривать как перечисляемый тип с уникальными значениями (что позволяет проверить или передано корректное значение).

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

Код выполняется в том глобальном контексте, в котором этот код объявлен, но у нас есть возможность выполнить код в другом контексте и это уже может вызвать некоторые проблемы. К примеру в браузере код, который объявлен в iframe будет иметь другой глобальный контекст и каждый iframe будет иметь свою копию для RegExp, Array, Date и т.д. И проблема в том, что символы также существуют и уникальны только в рамках контекста, в котором они объявлены. Если ваш код взаимодействует с кодом в iframe, то в данном iframe у вас не будет ссылок на символы и вы не сможете получить доступ к свойствам которые будут объявлены через эти символы.

Для решения этой проблемы был введен глобальный реестр для символов. Чтобы создать символ который будет доступен во всех контекстах нужно воспользоваться методом Symbol.for:

	var UUIDSymbol = Symbol.for('uuid');

Этот метод также используется для получения символа из глобального реестра. Логика работы метода следующая: если символ найден, тогда он будет возвращен как результат работы метода; если символ не найден в реестре, он будет создан, добавлен в глобальный реестр и возвращен как результат.

Аналогичная ситуация с необходимостью использовать глобальный реестр есть и на стороне сервера. Например, в Node.js есть модуль vm, который позволяет выполнять код в другом контексте.

	var vm = require('vm');
	var ourArray = Array;
	var ourSymbol = Symbol('uuid');
	var theirArray = vm.runInNewContext('Array');
	var theirSymbol = vm.runInNewContext('Symbol("uuid")');

	ourArray === theirArray; // false
	ourSymbol === theirSymbol; // false

Можно увидеть, что Array в текущем контексте не равен Array в другом контексте (каждый имеет свою копию). Аналогичное поведение как и в ситуации с iframe и со символами, они не равны друг другу в разных контекстах.

	var vm = require('vm');
	var ourSymbol = Symbol.for('uuid');
	var theirSymbol = vm.runInNewContext('Symbol.for("uuid")');

	ourSymbol === theirSymbol; // true

Объявив символы через Symbol.for мы получаем возможность использовать их во всех контекстах. Если в вашем проекте используется модуль vm с исполнением кода с определенными контекстами, использованик Symbol.for будет отличным решением в данной ситуации.

Есть также метод, который позволяет получить ключ, по которому символ добавлен в реестр — Symbol.keyFor:

    var UUIDSymbolKey = Symbol.keyFor( UUIDSymbol );

Если указанный символ не будет найден в реестре, вернется undefined.

Хочу еще раз отметить, что простое создание символа через Symbol('score') — не создат символ в глобальном реестре.

В следующей части мы продолжим рассматривать объект Symbol, а также затронем такое понятие как Well-known symbols и посмотрим какие возможности они перед нами открывают.
Tags:
Hubs:
+20
Comments5

Articles