• JavaScript Symbols: But Why?
  • Thomas Hunter II
  • The Nuggets translation Project
  • Permanent link to this article: github.com/xitu/gold-m…
  • Translator: xionglong58
  • Proofreader: EdmondWang, Xuyuey

As the newest basic type, Symbol brings many benefits to the JavaScript language, especially when used on object attributes. But what does Symbol have that String doesn’t?

Before diving into Symbol, let’s take a look at some JavaScript features that many developers may not know about.

background

There are two types of data in JavaScript: Basic data types and objects (objects also include functions). Basic data types include simple data types such as Number (integers to floating-point numbers, Boolean, string, undefined, null (note that null is still a basic datatype even though typeof NULL === ‘object’).

The value of a primitive data type is immutable, that is, the original value of a variable cannot be changed. You can, of course, reassign variables. For example, the code let x = 1; x++; Although you have changed the value of the variable x by reassigning, the original value of the variable 1 remains unchanged.

Some languages, such as C, have the concepts of passing by reference and passing by value. JavaScript has a similar concept, which is inferred from the type of data being passed. If a value is passed into a function, reassigning it in the function does not change its value at the call location. However, if you modify the value of the base data, the modified value will be modified where it was called.

Consider the following example:

function primitiveMutator(val) {
  val = val + 1;
}

let x = 1;
primitiveMutator(x);
console.log(x); / / 1

function objectMutator(val) {
  val.prop = val.prop + 1;
}

let obj = { prop: 1 };
objectMutator(obj);
console.log(obj.prop); / / 2
Copy the code

A base data type (except for NaN) is always exactly equal to another base data type that has the same value. As follows:

const first = "abc" + "def";
const second = "ab" + "cd" + "ef";

console.log(first === second); // true
Copy the code

However, constructing two non-basic data types with the same value results in unequal results. We can see what happens:

const obj1 = { name: "Intrinsic" };
const obj2 = { name: "Intrinsic" };

console.log(obj1 === obj2); // false

Console. log(obj1.name === obj2.name); // However, console.log(obj1.name === obj2.name); // true
Copy the code

Objects play an important role in JavaScript and can be found almost everywhere. Objects are usually collections of key/value pairs, but the biggest limitation of this form is that the key of an object can only be a string, until Symbol appears. If we use a non-string value as the key of an object, that value is cast to a string. You can see this cast in the following program:

const obj = {};
obj.foo = 'foo';
obj['bar'] = 'bar';
obj[2] = 2;
obj[{}] = 'someobj';

console.log(obj);
// { '2': 2, foo: 'foo', bar: 'bar','[object Object]': 'someobj' }
Copy the code

Note: Although somewhat off topic, it is important to know that part of the reason Map data structures were created was to allow key/value storage when keys are not strings.

What is Symbol?

Now that we know what the basic data types are, we can finally define Symbol. Symbol is a basic data type that cannot be recreated. In this case, Symbol is similar to an object in that creating multiple instances of an object also results in values that are not exactly equal. However, Symbol is also a basic data type because it cannot be changed. Here is an example of the use of Symbol:

const s1 = Symbol(a);const s2 = Symbol(a);console.log(s1 === s2); // false
Copy the code

When instantiating a symbol value, there is an optional preferred argument that you can assign to a string. This value is used for debugging code and does not really affect the Symbol itself.

const s1 = Symbol('debug');
const str = 'debug';
const s2 = Symbol('xxyy');

console.log(s1 === str); // false
console.log(s1 === s2); // false
console.log(s1); // Symbol(debug)
Copy the code

Symbol is the object property

There is another important use for symbols. They can be used as keys in objects! Here is an example of using symbol as a key in an object:

const obj = {};
const sym = Symbol(a); obj[sym] ='foo';
obj.bar = 'bar';

console.log(obj); // { bar: 'bar' }
console.log(sym in obj); // true
console.log(obj[sym]); // foo
console.log(Object.keys(obj)); // ['bar']
Copy the code

Note that the symbols key is not returned in object.keys (). This is also for backward compatibility. Older versions of JavaScript do not have a symbol data type and should not be returned from the older object.keys () method.

At first glance, this looks like you can create private properties on objects using Symbols! Many other programming languages can have private attributes in their classes, and JavaScript’s omission of this feature has long been seen as a weakness in its syntax.

Unfortunately, code that interacts with this object can still access properties of the object that have the symbols key. This can even happen if the calling code cannot access the Symbol itself. For example, the reflect.ownkeys () method can get a list of all the keys of an object, including strings and symbols:

function tryToAddPrivate(obj) {
  obj[Symbol('Pseudo Private')] = 42;
}

const obj = { prop: 'hello' };
tryToAddPrivate(obj);

console.log(Reflect.ownKeys(obj));

console.log(obj[Reflect.ownKeys(obj)[1]]); / / 42
Copy the code

Note: There is currently some work to address the issue of adding private attributes to classes in JavaScript. The feature is Private Fields and while this won’t be good for all objects, it will be good for objects that are instances of the class. Private Fields is available from Chrome 74.

Prevents property name conflicts

The Symbol type can be detrimental to obtaining private attributes of objects in JavaScript. Another reason they are useful is that Symbols avoids the risk of naming conflicts when different libraries want to add attributes to objects.

If you have two different libraries that want to attach some kind of metadata to an object, both might want to set some kind of identifier on the object. With just two string ids as keys, the risk of multiple libraries using the same key is high.

function lib1tag(obj) {
  obj.id = 42;
}

function lib2tag(obj) {
  obj.id = 369;
}
Copy the code

Using symbols, each library can generate the symbols it needs by instantiating the Symbol class. Then, at any time, the keys corresponding to symbols can be checked and assigned on the corresponding objects.

const library1property = Symbol('lib1');
function lib1tag(obj) {
  obj[library1property] = 42;
}

const library2property = Symbol('lib2');
function lib2tag(obj) {
  obj[library2property] = 369;
}
Copy the code

For this reason symbols really benefits JavaScript.

However, you might wonder why each library can’t simply generate a random string or use a special namespace when instantiated?

const library1property = uuid(); // Random method
function lib1tag(obj) {
  obj[library1property] = 42;
}

const library2property = 'LIB2-NAMESPACE-id'; // namespaced approach
function lib2tag(obj) {
  obj[library2property] = 369;
}
Copy the code

You could be right. The two methods above are very similar to using Symbols. Unless two libraries use the same attribute name, there is no risk of conflict.

At this point, astute readers will point out that the two approaches are not identical. Attribute names with unique names still have one disadvantage: their keys are easy to find, especially when running code to iterate over keys or otherwise serialize objects. Consider the following example:

const library2property = 'LIB2-NAMESPACE-id'; // namespaced
function lib2tag(obj) {
  obj[library2property] = 369;
}

const user = {
  name: 'Thomas Hunter II'.age: 32
};

lib2tag(user);

JSON.stringify(user);
// '{"name":"Thomas Hunter II","age":32,"LIB2-NAMESPACE-id":369}'
Copy the code

If we use a Symbol for the attribute name of the object, the JSON output will not contain the corresponding value of symbol. Why is that? Just because JavaScript supports Symbols doesn’t mean the JSON specification has changed! JSON only allows strings as keys, and JavaScript does not attempt to render the Symbol attribute in the final JSON payload.

We can easily correct library object string contamination of JSON output by using object.defineProperty () :

const library2property = uuid(); // namespaced approach
function lib2tag(obj) {
  Object.defineProperty(obj, library2property, {
    enumerable: false.value: 369
  });
}

const user = {
  name: 'Thomas Hunter II'.age: 32
};

lib2tag(user);
// '{"name":"Thomas Hunter II","age":32,"f468c902-26ed-4b2e-81d6-5775ae7eec5d":369}
console.log(JSON.stringify(user));
console.log(user[library2property]); / / 369
Copy the code

“Hiding” string keys by setting their enumerable descriptor to false behaves much like the Symbol key. They are also invisible through object.keys () traversal, but can be shown through reflect.ownkeys (), as shown below:

const obj = {};
obj[Symbol= ()]1;
Object.defineProperty(obj, 'foo', {
  enumberable: false.value: 2
});

console.log(Object.keys(obj)); / / []
console.log(Reflect.ownKeys(obj)); // [ 'foo', Symbol() ]
console.log(JSON.stringify(obj)); / / {}
Copy the code

At this point, we almost recreated Symbols. Hidden string attributes and symbols are invisible to the serializer. Both of these attributes can be extracted using the reflect.ownkeys () method, so they are not actually private. Assuming we use some sort of namespace/random value for string attributes, we eliminate the risk of accidental naming conflicts across multiple libraries.

However, there is still a slight difference. Since strings are immutable and Symbol is always guaranteed to be unique, it is still possible to generate identical strings and cause conflicts. From a mathematical point of view, this means that Symbols does offer benefits that we can’t get from strings.

In Node.js, when inspecting an object (for example, using console.log()), if a method named inspect is encountered on the object, this function is called and the output log is represented as the object. As you can imagine, this behavior is not what everyone expects, and the method commonly named inspect often conflicts with objects created by users. Symbol is now available to implement this function and can be used in require(‘util’).inspection.custom. The inspect method was deprecated in Node.js V10 and completely ignored in V11. Now nobody’s going to change Inspect’s behavior by accident!

Simulate private properties

There is an interesting method that we can use to simulate private properties on objects. This approach takes advantage of another JavaScript feature: proxies. Proxy essentially encapsulates an object and allows us to interact with that object differently.

Proxy provides a number of methods to intercept operations performed on objects. We are interested in what the proxy does when it tries to read the key of an object. I won’t go into the details of how proxy works, but if you want more information, check out our other article: JavaScript Object Property Descriptors, Proxies, and Preventing Extension.

We can use proxy to lie about the properties available on an object. In this case, we will create a proxy that hides our two known hidden attributes, the string _favColor and the symbol assigned to the favBook:

let proxy;

{
  const favBook = Symbol('fav book');

  const obj = {
    name: 'Thomas Hunter II'.age: 32._favColor: 'blue',
    [favBook]: 'Metro 2033'[Symbol('visible')]: 'foo'
  };

  const handler = {
    ownKeys: (target) = > {
      const reportedKeys = [];
      const actualKeys = Reflect.ownKeys(target);

      for (const key of actualKeys) {
        if (key === favBook || key === '_favColor') {
          continue;
        }
        reportedKeys.push(key);
      }

      returnreportedKeys; }}; proxy =new Proxy(obj, handler);
}

console.log(Object.keys(proxy)); // [ 'name', 'age' ]
console.log(Reflect.ownKeys(proxy)); // [ 'name', 'age', Symbol(visible) ]
console.log(Object.getOwnPropertyNames(proxy)); // [ 'name', 'age' ]
console.log(Object.getOwnPropertySymbols(proxy)); // [Symbol(visible)]
console.log(proxy._favColor); // 'blue'
Copy the code

Using the _favColor string is simple: just read the source code of the library. In addition, dynamic keys can be found (as in the PREVIOUS UUID example) by force. However, no one can access the value Metro 2033 from the proxy object without directly referencing Symbol.

Node.js declaration: a feature in Node.js breaks proxy privacy. This feature does not exist in the JavaScript language itself, nor does it work in other situations, such as web browsers. This feature allows access to the underlying objects at a given proxy. Here is an example of using this feature to break the above private property:

const [originalObject] = process
  .binding('util')
  .getProxyDetails(proxy);

const allKeys = Reflect.ownKeys(originalObject);
console.log(allKeys[3]); // Symbol(fav book)
Copy the code

We now need to modify the global Reflect objects, or modify the util process bindings, to prevent them from being used in specific Node.js instances. But that’s the gateway to a new world, and if you want to understand it, check out our other blog: Protecting Your JavaScript APIs.

I wrote this article with Thomas Hunter II. I work for a company called Intricsic (we’re hiring, by the way!) , written specifically to protect Node.js applications. We currently have a product that applies the Least Privilege model to protect applications. Our product proactively protects Node.js applications from attackers and is very easy to implement. If you’re looking for ways to secure node.js applications, please contact us at [email protected].


Author of the banner photoChunlea Ju

If you find any mistakes in your translation or other areas that need to be improved, you are welcome to the Nuggets Translation Program to revise and PR your translation, and you can also get the corresponding reward points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.


The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.