JavaScript内核与高级编程之:`JavaScript`的`Symbol`类型:其在`Object`属性中的独特性。

各位观众老爷,晚上好!我是你们的老朋友,今天咱唠唠嗑,说说JavaScript里一个有点神秘又有点意思的家伙:Symbol

说它神秘,是因为很多人觉得这玩意儿不常用,不知道有啥用;说它有意思,是因为它确实能解决一些实际问题,让你的代码更优雅,更安全。

咱们今天就来扒一扒Symbol的底裤,看看它到底是个什么玩意儿,以及它在Object属性里那些独一无二的骚操作。

一、Symbol是啥?别跟我扯概念,说人话!

咱们先抛开那些官方的、晦涩难懂的定义。 简单来说,Symbol就是一种唯一的标识符。 注意,是唯一的! 这玩意儿创建出来就跟身份证一样,独一无二,谁也别想冒充。

以前我们用字符串来表示对象的属性名,比如 obj.name = "张三"。 但是,字符串有个问题,就是容易冲突。 如果两个库都想给同一个对象添加一个 name 属性,那就完犊子了,后面的会覆盖前面的。

Symbol 的出现就是为了解决这个问题。 它保证了即使你用相同的描述创建两个 Symbol,它们也是不同的。

二、Symbol 怎么用?来点代码!

创建 Symbol 很简单,只需要调用 Symbol() 函数就行了。

const mySymbol = Symbol(); // 创建一个 Symbol
const anotherSymbol = Symbol(); // 创建另一个 Symbol

console.log(mySymbol === anotherSymbol); // 输出:false  (它们是不一样的!)

看到了吧? 即使你没给 Symbol() 传任何参数,创建出来的两个 Symbol 也是不一样的。

你也可以给 Symbol() 传一个字符串作为描述,方便调试的时候区分。

const symbolWithDescription = Symbol("这是一个描述");

console.log(symbolWithDescription.description); // 输出:"这是一个描述"

注意,这个描述仅仅是方便你调试用的,并不会影响 Symbol 的唯一性。

三、Symbol 在 Object 属性里的骚操作

重点来了! Symbol 最重要的用途之一就是作为对象的属性名。 而且,用 Symbol 作为属性名,能保证这个属性不会被意外访问到。

const myObj = {};

const mySymbol = Symbol("秘密属性");

myObj[mySymbol] = "不能说的秘密";

console.log(myObj[mySymbol]); // 输出:"不能说的秘密"

// 尝试用字符串访问
console.log(myObj["秘密属性"]); // 输出:undefined

看到了没? 我们用 Symbol 作为属性名,只能用 Symbol 本身才能访问到这个属性。 用字符串访问是无效的。 这就像给你的秘密文件加了一把锁,只有你知道钥匙才能打开。

四、Symbol 属性的隐藏与发现

Symbol 属性在一些场景下会被隐藏起来,比如用 for...in 循环和 Object.keys() 方法。

const myObj = {};
const symbolKey = Symbol("symbolKey");
myObj.name = "张三";
myObj[symbolKey] = "李四";

console.log(Object.keys(myObj)); // 输出:["name"]  (Symbol 属性被忽略了)

for (let key in myObj) {
  console.log(key); // 只会输出 "name"
}

但是,Symbol 属性也不是完全无法访问。 你可以用 Object.getOwnPropertySymbols() 方法来获取对象的所有 Symbol 属性。

const myObj = {};
const symbolKey = Symbol("symbolKey");
myObj.name = "张三";
myObj[symbolKey] = "李四";

const symbolKeys = Object.getOwnPropertySymbols(myObj);

console.log(symbolKeys); // 输出:[Symbol(symbolKey)]

console.log(myObj[symbolKeys[0]]); // 输出:"李四"

此外,Reflect.ownKeys()方法可以返回对象所有的自身属性键,包括字符串键和Symbol键。

const myObj = {};
const symbolKey = Symbol("symbolKey");
myObj.name = "张三";
myObj[symbolKey] = "李四";

const allKeys = Reflect.ownKeys(myObj);

console.log(allKeys); // 输出:["name", Symbol(symbolKey)]

五、Symbol 的应用场景:防止属性冲突、模拟私有属性、元编程

  • 防止属性冲突: 这是 Symbol 最常见的用途。 当你需要给一个对象添加属性,但又担心和别人添加的属性冲突时,就可以用 Symbol。 尤其是在开发大型项目或者使用第三方库的时候,这个功能非常有用。

  • 模拟私有属性: JavaScript 没有真正的私有属性,但是我们可以用 Symbol 来模拟。 通过把属性名设置为 Symbol,可以防止外部代码直接访问和修改这些属性。

    class MyClass {
      constructor() {
        this[Symbol("privateProperty")] = "这是一个私有属性";
      }
    
      getPrivateProperty() {
        return this[Symbol("privateProperty")]; // 错误用法!每次都创建一个新的Symbol
      }
    }
    
    const myInstance = new MyClass();
    console.log(myInstance.getPrivateProperty()); //输出 undefined

    上面的例子是错误的,因为每次都创建一个新的Symbol,所以无法访问到真正的私有属性。正确的做法如下:

    const _privateProperty = Symbol("privateProperty"); // 在类外部创建唯一的Symbol
    
    class MyClass {
        constructor() {
            this[_privateProperty] = "这是一个私有属性";
        }
    
        getPrivateProperty() {
            return this[_privateProperty]; // 使用同一个Symbol
        }
    }
    
    const myInstance = new MyClass();
    console.log(myInstance.getPrivateProperty()); // 输出 "这是一个私有属性"

    虽然外部仍然可以通过 Object.getOwnPropertySymbols() 访问到这个属性,但这已经足够防止大部分的意外访问了。

  • 元编程: Symbol 可以用来修改 JavaScript 的内置行为。 比如,你可以用 Symbol.iterator 来定义一个对象的迭代器。 这个我们后面会讲到。

六、Symbol 的内置 Symbol:Well-known Symbols

JavaScript 预定义了一些 Symbol,被称为 Well-known Symbols。 这些 Symbol 有特殊的含义,可以用来修改 JavaScript 的内置行为。

Well-known Symbol 描述
Symbol.iterator 用于定义对象的迭代器。
Symbol.toStringTag 用于自定义对象的 toString() 方法的返回值。
Symbol.hasInstance 用于自定义 instanceof 运算符的行为。
Symbol.toPrimitive 用于将对象转换为原始值。
Symbol.asyncIterator 用于定义对象的异步迭代器。
更多… 还有很多其他的 Well-known Symbols,可以参考 MDN 文档: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/well-known_symbols

咱们挑几个常用的来说说:

  • Symbol.iterator 这个 Symbol 用于定义对象的迭代器。 如果一个对象定义了 Symbol.iterator 属性,那么它就可以用 for...of 循环来遍历。

    const myObj = {
      data: [1, 2, 3],
      [Symbol.iterator]() {
        let index = 0;
        return {
          next: () => {
            if (index < this.data.length) {
              return { value: this.data[index++], done: false };
            } else {
              return { value: undefined, done: true };
            }
          },
        };
      },
    };
    
    for (let item of myObj) {
      console.log(item); // 输出:1 2 3
    }
  • Symbol.toStringTag 这个 Symbol 用于自定义对象的 toString() 方法的返回值。

    class MyClass {
      get [Symbol.toStringTag]() {
        return "MyClass";
      }
    }
    
    const myInstance = new MyClass();
    
    console.log(Object.prototype.toString.call(myInstance)); // 输出:[object MyClass]

    如果没有定义 Symbol.toStringTag,那么 Object.prototype.toString.call(myInstance) 会返回 [object Object]

七、全局 Symbol 注册表:Symbol.for()Symbol.keyFor()

Symbol.for() 方法可以在全局 Symbol 注册表中创建一个 Symbol。 如果注册表中已经存在相同的描述,那么它会返回已存在的 Symbol,而不是创建一个新的。

const symbol1 = Symbol.for("mySymbol");
const symbol2 = Symbol.for("mySymbol");

console.log(symbol1 === symbol2); // 输出:true  (它们是同一个 Symbol)

const symbol3 = Symbol("mySymbol");
console.log(symbol1 === symbol3); // 输出:false (它们不是同一个Symbol)

Symbol.keyFor() 方法可以获取全局 Symbol 注册表中 Symbol 的描述。

const symbol1 = Symbol.for("mySymbol");

console.log(Symbol.keyFor(symbol1)); // 输出:"mySymbol"

const symbol2 = Symbol("mySymbol");

console.log(Symbol.keyFor(symbol2)); // 输出:undefined  (symbol2 没有在全局注册表中注册)

八、Symbol 的注意事项

  • Symbol 不是对象,而是一种原始类型。 也就是说,typeof Symbol() 返回的是 "symbol"
  • Symbol 不能用 new 关键字来创建。 new Symbol() 会抛出一个错误。
  • Symbol 的主要用途是作为对象的属性名,用于防止属性冲突和模拟私有属性。
  • Symbol 可以用来修改 JavaScript 的内置行为,比如定义对象的迭代器和自定义 toString() 方法的返回值。

九、总结

Symbol 是 JavaScript 中一种独特的原始类型,它提供了一种创建唯一标识符的方式,可以有效地防止属性冲突,模拟私有属性,以及修改 JavaScript 的内置行为。 虽然 Symbol 在日常开发中可能不常用,但是了解它的特性和用途,可以帮助你写出更健壮、更安全的代码。

好了,今天的讲座就到这里。 希望大家对 Symbol 有了更深入的了解。 下次有机会再和大家聊聊其他的 JavaScript 黑魔法。 再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注