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雖然強大,可是用好它還須要在實踐中結合業務場景進行掌握。