mowatermelon / learn-es6

一个有趣的人写的有趣的前端基础
http://blog.iiwhy.cn/learn-es6
7 stars 5 forks source link

learn Symbol #85

Open mowatermelon opened 4 years ago

mowatermelon commented 4 years ago

标记(Symbol)

ECMAScript 2015起,symbol成为了一种新的[原生类型][Primitive],就像numberstring一样。

详细说明请查看[MDN Symbol][Symbol]

基础说明

symbol类型的值是通过Symbol构造函数创建的。

// @param {Any} description 可选的,字符串类型。对symbol的描述,可用于调试但不是访问symbol本身
// Symbol([description])

const { log } = console;
const sym1 = Symbol();

const sym2 = Symbol("key"); // 可选的字符串key

log(sym1);// Uncaught (in promise) TypeError: Cannot convert a Symbol value to a string
log(sym2);// Uncaught (in promise) TypeError: Cannot convert a Symbol value to a string

Symbol()函数会返回symbol类型的值,该类型具有静态属性静态方法

它的静态属性会暴露几个内建的成员对象;它的静态方法会暴露全局的symbol注册,且类似于内建对象类

上面使用Symbol() 函数的语法,不会在你的整个代码库中创建一个可用的全局symbol类型。 要创建跨文件可用的symbol,甚至跨域(每个都有它自己的全局作用域) , 使用 Symbol.for() 方法和 Symbol.keyFor() 方法从全局的symbol注册表设置和取得symbol


不可用 new 关键词

但作为构造函数来说它并不完整,因为它不支持语法:"new Symbol()"。

const sym = new Symbol();// 'new' expression, whose target lacks a construct signature, implicitly has an 'any' type.

这会阻止创建一个显式的 Symbol 包装器对象而不是一个 Symbol 值。围绕原始数据类型创建一个显式包装器对象从 ECMAScript 6 开始不再被支持。 然而,现有的原始包装器对象,如 new Booleannew String以及new Number,因为遗留原因仍可被创建。

如果你真的想创建一个 Symbol 包装器对象 (Symbol wrapper object),你可以使用 Object() 函数

const { log } = console;
const sym = Symbol("foo");
log(typeof sym);     // "symbol"
const symObj = Object(sym);
log(typeof symObj);  // "object"

log(sym == symObj);// true,宽松匹配模式下两者相等
log(sym === symObj);// false,严格匹配模式下两者不相等

Symbols是不可改变且唯一的。

const { log } = console;
const sym2 = Symbol("key");
const sym3 = Symbol("key");

log(typeof sym2 === typeof sym3);// true

log(sym2 === sym3);// false, symbols是唯一的,每次都会创建一个新的 symbol类型

像字符串一样,symbols也可以被用做对象属性的键。

const { log } = console;
const sym = Symbol();

const testObj = {aaa: 1};

const obj = {
    [sym]: "value",
    [`${String(sym)}1`]:2,
    [Symbol()]: 3,
    aaa: 1
};

log(obj[sym]); // "value"
log(Object.getOwnPropertySymbols(obj));// [Symbol(), Symbol()]
log(Object.getOwnPropertySymbols(testObj));// []

注意事项


Symbols也可以与计算出的属性名声明相结合,来声明对象的属性和类成员。

const { log } = console;
const getClassNameSymbol = Symbol();

class C {
    [getClassNameSymbol](){
       return "C";
    }
}

const c = new C();
const className: string = c[getClassNameSymbol]();
log(className);// "C"

SymbolsJSON.stringify()

当使用 JSON.stringify() 时,以 symbol 值作为键的属性会被完全忽略:

const { log } = console;
log(JSON.stringify({[Symbol("foo")]: "foo"}));
// '{}'

Symbolsfor...in 迭代

Symbolsfor...in 迭代中不可枚举。另外,Object.getOwnPropertyNames() 不会返回 symbol 对象的属性

const { log } = console;
const obj= {
    [Symbol("a")]:"a",
    [Symbol.for("b")]:"b",
    ["c"]:"c",
    d:"d"
};

for (const i in obj) {
   log(i); // logs "c" and "d"
}
log(Object.getOwnPropertyNames(obj));// [ "c", "d" ]
log(Object.keys(obj)); // [ "c", "d" ]
mowatermelon commented 4 years ago

静态方法说明

Symbol.for(key)

使用给定的key搜索现有的symbol,如果找到则返回该symbol。否则将使用给定的key在全局symbol注册表中创建一个新的symbol

Symbol() 不同的是,用 Symbol.for() 方法创建的的 symbol 会被放入一个全局 symbol 注册表中。

Symbol.for() 并不是每次都会创建一个新的 symbol,它会首先检查给定的 key 是否已经在注册表中了。假如是,则会直接返回上次存储的那个。否则,它会再新建一个。

字段名 字段值
[[key]] 一个字符串,用来标识每个 symbol
[[symbol]] 存储的 symbol 值
const { log } = console;
log(Symbol.for("foo")); // Symbol(foo) 创建一个 symbol 并放入 symbol 注册表中,键为 "foo"
log(Symbol.for("foo")); // Symbol(foo) 从 symbol 注册表中读取键为"foo"的 symbol

log(Symbol.for("bar") === Symbol.for("bar")); // true,证明了上面说的
log(Symbol("bar") === Symbol("bar")); // false,Symbol() 函数每次都会返回新的一个 symbol

const sym = Symbol.for("mario");
log(sym.toString());// "Symbol(mario)"
// mario 既是该 symbol 在 symbol 注册表中的键名,又是该 symbol 自身的描述字符串

Symbol.keyFor(sym)

Symbol.keyFor(sym) 方法用来获取 symbol 注册表中与某个 symbol 关联的键。

如果全局注册表中查找到该symbol,则返回该symbolkey值,形式为string。如果symbol未在注册表中,返回undefined

const { log } = console;
// 创建一个 symbol 并放入 Symbol 注册表,key 为 "foo"
const globalSym = Symbol.for("foo"); 
log(Symbol.keyFor(globalSym)); // "foo"

// 创建一个 symbol,但不放入 symbol 注册表中
const localSym = Symbol(); 
log(Symbol.keyFor(localSym)); // undefined,所以是找不到 key 的

// well-known symbol 们并不在 symbol 注册表中
log(Symbol.keyFor(Symbol.iterator)); // undefined

mowatermelon commented 4 years ago

静态属性说明

迭代 symbols 静态属性

Symbol.iterator

一个返回一个对象默认迭代器的方法。被 for...of 使用。

Symbol.iterator 属性的属性特性:

属性特性 属性特性值
writable false
enumerable false
configurable false
const { log } = console;
const myIterable = {
    [Symbol.iterator] : async function*() {
        yield "hello";
        yield "async";
        yield "iteration!";
    }
};
log([...myIterable]);// [1, 2, 3]

如果一个迭代器 @@iterator 没有返回一个迭代器对象,那么它就是一个不符合标准的迭代器,这样的迭代器将会在运行期抛出异常,甚至非常诡异的 Bug。

const { log } = console;
const nonWellFormedIterable = {
    [Symbol.iterator] : () => 1
};
log([...nonWellFormedIterable] );// TypeError: Result of the Symbol.iterator method is not an object

Symbol.asyncIterator

Symbol.asyncIterator 属性的属性特性:

属性特性 属性特性值
writable false
enumerable false
configurable false

一个返回对象默认的异步迭代器的方法,被 for await of 使用。

目前没有默认设定了[Symbol.asyncIterator]属性的JavaScript内建的对象。不过,WHATWG(网页超文本应用技术工作小组)Streams会被设定为第一批异步可迭代对象,[Symbol.asyncIterator] 最近已在设计规范中落地。

目前TS中未支持Symbol.asyncIterator

const { log } = console;
const myAsyncIterable = {
  [Symbol.asyncIterator] : async function*() {
      yield "hello";
      yield "async";
      yield "iteration!";
  }
};

(async () => {
  for await (const x of myAsyncIterable) {
      log(x);
      //    "hello"
      //    "async"
      //    "iteration!"
  }
})();

正则表达式 symbols 静态属性

Symbol.match

Symbol.match 属性的属性特性:

属性特性 属性特性值
writable false
enumerable false
configurable false

一个用于对字符串进行匹配的方法,也用于确定一个对象是否可以作为正则表达式使用。被 String.prototype.match() 使用。

比如, String.prototype.startsWith()String.prototype.endsWith()String.prototype.includes() 这些方法会检查其第一个参数是否是正则表达式,是正则表达式就抛出一个TypeError。现在,如果 match symbol 设置为 false(或者一个 假值),就表示该对象不打算用作正则表达式对象。

const { log } = console;
const testStr = 'mowatermelon';
const testObj = { a : 1 };
const reg = /melon/;
const matchFn = reg[Symbol.match];

log(matchFn);// function [Symbol.match]() { [native code] }
// Matches a string with this regular expression, and returns an array containing the results of that search.
// const matchFn: (string: string) => RegExpMatchArray | null
log(typeof matchFn);// function

// log(testStr.startsWith(re)); // TypeError: First argument to String.prototype.startsWith must not be a regular expression

reg[Symbol.match] = false;
log(testStr.startsWith(reg)); // false
log(testStr.includes(reg)); // false
log("/melon/".endsWith(reg));   // true

Symbol.replace

Symbol.replace 属性的属性特性:

属性特性 属性特性值
writable false
enumerable false
configurable false

一个替换匹配字符串的子串的方法. 被 String.prototype.replace() 使用。

const { log } = console;
const reg = /melon/;

const replaceFn = reg[Symbol.replace];

log(replaceFn);
// const replaceFn: {
//     (string: string, replaceValue: string): string;
//     (string: string, replacer: (substring: string, ...args: any[]) => string): string;
// }
// Replaces text in a string, using this regular expression.
log(typeof replaceFn);// function

const testStr = 'mo';
const testStr2 = 'watermelon';
class Replace1 {
  constructor(value) {
    this.value = value;
  }
  [Symbol.replace](str) {
    return `${this.value}, ${str}!`;
  }
}

log(testStr.replace(new Replace1('hello'), 111));// hello, mo!
log(testStr2.replace(new Replace1('hello'), 111));// hello, watermelon!

Symbol.search

Symbol.search 属性的属性特性:

属性特性 属性特性值
writable false
enumerable false
configurable false

Symbol.search 指定了一个搜索方法,这个方法接受用户输入的正则表达式,返回该正则表达式在字符串中匹配到的下标,这个方法由以下的方法来调用 String.prototype.search()

const { log } = console;
const reg = /melon/g;
const searchFn = reg[Symbol.search];

log(searchFn);
// const searchFn: (string: string) => number
// Finds the position beginning first substring match in a regular expression search using this regular expression.
log(typeof searchFn);// function

const testStr = 'mo';
const testStr2 = 'watermelon';
reg[Symbol.search] = function(str){
    return `this is ${str}'s rule, [${reg}]`;
}
class Search1 {
  constructor(value) {
    this.value = value;
  }
  [Symbol.search](str) {
    return `this is ${str}'s rule, [${this.value}]`;
  }
}

log(testStr.search(reg));// this is mo's rule, [/melon/g]
log(testStr2.search(reg));// this is watermelon's rule, [/melon/g]

log(testStr.search(new Search1(reg)));// this is mo's rule, [/melon/g]
log(testStr2.search(new Search1(reg)));// this is watermelon's rule, [/melon/g]

Symbol.split

Symbol.split 属性的属性特性:

属性特性 属性特性值
writable false
enumerable false
configurable false

一个在匹配正则表达式的索引处拆分一个字符串的方法。被 String.prototype.split() 使用。

const { log } = console;
const reg = /melon/g;
const splitFn = reg[Symbol.split];

log(splitFn);
// const splitFn: (string: string, limit?: number | undefined) => string[]
// Returns an array of substrings that were delimited by strings in the original input that match against this regular expression.
// If the regular expression contains capturing parentheses, then each time this regular expression matches, the results (including any undefined results) of the capturing parentheses are spliced.

log(typeof splitFn);// function

const testStr = 'mo';
const testStr2 = 'watermelon';
reg[Symbol.split] = function(str){
    return `this is ${str}'s rule, [${reg}]`;
}
class Split1 {
  constructor(value) {
    this.value = value;
  }
  [Symbol.split](str) {
    return `this is ${str}'s rule, [${this.value}]`;
  }
}

log(testStr.split(reg));// this is mo's rule, [/melon/g]
log(testStr2.split(reg));// this is watermelon's rule, [/melon/g]

log(testStr.split(new Split1(reg)));// this is mo's rule, [/melon/g]
log(testStr2.split(new Split1(reg)));// this is watermelon's rule, [/melon/g]

其他 symbols 静态属性

Symbol.hasInstance

Symbol.hasInstance 属性的属性特性:

属性特性 属性特性值
writable false
enumerable false
configurable false

一个确定一个构造器对象识别的对象是否为它的实例的方法。被 instanceof 使用。

const { log } = console;

log(String[Symbol.hasInstance]);// ƒ [Symbol.hasInstance]() { [native code] }
log(Number[Symbol.hasInstance] === String[Symbol.hasInstance]);// true
log(Number[Symbol.hasInstance] === String[Symbol.hasInstance]);// true
log(Function[Symbol.hasInstance] === String[Symbol.hasInstance]);// true
log(Boolean[Symbol.hasInstance] === String[Symbol.hasInstance]);// true
log(Array[Symbol.hasInstance] === String[Symbol.hasInstance]);// true
log(Object[Symbol.hasInstance] === String[Symbol.hasInstance]);// true

class MyArray {  
  static [Symbol.hasInstance](instance) {
    return Array.isArray(instance);
  }
}
log([] instanceof MyArray); // true

Symbol.isConcatSpreadable

Symbol.isConcatSpreadable 属性的属性特性:

属性特性 属性特性值
writable false
enumerable false
configurable false

@@isConcatSpreadable 符号 (Symbol.isConcatSpreadable) 可以直接定义为对象属性或继承而来,它是布尔类型。它可以控制数组或类似数组(array-like)的对象的行为:

对于数组对象,默认情况下,用于concat时,会按数组元素展开然后进行连接(数组元素作为新数组的元素)。

重置Symbol.isConcatSpreadable可以改变默认行为。

对于类似数组的对象,用于concat时,该对象整体作为新数组的元素,重置Symbol.isConcatSpreadable可改变默认行为。

const { log } = console;

const arr1 = [1,2,3];
const arr2 = [4,5,6];
// A Boolean value that if true indicates that an object should flatten to its array elements by Array.prototype.concat.

log(arr1.concat(arr2));// [1, 2, 3, 4, 5, 6]

arr2[Symbol.isConcatSpreadable] = false;

log(arr1.concat(arr2));// [1, 2, 3, Array(3)]

Symbol.unscopables

Symbol.unscopables 属性的属性特性:

属性特性 属性特性值
writable false
enumerable false
configurable false

可以在任何对象上定义 @@unscopables symbol (Symbol.unscopables),用于排除属性名称并与 with 环境绑定在一起作为词法变量公开。

请注意,如果使用 Strict mode,语句将不可用,并且可能也不需要 symbol

unscopables 对象上设置属性为 true,将使其 unscopable 并且因此该属性也将不会在词法环境变量中出现。 如果设置属性为 false ,则将使其可 scopable 并且该属性会出现在词法环境变量中。

const { log } = console;
const keys = [];

with(Array.prototype) {
  keys.push("something");
}

Object.keys(Array.prototype[Symbol.unscopables]);
// ["copyWithin", "entries", "fill", "find", "findIndex", "includes", "keys", "values"]

// 也可以为你自己的对象设置 `unscopables`
const obj = {
  foo: 1,
  bar: 2
};

obj[Symbol.unscopables] = {
  foo: false,
  bar: true
};

with(obj) {
  log(foo); // 1
  log(bar); // ReferenceError: bar is not defined
}

Symbol.species

Symbol.species 属性的属性特性:

属性特性 属性特性值
writable false
enumerable false
configurable false

Symbol.species 是个函数值属性,其被构造函数用以创建派生对象。species 访问器属性允许子类覆盖对象的默认构造函数。

const { log } = console;
class MyArray extends Array {
  // 覆盖 species 到父级的 Array 构造函数上
  static get [Symbol.species]() { return Array; }
}
const a = new MyArray(1,2,3);
const mapped = a.map(x => x * x);

log(mapped instanceof MyArray); // false
log(mapped instanceof Array);   // true

Symbol.toPrimitive

Symbol.toPrimitive 属性的属性特性:

属性特性 属性特性值
writable false
enumerable false
configurable false

Symbol.toPrimitive 是一个内置的 Symbol 值,它是作为对象的函数值属性存在的,当一个对象转换为对应的原始值时,会调用此函数。

const { log } = console;
// 一个没有提供 Symbol.toPrimitive 属性的对象,参与运算时的输出结果
const obj1 = {};
log(+obj1);     // NaN
log(`${obj1}`); // "[object Object]"
log(obj1 + ""); // "[object Object]"

// 接下面声明一个对象,手动赋予了 Symbol.toPrimitive 属性,再来查看输出结果
const obj2 = {
  [Symbol.toPrimitive](hint) {
    if (hint == "number") {
      return 10;
    }
    if (hint == "string") {
      return "hello";
    }
    return true;
  }
};
log(+obj2);     // 10      -- hint 参数值是 "number"
log(`${obj2}`); // "hello" -- hint 参数值是 "string"
log(obj2 + ""); // "true"  -- hint 参数值是 "default"

Symbol.toStringTag

Symbol.toStringTag 属性的属性特性:

属性特性 属性特性值
writable false
enumerable false
configurable false

Symbol.toStringTag 是一个内置 symbol,它通常作为对象的属性键使用,对应的属性值应该为字符串类型。

这个字符串用来表示该对象的自定义类型标签,通常只有内置的 Object.prototype.toString() 方法会去读取这个标签并把它包含在自己的返回值里。

许多内置的 JavaScript 对象类型即便没有 toStringTag 属性,也能被 toString() 方法识别并返回特定的类型标签。

const { log } = console;
log(Object.prototype.toString.call('foo'));     // "[object String]"
log(Object.prototype.toString.call([1, 2]));    // "[object Array]"
log(Object.prototype.toString.call(3));         // "[object Number]"
log(Object.prototype.toString.call(true));      // "[object Boolean]"
log(Object.prototype.toString.call(undefined)); // "[object Undefined]"
log(Object.prototype.toString.call(null));      // "[object Null]"

另外一些对象类型则不然,toString() 方法能识别它们是因为引擎为它们设置好了 toStringTag 标签

const { log } = console;
log(Object.prototype.toString.call(new Map()));       // "[object Map]"
log(Object.prototype.toString.call(function* () {})); // "[object GeneratorFunction]"
log(Object.prototype.toString.call(Promise.resolve())); // "[object Promise]"

但你自己创建的类不会有这份特殊待遇,toString() 找不到 toStringTag 属性时只好返回默认的 Object标签,加上 toStringTag 属性,你的类也会有自定义的类型标签了。

const { log } = console;
class ValidatorClass1 {}

log(Object.prototype.toString.call(new ValidatorClass1())); // "[object Object]"

class ValidatorClass2 {
  get [Symbol.toStringTag]() {
    return "Validator";
  }
}

log(Object.prototype.toString.call(new ValidatorClass2())); // "[object Validator]"