开篇

ES6之前我们都清楚JS有六种数据类型:Undefined、Null、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object),今天笔者讲的Symbol类型是ES6才加入的,它最大的特点就如标题所说“独一无二”。

本篇文章将从以下方面进行介绍:

  • 值类型和引用类型介绍

  • 如何声明一个Symbol?

  • 为什么要有Symbol?

  • Symbol的常用用法

  • 内置常用Symbol值的用法

本篇文章阅读时间预计15分钟

01

值类型和引用类型介绍

在了解Symbol之前,我们需要了解下JS的数据类型,在JS中数据类型分为两类:值类型和引用类型。

  • 值类型:数值型(Number),字符类型(String),布尔值型(Boolean),null 和 underfined

  • 引用类型:对象(Object)

所谓的值类型可以这样理解:变量之间的互相赋值,是指开辟一块新的内存空间,将变量值赋给新变量保存到新开辟的内存里面;之后两个变量的值变动互不影响。 如下段代码所示:

let weChatName ="前端达人";//开辟一块内存空间保存变量 weChatName 的值“前端达人”;let touTiao =weChatName;//给变量 touTiao 开辟一块新的内存空间,将 weChatName 的值 “前端达人” 赋值一份保存到新的内存里;//weChatName 和 touTiao 的值以后无论如何变化,都不会影响到对方的值;

一些语言,比如 C,有引用传递和值传递的概念。JS 也有类似的概念,它是根据传递的数据类型推断的。如果将值传递给函数,则重新分配该值不会修改调用位置中的值。但是,如果你修改的是引用类型,那么修改后的值也将在调用它的地方被修改。

所谓的引用类型可以这样理解:变量之间的互相赋值,只是指针的交换,而并非将对象复制一份给新的变量,对象依然还是只有一个,只是多了一个指引~~;如下段代码所示:

let weChat = { name: "前端达人", regYear:"2014" };//需要开辟内存空间保存对象,变量 weChat 的值是一个地址,这个地址指向保存对象的空间;let touTiao= weChat;// 将 weChat 的指引地址赋值给 touTiao,而并非复制一给对象且新开一块内存空间来保存;weChat.regYear="2018";console.log(touTiao);//output:{ name: '前端达人', regYear: '2018' }// 这个时候通过 weChat 来修改对象的属性,则通过 touTiao 来查看属性时对象属性已经发生改变;

那Symbol是什么数据类型呢?这里笔者先告诉大家是值类型,下面会有详细的介绍。

02

如何声明一个Symbol?

Symbol最大的特点就如本篇文章的标题一样:独一无二。这个独一无二怎么解释呢?就好比双胞胎,外表看不出差别,但是相对个体比如性格爱好还是有差异的,每个人都是独一无二。Symbol表示独一无二的值,是一种互不等价标识,声明Symbol十分简单,如下段代码所示:

const s = Symbol();

Symbol([description]) 声明方式,支持一个可选参数,只是用于描述,方便我们开发调试而已。每次执行Symbol()都会生成一个独一无二的Symbol值,如下段代码所示:

let s1 = Symbol("My Symbol");let s2 = Symbol("My Symbol");console.log(s1 === s2); // Outputs false”

由此可见,即使Symbol的描述值参数相同,它们的值也不相同,描述值仅仅是起描述的作用,不会对Symbol值本身起到任何的改变。关于描述值需要注意的一点:接受除Symbol值以外所有的值,怎么理解呢,请看下段代码所示:

const symbol = Symbol();const symbolWithString=Symbol('前端达人');//Symbol(前端达人)const SymbolWithNum=Symbol(3.14);//Symbol(3.14)const SymbolWithObj=Symbol({foo:'bar'});//Symbol([object Object])const anotherSymbol=Symbol(symbol);//TypeError: Cannot convert a Symbol value to a string

接下来笔者来详细解释下,为什么Symbol是值类型,而不是引用类型。Symbol函数并不是构造函数,因此不能使用new方法来生成一个Symbol对象,否则编译器会抛出异常,如执行下段代码所示:

new Symbol();//TypeError: Symbol is not a constructor

由此可见,Symbol是一种值类型而非引用类型,这就意味着如果将Symbol作为参数传值的话,将会是值传值而非引用传值,如下段代码所示(值的改变没有互相影响):

const symbol=Symbol('前端达人');function fn1(_symbol) {    return _symbol==symbol;}console.log(fn1(symbol));//output:true;function fn2(_symbol) {    _symbol=null;    console.log(_symbol);}fn2(symbol);//output:null;console.log(symbol);//Symbol(前端达人)

03

为什么要有Symbol?

介绍了这么多,Symbol存在的意义是什么?笔者先举个简单的业务场景:

在前端的JavaScript应用开发中,需要先通过渲染引擎所提供的API来获取一个DOM元素对象,并保留在JavaScript运行时中。因为业务需要,需要通过一个第三方库对这个DOM元素对象进行一些修饰和调整,即对该DOM元素对象进行一些新属性的插入。

而后来因为新需求的出现,需要再次利用另外一个第三方库对同一个DOM元素对象进行修饰。但非常不巧的是这个第三方库同样需要对该DOM元素对象进行属性插入,而恰好这个库所需要操作的属性与前一个第三方库所操作的属性相同。这种情况下就很有可能会出现两个第三方库都无法正常运行的现象,而使用这些第三方库的开发者却难以进行定位和修复。

针对上述问题, Symbol可以提供一种良好的解决方案。这是因为Symbol的实例值带有互不等价的特性,即任意两个Symbol值都不相等。在ES2015标准中,字面量对象除了可以使用字符串、数字作为属性键以外,还可以使用Symbol作为属性键,因此便可以利用Symbol值的互不等价特性来实现属性操作的互不干扰了。

04

Symbol的常用用法

1、判断是否是Symbol

如何判断一个变量是不是Symbol类型呢?目前唯一的方法就是使用typeof,如下段代码所示:

const s = Symbol();console.log(typeof s);//Outputs "symbol”

2、用作对象的属性

通常我们使用字符串定义对象的属性(Key),有了Symbol类型后,我们当然可以使用Symbol作为对象的属性,唯一不同的地方,我们需要使用[]语法定义属性,如下段代码所示:

const WECHAT_NAME = Symbol();const WECHAT_REG = Symbol();let obj = {    [WECHAT_NAME]: "前端达人";}obj[WECHAT_REG] = 2014;console.log(obj[WECHAT_NAME]); //output: 前端达人console.log(obj[WECHAT_REG]); //output:2014

还有一点需要强调的是,使用Symbol作为对象的Key值时,具有私有性,我们无法通过枚举获取Key值,如下段代码所示:

let obj = {    weChatName:'前端达人',    regYear: 2014,    [Symbol('pwd')]: 'wjqw@$#sndk9012',};console.log(Object.keys(obj));// ['weChatName', 'regYear']for (let p in obj) {    console.log(p)    // 分别会输出:'weChatName' 和 'regYear'}console.log(Object.getOwnPropertyNames(obj));// [ 'weChatName', 'regYear' ]

从上述代码中,可以看出Symbol类型的key是不能通过Object.keys()或者for...in来枚举的,它未被包含在对象自身的属性名集合(property names)之中。利用该特性,我们可以把一些不需要对外操作和访问的属性可以使用Symbol来定义。由于这一特性的存在,我们使用JSON.stringify()将对象转换成JSON字符串的时候,Symbol属性也会被排除在输出内容之外,在上述代码中执行下段代码:

console.log(JSON.stringify(obj));//output:{"weChatName":"前端达人","regYear":2014}

基于这一特性,我们可以更好的去设计我们的数据对象,让“对内操作”和“对外选择性输出”变得更加灵活。

我们难道就没有办法获取Symbol方式定义的对象属性了么?私有并不是绝对的,我们可以通过一些API函数进行获取,在上述代码中执行下段代码:

// 使用Object的APIconsole.log(Object.getOwnPropertySymbols(obj)); // [Symbol(pwd)]
// 使用新增的反射APIconsole.log(Reflect.ownKeys(obj));// [Symbol(pwd), 'age', 'title']


3、定义类的私有属性/方法

我们都清楚在JS中,是没有如Java等面向对象语言的访问控制关键字private的,类上所有定义的属性或方法都是可公开访问的。上面笔者讲到作为对象属性具有私有性的特点,我们定义类的私有属性和方法才能实现,如下段代码所示:

我们先建立一个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,引入a.js文件,如下所示:

import  Login from './a.js';const login = new Login('admin', '123456');console.log(login.checkPassword('123456'));  // trueconsole.log(login.PASSWORD);  // undefinedconsole.log(login[PASSWORD]);// PASSWORD is not definedconsole.log(login["PASSWORD"]); // undefined

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

4、创建共享Symbol

虽然Symbol是独一无二的,但是有些业务场景,我们需要共享一个Symbol,我们如何实现呢?这种情况下,我们就需要使用另一个API来创建或获取Symbol,那就是Symbol.for(),它可以注册或获取一个全局的Symbol实例,如下段代码所示:

let obj = {};(function(){    let s1 = Symbol("name");    obj[s1] = "Eden";})();console.log(obj[s1]);//SyntaxError: Unexpected identifier cannot be accessed here(function(){    let s2 = Symbol.for("age");    obj[s2] = 27;})();console.log(obj[Symbol.for("age")]); //Output "27”

从上述代码可以看出,Symbol.for()会注册一个全局作用域的Symbol值,如果这个Key值从未使用则会进行创建注册,如果已被注册,则会返回一个与第一次使用创建的Symbol值等价的Symbol,如下段代码所示:

const symbol=Symbol.for('foo');const obj={};obj[symbol]='bar';const anotherSymbol=Symbol.for('foo');console.log(symbol===anotherSymbol);//output:trueconsole.log(obj[anotherSymbol]);//output:bar


05

常用Symbol值及意义

我们除了可以自行创建Symbol值以外,ES6还将其应用到了ECMAScript引擎的各个角落,我们可以运用这些常用值对底层代码的实现逻辑进行修改,以实现更高级的定制化的需求。

以下表格进行了常用Symbol值的总结:

定义项

描述

含义

@@iterator

"Symbol.iterator"

用于为对象定义一个方法并返回一个属于所对应对象的迭代器。该迭代器会被for-of循环使用。

@@hasInstance

"Symbol.hasInStance"

用于为类定义一个方法。该方法会因为instanceof语句的使用而被调用,来检查一个对象是否是某一个类的实例。

@@match

"Symobol.match"

用于正则表达式定义一个可被String.prototype.match()方法使用的方法,检查对应字符串与当前正则表达式是否匹配

@@replace

"Symbol.replace"

用于正则表达式会对象定义一个方法。该方法会因为String.prototype.replace()方法的使用而被调用,用于处理当前字符串使用该正则表达式或对象作为替换标志时的内部处理逻辑

@@search

"Symbol.search"

用于正则表达式会对象定义一个方法。该方法会因为String.prototype.search()方法的使用而被调用,用于处理当前字符串使用该正则表达式或对象作为位置检索标志时的内部处理逻辑

@@split

"Symbol.split"

用于正则表达式会对象定义一个方法。该方法会因为String.prototype.split()方法的使用而被调用,用于处理当前字符串使用该正则表达式或对象作为分割标志时的内部处理逻辑

@@unscopables

"Symbol.unscopables"

用于为对象定义一个属性。该属性用于描述该对象中哪些属性是可以被with语句所使用的。

@@isConcatSpreadable

"Symbol.isConcatSpreadable"

用于为对象定义一个属性。该属性用于决定该对象作为Array.prototype.concat()方法参数时,是否被展开。

@@species

"Symbol.species"

用于为类定义一个静态属性,该属性用于决定该类的默认构建函数。

@@toPrimitive

"Symbol.toPrimitive"

用于为对象定义一个方法。该方法会在该对象需要转换为值类型的时候被调用,可以根据程序的行为决定该对象需要被转换成的值。

@@toStringTag

"Symbol.toStringTag"

用于为类定义一个属性。该属性可以决定这个类的实例在调用toString()方法时,其中标签的内容。

由于常用Symbol值比较多,笔者只对其中最常用的几个进行解释。

1、Symbol.iterator

我们可以使用Symbol.iterator来自定义一个可以迭代的对象,我们可以使用Symbol.iterator作为方法名的方法属性,该方法返回一个迭代器(Iterator)。虽然JS中没有协议(Protocal)的概念,我们可以将迭代器看做成一个协议,即迭代器协议(Iterator Protocal),该协议定义了一个方法next(),含义是进入下一次迭代的迭代状态,第一次执行即返回第一次的迭代状态,该迭代状态有两个属性,如表格所示:

定义项

描述

含义

done

Boolean

该迭代器是否已经迭代结束

value

Any

当前迭代状态值

以下是我们使用Symbol.iterator带迭代的方法,如下段代码所示:

let obj = {    array: [1, 2, 3, 4, 5],    nextIndex: 0,    [Symbol.iterator]: function(){        return {            array: this.array,            nextIndex: this.nextIndex,            next: function(){                return this.nextIndex < this.array.length ?                    {value: this.array[this.nextIndex++], done: false} :                    {done: true};            }        }    }};let iterable = obj[Symbol.iterator]();console.log(iterable.next().value);console.log(iterable.next().value);console.log(iterable.next().value);console.log(iterable.next().value);console.log(iterable.next().value);console.log(iterable.next().done);

以上代码将会输出:

12345true

除了可以自定义迭代的逻辑,我们也可以使用引擎默认的迭代,从而节省了我们的代码量,如下段代码所示:

const arr = [1, 2];const iterator = arr[Symbol.iterator](); // returns you an iteratorconsole.log(iterator.next());console.log(iterator.next());console.log(iterator.next());

以上代码将会输出:

{ value: 1, done: false }{ value: 2, done: false }{ value: undefined, done: true }

2、Symbol.hasInstance

用于为类定义一个方法。该方法会因为instanceof语句的使用而被调用,来检查一个对象是否是某一个类的实例, 用于扩展instanceof的内部逻辑,我们可以用于为一个类定一个静态方法,该方法的第一个形参便是被检测的对象,而自定义的方法内容决定了instanceof语句的返回结果,代码如下:

class Foo{    static [Symbol.hasInstance](obj){        console.log(obj);        return true;    }}console.log( {} instanceof  Foo);

以上代码将会输出:

{}true


3、Symbol.match

Symbol.match 在字符串使用match()方法时,为其实现自定义的逻辑。如下段代码所示:

没自定义前:

const re=/foo/console.log('bar'.match(re));//nullconsole.log('foo'.match(re));//[ 'foo', index: 0, input: 'foo', groups: undefined ]

使用Symbol.match后:

const re=/foo/;re[Symbol.match]=function (str) {    const regexp=this;    console.log(str);    return true;};console.log('bar'.match(re));console.log('foo'.match(re));

上段代码将会输出:

bartruefootrue


4、Symbol.toPrimitive

在JS开发中,我们会利用其中的隐式转换规则,其中就包括将引用类型转换成值类型,然而有时隐式转换的结果并不是我们所期望的。虽然我们可以重写toString()方法来自定义对象在隐式转换成字符串的处理,但是如果出现需要转换成数字时变得无从入手。我们可以使用Symbol.toPrimitive来定义更灵活处理方式,如下段代码所示(仅为演示,可结合自己的业务自行修改):

const obj={};console.log(+obj);console.log(`${obj}`);console.log(obj+"");//output://NaN//[object Object]//[object Object]const transTen={    [Symbol.toPrimitive](hint){        switch (hint) {            case 'number':                return 10;            case 'string':                return 'Ten';            default:                return true;        }    }}console.log(+transTen);console.log(`${transTen}`);console.log(transTen+"");//output://10//Ten//true

5、Symbol.toStringTag

前面的表格提到过,Symbol.toStringTag的作用就是自定义这个类的实例在调用toString()时的标签内容。比如我们在开发中定义的类,就可以通过Symbol.toStringTag来修改toString()中的内容,利用它做为属性键为类型定一个Getter。如下段代码所示:

class Foo{    get [Symbol.toStringTag](){return 'Bar'}}const obj=new Foo();console.log(obj.toString());//output:[object Bar]


05

小节

今天的内容有些多,需要慢慢理解,我们清楚了Symbol值是独一无二的,Symbol的一些使用场景,以及使用Symbol常用值改写更底层的方法,让我们写出更灵活的处理逻辑。Symbol虽然强大,但是用好它还需要在实践中结合业务场景进行掌握。

精彩推荐

ES6基础丨let和作用域

ES6基础丨const介绍

ES6基础丨默认参数值

ES6基础丨展开语法(Spread syntax)

ES6基础丨解构赋值(destructuring assignment)

ES6基础丨箭头函数(Arrow functions)

ES6基础丨模板字符串(Template String)

ES6基础丨Set与WeakSet

ES6基础丨Map与WeakMap

JavaScript基础丨前端不懂它,会再多框架也不过只是会用而已!

JavaScript基础丨你真的了解JavaScript吗?

JavaScript基础丨回调(callback)是什么?

JavaScript基础丨Promise使用指南

JavaScript基础丨深入学习async/await

JS加载慢?谷歌大神带你飞!(文末送电子书)

19年你应该关注这50款前端热门工具(上)

19年你应该关注这50款前端热门工具(中)

19年你应该关注这50款前端热门工具(下)

专注分享当下最实用的前端技术。关注前端达人,与达人一起学习进步!

长按关注"前端达人"

【ES6基础】Symbol介绍:独一无二的值相关推荐

  1. android log 如何获取double类型后小数点的值_【ES6基础】Symbol介绍:独一无二的值...

    开篇 ES6之前我们都清楚JS有六种数据类型:Undefined.Null.布尔值(Boolean).字符串(String).数值(Number).对象(Object),今天笔者讲的Symbol类型是 ...

  2. ES6基础1—面向过程的数据结构

    一 .基本数据类型 1.数值 2.字符串 1).字符串处理 2).`` 模版字符串 3.Symbol 1). 创建Symbol的两种方式 2). 对Symbol的操作 二.集合 1.数组 Array ...

  3. 如何修改Series和DataFrame类型中的元素值_「ES6基础」Symbol介绍:独一无二的值

    ES6之前我们都清楚JS有六种数据类型:Undefined.Null.布尔值(Boolean).字符串(String).数值(Number).对象(Object),今天笔者讲的Symbol类型是ES6 ...

  4. golang基础-etcd介绍与使用、etcd存取值、etcd监测数据写入

    关注公众号"风色年代"订阅更多精彩文章,本博大部分文章为转载并已标明原文出处,如有再转敬请保留,请自觉尊重原创作者的劳动成果! golang基础-etcd介绍与使用.etcd存取值 ...

  5. 【ES6基础】Object的新方法

    Object对象可谓是JS的重要核心内容,在你使用JS的过程中,你会发现自己的工作大部分都是在操作对象,ES6.ES7.ES8引入了不少新的方法,本篇文章笔者将带着大家一起熟悉下重点的新方法. 本篇文 ...

  6. ES6基础4(数据结构)-学习笔记

    文章目录 ES6基础4(数据结构)-学习笔记 set map symbol ES6基础4(数据结构)-学习笔记 set //set 数据结构 类似数组 成员信息唯一性var s = new Set() ...

  7. 【ES6基础】Map与WeakMap

    开篇 ES6里除了增加了Set(集合)类型外(笔者在这篇文章<Set与WeakSet>有过介绍),今天的这篇文章笔者将继续介绍ES6引入的新类型--Map(映射类型)和其对应的弱类型Wea ...

  8. 带你一文读懂Javascript中ES6的Symbol

    带你一文读懂Javascript中ES6的Symbol 前言 基础类型 Symbol Symbol.for 与 Symbol.keyFor Symbol.iterator Symbol.search ...

  9. ES6新增 Symbol

    Symbol的诞生? Symbol的诞生,也就是Symbol存在的意义 之前我们的对象属性的数据类型都是字符串,没有其他的了.所以会导致属性名重复,导致属性值被覆盖的情况.比如,你使用了一个他人提供的 ...

最新文章

  1. 2022-2028年中国第五代移动通信技术(5G)市场研究及前瞻分析报告
  2. 关于Advertising Campaign
  3. python输入多行字符串_python中怎么输入多行字符串
  4. Revit API取得变量的内参名称
  5. VTK:绘图之FunctionalBagPlot
  6. hdu4821 字符串hash(有多少(M*L长的,M个不相同)子串))
  7. js 两个map合并为一个map_ArcGIS API for JS3.x教程二:构建第一个简单的程序
  8. 图片资源添加出现问题: No resource found that matches the given name
  9. c#构造器的一点理解(三)
  10. JAVA生成条码(jbarcode)
  11. IS-IS认证原理(华为设备)
  12. sqlserver200864位下载_sql2008r2企业版下载-sql2008r2安装包64位 最新版 - 极光下载站...
  13. 下列关于linux扩展名说法错误的是,全国计算机一级考试选择题集锦(2015年1月)
  14. 2019 vs 查看类图结构_在建筑网站上使用单页设计还是多页设计哪个更好_学云网...
  15. 【编程入门】密码破译
  16. LeetCode:数组刷题(17道经典题目)
  17. 【科软课程-信息安全】Lab13 Packet Sniffing and Spoofing
  18. 视频融合技术平台解决方案
  19. 算法系列之二十:计算中国农历(一)
  20. Android Launcher3简介

热门文章

  1. 推荐一款Gin+Vue+ElementUI实现的智慧城市后台管理系统
  2. Python常用内建模块(内含实例)
  3. Java日志框架SLF4J和log4j以及logback的联系和区别
  4. 从本地加载FASHION MNIST数据集并输入到模型进行训练
  5. sql联合查询(内联、左联、右联、全联)
  6. github使用AccessToken登录配置教程
  7. 文件上传漏洞及常见的利用方式
  8. 【综合】在VirtualBox虚拟机内装Mac OS X 苹果黑系统
  9. 堆内存与栈内存的区别?
  10. TCP会话劫持攻击实验