Abstract: Why is more important than how.

  • Why does JavaScript have a Symbol type
  • Author: Front-end xiaozhi

Symbols is a new data type introduced in ES6 that brings some benefits to JS, especially when it comes to object attributes. But what can they do for us that strings can’t?

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

background

Js data types are generally divided into two types: value type and reference type

  • Value types (basic types) : NUMERIC (Number), character (String), Boolean, NULL, and underpay
  • Reference types (classes) : functions, objects, arrays, etc

** value type understanding: the mutual assignment between ** variables, refers to open up a new memory space, the variable value assigned to the new variable stored in the new opened memory; The value changes of the following two variables do not affect each other, for example:

var a = 10; // create a memory space to hold the value of variable A "10";
var b = a; // Create a new memory space for variable b, and store the assignment of a value "10" in the new memory;
// No matter how the values of a and B change in the future, the values of the other will not be affected;
Copy the code

Some languages, such as C, have the concept of reference passing and value passing. JavaScript has a similar concept, which is inferred from the data type passed. If a value is passed to a function, reassigning the value does not modify the value in the calling location. However, if you change the reference type, the modified value will also be changed where it was called.

** Reference type understanding: ** variables between the mutual assignment, just the exchange of Pointers, rather than the object (ordinary object, function object, array object) copy a new variable, the object is still only one, just a more guidance ~~; Such as:

var a = { x: 1.y: 2 }; The value of variable A is an address. This address refers to the space where the object is stored.
var b = a; // Assign a's guide address to B instead of copying an object and creating a new memory space to store it;
// If a is used to modify the properties of the object, then b is used to view the properties of the object.
Copy the code

The value type (except for mysterious NaN values) will always be exactly the same as that of another value type with the same value, as follows:

const first = "abc" + "def";
const second = "ab" + "cd" + "ef";
console.log(first === second); // true
Copy the code

But reference types with exactly the same structure are not equal:

const obj1 = { name: "Intrinsic" };
const obj2 = { name: "Intrinsic" };
console.log(obj1 === obj2); // false
// However, their.name attribute is primitive:
console.log(obj1.name === obj2.name); // true
Copy the code

Objects play an important role in the JavaScript language, and their use is ubiquitous. Objects are typically used as collections of key/value pairs, however, there is one big limitation to using them in this way: before symbol, the object key could only be a string, and if you try to use a non-string value as the key of an object, that value would be cast to a string, as follows:

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

What is the Symbol

The Symbol() function returns a value of type Symbol, which has static attributes and static methods. Its static attributes expose several built-in member objects; Its static method exposes the global Symbol registry and is similar to the built-in object class, but as a constructor it is incomplete because it does not support the syntax: “new Symbol()”. So the values generated using Symbol are not equal:

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

When you instantiate symbol, there is an optional first argument for which you can optionally supply a string. This value is intended for debugging code, otherwise it doesn’t really affect 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

Symbols have another important use. They can be used as keys in objects, as follows:

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

At first glance, this looks like you can use Symbol to create private attributes on objects. Many other programming languages have their own private attributes in their classes, and private attribute omissions have long been seen as a weakness of JavaScript.

Unfortunately, code that interacts with the object still has access to a property whose key is Symbol. This is even possible in cases where the calling code does not yet have access to the Symbol itself. For example, the reflect.ownkeys () method gets a list of all keys on an object, including strings and symbols:

function tryToAddPrivate(o) {
    o[Symbol("Pseudo Private")] = 42;
}
const obj = { prop: "hello" };
tryToAddPrivate(obj);
console.log(Reflect.ownKeys(obj));
// [ 'prop', Symbol(Pseudo Private) ]
console.log(obj[Reflect.ownKeys(obj)[1]]); / / 42
Copy the code

Note: Some work is being done to address the issue of adding private attributes to classes in JavaScript. The name of this feature is called private fields, and while this will not benefit all objects, it will benefit objects of class instances. Private fields are available from Chrome 74.

The bugs that may exist after code deployment cannot be known in real time. In order to solve these bugs, I spent a lot of time on log debugging. Incidentally, I recommend a good BUG monitoring tool for youFundebug.

Prevents attribute name conflicts

Symbols may not directly benefit from JavaScript providing private attributes for objects. However, they are beneficial for another reason. They are useful when different libraries want to add attributes to objects without the risk of name conflicts.

Symbol is a little difficult to provide private attributes to JavaScrit objects, but Symbol has the added benefit of avoiding the risk of naming conflicts when different libraries add attributes to objects.

Consider the case where two different libraries want to add basic data to an object, and perhaps they both want to set some kind of identifier on the object. By simply using ID as the key, there is a huge risk that multiple libraries will use the same key.

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

By using symbols, each library can generate the required symbols at instantiation time. Then use the generated Symbol value as the object’s attribute:

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, Symbol does seem to benefit JavaScript.

But, you might ask, why can’t each library simply generate random strings or use namespaces when instantiated?

const library1property = uuid(); // random approach
function lib1tag(obj) {
    obj[library1property] = 42;
}
const library2property = "LIB2-NAMESPACE-id"; // namespaced approach
function lib2tag(obj) {
    obj[library2property] = 369;
}
Copy the code

This approach is correct; it is actually very similar to Symbol’s approach, and there is no risk of conflict unless both libraries choose to use the same attribute name.

At this point, the intelligent reader will point out that the two approaches are not exactly the same. The attribute names we used with unique names still have one disadvantage: their keys are very easy to find, especially when running code to iterate over keys or 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 Symbol for the attribute name of the object, the JSON output will not contain its value. Why is that? Just because JavaScript has gained support for Symbol doesn’t mean the JSON specification has changed! JSON only allows strings as keys, and JavaScript does not attempt to represent the Symbol property in the final JSON payload.

const library2property = "f468c902-26ed-4b2e-81d6-5775ae7eec5d"; // namespaced approach
function lib2tag(obj) {
    Object.defineProperty(obj, library2property, {
        enumerable: false.value: 369
    });
}
const user = {
    name: "Thomas Hunter II".age: 32
};
lib2tag(user);
console.log(user); // {name: "Thomas Hunter II", age: 32, f468c902-26ed-4b2e-81d6-5775ae7eec5d: 369}
console.log(JSON.stringify(user)); // {"name":"Thomas Hunter II","age":32}
console.log(user[library2property]); / / 369
Copy the code

A string key that “hides” by setting the Enumerable property 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 in the following example:

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 have almost recreated Symbol. Hidden string attributes and symbols are hidden from the serializer. Both properties can be read using the reflect.ownkeys () method, so they are not actually private. Assuming we use some sort of namespace/random value for string versions of attribute names, we eliminate the risk of accidental name collisions across multiple libraries.

However, there is still a slight difference. Since strings are immutable, and symbols are always guaranteed to be unique, it is still possible to generate string combinations that can cause conflicts. Mathematically, this means that Symbol does provide benefits that we can’t get from strings.

In Node.js, when inspecting an object (for example, using console.log()), if a method is encountered on an object named inspect, this function is called and the contents are printed. 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 can now be used to do this and can be used in equire(‘util’).inspect.custom. The inspect method was deprecated in Node.js V10, completely ignored in Vv1, and now no one accidentally changes the behavior of inspections.

Simulate private properties

There is an interesting method we can use to simulate private properties on objects. This approach takes advantage of another JavaScript feature: proxy. A proxy essentially encapsulates an object and allows us to intervene in various operations with that object.

Proxies provide a number of methods to intercept operations performed on objects. We can use proxies to specify the properties available on our object, in which case we will make a proxy that hides our two known hidden properties, one is the string _favColor and the other is the SMyBOL 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 for the library. Also, find dynamic keys by brute force (such as the previous UUID example). However, no one can access the ‘Metro 2033’ value from the proxy object without a direct reference to Symbol.

Node.js Warning: There is a feature in Node.js that breaks agent privacy. The JavaScript language itself does not have this capability, and it does not work in other situations, such as Web browsers. It allows access to the underlying object at the time of a given proxy. Here is an example of using this feature to break the above private property example:

const [originalObject] = process.binding("util").getProxyDetails(proxy);
const allKeys = Reflect.ownKeys(originalObject);
console.log(allKeys[3]); // Symbol(fav book)
Copy the code

Now we 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 it’s a scary rabbit hole. If you’re interested in falling down such a rabbit hole, check out our other blog post: Protecting Your JavaScript APIs.

About Fundebug

Fundebug focuses on real-time BUG monitoring for JavaScript, wechat applets, wechat games, Alipay applets, React Native, Node.js and Java online applications. Since its launch on November 11, 2016, Fundebug has handled more than 1 billion error events in total, and paid customers include Google, 360, Kingsoft, Minming.com and many other brands. Welcome to try it for free!