Symbol

Overview

ES5 object attribute names are all strings, which can easily cause conflicts of attribute names. For example, if you use an object provided by others, but you want to add a new method (mixin mode) to this object, the name of the new method may conflict with the existing method. If there is a mechanism to ensure that the name of each attribute is unique, it would fundamentally prevent attribute name conflicts. This is why ES6 introduced Symbol.

ES6 introduces a new primitive data type Symbol, which represents a unique value. It is the seventh data type of the JavaScript language. The first six are: undefined, null, Boolean, String, Number, Object.

Symbol value is generated by Symbol function. That is to say, the property name of the object can now have two types, one is the original string, and the other is the newly-added Symbol type. Any attribute name belonging to the Symbol type is unique, and it can be guaranteed that it will not conflict with other attribute names.

let s = Symbol();

typeof s;
// "symbol"

In the above code, the variable s is a unique value. The result of the typeof operator indicates that the variable s is of the Symbol data type, rather than other types such as strings.

Note that the new command cannot be used before the Symbol function, otherwise an error will be reported. This is because the generated Symbol is a primitive value, not an object. In other words, because the Symbol value is not an object, you cannot add attributes. Basically, it is a data type similar to a string.

The Symbol function can accept a string as a parameter, which represents the description of the Symbol instance, which is mainly for displaying on the console or converting it to a string for easier distinction.

let s1 = Symbol("foo");
let s2 = Symbol("bar");

s1; // Symbol(foo)
s2; // Symbol(bar)

s1.toString(); // "Symbol(foo)"
s2.toString(); // "Symbol(bar)"

In the above code, s1 and s2 are two Symbol values. If no parameters are added, they will all be Symbol() in the console output, which is not conducive to distinguishing. With the parameters, it is equivalent to adding a description to them, and when outputting, you can distinguish which value it is.

If the parameter of Symbol is an object, the toString method of the object will be called to convert it to a string, and then a Symbol value will be generated.

const obj = {
  toString() {
    return "abc";
  },
};
const sym = Symbol(obj);
sym; // Symbol(abc)

Note that the parameters of the Symbol function only represent the description of the current Symbol value, so the return values ​​of the Symbol functions with the same parameters are not equal.

// Without parameters
let s1 = Symbol();
let s2 = Symbol();

s1 === s2; // false

// With parameters
let s1 = Symbol("foo");
let s2 = Symbol("foo");

s1 === s2; // false

In the above code, both s1 and s2 are the return values ​​of the Symbol function, and the parameters are the same, but they are not equal.

Symbol values ​​cannot be operated on with other types of values, and an error will be reported.

let sym = Symbol("My symbol");

"your symbol is " +
  sym // TypeError: can't convert symbol to string
  `your symbol is ${sym}`;
// TypeError: can't convert symbol to string

However, the Symbol value can be explicitly converted to a string.

let sym = Symbol("My symbol");

String(sym); //'Symbol(My symbol)'
sym.toString(); //'Symbol(My symbol)'

In addition, the Symbol value can also be converted to a Boolean value, but it cannot be converted to a numeric value.

let sym = Symbol();
Boolean(sym); // true
!sym; // false

if (sym) {
  // ...
}

Number(sym); // TypeError
sym + 2; // TypeError

Symbol.prototype.description

When creating a Symbol, you can add a description.

const sym = Symbol("foo");

In the above code, the description of sym is the string foo.

However, to read this description, you need to explicitly convert Symbol to a string, which is the following writing.

const sym = Symbol("foo");

String(sym); // "Symbol(foo)"
sym.toString(); // "Symbol(foo)"

The above usage is not very convenient. ES2019 provides an instance property description, which directly returns the description of the Symbol.

const sym = Symbol("foo");

sym.description; // "foo"

Symbol as the attribute name

Since each Symbol value is not equal, this means that the Symbol value can be used as an identifier for the attribute name of the object, and it can be guaranteed that there will be no attributes with the same name. This is very useful when an object is composed of multiple modules to prevent a certain key from being overwritten or overwritten accidentally.

let mySymbol = Symbol();

// The first way of writing
let a = {};
a[mySymbol] = "Hello!";

// The second way of writing
let a = {
  [mySymbol]: "Hello!",
};

// The third way of writing
let a = {};
Object.defineProperty(a, mySymbol, { value: "Hello!" });

// The above writing methods all get the same result
a[mySymbol]; // "Hello!"

The above code uses the square bracket structure and Object.defineProperty to specify the property name of the object as a Symbol value.

Note that when the Symbol value is used as the object attribute name, the dot operator cannot be used.

const mySymbol = Symbol();
const a = {};

a.mySymbol = "Hello!";
a[mySymbol]; // undefined
a["mySymbol"]; // "Hello!"

In the above code, because the dot operator is always a string, the value referred to by mySymbol as the identification name will not be read, resulting in the attribute name of a is actually a string, not a Symbol value.

In the same way, when using Symbol values ​​to define attributes inside objects, the Symbol values ​​must be placed in square brackets.

let s = Symbol();

let obj = {
  [s]: function (arg) {...}
};

obj[s](123);

In the above code, if s is not placed in square brackets, the key name of the attribute is the string s, not the Symbol value represented by s.

With enhanced object writing, the obj object in the above code can be written more concisely.

let obj = {
  [s](arg) {...}
};

The Symbol type can also be used to define a set of constants to ensure that the values ​​of these constants are not equal.

const log = {};

log.levels = {
  DEBUG: Symbol("debug"),
  INFO: Symbol("info"),
  WARN: Symbol("warn"),
};
console.log(log.levels.DEBUG, "debug message");
console.log(log.levels.INFO, "info message");

Here is another example.

const COLOR_RED = Symbol();
const COLOR_GREEN = Symbol();

function getComplement(color) {
  switch (color) {
    case COLOR_RED:
      return COLOR_GREEN;
    case COLOR_GREEN:
      return COLOR_RED;
    default:
      throw new Error("Undefined color");
  }
}

The biggest advantage of using Symbol values ​​for constants is that no other value can have the same value, so the above switch statement can be guaranteed to work as designed.

One more thing to note is that when the Symbol value is used as an attribute name, the attribute is still a public attribute, not a private attribute.

Example: Eliminating the magic string

The magic string refers to a specific string or value that appears multiple times in the code and forms a strong coupling with the code. Well-styled code should try to eliminate magic strings and replace them with clear-meaning variables.

function getArea(shape, options) {
  let area = 0;

  switch (shape) {
    case "Triangle": // Magic string
      area = 0.5 * options.width * options.height;
      break;
    /* ... more code ... */
  }

  return area;
}

getArea("Triangle", { width: 100, height: 100 }); // Magic string

In the above code, the string Triangle is a magic string. It appears many times and forms a "strong coupling" with the code, which is not conducive to future modification and maintenance.

The common way to eliminate the magic string is to write it as a variable.

const shapeType = {
  triangle: "Triangle",
};

function getArea(shape, options) {
  let area = 0;
  switch (shape) {
    case shapeType.triangle:
      area = 0.5 * options.width * options.height;
      break;
  }
  return area;
}

getArea(shapeType.triangle, { width: 100, height: 100 });

In the above code, we write the Triangle as the triangle property of the shapeType object, which eliminates the strong coupling.

If you analyze it carefully, you can find that it does not matter which value shapeType.triangle is equal to, as long as it does not conflict with the values ​​of other shapeType attributes. Therefore, it is very suitable to use Symbol value instead.

const shapeType = {
  triangle: Symbol(),
};

In the above code, except for setting the value of shapeType.triangle to a Symbol, there is no need to modify other places.

Traversal of attribute names

Symbol as the property name, when traversing the object, the property will not appear in the for...in, for...of loop, nor will it be used by Object.keys(), Object.getOwnPropertyNames (), JSON.stringify() return.

However, it is not a private property. There is a Object.getOwnPropertySymbols() method to get all Symbol property names of a specified object. This method returns an array whose members are all Symbol values ​​of the current object used as attribute names.

const obj = {};
let a = Symbol("a");
let b = Symbol("b");

obj[a] = "Hello";
obj[b] = "World";

const objectSymbols = Object.getOwnPropertySymbols(obj);

objectSymbols;
// [Symbol(a), Symbol(b)]

The above code is an example of the Object.getOwnPropertySymbols() method, which can get all Symbol property names.

The following is another example of comparing the Object.getOwnPropertySymbols() method with the for...in loop and the Object.getOwnPropertyNames method.

const obj = {};
const foo = Symbol("foo");

obj[foo] = "bar";

for (let i in obj) {
  console.log(i); // no output
}

Object.getOwnPropertyNames(obj); // []
Object.getOwnPropertySymbols(obj); // [Symbol(foo)]

In the above code, using the for...in loop and the Object.getOwnPropertyNames() method can not get the Symbol key name, you need to use the Object.getOwnPropertySymbols() method.

Another new API, Reflect.ownKeys() method can return all types of key names, including regular key names and Symbol key names.

let obj = {
  [Symbol("my_key")]: 1,
  enum: 2,
  nonEnum: 3,
};

Reflect.ownKeys(obj);
// ["enum", "nonEnum", Symbol(my_key)]

Since the Symbol value is used as the key name, it will not be traversed by conventional methods. We can use this feature to define some non-private methods for the object, but hope to only be used internally.

let size = Symbol("size");

class Collection {
  constructor() {
    this[size] = 0;
  }

  add(item) {
    this[this[size]] = item;
    this[size]++;
  }

  static sizeOf(instance) {
    return instance[size];
  }
}

let x = new Collection();
Collection.sizeOf(x); // 0

x.add("foo");
Collection.sizeOf(x); // 1

Object.keys(x); // ['0']
Object.getOwnPropertyNames(x); // ['0']
Object.getOwnPropertySymbols(x); // [Symbol(size)]

In the above code, the size property of the object x is a Symbol value, so neither Object.keys(x) nor Object.getOwnPropertyNames(x) can get it. This creates the effect of a non-private internal method.

Symbol.for(), Symbol.keyFor()

Sometimes, we want to reuse the same Symbol value, the Symbol.for() method can do this. It accepts a string as a parameter, and then searches for a Symbol value with the parameter as the name. If there is, return the Symbol value, otherwise, create a new Symbol value with the string as the name, and register it to the global.

let s1 = Symbol.for("foo");
let s2 = Symbol.for("foo");

s1 === s2; // true

In the above code, s1 and s2 are both Symbol values, but they are both generated by the Symbol.for method with the same parameters, so they are actually the same value.

Both Symbol.for() and Symbol() will generate new Symbols. The difference between them is that the former will be registered in the global environment for search, while the latter will not. Symbol.for() will not return a new Symbol type value every time it is called. Instead, it will first check whether the given key already exists, and create a new value if it does not exist. For example, if you call Symbol.for("cat") 30 times, the same Symbol value will be returned each time, but calling Symbol("cat") 30 times will return 30 different Symbol values.

Symbol.for("bar") === Symbol.for("bar");
// true

Symbol("bar") === Symbol("bar");
// false

In the above code, because Symbol() has no registration mechanism, each call will return a different value.

The Symbol.keyFor() method returns a key of a registered Symbol type value.

let s1 = Symbol.for("foo");
Symbol.keyFor(s1); // "foo"

let s2 = Symbol("foo");
Symbol.keyFor(s2); // undefined

In the above code, the variable s2 belongs to the unregistered Symbol value, so it returns undefined.

Note that the name registered by Symbol.for() for the Symbol value is in the global environment, regardless of whether it is running in the global environment or not.

function foo() {
  return Symbol.for("bar");
}

const x = foo();
const y = Symbol.for("bar");
console.log(x === y); // true

In the above code, Symbol.for('bar') is run inside the function, but the generated Symbol value is registered in the global environment. Therefore, you can get this Symbol value by running Symbol.for('bar') for the second time.

The global registration feature of Symbol.for() can be used in different iframes or service workers to get the same value.

iframe = document.createElement("iframe");
iframe.src = String(window.location);
document.body.appendChild(iframe);

iframe.contentWindow.Symbol.for("foo") === Symbol.for("foo");
// true

In the above code, the Symbol value generated by the iframe window can be obtained on the main page.

Example: Singleton mode of the module

The Singleton pattern refers to calling a class and returning the same instance at any time.

For Node, the module file can be regarded as a class. How to ensure that every time this module file is executed, the same instance is returned?

It is easy to think that you can put the instance in the top-level object global.

// mod.js
function A() {
  this.foo = "hello";
}

if (!global._foo) {
  global._foo = new A();
}

module.exports = global._foo;

Then, load the above mod.js.

const a = require("./mod.js");
console.log(a.foo);

In the above code, the variable a loaded at any time is the same instance of A.

However, there is a problem here. The global variable global._foo is writable and any file can be modified.

global._foo = { foo: "world" };

const a = require("./mod.js");
console.log(a.foo);

The above code will make the script loading mod.js distorted.

To prevent this from happening, we can use Symbol.

// mod.js
const FOO_KEY = Symbol.for("foo");

function A() {
  this.foo = "hello";
}

if (!global[FOO_KEY]) {
  global[FOO_KEY] = new A();
}

module.exports = global[FOO_KEY];

In the above code, it can be guaranteed that global[FOO_KEY] will not be overwritten unintentionally, but it can still be rewritten.

global[Symbol.for("foo")] = { foo: "world" };

const a = require("./mod.js");

If the key name is generated using the Symbol method, then the value cannot be referenced externally, and of course it cannot be rewritten.

// mod.js
const FOO_KEY = Symbol("foo");

// The code behind is the same...

The above code will cause other scripts to be unable to reference FOO_KEY. But there is a problem with this, that is, if you execute this script multiple times, the FOO_KEY you get is different each time. Although Node will cache the execution results of the script, in general, the same script will not be executed multiple times, but the user can manually clear the cache, so it is not absolutely reliable.

Built-in Symbol value

In addition to defining the Symbol values ​​that you use, ES6 also provides 11 built-in Symbol values ​​that point to methods used internally by the language.

Symbol.hasInstance

The Symbol.hasInstance property of the object points to an internal method. This method is called when another object uses the instanceof operator to determine whether it is an instance of the object. For example, foo instanceof Foo inside the language actually calls Foo[Symbol.hasInstance](foo).

class MyClass {
  [Symbol.hasInstance](foo) {
    return foo instanceof Array;
  }
}

[1, 2, 3] instanceof new MyClass(); // true

In the above code, MyClass is a class, and new MyClass() will return an instance. The Symbol.hasInstance method of this instance will be automatically called when the instanceof operation is performed to determine whether the operator on the left is an instance of Array.

Here is another example.

class Even {
  static [Symbol.hasInstance](obj) {
    return Number(obj) % 2 === 0;
  }
}

// Equivalent to
const Even = {
  [Symbol.hasInstance](obj) {
    return Number(obj) % 2 === 0;
  },
};

1 instanceof Even; // false
2 instanceof Even; // true
12345 instanceof Even; // false

Symbol.isConcatSpreadable

The Symbol.isConcatSpreadable property of the object is equal to a boolean value, indicating whether the object can be expanded when used in Array.prototype.concat().

let arr1 = ["c", "d"];
["a", "b"].concat(arr1, "e"); // ['a','b','c','d','e']
arr1[Symbol.isConcatSpreadable]; // undefined

let arr2 = ["c", "d"];
arr2[Symbol.isConcatSpreadable] = false;
["a", "b"].concat(arr2, "e"); // ['a','b', ['c','d'],'e']

The above code shows that the default behavior of the array is that it can be expanded, and Symbol.isConcatSpreadable is equal to undefined by default. When this attribute is equal to true, there is also an expansion effect.

Objects like arrays are just the opposite and are not expanded by default. Its Symbol.isConcatSpreadable property is set to true before it can be expanded.

let obj = { length: 2, 0: "c", 1: "d" };
["a", "b"].concat(obj, "e"); // ['a','b', obj,'e']

obj[Symbol.isConcatSpreadable] = true;
["a", "b"].concat(obj, "e"); // ['a','b','c','d','e']

The Symbol.isConcatSpreadable property can also be defined in the class.

class A1 extends Array {
  constructor(args) {
    super(args);
    this[Symbol.isConcatSpreadable] = true;
  }
}
class A2 extends Array {
  constructor(args) {
    super(args);
  }
  get [Symbol.isConcatSpreadable]() {
    return false;
  }
}
let a1 = new A1();
a1[0] = 3;
a1[1] = 4;
let a2 = new A2();
a2[0] = 5;
a2[1] = 6;
[1, 2].concat(a1).concat(a2);
// [1, 2, 3, 4, [5, 6]]

In the above code, the class A1 is expandable, and the class A2 is not expandable, so there are different results when using concat.

Note that the position difference of Symbol.isConcatSpreadable, A1 is defined on the instance, and A2 is defined on the class itself, the effect is the same.

Symbol.species

The Symbol.species property of the object points to a constructor. This property is used when creating derived objects.

class MyArray extends Array {}

const a = new MyArray(1, 2, 3);
const b = a.map((x) => x);
const c = a.filter((x) => x > 1);

b instanceof MyArray; // true
c instanceof MyArray; // true

In the above code, the subclass MyArray inherits the parent class Array, a is an instance of MyArray, and b and c are derived objects of a. You might think that both b and c are generated by calling the array method, so they should be arrays (instances of Array), but in fact they are also instances of MyArray.

The Symbol.species property is provided to solve this problem. Now, we can set the Symbol.species property for MyArray.

class MyArray extends Array {
  static get [Symbol.species]() {
    return Array;
  }
}

In the above code, since the Symbol.species property is defined, the function returned by this property will be used as the constructor when creating the derived object. This example also shows that to define the Symbol.species property, use the get valuer. The default Symbol.species property is equivalent to the following writing.

static get [Symbol.species]() {
  return this;
}

Now, let's look at the previous example again.

class MyArray extends Array {
  static get [Symbol.species]() {
    return Array;
  }
}

const a = new MyArray();
const b = a.map((x) => x);

b instanceof MyArray; // false
b instanceof Array; // true

In the above code, the derived object generated by a.map(x => x) is not an instance of MyArray, but directly an instance of Array.

Let's look at another example.

class T1 extends Promise {}

class T2 extends Promise {
  static get [Symbol.species]() {
    return Promise;
  }
}

new T1((r) => r()).then((v) => v) instanceof T1; // true
new T2((r) => r()).then((v) => v) instanceof T2; // false

In the above code, T2 defines the Symbol.species property, and T1 does not. As a result, when creating a derived object (the then method), T1 calls its own constructor, while T2 calls the Promise constructor.

In short, the function of Symbol.species is that when the instance object needs to call its own constructor again during the running process, it will call the constructor specified by the attribute. Its main purpose is that some class libraries are modified on the basis of the base class, so when the subclass uses inherited methods, the author may wish to return an instance of the base class instead of an instance of the subclass.

Symbol.match

The Symbol.match property of the object points to a function. When executing str.match(myObject), if the attribute exists, it will be called and the return value of the method will be returned.

String.prototype.match(regexp);
// Equivalent to
regexp[Symbol.match](this);

class MyMatcher {
  [Symbol.match](string) {
    return "hello world".indexOf(string);
  }
}

"e".match(new MyMatcher()); // 1

Symbol.replace

The Symbol.replace property of an object points to a method. When the object is called by the String.prototype.replace method, the return value of the method will be returned.

String.prototype.replace(searchValue, replaceValue);
// Equivalent to
searchValue[Symbol.replace](this, replaceValue);

Below is an example.

const x = {};
x[Symbol.replace] = (...s) => console.log(s);

"Hello".replace(x, "World"); // ["Hello", "World"]

The Symbol.replace method will receive two parameters. The first parameter is the object that the replace method is working on. The above example is Hello, and the second parameter is the replaced value. The above example is World .

The Symbol.search property of the object points to a method, when the object is called by the String.prototype.search method, the return value of the method will be returned.

String.prototype.search(regexp);
// Equivalent to
regexp[Symbol.search](this);

class MySearch {
  constructor(value) {
    this.value = value;
  }
  [Symbol.search](string) {
    return string.indexOf(this.value);
  }
}
"foobar".search(new MySearch("foo")); // 0

Symbol.split

The Symbol.split property of an object points to a method. When the object is called by the String.prototype.split method, the return value of the method will be returned.

String.prototype.split(separator, limit);
// Equivalent to
separator[Symbol.split](this, limit);

Below is an example.

class MySplitter {
  constructor(value) {
    this.value = value;
  }
  [Symbol.split](string) {
    let index = string.indexOf(this.value);
    if (index === -1) {
      return string;
    }
    return [string.substr(0, index), string.substr(index + this.value.length)];
  }
}

"foobar".split(new MySplitter("foo"));
// ['','bar']

"foobar".split(new MySplitter("bar"));
// ['foo','']

"foobar".split(new MySplitter("baz"));
//'foobar'

The above method uses the Symbol.split method to redefine the behavior of the split method of the string object.

Symbol.iterator

The Symbol.iterator property of the object points to the default iterator method of the object.

const myIterable = {};
myIterable[Symbol.iterator] = function* () {
  yield 1;
  yield 2;
  yield 3;
};

[...myIterable]; // [1, 2, 3]

When an object performs a for...of loop, it will call the Symbol.iterator method to return the object's default iterator. For details, please refer to the chapter "Iterator and for...of Loop".

class Collection {
  *[Symbol.iterator]() {
    let i = 0;
    while (this[i] !== undefined) {
      yield this[i];
      ++i;
    }
  }
}

let myCollection = new Collection();
myCollection[0] = 1;
myCollection[1] = 2;

for (let value of myCollection) {
  console.log(value);
}
// 1
// 2

Symbol.toPrimitive

The Symbol.toPrimitive property of the object points to a method. When the object is converted to a value of the original type, this method will be called to return the value of the original type corresponding to the object.

When Symbol.toPrimitive is called, it will accept a string parameter that represents the current operation mode. There are three modes.

-Number: This occasion needs to be converted to a number -String: This occasion needs to be converted into a string -Default: In this case, it can be converted into a numeric value or a string

let obj = {
  [Symbol.toPrimitive](hint) {
    switch (hint) {
      case "number":
        return 123;
      case "string":
        return "str";
      case "default":
        return "default";
      default:
        throw new Error();
    }
  },
};

2 * obj; // 246
3 + obj; // '3default'
obj == "default"; // true
String(obj); //'str'

Symbol.toStringTag

The Symbol.toStringTag property of the object points to a method. When calling the Object.prototype.toString method on the object, if this property exists, its return value will appear in the string returned by the toString method, indicating the type of the object. In other words, this property can be used to customize the string after object in [object Object] or [object Array].

// Example 1
({ [Symbol.toStringTag]: "Foo" }.toString());
// "[object Foo]"

// Example 2
class Collection {
  get [Symbol.toStringTag]() {
    return "xxx";
  }
}
let x = new Collection();
Object.prototype.toString.call(x); // "[object xxx]"

The value of the Symbol.toStringTag property of the new ES6 built-in object is as follows.

-JSON[Symbol.toStringTag]:'JSON' -Math[Symbol.toStringTag]:'Math' -Module object M[Symbol.toStringTag]:'Module' -ArrayBuffer.prototype[Symbol.toStringTag]:'ArrayBuffer' -DataView.prototype[Symbol.toStringTag]:'DataView' -Map.prototype[Symbol.toStringTag]:'Map' -Promise.prototype[Symbol.toStringTag]:'Promise' -Set.prototype[Symbol.toStringTag]:'Set' -%TypedArray%.prototype[Symbol.toStringTag]:'Uint8Array' etc. -WeakMap.prototype[Symbol.toStringTag]:'WeakMap' -WeakSet.prototype[Symbol.toStringTag]:'WeakSet' -%MapIteratorPrototype%[Symbol.toStringTag]:'Map Iterator' -%SetIteratorPrototype%[Symbol.toStringTag]:'Set Iterator' -%StringIteratorPrototype%[Symbol.toStringTag]:'String Iterator' -Symbol.prototype[Symbol.toStringTag]:'Symbol' -Generator.prototype[Symbol.toStringTag]:'Generator' -GeneratorFunction.prototype[Symbol.toStringTag]:'GeneratorFunction'

Symbol.unscopables

The Symbol.unscopables property of an object points to an object. This object specifies which attributes will be excluded by the with environment when the with keyword is used.

Array.prototype[Symbol.unscopables];
// {
// copyWithin: true,
// entries: true,
// fill: true,
// find: true,
// findIndex: true,
// includes: true,
// keys: true
//}

Object.keys(Array.prototype[Symbol.unscopables]);
// ['copyWithin','entries','fill','find','findIndex','includes','keys']

The above code shows that the array has 7 attributes, which will be excluded by the with command.

// when there are no unscopables
class MyClass {
  foo() {
    return 1;
  }
}

var foo = function () {
  return 2;
};

with (MyClass.prototype) {
  foo(); // 1
}

// when there are unscopables
class MyClass {
  foo() {
    return 1;
  }
  get [Symbol.unscopables]() {
    return { foo: true };
  }
}

var foo = function () {
  return 2;
};

with (MyClass.prototype) {
  foo(); // 2
}

The above code specifies the Symbol.unscopables property so that the with syntax block will not look for the foo property in the current scope, that is, foo will point to a variable in the outer scope.