js之Symbol类型
文章目录
ECMA2019资料
引入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资料:
欢迎转载,转载请注明出处