likes
comments
collection
share

一文理解Symbol应用场景---近万字总结

作者站长头像
站长
· 阅读数 36

前言

做了很多年的前端了,但是依然对一个基础类型比较陌生,那就是symbol,有一次面试突然被面试官问道symbol的应用场景,我顿时哑口无言了,因为咱确实没有在实际场景中用过,也不知道这个基础类型到底用来干嘛呢

我写这篇文章就希望大家能够看懂,并且真正的能根据自己的应用场景用起来

symbol的由来

Symbol 的设计初衷是为了创建一种独一无二的标识符,以防止属性名冲突。在 JavaScript 中,对象的属性名是字符串,如果两个对象的属性使用相同的字符串作为属性名,就会导致冲突。为了避免这种冲突,Symbol 提供了一种生成唯一标识符的机制。

通过这个由来我们带大家建立symbol的第一印象,那就是symbol是用来解决对象属性名命名冲突的

symbol的特点

这里先说一下Symbol的特点

  1. 唯一性:每个 Symbol 值都是唯一的,没有两个 Symbol 值是相等的。
    • 注意Symbol不支持"new Symbol()",他并不是完整的构造函数
    • 每个从Symbol()返回的symbol值都是唯一的。一个symbol值能作为对象属性的标识符;这是该数据类型仅有的目的。
  2. 不可变性:Symbol 值是不可变的,一旦创建就不能修改。
  3. 私有性: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(),用于创建全局符号和获取已经存在的全局符号。

  1. 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属性为undefineddone属性为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,所以我们主要实现了一下功能

  1. symbol不能使用new命令
  2. instanceof的结果为false
  3. symbol接受一个字符串作为参数
  4. symbol值是唯一的,因为两个对象值不一样
  5. 调用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很鸡肋么?

  1. 可读性差:由于 Symbol 是一种唯一且不可变的值,它的值在代码中不易理解和追踪。相比起字符串或其他基本类型的属性名,Symbol 的用途和含义可能更加隐晦。
  2. 难以扩展:Symbol 的唯一性可能导致扩展困难。如果库或框架使用 Symbol 定义了一些内部属性或方法,但用户想要在其上进行自定义扩展时,可能会受限于无法直接访问和修改 Symbol 属性。
  3. 兼容性问题:旧版的 JavaScript 引擎可能不支持 Symbol 或只支持部分功能,这可能导致在一些特定环境中使用 Symbol 的代码无法正常运行。
  4. 额外的复杂性:引入 Symbol 可能会增加代码的复杂性。使用 Symbol 的场景通常是在一些高级、复杂的应用中,对于一般的开发需求来说,它可能会增加学习成本和开发复杂性,而并非每个项目都需要使用 Symbol。

尽管 Symbol 有一些限制和问题,但它仍然具有一些有价值的应用场景,特别是在构建库、框架或需要确保属性名唯一性和隐藏性的高级应用中。它可以提供一种确保标识符的独特性和不可变性的机制。因此,对于特定的用途和需求,Symbol 仍然是一个有用的功能。

转载自:https://juejin.cn/post/7239715208792457272
评论
请登录