详解JavaScript 为什么要有 Symbol 类型?
Symbols是ES6引入了一个新的数据类型,它为JS带来了一些好处,尤其是对象属性时。但是,它们能为我们做些字符串不能做的事情呢?
在深入探讨Symbol之前,让我们先看看一些JavaScript特性,许多开发人员可能不知道这些特性。
背景
js中的数据类型总体来说分为两种,他们分别是:值类型和引用类型
值类型(基本类型):数值型(Number),字符类型(String),布尔值型(Boolean),null和underfined
引用类型(类):函数,对象,数组等
值类型理解:变量之间的互相赋值,是指开辟一块新的内存空间,将变量值赋给新变量保存到新开辟的内存里面;之后两个变量的值变动互不影响,例如:
vara=10;//开辟一块内存空间保存变量a的值“10”; varb=a;//给变量b开辟一块新的内存空间,将a的值“10”赋值一份保存到新的内存里; //a和b的值以后无论如何变化,都不会影响到对方的值;
一些语言,比如C,有引用传递和值传递的概念。JavaScript也有类似的概念,它是根据传递的数据类型推断的。如果将值传递给函数,则重新分配该值不会修改调用位置中的值。但是,如果你修改的是引用类型,那么修改后的值也将在调用它的地方被修改。
引用类型理解:变量之间的互相赋值,只是指针的交换,而并非将对象(普通对象,函数对象,数组对象)复制一份给新的变量,对象依然还是只有一个,只是多了一个指引~~;例如:
vara={x:1,y:2};//需要开辟内存空间保存对象,变量a的值是一个地址,这个地址指向保存对象的空间; varb=a;//将a的指引地址赋值给b,而并非复制一给对象且新开一块内存空间来保存; //这个时候通过a来修改对象的属性,则通过b来查看属性时对象属性已经发生改变;
值类型(神秘的NaN值除外)将始终与具有相同值的另一个值类型的完全相等,如下:
constfirst="abc"+"def"; constsecond="ab"+"cd"+"ef"; console.log(first===second);//true
但是完全相同结构的引用类型是不相等的:
constobj1={name:"Intrinsic"}; constobj2={name:"Intrinsic"}; console.log(obj1===obj2);//false //但是,它们的.name属性是基本类型: console.log(obj1.name===obj2.name);//true
对象在JavaScript语言中扮演重要角色,它们的使用无处不在。对象通常用作键/值对的集合,然而,以这种方式使用它们有一个很大的限制:在symbol出现之前,对象键只能是字符串,如果试图使用非字符串值作为对象的键,那么该值将被强制转换为字符串,如下:
constobj={}; obj.foo='foo'; obj['bar']='bar'; obj[2]=2; obj[{}]='someobj'; console.log(obj); //{'2':2,foo:'foo',bar:'bar', '[objectObject]':'someobj'}
Symbol是什么
Symbol()函数会返回symbol类型的值,该类型具有静态属性和静态方法。它的静态属性会暴露几个内建的成员对象;它的静态方法会暴露全局的symbol注册,且类似于内建对象类,但作为构造函数来说它并不完整,因为它不支持语法:“newSymbol()”。所以使用Symbol生成的值是不相等:
consts1=Symbol(); consts2=Symbol(); console.log(s1===s2);//false
实例化symbol时,有一个可选的第一个参数,你可以选择为其提供字符串。此值旨在用于调试代码,否则它不会真正影响symbol本身。
consts1=Symbol("debug"); conststr="debug"; consts2=Symbol("xxyy"); console.log(s1===str);//false console.log(s1===s2);//false console.log(s1);//Symbol(debug)
symbol作为对象属性
symbol还有另一个重要的用途,它们可以用作对象中的键,如下:
constobj={}; constsym=Symbol(); obj[sym]="foo"; obj.bar="bar"; console.log(obj);//{bar:'bar'} console.log(syminobj);//true console.log(obj[sym]);//foo console.log(Object.keys(obj));//['bar']
乍一看,这看起来就像可以使用symbol在对象上创建私有属性,许多其他编程语言在其类中有自己的私有属性,私有属性遗漏一直被视为JavaScript的缺点。
不幸的是,与该对象交互的代码仍然可以访问其键为symbol的属性。在调用代码尚不能访问symbol本身的情况下,这甚至是可能的。例如,Reflect.ownKeys()方法能够获取对象上所有键的列表,包括字符串和symbol:
functiontryToAddPrivate(o){ o[Symbol("PseudoPrivate")]=42; } constobj={prop:"hello"}; tryToAddPrivate(obj); console.log(Reflect.ownKeys(obj)); //['prop',Symbol(PseudoPrivate)] console.log(obj[Reflect.ownKeys(obj)[1]]);//42
注意:目前正在做一些工作来处理在JavaScript中向类添加私有属性的问题。这个特性的名称被称为私有字段,虽然这不会使所有对象受益,但会使类实例的对象受益。私有字段从Chrome74开始可用。
代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log调试,这边顺便给大家推荐一个好用的BUG监控工具Fundebug。
防止属性名称冲突
符号可能不会直接受益于JavaScript为对象提供私有属性。然而,他们是有益的另一个原因。当不同的库希望向对象添加属性而不存在名称冲突的风险时,它们非常有用。
Symbol为JavaScrit对象提供私有属性还有点困难,但Symbol还有别外一个好处,就是避免当不同的库向对象添加属性存在命名冲突的风险。
考虑这样一种情况:两个不同的库想要向一个对象添加基本数据,可能它们都想在对象上设置某种标识符。通过简单地使用id作为键,这样存在一个巨大的风险,就是多个库将使用相同的键。
functionlib1tag(obj){ obj.id=42; } functionlib2tag(obj){ obj.id=369; }
通过使用Symbol,每个库可以在实例化时生成所需的Symbol。然后用生成Symbol的值做为对象的属性:
constlibrary1property=Symbol("lib1"); functionlib1tag(obj){ obj[library1property]=42; } constlibrary2property=Symbol("lib2"); functionlib2tag(obj){ obj[library2property]=369; }
出于这个原因,Symbol似乎确实有利于JavaScript。
但是,你可能会问,为什么每个库在实例化时不能简单地生成随机字符串或使用命名空间?
constlibrary1property=uuid();//randomapproach functionlib1tag(obj){ obj[library1property]=42; } constlibrary2property="LIB2-NAMESPACE-id";//namespacedapproach functionlib2tag(obj){ obj[library2property]=369; }
这种方法是没错的,这种方法实际上与Symbol的方法非常相似,除非两个库选择使用相同的属性名,否则不会有冲突的风险。
在这一点上,聪明的读者会指出,这两种方法并不完全相同。我们使用唯一名称的属性名仍然有一个缺点:它们的键非常容易找到,特别是当运行代码来迭代键或序列化对象时。考虑下面的例子:
constlibrary2property="LIB2-NAMESPACE-id";//namespaced functionlib2tag(obj){ obj[library2property]=369; } constuser={ name:"ThomasHunterII", age:32 }; lib2tag(user); JSON.stringify(user); //'{"name":"ThomasHunterII","age":32,"LIB2-NAMESPACE-id":369}'
如果我们为对象的属性名使用了Symbol,那么JSON输出将不包含它的值。这是为什么呢?虽然JavaScript获得了对Symbol的支持,但这并不意味着JSON规范已经改变!JSON只允许字符串作为键,JavaScript不会尝试在最终JSON有效负载中表示Symbol属性。
constlibrary2property="f468c902-26ed-4b2e-81d6-5775ae7eec5d";//namespacedapproach functionlib2tag(obj){ Object.defineProperty(obj,library2property,{ enumerable:false, value:369 }); } constuser={ name:"ThomasHunterII", age:32 }; lib2tag(user); console.log(user);//{name:"ThomasHunterII",age:32,f468c902-26ed-4b2e-81d6-5775ae7eec5d:369} console.log(JSON.stringify(user));//{"name":"ThomasHunterII","age":32} console.log(user[library2property]);//369
通过将enumerable属性设置为false而“隐藏”的字符串键的行为非常类似于Symbol键。它们通过Object.keys()遍历也看不到,但可以通过Reflect.ownKeys()显示,如下的示例所示:
constobj={}; obj[Symbol()]=1; Object.defineProperty(obj,"foo",{ enumberable:false, value:2 }); console.log(Object.keys(obj));//[] console.log(Reflect.ownKeys(obj));//['foo',Symbol()] console.log(JSON.stringify(obj));//{}
在这点上,我们几乎重新创建了Symbol。隐藏的字符串属性和Symbol都对序列化器隐藏。这两个属性都可以使用Reflect.ownKeys()方法读取,因此它们实际上不是私有的。假设我们为属性名的字符串版本使用某种名称空间/随机值,那么我们就消除了多个库意外发生名称冲突的风险。
但是,仍然有一个微小的区别。由于字符串是不可变的,而且Symbol总是保证惟一的,所以仍然有可能生成字符串组合会产生冲突。从数学上讲,这意味着Symbol确实提供了我们无法从字符串中得到的好处。
在Node.js中,检查对象时(例如使用console.log()),如果遇到名为inspect的对象上的方法,将调用该函数,并将打印内容。可以想象,这种行为并不是每个人都期望的,通常命名为inspect的方法经常与用户创建的对象发生冲突。
现在Symbol可用来实现这个功能,并且可以在equire(‘util').inspect.custom中使用。inspect方法在Node.jsv10中被废弃,在v11中完全被忽略,现在没有人会偶然改变检查的行为。
模拟私有属性
这里有一个有趣的方法,我们可以用来模拟对象上的私有属性。这种方法将利用另一个JavaScript特性:proxy(代理)。代理本质上封装了一个对象,并允许我们对与该对象的各种操作进行干预。
代理提供了许多方法来拦截在对象上执行的操作。我们可以使用代理来说明我们的对象上可用的属性,在这种情况下,我们将制作一个隐藏我们两个已知隐藏属性的代理,一个是字符串_favColor,另一个是分配给favBook的Symbol:
letproxy; { constfavBook=Symbol("favbook"); constobj={ name:"ThomasHunterII", age:32, _favColor:"blue", [favBook]:"Metro2033", [Symbol("visible")]:"foo" }; consthandler={ ownKeys:target=>{ constreportedKeys=[]; constactualKeys=Reflect.ownKeys(target); for(constkeyofactualKeys){ if(key===favBook||key==="_favColor"){ continue; } reportedKeys.push(key); } returnreportedKeys; } }; proxy=newProxy(obj,handler); } console.log(Object.keys(proxy));//['name','age'] console.log(Reflect.ownKeys(proxy));//['name','age',Symbol(visible)] console.log(Object.getOwnPropertyNames(proxy));//['name','age'] console.log(Object.getOwnPropertySymbols(proxy));//[Symbol(visible)] console.log(proxy._favColor);//'blue'
使用_favColor字符串很简单:只需阅读库的源代码即可。另外,通过蛮力找到动态键(例如前面的uuid示例)。但是,如果没有对Symbol的直接引用,任何人都不能从proxy对象访问'Metro2033'值。
Node.js警告:Node.js中有一个功能会破坏代理的隐私。JavaScript语言本身不存在此功能,并且不适用于其他情况,例如Web浏览器。它允许在给定代理时获得对底层对象的访问权。以下是使用此功能打破上述私有属性示例的示例:
const[originalObject]=process.binding("util").getProxyDetails(proxy); constallKeys=Reflect.ownKeys(originalObject); console.log(allKeys[3]);//Symbol(favbook)
现在,我们需要修改全局Reflect对象,或者修改util流程绑定,以防止它们在特定的Node.js实例中使用。
以上所述是小编给大家介绍的JavaScript为什么要有Symbol类型详解整合,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对毛票票网站的支持!