知名符号的详细概述





朋友们,美好的一天!



Symbol是ECMAScript2015(ES6)中引入的原始数据类型,可让您创建唯一的标识符:const uniqueKey = Symbol('SymbolName')。



您可以将符号用作对象属性的键。 JavaScript以特殊方式处理的符号称为众所周知的符号。这些字符由内置JavaScript算法使用。例如,Symbol.iterator用于迭代数组,字符串的元素。它也可以用来定义自己的迭代器函数。



这些符号起着重要的作用,因为它们使您可以微调对象的行为。



作为符号的唯一性,使用符号作为对象键(而不是字符串)可以轻松地向对象添加新功能。同时,无需担心键之间的冲突(因为每个字符都是唯一的),这在使用字符串时可能会成为问题。



本文将重点介绍知名符号及其用法示例。



为了简单起见,众所周知的Symbol。<name>符号的语法为@@ <name>格式。例如,Symbol.iterator表示为@@迭代器,Symbol.toPrimitive表示为@@ toPrimitive,等等。



如果我们说一个对象具有@@迭代器方法,则该对象包含一个称为Symbol.iterator的属性,该属性由以下函数表示:{[Symbol.iterator]:function(){}}。



1.符号简介



字符是原始类型(例如数字,字符串或布尔值),唯一且不可变(immutable)。



要创建符号,请使用可选参数调用Symbol()函数-符号的名称,或更准确地说,描述:



const mySymbol = Symbol()
const namedSymbol = Symbol('myName')
typeof mySymbol // symbol
typeof namedSymbol // symbol


mySymbol和namedSymbol是原始符号。namedSymbol的名称为“ myName”,通常用于调试代码。



每次调用Symbol()都会创建一个新的唯一符号。即使两个字符具有相同的名称,它们也是唯一的(或特殊的):



const first = Symbol()
const second = Symbol()
first === second // false

const firstNamed = Symbol('Lorem')
const secondNamed = Symbol('Lorem')
firstNamed === secondNamed // false


符号可以是对象的键。为此,请在对象文字或类定义中使用计算属性语法([symbol]):



const strSymbol = Symbol('String')

const myObj = {
  num: 1,
  [strSymbol]: 'Hello World'
}

myObj[strSymbol] // Hello World
Object.getOwnPropertyNames(myObj) // ['num']
Object.getOwnPropertySymbols(myObj) // [Symbol(String)]


无法使用Object.keys()或Object.getOwnPropertyNames()来检索符号属性。要访问它们,您需要使用特殊函数Object.getOwnPropertySymbols()。



使用众所周知的符号作为键,可以更改对象的行为。



众所周知的符号可用作Symbol对象的不可枚举,不可变和不可配置的属性。要获取它们,请使用点符号:Symbol.iterator,Symbol.hasInstance等。



以下是获取知名符号列表的方法:



Object.getOwnPropertyNames(Symbol)
// ["hasInstance", "isConcatSpreadable", "iterator", "toPrimitive",
//  "toStringTag", "unscopables", "match", "replace", "search",
//  "split", "species", ...]

typeof Symbol.iterator // symbol


Object.getOwnPropertyNames(Symbol)返回Symbol对象的本机属性的列表,包括众所周知的符号。当然,Symbol.iterator是symbol类型。



2. @@迭代器,它使您可以使对象可迭代(可迭代)



Symbol.iterator也许是最著名的符号。它允许您定义如何使用for-of语句或散布运算符迭代对象(以及是否应该迭代对象)。



默认情况下,许多内置类型(例如字符串,数组,映射,集合或集合)是可迭代的,因为它们具有@@迭代器方法:



const myStr = 'Hi'
typeof myStr[Symbol.iterator] // function
for (const char of myStr) {
  console.log(char) //     :  'H',  'i'
}
[...myStr] // ['H', 'i']


变量myStr包含具有Symbol.iterator属性的原始字符串。此属性包含用于遍历字符串中的字符的函数。



定义Symbol.iterator方法的对象必须符合迭代(iterator)协议。更准确地说,此方法必须返回符合指定协议的对象。这样的对象必须具有next()方法,该方法返回{value:<iterator_value>,完成:<boolean_finished_iterator>}。



在下面的示例中,我们创建一个可迭代的myMethods对象以迭代其方法:



function methodsIterator() {
  let index = 0
  const methods = Object.keys(this)
    .filter(key => typeof this[key] === 'function')

    return {
      next: () => ({
        done: index === methods.length,
        value: methods[index++]
      })
    }
}

const myMethods = {
  toString: () => '[object myMethods]',
  sum: (a, b) => a + b,
  numbers: [1, 3, 5],
  [Symbol.iterator]: methodsIterator
}

for (const method of myMethods) {
  console.log(method) // toString, sum
}


MethodsIterator()是一个返回迭代器{next:function(){}}的函数。 myMethods对象使用值methodsIterator定义了一个计算的属性[Symbol.iterator]。这使得对象可以使用for-of循环进行迭代。对象方法也可以使用[... myMethods]获得。可以使用Array.from(myMethods)将此类对象转换为数组。



可以使用生成器函数简化可迭代对象的创建。此函数返回符合迭代协议Generator对象



让我们用@@迭代器方法创建一个Fibonacci类,该类生成一系列Fibonacci数字:



class Fibonacci {
  constructor(n) {
    this.n = n
  }

  *[Symbol.iterator]() {
    let a = 0, b = 1, index = 0
    while (index < this.n) {
      index++
      let current = a
      a = b
      b = current + a
      yield current
    }
  }
}

const sequence = new Fibonacci(6)
const numbers = [...sequence]
console.log(numbers) // [0, 1, 1, 2, 3, 5]


* [Symbol.iterator](){}定义了一个类方法-一个生成器函数。Fibonacci实例符合蛮力协议。散布运算符调用@@迭代器方法来创建数字数组。



如果原始类型或对象包含@@迭代器,则可以在以下情况下使用它:



  • for-of遍历元素
  • 使用散布运算符创建元素数组
  • 使用Array.from(iterableObject)创建一个数组
  • 在yield *表达式中传递给另一个生成器
  • 在构造函数Map(),WeakMap(),Set()和WeakSet()中
  • 在静态方法Promise.all(),Promise.race()等中


您可以在此处阅读有关创建可迭代对象的更多信息



3. @@ hasInstance用于设置instanceof



默认情况下,构造函数操作符的obj instanceof检查obj原型链中是否存在Constructor.prototype对象。让我们考虑一个例子:



function Constructor() {
  // ...
}
const obj = new Constructor()
const objProto = Object.getPrototypeOf(obj)

objProto === Constructor.prototype // true
obj instanceof Constructor // true
obj instanceof Object // true


obj instanceof Constructor返回true,因为obj的原型是Constructor.prototype(作为调用构造函数的结果)。instanceof根据需要引用原型链,因此obj instanceof Object也返回true。



有时应用程序需要更严格的实例检查。



幸运的是,我们能够定义@@ hasInstance方法来更改instanceof的行为。obj instanceof Type等效于Type [Symbol.hasInstance](obj)。



让我们检查一下变量是否可迭代:



class Iterable {
  static [Symbol.hasInstance](obj) {
    return typeof obj[Symbol.iterator] === 'function'
  }
}

const arr = [1, 3, 5]
const str = 'Hi'
const num = 21
arr instanceof Iterable // true
str instanceof Iterable // true
num instanceof Iterable // false


Iterable类包含一个静态方法@@ hasInstance。此方法检查obj是否可迭代,即 是否包含Symbol.iterator属性。arr和str是可迭代的,但num不是。



4. @@ toPrimitive将对象转换为基本体



使用Symbol.toPrimitive定义一个属性,其值是原始转换函数的对象。@@ toPrimitive采用一个参数,提示,可以是数字,字符串或默认值。提示指示返回值的类型。



让我们改善数组的转换:



function arrayToPrimitive(hint) {
  if (hint === 'number') {
    return this.reduce((x, y) => x + y)
  } else if (hint === 'string') {
    return `[${this.join(', ')}]`
  } else {
    // hint    
    return this.toString()
  }
}

const array = [1, 3, 5]
array[Symbol.toPrimitive] = arrayToPrimitive

//    . hint  
+ array // 9
//    . hint  
`array is ${array}` // array is [1, 3, 5]
//   . hint   default
'array elements: ' + array // array elements: 1,3,5


arrayToPrimitive(hint)是一个根据提示值将数组转换为基元的函数。将数组[Symbol.toPrimitive]设置为arrayToPrimitive会强制数组使用新的变换方法。用+的数字提示值对@@ toPrimitive进行+数组调用。返回数组元素的总和。array是$ {array}调用@@ toPrimitive并带有hint = string。数组将转换为字符串“ [1、3、5]”。最后,'array elements:'+ array使用hint = default进行转换。该数组将转换为“ 1,3,5”。



@@ toPrimitive方法用于将对象表示为原始类型:



  • 当使用宽松的(抽象的)相等运算符时:object == primary
  • 当使用加法/串联运算符时:对象+原语
  • 使用减法运算符时:对象-原语
  • 在各种情况下,将对象转换为基元:字符串(对象),数字(对象)等。


5. @@ toStringTag创建一个标准的对象描述



使用Symbol.toStringTag定义一个属性,其值是描述对象类型的字符串。@@ toStringTag方法由Object.prototype.toString()使用。



规范定义了由Object.prototype.toString()返回的许多类型的默认值:



const toString = Object.prototype.toString
toString.call(undefined) // [object Undefined]
toString.call(null)      // [object Null]
toString.call([1, 4])    // [object Array]
toString.call('Hello')   // [object String]
toString.call(15)        // [object Number]
toString.call(true)      // [object Boolean]
// Function, Arguments, Error, Date, RegExp  ..
toString.call({})        // [object Object]


这些类型没有Symbol.toStringTag属性,因为Object.prototype.toString()算法以一种特殊的方式对其进行评估。



所讨论的属性以符号,生成器函数,卡,promise等类型定义。请考虑一个示例:



const toString = Object.prototype.toString
const noop = function() { }

Symbol.iterator[Symbol.toStringTag]   // Symbol
(function* () {})[Symbol.toStringTag] // GeneratorFunction
new Map()[Symbol.toStringTag]         // Map
new Promise(noop)[Symbol.toStringTag] // Promise

toString.call(Symbol.iterator)   // [object Symbol]
toString.call(function* () {})   // [object GeneratorFunction]
toString.call(new Map())         // [object Map]
toString.call(new Promise(noop)) // [object Promise]


如果对象不是标准类型组并且不包含@@ toStringTag属性,则返回Object。当然,我们可以更改此设置:



const toString = Object.prototype.toString

class SimpleClass { }
toString.call(new SimpleClass) // [object Object]

class MyTypeClass {
  constructor() {
    this[Symbol.toStringTag] = 'MyType'
  }
}

toString.call(new MyTypeClass) // [object MyType]


SimpleClass类的实例没有@@ toStringTag属性,因此Object.prototype.toString()返回[object Object]。MyTypeClass类的构造函数将@@ toStringTag属性分配给具有MyType值的实例,因此Object.prototype.toString()返回[object MyType]。



注意,出于向后兼容性的原因引入了@@ toStringTag。不希望使用它。最好使用instanceof(与@@ hasInstance一起使用)或typeof来确定对象的类型。



6. @@物种创建派生对象



使用Symbol.species定义一个属性,其值是用于创建派生对象的构造函数。



许多构造函数的@@种类值是构造函数本身:



Array[Symbol.species] === Array // true
Map[Symbol.species] === Map // true
RegExp[Symbol.species] === RegExp // true


首先,请注意,派生对象是在对原始对象执行特定操作之后返回的对象。例如,调用map()返回一个派生对象-转换数组元素的结果。



通常,派生对象与原始对象引用相同的构造函数。但有时有必要定义另一个构造函数(也许是标准类之一):@@物种可以在其中提供帮助。



假设我们使用MyArray子类扩展Array构造函数以添加一些有用的方法。这样做时,我们希望MyArray实例的派生对象的构造函数为Array。为此,您需要使用Array值定义一个计算属性@@种:



class MyArray extends Array {
  isEmpty() {
    return this.length === 0
  }
  static get [Symbol.species]() {
    return Array
  }
}
const array = new MyArray(2, 3, 5)
array.isEmpty() // false
const odds = array.filter(item => item % 2 === 1)
odds instanceof Array // true
odds instanceof MyArray // false


MyArray定义了静态计算属性Symbol.species。它指定派生对象的构造函数应为Array构造函数。稍后在过滤数组的元素时,array.filter()返回Array。



@@物种的计算属性由数组和类型化的数组方法使用,例如map(),concat(),slice(),splice(),它们返回派生对象。使用此属性可以在保留原始构造函数的同时扩展映射,正则表达式或Promise。



7.创建对象形式的正则表达式:@@匹配,@@替换,@@搜索和@@拆分



字符串原型包含4个使用正则表达式作为参数的方法:



  • String.prototype.match(regExp)
  • String.prototype.replace(regExp,newSubstr)
  • String.prototype.search(regExp)
  • String.prototype.split(regExp,限制)


ES6通过定义相应的计算属性允许这些方法接受其他类型:@@匹配,@@替换,@@搜索和@@拆分。



奇怪的是,RegExp原型包含指定的方法,这些方法也使用符号定义:



typeof RegExp.prototype[Symbol.match]   // function
typeof RegExp.prototype[Symbol.replace] // function
typeof RegExp.prototype[Symbol.search]  // function
typeof RegExp.prototype[Symbol.split]   // function


在下面的示例中,我们定义一个可以代替正则表达式使用的类:



class Expression {
  constructor(pattern) {
    this.pattern = pattern
  }
  [Symbol.match](str) {
    return str.includes(this.pattern)
  }
  [Symbol.replace](str, replace) {
    return str.split(this.pattern).join(replace)
  }
  [Symbol.search](str) {
    return str.indexOf(this.pattern)
  }
  [Symbol.split](str) {
    return str.split(this.pattern)
  }
}

const sunExp = new Expression('')

' '.match(sunExp) // true
' '.match(sunExp) // false
' day'.replace(sunExp, '') // ' '
'  '.search(sunExp) // 8
''.split(sunExp) // ['', '']


Expression类定义@@ match,@@ replace,@@ search和@@ split方法。然后,在适当的方法中使用此类的实例-sunExp代替正则表达式。



8. @@ isConcatSpread将对象转换为数组



Symbol.isConcatSpread是一个布尔值,指示可以使用Array.prototype.concat()方法将对象转换为数组。



默认情况下,concat()方法在连接数组时检索数组的元素(将数组分解为它组成的元素):



const letters = ['a', 'b']
const otherLetters = ['c', 'd']
otherLetters.concat('e', letters) // ['c', 'd', 'e', 'a', 'b']


要连接两个数组,请将字母作为参数传递给concat()方法。字母数组的元素成为串联结果的一部分:['c','d','e','a','b']。



为了防止将数组分解为元素并使数组成为联合结果的一部分,@@ isConcatSpread属性应设置为false:



const letters = ['a', 'b']
letters[Symbol.isConcatSpreadable] = false
const otherLetters = ['c', 'd']
otherLetters.concat('e', letters) // ['c', 'd', 'e', ['a', 'b']]


与数组相反,concat()方法不会将类似于数组的对象分解为元素。也可以使用@@ isConcatSpread更改此行为:



const letters = { 0: 'a', 1: 'b', length: 2 }
const otherLetters = ['c', 'd']
otherLetters.concat('e', letters)
// ['c', 'd', 'e', {0: 'a', 1: 'b', length: 2}]
letters[Symbol.isConcatSpreadable] = true
otherLetters.concat('e', letters) // ['c', 'd', 'e', 'a', 'b']


9. @@ unscopables使用with访问属性



Symbol.unscopables是一个计算属性,使用with语句将专有名称从添加到作用域链开头的对象中排除。@@ unscopables属性具有以下格式:{propertyName:<boolean_exclude_binding>}。



ES6仅为数组定义@@ unscopables。这样做是为了隐藏可以覆盖旧代码中同名变量的新方法:



Array.prototype[Symbol.unscopables]
// { copyWithin: true, entries: true, fill: true,
//   find: true, findIndex: true, keys: true }
let numbers = [1, 3, 5]
with (numbers) {
  concat(7) // [1, 3, 5, 7]
  entries // ReferenceError: entries is not defined
}


我们可以在with主体中访问concat()方法,因为该方法不包含在@@ unscopables属性中。entry()方法在此属性中指定,并设置为true,这使得它在内部不可用。



引入@@ unscopables仅是为了使用with语句(在严格模式下不建议使用和禁止使用)与旧代码向后兼容。



希望您发现自己感兴趣的东西。感谢您的关注。



All Articles