# Symbol用法
# 前言
- 前端做了这么多年,当面试官问到
 symbol这个类型可以简单说下吗?尴尬的答复到是一种新型的数据类型,主要保证数据的唯一性- 基于上面的说法太过于单薄,于是乎写这篇文章也是一个总结报告,深入学习
 symbol到底是干嘛的
# symbol的由来
Symbol 的设计初衷是为了创建一种独一无二的标识符,以防止属性名冲突。在 JavaScript 中,对象的属性名是字符串,如果两个对象使用相同的字符串作为属性名,就会导致冲突。为了避免这种冲突,Symbol 提供了一种生成唯一标识符的机制。
通过这个由来我们带大家建立Symbol的第一印象,那就是symbol是用来解决对象属性名命名冲突的
# symbol的特点
这里先说一下Symbol的特点:
- 唯一性:每个 
Symbol值都是唯一的,没有两个Symbol值是相等的。- 注意
Symbol不支持new Symbol(),它他并不是完整的构造函数 - 每个从
Symbol()返回的symbol值都是唯一的。一个symbol值能作为对象属性的标识符;这是该数据类型仅有的目的 
 - 注意
 - 不可变性:
Symbol值是不可变的,一旦创建就不能修改。 - 私有性:
Symbol值在全局范围内是唯一可见的,无法通过遍历对象的方式获取到Symbol类型的属性。因为这个特点,我们还可以建立对象的私有属性,这样不能通过对象遍历出来,有助于保护对象的实现细节 
# symbol的属性和常用方法
定义与使用:
在JavaScript中,可以使用Symbol()函数来创建一个符号,如下所示:
const mySymbol = Symbol();
 Symbol函数可以接受一个描述性字符串作为参数,用于标识符号的含义,如下所示:
const mySymbol = Symbol('my symbol');
 需要注意的是,每个Symbol()函数调用都会返回一个唯一的符号,即使描述性字符串相同,它们也是不同的符号。
Symbol类型的值可以用作对象的属性名,如下所示:
const mySymbol = Symbol('my symbol');
const myObject = {
  [mySymbol]: 'hello'
};
console.log(myObject[mySymbol]);  // 输出:'hello'
 2
3
4
5
在上面的代码中,我们使用符号mySymbol作为对象myObject的属性名,并将其值设置为'hello'。使用符号作为属性名的好处是它们不会与其他属性名冲突,并且对外不可见,因此可以用于实现私有属性或方法等场景。
Symbol.lengthSymbol构造函数的length属性值为0。
console.log(Symbol.length); // 0
 另外,JavaScript中的Symbol类型有两个特殊的方法Symbol.for()和Symbol.keyFor(),用于创建全局符号和获取已经存在的全局符号。
Symbol.for(): 用于创建或获取一个全局符号,如果全局符号已经存在,则返回已经存在的符号,否则创建一个新的全局符号。例如:
const mySymbol = Symbol.for('my symbol');
const sameSymbol = Symbol.for('my symbol');
console.log(mySymbol === sameSymbol);  // 输出:true
 2
3
4
在上面的代码中,我们使用
Symbol.for()方法来创建一个全局符号'my symbol',并将其赋值给mySymbol变量。然后,我们再次使用Symbol.for()方法来获取同一个全局符号,赋值给sameSymbol变量。由于全局符号已经存在,因此sameSymbol变量的值等于mySymbol变量的值,输出true。
Symbol.keyFor()方法会返回一个已经存在的Symbol值的key。如果给定的Symbol值不存在于全局Symbol注册表中,则返回undefined。
const symbol1 = Symbol.for('foo');
const key1 = Symbol.keyFor(symbol1);
const symbol2 = Symbol('bar');
const key2 = Symbol.keyFor(symbol2);
console.log(key1); // 'foo'
console.log(key2); // undefined
 2
3
4
5
6
7
8
使用场景: 当我们需要获取一个全局唯一的Symbol值的key时,可以使用Symbol.keyFor()方法。但需要注意的是,只有在该Symbol值被注册到全局Symbol注册表中时,才能使用Symbol.keyFor()方法获取到其key。
Symbol.iteratorSymbol.iterator是一个预定义好的Symbol值,表示对象的默认迭代器方法。该方法返回一个迭代器对象,可以用于遍历该对象的所有可遍历属性。
const obj = { a: 1, b: 2 };
for (const key of Object.keys(obj)) {
  console.log(key);
}
// Output:
// 'a'
// 'b'
for (const key of Object.getOwnPropertyNames(obj)) {
  console.log(key);
}
// Output:
// 'a'
// 'b'
for (const key of Object.getOwnPropertySymbols(obj)) {
  console.log(key);
}
// Output: 
// No output
obj[Symbol.iterator] = function* () {
  for (const key of Object.keys(this)) {
    yield key;
  }
}
for (const key of obj) {
  console.log(key);
}
// Output:
// 'a'
// 'b'
 2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
使用场景: 当我们需要自定义一个对象的迭代行为时,可以通过定义Symbol.iterator属性来实现。例如,对于自定义的数据结构,我们可以定义它的Symbol.iterator方法以便能够使用for...of语句进行遍历。
Symbol.isConcatSpreadableSymbol.isConcatSpreadable是一个预定义好的Symbol值,用于定义对象在使用concat()方法时的展开行为。如果一个对象的Symbol.isConcatSpreadable属性为false,则在调用concat()方法时,该对象不会被展开。
const arr1 = [1, 2];
const arr2 = [3, 4];
const obj = { length: 2, 0: 5, 1: 6, [Symbol.isConcatSpreadable]: false };
console.log(arr1.concat(arr2)); // [1, 2, 3, 4]
console.log(arr1.concat(obj)); // [1, 2, { length: 2, 0: 5, 1: 6, [Symbol(Symbol.isConcatSpreadable)]: false }]
 2
3
4
5
6
Symbol.toPrimitive
Symbol.toPrimitive是一个预定义好的Symbol值,用于定义对象在被强制类型转换时的行为。如果一个对象定义了Symbol.toPrimitive方法,则在将该对象转换为原始值时,会调用该方法。
const obj = {
  valueOf() {
    return 1;
  },
  [Symbol.toPrimitive](hint) {
    if (hint === 'number') {
      return 2;
    } else if (hint === 'string') {
      return 'foo';
    } else {
      return 'default';
    }
  }
};
console.log(+obj); // 2
console.log(`${obj}`); // 'foo'
console.log(obj + ''); // 'default'
 2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Symbol.prototype.valueOf()Symbol.prototype.valueOf()方法会返回Symbol值本身。
const symbol = Symbol('foo');
console.log(symbol.valueOf()); // Symbol(foo)
使用场景: 当我们需要获取一个Symbol值本身时,可以使用Symbol.prototype.valueOf()方法。
 2
3
4
5
Symbol.replaceSymbol.replace是一个预定义好的Symbol值,用于定义对象在调用String.prototype.replace()方法时的行为。如果一个对象定义了Symbol.replace方法,则在调用该对象的replace()方法时,会调用该方法进行替换。
class Foo {
  [Symbol.replace](str, replacement) {
    return str.replace('foo', replacement);
  }
}
console.log('foobar'.replace(new Foo(), 'baz')); // 'bazbar'
console.log('barbaz'.replace(new Foo(), 'baz')); // 'barbaz'
使用场景: 当我们需要自定义一个对象在调用String.prototype.replace()方法时的行为时,可以通过定义Symbol.replace方法来实现。
 2
3
4
5
6
7
8
9
10
Symbol.searchSymbol.search是一个预定义好的Symbol值,用于定义对象在调用String.prototype.search()方法时的行为。如果一个对象定义了Symbol.search
class Foo {
  [Symbol.search](str) {
    return str.indexOf('foo');
  }
}
console.log('foobar'.search(new Foo())); // 0
console.log('barbaz'.search(new Foo())); // -1
使用场景: 当我们需要自定义一个对象在调用String.prototype.search()方法时的行为时,可以通过定义Symbol.search方法来实现。
 2
3
4
5
6
7
8
9
10
Symbol.splitSymbol.split是一个预定义好的Symbol值,用于定义对象在调用String.prototype.split()方法时的行为。如果一个对象定义了Symbol.split方法,则在调用该对象的split()方法时,会调用该方法进行分割。
class Foo {
  [Symbol.split](str) {
    return str.split(' ');
  }
}
console.log('foo bar baz'.split(new Foo())); // ['foo', 'bar', 'baz']
console.log('foobarbaz'.split(new Foo())); // ['foobarbaz']
使用场景: 当我们需要自定义一个对象在调用String.prototype.split()方法时的行为时,可以通过定义Symbol.split方法来实现。
 2
3
4
5
6
7
8
9
10
Symbol.prototype.toString()Symbol.prototype.toString()方法会返回Symbol值的字符串表示形式,该表示形式包含Symbol()函数创建时指定的描述信息。
const symbol = Symbol('foo');
console.log(symbol.toString()); // 'Symbol(foo)'
使用场景: 当我们需要将一个Symbol值转换成字符串时,可以使用Symbol.prototype.toString()方法。
 2
3
4
5
# Symbol的重要属性
1. Symbol.iterator: 用于指定对象的默认迭代器,例如:
const myObject = {
  *[Symbol.iterator]() {
    yield 1;
    yield 2;
    yield 3;
  }
};
for (const value of myObject) {
  console.log(value);
}
// 输出:1 2 3
 2
3
4
5
6
7
8
9
10
11
12
在上面的代码中,我们为
myObject对象设置了Symbol.iterator符号,并指定了一个生成器函数作为迭代器的实现。然后,我们可以使用for...of循环迭代myObject对象,并输出其中的值。
2. Symbol.hasInstance: 用于定义一个对象是否为某个构造函数的实例。
class MyClass {
  static [Symbol.hasInstance](obj) {
    return obj instanceof Array;
  }
}
console.log([] instanceof MyClass);  // 输出:true
console.log({} instanceof MyClass);  // 输出:false
 2
3
4
5
6
7
8
- 在上面的代码中,我们定义了一个
 MyClass类,并使用Symbol.hasInstance方法自定义了instanceof运算符的行为,使其检查对象是否为数组。当检查[]对象时,instanceof运算符返回true,因为[]是Array的实例;当检查{}对象时,instanceof运算符返回false,因为{}不是Array的实例。- 需要注意的是,
 Symbol.hasInstance方法是一个静态方法,需要定义在构造函数的静态属性中。另外,Symbol.hasInstance方法不能被继承,因此子类需要重新定义该方法。
3. Symbol.toStringTag: 用于自定义对象的默认字符串描述。
当调用Object.prototype.toString()方法时,会使用该对象的Symbol.toStringTag属性作为默认的字符串描述,例如:
class MyObject {
  get [Symbol.toStringTag]() {
    return 'MyObject';
  }
}
const obj = new MyObject();
console.log(Object.prototype.toString.call(obj));  // 输出:'[object MyObject]'
 2
3
4
5
6
7
8
- 在上面的代码中,我们定义了一个
 MyObject类,并使用Symbol.toStringTag属性自定义了该类的默认字符串描述。然后,我们创建了一个obj对象,并使用Object.prototype.toString()方法获取其字符串描述,输出'[object MyObject]'。- 需要注意的是,
 Symbol.toStringTag属性只有在调用Object.prototype.toString()方法时才会生效,对其他方法没有影响。另外,如果没有定义Symbol.toStringTag属性,则默认使用构造函数的名称作为字符串描述。
4. Symbol.asyncIterator: 用于指定对象的默认异步迭代器。
当使用for await...of循环迭代一个对象时,会调用该对象的Symbol.asyncIterator方法获取异步迭代器。
Symbol.asyncIterator方法需要返回一个异步迭代器对象,该对象实现了next()方法,并返回一个Promise对象。当迭代器迭代到结束时,next()方法应该返回一个Promise对象,该Promise对象的value属性为undefined,done属性为true。
例如,下面的代码演示了如何使用Symbol.asyncIterator属性定义一个异步迭代器:
const myObject = {
  async *[Symbol.asyncIterator]() {
    yield Promise.resolve(1);
    yield Promise.resolve(2);
    yield Promise.resolve(3);
  }
};
(async function() {
  for await (const value of myObject) {
    console.log(value);
  }
})();
// 输出:1 2 3
 2
3
4
5
6
7
8
9
10
11
12
13
14
- 在上面的代码中,我们为
 myObject对象设置了Symbol.asyncIterator符号,并指定了一个异步生成器函数作为异步迭代器的实现。然后,我们使用for await...of循环迭代myObject对象,并输出其中的值。- 需要注意的是,使用
 Symbol.asyncIterator属性定义的异步迭代器只能使用for await...of循环进行迭代,不能使用普通的for...of循环。此外,Symbol.asyncIterator属性只有在支持异步迭代器的环境中才能使用,例如Node.js的版本必须在10.0.0以上才支持异步迭代器。
# symbol的应用场景
# 使用场景
- 解决属性名称冲突:
 
当你在开发一个库或框架时,为了避免属性名冲突,可以使用 Symbol 作为对象的属性名。这样可以确保属性名的唯一性,例如:
const PRIVATE_KEY = Symbol('private');
const obj = {
  [PRIVATE_KEY]: 'private value',
};
console.log(obj[PRIVATE_KEY]); // 访问私有属性
 2
3
4
5
6
7
- 创建私有方法和属性:
 
Symbol 可以用于创建类中的私有方法和属性,以实现封装和隐藏内部实现细节,例如:
const PRIVATE_METHOD = Symbol('privateMethod');
class MyClass {
  constructor() {
    this[PRIVATE_METHOD] = function() {
      // 私有方法实现
    };
  }
  publicMethod() {
    // 公共方法调用私有方法
    this[PRIVATE_METHOD]();
  }
}
 2
3
4
5
6
7
8
9
10
11
12
13
14
外面是访问不到PRIVATE_METHOD属性的,只能通过publicMethod方法才能访问到
- 定义常量: 可能有这样的场景,我们开发的大型项目里面有很多常量,但是有可能常量名会冲突,这时候也可以使用
Symbol来解决此类问题 
const STATUS_SUCCESS = Symbol('success');
const STATUS_ERROR = Symbol('error');
function handleResponse(response) {
  if (response.status === STATUS_SUCCESS) {
    // 处理成功情况
  } else if (response.status === STATUS_ERROR) {
    // 处理错误情况
  }
}
 2
3
4
5
6
7
8
9
10
- 自定义迭代器
 
# 实现symbol
有的人觉得,为什么要实现symbol, 有什么好处?
这个主要是锻炼我们实现功能的能力,要知道我们的js无所不能的,只有多锻炼,才能早点达到无所不能的能力
因为使用原生js并不能完全实现Symbol,所以我们主要实现了一下功能
symbol不能使用new命令instanceof的结果为falsesymbol接受一个字符串作为参数symbol值是唯一的,因为两个对象值不一样- 调用
toString方法,输出的值也是唯一的 
(function () {
  var root = this;
  var generateName = (function () {
    var postfix = 0;
    return function (descString) {
      postfix++;
      return '@@' + descString + '_' + postfix;
    }
  })()
  var SymbolPolyfill = function Symbol(description) {
    if (this instanceof SymbolPolyfill) throw new TypeError('Symbol is not a constructor');
    var descString = description === undefined ? undefined : String(description);
    var symbol = Object.create({
      toString: function() {
        return this.__Name__;
      },
    });
    Object.defineProperties(symbol, {
      '__Description__': {
        value: descString,
        writable: false,
        enumrable: false,
        configurable: false,
      },
      '__Name__': {
        value: generateName(descString),
        writable: false,
        enumerable: false,
        configurable: false
      }
    })
    return symbol; 
  }
  root.SymbolPolyfill = SymbolPolyfill;
})();
 2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
然后我们测试一下
const a = SymbolPolyfill('foo');
const b = SymbolPolyfill('foo');
console.log(a===b);
var o = {};
o[a] = 'hello';
o[b] = 'hi';
console.log(o); // Object { "@@foo_1": "hello", "@@foo_2": "hi" }
 2
3
4
5
6
7
8
9
# 总结
上面学习完了symbol,接下来我们吐槽一下,难道大家没有感觉Symbol很鸡肋么?
- 可读性差:由于 
Symbol是一种唯一且不可变的值,它的值在代码中不易理解和追踪。相比起字符串或其他基本类型的属性名,Symbol 的用途和含义可能更加隐晦。 - 难以扩展:
Symbol的唯一性可能导致扩展困难。如果库或框架使用Symbol定义了一些内部属性或方法,但用户想要在其上进行自定义扩展时,可能会受限于无法直接访问和修改Symbol属性。 - 兼容性问题:旧版的 
JavaScript引擎可能不支持Symbol或只支持部分功能,这可能导致在一些特定环境中使用Symbol的代码无法正常运行。 - 额外的复杂性:引入 
Symbol可能会增加代码的复杂性。使用Symbol的场景通常是在一些高级、复杂的应用中,对于一般的开发需求来说,它可能会增加学习成本和开发复杂性,而并非每个项目都需要使用Symbol。 
尽管 Symbol 有一些限制和问题,但它仍然具有一些有价值的应用场景,特别是在构建库、框架或需要确保属性名唯一性和隐藏性的高级应用中。它可以提供一种确保标识符的独特性和不可变性的机制。因此,对于特定的用途和需求,Symbol 仍然是一个有用的功能。