前端 JavaScript 自測清單 2
以下文章轉載自前端自習課,作者王平安
前言
在《初中級前端 JavaScript 自測清單 - 1》部分中,和大家簡單過了一遍 JavaScript 的一些基礎知識,沒看過的朋友可以回顧一下 😁
本系列文章是我在我們團隊內部的 “「現代 JavaScript 突擊隊」”,第一期學習內容為《現代 JavaScript 教程》系列的「第二部分」輸出內容,希望這份自測清單,能夠幫助大家鞏固知識,温故知新。
本部分內容,以 「JavaScript 對象」為主,大致包括以下內容:
對象
JavaScript 有八種數據額類型,有七種原始類型,它們值只包含一種類型(字符串,數字或其他),而對象是用來「保存鍵值對和更復雜實體。」我們可以通過使用帶有可選「屬性列表」的花括號 {...}
來創建對象,一個屬性就是一個鍵值對 {"key" : "value"}
,其中鍵( key
)是一個字符串(或稱屬性名),值( value
)可以是任何類型。
創建對象
我們可以使用 2 種方式來創建一個新對象:
1 | // 1. 通過“構造函數”創建 |
對象文本和屬性
創建對象時,可以初始化對象的一些屬性:
1 | let user = { |
然後可以對該對象進行屬性對「增刪改查」操作:
1 | // 增加屬性 |
方括號的使用
當然對象的鍵( key
)也可以是多詞屬性,但必須加引號,使用的時候,必須使用方括號( []
)讀取:
1 | let user = { |
我們也可以在方括號中使用變量,來獲取屬性值:
1 | let key = 'name' |
計算屬性
創建對象時,可以在對象字面量中使用方括號,即 「計算屬性」 :
1 | let key = 'name' |
當然,計算屬性也可以是表達式:
1 | let key = 'name' |
屬性名簡寫
實際開發中,可以將相同的屬性名和屬性值簡寫成更短的語法:
1 | // 原本書寫方式 |
也可以混用:
1 | // 原本書寫方式 |
對象屬性存在性檢測
使用 in 關鍵字
該方法可以判斷「對象的自有屬性和繼承來的屬性」是否存在。
1 | let user = { name: 'leo' } |
使用對象的 hasOwnProperty() 方法。
該方法只能判斷「自有屬性」是否存在,對於「繼承屬性」會返回 false
。
1 | let user = {name: "leo"}; |
6.3 用 undefined 判斷
該方法可以判斷對象的「自有屬性和繼承屬性」。
1 | let user = {name: "leo"}; |
該方法存在一個問題,如果屬性的值就是 undefined
的話,該方法不能返回想要的結果:
1 | let user = {name: undefined}; |
6.4 在條件語句中直接判斷
1 | let user = {} |
對象循環遍歷
當我們需要遍歷對象中每一個屬性,可以使用 for...in
語句來實現
for…in 循環
for...in
語句以任意順序遍歷一個對象的除 Symbol
以外的可枚舉屬性。「注意」 :for...in
不應該應用在一個數組,其中索引順序很重要。
1 | let user = { |
ES7 新增方法
ES7 中新增加的 Object.values()
和Object.entries()
與之前的Object.keys()
類似,返回數組類型。
- Object.keys()
返回一個數組,成員是參數對象自身的(不含繼承的)所有「可遍歷屬性」的健名。
1 | let user = { name: 'leo', age: 18 } |
- Object.values()
返回一個數組,成員是參數對象自身的(不含繼承的)所有可遍歷屬性的鍵值。
1 | let user = { name: 'leo', age: 18 } |
如果參數不是對象,則返回空數組:
1 | Object.values(10) // [] |
- Object.entries()
返回一個數組,成員是參數對象自身的(不含繼承的)所有「可遍歷屬性」的鍵值對數組。
1 | let user = { name: 'leo', age: 18 } |
手動實現Object.entries()
方法:
1 | // Generator函數實現: |
- Object.getOwnPropertyNames(Obj)
該方法返回一個數組,它包含了對象 Obj
所有擁有的屬性(「無論是否可枚舉」)的名稱。
1 | let user = { name: 'leo', age: 18 } |
對象拷貝
參考文章《搞不懂 JS 中賦值 · 淺拷貝 · 深拷貝的請看這裏》
賦值操作
首先回顧下基本數據類型和引用數據類型:
基本類型
概念:基本類型值在內存中佔據固定大小,保存在
棧內存
中(不包含閉包
中的變量)。常見包括:undefined, null, Boolean, String, Number, Symbol引用類型
概念:引用類型的值是對象,保存在
堆內存
中。而棧內存存儲的是對象的變量標識符以及對象在堆內存中的存儲地址 (引用),引用數據類型在棧中存儲了指針,該指針指向堆中該實體的起始地址。當解釋器尋找引用值時,會首先檢索其在棧中的地址,取得地址後從堆中獲得實體。常見包括:Object,Array,Date,Function,RegExp 等
基本數據類型賦值
在棧內存中的數據發生數據變化的時候,系統會自動為新的變量分配一個新的之值在棧內存中,兩個變量相互獨立,互不影響的。
1 | let user = 'leo' |
引用數據類型賦值
在 JavaScript 中,變量不存儲對象本身,而是存儲其 “內存中的地址”,換句話説就是存儲對其的 “引用”。如下面 leo
變量只是保存對user
對象對應引用:
1 | let user = { name: 'leo', age: 18 } |
其他變量也可以引用 user
對象:
1 | let leo1 = user |
但是由於變量保存的是引用,所以當我們修改變量 leo
\ leo1
\ leo2
這些值時,「也會改動到引用對象」 user
,但當 user
修改,則其他引用該對象的變量,值都會發生變化:
1 | leo.name = 'pingan' |
這個過程中涉及變量地址指針指向問題,這裏暫時不展開討論,有興趣的朋友可以網上查閲相關資料。
對象比較
當兩個變量引用同一個對象時,它們無論是 ==
還是 ===
都會返回 true
。
1 | let user = { name: 'leo', age: 18 } |
但如果兩個變量是空對象 {}
,則不相等:
1 | let leo1 = {} |
淺拷貝
概念
概念:「新的對象複製已有對象中非對象屬性的值和對象屬性的引用」。也可以理解為:「一個新的對象直接拷貝已存在的對象的對象屬性的引用」,即淺拷貝。
淺拷貝「只對第一層屬性進行了拷貝」,當第一層的屬性值是基本數據類型時,新的對象和原對象互不影響,但是如果第一層的屬性值是複雜數據類型,那麼新對象和原對象的屬性值其指向的是同一塊內存地址。
通過示例代碼演示沒有使用淺拷貝場景:
1 | // 示例1 對象原始拷貝 |
從上面示例代碼可以看出:由於對象被直接拷貝,相當於拷貝 「引用數據類型」 ,所以在新對象修改任何值時,都會改動到源數據。
接下來實現淺拷貝,對比以下。
實現淺拷貝
- Object.assign()
語法:Object.assign(target, ...sources)
ES6 中拷貝對象的方法,接受的第一個參數是拷貝的目標 target,剩下的參數是拷貝的源對象 sources(可以是多個)。詳細介紹,可以閲讀文檔《MDN Object.assign》。
1 | // 示例1 對象淺拷貝 |
從打印結果可以看出,淺拷貝只是在根屬性 (對象的第一層級) 創建了一個新的對象,但是對於屬性的值是對象的話只會拷貝一份相同的內存地址。
Object.assign()
使用注意:
- 只拷貝源對象的自身屬性(不拷貝繼承屬性);
- 不會拷貝對象不可枚舉的屬性;
- 屬性名為`Symbol` 值的屬性,可以被 Object.assign 拷貝;
- `undefined`和`null`無法轉成對象,它們不能作為`Object.assign`參數,但是可以作為源對象。
1
2
3
4
5
6
7
Object.assign(undefined) // 報錯
Object.assign(null) // 報錯
Object.assign({}, undefined) // {}
Object.assign({}, null) // {}
let user = { name: 'leo' }
Object.assign(user, undefined) === user // true
Object.assign(user, null) === user // true
- Array.prototype.slice()
語法:arr.slice([begin[, end]])``slice()
方法返回一個新的數組對象,這一對象是一個由 begin
和 end
決定的原數組的淺拷貝(包括 begin
,不包括end
)。原始數組不會被改變。詳細介紹,可以閲讀文檔《MDN Array slice》。
1 | // 示例 數組深拷貝 |
- Array.prototype.concat()
語法:var new_array = old_array.concat(value1[, value2[, ...[, valueN]]])``concat()
方法用於合併兩個或多個數組。此方法不會更改現有數組,而是返回一個新數組。詳細介紹,可以閲讀文檔《MDN Array concat》。
1 | let user = [{ name: 'leo' }, { age: 18 }] |
Array.prototype.concat
也是一個淺拷貝,只是在根屬性 (對象的第一層級) 創建了一個新的對象,但是對於屬性的值是對象的話只會拷貝一份相同的內存地址。
- 拓展運算符(…)
語法:var cloneObj = { ...obj };
擴展運算符也是淺拷貝,對於值是對象的屬性無法完全拷貝成 2 個不同對象,但是如果屬性都是基本類型的值的話,使用擴展運算符也是優勢方便的地方。
1 | let user = { name: 'leo', skill: { JavaScript: 90, CSS: 80 } } |
手寫淺拷貝
實現原理:新的對象複製已有對象中非對象屬性的值和對象屬性的「引用」, 也就是説對象屬性並不複製到內存。
1 | function cloneShallow(source) { |
- 「for in」
for…in 語句以任意順序遍歷一個對象自有的、繼承的、可枚舉的
、非 Symbol 的屬性。對於每個不同的屬性,語句都會被執行。
- 「hasOwnProperty」
該函數返回值為布爾值,所有繼承了 Object 的對象都會繼承到 hasOwnProperty
方法,和 in
運算符不同,該函數會忽略掉那些從原型鏈上繼承到的屬性和自身屬性。語法:obj.hasOwnProperty(prop)
prop
是要檢測的屬性「字符串名稱」或者Symbol
。
深拷貝
概念
複製變量值,對於引用數據,則遞歸至基本類型後,再複製。深拷貝後的對象「與原來的對象完全隔離」,互不影響,對一個對象的修改並不會影響另一個對象。
實現深拷貝
- JSON.parse(JSON.stringify())
其原理是把一個對象序列化成為一個 JSON 字符串,將對象的內容轉換成字符串的形式再保存在磁盤上,再用JSON.parse()
反序列化將 JSON 字符串變成一個新的對象。
1 | let user = { name: 'leo', skill: { JavaScript: 90, CSS: 80 } } |
JSON.stringify()
使用注意:
- 拷貝的對象的值中如果有函數,
undefined
,symbol
則經過JSON.stringify()
` 序列化後的 JSON 字符串中這個鍵值對會消失; - 無法拷貝不可枚舉的屬性,無法拷貝對象的原型鏈;
- 拷貝
Date
引用類型會變成字符串; - 拷貝
RegExp
引用類型會變成空對象; - 對象中含有
NaN
、Infinity
和-Infinity
,則序列化的結果會變成null
; - 無法拷貝對象的循環應用 (即
obj[key] = obj
)。
- 第三方庫
手寫深拷貝
核心思想是「遞歸」,遍歷對象、數組直到裏邊都是基本數據類型,然後再去複製,就是深度拷貝。實現代碼:
1 | const isObject = obj => typeof obj === 'object' && obj != null; |
該方法缺陷:遇到循環引用,會陷入一個循環的遞歸過程,從而導致爆棧。其他寫法,可以閲讀《如何寫出一個驚豔面試官的深拷貝?》 。
小結
「淺拷貝」:將對象的每個屬性進行依次複製,但是當對象的屬性值是引用類型時,實質複製的是其引用,當引用指向的值改變時也會跟着變化。
「深拷貝」:複製變量值,對於引用數據,則遞歸至基本類型後,再複製。深拷貝後的對象「與原來的對象完全隔離」,互不影響,對一個對象的修改並不會影響另一個對象。
「深拷貝和淺拷貝是針對複雜數據類型來説的,淺拷貝只拷貝一層,而深拷貝是層層拷貝。」
垃圾回收機制(GC)
垃圾回收(Garbage Collection,縮寫為 GC)是一種自動的存儲器管理機制。當某個程序佔用的一部分內存空間不再被這個程序訪問時,這個程序會藉助垃圾回收算法向操作系統歸還這部分內存空間。垃圾回收器可以減輕程序員的負擔,也減少程序中的錯誤。垃圾回收最早起源於 LISP 語言。目前許多語言如 Smalltalk、Java、C# 和 D 語言都支持垃圾回收器,我們熟知的 JavaScript 具有自動垃圾回收機制。
「在 JavaScript 中,原始類型的數據被分配到棧空間中,引用類型的數據會被分配到堆空間中。」
棧空間中的垃圾回收
當函數 showName
調用完成後,通過下移 ESP(Extended Stack Pointer)指針,來銷燬 showName
函數,之後調用其他函數時,將覆蓋掉舊內存,存放另一個函數的執行上下文,實現垃圾回收。圖片來自《瀏覽器工作原理與實踐》
堆空間中的垃圾回收
堆中數據垃圾回收策略的基礎是:「代際假説」(The Generational Hypothesis)。即:
- 大部分對象在內存中存在時間極短,很多對象很快就不可訪問。
- 不死的對象將活得更久。
這兩個特點不僅僅適用於 JavaScript,同樣適用於大多數的動態語言,如 Java、Python 等。V8 引擎將堆空間分為「新生代」(存放生存「時間短」的對象)和「老生代」(存放生存「時間長」的對象)兩個區域,並使用不同的垃圾回收器。
- 副垃圾回收器,主要負責新生代的垃圾回收。
- 主垃圾回收器,主要負責老生代的垃圾回收。
不管是哪種垃圾回收器,都使用相同垃圾回收流程:「標記活動對象和非活動對象,回收非活動對象的內存,最後內存整理。」
副垃圾回收器
使用 Scavenge 算法處理,將新生代空間對半分為兩個區域,一個對象區域,一個空閒區域。圖片來自《瀏覽器工作原理與實踐》
執行流程:
- 新對象存在在「對象區域」,當對象區域將要寫滿時,執行一次垃圾回收;
- 垃圾回收過程中,首先對對象區域中的垃圾做標記,然後副垃圾回收器將存活的對象複製並有序排列到空閒區域,相當於完成內存整理。
- 複製完成後,將對象區域和空閒區域翻轉,完成垃圾回收操作,這也讓新生代中兩塊區域無限重複使用。
當然,這也存在一些問題:若複製操作的數據較大則影響清理效率。JavaScript 引擎的解決方式是:將新生代區域設置得比較小,並採用對象晉升策略(經過兩次回收仍存活的對象,會被移動到老生區),避免因為新生代區域較小引起存活對象裝滿整個區域的問題。
主垃圾回收器
分為:「標記 - 清除(Mark-Sweep)算法」,和「標記 - 整理(Mark-Compact)算法」。
「a) 標記 - 清除(Mark-Sweep)算法」 「過程:」
- 標記過程:從一組根元素開始遍歷整個元素,能到達的元素為活動對象,反之為垃圾數據;
- 清除過程:清理被標記的數據,併產生大量碎片內存。(缺點:導致大對象無法分配到足夠的連續內存)
圖片來自《瀏覽器工作原理與實踐》
「b) 標記 - 整理(Mark-Compact)算法」 「過程:」
- 標記過程:從一組根元素開始遍歷整個元素,能到達的元素為活動對象,反之為垃圾數據;
- 整理過程:將所有存活的對象,向一段移動,然後清除端邊界以外的內容。
圖片來自《瀏覽器工作原理與實踐》
拓展閲讀
1.《圖解 Java 垃圾回收機制》2.《MDN 內存管理》
對象方法和 this
對象方法
具體介紹可閲讀 《MDN 方法的定義》 。將作為對象屬性的方法稱為 “對象方法”,如下面 user
對象的 say
方法:
1 | let user = {} |
也可以使用更加簡潔的方法:
1 | let user = { |
當然對象方法的名稱,還支持計算的屬性名稱作為方法名:
1 | const hello = 'Hello' |
另外需要注意的是:所有方法定義不是構造函數,如果您嘗試實例化它們,將拋出TypeError
。
1 | let user = { |
this
this 簡介
當對象方法需要使用對象中的屬性,可以使用 this
關鍵字:
1 | let user = { |
當代碼 user.say()
執行過程中, this
指的是 user
對象。當然也可以直接使用變量名 user
來引用 say()
方法:
1 | let user = { |
但是這樣並不安全,因為 user
對象可能賦值給另外一個變量,並且將其他值賦值給 user
對象,就可能導致報錯:
1 | let user = { |
但將 user.name
改成 this.name
代碼便正常運行。
this 取值
this
的值是在 「代碼運行時計算出來」 的,它的值取決於代碼上下文:
1 | let user = { name: 'leo' } |
規則:如果 obj.fun()
被調用,則 this
在 fun
函數調用期間是 obj
,所以上面的 this
先是 user
,然後是 admin
。
但是在全局環境中,無論是否開啟嚴格模式, this
都指向全局對象
1 | console.log(this == window) // true |
箭頭函數沒有自己的 this
箭頭函數比較特別,沒有自己的 this
,如果有引用 this
的話,則指向外部正常函數,下面例子中, this
指向 user.say()
方法:
1 | let user = { |
call / apply / bind
詳細可以閲讀《js 基礎 - 關於 call,apply,bind 的一切》 。當我們想把 this
值綁定到另一個環境中,就可以使用 call
/ apply
/ bind
方法實現:
1 | var user = { name: 'leo' } |
注意:這裏的 var name = 'pingan';
需要使用 var
來聲明,使用 let
的話, window
上將沒有 name
變量。
三者語法如下:
1 | fun.call(thisArg, param1, param2, ...) |
構造函數和 new 運算符
構造函數
構造函數的作用在於 「實現可重用的對象創建代碼」 。通常,對於構造函數有兩個約定:
- 命名時首字母大寫;
- 只能使用
new
運算符執行。
「new
運算符」創建一個用户定義的對象類型的實例或具有構造函數的內置對象的實例。語法如下:
1 | new constructor[([arguments])] |
參數如下:
constructor
一個指定對象實例的類型的類或函數。arguments
一個用於被constructor
調用的參數列表。
簡單示例
舉個簡單示例:
1 | function User(name) { |
new 運算符操作過程
當一個函數被使用 new
運算符執行時,它按照以下步驟:
- 一個新的空對象被創建並分配給
this
。 - 函數體執行。通常它會修改
this
,為其添加新的屬性。 - 返回
this
的值。
以前面 User
方法為例:
1 | function User(name) { |
當我們執行 new User('leo')
時,發生以下事情:
- 一個繼承自
User.prototype
的新對象被創建; - 使用指定參數調用構造函數
User
,並將this
綁定到新創建的對象; - 由構造函數返回的對象就是
new
表達式的結果。如果構造函數沒有顯式返回一個對象,則使用步驟 1 創建的對象。
「需要注意」:
- 一般情況下,構造函數不返回值,但是開發者可以選擇主動返回對象,來覆蓋正常的對象創建步驟;
new User
等同於new User()
,只是沒有指定參數列表,即User
不帶參數的情況;
1 | let user = new User() // <-- 沒有參數 |
- 任何函數都可以作為構造器,即都可以使用
new
運算符運行。 - 構造函數中的方法
在構造函數中,也可以將方法綁定到 this
上:
1 | function User(name) { |
可選鏈 “?.”
詳細介紹可以查看 《MDN 可選鏈操作符》 。
背景介紹
在實際開發中,常常出現下面幾種報錯情況:
1 | // 1. 對象中不存在指定屬性 |
在可選鏈 ?.
出現之前,我們會使用短路操作 &&
運算符來解決該問題:
1 | const leo = {}; |
這種寫法的缺點就是 「太麻煩了」 。
可選鏈介紹
可選鏈 ?.
是一種 「訪問嵌套對象屬性的防錯誤方法」 。即使中間的屬性不存在,也不會出現錯誤。如果可選鏈 ?.
前面部分是 undefined
或者 null
,它會停止運算並返回 undefined
。
語法:
1 | obj?.prop |
「我們改造前面示例代碼:」
1 | // 1. 對象中不存在指定屬性 |
使用注意
可選鏈雖然好用,但需要注意以下幾點:
- 「不能過度使用可選鏈」;
我們應該只將 ?.
使用在一些屬性或方法可以不存在的地方,以上面示例代碼為例:
1 | const leo = {}; |
這樣寫會更好,因為 leo
對象是必須存在,而 name
屬性則可能不存在。
- 「可選鏈
?.
之前的變量必須已聲明」;
在可選鏈 ?.
之前的變量必須使用 let/const/var
聲明,否則會報錯:
1 | leo?.name |
- 「可選鏈不能用於賦值」 ;
1 | let object = {} |
- 「可選鏈訪問數組元素的方法」 ;
1 | let arrayItem = arr?.[42] |
其他情況:?.() 和 ?.[]
需要説明的是 ?.
是一個特殊的語法結構,而不是一個運算符,它還可以與其 ()
和 []
一起使用:
可選鏈與函數調用 ?.()
?.()
用於調用一個可能不存在的函數,比如:
1 | let user1 = { |
?.()
會檢查它左邊的部分:如果 admin 函數存在,那麼就調用運行它(對於 user1
)。否則(對於 user2
)運算停止,沒有錯誤。
可選鏈和表達式 ?.[]
?.[]
允許從一個可能不存在的對象上安全地讀取屬性。
1 | let user1 = { |
可選鏈 ?.
語法總結
可選鏈 ?.
語法有三種形式:
obj?.prop
—— 如果obj
存在則返回obj.prop
,否則返回undefined
。obj?.[prop]
—— 如果obj
存在則返回obj[prop]
,否則返回undefined
。obj?.method()
—— 如果obj
存在則調用obj.method()
,否則返回undefined
。
正如我們所看到的,這些語法形式用起來都很簡單直接。?.
檢查左邊部分是否為 null/undefined
,如果不是則繼續運算。?.
鏈使我們能夠安全地訪問嵌套屬性。
Symbol
規範規定,JavaScript 中對象的屬性只能為 「字符串類型」 或者 「Symbol 類型」 ,畢竟我們也只見過這兩種類型。
概念介紹
ES6 引入Symbol
作為一種新的「原始數據類型」,表示「獨一無二」的值,主要是為了「防止屬性名衝突」。ES6 之後,JavaScript 一共有其中數據類型:Symbol
、undefined
、null
、Boolean
、String
、Number
、Object
。「簡單使用」:
1 | let leo = Symbol() |
Symbol 支持傳入參數作為 Symbol 名,方便代碼調試:**
1 | let leo = Symbol('leo') |
注意事項
Symbol
函數不能用new
,會報錯。
由於Symbol
是一個原始類型,不是對象,所以不能添加屬性,它是類似於字符串的數據類型。
1 | let leo = new Symbol() |
Symbol
都是不相等的,「即使參數相同」。
1 | // 沒有參數 |
Symbol
不能與其他類型的值計算,會報錯。
1 | let leo = Symbol('hello') |
Symbol
不能自動轉換為字符串,只能顯式轉換。
1 | let leo = Symbol('hello'); |
Symbol
可以轉換為布爾值,但不能轉為數值:
1 | let a1 = Symbol() |
Symbol
屬性不參與for...in/of
循環。
1 | let id = Symbol('id') |
字面量中使用 Symbol 作為屬性名
在對象字面量中使用 Symbol
作為屬性名時,需要使用 「方括號」 ( []
),如 [leo]: "leo"
。好處:防止同名屬性,還有防止鍵被改寫或覆蓋。
1 | let leo = Symbol() |
「需要注意」 :Symbol 作為對象屬性名時,不能用點運算符,並且必須放在方括號內。
1 | let leo = Symbol() |
「常常還用於創建一組常量,保證所有值不相等」:
1 | let user = {} |
應用:消除魔術字符串
「魔術字符串」:指代碼中多次出現,強耦合的字符串或數值,應該避免,而使用含義清晰的變量代替。
1 | function fun(name) { |
常使用變量,消除魔術字符串:
1 | let obj = { |
使用Symbol
消除強耦合,使得不需關係具體的值:
1 | let obj = { |
屬性名遍歷
Symbol 作為屬性名遍歷,不出現在for...in
、for...of
循環,也不被Object.keys()
、Object.getOwnPropertyNames()
、JSON.stringify()
返回。
1 | let leo = Symbol('leo'), |
Object.getOwnPropertySymbols
方法返回一個數組,包含當前對象所有用做屬性名的 Symbol 值。
1 | let user = {} |
另外可以使用Reflect.ownKeys
方法可以返回所有類型的鍵名,包括常規鍵名和 Symbol 鍵名。
1 | let user = { |
由於 Symbol 值作為名稱的屬性不被常規方法遍歷獲取,因此常用於定義對象的一些非私有,且內部使用的方法。
Symbol.for()、Symbol.keyFor()
Symbol.for()
「用於重複使用一個 Symbol 值」,接收一個「字符串」作為參數,若存在用此參數作為名稱的 Symbol 值,返回這個 Symbol,否則新建並返回以這個參數為名稱的 Symbol 值。
1 | let leo = Symbol.for('leo') |
Symbol()
和 Symbol.for()
區別:
1 | Symbol.for('leo') === Symbol.for('leo') // true |
Symbol.keyFor()
「用於返回一個已使用的 Symbol 類型的 key」:
1 | let leo = Symbol.for('leo') |
內置的 Symbol 值
ES6 提供 11 個內置的 Symbol 值,指向語言內部使用的方法:
Symbol.hasInstance
當其他對象使用instanceof
運算符,判斷是否為該對象的實例時,會調用這個方法。比如,foo instanceof Foo
在語言內部,實際調用的是Foo[Symbol.hasInstance](foo)
。
1 | class P { |
P 是一個類,new P() 會返回一個實例,該實例的Symbol.hasInstance
方法,會在進行instanceof
運算時自動調用,判斷左側的運算子是否為Array
的實例。
Symbol.isConcatSpreadable
值為布爾值,表示該對象用於Array.prototype.concat()
時,是否可以展開。
1 | let a = ['aa', 'bb'] |
Symbol.species
指向一個構造函數,在創建衍生對象時會使用,使用時需要用get
取值器。
1 | class P extends Array { |
解決下面問題:
1 | // 問題: b應該是 Array 的實例,實際上是 P 的實例 |
Symbol.match
當執行str.match(myObject)
,傳入的屬性存在時會調用,並返回該方法的返回值。
1 | class P { |
Symbol.replace
當該對象被String.prototype.replace
方法調用時,會返回該方法的返回值。
1 | let a = {} |
Symbol.hasInstance
當該對象被String.prototype.search
方法調用時,會返回該方法的返回值。
1 | class P { |
Symbol.split
當該對象被String.prototype.split
方法調用時,會返回該方法的返回值。
1 | // 重新定義了字符串對象的split方法的行為 |
Symbol.iterator
對象進行for...of
循環時,會調用Symbol.iterator
方法,返回該對象的默認遍歷器。
1 | class P { |
Symbol.toPrimitive
該對象被轉為原始類型的值時,會調用這個方法,返回該對象對應的原始類型值。調用時,需要接收一個字符串參數,表示當前運算模式,運算模式有:
- Number : 此時需要轉換成數值
- String : 此時需要轉換成字符串
- Default : 此時可以轉換成數值或字符串
1 | let obj = { |
Symbol.toStringTag
在該對象上面調用Object.prototype.toString
方法時,如果這個屬性存在,它的返回值會出現在toString
方法返回的字符串之中,表示對象的類型。也就是説,這個屬性可以用來定製[object Object]
或[object Array]
中object
後面的那個字符串。
1 | // 例一 |
Symbol.unscopables
該對象指定了使用 with 關鍵字時,哪些屬性會被 with 環境排除。
1 | // 沒有 unscopables 時 |
上面代碼通過指定Symbol.unscopables
屬性,使得with
語法塊不會在當前作用域尋找foo
屬性,即foo
將指向外層作用域的變量。
原始值轉換
前面複習到字符串、數值、布爾值等的轉換,但是沒有講到對象的轉換規則,這部分就一起看看:。需要記住幾個規則:
- 所有對象在布爾上下文中都為
true
,並且不存在轉換為布爾值的操作,只有字符串和數值轉換有。 - 數值轉換髮生在對象相減或應用數學函數時。如
Date
對象可以相減,如date1 - date2
結果為兩個時間的差值。 - 在字符串轉換,通常出現在如
alert(obj)
這種形式。
當然我們可以使用特殊的對象方法,對字符串和數值轉換進行微調。下面介紹三個類型(hint)轉換情況:
object to string
對象到字符串的轉換,當我們對期望一個字符串的對象執行操作時,如 “alert”:
1 | // 輸出 |
object to number
對象到數字的轉換,例如當我們進行數學運算時:
1 | // 顯式轉換 |
object to default
少數情況下,「當運算符 “不確定” 期望值類型時」。例如,二進制加法 +
可用於字符串(連接),也可以用於數字(相加),所以字符串和數字這兩種類型都可以。因此,當二元加法得到對象類型的參數時,它將依據 "default"
來對其進行轉換。此外,如果對象被用於與字符串、數字或 symbol 進行 ==
比較,這時到底應該進行哪種轉換也不是很明確,因此使用 "default"
。
1 | // 二元加法使用默認 hint |
類型轉換算法
「為了進行轉換,JavaScript 嘗試查找並調用三個對象方法:」
- 調用
obj[Symbol.toPrimitive](hint)
—— 帶有 symbol 鍵Symbol.toPrimitive
(系統 symbol)的方法,如果這個方法存在的話, - 否則,如果 hint 是
"string"
—— 嘗試obj.toString()
和obj.valueOf()
,無論哪個存在。 - 否則,如果 hint 是
"number"
或"default"
—— 嘗試obj.valueOf()
和obj.toString()
,無論哪個存在。
Symbol.toPrimitive
詳細介紹可閲讀《MDN | Symbol.toPrimitive》 。Symbol.toPrimitive
是一個內置的 Symbol 值,它是作為對象的函數值屬性存在的,當一個對象轉換為對應的原始值時,會調用此函數。簡單示例介紹:
1 | let user = { |
toString/valueOf
toString
/ valueOf()
是兩個比較早期的實現轉換的方法。當沒有 Symbol.toPrimitive
,那麼 JavaScript 將嘗試找到它們,並且按照下面的順序進行嘗試:
- 對於 “string” hint,
toString -> valueOf
。 - 其他情況,
valueOf -> toString
。
這兩個方法必須返回一個原始值。如果 toString
或 valueOf
返回了一個對象,那麼返回值會被忽略。默認情況下,普通對象具有 toString
和 valueOf
方法:
toString
方法返回一個字符串"[object Object]"
。valueOf
方法返回對象自身。
簡單示例介紹:
1 | const user = {name: "Leo"}; |
我們也可以結合 toString
/ valueOf()
實現前面第 5 點介紹的 user
對象:
1 | let user = { |
總結
本文作為《初中級前端 JavaScript 自測清單》第二部分,介紹的內容以 JavaScript 對象為主,其中有讓我眼前一亮的知識點,如 Symbol.toPrimitive
方法。我也希望這個清單能幫助大家自測自己的 JavaScript 水平並查缺補漏,温故知新。
如果你喜歡這篇文章,可以關注原作者的公眾號 前端自習課