ES6以前咱们都清楚JS有六种数据类型:Undefined、Null、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object),今天笔者讲的Symbol类型是ES6才加入的,它最大的特色就如标题所说“独一无二”。javascript
本篇文章笔者将从如下几个方面进行介绍:前端
本篇文章阅读时间预计15分钟。java
在了解Symbol以前,咱们须要了解下JS的数据类型,在JS中数据类型分为两类:值类型和引用类型。正则表达式
所谓的值类型能够这样理解:变量之间的互相赋值,是指开辟一块新的内存空间,将变量值赋给新变量保存到新开辟的内存里面;以后两个变量的值变更互不影响。 以下段代码所示:bash
let weChatName ="前端达人";
//开辟一块内存空间保存变量 weChatName 的值“前端达人”;
let touTiao =weChatName;
//给变量 touTiao 开辟一块新的内存空间,将 weChatName 的值 “前端达人” 赋值一份保存到新的内存里;
//weChatName 和 touTiao 的值之后不管如何变化,都不会影响到对方的值;复制代码
一些语言,好比 C,有引用传递和值传递的概念。JS 也有相似的概念,它是根据传递的数据类型推断的。若是将值传递给函数,则从新分配该值不会修改调用位置中的值。可是,若是你修改的是引用类型,那么修改后的值也将在调用它的地方被修改。微信
所谓的引用类型能够这样理解:变量之间的互相赋值,只是指针的交换,而并不是将对象复制一份给新的变量,对象依然仍是只有一个,只是多了一个指引~~; 以下段代码所示:ide
let weChat = { name: "前端达人", regYear:"2014" };
//须要开辟内存空间保存对象,变量 weChat 的值是一个地址,这个地址指向保存对象的空间;
let touTiao= weChat;
// 将 weChat 的指引地址赋值给 touTiao,而并不是复制一给对象且新开一块内存空间来保存;
weChat.regYear="2018";
console.log(touTiao);
//output:{ name: '前端达人', regYear: '2018' }
// 这个时候经过 weChat 来修改对象的属性,则经过 touTiao 来查看属性时对象属性已经发生改变;复制代码
那Symbol是什么数据类型呢?这里笔者先告诉你们是值类型,下面会有详细的介绍。函数
Symbol最大的特色就如本篇文章的标题同样:独一无二。这个独一无二怎么解释呢?就比如双胞胎,外表看不出差异,可是相对个体好比性格爱好仍是有差别的,每一个人都是独一无二。Symbol表示独一无二的值,是一种互不等价标识,声明Symbol十分简单,以下段代码所示:post
const s = Symbol();复制代码
Symbol([description]) 声明方式,支持一个可选参数,只是用于描述,方便咱们开发调试而已。每次执行Symbol()都会生成一个独一无二的Symbol值,以下段代码所示:ui
let s1 = Symbol("My Symbol");
let s2 = Symbol("My Symbol");
console.log(s1 === s2); // Outputs false”复制代码
因而可知,即便Symbol的描述值参数相同,它们的值也不相同,描述值仅仅是起描述的做用,不会对Symbol值自己起到任何的改变。关于描述值须要注意的一点:接受除Symbol值之外全部的值,怎么理解呢,请看下段代码所示:
const symbol = Symbol();
const symbolWithString=Symbol('前端达人');
//Symbol(前端达人)
const SymbolWithNum=Symbol(3.14);
//Symbol(3.14)
const SymbolWithObj=Symbol({foo:'bar'});
//Symbol([object Object])
const anotherSymbol=Symbol(symbol);
//TypeError: Cannot convert a Symbol value to a string复制代码
接下来笔者来详细解释下,为何Symbol是值类型,而不是引用类型。Symbol函数并非构造函数,所以不能使用new方法来生成一个Symbol对象,不然编译器会抛出异常,如执行下段代码所示:
new Symbol();
//TypeError: Symbol is not a constructor复制代码
因而可知,Symbol是一种值类型而非引用类型,这就意味着若是将Symbol做为参数传值的话,将会是值传值而非引用传值,以下段代码所示(值的改变没有互相影响):
const symbol=Symbol('前端达人');
function fn1(_symbol) {
return _symbol==symbol;
}
console.log(fn1(symbol));
//output:true;
function fn2(_symbol) {
_symbol=null;
console.log(_symbol);
}
fn2(symbol);
//output:null;
console.log(symbol);
//Symbol(前端达人)复制代码
介绍了这么多,Symbol存在的意义是什么?笔者先举个简单的业务场景:
在前端的JavaScript应用开发中,须要先经过渲染引擎所提供的API来获取一个DOM元素对象,并保留在JavaScript运行时中。由于业务须要,须要经过一个第三方库对这个DOM元素对象进行一些修饰和调整,即对该DOM元素对象进行一些新属性的插入。
然后来由于新需求的出现,须要再次利用另一个第三方库对同一个DOM元素对象进行修饰。但很是不巧的是这个第三方库一样须要对该DOM元素对象进行属性插入,而刚好这个库所须要操做的属性与前一个第三方库所操做的属性相同。这种状况下就颇有可能会出现两个第三方库都没法正常运行的现象,而使用这些第三方库的开发者却难以进行定位和修复。
针对上述问题, Symbol能够提供一种良好的解决方案。这是由于Symbol的实例值带有互不等价的特性,即任意两个Symbol值都不相等。在ES2015标准中,字面量对象除了可使用字符串、数字做为属性键之外,还可使用Symbol做为属性键,所以即可以利用Symbol值的互不等价特性来实现属性操做的互不干扰了。
如何判断一个变量是否是Symbol类型呢?目前惟一的方法就是使用typeof,以下段代码所示:
const s = Symbol();
console.log(typeof s);
//Outputs "symbol”复制代码
一般咱们使用字符串定义对象的属性(Key),有了Symbol类型后,咱们固然可使用Symbol做为对象的属性,惟一不一样的地方,咱们须要使用[]语法定义属性,以下段代码所示:
const WECHAR_NAME = Symbol();
const WECHAR_REG = Symbol();
let obj = {
[WECHAR_NAME]: "前端达人";
}
obj[WECHAR_REG] = 2014;
console.log(obj[WECHAR_NAME]) //output: 前端达人
console.log(obj[WECHAR_REG]) //output:2014复制代码
还有一点须要强调的是,使用Symbol做为对象的Key值时,具备私有性,咱们没法经过枚举获取Key值,以下段代码所示:
let obj = {
weChatName:'前端达人',
regYear: 2014,
[Symbol('pwd')]: 'wjqw@$#sndk9012',
}
console.log(Object.keys(obj));
// ['weChatName', 'regYear']
for (let p in obj) {
console.log(p)
// 分别会输出:'weChatName' 和 'regYear'
}
console.log(Object.getOwnPropertyNames(obj));
// [ 'weChatName', 'regYear' ]复制代码
从上述代码中,能够看出Symbol类型的key是不能经过Object.keys()或者for...in来枚举的,它未被包含在对象自身的属性名集合(property names)之中。利用该特性,咱们能够把一些不须要对外操做和访问的属性可使用Symbol来定义。因为这一特性的存在,咱们使用JSON.stringify()将对象转换成JSON字符串的时候,Symbol属性也会被排除在输出内容以外,在上述代码中执行下段代码:
console.log(JSON.stringify(obj));
//output:{"weChatName":"前端达人","regYear":2014}复制代码
基于这一特性,咱们能够更好的去设计咱们的数据对象,让“对内操做”和“对外选择性输出”变得更加灵活。
咱们难道就没有办法获取Symbol方式定义的对象属性了么?私有并非绝对的,咱们能够经过一些API函数进行获取,在上述代码中执行下段代码:
// 使用Object的API
console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(pwd)]
// 使用新增的反射API
console.log(Reflect.ownKeys(obj));// [Symbol(pwd), 'age', 'title']复制代码
咱们都清楚在JS中,是没有如Java等面向对象语言的访问控制关键字private的,类上全部定义的属性或方法都是可公开访问的。上面笔者讲到做为对象属性具备私有性的特色,咱们定义类的私有属性和方法才能实现,以下段代码所示:
咱们先创建一个a.js的文件,以下所示:
const PASSWORD = Symbol();
class Login {
constructor(username, password) {
this.username = username;
this[PASSWORD] = password;
}
checkPassword(pwd) {
return this[PASSWORD] === pwd;
}
}
export default Login;复制代码
咱们在创建一个文件b.js,引入a.js文件,以下所示:
import Login from './a.js';
const login = new Login('admin', '123456');
console.log(login.checkPassword('123456')); // true
console.log(login.PASSWORD); // undefined
console.log(login[PASSWORD]);// PASSWORD is not defined
console.log(login["PASSWORD"]); // undefined复制代码
因为Symbol常量PASSWORD被定义在a.js所在的模块中,外面的模块获取不到这个Symbol,也不可能再建立一个如出一辙的Symbol出来(由于Symbol是独一无二的),所以这个PASSWORD的Symbol只能被限制在a.js内部使用,因此使用它来定义的类属性是没有办法被模块外访问到的,从而实现了私有化的效果。
虽然Symbol是独一无二的,可是有些业务场景,咱们须要共享一个Symbol,咱们如何实现呢?这种状况下,咱们就须要使用另外一个API来建立或获取Symbol,那就是Symbol.for(),它能够注册或获取一个全局的Symbol实例,以下段代码所示:
let obj = {};
(function(){
let s1 = Symbol("name");
obj[s1] = "Eden";
})();
console.log(obj[s1]);
//SyntaxError: Unexpected identifier cannot be accessed here
(function(){
let s2 = Symbol.for("age");
obj[s2] = 27;
})();
console.log(obj[Symbol.for("age")]); //Output "27”复制代码
从上述代码能够看出,Symbol.for()会注册一个全局做用域的Symbol值,若是这个Key值从未使用则会进行建立注册,若是已被注册,则会返回一个与第一次使用建立的Symbol值等价的Symbol,以下段代码所示:
const symbol=Symbol.for('foo');
const obj={};
obj[symbol]='bar';
const anotherSymbol=Symbol.for('foo');
console.log(symbol===anotherSymbol);
//output:true
console.log(obj[anotherSymbol]);
//output:bar复制代码
咱们除了能够自行建立Symbol值之外,ES6还将其应用到了ECMAScript引擎的各个角落,咱们能够运用这些经常使用值对底层代码的实现逻辑进行修改,以实现更高级的定制化的需求。
如下表格进行了经常使用Symbol值的总结
定义项 |
描述 |
含义 |
---|---|---|
@@iterator |
"Symbol.iterator" |
用于为对象定义一个方法并返回一个属于所对应对象的迭代器。该迭代器会被for-of循环使用。 |
@@hasInstance |
"Symbol.hasInStance" |
用于为类定义一个方法。该方法会由于instanceof语句的使用而被调用,来检查一个对象是不是某一个类的实例。 |
@@match |
"Symobol.match" |
用于正则表达式定义一个可被String.prototype.match()方法使用的方法,检查对应字符串与当前正则表达式是否匹配 |
@@replace |
"Symbol.replace" |
用于正则表达式会对象定义一个方法。该方法会由于String.prototype.replace()方法的使用而被调用,用于处理当前字符串使用该正则表达式或对象做为替换标志时的内部处理逻辑 |
@@search |
"Symbol.search" |
用于正则表达式会对象定义一个方法。该方法会由于String.prototype.search()方法的使用而被调用,用于处理当前字符串使用该正则表达式或对象做为位置检索标志时的内部处理逻辑 |
@@split |
"Symbol.split" |
用于正则表达式会对象定义一个方法。该方法会由于String.prototype.split()方法的使用而被调用,用于处理当前字符串使用该正则表达式或对象做为分割标志时的内部处理逻辑 |
@@unscopables |
"Symbol.unscopables" |
用于为对象定义一个属性。该属性用于描述该对象中哪些属性是能够被with语句所使用的。 |
@@isConcatSpreadable |
"Symbol.isConcatSpreadable" |
用于为对象定义一个属性。该属性用于决定该对象做为Array.prototype.concat()方法参数时,是否被展开。 |
@@species |
"Symbol.species" |
用于为类定义一个静态属性,该属性用于决定该类的默认构建函数。 |
@@toPrimitive |
"Symbol.toPrimitive" |
用于为对象定义一个方法。该方法会在该对象须要转换为值类型的时候被调用,能够根据程序的行为决定该对象须要被转换成的值。 |
@@toStringTag |
"Symbol.toStringTag" |
用于为类定义一个属性。该属性能够决定这个类的实例在调用toString()方法时,其中标签的内容。 |
因为经常使用Symbol值比较多,笔者只对其中最经常使用的几个进行解释。
咱们可使用Symbol.iterator来自定义一个能够迭代的对象,咱们可使用Symbol.iterator做为方法名的方法属性,该方法返回一个迭代器(Iterator)。虽然JS中没有协议(Protocal)的概念,咱们能够将迭代器看作成一个协议,即迭代器协议(Iterator Protocal),该协议定义了一个方法next(),含义是进入下一次迭代的迭代状态,第一次执行即返回第一次的迭代状态,该迭代状态有两个属性,如表格所示:
定义项 |
描述 |
含义 |
---|---|---|
done |
Boolean |
该迭代器是否已经迭代结束 |
value |
Any |
当前迭代状态值 |
如下是咱们使用Symbol.iterator带迭代的方法,以下段代码所示:
let obj = {
array: [1, 2, 3, 4, 5],
nextIndex: 0,
[Symbol.iterator]: function(){
return {
array: this.array,
nextIndex: this.nextIndex,
next: function(){
return this.nextIndex < this.array.length ?
{value: this.array[this.nextIndex++], done: false} :
{done: true};
}
}
}
};
let iterable = obj[Symbol.iterator]();
console.log(iterable.next().value);
console.log(iterable.next().value);
console.log(iterable.next().value);
console.log(iterable.next().value);
console.log(iterable.next().value);
console.log(iterable.next().done);复制代码
以上代码将会输出:
1
2
3
4
5
true复制代码
除了能够自定义迭代的逻辑,咱们也可使用引擎默认的迭代,从而节省了咱们的代码量,以下段代码所示:
const arr = [1, 2];
const iterator = arr[Symbol.iterator](); // returns you an iterator
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());复制代码
以上代码将会输出
{ value: 1, done: false }
{ value: 2, done: false }
{ value: undefined, done: true }复制代码
用于为类定义一个方法。该方法会由于instanceof语句的使用而被调用,来检查一个对象是不是某一个类的实例, 用于扩展instanceof的内部逻辑,咱们能够用于为一个类定一个静态方法,该方法的第一个形参即是被检测的对象,而自定义的方法内容决定了instanceof语句的返回结果,代码以下:
class Foo{
static [Symbol.hasInstance](obj){
console.log(obj);
return true;
}
}
console.log( {} instanceof Foo);复制代码
以上代码将会输出
{}
true复制代码
Symbol.match 在字符串使用match()方法时,为其实现自定义的逻辑。以下段代码所示:
没自定义前:
const re=/foo/
console.log('bar'.match(re));//null
console.log('foo'.match(re));
//[ 'foo', index: 0, input: 'foo', groups: undefined ]复制代码
使用Symbol.match后:
const re=/foo/
re[Symbol.match]=function (str) {
const regexp=this;
console.log(str);
return true;
}
console.log('bar'.match(re));
console.log('foo'.match(re));复制代码
上端代码将会输出:
bar
true
foo
true复制代码
在JS开发中,咱们会利用其中的隐式转换规则,其中就包括将引用类型转换成值类型,然而有时隐式转换的结果并非咱们所指望的。虽然咱们能够重写toString()方法来自定义对象在隐式转换成字符串的处理,可是若是出现须要转换成数字时变得无从入手。咱们可使用Symbol.toPrimitive来定义更灵活处理方式,以下段代码所示(仅为演示,可结合本身的业务自行修改):
const obj={};
console.log(+obj);
console.log(`${obj}`);
console.log(obj+"");
//output:
//NaN
//[object Object]
//[object Object]
const transTen={
[Symbol.toPrimitive](hint){
switch (hint) {
case 'number':
return 10;
case 'string':
return 'Ten';
default:
return true;
}
}
}
console.log(+transTen);
console.log(`${transTen}`);
console.log(transTen+"");
//output:
//10
//Ten
//true复制代码
前面的表格提到过,Symbol.toStringTag的做用就是自定义这个类的实例在调用toString()时的标签内容。 好比咱们在开发中定义的类,就能够经过Symbol.toStringTag来修改toString()中的内容,利用它作为属性键为类型定一个Getter。
class Foo{
get [Symbol.toStringTag](){return 'Bar'}
}
const obj=new Foo();
console.log(obj.toString());
//output:[object Bar]复制代码
今天的内容有些多,须要慢慢理解,咱们清楚了Symbol值是独一无二的,Symbol的一些使用场景,以及使用Symbol经常使用值改写更底层的方法,让咱们写出更灵活的处理逻辑。Symbol虽然强大,可是用好它还须要在实践中结合业务场景进行掌握。