js之Symbol类型

ECMA2019资料

官方文档地址

ECMA2019离线版(html版+pdf版+ES6入门)

引入Symbol类型的背景

  • ES5 的对象属性名都是字符串,这容易造成属性名冲突的问题

    举例: 使用别人的模块/对象, 又想为之添加新的属性,这就容易使得新属性名与原有属性名冲突

Symbol类型简介

  • Symbol是一种原始数据类型

    • 其余原始类型: undefined 、 null 、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)
    • Symbol表示独一无二的值
    • Symbol类型的"真实值"无法获取,也就是说Symbol类型没有对应的字面量
    • Symbol类型的意义在于区分彼此和不重复,不在于真实值

Symbol特性(所有数据类型都需要探讨的问题)

创建

  • Symbol值只能通过Symbol()函数生成
let s1 s1 = = Symbol Symbol( ('foo' 'foo') ); ;
let let s2 s2 = = Symbol Symbol( ('bar' 'bar') ); ;
s1 s1 // Symbol(foo) // Symbol(foo)
s2 s2 // Symbol(bar) // Symbol(bar)
s1 s1. .toString toString( () ) // "Symbol(foo)" // "Symbol(foo)"
s2 s2. .toString toString( () ) // "Symbol(bar)"
  • Symbol()函数前不能使用new命令(Symbol类型是原始值,不是对象)
  • Symbol类型不能添加属性(不是对象)
  • Symbol类型是一种类似字符串的类型(可用作属性名

ECMA2019标准相关:
在这里插入图片描述

​ 解读: (NewTarget是使用new命令调用函数时会创建的原生对象)

​ (1) 如果NewTarget不为undefined(也就是使用了new命令), 抛出错误==>Symbol函数前不能使用new

​ (2) 描述字符串description保存在symbol值内部的[[Description]]中

Symbol值的描述

  • Symbol 函数可以接受一个字符串作为参数

  • Symbol 函数的参数只是表示对当前 Symbol 值的描述

  • Symbol值的描述: 帮助开发者区分Symbol值

    • 在控制台打印两Symbol值时,能区分开来
    • 转为字符串时,能区分开来

Symbol值的类型转换与运算

类型转换

根据ECMA2019,:

  • 类型转换是用一些抽象操作来描述的
  • 隐式转换是直接调用某一个抽象操作
  • 显示转换是抽象操作的包装(加入判断和控制等)
    • 类型的构造函数 当做 函数使用(例如: String())
    • 一些可能调用抽象操作的方法(例如: toString()valueOf())

主要的类型转换抽象操作:

1.对象转原始类型:
在这里插入图片描述
在这里插入图片描述

2.转Boolean:
在这里插入图片描述

3.转Number:
在这里插入图片描述

4.转字符串:
在这里插入图片描述

5.原始类型转对象:
在这里插入图片描述

如上图, 直接对symbol值应用抽象操作(隐式转换):

  • ToBoolean(Symbol)==>true
  • ToString(Symbol)==>报错
  • ToNumber==>报错

类型转换举例

let sym = Symbol('My symbol');

"your symbol is" + sym; //TypeError: can't convert symbol to string
`your symbol is ${sym}`; //TypeError: can't convert

String(sym); //'Symbol(My symbol)'
sym.toString(); //'Symbol(My symbol)'

为什么上述代码中String()和sym.toString()可以成功将symbol转换为字符串?

ECMA2019:
在这里插入图片描述
(1)Symbol作为原始类型, 有对应的包装对象类型, 所以我们可以用sym.toString() 调用方法而不出错.

更进一步,我们测试一下Symbol类型的实例是否可改变和添加属性:

let s = Symbol('s');

//实例对象的对象保护检测
console.log(`s实例是否可扩展: ${Object.isExtensible(s)}`);
console.log(`s实例是否被冻结: ${Object.isFrozen(s)}`);
console.log(`s实例是否被封闭: ${Object.isSealed(s)}`);
//输出:
// s实例是否可扩展: false
// s实例是否被冻结: true
// s实例是否被seal: true


//实例对象是否存在toString实例方法与[[Symbol.toStringTag]]
console.log(`s实例是否有自定义toString方法: ${s.hasOwnProperty('toString')}`);
console.log(`s实例是否有[[Symbol.toStringTag]]: ${s.hasOwnProperty(Symbol.toStringTag)}`);
//输出:
// s实例是否有自定义toString方法: false
// s实例是否有[[Symbol.toStringTag]]: false

//----------------------------------------------------------------------------------------------------------

//Symbol.prototype的对象保护检测
console.log(`Symbol.prototype是否可扩展: ${Object.isExtensible(Symbol.prototype)}`);
console.log(`Symbol.prototype是否被冻结: ${Object.isFrozen(Symbol.prototype)}`);
console.log(`Symbol.prototype是否被封闭: ${Object.isSealed(Symbol.prototype)}`);
//输出:
// Symbol.prototype是否可扩展: true
// Symbol.prototype是否被冻结: false
// Symbol.prototype是否被封闭: false

总结:

  • Symbol类的实例是不可扩展的,不能添加,修改和删除属性
  • 因此, sym.toString()所调用的,必定是Symbol.prototype.toString()原型方法
  • Symbol.prototype.toString()调用Symbol.DescriptiveString(sym),返回一个组合字符串: "Symbol(" + 描述字符串 + ")"

(2)String(sym)可行的原因:
在这里插入图片描述

可见, 在进行ToString抽象操作之前,对参数进行了判断, 并对为Symbol类型的情况另行处理(调用Symbol.DescriptiveString(value)),故而没有发生我们预期中的报错.

涉及Symbol类型的运算: ±*/%

结论: 一律报错

原因:(ECMA2019)

加法:
在这里插入图片描述

减法:
在这里插入图片描述

乘除余:
在这里插入图片描述

可见, 进行运算,必然逃不过要进行抽象操作 ToNumber,而对Symbol值进行ToNumber抽象操作就会报错.因此,Symbol值无法参与运算.


全局Symbol表

Symbol.for() 与 Symbol.keyFor()

  • 应用背景:

    • Symbol值解决了对象属性不被覆盖问题
    • 但Symbol值只能作用在局部
      • node环境: 不同时导出Symbol值, 导出对象的对应属性无法被其他模块主动访问
      • 浏览器属性: 不同时传递Symbol值, 对象的对应属性不能被其他iframe主动访问
    • 为此,出现了全局Symbol表及管理它的两个函数Symbol.for()与Symbol.keyFor()
  • Symbol.for()函数

    ECMA2019:
    在这里插入图片描述

  • Symbol.keyFor()函数

    ECMA2019:
    在这里插入图片描述

全局Symbol表模型

在这里插入图片描述

注意事项

​ Symbol.for()与Symbol.keyFor()都是针对全局Symbol表进行查询和新建的, 其余位置的Symbol值不会被访问或影响.

全局的测试

  • node: 全局环境–js程序的运行环境(在各个模块之上)
//主文件.js
const symMain = Symbol.for('Maintest');

const {sym,IsSameSymbol} = require('./附文件.js');

console.log(sym === Symbol.for('test')); //true
console.log(IsSameSymbol(symMain)); //true
//附文件.js
const sym = Symbol.for('test');
const symMain = Symbol.for('Maintest');

function IsSameSymbol(s){
	return s===symMain;
}

module.exports = {
	sym: sym,
	IsSameSymbol: IsSameSymbol
};
  • 浏览器端: 全局环境–在各个frame之上
  <!DOCTYPE html>
  <html lang="zh-cn">
  <head>
  	<meta charset="UTF-8">
  	<title>全局Symbol测试</title>
  </head>
  <body>
  	hello world!
  	<script>
  		var iframe = document.createElement('iframe');
  		iframe.src = String(window.location);
  		document.body.appendChild(iframe);
  
  		window.onload = function(){
  			alert(iframe.contentWindow.Symbol.for('test')===Symbol.for('test'));
  		};
  	</script>
  </body>
  </html>

Symbol类型的辨析和理解

  • symbol数据类型的真实值

    • 与symbol值关联的字符串其实是 它的描述,方便控制台打印时区分各个symbol值,而不是symbol值的真实值

    • symbol类型的真实值是无法获取和访问的,也并不重要,因为不会被开发者用到

    • symbol类型的意义在于它的唯一不可重复性,而不在于其真实值

  • 如何理解symbol类型的值

    • symbol类型值的特点

      • 新的原始数据类型(不同于number,string,boolean等)
      • 可以充当属性名和变量名(类似string)
      • 除非是同一句symbol()生成的值,否则不相等(类似object)

      根据以上特点,这个symbol类型的值特别像我们日常生活中的一种东西–二维码. 二维码的特点如下:

      • 属于图片(不是数值,也不是字符串)(新的类型)

      • 可以完成字符串的一些功能

    • 我们可以把symbol类型理解成 包含特定信息的二维码

      它包含的特定信息有三个:

      • 生成它的symbol()代码调用的行号
      • 生成它的symbol()代码调用的起始列号
      • 该symbol()代码调用所在文件的完整文件路径(包含文件名)

      通过这三个特定信息,我们不难看出每一个symbol()调用生成的symbol值(二维码)都一定是唯一且不可重复的: (举例说明)

      完整文件路径
      在这里插入图片描述

      行号与起始列号
      在这里插入图片描述

      行号 起始列号
      t1 1 10
      t2 1 29
      t3 2 10

      由此可见: 要想三个信息都符合, 那必须是同一个文件中的同一句Symbol()生成的同一个symbol值.因此是唯一不可重复的.

      接着,我们将这三个信息生成二维码:
      在这里插入图片描述

      接着,我们用t1代表的symbol值作为属性名给obj对象添加属性:

      var t1 = Symbol(); var t2 = Symbol();
      var t3 = Symbol();
      
      console.log(`t1==t2: ${t1==t2}
      t1==t3: ${t1==t3}
      t2==t3: ${t2==t3}`); //全是false
      
      var obj = {
      	[t1]: function(){
      		console.log('hello_world');
      	}
      };
      
      obj[t1]();
      

    我们把symbol类型值比作二维码的话, obj[t1](); 就相当于:
    在这里插入图片描述


Symbol值作为属性名

  • 概述:

    • 每个Symbol值均不相等,独一无二
    • Symbol值作为标识符
      • 完全避免属性同名问题
      • 防止方法/属性被不小心覆盖
  • 定义Symbol属性名的三种方法:

    • 当做普通字符串使用(不必加引号)

      let mySymbol = Symbol();
      
      let a ={};
      a[mySymbol] = 'hello';
      
    • 字面量对象内的方括号定义

      let mySymbol = Symbol();
      
      let a = {
      	[mySymbol]:'hello'
      };
      
    • 用Object.defineProperty定义(实质上还是直接当做普通字符串)

      let mySymbol = Symbol();
      
      let a = {};
      Object.defineProperty(a, mySymbol, {value: 'hello'});
      
  • 注意事项

    • 不能用点运算符访问Symbol名的属性

    • 字面量方括号定义法中,方括号是必须的,否则仍是一个普通字符串名属性

    • 因为每一个symbol值都是独一无二的
      要使用以symbol为名的属性,只能在定义symbol值的模块内使用
      要想将该对象的该属性开放给其模块访问,必须同时导出: 对象 + symbol值

      (该特性可以用于分配和限制属性的访问权限?)

      • 有权限的,导出symbol值
      • 无权限的,不导出symbol值

含Symbol名属性的对象的遍历

  • Symbol名属性的键值无法被常规遍历方法发现,包括:
    • for…in
    • for…of
    • Object.keys()
    • Object.getOwnPropertyNames()
    • JSON.stringify()
  • 获取方法:
    • Object.getOwnPropertySymbols()
    • Reflect.ownKeys()–常规键名+Symbol键名

ES6内置Symbol值

说明:

  • 这些值保存在Symbol的Constructor函数对象的属性之中

  • 这些值通常指向对象的内部方法

  • 通过这些Symbol值修改对象内部方法不一定有效:

    • 对象可能被冻结或封闭(例如: Symbol类型值)

    • Symbol名属性的配置项可能被设为不可配置与不可修改

      (即属性描述符为:{configurable:false,writable:false})

简介

instanceof相关–Symbol.hasInstance

foo instanceof Foo; ==> Foo[Symbol.hasInstance](foo)

下例展示了一个对instanceof的欺骗:

class Myclass{
    [Symbol.hasInstance](foo){
        return foo instanceof Array;
    }
}

[1,2,3] instanceof new Myclass; //true

类数组连接展开–Symbol.isConcatSpreadable

​ 该属性规定了该对象用于Array.prototype.concat() 时,是否可以展开

let arr1 = ['c','d'];
['a','b'].concat(arr1,'e'); //['a','b','c','d','e']
arr1[Symbol.isConcatSpreadable]; //undefined

let arr2 = ['c','d'];
arr1[Symbol.isConcatSpreadable] = false;
['a','b'].concat(arr2,'e'); //['a','b',['c','d'],'e']

ECMA2019相关资料:

(1)Array.prototype.concat
在这里插入图片描述

(2)IsConcatSpreadable(O)
在这里插入图片描述

解读:

  • 只在调用Array.prorotype.concat()函数中生效,

    • 其他concat函数中无效(没有针对该symbol名属性的操作)
  • 根据上述资料:

    • 不仅可用于数组,也可以用于类数组对象(有length属性,有数字键名)

    • 不设置该symbol名属性时(undefined值),数组与类数组表现相反:

      这是因为IsConcatSpreadble(o)资料中的最后一句:return ?IsArray(o)

      也就是说: 无定义时,数组默认展开,类数组默认不展开

    • 数组/类数组之中的空位(数字键不连续)保留到合并后的新数组

指定生成衍生对象的构造函数–访问器属性Symbol.species

  • Symbol.species

    • 是一个构造函数(类)的访问器属性,需要用get设置

    • 存在于一些可进行衍生的类中

      ECMA2019资料:
      21.2.4.2get RegExp [ @@species ]
      22.1.2.5get Array [ @@species ]
      22.2.2.4get %TypedArray% [ @@species ]
      23.1.2.2get Map [ @@species ]
      23.2.2.2get Set [ @@species ]
      24.1.3.3get ArrayBuffer [ @@species ]
      24.2.3.2get SharedArrayBuffer [ @@species ]
      25.6.4.6get Promise [ @@species ]

    • 该Symbol值代表的属性(访问器的返回值)是一个构造函数,被用于构造衍生对象

    • 默认值: this.constructor,也就是用于创建衍生对象的原对象实例的构造函数

  • 举例说明: (以数组的map方法为例)

    代码:

    class MyArray extends Array {
    	static get [Symbol.species](){
    		return Array;
    	}
    
    	introduce(){
    		console.log('I am a MyArray instance');
    	}
    }
    
    const a = new MyArray();
    const b = a.map(x=>x); //利用Array.prototype.map()创建衍生对象b
    
    console.log(a.constructor); //[Function: MyArray]
    console.log(b instanceof MyArray); //false
    console.log(b.introduce); //undefined
    

    资料及知识补充:

    Array.prototype.map
    在这里插入图片描述

    ArraySpeciesCreate
    在这里插入图片描述

由上图,可见, 确实是在创建衍生对象时,获取了原对象的构造函数的Symbol.species属性作为衍生对象的构造函数.结合示例代码:

(1)原对象: 对象a–MyArray类的实例

(2)原对象的构造函数: MyArray类的构造函数

(3)原对象的构造函数的Symbol.species: 返回值为数组的构造函数–Array

String.prototype.match匹配相关–Symbol.match

在这里插入图片描述

因此: str.match(obj)等同于obj[Symbol.match](str)

class MyMatcher {
	[Symbol.match](str){
        return 'hello world'.indexOf(str);
    }
}

'e'.match(new MyMatcher()); //1

同系列的其他Symbol:

  • Symbol.search
  • Symbol.replace
  • Symbol.split

ToPrimitive抽象操作–Symbol.toPrimitive

​ 其值是一个将对象类型转换为原始类型值的方法, 被抽象操作ToPrimitive调用.
在这里插入图片描述

如上图, 可见该函数接收一个字符串参数hint(三种取值对应三种模式):

  • Number:需要转成数值
  • String:需要转成字符串
  • Default: 数值,字符串皆可(number)

示例代码:

let obj = {
    [Symbol.toPrimitive](hint){
        switch(hint){
            case 'number': return 123;
            case 'default': return 'default';
            case 'string': return 'str';
            default: throw new Error();
        }
    }
};

console.log(2 * obj); //246
console.log(3 + obj); //3default, 此时hint为default,详见ECMA2019 加号(addtion operator)
console.log(String(obj)); //str

类型字符串–Symbol.toStringTag

对象的 Symbol.toStringTag 属性的值是一个字符串

在该对象上调用 Object.prototype.toString 方法,若该属性存在,其值会出现在 toString 方法返回的字符串之中,表示对象的类型。也就是说,这个属性可以用来定制[object Object] 或 [object Array] 中 object 后面的那个字符串.

ECMA2019资料:
在这里插入图片描述

posted @ 2020-03-15 15:19  peterzhangsnail  阅读(1229)  评论(0编辑  收藏  举报