# 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.length
Symbol
构造函数的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.iterator
Symbol.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.isConcatSpreadable
Symbol.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.replace
Symbol.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.search
Symbol.searc
h是一个预定义好的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.split
Symbol.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
的结果为false
symbol
接受一个字符串作为参数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
仍然是一个有用的功能。