编程语言应用

首页 » 常识 » 问答 » ECMAScript6入门Symbol
TUhjnbcbe - 2023/7/12 20:06:00

概述

ES5的对象属性名都是字符串,这容易造成属性名的冲突。比如,你使用了一个他人提供的对象,但又想为这个对象添加新的方法(mixin模式),新方法的名字就有可能与现有方法产生冲突。如果有一种机制,保证每个属性的名字都是独一无二的就好了,这样就从根本上防止属性名的冲突。这就是ES6引入Symbol的原因。

ES6引入了一种新的原始数据类型Symbol,表示独一无二的值。它属于JavaScript语言的数据类型之一,其他数据类型是:undefined、null、布尔值(Boolean)、字符串(String)、数值(Number)、大整数(BigInt)、对象(Object)。

Symbol值通过Symbol()函数生成。这就是说,对象的属性名现在可以有两种类型,一种是原来就有的字符串,另一种就是新增的Symbol类型。凡是属性名属于Symbol类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。

lets=Symbol();typeofs//"symbol"

上面代码中,变量s就是一个独一无二的值。typeof运算符的结果,表明变量s是Symbol数据类型,而不是字符串之类的其他类型。

注意,Symbol函数前不能使用new命令,否则会报错。这是因为生成的Symbol是一个原始类型的值,不是对象。也就是说,由于Symbol值不是对象,所以不能添加属性。基本上,它是一种类似于字符串的数据类型。

Symbol函数可以接受一个字符串作为参数,表示对Symbol实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。

lets1=Symbol(foo);lets2=Symbol(bar);s1//Symbol(foo)s2//Symbol(bar)s1.toString()//"Symbol(foo)"s2.toString()//"Symbol(bar)"

上面代码中,s1和s2是两个Symbol值。如果不加参数,它们在控制台的输出都是Symbol(),不利于区分。有了参数以后,就等于为它们加上了描述,输出的时候就能够分清,到底是哪一个值。

如果Symbol的参数是一个对象,就会调用该对象的toString方法,将其转为字符串,然后才生成一个Symbol值。

constobj={toString(){turnabc;}};constsym=Symbol(obj);sym//Symbol(abc)

注意,Symbol函数的参数只是表示对当前Symbol值的描述,因此相同参数的Symbol函数的返回值是不相等的。

//没有参数的情况lets1=Symbol();lets2=Symbol();s1===s2//false//有参数的情况lets1=Symbol(foo);lets2=Symbol(foo);s1===s2//false

上面代码中,s1和s2都是Symbol函数的返回值,而且参数相同,但是它们是不相等的。

Symbol值不能与其他类型的值进行运算,会报错。

letsym=Symbol(Mysymbol);"yoursymbolis"+sym//TypeError:cantconvertsymboltostring`yoursymbolis${sym}`//TypeError:cantconvertsymboltostring

但是,Symbol值可以显式转为字符串。

letsym=Symbol(Mysymbol);String(sym)//Symbol(Mysymbol)sym.toString()//Symbol(Mysymbol)

另外,Symbol值也可以转为布尔值,但是不能转为数值。

letsym=Symbol();Boolean(sym)//true!sym//falseif(sym){//...}Number(sym)//TypeErrorsym+2//TypeError

Symbol.prototype.description

创建Symbol的时候,可以添加一个描述。

constsym=Symbol(foo);

上面代码中,sym的描述就是字符串foo。

但是,读取这个描述需要将Symbol显式转为字符串,即下面的写法。

constsym=Symbol(foo);String(sym)//"Symbol(foo)"sym.toString()//"Symbol(foo)"

上面的用法不是很方便。ES提供了一个实例属性description,直接返回Symbol的描述。

constsym=Symbol(foo);sym.description//"foo"

作为属性名的Symbol

由于每一个Symbol值都是不相等的,这意味着Symbol值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性。这对于一个对象由多个模块构成的情况非常有用,能防止某一个键被不小心改写或覆盖。

letmySymbol=Symbol();//第一种写法leta={};a[mySymbol]=Hello!;//第二种写法leta={[mySymbol]:Hello!};//第三种写法leta={};Object.defineProperty(a,mySymbol,{value:Hello!});//以上写法都得到同样结果a[mySymbol]//"Hello!"

上面代码通过方括号结构和Object.defineProperty,将对象的属性名指定为一个Symbol值。

注意,Symbol值作为对象属性名时,不能用点运算符。

constmySymbol=Symbol();consta={};a.mySymbol=Hello!;a[mySymbol]//undefineda[mySymbol]//"Hello!"

上面代码中,因为点运算符后面总是字符串,所以不会读取mySymbol作为标识名所指代的那个值,导致a的属性名实际上是一个字符串,而不是一个Symbol值。

同理,在对象的内部,使用Symbol值定义属性时,Symbol值必须放在方括号之中。

lets=Symbol();letobj={[s]:function(arg){...}};obj[s]();

上面代码中,如果s不放在方括号中,该属性的键名就是字符串s,而不是s所代表的那个Symbol值。

采用增强的对象写法,上面代码的obj对象可以写得更简洁一些。

letobj={[s](arg){...}};

Symbol类型还可以用于定义一组常量,保证这组常量的值都是不相等的。

constlog={};log.levels={DEBUG:Symbol(debug),INFO:Symbol(info),WARN:Symbol(warn)};console.log(log.levels.DEBUG,debugmessage);console.log(log.levels.INFO,infomessage);

下面是另外一个例子。

constCOLOR_RED=Symbol();constCOLOR_GREEN=Symbol();functiongetComplement(color){switch(color){caseCOLOR_RED:turnCOLOR_GREEN;caseCOLOR_GREEN:turnCOLOR_RED;default:thrownewError(Undefinedcolor);}}

常量使用Symbol值最大的好处,就是其他任何值都不可能有相同的值了,因此可以保证上面的switch语句会按设计的方式工作。

还有一点需要注意,Symbol值作为属性名时,该属性还是公开属性,不是私有属性。

实例:消除魔术字符串

魔术字符串指的是,在代码之中多次出现、与代码形成强耦合的某一个具体的字符串或者数值。风格良好的代码,应该尽量消除魔术字符串,改由含义清晰的变量代替。

functiongetAa(shape,options){letaa=0;switch(shape){caseTriangle://魔术字符串aa=.5*options.width*options.height;bak;/*...mocode...*/}turnaa;}getAa(Triangle,{width:,height:});//魔术字符串

上面代码中,字符串Triangle就是一个魔术字符串。它多次出现,与代码形成“强耦合”,不利于将来的修改和维护。

常用的消除魔术字符串的方法,就是把它写成一个变量。

constshapeType={triangle:Triangle};functiongetAa(shape,options){letaa=0;switch(shape){caseshapeType.triangle:aa=.5*options.width*options.height;bak;}turnaa;}getAa(shapeType.triangle,{width:,height:});

上面代码中,我们把Triangle写成shapeType对象的triangle属性,这样就消除了强耦合。

如果仔细分析,可以发现shapeType.triangle等于哪个值并不重要,只要确保不会跟其他shapeType属性的值冲突即可。因此,这里就很适合改用Symbol值。

constshapeType={triangle:Symbol()};

上面代码中,除了将shapeType.triangle的值设为一个Symbol,其他地方都不用修改。

属性名的遍历

Symbol作为属性名,遍历对象的时候,该属性不会出现在for...in、for...of循环中,也不会被Object.keys()、Object.getOwnPropertyNames()、JSON.stringify()返回。

但是,它也不是私有属性,有一个Object.getOwnPropertySymbols()方法,可以获取指定对象的所有Symbol属性名。该方法返回一个数组,成员是当前对象的所有用作属性名的Symbol值。

constobj={};leta=Symbol(a);letb=Symbol(b);obj[a]=Hello;obj=World;constobjectSymbols=Object.getOwnPropertySymbols(obj);objectSymbols//[Symbol(a),Symbol(b)]

上面代码是Object.getOwnPropertySymbols()方法的示例,可以获取所有Symbol属性名。

下面是另一个例子,Object.getOwnPropertySymbols()方法与for...in循环、Object.getOwnPropertyNames方法进行对比的例子。

constobj={};constfoo=Symbol(foo);obj[foo]=bar;for(letiinobj){console.log(i);//无输出}Object.getOwnPropertyNames(obj)//[]Object.getOwnPropertySymbols(obj)//[Symbol(foo)]

上面代码中,使用for...in循环和Object.getOwnPropertyNames()方法都得不到Symbol键名,需要使用Object.getOwnPropertySymbols()方法。

另一个新的API,Reflect.ownKeys()方法可以返回所有类型的键名,包括常规键名和Symbol键名。

letobj={[Symbol(my_key)]:1,enum:2,nonEnum:3};Reflect.ownKeys(obj)//["enum","nonEnum",Symbol(my_key)]

由于以Symbol值作为键名,不会被常规方法遍历得到。我们可以利用这个特性,为对象定义一些非私有的、但又希望只用于内部的方法。

letsize=Symbol(size);classCollection{constructor(){this[size]=0;}add(item){this[this[size]]=item;this[size]++;}staticsizeOf(instance){turninstance[size];}}letx=newCollection();Collection.sizeOf(x)//0x.add(foo);Collection.sizeOf(x)//1Object.keys(x)//[0]Object.getOwnPropertyNames(x)//[0]Object.getOwnPropertySymbols(x)//[Symbol(size)]

上面代码中,对象x的size属性是一个Symbol值,所以Object.keys(x)、Object.getOwnPropertyNames(x)都无法获取它。这就造成了一种非私有的内部方法的效果。

Symbol.for(),Symbol.keyFor()

有时,我们希望重新使用同一个Symbol值,Symbol.for()方法可以做到这一点。它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的Symbol值。如果有,就返回这个Symbol值,否则就新建一个以该字符串为名称的Symbol值,并将其注册到全局。

lets1=Symbol.for(foo);lets2=Symbol.for(foo);s1===s2//true

上面代码中,s1和s2都是Symbol值,但是它们都是由同样参数的Symbol.for方法生成的,所以实际上是同一个值。

Symbol.for()与Symbol()这两种写法,都会生成新的Symbol。它们的区别是,前者会被登记在全局环境中供搜索,后者不会。Symbol.for()不会每次调用就返回一个新的Symbol类型的值,而是会先检查给定的key是否已经存在,如果不存在才会新建一个值。比如,如果你调用Symbol.for("cat")30次,每次都会返回同一个Symbol值,但是调用Symbol("cat")30次,会返回30个不同的Symbol值。

Symbol.for("bar")===Symbol.for("bar")//trueSymbol("bar")===Symbol("bar")//false

上面代码中,由于Symbol()写法没有登记机制,所以每次调用都会返回一个不同的值。

Symbol.keyFor()方法返回一个已登记的Symbol类型值的key。

lets1=Symbol.for("foo");Symbol.keyFor(s1)//"foo"lets2=Symbol("foo");Symbol.keyFor(s2)//undefined

上面代码中,变量s2属于未登记的Symbol值,所以返回undefined。

注意,Symbol.for()为Symbol值登记的名字,是全局环境的,不管有没有在全局环境运行。

functionfoo(){turnSymbol.for(bar);}constx=foo();consty=Symbol.for(bar);console.log(x===y);//true

上面代码中,Symbol.for(bar)是函数内部运行的,但是生成的Symbol值是登记在全局环境的。所以,第二次运行Symbol.for(bar)可以取到这个Symbol值。

Symbol.for()的这个全局登记特性,可以用在不同的iframe或serviceworker中取到同一个值。

iframe=document.cateElement(iframe);iframe.src=String(window.location);document.body.appendChild(iframe);iframe.contentWindow.Symbol.for(foo)===Symbol.for(foo)//true

上面代码中,iframe窗口生成的Symbol值,可以在主页面得到。

实例:模块的Singleton模式

Singleton模式指的是调用一个类,任何时候返回的都是同一个实例。

对于Node来说,模块文件可以看成是一个类。怎么保证每次执行这个模块文件,返回的都是同一个实例呢?

很容易想到,可以把实例放到顶层对象global。

//mod.jsfunctionA(){this.foo=hello;}if(!global._foo){global._foo=newA();}module.exports=global._foo;

然后,加载上面的mod.js。

consta=qui(./mod.js);console.log(a.foo);

上面代码中,变量a任何时候加载的都是A的同一个实例。

但是,这里有一个问题,全局变量global._foo是可写的,任何文件都可以修改。

global._foo={foo:world};consta=qui(./mod.js);console.log(a.foo);

上面的代码,会使得加载mod.js的脚本都失真。

为了防止这种情况出现,我们就可以使用Symbol。

//mod.jsconstFOO_KEY=Symbol.for(foo);functionA(){this.foo=hello;}if(!global[FOO_KEY]){global[FOO_KEY]=newA();}module.exports=global[FOO_KEY];

上面代码中,可以保证global[FOO_KEY]不会被无意间覆盖,但还是可以被改写。

global[Symbol.for(foo)]={foo:world};consta=qui(./mod.js);

如果键名使用Symbol方法生成,那么外部将无法引用这个值,当然也就无法改写。

//mod.jsconstFOO_KEY=Symbol(foo);//后面代码相同……

上面代码将导致其他脚本都无法引用FOO_KEY。但这样也有一个问题,就是如果多次执行这个脚本,每次得到的FOO_KEY都是不一样的。虽然Node会将脚本的执行结果缓存,一般情况下,不会多次执行同一个脚本,但是用户可以手动清除缓存,所以也不是绝对可靠。

内置的Symbol值

除了定义自己使用的Symbol值以外,ES6还提供了11个内置的Symbol值,指向语言内部使用的方法。

Symbol.hasInstance

对象的Symbol.hasInstance属性,指向一个内部方法。当其他对象使用instanceof运算符,判断是否为该对象的实例时,会调用这个方法。比如,fooinstanceofFoo在语言内部,实际调用的是Foo[Symbol.hasInstance](foo)。

classMyClass{[Symbol.hasInstance](foo){turnfooinstanceofArray;}}[1,2,3]instanceofnewMyClass()//true

上面代码中,MyClass是一个类,newMyClass()会返回一个实例。该实例的Symbol.hasInstance方法,会在进行instanceof运算时自动调用,判断左侧的运算子是否为Array的实例。

下面是另一个例子。

classEven{static[Symbol.hasInstance](obj){turnNumber(obj)%2===0;}}//等同于constEven={[Symbol.hasInstance](obj){turnNumber(obj)%2===0;}};1instanceofEven//false2instanceofEven//true45instanceofEven//false

Symbol.isConcatSpadable

对象的Symbol.isConcatSpadable属性等于一个布尔值,表示该对象用于Array.prototype.concat()时,是否可以展开。

letarr1=[c,d];[a,b].concat(arr1,e)//[a,b,c,d,e]arr1[Symbol.isConcatSpadable]//undefinedletarr2=[c,d];arr2[Symbol.isConcatSpadable]=false;[a,b].concat(arr2,e)//[a,b,[c,d],e]

上面代码说明,数组的默认行为是可以展开,Symbol.isConcatSpadable默认等于undefined。该属性等于true时,也有展开的效果。

类似数组的对象正好相反,默认不展开。它的Symbol.isConcatSpadable属性设为true,才可以展开。

letobj={length:2,0:c,1:d};[a,b].concat(obj,e)//[a,b,obj,e]obj[Symbol.isConcatSpadable]=true;[a,b].concat(obj,e)//[a,b,c,d,e]

Symbol.isConcatSpadable属性也可以定义在类里面。

classA1extendsArray{constructor(args){super(args);this[Symbol.isConcatSpadable]=true;}}classA2extendsArray{constructor(args){super(args);}get[Symbol.isConcatSpadable](){turnfalse;}}leta1=newA1();a1[0]=3;a1[1]=4;leta2=newA2();a2[0]=5;a2[1]=6;[1,2].concat(a1).concat(a2)//[1,2,3,4,[5,6]]

上面代码中,类A1是可展开的,类A2是不可展开的,所以使用concat时有不一样的结果。

注意,Symbol.isConcatSpadable的位置差异,A1是定义在实例上,A2是定义在类本身,效果相同。

Symbol.species

对象的Symbol.species属性,指向一个构造函数。创建衍生对象时,会使用该属性。

classMyArrayextendsArray{}consta=newMyArray(1,2,3);constb=a.map(x=x);constc=a.filter(x=x1);binstanceofMyArray//truecinstanceofMyArray//true

上面代码中,子类MyArray继承了父类Array,a是MyArray的实例,b和c是a的衍生对象。你可能会认为,b和c都是调用数组方法生成的,所以应该是数组(Array的实例),但实际上它们也是MyArray的实例。

Symbol.species属性就是为了解决这个问题而提供的。现在,我们可以为MyArray设置Symbol.species属性。

classMyArrayextendsArray{staticget[Symbol.species](){turnArray;}}

上面代码中,由于定义了Symbol.species属性,创建衍生对象时就会使用这个属性返回的函数,作为构造函数。这个例子也说明,定义Symbol.species属性要采用get取值器。默认的Symbol.species属性等同于下面的写法。

staticget[Symbol.species](){turnthis;}

现在,再来看前面的例子。

classMyArrayextendsArray{staticget[Symbol.species](){turnArray;}}consta=newMyArray();constb=a.map(x=x);binstanceofMyArray//falsebinstanceofArray//true

上面代码中,a.map(x=x)生成的衍生对象,就不是MyArray的实例,而直接就是Array的实例。

再看一个例子。

classT1extendsPromise{}classT2extendsPromise{staticget[Symbol.species](){turnPromise;}}newT1(r=r()).then(v=v)instanceofT1//truenewT2(r=r()).then(v=v)instanceofT2//false

上面代码中,T2定义了Symbol.species属性,T1没有。结果就导致了创建衍生对象时(then方法),T1调用的是自身的构造方法,而T2调用的是Promise的构造方法。

总之,Symbol.species的作用在于,实例对象在运行过程中,需要再次调用自身的构造函数时,会调用该属性指定的构造函数。它主要的用途是,有些类库是在基类的基础上修改的,那么子类使用继承的方法时,作者可能希望返回基类的实例,而不是子类的实例。

Symbol.match

对象的Symbol.match属性,指向一个函数。当执行str.match(myObject)时,如果该属性存在,会调用它,返回该方法的返回值。

String.prototype.match(gexp)//等同于gexp[Symbol.match](this)classMyMatcher{[Symbol.match](string){turnhelloworld.indexOf(string);}}e.match(newMyMatcher())//1

Symbol.place

对象的Symbol.place属性,指向一个方法,当该对象被String.prototype.place方法调用时,会返回该方法的返回值。

String.prototype.place(searchValue,placeValue)//等同于searchValue[Symbol.place](this,placeValue)

下面是一个例子。

constx={};x[Symbol.place]=(...s)=console.log(s);Hello.place(x,World)//["Hello","World"]

Symbol.place方法会收到两个参数,第一个参数是place方法正在作用的对象,上面例子是Hello,第二个参数是替换后的值,上面例子是World。

Symbol.search

对象的Symbol.search属性,指向一个方法,当该对象被String.prototype.search方法调用时,会返回该方法的返回值。

String.prototype.search(gexp)//等同于gexp[Symbol.search](this)classMySearch{constructor(value){this.value=value;}[Symbol.search](string){turnstring.indexOf(this.value);}}foobar.search(newMySearch(foo))//0

Symbol.split

对象的Symbol.split属性,指向一个方法,当该对象被String.prototype.split方法调用时,会返回该方法的返回值。

String.prototype.split(separator,limit)//等同于separator[Symbol.split](this,limit)

下面是一个例子。

classMySplitter{constructor(value){this.value=value;}[Symbol.split](string){letindex=string.indexOf(this.value);if(index===-1){turnstring;}turn[string.substr(0,index),string.substr(index+this.value.length)];}}foobar.split(newMySplitter(foo))//[,bar]foobar.split(newMySplitter(bar))//[foo,]foobar.split(newMySplitter(baz))//foobar

上面方法使用Symbol.split方法,重新定义了字符串对象的split方法的行为,

Symbol.iterator

对象的Symbol.iterator属性,指向该对象的默认遍历器方法。

constmyIterable={};myIterable[Symbol.iterator]=function*(){yield1;yield2;yield3;};[...myIterable]//[1,2,3]

对象进行for...of循环时,会调用Symbol.iterator方法,返回该对象的默认遍历器,详细介绍参见《Iterator和for...of循环》一章。

classCollection{*[Symbol.iterator](){leti=0;while(this!==undefined){yieldthis;++i;}}}letmyCollection=newCollection();myCollection[0]=1;myCollection[1]=2;for(letvalueofmyCollection){console.log(value);}//1//2

Symbol.toPrimitive

对象的Symbol.toPrimitive属性,指向一个方法。该对象被转为原始类型的值时,会调用这个方法,返回该对象对应的原始类型值。

Symbol.toPrimitive被调用时,会接受一个字符串参数,表示当前运算的模式,一共有三种模式。

Number:该场合需要转成数值String:该场合需要转成字符串Default:该场合可以转成数值,也可以转成字符串

letobj={[Symbol.toPrimitive](hint){switch(hint){casenumber:turn;casestring:turnstr;casedefault:turndefault;default:thrownewError();}}};2*obj//+obj//3defaultobj==default//trueString(obj)//str

Symbol.toStringTag

对象的Symbol.toStringTag属性,指向一个方法。在该对象上面调用Object.prototype.toString方法时,如果这个属性存在,它的返回值会出现在toString方法返回的字符串之中,表示对象的类型。也就是说,这个属性可以用来定制[objectObject]或[objectArray]中object后面的那个字符串。

//例一({[Symbol.toStringTag]:Foo}.toString())//"[objectFoo]"//例二classCollection{get[Symbol.toStringTag](){turnxxx;}}letx=newCollection();Object.prototype.toString.call(x)//"[objectxxx]"

ES6新增内置对象的Symbol.toStringTag属性值如下。

JSON[Symbol.toStringTag]:JSONMath[Symbol.toStringTag]:MathModule对象M[Symbol.toStringTag]:ModuleArrayBuffer.prototype[Symbol.toStringTag]:ArrayBufferDataView.prototype[Symbol.toStringTag]:DataViewMap.prototype[Symbol.toStringTag]:MapPromise.prototype[Symbol.toStringTag]:PromiseSet.prototype[Symbol.toStringTag]:Set%TypedArray%.prototype[Symbol.toStringTag]:Uint8Array等WeakMap.prototype[Symbol.toStringTag]:WeakMapWeakSet.prototype[Symbol.toStringTag]:WeakSet%MapIteratorPrototype%[Symbol.toStringTag]:MapIterator%SetIteratorPrototype%[Symbol.toStringTag]:SetIterator%StringIteratorPrototype%[Symbol.toStringTag]:StringIteratorSymbol.prototype[Symbol.toStringTag]:SymbolGenerator.prototype[Symbol.toStringTag]:GeneratorGeneratorFunction.prototype[Symbol.toStringTag]:GeneratorFunction

Symbol.unscopables

对象的Symbol.unscopables属性,指向一个对象。该对象指定了使用with关键字时,哪些属性会被with环境排除。

Array.prototype[Symbol.unscopables]//{//copyWithin:true,//entries:true,//fill:true,//find:true,//findIndex:true,//includes:true,//keys:true//}Object.keys(Array.prototype[Symbol.unscopables])//[copyWithin,entries,fill,find,findIndex,includes,keys]

上面代码说明,数组有7个属性,会被with命令排除。

//没有unscopables时classMyClass{foo(){turn1;}}varfoo=function(){turn2;};with(MyClass.prototype){foo();//1}//有unscopables时classMyClass{foo(){turn1;}get[Symbol.unscopables](){turn{foo:true};}}varfoo=function(){turn2;};with(MyClass.prototype){foo();//2}

上面代码通过指定Symbol.unscopables属性,使得with语法块不会在当前作用域寻找foo属性,即foo将指向外层作用域的变量。

Symbol

“Symbol”值表示唯一的标识符。

可以使用Symbol()来创建这种类型的值:

//id是symbol的一个实例化对象letid=Symbol();

创建时,我们可以给Symbol一个描述(也称为Symbol名),这在代码调试时非常有用:

//id是描述为"id"的Symbolletid=Symbol("id");

Symbol保证是唯一的。即使我们创建了许多具有相同描述的Symbol,它们的值也是不同。描述只是一个标签,不影响任何东西。

例如,这里有两个描述相同的Symbol——它们不相等:

letid1=Symbol("id");letid2=Symbol("id");alert(id1==id2);//false

如果你熟悉Ruby或者其他有“Symbol”的语言——别被误导。JavaScript的Symbol是不同的。

Symbol不会被自动转换为字符串

JavaScript中的大多数值都支持字符串的隐式转换。例如,我们可以alert任何值,都可以生效。Symbol比较特殊,它不会被自动转换。

例如,这个alert将会提示出错:

letid=Symbol("id");alert(id);//类型错误:无法将Symbol值转换为字符串。

这是一种防止混乱的“语言保护”,因为字符串和Symbol有本质上的不同,不应该意外地将它们转换成另一个。

如果我们真的想显示一个Symbol,我们需要在它上面调用.toString(),如下所示:

letid=Symbol("id");alert(id.toString());//Symbol(id),现在它有效了

或者获取symbol.description属性,只显示描述(description):

letid=Symbol("id");alert(id.description);//id

“隐藏”属性

Symbol允许我们创建对象的“隐藏”属性,代码的任何其他部分都不能意外访问或重写这些属性。

例如,如果我们使用的是属于第三方代码的user对象,我们想要给它们添加一些标识符。

我们可以给它们使用Symbol键:

letuser={//属于另一个代码name:"John"};letid=Symbol("id");user[id]=1;alert(user[id]);//我们可以使用Symbol作为键来访问数据

使用Symbol("id")作为键,比起用字符串"id"来有什么好处呢?

因为user对象属于其他的代码,那些代码也会使用这个对象,所以我们不应该在它上面直接添加任何字段,这样很不安全。但是你添加的Symbol属性不会被意外访问到,第三方代码根本不会看到它,所以使用Symbol基本上不会有问题。

另外,假设另一个脚本希望在user中有自己的标识符,以实现自己的目的。这可能是另一个JavaScript库,因此脚本之间完全不了解彼此。

然后该脚本可以创建自己的Symbol("id"),像这样:

//...letid=Symbol("id");user[id]="Theiridvalue";

我们的标识符和它们的标识符之间不会有冲突,因为Symbol总是不同的,即使它们有相同的名字。

……但如果我们处于同样的目的,使用字符串"id"而不是用symbol,那么就会出现冲突:

letuser={name:"John"};//我们的脚本使用了"id"属性。user.id="Ouridvalue";//……另一个脚本也想将"id"用于它的目的……user.id="Theiridvalue"//砰!无意中被另一个脚本重写了id!

对象字面量中的Symbol

如果我们要在对象字面量{...}中使用Symbol,则需要使用方括号把它括起来。

就像这样:

letid=Symbol("id");letuser={name:"John",[id]://而不是"id":};

这是因为我们需要变量id的值作为键,而不是字符串“id”。

Symbol在for…in中会被跳过

Symbol属性不参与for..in循环。

例如:

letid=Symbol("id");letuser={name:"John",age:30,[id]:};for(letkeyinuser)alert(key);//name,age(nosymbols)//使用Symbol任务直接访问alert("Dict:"+user[id]);

Object.keys(user)也会忽略它们。这是一般“隐藏符号属性”原则的一部分。如果另一个脚本或库遍历我们的对象,它不会意外地访问到符号属性。

相反,Object.assign会同时复制字符串和symbol属性:

letid=Symbol("id");letuser={[id]:};letclone=Object.assign({},user);alert(clone[id]);//

这里并不矛盾,就是这样设计的。这里的想法是当我们克隆或者合并一个object时,通常希望所有属性被复制(包括像id这样的Symbol)。

全局symbol

正如我们所看到的,通常所有的Symbol都是不同的,即使它们有相同的名字。但有时我们想要名字相同的Symbol具有相同的实体。例如,应用程序的不同部分想要访问的Symbol"id"指的是完全相同的属性。

为了实现这一点,这里有一个全局Symbol注册表。我们可以在其中创建Symbol并在稍后访问它们,它可以确保每次访问相同名字的Symbol时,返回的都是相同的Symbol。

要从注册表中读取(不存在则创建)Symbol,请使用Symbol.for(key)。

该调用会检查全局注册表,如果有一个描述为key的Symbol,则返回该Symbol,否则将创建一个新Symbol(Symbol(key)),并通过给定的key将其存储在注册表中。

例如:

//从全局注册表中读取letid=Symbol.for("id");//如果该Symbol不存在,则创建它//再次读取(可能是在代码中的另一个位置)letidAgain=Symbol.for("id");//相同的Symbolalert(id===idAgain);//true

注册表内的Symbol被称为全局Symbol。如果我们想要一个应用程序范围内的Symbol,可以在代码中随处访问——这就是它们的用途。

这听起来像Ruby

在一些编程语言中,例如Ruby,每个名字都有一个Symbol。

正如我们所看到的,在JavaScript中,全局Symbol也是这样的。

Symbol.keyFor

对于全局Symbol,不仅有Symbol.for(key)按名字返回一个Symbol,还有一个反向调用:Symbol.keyFor(sym),它的作用完全反过来:通过全局Symbol返回一个名字。

例如:

//通过name获取Symbolletsym=Symbol.for("name");letsym2=Symbol.for("id");//通过Symbol获取namealert(Symbol.keyFor(sym));//namealert(Symbol.keyFor(sym2));//id

Symbol.keyFor内部使用全局Symbol注册表来查找Symbol的键。所以它不适用于非全局Symbol。如果Symbol不是全局的,它将无法找到它并返回undefined。

也就是说,任何Symbol都具有description属性。

例如:

letglobalSymbol=Symbol.for("name");letlocalSymbol=Symbol("name");alert(Symbol.keyFor(globalSymbol));//name,全局Symbolalert(Symbol.keyFor(localSymbol));//undefined,非全局alert(localSymbol.description);//name

系统Symbol

JavaScript内部有很多“系统”Symbol,我们可以使用它们来微调对象的各个方面。

它们都被列在了众所周知的Symbol表的规范中:

Symbol.hasInstanceSymbol.isConcatSpadableSymbol.iteratorSymbol.toPrimitive……等等。

例如,Symbol.toPrimitive允许我们将对象描述为原始值转换。我们很快就会看到它的使用。

当我们研究相应的语言特征时,我们对其他的Symbol也会慢慢熟悉起来。

总结

Symbol是唯一标识符的基本类型

Symbol是使用带有可选描述(name)的Symbol()调用创建的。

Symbol总是不同的值,即使它们有相同的名字。如果我们希望同名的Symbol相等,那么我们应该使用全局注册表:Symbol.for(key)返回(如果需要的话则创建)一个以key作为名字的全局Symbol。使用Symbol.for多次调用key相同的Symbol时,返回的就是同一个Symbol。

Symbol有两个主要的使用场景:

“隐藏”对象属性。如果我们想要向“属于”另一个脚本或者库的对象添加一个属性,我们可以创建一个Symbol并使用它作为属性的键。Symbol属性不会出现在for..in中,因此它不会意外地被与其他属性一起处理。并且,它不会被直接访问,因为另一个脚本没有我们的symbol。因此,该属性将受到保护,防止被意外使用或重写。因此我们可以使用Symbol属性“秘密地”将一些东西隐藏到我们需要的对象中,但其他地方看不到它。JavaScript使用了许多系统Symbol,这些Symbol可以作为Symbol.*访问。我们可以使用它们来改变一些内建行为。例如,在本教程的后面部分,我们将使用Symbol.iterator来进行迭代操作,使用Symbol.toPrimitive来设置对象原始值的转换等等。

从技术上说,Symbol不是%隐藏的。有一个内建方法Object.getOwnPropertySymbols(obj)允许我们获取所有的Symbol。还有一个名为Reflect.ownKeys(obj)的方法可以返回一个对象的所有键,包括Symbol。所以它们并不是真正的隐藏。但是大多数库、内建方法和语法结构都没有使用这些方法。

1
查看完整版本: ECMAScript6入门Symbol