发布时间:2023-02-06 16:00
Symbol的诞生,也就是Symbol存在的意义
之前我们的对象属性的数据类型都是字符串,没有其他的了。所以会导致属性名重复,导致属性值被覆盖的情况。比如,你使用了一个他人提供的对象,但又想为这个对象添加新的方法,在添加的操作就很容易覆盖了原有的方法。所以需要一个独一无二的数据类型来完成这个使命。所以Symbol出来主持大局了。
Symbol是ES6中引入的一种新的基本数据类型,用于表示一个独一无二的值。它是JavaScript中的第七种数据类型,与undefined、null、Number(数值)、String(字符串)、Boolean(布尔值)、Object(对象)并列。
你可以这样创建一个Symbol值:
const a = Symbol();
console.log(a); //Symbol()
使用Symbol函数可以生成一个Symbol类型的值,但是你不能在调用Symbol时使用new关键字,因为Symbol是基本数据类型,而不是对象。比如下面的写法是错误的:
//报错,Symbol is not a constructor
const a = new Symbol();
使用Symbol()创建一个Symbol类型的值并赋值给a变量后,你就得到了一个在内存中独一无二的值。
let a = Symbol('a');
let b = Symbol('a');
console.log(a == b) // fasle
尽管a和b都是使用Symbol()创建出来的,但是它们在内存中看起来却是这样的:
实际上,a变量拿到了内存中某块内存的唯一引用(这里所说的引用,其实就是该内存的地址)。如果不借助a变量,你不可能再得到这个地址。因此:
a !== b; //a和b持有的是两块内存的引用
const c = a; //手动把a里保存的地址保存在c变量中
a === c; //c和a现在指向同一块内存,因为它们保存了同样的地址
这种行为看似难以理解,但其实它与对象遵循相同的规则,如:
var a = {};
var b = {};
a !== b; //a和b各自被分配了不同的内存,因此它们保存了不同的地址
//借助变量a,变量c拿到了a指向的那个对象的地址,因此两者相等
var c = a;
a === c;
但是对于同为基本数据类型的字符串来说,它不遵循类似的规则。
比如:
var a = "123";
var b = "123";
a === b; //返回true。两者在常量区引用同一个字符串
我们首先通过变量a在内存中创建了字符串“123”,然后在不借助变量a的情况下,又通过var b = "123"拿到了对“123”这个字符串的引用,两者指向内存中的同一块内存地址。
因此我们说,a无法确保别的变量无法拿到它保存的地址(前提是不通过a)。但是对于var a = Symbol()这样的语句,a变量内保存的值是唯一的,因为除了借助a变量,你永远无法得到a中保存的值。这也是Symbol的本质。
可能很多人比较奇怪,一个Symbol类型的变量里面到底保存了什么呢?
我们看两行代码:
var a = Symbol();
console.log(a); //Symbol()
我们试图输出a的值,但js引擎输出了Symbol()。显然它不能说明a的值是字符串,因为:
typeof a === "symbol";
所以说如果你想问js引擎a的值是多少,引擎只会告诉你它是一个Symbol类型的值。也就是说,Symbol真正存储了什么并不重要,重要的是它的值永远不会与别的值相等。Symbol的中文释义为“标志,符号”,一个Symbol类型的变量只是为了标记一块唯一的内存而存在的。也正是因为这样,Symbol类型的值不参与运算。
有人会好奇Symbol( 'a')里面的参数a又是怎么回事呢?字符串a表示一种修饰,对你当前创建的Symbol类型的一种修饰,作为区分使用,否则当你创建多个Symbol数据时,容易混淆。
Symbol不能用四则运算进行操作,否则报错。它只能用显示的方式转为字符串和布尔值,即:String(Symbol())/ Boolean(Symbol())
作为对象属性时,要注意用以下三种方式来书写
let msMymbol = Symbol();
// 第一种写法
let a = {};
a{mySymbol} = 'Hello'
// 第二种写法
let a = {
[mySymbol]:'Hello';
}
// 第三种写法
let a = {}
Object.defineProperty(a,mySymbol,{value:'Hello'})
// 以上的都可以得到 a[mySymbol] // 'hello'
以上说了对象属性的创建,但是我们要格外的注意,Symbol作为属性名,该属性不会出现在for...in、for...of 循环中,也不会被Object.keys、Object.getOwnPropertyNames()、JSON.ownKeys(),这个方法就可以返回对象所有的属性,也就是字符串属性和Symbol属性。
但该属性并不是私有属性,它可以被专门的Object.getOwnPropertySymbols()方法遍历出来。该方法返回一个数组,包含了当前对象的所有用作属性名的Symbol值:
var s1 = Symbol('a');
var s2 = Symbol('b');
var a = {
name: "江左梅郎",
[s1]: 24,
[s2]: function(){}
}
var s = Object.getOwnPropertySymbols(a); //[Symbol(a), Symbol(b)]
a[s[0]] = 24; //返回的数组元素不是字符串,而是实际的Symbol值,
//因此可以通过它引用到对象的该属性
因此遍历该方法的返回值即可遍历所有的Symbol属性。
另外,ES6新增的Reflect.ownKeys()方法可以遍历出所有的常规键名和Symbol键名。语法为:
Reflect.ownKeys(a); //["name", Symbol(a), Symbol(b)]
有时,我们希望重新使用同一个 Symbol 值,以上我们都说了 Symbol 数据类型唯一的,所以只要用 Symbol() 方法创建的 Symbol 类型是无法实现的。所以我们都可以用Symbol.for() 这个方法来实现的。
let s1 = Symbol.for('foo');
let s2 = Symbol.for('foo');
console.log(s1 === s2) // true
注意:这里的Symbol.for() 和 Symbol() 创建的都是Symbol 类型,但是他们的创建机制有所不同,Symbol.for('a') 的创建方式会在创建之前在全局中寻找,有没有用Symbol.for() 的方式,并且key是 ’a' 的字符串创建了 Symbol类型(创建了就会在全局中登记);如果有则不重复创建(已登记的)。然而Symbol('a') 的创建是不会去检索全局的,是直接创建一个新的Symbol类型。这也是用Symbol('a') 创建的两个 Symbol 类型不相等的根本原因。
Symbol.keyFor() 方法返回一个已登记的 Symbol 类型值的 key
let s1 = Symbol.for('foo');
console.log(Symbol.keyFor(s1)) // "foo"
let s2 = Symbol('foo');
console.log(Symbol.keyFor(s2)) // undefined
上面代码中,变量s2 属于未登记的Symbol值,所以返回undefined。也就是Symbol.keyFor() 这个方法,主要服务于Symbol.for() 的。因为Symbol() 方法创建的值用Symbol.keyFor() 永远是undefined