你了解过Symbol嘛|小册免费学

306 阅读5分钟

很多小伙伴对于Symbol的了解就是ES6新增的基本数据类型,用于创建一个唯一值可以解决key名重复的问题。本文主要探讨几个对于我们有所帮助的几个Symbol函数提供的方法,可以用于啥

Symbol.toStringTag

Symbol.toStringTag这个方法的核心用处就是可以用来检测数据类型,我们都知道检测数据类型最准确的就是Object.prototype.toString.call(),为啥一定要是Object原型上的toString?因为其他类各自实现了属于自己的toString方法按照原型查找机制也就找不到Object原型上的toString方法

对于原型上没有去实现toString的就可以直接通过隐式调用Object.prototype上的toString来返回对应构造函数的name

JSON.toString() // "[object JSON]"
Math.toString()  // => "[object Math]"
(new Map()).toString() // => "[object Map]"
(new Set()).toString() // => "[object Set]"
...

以上这些都是类型的原型上没有实现自己的toSring方法所以,可以直接通过原型链找到Objec基类上的toString来通过toStringTag获取到对应构造函数(类)的name

下面通过自定义一个类的toStringTag来看看是不是跟上面说的一样: 调用toSring是通过隐示读取toStringTag来获构造函数的name

let stringTagName = ''
class Beige {
  constructor() {
   	// new.target属性: 用于构造函数之中,返回new命令作用于的那个构造函数
    // new.target.name === this.constructor.name 
    stringTagName = new.target.name
    this.constructor
  }
  // Symbol.toStringTag是内置成员且不允许我们修改
  // 所以我们可以通过getter来监听: 当toString读取Symbol.toStringTag的时候来返回自己想返回的数据
  get [Symbol.toStringTag]() {
    return stringTagName
  }
}

let beige = new Beige()
console.log(beige.toString()); // [object Beige]

1.png

dir这个类发现我们是没有给原型上去定义toString方法的,所以通过原型链查找机制找到了Object.prototype.toString,该方法内部通过toStringTag来返回了我们定义的Beige(也就是类的名称)

下面我们在原型上加个String方法来验证下Object.prototype.toString会隐示读取类原型上的Symbol.toStringTag

class Beige {
  get [Symbol.toStringTag]() {
    console.log('你读取了Symbol.toStringTag');
  }
}
(new Beige().toString())  // 你读取了Symbol.toStringTag


// 在原型上写个toString方法
class Beige {
  get [Symbol.toStringTag]() {
    console.log('你读取了Symbol.toStringTag');
  }
+ toString() {
+  return '重写String'
+ }
}
(new Beige().toString()) // 没有没有任何输出

扩展new.target

target是ES6给new命令引入的成员属性: 该属性返回了new命令作用于的那个构造函数;这个方法一般用于我们通过写函数的方式来实现类会用到

// 通过new.target来明确该函数是一个构造函数只能通过new来调用
function Beige(name) {
  if (new.target !== Beige) {
    throw new Error(`${Beige.name}必须使用 new 命令生成实例`)
  }

  this.name = name
}

let beige = new Beige('a')
Beige.call(beige, 'b') // => 报错
Reflect.construct(beige.constructor, ['b'], Array.constructor) // => 报错


// class的实现方式一定要通过new来生成实例,不然过不了语法校验
class Beige(name) {
}
Beige.call(beige, 'b') // TypeError Class constructor Beige cannot be invoked without 'new'

相反的我们也可以通过new.traget来明确某些方法是个静态方法不允许去通过new来生成实例

function symbol() {
  if (new.target) {
    throw new Error(`TypeError: ${symbol.name} is not a constructor`)
  }
}

let s = new symbol() // symbol is not a constructor

扩展Reflect.construct()

该等同于new target(...args),可以不使用new,来调用构造函数的方法。且

class Beige {
  constructor(publicAccounts, ...skill) {
    // new.target => Array 
    this.public = publicAccounts
    this.skill = skill
  }
}

let publicAccounts = '前端自学驿站'
let skill = ['JS', 'Vue', 'Webpack', 'Node']
 
let beige = Reflect.construct(
  Beige,  // 要执行的函数
 [publicAccounts, ...skill], // 执行函数的入参必须是类数组Array|argumentsList
 Array // 作为新创建对象的原型链对象的constructor() => 实例对象.__proto__.constructor
)

2.png

typeof beige // => 'object' 

虽然是对象类型,但将Array的prototype作为了实例对象的原型链

beige.skill.push('Babel') // => ['JS', 'Vue', 'Webpack', 'Node', 'Babel']
// 通过Reflect.construct挂载的成员默认不可枚举
beige.push('1') // beige.length === 1

Symbol.toPrimitive

Symbol.toStringTag这个方法的核心用处就是数据类型转换, 比如下面这个面试题

let a = ?
if (a == 1 && a == 2 & a == 3) {
    console.log(1)
}
// 怎么样可以让结果输入1???

这种很多方式可以实现: 重写toString/valueOf/gettter监听,重写toString/valueOf是因为"=="隐式转换会调用

这里梳理下隐式转换的条件:

  1. 当使用 ==&&|| 等逻辑操作符进行判断时
  2. 当使用 + - * / % 四则运算符进行操作时

逻辑操作符操作符隐式转换

// || && 肯定是都将左边转换为Boolean,这大家编程中都常用就不多bb了


// 这里探讨下(==)两等号: 只会在类型不同时进行转换
// 也只有下面两种特殊情况需要特殊记忆,其他情况下都是把类型转换为Number进行比较
null == undefine // => true
NaN == NaN // => false

四则运算符隐式转换

同样都是将类型转换为Number, 只有'+'比较特殊,因为'+'在js中也有字符拼接的作用,所以当类型不同出现'+'甭管是啥肯定都得转换为字符进行拼接

[] + {} 
// [] => '' 
// {} => "[object Object]"
// => [object Object]

回到正题,我们说了这么多隐式转换,那对于最终转换的类型是怎么来确定的?

console.log(+[1]) // number: 1
console.log(`${{}}`) // string: "[object Object]"

他昨就知道我上面最终是number(1),我为啥不能给他转成string(1)??

let obj = {
  name: '北歌',

  [Symbol.toPrimitive](hint) {
    console.log(hint);
  }
}

console.log(obj); // hint =>  {name: "北歌", Symbol(Symbol.toPrimitive): ƒ}
console.log(+obj); // hint => number
console.log(obj + {}); // hint => default
console.log(`${obj}`); // hint => string

hint参数表示我们在读取这个对象原始值的时候是要转换为那种预期类型

let a = ?
if (a == 1 && a == 2 & a == 3) {
    console.log(1)
}
// 回到这道题上来
let a = {
  value: 0,
 	[Symbol.toPrimitive]() {
    return ++this.value
 	}
}

我们通过重写Symbol.toPrimitive,在读取a的原始值的时候就返回了基本类型Number(1),也就不用去调用valueOf|toString

对于Object类型转换基本值具体规则如图下所示

3.png

写在最后

如果文章中有那块写的不太好或有问题欢迎大家指出,我也会在后面的文章不停修改。也希望自己进步的同时能跟你们一起成长。喜欢我文章的朋友们也可以关注一下

我会很感激第一批关注我的人。此时,年轻的我和你,轻装上阵;而后,富裕的你和我,满载而归。

往期文章

【建议追更】以模块化的思想来搭建中后台项目

【以模块化的思想开发中后台项目】第一章

【前端体系】从一道面试题谈谈对EventLoop的理解(更新了四道进阶题的解析)

【前端体系】从地基开始打造一座万丈高楼

【前端体系】正则在开发中的应用场景可不只是规则校验

「函数式编程的实用场景 | 掘金技术征文-双节特别篇」

【建议收藏】css晦涩难懂的点都在这啦