ES6之前我們都清楚JS有六種資料型別:Undefined、Null、布林值(Boolean)、字串(String)、數值(Number)、物件(Object),今天筆者講的Symbol型別是ES6才加入的,它最大的特點就如標題所說“獨一無二”。
本篇文章筆者將從以下幾個方面進行介紹:
- 值型別和引用型別介紹
- 如何宣告一個Symbol?
- 為什麼要有Symbol?
- Symbol的常用用法
- 內建常用Symbol值的用法
本篇文章閱讀時間預計15分鐘。
值型別和引用型別介紹
在瞭解Symbol之前,我們需要了解下JS的資料型別,在JS中資料型別分為兩類:值型別和引用型別。
- 值型別:數值型(Number),字元型別(String),布林值型(Boolean),null 和 underfined
- 引用型別:物件(Object)
所謂的值型別可以這樣理解:變數之間的互相賦值,是指開闢一塊新的記憶體空間,將變數值賦給新變數儲存到新開闢的記憶體裡面;之後兩個變數的值變動互不影響。 如下段程式碼所示:
let weChatName ="前端達人";
//開闢一塊記憶體空間儲存變數 weChatName 的值“前端達人”;
let touTiao =weChatName;
//給變數 touTiao 開闢一塊新的記憶體空間,將 weChatName 的值 “前端達人” 賦值一份儲存到新的記憶體裡;
//weChatName 和 touTiao 的值以後無論如何變化,都不會影響到對方的值;複製程式碼
一些語言,比如 C,有引用傳遞和值傳遞的概念。JS 也有類似的概念,它是根據傳遞的資料型別推斷的。如果將值傳遞給函式,則重新分配該值不會修改呼叫位置中的值。但是,如果你修改的是引用型別,那麼修改後的值也將在呼叫它的地方被修改。
所謂的引用型別可以這樣理解:變數之間的互相賦值,只是指標的交換,而並非將物件複製一份給新的變數,物件依然還是隻有一個,只是多了一個指引~~; 如下段程式碼所示:
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表示獨一無二的值,是一種互不等價標識,宣告Symbol十分簡單,如下段程式碼所示:
const s = Symbol();複製程式碼
Symbol([description]) 宣告方式,支援一個可選引數,只是用於描述,方便我們開發除錯而已。每次執行Symbol()都會生成一個獨一無二的Symbol值,如下段程式碼所示:
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?
介紹了這麼多,Symbol存在的意義是什麼?筆者先舉個簡單的業務場景:
在前端的JavaScript應用開發中,需要先通過渲染引擎所提供的API來獲取一個DOM元素物件,並保留在JavaScript執行時中。因為業務需要,需要通過一個第三方庫對這個DOM元素物件進行一些修飾和調整,即對該DOM元素物件進行一些新屬性的插入。
而後來因為新需求的出現,需要再次利用另外一個第三方庫對同一個DOM元素物件進行修飾。但非常不巧的是這個第三方庫同樣需要對該DOM元素物件進行屬性插入,而恰好這個庫所需要操作的屬性與前一個第三方庫所操作的屬性相同。這種情況下就很有可能會出現兩個第三方庫都無法正常執行的現象,而使用這些第三方庫的開發者卻難以進行定位和修復。
針對上述問題, Symbol可以提供一種良好的解決方案。這是因為Symbol的例項值帶有互不等價的特性,即任意兩個Symbol值都不相等。在ES2015標準中,字面量物件除了可以使用字串、數字作為屬性鍵以外,還可以使用Symbol作為屬性鍵,因此便可以利用Symbol值的互不等價特性來實現屬性操作的互不干擾了。
Symbol的常用用法
1、判斷是否是Symbol
如何判斷一個變數是不是Symbol型別呢?目前唯一的方法就是使用typeof,如下段程式碼所示:
const s = Symbol();
console.log(typeof s);
//Outputs "symbol”複製程式碼
2、用作物件的屬性
通常我們使用字串定義物件的屬性(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']複製程式碼
3、定義類的私有屬性/方法
我們都清楚在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內部使用,所以使用它來定義的類屬性是沒有辦法被模組外訪問到的,從而實現了私有化的效果。
4、建立共享Symbol
雖然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值及意義
我們除了可以自行建立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值比較多,筆者只對其中最常用的幾個進行解釋。
1、Symbol.iterator
我們可以使用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 }複製程式碼
2、Symbol.hasInstance
用於為類定義一個方法。該方法會因為instanceof語句的使用而被呼叫,來檢查一個物件是否是某一個類的例項, 用於擴充套件instanceof的內部邏輯,我們可以用於為一個類定一個靜態方法,該方法的第一個形參便是被檢測的物件,而自定義的方法內容決定了instanceof語句的返回結果,程式碼如下:
class Foo{
static [Symbol.hasInstance](obj){
console.log(obj);
return true;
}
}
console.log( {} instanceof Foo);複製程式碼
以上程式碼將會輸出
{}
true複製程式碼
3、Symbol.match
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複製程式碼
4、Symbol.toPrimitive
在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複製程式碼
5、Symbol.toStringTag
前面的表格提到過,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雖然強大,但是用好它還需要在實踐中結合業務場景進行掌握。