一文理解Symbol应用场景---近万字总结
前言
做了很多年的前端了,但是依然对一个基础类型比较陌生,那就是symbol
,有一次面试突然被面试官问道symbol
的应用场景,我顿时哑口无言了,因为咱确实没有在实际场景中用过,也不知道这个基础类型到底用来干嘛呢
我写这篇文章就希望大家能够看懂,并且真正的能根据自己的应用场景用起来
symbol的由来
Symbol
的设计初衷是为了创建一种独一无二的标识符,以防止属性名冲突。在 JavaScript
中,对象的属性名是字符串,如果两个对象的属性使用相同的字符串作为属性名,就会导致冲突。为了避免这种冲突,Symbol
提供了一种生成唯一标识符的机制。
通过这个由来我们带大家建立symbol的第一印象,那就是symbol是用来解决对象属性名命名冲突的
symbol的特点
这里先说一下Symbol
的特点
- 唯一性:每个
Symbol
值都是唯一的,没有两个Symbol
值是相等的。- 注意Symbol不支持"new Symbol()",他并不是完整的构造函数
- 每个从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'
在上面的代码中,我们使用符号mySymbol
作为对象myObject
的属性名,并将其值设置为'hello'
。使用符号作为属性名的好处是它们不会与其他属性名冲突,并且对外不可见,因此可以用于实现私有属性或方法等场景。
另外,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
在上面的代码中,我们使用Symbol.for()
方法来创建一个全局符号'my symbol'
,并将其赋值给mySymbol
变量。然后,我们再次使用Symbol.for()
方法来获取同一个全局符号,赋值给sameSymbol
变量。由于全局符号已经存在,因此sameSymbol
变量的值等于mySymbol
变量的值,输出true
。
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
在上面的代码中,我们为myObject
对象设置了Symbol.iterator
符号,并指定了一个生成器函数作为迭代器的实现。然后,我们可以使用for...of
循环迭代myObject
对象,并输出其中的值。
2. Symbol.hasInstance
: 用于定义一个对象是否为某个构造函数的实例。
Symbol.hasInstance
方法接受一个参数,表示要检查的对象。该方法需要返回一个布尔值,表示该对象是否为该构造函数的实例。例如:
class MyClass {
static [Symbol.hasInstance](obj) {
return obj instanceof Array;
}
}
console.log([] instanceof MyClass); // 输出:true
console.log({} instanceof MyClass); // 输出:false
在上面的代码中,我们定义了一个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]'
在上面的代码中,我们定义了一个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属性定义一个异步迭代器:
jsx
复制代码
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
在上面的代码中,我们为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]); // 访问私有属性
- 创建私有方法和属性:
Symbol 可以用于创建类中的私有方法和属性,以实现封装和隐藏内部实现细节,例如:
const PRIVATE_METHOD = Symbol('privateMethod');
class MyClass {
constructor() {
this[PRIVATE_METHOD] = function() {
// 私有方法实现
};
}
publicMethod() {
// 公共方法调用私有方法
this[PRIVATE_METHOD]();
}
}
外面是访问不到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) {
// 处理错误情况
}
}
- 自定义迭代器
实现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;
})();
然后我们测试一下
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" }
总结
上面学习完了symbol,接下来我们吐槽一下,难道大家没有感觉Symbol
很鸡肋么?
- 可读性差:由于 Symbol 是一种唯一且不可变的值,它的值在代码中不易理解和追踪。相比起字符串或其他基本类型的属性名,Symbol 的用途和含义可能更加隐晦。
- 难以扩展:Symbol 的唯一性可能导致扩展困难。如果库或框架使用 Symbol 定义了一些内部属性或方法,但用户想要在其上进行自定义扩展时,可能会受限于无法直接访问和修改 Symbol 属性。
- 兼容性问题:旧版的 JavaScript 引擎可能不支持 Symbol 或只支持部分功能,这可能导致在一些特定环境中使用 Symbol 的代码无法正常运行。
- 额外的复杂性:引入 Symbol 可能会增加代码的复杂性。使用 Symbol 的场景通常是在一些高级、复杂的应用中,对于一般的开发需求来说,它可能会增加学习成本和开发复杂性,而并非每个项目都需要使用 Symbol。
尽管 Symbol 有一些限制和问题,但它仍然具有一些有价值的应用场景,特别是在构建库、框架或需要确保属性名唯一性和隐藏性的高级应用中。它可以提供一种确保标识符的独特性和不可变性的机制。因此,对于特定的用途和需求,Symbol 仍然是一个有用的功能。
转载自:https://juejin.cn/post/7239715208792457272