【每日一题】说一下你对Symbol的理解

前端印记

共 13111字,需浏览 27分钟

 ·

2021-09-13 01:28

人生苦短,总需要一点仪式感。比如学前端~

思维脑图

以下内容的简单脑图

Symbol初识

ES6 中引入了—种新的基础数据类型: Symbol ,这是一种新的 基础数据类型 (primitive type)

它的功能类似于一种标识唯一性的 ID。通常情况下,我们可以通过调用 Symbol()函数 来创建一个 Symbol 实例 :

let s1 = Symbol();

Symbol函数直接调用,不能用new

let s1 = new Symbol(); // 报错 Uncaught TypeError: Symbol is not a constructor

参数

你可以在调用 Symbol() 函数时传入一个可选的字符串参数,相当于给你创建的 Symbol 实例一个描述信息:

let s2 = Symbol("another symbol");

传参数主要是为了在控制台显示或者转为字符串时比较容易区分。所以尽管两个参数一样值也不相等

let s3 = Symbol("another symbol");
s2 === s3 // false

如果用 TypeScript 的方式来描述这个 Symbol() 函数的话,可以表示成:

/**
 *  @param {any} description 描述信息。可以是任何可以被转型成字符串的值,如:字符串、数字、对象、数组等
 */

function Symbol(description: any): symbol;

typeof symbol

由于 Symbol 是一种新的基础数据类型,所以当我们使用 typeof 去检查它的类型的时候,它会返回一个属于自己的类型:symbol

typeof Symbol(); // 'symbol'

symbol唯一性

另外,我们需要重点记住的一点是:  每个 Symbol 实例都是唯一的。因此,当你比较两个 Symbol 实例的时候,将总会返回 false:

let s1 = Symbol();
let s2 = Symbol("another symbol");
let s3 = Symbol("another symbol");

s1 === s2; //false
s2 === s3; //false

作为属性名的symbol

使用 Symbol 作为对象的属性 key。在对象的内部,使用 Symbol 值定义属性时,Symbol 值必须放在方括号之中。(这是es6提供的新写法:字面量定义对象时,可以用表达式作为对象的属性名,把表达式放在方括号内来直接创建属性。)

let name = Symbol("name")
let obj = {
    [name]: "小石头"// 这里,name是一个变量而不是字符串,所以也需要放到方括号之中
    [Symbol("private")]: 'private key'// 这里因为Symbol()函数调用需要执行,因此要放到方括号里。
    age18,
    title"对象键名用Symbol"
};

局限性

Symbol 值作为对象属性名时不能用点运算符,因为点运算符后面总是字符串,所以不会读取Symbol作为标识名所指代的那个值,导致对象的属性名实际上是一个字符串,而不是一个 Symbol 值。也就不能获取到你要的数据。

// ✅ 正确
obj[name] // 得到正确值‘小石头’。因为必须要用之前保存的name变量值来获取。

// ❌ 错误
obj.name // 这时访问的是obj['name']字符串,obj上没有name字符串这个属性值,只有标识符为'name'的symbol值。

因为[Symbol("private")]: 'private key' 这个属性定义的时候没用变量接受,再次创建的key跟他也不想等,所以这里想直接获取是不可能了,因此说这个属性在这里相对来说比较私有了。

obj[Symbol("private")] // undefined,因为这个Symbol("private")不是那个Symbol("private")


对象中,Symbol 类型的属性不能被 Object.keys()Object.entries()Object.values() 获取,也不能被 for. ..in 枚举,它未被包含在对象自身的 属性名集合 (property names)之中。来看以下示例代码:

// 以下方式都是白费,拿不到symbol类型的属性

Object.keys(obj); // ["age","title"]
Object.entires(obj); // [["age", 18], ["title", "对象键名用Symbol"]]
Object.values(obj); // [18, "对象键名用Symbol"]
for (let p in obj) {
    console.log(p); // 分别输出 “age” 和 “title”
}
Object.getOwnPropertyNames(obj); // ["age","title"],没有能得到 “小石头”

当使用 JSON.stringify() 将对象转换成 JSON 字符串时,Symbol属性也会被排除在输出内容之外:

JSON.stringify(obj); // "{\"age\":18,\"title\":\"对象键名用Symbol\"}"

获取symbol属性

可以用 object.getOwnPropertySymbols 方法获取。他返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。

// 使用Object的API
Object.getOwnPropertySymbols(obj); // [Symbol(name), Symbol(private)]

也可以用Reflect.ownKeys(obj)方法获取。

// 使用新增的的反射API
Reflect.ownKeys(obj); // ["age", "title", Symbol(name), Symbol(private)]
obj[arr[3]]; // 辗转拿到这个特殊属性的值 "private key",但实际业务中,这个数组获取值的顺序你又控制不了,这种写法还是挺冒险的

Symbol.for()

Symbol.for() 接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值:如果有,就返回这个 Symbol 值;否则就新建 Symbol值,并返回一个以该字符串为名称的 Symbol 值。同时将属性值的名字登记在全局环境中,供后续搜索公用。

let s1 = Symbol('小石头');
let s2 = Symbol('小石头');

s1 === s2 // false

用于实在想使用一个公共 Symbol 值的情况。

let s1 = Symbol.for('小石头');
let s2 = Symbol.for('小石头');

s1 === s2 // true

应用:多window间共用一个Symbol实例

注册和获取全局 Symbol 通常情况下,我们在一个浏览器窗口中(window),使用 Symbol()函数定义 Symbol 实例就足够啦,但是,如果你的应用涉及到多个 window(最典型的就是页面中使用了iframe),并需要这些 window 中使用的某些 Symbol 是同一个,那就不能使用 Symbol ()函数了,因为用它在不同 window 中创建的 Symbol 实例总是唯一的,而我们需要的是在所有这些 window 环境下保持一个共享的 Symbol。

这种情况下,我们就需要使用另一个 API 来创建或获取 Symbol,那就是 Symbol.for() ,它可以注册或获取一个 window 间全局的 Symbol 实例:

let gs1 = Symbol.for("global_symbol_1"); // 注册一个全局Symbol
let gs2 = Symbol.for("global_symbol_2"); // 获取全局Symbol

gs1 === gs2; // true

这样一个 Symbol 不光在单个 window 中是唯一的,在多个相关的 window 间也是唯一的了

Symbol.keyFor()

Symbol.keyFor() 方法返回一个已登记的 Symbol 类型值的 key。

let s1 = Symbol.for('使用for在全局登记后能查到名字');
Symbol.keyFor(s1) // ‘使用for在全局登记后能查到名字’

let s2 = Symbol('不用for不在全局登记查不到返回undefined')
Symbol.keyFor(s2) // undefined 

一些应用场景

使用 Symbol 来作为对象属性名(key)

在Symbol之前,我们通常定义或访问对象的属性时都是使用字符串,比如下面的代码:

let obj = {
    txt"字符串",
};
obj["txt"]; // 'world'

而现在 symbol 可同样用于对象属性的定义和访问:

const PROP_NAME = Symbol();
// 变量,用中括号包裹
let obj = {
    [PROP_NAME]: "小石头"
};

// 获取,变量也用中括号包裹
obj[PROP_NAME]; // '小石头' 

之前的写法容易造成属性名冲突,比如VUE的Mixin使用时。使用Symbol作为属性名能保证是独一无二不重复的,这就能防止属性名冲突。

这就是 ES6 引入Symbol的原因。https://es6.ruanyifeng.com/#docs/symbol

因此vue中写混入 (mixin) 的时候,就可以使用symbol使得计算属性、方法名、data属性值等唯一,不可覆盖。

使用symbol让数据对象的特殊属性私有化

Symbol 类型的 key 不能用点运算符获取、不能通过 Object.keys() ……等方法获取(多到我都写烦了) 、 不能用 for…in 来枚举的、JSON.stringify 也排除他……

用起来“这么麻烦的属性”,我们为啥不用来存一些不想让人轻易获取到的属性呢!利用该特性,我们可以把一些不需要对外操作和访问的属性使用 Symbol 来定义。

我们可以利用这一特点来更好的设计我们的数据对象,让“对内操作"和“对外选择性输出”变得更加优雅。

使用 Symbol 代替常量

先来看看下面的代码:

// 你的reducers/mutations里是不是经常这么写:

const TYPE_AUDIO = "AUDIO";
const TYPE_VIDEO = "VIDEO";
const TYPE_IMAGE = "IMAGE";

function handleFileResource(resource{
  switch (resource.type) {
    case TYPE_AUDIO:
      playAudio(resource);
      break;
    case TYPE_VIDEO:
      playVideo(resource);
      break;
    case TYPE_IMAGE:
      previewImage(resource);
      break;
    default:
      throw new Error("Unknown type of resource");
  }
}

如上面的代码中那样,我们经常定义一组常量来代表一种业务逻辑下的几个不同类型,我们通常希望这几个常量之间是唯一的关系,为了保证这一点,我们需要为常量赋—个唯一的值(比如这的'AUDIO、'VIDEO'、'IMAGE'),常量少的时候还算好,但是常量一多,你可能还得花点脑子好好为他们取个好点的名字。

现在有了 Symbol,我们大可不必这么麻烦了:

const TYPE_AUDIO = Symbol();
const TYPE_VIDEO = Symbol();
const TYPE_IMAGE = Symbol();

这样定义,就能保证三个常量的值是唯一的了!

项目中,vuex/redux中做行为派发的时候需要统一管理派发的行为标识,标识的值为了是唯一值我们就可以这么做。

使用 Symbol 定义类的私有属性或方法

我们知道在 JavaScript 中,是没有如 Java 等面向对象语言的访问控制关键字 private 的,类上所有定义的属性或方法都是可公开访问的。

因此这对我们进行 API 的设计时造成了一些困扰。而有了 Symbol 以及模块化机制,类的私有属性和方法才变成可能。例如:

a.js

const PASSWORD = Symbol();

class Login {
    constructor(userName, password) {
        this.userName = userName;
        this[PASSWORD] = password;
    }
    checkPassword(pwd) {
        return this[PASSWORD] === pwd;
    }
}
export default Login;

b.js

import Login from "./a";

const login = new Login("admin""123456");

login.checkPassword("admin"); // true

login.PASSWORD; // undefined
login[PASSWORD]; // PASSWORD is not defined
login["PASSWORD"]; // undefined

由于 Symbol 常量 PASSWORD 被定义在 a.js 所在的模块中,外面的模块获取不到这个 Symbol,也不可能再创建一个一模一样的 Symbol 出来(因为 Symbol 是唯一的),因此这个 PASSWORD 的 Symbol 只能被限制在 a.js 内部使用,所以使用它来定义的类属性是没有办法被模块外访问到的,达到了一个 私有化 的效果。

使用注意事项

  1. Symbol 函数前不能使用 new 命令,否则会报错。
  2. Symbol 函数参数只是表示对当前 Symbol 值的描述,因此相同参数的Symbol函数的返回值是不相等的
  3. 在对象的内部,使用 Symbol 值定义属性时,Symbol 值必须放在方括号之中。
  4. Symbol 值作为对象属性名时,获取不能用点运算符,只能用方括号获取
  5. Symbol 作为属性名,该属性不会出现在 for…in 循环中,也不会被 Object.keys()、Object.entries()、Object.values()、Object.getOwnPropertyNames() 、JSON.stringify ()返回
  6. Symbol 值作为属性名时,该属性还是公开属性,不是私有属性。
  7. Symbol.for()为 Symbol 值登记的名字是全局环境的,不管有没有在全局环境运行。

11个内置的 Symbol 值

https://es6.ruanyifeng.com/#docs/symbol#Symbol-hasInstance

Symbol.hasInstance

指向一个内部方法。当其他对象使用instanceof运算符,判断是否为该对象的实例时,会调用这个方法。

Symbol.isConcatSpreadable

该属性等于一个布尔值,表示该对象用于Array.prototype.concat()时,是否可以展开

Symbol.species

该属性指向一个构造函数。创建衍生对象时就会使用这个属性返回的函数,作为构造函数。

class MyArray extends Array {
}

const a = new MyArray(123);
const b = a.map(x => x);
const c = a.filter(x => x > 1);

instanceof MyArray // true
instanceof MyArray // true

// 上边代码中 子类MyArray继承了父类Array,a是MyArray的实例,b和c是a的衍生对象。

由于定义了Symbol.species属性,访问b实例的构造函数不再返回MyArray,而是Array。但是b的原型链没有改变

class MyArray extends Array {
  static get [Symbol.species]() { return Array; }
}

const a = new MyArray();
const b = a.map(x => x);

instanceof MyArray // false
instanceof Array // true

Symbol.species的作用在于,实例对象在运行过程中,需要再次调用自身的构造函数时,会调用该属性指定的构造函数。它主要的用途是,有些类库是在基类的基础上修改的,那么子类使用继承的方法时,作者可能希望返回基类的实例,而不是子类的实例。https://es6.ruanyifeng.com/#docs/symbol#Symbol-species

Symbol.match

该属性指向一个函数。当执行str.match(myObject)时,如果该属性存在,会调用它,返回该方法的返回值。

Symbol.replace

指向一个方法,当该对象被String.prototype.replace方法调用时,会返回该方法的返回值。

Symbol.search

该属性指向一个方法,当该对象被String.prototype.search方法调用时,会返回该方法的返回值。

Symbol.split

该属性指向一个方法,当该对象被String.prototype.split方法调用时,会返回该方法的返回值。

Symbol.iterator

该属性指向该对象的默认遍历器方法。对象进行for...of循环时,会调用Symbol.iterator方法,返回该对象的默认遍历器,

Symbol.toPrimitive

该属性指向一个方法。该对象被转为原始类型的值时,会调用这个方法,返回该对象对应的原始类型值。

Symbol.toStringTag

该属性,指向一个方法。在该对象上面调用Object.prototype.toString方法时,用来定制[object Object]或[object Array]中object后面的那个字符串。

Symbol.unscopables

该属性指向一个对象。该对象指定了使用with关键字时,哪些属性会被with环境排除。

比如,数组的以下几个属性会被with环境排除

不过,可以通过指定Symbol.unscopables属性,使得with语法块不会在当前作用域寻找指定属性,即将指定属性指向外层作用域的变量。

// 没有设置 Symbol.unscopables 时
class MyClass {
  foo() { return 1; }
}

var foo = function (return 2; };

with (MyClass.prototype) {
  foo(); // 1
}

// 有 Symbol.unscopables 时
class MyClass {
  foo() { return 1; }
  get [Symbol.unscopables]() {
    return { footrue };
  }
}

var foo = function (return 2; };

with (MyClass.prototype) {
  foo(); // 2
}

END
愿你历尽千帆,归来仍是少年。


让我们一起携手同走前端路!

关注公众号回复【加群】即可

● 工作中常见页面布局的n种实现方法

● 三栏响应式布局(左右固宽中间自适应)的5种方法

● 两栏自适应布局的n种实现方法汇总

● 工作中常见的两栏布局案例及分析

● 垂直居中布局的一百种实现方式

● 常用九宫格布局的几大方法汇总

● 为什么操作DOM会影响WEB应用的性能?

● 移动端滚动穿透的6种解决方案

● Vue + TypeScript 踩坑总结

浏览 64
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报