Symbol 类型

在使用别人提供的对象时,在拿到这个对象,然后想往里面添加新的属性名,为了防止和别人的属性名重复,这样是会发生冲突的,对象的属性名必须要求是独一无二的,因此,ES6 中加入了 Symbol 数据类型,确保对象的属性名 独一无二 Symbol 实际上是引入的一种原始数据类型

// ES6的symbol
let sym = Symbol("addr");
console.log(sym);
console.log(typeof sym);

Symbol 函数不是一个构造函数,前面不能用 new 操作符,所以 Symbol 类型的值也不是对象,不能添加任何属性,只是类似于字符型的数据类型,强加 new 操作符会报错!


Symbol 值是唯一的,这是 Symbol 变量最大的特点。通过同样的方式创建的两个变量,即使参数也一样,这两个 Symbol 变量也是不相等的。

let x = Symbol();
let y = Symbol();

console.info(x === x);  // true
console.info(x === y);  // false


symbols 重要的用途,就是用作对象的 key。

const obj = {};
const sym = Symbol();
obj[sym] = 'foo';
obj.bar = 'bar';

console.log(obj);  // { bar: 'bar' }
console.log(sym in obj);   // true
console.log(obj[sym]);   // foo
console.log(Object.keys(obj));   // ['bar']

我们注意到使用 Object.keys() 并没有返回 symbols,这是为了向后兼容性的考虑。老代码不兼容 symbols,因此古老的 Object.keys() 不应该返回 symbols。


Symbol函数的参数

Symbol 函数还可以接受一个字符参数,来对产生的 Symbol 值进行描述,方便我们区分不同的 Symbol值

let symName = Symbol('test');
let symAddr = Symbol('test');
console.log('symName',symName);
console.log('symAddr',symAddr);
console.log('symName == symAddr?',symName == symAddr);
let sAddr = Symbol('symAddr');
console.log('sAddr == symAddr?',sAddr == symAddr);

如果创建 Symbol 值时传入的是对象,那么会先调用对象的 toString 方法得到一个字符串,再将结果字符串作为参数生成一个 Symbol 值。所以,Symbol 函数的参数只能是字符串。

const obj = {
  a: "a",
  b: "b",
  toString() {
    return "xx";
  },
};
console.info(Symbol(obj)); // Symbol(xx)

如果创建 Symbol 值时传入的是对象,没有定义 toString 方法:

const obj = {
  a: "a",
  b: "b",
};

console.info(Symbol(obj)); // Symbol([object Object])


Symbol 值不可以进行运算

Symbol 是一种数据类型,但它与 Number 以及 String 类型不同,不能参与运算,Symbol 值可以转化为字符串或布尔值,但是不能转为数值。

let sym1 = Symbol('sym1');
let sym2 = Symbol('sym2');
//转为字符串
console.log(String(sym1));
//转为布尔值
console.log(Boolean(sym2));
// 直接进行运算
console.log('sym1+sym2=?',sym1+sym2);


Symbol 作为属性

Symbol 就是用于属性去创造的,下面是几种 Symbol 作为属性的写法

//写法一:
let obj = {}; //创建一个对象
 let sym = Symbol(); 
obj[sym] = '湖南省长沙市';
//取属性值
console.log(obj[sym]);
// 写法二
let sym = Symbol(); 
let obj = {
    [sym]:'安徽省芜湖市'
}; 
console.log(obj[sym]);
//写法三
let obj = {}
let sym = Symbol()
Object.defineProperty(obj,sym,{value:'小陈同学'});
console.log(obj[sym]);

Symbol 值作为属性名时,获取相应的属性值不能用作点运算符,如果点运算符来给对象的属性赋 Symbol 类型的值,实际上属性名会变成字符串,而不是 Symbol 值,在对象内部,使用 Symbol 值定义属性时,Symbol 值必须放在方括号内,否则就是一个字符串。


getOwnPropertySymbols()

通常遍历对象,会有 for...in , for...of Object.keys() Object.getOwnPropertyNames() 这些方法,但是,这些方法都不能获取到对象的 Symbol 值的属性名。那是不是在外部就没有办法获取 Symbol 值的属性名了呢?办法当然还是有的。

Object.getOwnPropertySymbols() 就可以返回对象中的所有类型为 Symbol 值的属性名,得到一个元素类型为 symbol 的数组。除此之外,还有一个方法 Reflect.ownKeys() 可以返回包含 Symbol 属性名的所有属性名。

const obj = {
  a: 1,
  [Symbol()]: 2,
};

console.info("obj", Object.getOwnPropertyNames(obj));   //  [ 'a' ]
console.info("obj", Object.getOwnPropertySymbols(obj));  //  [ Symbol() ]
console.info("obj", Reflect.ownKeys(obj));   //  [ 'a', Symbol() ]
var obj={
  [Symbol('name')]:'like',
  [Symbol('age')]:'18',
  [Symbol()]:'play dou dou',
  sex:'male'
}

obj.sex  // male

//获取不到
obj[Symbol('name')]  //undefined
obj[Symbol('age')]  //undefined
obj[Symbol()]  //undefined

//for 检测不到
for(var i in obj){
  console.log(obj[i])
}
// 输出结果只有:male
let name = Symbol('name');
let age = Symbol('age');
let hobby = Symbol('hobby');

var obj={
  [name]:'like',
  [age]:'18',
  [hobby]:'play dou dou',
  sex:'male'
}

obj[name]  //like
obj[age]  //18

//仍然遍历不到
for(var i in obj){
  console.log(obj[i])
}
// 输出结果只有:male

//Object.getOwnPropertySymbols(obj) 可以遍历到
let symbols = Object.getOwnPropertySymbols(obj)
for(var i in symbols){
  console.log(obj[symbols[i]])
}
/*遍历结果:
like
18
play dou dou
*/


Symbol.for()、Symbol.keyFor()

Symbol 类型的变量除了自身,不会等于其他任何变量,但是依然存在特例。区别于通过 Symbol() 函数创建 Symbol 变量,我们还可以通过 Symbol.for() 方法来创建 Symbol 变量,该方法可以传入参数,而且在传入参数相同的时候,创建的 Symbol 也是相等的。

const a = Symbol.for("aa");
const b = Symbol.for("aa");
console.info("a===b", a === b);  // a===b true
console.info("type", typeof a);  // type symbol

Symbol 类型的 a 和 b ,通过 === 运算可以得到 true 。这是为什么呢?

因为 Symbol.for() 其实是带有类似重用机制的,具体的说,就是通过 Symbol.for() 创建变量时,传入的参数(假设为 x)会作为 Symbol 变量的 key ,然后到全局中搜索,是否已经有相同 key 的 Symbol 变量,如果存在,则直接返回这个 Symbol 变量。如果没有,才会创建一个 key 为传入参数 x 的 Symbol 变量,并将这个变量写到全局,供下次创建时被搜索。

通过 Symbol.for() 创建的 Symbol 变量,传入的参数是否相等决定得到的 Symbol 变量是否相等。

分析上面的代码,通过 Symbol.for("aa") 创建变量并赋值给 a 的时候,会在全局检索是否有 key 为'aa'的 Symbol 变量,这里显然是没有的,所有得到一个全新的 Symbol 变量,其带有的 key 是'aa',并且变量被写到全局。然后执行 const b = Symbol.for("aa"); 时,Symbol.for('aa')会找到之前的 key 为'aa'的 Symbol 变量,并赋值给 b ,所以,a 和 b 实际上是同一个 Symbol 变量。此时 Symbol.for("aa")不管执行多少次,得到的都是同一个 Symbol 变量。

既然通过 Symbol.for() 创建的 Symbol 变量的 key 这么重要,那我们怎么获取到这个 key 呢,那就要 Symbol.keyFor() 方法了,该函数会返回一个已经写到全局的 Symbol 变量的 key 值。这样获取到 Symbol 变量的 key,就可以创建一个和原 Symbol 变量相等的变量了。


内置的 Symbol 值

  • Symbol.hasInstance :对象的 Symbol.hasInstance 属性指向一个内部方法,当其他对象使用 instance 运算符,判断是否为该对象的实例时,会调用这个方法。
  • Symbol.isConcatSpreadable :布尔值,表示该对象用于 Array.prototype.concat()时,是否可以展开。
  • Symbol.species :对象的 Symbol.species 属性,指向一个构造函数。创建衍生对象时,会使用该属性定义 Symbol.species 属性要采用 get 取值器。
  • Symbol.match :指向一个函数。当执行 str.match(myObject)时,如果该属性存在,会调用他,返回该方法的返回值。
  • Symbol.replace :指向一个方法,当该对象被 String.prototype.replace 方法调用时,会返回该方法的返回值。
  • Symbol.search :指向一个方法,当该对象被 String.prototype.search 方法调用时,会返回该方法的返回值。
  • Symbol.split :指向一个方法,当该对象被 String.prototype.split 方法调用时,会返回该方法的返回值。
  • Symbol.iterator :对象的 Symbol.iterator 属性,指向该对象的默认遍历器方法。
  • Symbol.toPrimitive :指向一个方法,当该对象被转化为原始类型的值时,会返回该方法的返回值。被调用时,会接受一个字符串参数,表示当前运算的模式:Number,String,Default。
  • Symbol.toStringTag :指向一个方法,当该对象被 String.prototype.toString 方法调用时,可以用来定制[object xxx]中 object 后面的那个字符串。
  • Symbol.unscopables :指向一个对象,该对象指定了使用 with 关键字时,那些属性会被 with 环境排除(with 语句的作用是将代码的作用域设置到一个特定的作用域中)。