以下文章轉載自前端自習課,作者王平安

前言

《初中級前端 JavaScript 自測清單 - 1》部分中,和大家簡單過了一遍 JavaScript 的一些基礎知識,沒看過的朋友可以回顧一下 😁

本系列文章是我在我們團隊內部的 “「現代 JavaScript 突擊隊」”,第一期學習內容為《現代 JavaScript 教程》系列的「第二部分」輸出內容,希望這份自測清單,能夠幫助大家鞏固知識,温故知新。

本部分內容,以 「JavaScript 對象」為主,大致包括以下內容:

對象

JavaScript 有八種數據額類型,有七種原始類型,它們值只包含一種類型(字符串,數字或其他),而對象是用來「保存鍵值對和更復雜實體。」我們可以通過使用帶有可選「屬性列表」的花括號 {...} 來創建對象,一個屬性就是一個鍵值對 {"key" : "value"} ,其中鍵( key )是一個字符串(或稱屬性名),值( value )可以是任何類型。

創建對象

我們可以使用 2 種方式來創建一個新對象:

1
2
3
4
5
// 1. 通過“構造函數”創建
let user = new Object()

// 2. 通過“字面量”創建
let user = {}

對象文本和屬性

創建對象時,可以初始化對象的一些屬性:

1
2
3
4
let user = {
name: 'leo',
age: 18,
}

然後可以對該對象進行屬性對「增刪改查」操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 增加屬性
user.addr = 'China'
// user => {name: "leo", age: 18, addr: "China"}

// 刪除屬性
delete user.addr
// user => {name: "leo", age: 18}

// 修改屬性
user.age = 20
// user => {name: "leo", age: 20}

// 查找屬性
user.age
// 20

方括號的使用

當然對象的鍵( key )也可以是多詞屬性,但必須加引號,使用的時候,必須使用方括號( [] )讀取:

1
2
3
4
5
6
let user = {
name: 'leo',
'my interest': ['coding', 'football', 'cycling'],
}
user['my interest'] // ["coding", "football", "cycling"]
delete user['my interest']

我們也可以在方括號中使用變量,來獲取屬性值:

1
2
3
4
5
6
7
8
9
10
11
let key = 'name'
let user = {
name: 'leo',
age: 18,
}
// ok
user[key] // "leo"
user[key] = 'pingan'

// error
user.key // undefined

計算屬性

創建對象時,可以在對象字面量中使用方括號,即 「計算屬性」

1
2
3
4
5
6
7
8
let key = 'name'
let inputKey = prompt('請輸入key', 'age')
let user = {
[key]: 'leo',
[inputKey]: 18,
}
// 當用户在 prompt 上輸入 "age" 時,user 變成下面樣子:
// {name: "leo", age: 18}

當然,計算屬性也可以是表達式:

1
2
3
4
5
let key = 'name'
let user = {
['my_' + key]: 'leo',
}
user['my_' + key] // "leo"

屬性名簡寫

實際開發中,可以將相同的屬性名和屬性值簡寫成更短的語法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 原本書寫方式
let getUser = function (name, age) {
// ...
return {
name: name,
age: age,
}
}

// 簡寫方式
let getUser = function (name, age) {
// ...
return {
name,
age,
}
}

也可以混用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 原本書寫方式
let getUser = function (name, age) {
// ...
return {
name: name,
age: 18,
}
}

// 簡寫方式
let getUser = function (name, age) {
// ...
return {
name,
age: 18,
}
}

對象屬性存在性檢測

使用 in 關鍵字

該方法可以判斷「對象的自有屬性和繼承來的屬性」是否存在。

1
2
3
4
let user = { name: 'leo' }
'name' in user //true,自有屬性存在
'age' in user //false
"toString" in user; //true,是一個繼承屬性

使用對象的 hasOwnProperty() 方法。

該方法只能判斷「自有屬性」是否存在,對於「繼承屬性」會返回 false

1
2
3
4
let user = {name"leo"};
user.hasOwnProperty("name");       //true,自有屬性中有 name
user.hasOwnProperty("age");        //false,自有屬性中不存在 age
user.hasOwnProperty("toString");   //false,這是一個繼承屬性,但不是自有屬性

6.3 用 undefined 判斷

該方法可以判斷對象的「自有屬性和繼承屬性」

1
2
3
4
let user = {name"leo"};
user.name !== undefined;        // true
user.age  !== undefined;        // false
user.toString !== undefined      // true

該方法存在一個問題,如果屬性的值就是 undefined  的話,該方法不能返回想要的結果:

1
2
3
4
let user = {nameundefined};
user.name !== undefined;        // false,屬性存在,但值是undefined
user.age  !== undefined;        // false
user.toString !== undefined;    // true

6.4 在條件語句中直接判斷

1
2
3
4
5
let user = {}
if (user.name) user.name = 'pingan'
//如果 name 是 undefine, null, false, " ", 0 或 NaN,它將保持不變

user // {}

對象循環遍歷

當我們需要遍歷對象中每一個屬性,可以使用 for...in 語句來實現

for…in 循環

for...in 語句以任意順序遍歷一個對象的除 Symbol 以外的可枚舉屬性。「注意」for...in 不應該應用在一個數組,其中索引順序很重要。

1
2
3
4
5
6
7
8
9
10
let user = {
name: 'leo',
age: 18,
}

for (let k in user) {
console.log(k, user[k])
}
// name leo
// age 18

ES7 新增方法

ES7 中新增加的  Object.values()Object.entries()與之前的Object.keys()類似,返回數組類型。

  1. Object.keys()

返回一個數組,成員是參數對象自身的(不含繼承的)所有「可遍歷屬性」的健名。

1
2
let user = { name: 'leo', age: 18 }
Object.keys(user) // ["name", "age"]
  1. Object.values()

返回一個數組,成員是參數對象自身的(不含繼承的)所有可遍歷屬性的鍵值。

1
2
let user = { name: 'leo', age: 18 }
Object.values(user) // ["leo", 18]

如果參數不是對象,則返回空數組:

1
2
Object.values(10) // []
Object.values(true) // []
  1. Object.entries()

返回一個數組,成員是參數對象自身的(不含繼承的)所有「可遍歷屬性」的鍵值對數組。

1
2
3
let user = { name: 'leo', age: 18 }
Object.entries(user)
// [["name","leo"],["age",18]]

手動實現Object.entries()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Generator函數實現:
function* entries(obj) {
for (let k of Object.keys(obj)) {
yield [k, obj[k]]
}
}
// 非Generator函數實現:
function entries(obj) {
let arr = []
for (let k of Object.keys(obj)) {
arr.push([k, obj[k]])
}
return arr
}
  1. Object.getOwnPropertyNames(Obj)

該方法返回一個數組,它包含了對象  Obj  所有擁有的屬性(「無論是否可枚舉」)的名稱。

1
2
3
let user = { name: 'leo', age: 18 }
Object.getOwnPropertyNames(user)
// ["name", "age"]

對象拷貝

參考文章《搞不懂 JS 中賦值 · 淺拷貝 · 深拷貝的請看這裏》

賦值操作

首先回顧下基本數據類型和引用數據類型:

  • 基本類型

    概念:基本類型值在內存中佔據固定大小,保存在棧內存中(不包含閉包中的變量)。常見包括:undefined, null, Boolean, String, Number, Symbol

  • 引用類型

    概念:引用類型的值是對象,保存在堆內存中。而棧內存存儲的是對象的變量標識符以及對象在堆內存中的存儲地址 (引用),引用數據類型在棧中存儲了指針,該指針指向堆中該實體的起始地址。當解釋器尋找引用值時,會首先檢索其在棧中的地址,取得地址後從堆中獲得實體。常見包括:Object,Array,Date,Function,RegExp 等

基本數據類型賦值

在棧內存中的數據發生數據變化的時候,系統會自動為新的變量分配一個新的之值在棧內存中,兩個變量相互獨立,互不影響的。

1
2
3
4
5
let user = 'leo'
let user1 = user
user1 = 'pingan'
console.log(user) // "leo"
console.log(user1) // "pingan"

引用數據類型賦值

在 JavaScript 中,變量不存儲對象本身,而是存儲其 “內存中的地址”,換句話説就是存儲對其的 “引用”。如下面 leo  變量只是保存對user 對象對應引用:

1
2
let user = { name: 'leo', age: 18 }
let leo = user

其他變量也可以引用 user 對象:

1
2
let leo1 = user
let leo2 = user

但是由於變量保存的是引用,所以當我們修改變量 leo \ leo1 \ leo2 這些值時,「也會改動到引用對象」 user ,但當 user 修改,則其他引用該對象的變量,值都會發生變化:

1
2
3
4
5
6
7
8
9
10
leo.name = 'pingan'
console.log(leo) // {name: "pingan", age: 18}
console.log(leo1) // {name: "pingan", age: 18}
console.log(leo2) // {name: "pingan", age: 18}
console.log(user) // {name: "pingan", age: 18}
user.name = 'pingan8787'
console.log(leo) // {name: "pingan8787", age: 18}
console.log(leo1) // {name: "pingan8787", age: 18}
console.log(leo2) // {name: "pingan8787", age: 18}
console.log(user) // {name: "pingan8787", age: 18}

這個過程中涉及變量地址指針指向問題,這裏暫時不展開討論,有興趣的朋友可以網上查閲相關資料。

對象比較

當兩個變量引用同一個對象時,它們無論是 == 還是 === 都會返回 true

1
2
3
4
5
6
7
let user = { name: 'leo', age: 18 }
let leo = user
let leo1 = user
leo == leo1 // true
leo === leo1 // true
leo == user // true
leo === user // true

但如果兩個變量是空對象 {} ,則不相等:

1
2
3
4
let leo1 = {}
let leo2 = {}
leo1 == leo2 // false
leo1 === leo2 // false

淺拷貝

概念

概念:「新的對象複製已有對象中非對象屬性的值和對象屬性的引用」。也可以理解為:「一個新的對象直接拷貝已存在的對象的對象屬性的引用」,即淺拷貝。

淺拷貝「只對第一層屬性進行了拷貝」,當第一層的屬性值是基本數據類型時,新的對象和原對象互不影響,但是如果第一層的屬性值是複雜數據類型,那麼新對象和原對象的屬性值其指向的是同一塊內存地址。

通過示例代碼演示沒有使用淺拷貝場景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 示例1 對象原始拷貝
let user = { name: 'leo', skill: { JavaScript: 90, CSS: 80 } }
let leo = user
leo.name = 'leo1'
leo.skill.CSS = 90
console.log(leo.name) // "leo1"
console.log(user.name) // "leo1"
console.log(leo.skill.CSS) // 90
console.log(user.skill.CSS) // 90

// 示例2 數組原始拷貝
let user = ['leo', 'pingan', { name: 'pingan8787' }]
let leo = user
leo[0] = 'pingan888'
leo[2]['name'] = 'pingan999'
console.log(leo[0]) // "pingan888"
console.log(user[0]) // "pingan888"
console.log(leo[2]['name']) // "pingan999"
console.log(user[2]['name']) // "pingan999"

從上面示例代碼可以看出:由於對象被直接拷貝,相當於拷貝 「引用數據類型」 ,所以在新對象修改任何值時,都會改動到源數據。

接下來實現淺拷貝,對比以下。

實現淺拷貝

  1. Object.assign()

語法:Object.assign(target, ...sources)ES6 中拷貝對象的方法,接受的第一個參數是拷貝的目標 target,剩下的參數是拷貝的源對象 sources(可以是多個)。詳細介紹,可以閲讀文檔《MDN Object.assign》。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 示例1 對象淺拷貝
let user = { name: 'leo', skill: { JavaScript: 90, CSS: 80 } }
let leo = Object.assign({}, user)
leo.name = 'leo1'
leo.skill.CSS = 90
console.log(leo.name) // "leo1" ⚠️ 差異!
console.log(user.name) // "leo"  ⚠️ 差異!
console.log(leo.skill.CSS) // 90
console.log(user.skill.CSS) // 90

// 示例2 數組深拷貝
let user = ['leo', 'pingan', { name: 'pingan8787' }]
let leo = user
leo[0] = 'pingan888'
leo[2]['name'] = 'pingan999'
console.log(leo[0]) // "pingan888"  ⚠️ 差異!
console.log(user[0]) // "leo"        ⚠️ 差異!
console.log(leo[2]['name']) // "pingan999"
console.log(user[2]['name']) // "pingan999"

從打印結果可以看出,淺拷貝只是在根屬性 (對象的第一層級) 創建了一個新的對象,但是對於屬性的值是對象的話只會拷貝一份相同的內存地址。

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
  1. Array.prototype.slice()

語法:arr.slice([begin[, end]])``slice() 方法返回一個新的數組對象,這一對象是一個由 beginend 決定的原數組的淺拷貝(包括 begin,不包括end)。原始數組不會被改變。詳細介紹,可以閲讀文檔《MDN Array slice》。

1
2
3
4
5
6
7
8
9
// 示例 數組深拷貝
let user = ['leo', 'pingan', { name: 'pingan8787' }]
let leo = Array.prototype.slice.call(user)
leo[0] = 'pingan888'
leo[2]['name'] = 'pingan999'
console.log(leo[0]) // "pingan888"  ⚠️ 差異!
console.log(user[0]) // "leo"        ⚠️ 差異!
console.log(leo[2]['name']) // "pingan999"
console.log(user[2]['name']) // "pingan999"
  1. Array.prototype.concat()

語法:var new_array = old_array.concat(value1[, value2[, ...[, valueN]]])``concat() 方法用於合併兩個或多個數組。此方法不會更改現有數組,而是返回一個新數組。詳細介紹,可以閲讀文檔《MDN Array concat》。

1
2
3
4
5
6
7
let user = [{ name: 'leo' }, { age: 18 }]
let user1 = [{ age: 20 }, { addr: 'fujian' }]
let user2 = user.concat(user1)
user1[0]['age'] = 25
console.log(user) // [{"name":"leo"},{"age":18}]
console.log(user1) // [{"age":25},{"addr":"fujian"}]
console.log(user2) // [{"name":"leo"},{"age":18},{"age":25},{"addr":"fujian"}]

Array.prototype.concat 也是一個淺拷貝,只是在根屬性 (對象的第一層級) 創建了一個新的對象,但是對於屬性的值是對象的話只會拷貝一份相同的內存地址。

  1. 拓展運算符(…)

語法:var cloneObj = { ...obj };擴展運算符也是淺拷貝,對於值是對象的屬性無法完全拷貝成 2 個不同對象,但是如果屬性都是基本類型的值的話,使用擴展運算符也是優勢方便的地方。

1
2
3
4
5
6
7
8
let user = { name: 'leo', skill: { JavaScript: 90, CSS: 80 } }
let leo = { ...user }
leo.name = 'leo1'
leo.skill.CSS = 90
console.log(leo.name) // "leo1" ⚠️ 差異!
console.log(user.name) // "leo"  ⚠️ 差異!
console.log(leo.skill.CSS) // 90
console.log(user.skill.CSS) // 90

手寫淺拷貝

實現原理:新的對象複製已有對象中非對象屬性的值和對象屬性的「引用」, 也就是説對象屬性並不複製到內存。

1
2
3
4
5
6
7
8
9
function cloneShallow(source) {
let target = {};
for (let key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
}
return target;
}
  • 「for in」

for…in 語句以任意順序遍歷一個對象自有的、繼承的、可枚舉的、非 Symbol 的屬性。對於每個不同的屬性,語句都會被執行。

  • 「hasOwnProperty」

該函數返回值為布爾值,所有繼承了 Object 的對象都會繼承到 hasOwnProperty 方法,和 in 運算符不同,該函數會忽略掉那些從原型鏈上繼承到的屬性和自身屬性。語法:obj.hasOwnProperty(prop)
prop 是要檢測的屬性「字符串名稱」或者Symbol

深拷貝

概念

複製變量值,對於引用數據,則遞歸至基本類型後,再複製。深拷貝後的對象「與原來的對象完全隔離」,互不影響,對一個對象的修改並不會影響另一個對象。

實現深拷貝

  1. JSON.parse(JSON.stringify())

其原理是把一個對象序列化成為一個 JSON 字符串,將對象的內容轉換成字符串的形式再保存在磁盤上,再用JSON.parse() 反序列化將 JSON 字符串變成一個新的對象。

1
2
3
4
5
6
7
8
let user = { name: 'leo', skill: { JavaScript: 90, CSS: 80 } }
let leo = JSON.parse(JSON.stringify(user))
leo.name = 'leo1'
leo.skill.CSS = 90
console.log(leo.name) // "leo1" ⚠️ 差異!
console.log(user.name) // "leo"  ⚠️ 差異!
console.log(leo.skill.CSS) // 90 ⚠️ 差異!
console.log(user.skill.CSS) // 80 ⚠️ 差異!

JSON.stringify() 使用注意:

  • 拷貝的對象的值中如果有函數, undefinedsymbol 則經過 JSON.stringify() ` 序列化後的 JSON 字符串中這個鍵值對會消失;
  • 無法拷貝不可枚舉的屬性,無法拷貝對象的原型鏈;
  • 拷貝 Date 引用類型會變成字符串;
  • 拷貝 RegExp 引用類型會變成空對象;
  • 對象中含有 NaNInfinity-Infinity ,則序列化的結果會變成 null
  • 無法拷貝對象的循環應用 (即 obj[key] = obj )。
  1. 第三方庫

手寫深拷貝

核心思想是「遞歸」,遍歷對象、數組直到裏邊都是基本數據類型,然後再去複製,就是深度拷貝。實現代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const isObject = obj => typeof obj === 'object' && obj != null;

function cloneDeep(source) {
if (!isObject(source)) return source; // 非對象返回自身
const target = Array.isArray(source) ? [] : {};
for(var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
if (isObject(source[key])) {
target[key] = cloneDeep(source[key]); // 注意這裏
} else {
target[key] = source[key];
}
}
}
return target;
}

該方法缺陷:遇到循環引用,會陷入一個循環的遞歸過程,從而導致爆棧。其他寫法,可以閲讀《如何寫出一個驚豔面試官的深拷貝?》 。

小結

「淺拷貝」:將對象的每個屬性進行依次複製,但是當對象的屬性值是引用類型時,實質複製的是其引用,當引用指向的值改變時也會跟着變化。

「深拷貝」:複製變量值,對於引用數據,則遞歸至基本類型後,再複製。深拷貝後的對象「與原來的對象完全隔離」,互不影響,對一個對象的修改並不會影響另一個對象。

「深拷貝和淺拷貝是針對複雜數據類型來説的,淺拷貝只拷貝一層,而深拷貝是層層拷貝。」

垃圾回收機制(GC)

垃圾回收(Garbage Collection,縮寫為 GC)是一種自動的存儲器管理機制。當某個程序佔用的一部分內存空間不再被這個程序訪問時,這個程序會藉助垃圾回收算法向操作系統歸還這部分內存空間。垃圾回收器可以減輕程序員的負擔,也減少程序中的錯誤。垃圾回收最早起源於 LISP 語言。目前許多語言如 Smalltalk、Java、C# 和 D 語言都支持垃圾回收器,我們熟知的 JavaScript 具有自動垃圾回收機制。

「在 JavaScript 中,原始類型的數據被分配到棧空間中,引用類型的數據會被分配到堆空間中。」

棧空間中的垃圾回收

當函數 showName 調用完成後,通過下移 ESP(Extended Stack Pointer)指針,來銷燬 showName 函數,之後調用其他函數時,將覆蓋掉舊內存,存放另一個函數的執行上下文,實現垃圾回收。圖片來自《瀏覽器工作原理與實踐》

堆空間中的垃圾回收

堆中數據垃圾回收策略的基礎是:「代際假説」(The Generational Hypothesis)。即:

  1. 大部分對象在內存中存在時間極短,很多對象很快就不可訪問。
  2. 不死的對象將活得更久。

這兩個特點不僅僅適用於 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
2
3
4
5
6
7
let user = {}
let say = function () {
console.log('hello!')
}

user.say = say // 賦值到對象上
user.say() // "hello!"

也可以使用更加簡潔的方法:

1
2
3
4
5
6
7
8
9
10
let user = {
 sayfunction(){}
  
  // 簡寫為
 say (){console.log("hello!")}

 // ES8 async 方法
 async say (){/.../}
}
user.say();

當然對象方法的名稱,還支持計算的屬性名稱作為方法名:

1
2
3
4
5
6
7
const hello = 'Hello'
let user = {
['say' + hello]() {
console.log('hello!')
},
}
user['say' + hello]() // "hello!"

另外需要注意的是:所有方法定義不是構造函數,如果您嘗試實例化它們,將拋出TypeError

1
2
3
4
let user = {
say(){};
}
new user.say; // TypeError: user.say is not a constructor

this

this 簡介

當對象方法需要使用對象中的屬性,可以使用 this 關鍵字:

1
2
3
4
5
6
7
8
let user = {
name: 'leo',
say() {
console.log(`hello ${this.name}`)
},
}

user.say() // "hello leo"

當代碼 user.say() 執行過程中, this 指的是 user 對象。當然也可以直接使用變量名 user 來引用 say() 方法:

1
2
3
4
5
6
7
8
let user = {
name: 'leo',
say() {
console.log(`hello ${user.name}`)
},
}

user.say() // "hello leo"

但是這樣並不安全,因為 user 對象可能賦值給另外一個變量,並且將其他值賦值給 user 對象,就可能導致報錯:

1
2
3
4
5
6
7
8
9
10
11
let user = {
name: 'leo',
say() {
console.log(`hello ${user.name}`)
},
}

let leo = user
user = null

leo.say() // Uncaught TypeError: Cannot read property 'name' of null

但將  user.name   改成 this.name 代碼便正常運行。

this 取值

this 的值是在 「代碼運行時計算出來」 的,它的值取決於代碼上下文:

1
2
3
4
5
6
7
8
9
10
11
12
13
let user = { name: 'leo' }
let admin = { name: 'pingan' }
let say = function () {
console.log(`hello ${this.name}`)
}

user.fun = say
admin.fun = say

// 函數內部 this 是指“點符號前面”的對象
user.fun() // "hello leo"
admin.fun() // "hello pingan"
admin['fun']() // "hello pingan"

規則:如果 obj.fun() 被調用,則 thisfun 函數調用期間是 obj ,所以上面的 this 先是 user ,然後是 admin

但是在全局環境中,無論是否開啟嚴格模式, this 都指向全局對象

1
2
3
4
console.log(this == window) // true
let a = 10
this.b = 10
a === this.b // true

箭頭函數沒有自己的 this

箭頭函數比較特別,沒有自己的 this ,如果有引用 this 的話,則指向外部正常函數,下面例子中, this 指向 user.say() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
let user = {
name: 'leo',
say: () => {
console.log(`hello ${this.name}`)
},
hello() {
let fun = () => console.log(`hello ${this.name}`)
fun()
},
}

user.say() // hello      => say() 外部函數是 window
user.hello() // hello leo  => fun() 外部函數是 hello

call / apply / bind

詳細可以閲讀《js 基礎 - 關於 call,apply,bind 的一切》 。當我們想把 this 值綁定到另一個環境中,就可以使用 call / apply / bind 方法實現:

1
2
3
4
5
6
7
8
9
var user = { name: 'leo' }
var name = 'pingan'
function fun() {
return console.log(this.name) // this 的值取決於函數調用方式
}

fun() // "pingan"
fun.call(user) // "leo"
fun.apply(user) // "leo"

注意:這裏的 var name = 'pingan'; 需要使用 var 來聲明,使用 let 的話, window 上將沒有 name 變量。

三者語法如下:

1
2
3
fun.call(thisArg, param1, param2, ...)
fun.apply(thisArg, [param1,param2,...])
fun.bind(thisArg, param1, param2, ...)

構造函數和 new 運算符

構造函數

構造函數的作用在於 「實現可重用的對象創建代碼」 。通常,對於構造函數有兩個約定:

  • 命名時首字母大寫;
  • 只能使用 new 運算符執行。

new 運算符」創建一個用户定義的對象類型的實例或具有構造函數的內置對象的實例。語法如下:

1
new constructor[([arguments])]

參數如下:

  • constructor一個指定對象實例的類型的類或函數。
  • arguments一個用於被 constructor 調用的參數列表。

簡單示例

舉個簡單示例:

1
2
3
4
5
6
function User(name) {
this.name = name
this.isAdmin = false
}
const leo = new User('leo')
console.log(leo.name, leo.isAdmin) // "leo" false

new 運算符操作過程

當一個函數被使用 new 運算符執行時,它按照以下步驟:

  1. 一個新的空對象被創建並分配給 this
  2. 函數體執行。通常它會修改 this,為其添加新的屬性。
  3. 返回 this 的值。

以前面 User 方法為例:

1
2
3
4
5
6
7
8
9
function User(name) {
// this = {};(隱式創建)

// 添加屬性到 this
this.name = name
this.isAdmin = false // return this;(隱式返回)
}
const leo = new User('leo')
console.log(leo.name, leo.isAdmin) // "leo" false

當我們執行 new User('leo') 時,發生以下事情:

  1. 一個繼承自 User.prototype 的新對象被創建;
  2. 使用指定參數調用構造函數 User ,並將 this 綁定到新創建的對象;
  3. 由構造函數返回的對象就是 new 表達式的結果。如果構造函數沒有顯式返回一個對象,則使用步驟 1 創建的對象。

「需要注意」

  1. 一般情況下,構造函數不返回值,但是開發者可以選擇主動返回對象,來覆蓋正常的對象創建步驟;
  2. new User 等同於 new User() ,只是沒有指定參數列表,即 User 不帶參數的情況;
1
2
3
let user = new User() // <-- 沒有參數
// 等同於
let user = new User()
  1. 任何函數都可以作為構造器,即都可以使用 new 運算符運行。
  2. 構造函數中的方法

在構造函數中,也可以將方法綁定到 this 上:

1
2
3
4
5
6
7
8
9
10
function User(name) {
this.name = name
this.isAdmin = false
this.sayHello = function () {
console.log('hello ' + this.name)
}
}
const leo = new User('leo')
console.log(leo.name, leo.isAdmin) // "leo" false
leo.sayHello() // "hello leo"

可選鏈 “?.”

詳細介紹可以查看 《MDN 可選鏈操作符》 。

背景介紹

在實際開發中,常常出現下面幾種報錯情況:

1
2
3
4
5
6
7
8
// 1. 對象中不存在指定屬性
const leo = {};
console.log(leo.name.toString());
// Uncaught TypeError: Cannot read property 'toString' of undefined

// 2. 使用不存在的 DOM 節點屬性
const dom = document.getElementById("dom").innerHTML; 
// Uncaught TypeError: Cannot read property 'innerHTML' of null

在可選鏈 ?. 出現之前,我們會使用短路操作 && 運算符來解決該問題:

1
2
const leo = {};
console.log(leo && leo.name && leo.name.toString()); // undefined

這種寫法的缺點就是 「太麻煩了」

可選鏈介紹

可選鏈 ?. 是一種 「訪問嵌套對象屬性的防錯誤方法」 。即使中間的屬性不存在,也不會出現錯誤。如果可選鏈 ?. 前面部分是 undefined 或者 null,它會停止運算並返回 undefined

語法:

1
2
3
4
obj?.prop
obj?.[expr]
arr?.[index]
func?.(args)

「我們改造前面示例代碼:」

1
2
3
4
5
6
7
8
// 1. 對象中不存在指定屬性
const leo = {};
console.log(leo?.name?.toString());
// undefined

// 2. 使用不存在的 DOM 節點屬性
const dom = document?.getElementById("dom")?.innerHTML; 
// undefined

使用注意

可選鏈雖然好用,但需要注意以下幾點:

  1. 「不能過度使用可選鏈」

我們應該只將 ?. 使用在一些屬性或方法可以不存在的地方,以上面示例代碼為例:

1
2
const leo = {};
console.log(leo.name?.toString());

這樣寫會更好,因為 leo 對象是必須存在,而 name 屬性則可能不存在。

  1. 「可選鏈 ?. 之前的變量必須已聲明」

在可選鏈 ?. 之前的變量必須使用 let/const/var 聲明,否則會報錯:

1
2
leo?.name
// Uncaught ReferenceError: leo is not defined
  1. 「可選鏈不能用於賦值」
1
2
3
let object = {}
object?.property = 1
// Uncaught SyntaxError: Invalid left-hand side in assignment
  1. 「可選鏈訪問數組元素的方法」
1
let arrayItem = arr?.[42]

其他情況:?.() 和 ?.[]

需要説明的是 ?. 是一個特殊的語法結構,而不是一個運算符,它還可以與其 ()[] 一起使用:

可選鏈與函數調用 ?.()

?.() 用於調用一個可能不存在的函數,比如:

1
2
3
4
5
6
7
8
9
10
let user1 = {
admin() {
alert('I am admin')
},
}

let user2 = {}

user1.admin?.() // I am admin
user2.admin?.()

?.() 會檢查它左邊的部分:如果 admin 函數存在,那麼就調用運行它(對於 user1)。否則(對於 user2)運算停止,沒有錯誤。

可選鏈和表達式 ?.[]

?.[] 允許從一個可能不存在的對象上安全地讀取屬性。

1
2
3
4
5
6
7
8
9
10
let user1 = {
firstName: 'John',
}

let user2 = null // 假設,我們不能授權此用户
let key = 'firstName'

alert(user1?.[key]) // John
alert(user2?.[key]) // undefined
alert(user1?.[key]?.something?.not?.existing) // undefined

可選鏈 ?. 語法總結

可選鏈 ?. 語法有三種形式:

  1. obj?.prop —— 如果 obj 存在則返回 obj.prop,否則返回 undefined
  2. obj?.[prop] —— 如果 obj 存在則返回 obj[prop],否則返回 undefined
  3. obj?.method() —— 如果 obj 存在則調用 obj.method(),否則返回 undefined

正如我們所看到的,這些語法形式用起來都很簡單直接。?. 檢查左邊部分是否為 null/undefined,如果不是則繼續運算。?. 鏈使我們能夠安全地訪問嵌套屬性。

Symbol

規範規定,JavaScript 中對象的屬性只能為 「字符串類型」 或者 「Symbol 類型」 ,畢竟我們也只見過這兩種類型。

概念介紹

ES6 引入Symbol作為一種新的「原始數據類型」,表示「獨一無二」的值,主要是為了「防止屬性名衝突」。ES6 之後,JavaScript 一共有其中數據類型:SymbolundefinednullBooleanStringNumberObject「簡單使用」

1
2
let leo = Symbol()
typeof leo // "symbol"

Symbol 支持傳入參數作為 Symbol 名,方便代碼調試:**

1
let leo = Symbol('leo')

注意事項

  • Symbol函數不能用new,會報錯。

由於Symbol是一個原始類型,不是對象,所以不能添加屬性,它是類似於字符串的數據類型。

1
2
let leo = new Symbol()
// Uncaught TypeError: Symbol is not leo constructor
  • Symbol都是不相等的,「即使參數相同」
1
2
3
4
5
6
7
8
// 沒有參數
let leo1 = Symbol()
let leo2 = Symbol()
leo1 === leo2 // false
// 有參數
let leo1 = Symbol('leo')
let leo2 = Symbol('leo')
leo1 === leo2 // false
  • Symbol不能與其他類型的值計算,會報錯。
1
2
3
let leo = Symbol('hello')
leo + ' world!' // 報錯
;`${leo} world!` // 報錯
  • Symbol 不能自動轉換為字符串,只能顯式轉換。
1
2
3
4
5
6
let leo = Symbol('hello');
alert(leo); 
// Uncaught TypeError: Cannot convert a Symbol value to a string

String(leo); // "Symbol(hello)"
leo.toString(); // "Symbol(hello)"
  • Symbol 可以轉換為布爾值,但不能轉為數值:
1
2
3
4
5
let a1 = Symbol()
Boolean(a1)
!a1 // false
Number(a1) // TypeError
a1 + 1 // TypeError
  • Symbol 屬性不參與 for...in/of 循環。
1
2
3
4
5
6
7
8
9
10
let id = Symbol('id')
let user = {
name: 'Leo',
age: 30,
[id]: 123,
}

for (let key in user) console.log(key) // name, age (no symbols)
// 使用 Symbol 任務直接訪問
console.log('Direct: ' + user[id])

字面量中使用 Symbol 作為屬性名

在對象字面量中使用 Symbol 作為屬性名時,需要使用 「方括號」[] ),如 [leo]: "leo" 。好處:防止同名屬性,還有防止鍵被改寫或覆蓋。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let leo = Symbol()
// 寫法1
let user = {}
user[leo] = 'leo'

// 寫法2
let user = {
[leo]: 'leo',
}
// 寫法3
let user = {}
Object.defineProperty(user, leo, { value: 'leo' })

// 3種寫法 結果相同
user[leo] // 'leo'

「需要注意」 :Symbol 作為對象屬性名時,不能用點運算符,並且必須放在方括號內。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let leo = Symbol()
let user = {}
// 不能用點運算
user.leo = 'leo'
user[leo] // undefined
user['leo'] // 'leo'
// 必須放在方括號內
let user = {
[leo]: function (text) {
console.log(text)
},
}
user[leo]('leo') // 'leo'
// 上面等價於 更簡潔
let user = {
[leo](text) {
console.log(text)
},
}

「常常還用於創建一組常量,保證所有值不相等」

1
2
3
4
5
6
let user = {}
user.list = {
AAA: Symbol('Leo'),
BBB: Symbol('Robin'),
CCC: Symbol('Pingan'),
}

應用:消除魔術字符串

「魔術字符串」:指代碼中多次出現,強耦合的字符串或數值,應該避免,而使用含義清晰的變量代替。

1
2
3
4
5
6
function fun(name) {
if (name == 'leo') {
console.log('hello')
}
}
fun('leo') // 'hello' 為魔術字符串

常使用變量,消除魔術字符串:

1
2
3
4
5
6
7
8
9
let obj = {
name: 'leo',
}
function fun(name) {
if (name == obj.name) {
console.log('hello')
}
}
fun(obj.name) // 'hello'

使用Symbol消除強耦合,使得不需關係具體的值:

1
2
3
4
5
6
7
8
9
let obj = {
name: Symbol(),
}
function fun(name) {
if (name == obj.name) {
console.log('hello')
}
}
fun(obj.name) // 'hello'

屬性名遍歷

Symbol 作為屬性名遍歷,不出現在for...infor...of循環,也不被Object.keys()Object.getOwnPropertyNames()JSON.stringify()返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let leo = Symbol('leo'),
robin = Symbol('robin')
let user = {
[leo]: '18',
[robin]: '28',
}
for (let k of Object.values(user)) {
console.log(k)
}
// 無輸出

let user = {}
let leo = Symbol('leo')
Object.defineProperty(user, leo, { value: 'hi' })
for (let k in user) {
console.log(k) // 無輸出
}
Object.getOwnPropertyNames(user) // []
Object.getOwnPropertySymbols(user) // [Symbol(leo)]

Object.getOwnPropertySymbols方法返回一個數組,包含當前對象所有用做屬性名的 Symbol 值。

1
2
3
4
5
6
7
let user = {}
let leo = Symbol('leo')
let pingan = Symbol('pingan')
user[leo] = 'hi leo'
user[pingan] = 'hi pingan'
let obj = Object.getOwnPropertySymbols(user)
obj //  [Symbol(leo), Symbol(pingan)]

另外可以使用Reflect.ownKeys方法可以返回所有類型的鍵名,包括常規鍵名和 Symbol 鍵名。

1
2
3
4
5
6
let user = {
[Symbol('leo')]: 1,
age: 2,
address: 3,
}
Reflect.ownKeys(user) // ['age', 'address',Symbol('leo')]

由於 Symbol 值作為名稱的屬性不被常規方法遍歷獲取,因此常用於定義對象的一些非私有,且內部使用的方法。

Symbol.for()、Symbol.keyFor()

Symbol.for()

「用於重複使用一個 Symbol 值」,接收一個「字符串」作為參數,若存在用此參數作為名稱的 Symbol 值,返回這個 Symbol,否則新建並返回以這個參數為名稱的 Symbol 值。

1
2
3
let leo = Symbol.for('leo')
let pingan = Symbol.for('pingan')
leo === pingan // true

Symbol()Symbol.for()區別:

1
2
Symbol.for('leo') === Symbol.for('leo') // true
Symbol('leo') === Symbol('leo') // false

Symbol.keyFor()

「用於返回一個已使用的 Symbol 類型的 key」:

1
2
3
4
let leo = Symbol.for('leo')
Symbol.keyFor(leo) //  'leo'
let leo = Symbol('leo')
Symbol.keyFor(leo) //  undefined

內置的 Symbol 值

ES6 提供 11 個內置的 Symbol 值,指向語言內部使用的方法:

Symbol.hasInstance

當其他對象使用instanceof運算符,判斷是否為該對象的實例時,會調用這個方法。比如,foo instanceof Foo在語言內部,實際調用的是Foo[Symbol.hasInstance](foo)

1
2
3
4
5
6
class P {
[Symbol.hasInstance](a) {
return a instanceof Array
}
}
;[1, 2, 3] instanceof new P() // true

P 是一個類,new P() 會返回一個實例,該實例的Symbol.hasInstance方法,會在進行instanceof運算時自動調用,判斷左側的運算子是否為Array的實例。

Symbol.isConcatSpreadable

值為布爾值,表示該對象用於Array.prototype.concat()時,是否可以展開。

1
2
3
4
5
6
7
8
let a = ['aa', 'bb']
;['cc', 'dd'].concat(a, 'ee')
// ['cc', 'dd', 'aa', 'bb', 'ee']
a[Symbol.isConcatSpreadable] // undefined
let b = ['aa', 'bb']
b[Symbol.isConcatSpreadable] = false
;['cc', 'dd'].concat(b, 'ee')
// ['cc', 'dd',[ 'aa', 'bb'], 'ee']

Symbol.species

指向一個構造函數,在創建衍生對象時會使用,使用時需要用get取值器。

1
2
3
4
5
class P extends Array {
static get [Symbol.species]() {
return this
}
}

解決下面問題:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 問題:  b應該是 Array 的實例,實際上是 P 的實例
class P extends Array {}
let a = new P(1, 2, 3)
let b = a.map((x) => x)
b instanceof Array // true
b instanceof P // true
// 解決:  通過使用 Symbol.species
class P extends Array {
static get [Symbol.species]() {
return Array
}
}
let a = new P()
let b = a.map((x) => x)
b instanceof P // false
b instanceof Array // true

Symbol.match

當執行str.match(myObject),傳入的屬性存在時會調用,並返回該方法的返回值。

1
2
3
4
5
6
class P {
[Symbol.match](string) {
return 'hello world'.indexOf(string)
}
}
'h'.match(new P()) // 0

Symbol.replace

當該對象被String.prototype.replace方法調用時,會返回該方法的返回值。

1
2
3
let a = {}
a[Symbol.replace] = (...s) => console.log(s)
'Hello'.replace(a, 'World') // ["Hello", "World"]

Symbol.hasInstance

當該對象被String.prototype.search方法調用時,會返回該方法的返回值。

1
2
3
4
5
6
7
8
9
class P {
constructor(val) {
this.val = val;
}
[Symbol.search](s){
return s.indexOf(this.val);
}
}
'hileo'.search(new P('leo')); // 2

Symbol.split

當該對象被String.prototype.split方法調用時,會返回該方法的返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 重新定義了字符串對象的split方法的行為
class P {
constructor(val) {
this.val = val;
}
[Symbol.split](s) {
let i = s.indexOf(this.val);
if(i == -1) return s;
return [
s.substr(0, i),
s.substr(i + this.val.length)
]
}
}
'helloworld'.split(new P('hello')); // ["hello", ""]
'helloworld'.split(new P('world')); // ["", "world"]
'helloworld'.split(new P('leo')); // "helloworld"

Symbol.iterator

對象進行for...of循環時,會調用Symbol.iterator方法,返回該對象的默認遍歷器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class P {
*[Symbol.interator]() {
let i = 0
while (this[i] !== undefined) {
yield this[i]
++i
}
}
}
let a = new P()
a[0] = 1
a[1] = 2
for (let k of a) {
console.log(k)
}

Symbol.toPrimitive

該對象被轉為原始類型的值時,會調用這個方法,返回該對象對應的原始類型值。調用時,需要接收一個字符串參數,表示當前運算模式,運算模式有:

  • Number : 此時需要轉換成數值
  • String : 此時需要轉換成字符串
  • Default : 此時可以轉換成數值或字符串
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let obj = {
[Symbol.toPrimitive](hint) {
switch (hint) {
case 'number':
return 123
case 'string':
return 'str'
case 'default':
return 'default'
default:
throw new Error()
}
},
}
2 * obj // 246
3 + obj // '3default'
obj == 'default' // true
String(obj) // 'str'

Symbol.toStringTag

在該對象上面調用Object.prototype.toString方法時,如果這個屬性存在,它的返回值會出現在toString方法返回的字符串之中,表示對象的類型。也就是説,這個屬性可以用來定製[object Object][object Array]object後面的那個字符串。

1
2
3
4
5
6
7
8
9
10
11
// 例一
({[Symbol.toStringTag]: 'Foo'}.toString())
// "[object Foo]"
// 例二
class Collection {
get [Symbol.toStringTag]() {
return 'xxx';
}
}
let x = new Collection();
Object.prototype.toString.call(x) // "[object xxx]"

Symbol.unscopables

該對象指定了使用 with 關鍵字時,哪些屬性會被 with 環境排除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 沒有 unscopables 時
class MyClass {
foo() {
return 1
}
}
var foo = function () {
return 2
}
with (MyClass.prototype) {
foo() // 1
}
// 有 unscopables 時
class MyClass {
foo() {
return 1
}
get [Symbol.unscopables]() {
return { foo: true }
}
}
var foo = function () {
return 2
}
with (MyClass.prototype) {
foo() // 2
}

上面代碼通過指定Symbol.unscopables屬性,使得with語法塊不會在當前作用域尋找foo屬性,即foo將指向外層作用域的變量。

原始值轉換

前面複習到字符串、數值、布爾值等的轉換,但是沒有講到對象的轉換規則,這部分就一起看看:。需要記住幾個規則:

  1. 所有對象在布爾上下文中都為 true ,並且不存在轉換為布爾值的操作,只有字符串和數值轉換有。
  2. 數值轉換髮生在對象相減或應用數學函數時。如 Date 對象可以相減,如 date1 - date2 結果為兩個時間的差值。
  3. 在字符串轉換,通常出現在如 alert(obj) 這種形式。

當然我們可以使用特殊的對象方法,對字符串和數值轉換進行微調。下面介紹三個類型(hint)轉換情況:

object to string

對象到字符串的轉換,當我們對期望一個字符串的對象執行操作時,如 “alert”:

1
2
3
4
// 輸出
alert(obj)
// 將對象作為屬性鍵
anotherObj[obj] = 123

object to number

對象到數字的轉換,例如當我們進行數學運算時:

1
2
3
4
5
6
7
// 顯式轉換
let num = Number(obj)
// 數學運算(除了二進制加法)
let n = +obj // 一元加法
let delta = date1 - date2
// 小於/大於的比較
let greater = user1 > user2

object to default

少數情況下,「當運算符 “不確定” 期望值類型時」。例如,二進制加法 + 可用於字符串(連接),也可以用於數字(相加),所以字符串和數字這兩種類型都可以。因此,當二元加法得到對象類型的參數時,它將依據 "default" 來對其進行轉換。此外,如果對象被用於與字符串、數字或 symbol 進行 == 比較,這時到底應該進行哪種轉換也不是很明確,因此使用 "default"

1
2
3
4
// 二元加法使用默認 hint
let total = obj1 + obj2;
// obj == number 使用默認 hint
if (user == 1) { ... };

類型轉換算法

「為了進行轉換,JavaScript 嘗試查找並調用三個對象方法:」

  1. 調用 obj[Symbol.toPrimitive](hint) —— 帶有 symbol 鍵 Symbol.toPrimitive(系統 symbol)的方法,如果這個方法存在的話,
  2. 否則,如果 hint 是 "string" —— 嘗試 obj.toString()obj.valueOf(),無論哪個存在。
  3. 否則,如果 hint 是 "number""default" —— 嘗試 obj.valueOf()obj.toString(),無論哪個存在。

Symbol.toPrimitive

詳細介紹可閲讀《MDN | Symbol.toPrimitive》 。Symbol.toPrimitive 是一個內置的 Symbol 值,它是作為對象的函數值屬性存在的,當一個對象轉換為對應的原始值時,會調用此函數。簡單示例介紹:

1
2
3
4
5
6
7
8
9
10
11
12
13
let user = {
name: 'Leo',
money: 9999,

[Symbol.toPrimitive](hint) {
console.log(`hint: ${hint}`)
return hint == 'string' ? `{name: "${this.name}"}` : this.money
},
}

alert(user) // 控制枱:hint: string 彈框:{name: "John"}
alert(+user) // 控制枱:hint: number 彈框:9999
alert(user + 1) // 控制枱:hint: default 彈框:10000

toString/valueOf

toString / valueOf() 是兩個比較早期的實現轉換的方法。當沒有 Symbol.toPrimitive ,那麼 JavaScript 將嘗試找到它們,並且按照下面的順序進行嘗試:

  • 對於 “string” hint,toString -> valueOf
  • 其他情況,valueOf -> toString

這兩個方法必須返回一個原始值。如果 toStringvalueOf 返回了一個對象,那麼返回值會被忽略。默認情況下,普通對象具有 toStringvalueOf 方法:

  • toString 方法返回一個字符串 "[object Object]"
  • valueOf 方法返回對象自身。

簡單示例介紹:

1
2
3
4
const user = {name: "Leo"};

alert(user); // [object Object]
alert(user.valueOf() === user); // true

我們也可以結合 toString / valueOf()   實現前面第 5 點介紹的 user 對象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let user = {
  name"Leo",
  money9999,

  // 對於 hint="string"
  toString() {
    return `{name: "${this.name}"}`;
  },

  // 對於 hint="number" 或 "default"
  toString() {
    return this.money;
  }

};

alert(user);     // 控制枱:hint: string 彈框:{name: "John"}
alert(+user);    // 控制枱:hint: number 彈框:9999
alert(user + 1); // 控制枱:hint: default 彈框:10000

總結

本文作為《初中級前端 JavaScript 自測清單》第二部分,介紹的內容以 JavaScript 對象為主,其中有讓我眼前一亮的知識點,如 Symbol.toPrimitive  方法。我也希望這個清單能幫助大家自測自己的 JavaScript 水平並查缺補漏,温故知新。

如果你喜歡這篇文章,可以關注原作者的公眾號 前端自習課