ES6入门九:Symbol元编程
- JS第七种数据类型:Symbol
- Symbol的应用场景
- 11个Symbol静态属性
- Symbol元编程
一、JS第七种数据类型:Symbol
在ES6之前的JavaScript的基本数据类型有undefined、null、boolean、number、string、object,现在Symbol作为第七种基本数据类型。翻译symbol这个单词就是“符号,标志”的意思,顾名思义Symbol的应用场景也就离不开唯一性,想想“符号、标志”都是用来干嘛的?不就是用来标记特定事物的符号嘛,在程序中用来作为标记的不就是名称或者ID么,可能在这时候你会想到ES6提供的const常量声明字符,为什么有了const还需要Symbol呢?
注意:const常量的意思是值保持不变,而不是名称保持不变,也就说Symbol可以用来指代一个名称并保持不变的状态。
1.声明Symbol以及基本操作:
var os = Symbol("hello");
从上面示例中的Symbol变量成名中可以看到,Symbol不需要使用new关键字构造,所以Symbol不是对象,只有构造对象才使用new关键字。
typeof os ; //symbol
控制台打印Symbol类型值:
console.log(os);//Symbol(hello)
实际上在控制台打印Symbol类型值得时候Symbol()包裹得内容会发生toString()转换,来看下面这几个示例:
1 var os1 = Symbol({ 2 name:"txtx" 3 });//Symbol([object Object]) 4 5 var os2 = Symbol({ 6 name:"text", 7 toString:function(){ 8 return this.name; 9 } 10 });//Symbol(text)
2.Symbol值得唯一性:
1 var os3 = Symbol(); 2 var os4 = Symbol(); 3 console.log(os3 === os2);//false
基于Symbol实现属性名得唯一性:
1 var os5 = Symbol("text"); 2 var os6 = Symbol("text"); 3 var obj = { 4 [os5]:"hello", 5 [os6]:function(){ 6 return "hello"; 7 } 8 } 9 console.log(obj);//{Symbol(text): "hello", Symbol(text): ƒ} 10 // 字符串的属性名 11 var a = "text"; 12 var b = "text"; 13 var objStr = { 14 [a]:"hello", 15 [b]:function(){ 16 return "hello"; 17 } 18 } 19 console.log(objStr);//{text: ƒ}
3.关于Symbol值的类型转换,Symbol的值不能进行运算,否在会报错,但可以进行boolean逻辑运算,能隐式或者显式的转换成Boolean值,也可以显式的转换成字符串和对象:
1 if(os5){ 2 console.log(os6);//Symbol(text) 3 } 4 console.log(Boolean(Symbol()));//true 5 console.log(!Symbol());//false 6 console.log(String(os5));//Symbol(text) 7 console.log(Object(os6));//Symbol {Symbol(text)}
4.关于Symbol()、Symbol.for()、Symbol.keyFor()的值与使用:
Symbol()的返回值是一个全新的不重复的值;
Symbol.for()指向同一个key的Symbol值;
Symbol.keyFor()返回Symbol.for()登记在全局的Symbol值的key。
Symbol()与Symbol.for()两种方式都可以生成一个新的Symbol,前者是每次都会生成一个完全不同的Symbol,后者生成的值会被登记到全局环境中提供搜索,使用Symbol.for()生成的话会先查询全局中是否登记过同一个键的Symbol,如果有登记就直接返回该Symbol值,如果没有新生成一个并且登记到全局。(注意:Symbol.for只能查询全局登记的Symbol,所以Symbol.for()使用Symbol()使用相同的key也是不能查询到,而是Symbol.for自己生成一个全新的Symbol值登记到全局)
1 var osv1 = Symbol("value"); 2 var osv2 = Symbol.for("textValue"); 3 var obj = {}; 4 var osv3 = Symbol.for(obj); 5 var osT1 = Symbol.keyFor(osv1); 6 var osT2 = Symbol.keyFor(osv2); 7 var osT3 = Symbol.keyFor(osv3); 8 9 console.log(osT1);//undefined 10 console.log(osT2);//textValue 11 console.log(osT3);//[object Object]
通过示例可以看到,Symbol.keyFor()只能返回登记在全局的Symbol值的key,并且返回值为字符串,所以对象类型的key默认返回[object Object]。
二、Symbol的应用场景
场景一:使用Symbol值作为对象属性名
1 let obj = { 2 num:123, 3 text:"Str" 4 } 5 console.log( obj["num"] );//123 6 console.log( obj["text"] );//Str 7 8 const osNum = Symbol("num"); 9 const osText = Symbol("text"); 10 let obS = { 11 [osNum]:123, 12 [osText]:"Str" 13 } 14 console.log( obS[osNum] );//123 15 console.log( obS[osText] );//Str
看示简单的应用场景,其实这里隐藏了好几个值得注意的问题,使用Symbol值作为对象属性名不能被for...in和for...of正常枚举,不能使用Object.keys()、Object.getOwnPropertyNames()正常获取属性名,但是Object.getOwnPropertyDescriptor()获取的对象属性描述符中enumerable(枚举描述符)的值为true,也就说从对象描述符所表示的来看Symbol值作为对象属性名是可以被枚举的,但是需要一个特定的枚举方法来实现:Object.getOwnPropertySymbols()
1 for(var ele in obS){ 2 console.log(ele);//无输出 3 } 4 Object.keys(obj);//["num", "text"] 5 Object.keys(obS);//[] 6 Object.getOwnPropertyNames(obj);//["num", "text"] 7 Object.getOwnPropertyNames(obS);//[] 8 Object.getOwnPropertyDescriptor(obS,osNum) 9 //{value: 123, writable: true, enumerable: true, configurable: true} 10 Object.getOwnPropertySymbols(obS);//[Symbol(num), Symbol(text)]
由枚举的区别可以看到普通枚举方法不能枚举Symbol值作为名称的属性,这也就是说可以通过这样的特性来定义“对内操作的属性”,可以非常便捷的区分对象的“对内操作”与“对外操作”;并且这一特性被延伸到了JSON数据格式的转换中,看示例:
1 JSON.stringify(obj);//"{"num":123,"text":"Str"}" 2 JSON.stringify(obS);//"{}"
场景二:使用Symbol()替代常量值
1 const TYPE_AUDIO = Symbol(); 2 const TYPE_VIDEO = Symbol(); 3 const TYPE_IMAGE = Symbol(); 4 5 function handleFileResource(resourece){ 6 switch(resourece.type){ 7 case TYPE_AUDIO: 8 playAudio(resourece); 9 break; 10 case TYPE_VIDEO: 11 playVideo(resourece); 12 break; 13 case TYPE_IMAGE: 14 playImage(resourece); 15 break; 16 default: 17 throw new Error('Unknown type of resource'); 18 } 19 }
场景三:模块的Singleton模式(nodejs部分补充)
三、11个Symbol静态属性
1.Symbol.hasInstance:实现instanceof关键字底层计算逻辑。
//语法:判断a是否是b的构造实例 a instanceof b;
实际上instanceof的底层逻辑是b调用Symbol.hasInstance内部方法来实现的,看下面这个ES5语法示例:
1 function a(){ 2 this.name = "a"; 3 this[Symbol.hasInstance] = function(foo){ 4 return foo instanceof Array; 5 } 6 } 7 var obj = new a(); 8 console.log([1,2,3] instanceof obj);//true
再来看一下ES6语法的一个示例:
1 class Even{ 2 static name = "Even"; 3 static [Symbol.hasInstance](obj){ 4 console.log("instanceof调用了“" + this.name + "”上的Symbol.hasInstance方法,是由“" + obj + "”启动的命令。"); 5 } 6 } 7 8 1 instanceof Even;//instanceof调用了“Even”上的Symbol.hasInstance方法,是由“1”启动的命令。
注:Symbol.hasInstance只适合Object类型,不能自定义Function类型上的instanceof指令。
2.Symbol.isConcatSoreadable:该属性等于一个布尔值,表示对象使用Array.prototype.concat()时是否可以展开。
1 let arr1 = ['c','d']; 2 console.log( ['a','b'].concat(arr1,'e') ); //["a", "b", "c", "d", "e"] 3 console.log( arr1[Symbol.isConcatSpreadable] ); //undefined 4 arr1[Symbol.isConcatSpreadable] = false; 5 console.log( ['a','b'].concat(arr1,'e') ); //["a", "b", Array(2), "e"] 6 arr1[Symbol.isConcatSpreadable] = true; 7 console.log( ['a','b'].concat(arr1,'e') ); //["a", "b", "c", "d", "e"] 8 arr1[Symbol.isConcatSpreadable] = undefined; 9 console.log( ['a','b'].concat(arr1,'e') ); //["a", "b", "c", "d", "e"]
3.Symbol.species:属性指向当前对象的构造函数。创建实例时会默认调用这个方法。
1 class MyArray extends Array{ 2 static get [Symbol.species]() {return Array} 3 } 4 let a = new MyArray(1,2,3); 5 var mapped = a.map(x => x * x); 6 console.log(a); 7 console.log(a instanceof MyArray); //true 8 console.log(a instanceof Array); //true 9 10 console.log(mapped instanceof MyArray); //false 11 console.log(mapped instanceof Array); //true
4.Symbol.match:当String对象调用match方法时,实际是让匹配的正则表达式或者字符串调用Symbol.match这个标识指向的函数:
1 var a = String('abc'); 2 a[Symbol.match] = function(){ 3 console.log("match"); 4 } 5 console.log(a.match(/b/)); //["b", index: 1, input: "abc", groups: undefined] 6 7 class myMatcher extends String{ 8 static get [Symbol.species]() {return String} 9 // constructor(val){ 10 // this.value = val; 11 // } 12 [Symbol.match](str){ 13 return this.indexOf(str); 14 } 15 } 16 var b = new myMatcher("abc"); 17 console.log(b);//abc 18 console.log(a.match(b));//0 19 console.log('b'.match(b));//1
5.Symbol.replace:当字符串对象调用replace方法时,实际上是由该方法传入的参数调用Symbol.replace方法来实现的:
1 class myReplace extends String{ 2 static get [Symbol.species]() {return String} 3 [Symbol.replace](){ 4 console.log(this); 5 } 6 } 7 "abc def abc def".replace(new myReplace("abc"),'ccc'); //myReplace {"abc"}
6.Symbol.search:当字符串对象调用search方法时,实际上是由该方法传入的参数调用Symbol.search方法来实现的:
1 var str = "abcmnifjabcfsfk"; 2 String.prototype[Symbol.search] = function(string){ 3 console.log("返回" + this + "在当前字符串中的第一个匹配位置"); 4 return string.indexOf(this); 5 } 6 console.log(str.search("abc"));
7.Symbol.split:当字符串调用split方法时,实际上是由该方法传入的参数调用Symbol.split方法来实现,下面实现的是当参数为字符串时的方法,split方法参数为正则表达式时调用的是正则表达式对象原型上的Symbol.split方法(下面的示例暂时没有实现):
1 String.prototype[Symbol.split] = function(str){ 2 var arr = []; 3 var len = typeof this.toString() === "string" ? this.length : 0; 4 var index = str.indexOf(this); 5 if(index === -1 || index === 0){ 6 arr.push(str); 7 }else{ 8 arr.push(str.substr(0,index)); 9 str = str.slice(index + len,str.length) 10 arr = arr.concat(this[Symbol.split](str)); 11 } 12 return arr; 13 } 14 var str = "How are you doing today?"; 15 console.log( str.split(" ") );
8.Symbol.iterator:迭代器,在Array,Set,Map,Nodelist对象原型上都有这个迭代函数,在这篇博客不详细介绍迭代器,下面演示一个具备迭代特性的对象手动添加迭代器,让这个对象可以被循环迭代(关于迭代器会在下一篇博客中详细的解析):
1 var obj = { 2 0:"a", 3 1:"b", 4 2:"c", 5 length:3, 6 [Symbol.iterator]:function(){ 7 let curIndex = 0; 8 let next = () => { 9 return { 10 value:this[curIndex], 11 done:this.length == curIndex++, 12 } 13 } 14 return { 15 next 16 } 17 } 18 } 19 20 for(let p of obj){ 21 console.log(p); //输出a b c (如果不再obj上添加Symbol.iterator迭代方法,会报错:obj is not iterable) 22 }
9.Symbol.toPrimitive:该方法是当对象被转换为原始值类型时被触发,方法会接收到一个参数,这个参数根据执行的原始值转换类型分别为string、number、boolean三个字符串,手动实现代码如下:
1 Object.prototype[Symbol.toPrimitive] = function(hint){ 2 console.log(hint); 3 switch (hint){ 4 case 'number': 5 return this.valueOf(); 6 case 'boolean': 7 return this.valueOf(); 8 case 'string': 9 return this.toString() === '[object String]' ? this.valueOf() : this.toString(); 10 default: 11 throw new Error(); 12 } 13 14 }
10.Symbol.toStringTag:这个属性可以说是用来配置对象调用toString方法时的返回值:
1 var obj = {[Symbol.toStringTag]:"对象"}; 2 console.log(obj.toString());//[object 对象]
需要注意的是这种配置必须写在类的大括号中“{}”,否在会报错。
11.Symbol.unscopables:用来指定对象属性不被with扩展到作用于上:
1 var obj = { 2 name:"他乡踏雪", 3 age:3, 4 stature:666, 5 [Symbol.unscopables]:{ 6 name:true 7 } 8 } 9 with(obj){ 10 console.log(name);//这里打印一个空值 11 console.log(age);//3 12 console.log(stature);//666 13 }
四、Symbol元编程
元编程可以简单理解为针对程序本身编程,用来操作目标程序本身的行为特性的编程。比如查看对象a是否是在对象b的原型链上,这种元编程通常也被叫做内省introspection;还有宏也是元编程的典型,代码在编译时修改自身;用for...in循环枚举对象的键,或者检查一个对象是否是某个“类构造器”的实例,也都是常见的元编程例子。
元编程关注以下一个或者几个点:代码查看自身、代码修改自身、代码修改默认语言特性,以此来影响其他代码。
(以后补充)