От автора: в ES6 появились символы Javasript, как способ предотвращения конфликтов имен свойств. В качестве дополнительного бонуса символы также позволяют моделировать закрытые свойства в JavaScript 2015-2019.
Вступление
Самый простой способ создать символ в JavaScript — это вызвать функцию Symbol(). 2 ключевых вещи, которые делают символы такими особенными:
Символы могут использоваться как ключи объекта. В качестве ключей объекта могут использоваться только строки и символы.
Не существует двух одинаковых символов.
1 2 3 4 5 6 7 8 9 10 11 |
const symbol1 = Symbol(); const symbol2 = Symbol(); symbol1 === symbol2; // false const obj = {}; obj[symbol1] = 'Hello'; obj[symbol2] = 'World'; obj[symbol1]; // 'Hello' obj[symbol2]; // 'World' |
Хотя при вызове Symbol() создается впечатление, что символы являются объектами, символы на самом деле являются примитивным типом JavaScript. Использование Symbol в качестве конструктора с new выдает ошибку.
1 2 3 4 5 6 7 |
const symbol1 = Symbol(); typeof symbol1; // 'symbol' symbol1 instanceof Object; // false // Throws "TypeError: Symbol is not a constructor" new Symbol(); |
Descriptions
Функция Symbol() принимает один параметр, строка description. description предназначен только для целей отладки — description отображается в символе toString(). Однако два символа с одинаковым description не равны.
1 2 3 4 5 |
const symbol1 = Symbol('my symbol'); const symbol2 = Symbol('my symbol'); symbol1 === symbol2; // false console.log(symbol1); // 'Symbol(my symbol)' |
Существует также глобальный реестр символов. Создание символа с использованием Symbol.for() добавляет символ в глобальный реестр, ключ которого обозначен символом description. Другими словами, если вы создадите два символа с одинаковым описанием, используя Symbol.for() для этих двух символов, они будут равны.
1 2 3 4 5 |
const symbol1 = Symbol.for('test'); const symbol2 = Symbol.for('test'); symbol1 === symbol2; // true console.log(symbol1); // 'Symbol(test)' |
Вообще говоря, вы не должны использовать глобальный реестр символов, если у вас нет на то веских причин, потому что вы можете столкнуться с конфликтами имен.
Конфликты имен
Первым встроенным символом в JavaScript был символ Symbol.iterator ES6. Объект с функцией Symbol.iterator считается итеративным. Это означает, что вы можете использовать этот объект как правую часть for/of цикла.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
const fibonacci = { [Symbol.iterator]: function*() { let a = 1; let b = 1; let temp; yield b; while (true) { temp = a; a = a + b; b = temp; yield b; } } }; // Prints every Fibonacci number less than 100 for (const x of fibonacci) { if (x >= 100) { break; } console.log(x); } |
Почему Symbol.iterator символ, а не строка? Предположим, вместо использования итеративной спецификации Symbol.iterator проверяется наличие строкового свойства ‘iterator’. Кроме того, предположим, что у вас был следующий класс, который должен был быть повторяемым.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class MyClass { constructor(obj) { Object.assign(this, obj); } iterator() { const keys = Object.keys(this); let i = 0; return (function*() { if (i >= keys.length) { return; } yield keys[i++]; })(); } } |
Экземпляры MyClass будут повторяемыми, что позволит вам перебирать ключи объекта. Но у вышеупомянутого класса также есть потенциальный недостаток. Предположим, что злоумышленник передал объект со свойством iterator в MyClass.
1 |
const obj = new MyClass({ iterator: 'not a function' }); |
Если бы вы использовали for/of с obj, JavaScript выдал бы TypeError: obj is not iterable. Потому что пользовательская функция iterator перезаписывает свойство итератора класса. Это похоже на проблему безопасности с загрязнением прототипа, когда простое копирование пользовательских данных может вызвать проблемы со специальными свойствами, такими как __proto__и constructor.
Ключевой шаблон здесь заключается в том, что символы обеспечивают четкое разделение между данными пользователя и данными программы в объектах. Поскольку символы не могут быть представлены в JSON, нет риска передачи данных в Express API с ненадлежащим свойством Symbol.iterator. В объектах, которые смешивают пользовательские данные со встроенными функциями и методами, например в моделях Mongoose, вы можете использовать символы, чтобы гарантировать, что пользовательские данные не конфликтуют со встроенными функциями.
Закрытые свойства
Поскольку нет двух одинаковых символов, символы являются удобным способом имитации закрытых свойств в JavaScript. Символы не отображаются в Object.keys(), и поэтому, если вы явно не export символ, никакой другой код не может получить доступ к этому свойству, если вы явно не пройдете функцию Object.getOwnPropertySymbols().
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
function getObj() { const symbol = Symbol('test'); const obj = {}; obj[symbol] = 'test'; return obj; } const obj = getObj(); Object.keys(obj); // [] // Unless you explicitly have a reference to the symbol, you can't access the // symbol property. obj[Symbol('test')]; // undefined // You can still get a reference to the symbol using `getOwnPropertySymbols()` const [symbol] = Object.getOwnPropertySymbols(obj); obj[symbol]; // 'test' |
Символы также удобны для закрытых свойств, потому что они не отображаются в выходных данных JSON.stringify(). В частности, JSON.stringify() игнорирует ключи и значения символов.
1 2 3 4 |
const symbol = Symbol('test'); const obj = { [symbol]: 'test', test: symbol }; JSON.stringify(obj); // "{}" |
Что дальше?
Символы являются отличным инструментом для представления внутреннего состояния объектов, гарантируя, что пользовательские данные остаются отделенными от состояния программы. С использованием символов больше нет необходимости в соглашениях, таких как префиксирование свойств состояния программы с помощью ‘$’. Поэтому в следующий раз, когда вы установите для свойства объекта значение $$__internalFoo, подумайте об использовании вместо этого символа.
Автор: Valeri Karpov
Источник: //thecodebarbarian.com
Редакция: Команда webformyself.