|
| 1 | +# Symbol type |
| 2 | + |
| 3 | +By specification, object property keys may be either of string type, or of symbol type. Not numbers, not booleans, only strings or symbols, these two types. |
| 4 | + |
| 5 | +Till now we’ve only seen strings. Now let’s see the advantages that symbols can give us. |
| 6 | + |
| 7 | +## Symbols |
| 8 | + |
| 9 | +“Symbol” value represents a unique identifier. |
| 10 | + |
| 11 | +A value of this type can be created using `Symbol()`: |
| 12 | + |
| 13 | +``` |
| 14 | +// id is a new symbol |
| 15 | +let id = Symbol(); |
| 16 | +``` |
| 17 | + |
| 18 | +We can also give symbol a description (also called a symbol name), mostly useful for debugging purposes: |
| 19 | + |
| 20 | +``` |
| 21 | +// id is a symbol with the description "id" |
| 22 | +let id = Symbol("id"); |
| 23 | +``` |
| 24 | + |
| 25 | +Symbols are guaranteed to be unique. Even if we create many symbols with the same description, they are different values. The description is just a label that doesn’t affect anything. |
| 26 | + |
| 27 | +For instance, here are two symbols with the same description – they are not equal: |
| 28 | + |
| 29 | + |
| 30 | +``` |
| 31 | +let id1 = Symbol("id"); |
| 32 | +let id2 = Symbol("id"); |
| 33 | +
|
| 34 | +alert(id1 == id2); // false |
| 35 | +``` |
| 36 | + |
| 37 | +If you are familiar with Ruby or another language that also has some sort of “symbols” – please don’t be misguided. JavaScript symbols are different. |
| 38 | + |
| 39 | + |
| 40 | +*** |
| 41 | + |
| 42 | +#### Symbols don’t auto-convert to a string |
| 43 | + |
| 44 | +Most values in JavaScript support implicit conversion to a string. For instance, we can `alert` almost any value, and it will work. Symbols are special. They don’t auto-convert. |
| 45 | + |
| 46 | +For instance, this `alert` will show an error: |
| 47 | + |
| 48 | +``` |
| 49 | +let id = Symbol("id"); |
| 50 | +alert(id); // TypeError: Cannot convert a Symbol value to a string |
| 51 | +``` |
| 52 | + |
| 53 | +If we really want to show a symbol, we need to call `.toString()` on it, like here: |
| 54 | + |
| 55 | + |
| 56 | +``` |
| 57 | +let id = Symbol("id"); |
| 58 | +alert(id.toString()); // Symbol(id), now it works |
| 59 | +``` |
| 60 | + |
| 61 | +That’s a “language guard” against messing up, because strings and symbols are fundamentally different and should not occasionally convert one into another. |
| 62 | + |
| 63 | +*** |
| 64 | + |
| 65 | +## “Hidden” properties |
| 66 | + |
| 67 | +Symbols allow us to create “hidden” properties of an object, that no other part of code can occasionally access or overwrite. |
| 68 | + |
| 69 | +For instance, if we want to store an “identifier” for the object `user`, we can use a symbol as a key for it: |
| 70 | + |
| 71 | + |
| 72 | +``` |
| 73 | +let user = { name: "John" }; |
| 74 | +let id = Symbol("id"); |
| 75 | +
|
| 76 | +user[id] = "ID Value"; |
| 77 | +alert( user[id] ); // we can access the data using the symbol as the key |
| 78 | +``` |
| 79 | + |
| 80 | +What’s the benefit over using `Symbol("id")` over a string `"id"`? |
| 81 | + |
| 82 | +Let’s make the example a bit deeper to see that. |
| 83 | + |
| 84 | +Imagine that another script wants to have its own “id” property inside `user`, for its own purposes. That may be another JavaScript library, so the scripts are completely unaware of each other. |
| 85 | + |
| 86 | +Then that script can create its own `Symbol("id")`, like this: |
| 87 | + |
| 88 | +``` |
| 89 | +// ... |
| 90 | +let id = Symbol("id"); |
| 91 | +
|
| 92 | +user[id] = "Their id value"; |
| 93 | +``` |
| 94 | + |
| 95 | +There will be no conflict, because symbols are always different, even if they have the same name. |
| 96 | + |
| 97 | +Now note that if we used a string `"id"` instead of a symbol for the same purpose, then there would be a conflict: |
| 98 | + |
| 99 | + |
| 100 | +``` |
| 101 | +let user = { name: "John" }; |
| 102 | +
|
| 103 | +// our script uses "id" property |
| 104 | +user.id = "ID Value"; |
| 105 | +
|
| 106 | +// ...if later another script the uses "id" for its purposes... |
| 107 | +
|
| 108 | +user.id = "Their id value" |
| 109 | +// boom! overwritten! it did not mean to harm the colleague, but did it! |
| 110 | +``` |
| 111 | + |
| 112 | +### Symbols in a literal |
| 113 | + |
| 114 | +If we want to use a symbol in an object literal, we need square brackets. |
| 115 | + |
| 116 | +Like this: |
| 117 | + |
| 118 | +``` |
| 119 | +let id = Symbol("id"); |
| 120 | +
|
| 121 | +let user = { |
| 122 | + name: "John", |
| 123 | + [id]: 123 // not just "id: 123" |
| 124 | +}; |
| 125 | +``` |
| 126 | + |
| 127 | +That’s because we need the value from the variable `id` as the key, not the string “id”. |
| 128 | + |
| 129 | +### Symbols are skipped by for…in |
| 130 | + |
| 131 | +Symbolic properties do not participate in `for..in` loop. |
| 132 | + |
| 133 | +For instance: |
| 134 | + |
| 135 | + |
| 136 | +``` |
| 137 | +let id = Symbol("id"); |
| 138 | +let user = { |
| 139 | + name: "John", |
| 140 | + age: 30, |
| 141 | + [id]: 123 |
| 142 | +}; |
| 143 | +
|
| 144 | +for (let key in user) alert(key); // name, age (no symbols) |
| 145 | +
|
| 146 | +// the direct access by the symbol works |
| 147 | +alert( "Direct: " + user[id] ); |
| 148 | +``` |
| 149 | + |
| 150 | +That’s a part of the general “hiding” concept. If another script or a library loops over our object, it won’t unexpectedly access a symbolic property. |
| 151 | + |
| 152 | +In contrast, [Object.assign](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign "Object.assign") copies both string and symbol properties: |
| 153 | + |
| 154 | + |
| 155 | +``` |
| 156 | +let id = Symbol("id"); |
| 157 | +let user = { |
| 158 | + [id]: 123 |
| 159 | +}; |
| 160 | +
|
| 161 | +let clone = Object.assign({}, user); |
| 162 | +
|
| 163 | +alert( clone[id] ); // 123 |
| 164 | +``` |
| 165 | + |
| 166 | +There’s no paradox here. That’s by design. The idea is that when we clone an object or merge objects, we usually want all properties to be copied (including symbols like `id`). |
| 167 | + |
| 168 | +*** |
| 169 | + |
| 170 | +#### Property keys of other types are coerced to strings |
| 171 | + |
| 172 | +We can only use strings or symbols as keys in objects. Other types are converted to strings. |
| 173 | + |
| 174 | +For instance, a number `0` becomes a string `"0"` when used as a property key: |
| 175 | + |
| 176 | +``` |
| 177 | +let obj = { |
| 178 | + 0: "test" // same as "0": "test" |
| 179 | +}; |
| 180 | +
|
| 181 | +// both alerts access the same property (the number 0 is converted to string "0") |
| 182 | +alert( obj["0"] ); // test |
| 183 | +alert( obj[0] ); // test (same property) |
| 184 | +``` |
| 185 | + |
| 186 | +*** |
| 187 | + |
| 188 | +## Global symbols |
| 189 | + |
| 190 | +As we’ve seen, usually all symbols are different, even if they have the same names. But sometimes we want same-named symbols to be same entities. |
| 191 | + |
| 192 | +For instance, different parts of our application want to access symbol `"id"` meaning exactly the same property. |
| 193 | + |
| 194 | +To achieve that, there exists a global symbol registry. We can create symbols in it and access them later, and it guarantees that repeated accesses by the same name return exactly the same symbol. |
| 195 | + |
| 196 | +In order to create or read a symbol in the registry, use `Symbol.for(key)`. |
| 197 | + |
| 198 | +That call checks the global registry, and if there’s a symbol described as `key`, then returns it, otherwise creates a new symbol `Symbol(key)` and stores it in the registry by the given `key`. |
| 199 | + |
| 200 | +For instance: |
| 201 | + |
| 202 | + |
| 203 | +``` |
| 204 | +// read from the global registry |
| 205 | +let id = Symbol.for("id"); // if the symbol did not exist, it is created |
| 206 | +
|
| 207 | +// read it again |
| 208 | +let idAgain = Symbol.for("id"); |
| 209 | +
|
| 210 | +// the same symbol |
| 211 | +alert( id === idAgain ); // true |
| 212 | +``` |
| 213 | + |
| 214 | +Symbols inside the registry are called global symbols. If we want an application-wide symbol, accessible everywhere in the code – that’s what they are for. |
| 215 | + |
| 216 | +*** |
| 217 | + |
| 218 | +#### That sounds like Ruby |
| 219 | + |
| 220 | +In some programming languages, like Ruby, there’s a single symbol per name. |
| 221 | + |
| 222 | +In JavaScript, as we can see, that’s right for global symbols. |
| 223 | + |
| 224 | +*** |
| 225 | + |
| 226 | +### Symbol.keyFor |
| 227 | + |
| 228 | +For global symbols, not only `Symbol.for(key)` returns a symbol by name, but there’s a reverse call: `Symbol.keyFor(sym)`, that does the reverse: returns a name by a global symbol. |
| 229 | + |
| 230 | +For instance: |
| 231 | + |
| 232 | + |
| 233 | +``` |
| 234 | +let sym = Symbol.for("name"); |
| 235 | +let sym2 = Symbol.for("id"); |
| 236 | +
|
| 237 | +// get name from symbol |
| 238 | +alert( Symbol.keyFor(sym) ); // name |
| 239 | +alert( Symbol.keyFor(sym2) ); // id |
| 240 | +``` |
| 241 | + |
| 242 | +The `Symbol.keyFor` internally uses the global symbol registry to look up the key for the symbol. So it doesn’t work for non-global symbols. If the symbol is not global, it won’t be able to find it and return `undefined`. |
| 243 | + |
| 244 | +For instance: |
| 245 | + |
| 246 | + |
| 247 | +``` |
| 248 | +alert( Symbol.keyFor(Symbol.for("name")) ); // name, global symbol |
| 249 | +
|
| 250 | +alert( Symbol.keyFor(Symbol("name2")) ); // undefined, the argument isn't a global symbol |
| 251 | +``` |
| 252 | + |
| 253 | +## System symbols |
| 254 | + |
| 255 | +There exist many “system” symbols that JavaScript uses internally, and we can use them to fine-tune various aspects of our objects. |
| 256 | + |
| 257 | +They are listed in the specification in the [Well-known symbols](https://tc39.github.io/ecma262/#sec-well-known-symbols "Well-known symbols") table: |
| 258 | + |
| 259 | +* `Symbol.hasInstance` |
| 260 | + |
| 261 | +* `Symbol.isConcatSpreadable` |
| 262 | + |
| 263 | +* `Symbol.iterator` |
| 264 | + |
| 265 | +* `Symbol.toPrimitive` |
| 266 | + |
| 267 | +* ... and so on |
| 268 | + |
| 269 | +For instance, `Symbol.toPrimitive` allows us to describe object to primitive conversion. We’ll see its use very soon. |
| 270 | + |
| 271 | +Other symbols will also become familiar when we study the corresponding language features. |
| 272 | + |
| 273 | +## Summary |
| 274 | + |
| 275 | +`Symbol` is a primitive type for unique identifiers. |
| 276 | + |
| 277 | +Symbols are created with `Symbol()` call with an optional description. |
| 278 | + |
| 279 | +Symbols are always different values, even if they have the same name. If we want same-named symbols to be equal, then we should use the global registry: `Symbol.for(key)` returns (creates if needed) a global symbol with `key` as the name. Multiple calls of `Symbol.for` return exactly the same symbol. |
| 280 | + |
| 281 | +Symbols have two main use cases: |
| 282 | + |
| 283 | +1. “Hidden” object properties. If we want to add a property into an object that “belongs” to another script or a library, we can create a symbol and use it as a property key. A symbolic property does not appear in `for..in`, so it won’t be occasionally listed. Also it won’t be accessed directly, because another script does not have our symbol, so it will not occasionally intervene into its actions. |
| 284 | + |
| 285 | + So we can “covertly” hide something into objects that we need, but others should not see, using symbolic properties. |
| 286 | + |
| 287 | +2. There are many system symbols used by JavaScript which are accessible as `Symbol.*`. We can use them to alter some built-in behaviors. For instance, later in the tutorial we’ll use `Symbol.iterator` for iterables, `Symbol.toPrimitive` to setup object-to-primitive conversion and so on. |
| 288 | + |
| 289 | +Technically, symbols are not 100% hidden. There is a built-in method [Object.getOwnPropertySymbols(obj)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertySymbols "Object.getOwnPropertySymbols(obj)") that allows us to get all symbols. Also there is a method named [Reflect.ownKeys(obj)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/ownKeys "Reflect.ownKeys(obj)") that returns all keys of an object including symbolic ones. So they are not really hidden. But most libraries, built-in methods and syntax constructs adhere to a common agreement that they are. And the one who explicitly calls the aforementioned methods probably understands well what he’s doing. |
0 commit comments