xia的小窩

一起來coding和碼字吧

0%

js-物件與物件導向

下面是一個物件

1
2
const a = {
}

對於物件

我們知道物件跟陣列一樣是一個容器 ( 也稱為聚合 ( aggregate ) 或複合資料型態 ( complex data types ) )。物件跟陣列值主要有兩種差異 :

  1. 陣列儲存值、用數字索引,物件儲存特性,用字串或符號索引。
  2. 陣列有序,物件無序

物件導向程式設計 ( oop )

類別 → 統稱

實例 → 代表特定的東西

功能 → 方法

物件字面值 (Object Literal)

我們只需要用到 {},就能夠快速創造一個物件。

1
2
3
4
5
let Test_obj = {
name1 : value1,
name2 : value2,
// ...
}

存取物件內容

移個物件可以存放不同內容,而這些內容被存放在不同的地方,而透過物件的屬性名稱我們可以找到這個物件的各個內容

1
2
3
4
5
6
let obj = {
name : 'tom'
}
console.log(obj.name)

// tom

存取物件的方式有兩種 :

  1. 使用 . 符號
  2. 使用 [] 符號

一般來說,.符號是用來存取物件屬性 ( property ),[] 符號式來存取物件的鍵值 (key)

1
2
3
4
5
6
7
let obj = {
1 : 'tom'
}
const testValue = obj.1;
console.log(testValue);

// SyntaxError: Unexpected number

以上面的例子,如果用小數點,就不能存取數字開頭的屬性,這時候我們就可以利用 []

1
2
3
4
// ...
const testValue = obj[1];

// tom

我們也可以使用變數的形式

1
2
3
4
5
6
7
8
9
let obj = {
name : 'rex.'
}

const test = 'name';
const Val = obj[test];
console.log(Val);

// rex.

[]搭配字串有機會可以動態存取地取出不同屬性對應的值

1
2
3
4
5
6
7
let a = 'test'
let b = {
[a] : 'Test'
}
console.log(b)

// { test: 'Test' }

建立類別與實例

在 ES6 裡有加入一些方便的類別建立語法

1
2
3
4
class Car{
constructor(){
}
}

我們可以使用 new 去建立一個類別

1
2
3
4
5
6
7
8
9
function C_Test(){
this.test = 'test',
this.home = 'home'
}

let Test = new C_Test();
console.log(Test);

// C_Test { test: 'test', home: 'home' }

new is what?

new 這個關鍵字跟 JavaScript 的發展歷史有關,先不說這個,我們先來看看上面程式碼……

1
let emptyObject = new Object()

上述方式就是透過 new 語法創造出一個空物件,然後用函式 Object() 呼叫,而這個 Object 本身也是個物件 (函式也是物件)。而且 JS 預設提供的都是全域物件……

why?

為甚麼要用 new 關鍵字搭配函式的呼叫來創造物件 ? 如果不使用 new 這個關鍵字又會如何?

恩…..如果說 let Test = new C_Test(); 這邊不使用 new 的話也會收到一個空物件,但是在一般情況下部加上還是會出錯……

new 這個字本身是一個運算子,而一個運算子在被執行後會產生一個新的值,而這個值就是物件。

然後在 new 後面的函式也就是我們一般說的 建構函式(Constructor) 或是 函式建構式(Function Constructor),這個函式的目的只是跟著 new 來創造物件,而不會當成一般函式使用。

我們擷取了上面的範例

1
2
3
4
function C_Test(){
this.test = 'test',
this.home = 'home'
}

物件的內容可以按照自己想怎麼訂就怎麼訂,不過前面必須加上一個 this (如上)

我們可以從上面看到我們在建構函式裡使用到了 this 這個關鍵字,我們把它當成物件來使用,在上面動態地新增了物件屬性……

我們使用 new 時會有這幾件事情 :

  1. 有一個全新的物件被創造出來
  2. 這個新物件帶有 prototype 連結
  3. 在跟 new 搭配一起被呼叫地建構函式內創造一個名為 this 的特殊變數,並把 this 與第一步所創造的物件做連結,所以當我們在建構函式內使用 this 並對這個 this 做新增或修改屬性,其實是在對那個被創造的物件做變更。 this 這個變數在很多地方都能被取用,不過根據不同規則它所代表的值很有可能不同……
  4. 除非該函式提供了自己的替代物件來回傳,不然以 new 呼叫的函式會自回傳這個新建構的物件。

如果沒有加上 new 運算子呢 ?

這邊有一個很明顯的缺點,因為在建構函式時在一般的使用方法下不會回傳任何數值。我們來看看例子

1
2
3
4
let Test = C_Test();
console.log(Test);

// undefined

喔,這邊還有一個小地方要注意,就是一個函式建構子所命名的傳統 — 首字母大寫,以方便開發人員辨識

物件的常用操作

我們也同時整理了常用的幾個操作。

第一,檢查物件是否存在某屬性

一般常見的情況是我們想要拿物件的某個值去做運算,但是在運算之前我們必須要確定這個物件身上是否有對應的屬性。

使用 hasOwnProperty

我們直接來看一段程式碼

1
2
3
4
5
6
7
8
9
10
11
12
const test = {
name : 'rex',
}

let testhasname = test.hasOwnProperty('name');
console.log(testhasname)

let testhasname_false = test.hasOwnProperty('false');
console.log(testhasname_false)

// trye
// false

上面這段程式碼裡面,因為 test 物件裡面存放著 name 這個屬性,所以 test.hasOwnProperty('name'),會得到 true 的結果,反之則 false 。

如果 test 物件可以使用 hasOwnProperty方法,那 hasOwnProperty 也可以算是這個物件的屬性嗎?這個可以說算,也可以說不算。因為在 test 裡確實可以找到這個屬性,知道它是一個函式,但是這個物件卻不是存在於 test 上。

可以想成 test 物件是在某個基礎上被創造的,而 hasOwnProperty 這個函式就源自於這個基礎。有沒有很熟悉~~ 就是繼承啦。也就是說 test 物件本身沒有定義這個方法,但透過這個物件所來自的基礎,這我們在這樣的情況下也能夠讀取到它。

使用 in 運算子

我們也可以使用 in 運算子達到相同目標

1
2
3
4
5
6
7
8
9
const test = {
name : 'rex',
}

console.log('name' in test)
console.log('name1' in test)

// true
// false

使用 in 運算子可能比起前一個方法要簡短許多,不過它與 hasOwnProperty 的最大一個差異是 : 能不能檢查到物件上的那些繼承來的方法hasOwnProperty 只能檢查到物件裡面明確。

其實要用哪一種方法是看自己的需求而定。如果只是想要確認一個物件內的方法能不能被使用,那就可以使用 in ,因為某些時候物件的方法能透過繼承的關係被取得。若想要確定一個意見內是否存在開法者主動定義的屬性,那就限定一點使用 hasOwnProperty

直接存取開物件屬性來判斷

這個方法就很直接了。因為去存取一個不存在的物件就會得到 undefined 的結果。

1
2
3
4
5
6
7
8
const a = {
name : 'rex'
}

const test = a.Nonename === undefined;
console.log(test)

// true

不過這個方法有一個缺點,就是如果 a 裡面其實有這個屬性,只不過它的值被設成 undefined ,那麼用這個方法很可能被誤判,所以通常這樣子的用法只會用在 想要確定物件裡面某個屬性有沒有內容的情況下 ,而在這種情況下我們可以利用強制轉型來做我們想要的判斷。

1
2
3
4
5
6
const a = [
name : 'rex'
}
if (!! a.name){
// do something
}

這裡使用了!!反向運算子,將其值轉換成布林值來看看 a 裡面的 name 屬性是不是有內容可以給後面的邏輯使用。

尋訪物件

我們都用過陣列的 indexOf ,不過在物件裡我們需要 預設物件Object 的幫助

Object.keys

object 物件上有很多相關好用的方法

1
2
3
4
5
6
7
8
const a = {
name : 'rex',
pro : 'property'
}

console.log(Object.keys(a))

// [ 'name', 'pro' ]

這個函式會把物件裡面自有的 (非繼承而來的) 屬性抽出來,放到一個字串陣列內。

那既然知道回傳的是陣列型態,那我們可以使用 forEach,去取得該物件裡面的屬性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const a = {
name : 'rex',
pro : 'property'
}

let ob = Object.keys(a)
ob.forEach((key) => {
if (key === 'name') {
console.log('pass')
}
else{
console.log(key)
}
})

// pass
// pro
Object.values

相對於前面的 Object.keys ,取得的是物件裡面每個屬性的數值內容,並放進陣列回傳。

1
2
3
4
5
6
7
8
9
const a = {
name : 'rex',
pro : 'property'
}

let ob = Object.values(a);
console.log(ob);

// [ 'rex', 'property' ]

其實遇到陣列就有很多東西可以運用……

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function checkNumber(nums){
let i = Object.values(a);
let F = false;

if (i.indexOf(nums) >= 0){
F = true
}
return F;
}

let a = {
1 : 'rex',
2 : 'property'
}

let ans = checkNumber("property")
console.log(ans)

// true

在這裡配了 indexOf 去找 value 值

Object.entries

前兩章的集合體,回傳一個二維陣列,分別代表 物件的每個屬性對應的數值

1
2
3
4
5
6
7
8
9
const a = {
name : 'rex',
pro : 'property'
}

let ob = Object.entries(a);
console.log(ob);

// **[ [ 'name', 'rex' ], [ 'pro', 'property' ] ]**

解構賦值

解構賦值 ( Destructuring ),他是講到物件一定要提到的用法之一。ES6 版本才出現的使用方式,可以讓我們在取出物件某屬性資料時,非常快速的宣告出新的變數。

物件的解構賦值
1
2
3
4
5
6
7
8
9
let user = { 
name : 'rex',
test : 'mid'
}

let testuser = user.name;
// let { name } = user;

// rex

上面的兩個方法的結果都是一樣的。在這邊我們用到了最簡單的解構賦值,它的用法很像是 在創造物件時使用的物件字面值 ,只不過它的使用位置是完全相反的,在等號的左邊宣告變數的位置。

1
2
3
4
5
6
let user = { 
name : 'rex',
test : 'mid'
}

let { name, test } = user

在之前我們要取出物件的多筆資料並依據數件的屬性名稱來宣告變數,就需要一個一個指派。但現在可以用解構賦值來取出物件屬性並宣告,就可以根據物件裡面的多個屬性宣告多組變數。

1
2
3
4
5
6
7
8
9
let user = { 
name : 'rex',
test : 'mid'
}

let { name, test, age } = user
console.log(age)

// undefined

記住,如果要解構賦值的話,該變數一定要存在物件裡面,否則就像是為宣告的變數一樣,得到 undefined的結果。

但是我們可以有預設值

1
2
3
let { name, test, age = 20 } = user
console.log(age)
// 20

如果 JS 看到屬性對應的數值是 undefined ,就會拿我們給定的預設值作為宣告結果。

如果說我們有很多組相似資料呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let usera = {
name : 'rex',
test : 'mid'
}

let userb = {
name : 'cat',
test : 'def'
}

let { name : usernameA } = usera // usernameA 就是別名,中間用 : 隔開
let { name : usernameB } = userb

console.log(usernameA)
console.log(usernameB)

// rex
// cat

我們為了避免變數名稱互相衝突,我們可以為解構賦值的內容宣告一個別名,這樣就可以避免重複情況發生。

陣列的解構賦值

有物件的就有陣列的,雖然都算物件,不過因為存放的方式不一樣,使用的方式自然也不一樣。

1
2
3
4
5
let test = ['rex', 'cat']
let [fitst, second] = test;
console.log(first)

// rex

陣列相對於物件而言,沒有屬性查找相對應的內容,只有索引而已,這時在做解構賦值的時候,變數的對應位置就很重要。

複製物件

複製物件是一個在實際開發中的常見行為,我們常常為了避免一個物件因為錯誤的參考而被依外修改,而需要創造一個與某物件具有完全相同屬性的全新物件。

展開物件

使用 ... (Spread Operator) 展開運算子

1
2
3
4
5
6
7
8
9
10
11
12
13
let a = {
name : 'rex',
test : 'mid'
}

let b = {
height : 500,
...a
}

console.log(b)

// { height: 500, name: 'rex', test: 'mid' }

搭配展開運算子來展開物件時,如果我將一個物件在另一個空的物件裡做展開,這兩個物件的屬性內容可能會一樣,但是這兩個物件已經是個別被宣告的不同資料,這個情況下你就等於是複製了一份具有相同屬性的物件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let a = {
name : 'rex',
test : 'mid'
}

let b = {
...a
}

b.name = 'b'
b.test = 'fin'

console.log(a)
console.log(b)

// { name: 'rex', test: 'mid' }
// { name: 'b', test: 'fin' }

這邊的情況應該是…..

1
2
3
4
// { ... a }
// { name : 'rex', test : 'mid'}
// { name : 'rex', test : 'mid', name : 'b', test : 'fin'} 後面來的會蓋掉前向相同屬性的內容值,所以最後結果應該是
// { name: 'b', test: 'fin' }

當然,也可以用在陣列上

1
2
3
4
5
6
let a = [1, 2, 3]
let b = [4, 5, 6]
let c = [...a, ...b]
console.log(c)

// [1, 2, 3, 4, 5, 6]
Object.assign

使用 JS 預設的 全域物件 Object 上的 Object.assign 方法也可以達到複製物件的目的,這個方法接收2個參數,前是目標物件,後是來源物件,記住,是複製而不是繼承

1
2
3
4
5
6
7
8
9
10
11
12
13
let a = {
name : 'rex',
test : 'mid'
}

let b = {
age : 10,
}

let ans = Object.assign(a, b)
console.log(ans)

// { name: 'rex', test: 'mid', age: 10 }

複製到目標物件上,然後回傳這個目標物件。

其實不局限於只有一個,也可以加上第二個、第三個……

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let a = {
name : 'rex',
test : 'mid'
}

let b = {
age : 10,
}

let c = {
case : 50,
age : 50
}

let ans = Object.assign(a, b, c)
console.log(ans)

// { name: 'rex', test: 'mid', age: 50, case: 50 }

不過要考慮的是撰寫時的順序而被覆蓋的情況。

淺拷貝

在複製物件的時候,不管使用哪個方式,都必須注意一個現象,一般我們在複製物件的時候,通常是為了創造一個與另一個物件具有相同屬性的物件,不過因為 JS 的物件裡面基本上想儲存任何資料型別都可以,所以在一個物件裡面也有可能會是存放另一個物件。

若是在這個狀況下,透過被複製而創造出來的新物件,並不會一併複製原來物件屬性底下存放的物件,而是會透過物件參考的形式,所以複製後,乍看之下是一個全新的物件,不過事實上他們處於一個藕斷絲連的關係,這個特殊的複製現象我們稱為 淺拷貝 ( Shallow Copy ),它的意思有點像 只有物件表層真的被複製而已

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let originalObj = {
valuea: 'a',
innerObj : {
valueb: 'b'
}
}

let newObj = { ...originalObj }
newObj.innerObj.valueb = 'example b'

console.log(originalObj.innerObj.valueb)
console.log(newObj.innerObj.valueb)

console.log(originalObj.innerObj === newObj.innerObj)

// example b
// example b
// true

我們使用了全相等來觀察到了這兩個內層物件是完全相等的,也就表示這兩個看起來不同的物件其實是來自同一記憶體。也就是說原來的 originalObj 透過展開運算子被複製,並且我們去修改 newObj 的內容物件 innerObj 的內容後,會發現 originalObj 裡的 innerObj 也一併被複製了。

深拷貝

相對於淺拷貝,深拷貝就是完全複製整個物件內容了。但是要在 Js 裡面實現深拷貝就相對困難,因為使用一般的複製行為就是淺拷貝,如果說想要自己做出兩個完全隔離的物件的複製,這就需要手動處理了。

1
2
3
4
5
let a = { fir : 'a' }
let b = { ...a }

a.fir = b
let c = { ...b }

這看起來就是沒完沒了的複製迴圈……

如果說要真正達到深拷貝,可以透過另一個常見的資料格式稱為 Json 來將一個物件轉為類似純文字的內容,再轉回 JS 物件,或是購過 JS 在新版本提供的 Object.create

不過我們基本上不會這麼做……

物件的屬性描述器

基本上我們創造物件實不用太多的設定,都是直接指派數值。而在物件的屬性上,其實還有一些附加的資訊,讓我們能控制每個屬性的行為,這些資訊一般我們都不會看見,必須透過 屬性描述器 ( Property Descriptor ) 才有辦法得到或修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
let obj = {
a : 'a',
}

let des = Object.getOwnPropertyDescriptor(obj, 'a')
console.log(des)

// {
// value: 'a',
// writable: true,
// enumerable: true,
// configurable: true
// }

getOwnPropertyDescriptor這個方法接受三個參數,第一個是要新增的屬性目標物件,第二個是屬性名稱,第三個是這個屬性的描述器設定

  • value : 屬性的值,就是一般我們指派給物件屬性的內容
  • writable : 定義這個屬性可以被變更,如果維 true 就代表這個屬性可以透過如 obj.a = 'new value' 被更新
  • enumerable : 定義屬性再透過物件上尋訪的方法 ( 如 : Object、 Keys)。
  • configurable : 定義這個物件的描述器這定是否能被修改,如 : writableenumerable,以及自身 configurable
  • get : 物件屬性上的 getter 函式,定義取用物件屬性時的行為
  • ser : 物件屬性上的 setter 函式,定義取用物件屬性時的行為

Object.defineProperty

從前一章可以看到如果直接創造物件的話,預設就會跟描述的一樣。但是我們可以做更動~

1
2
3
4
5
6
7
8
9
10
11
12
13
let obj = {};
Object.defineProperty(obj, "a", {
configurable: true,
enumerable: true,
value: function(){
return 100
}(),
writable: true
})

console.log(obj)

// 100

其實一般來說是不需要做更動的……除非你有特殊的需求,接下來我們可以一個一個更動看看情況究竟會如何

writable

物件描述屬性 writable 決定了一個物件屬性,決定物件屬性是否能變更

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let obj = {};
Object.defineProperty(obj, "a", {
configurable: true,
enumerable: true,
value: function(){
return 100
}(),
writable: false
})

obj.a = 50
console.log(obj)

// 100

如果將其改成 false 就無法進行變更。

enumerable

這個東西決定了一個屬性能不能在物件的屬性尋訪中被取得,預設是 true。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let obj = {};
Object.defineProperty(obj, "a", {
enumerable: true,
value: "rex"
})

Object.defineProperty(obj, "b", {
enumerable: false,
value: "jam"
})

console.log(obj)
console.log(Object.keys(obj))
console.log(obj.hasOwnProperty('a'))

// { a: 'rex' }
// [ 'a' ]
// true

從以上例子可以看出如果 enumerable 被設為 false ,那在我們透過 Object.keys() 來取用屬性時就無法取得,但是它還算是我們自己定義的屬性,所以我們用 Object.hasOwnProperty() 來檢查會是 true 的結果~

那如果要取得自己的全部屬性……

1
2
3
4
let anyoneinObj = Object.getOwnPropertyNames(obj)
console.log(anyoneinObj)

// [ 'a', 'b' ]

使用 Object.getOwnPropertyNames(obj)就可以達到效果。

Configurable

這個東西他負責一個物件是否能被修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let obj = {};
Object.defineProperty(obj, 'name', {
value: 'rex',
writable: true,
configurable: false
})

obj.name = 'cat'
console.log(obj.name)

Object.defineProperty(obj, 'name', {
configurable: true
})

// cat
// Object.defineProperty(obj, 'name', {
// ^
// TypeError: Cannot redefine property: name

如果被認定為 false 後,在嘗試修改就會出現 TypeError 的狀況。

Get 、 Set

屬性描述器裡的 get 、 set 是屬性描述上的兩個隱藏函式,分別決定了物件屬性被取用時以及被賦值時的行為。如果這兩個值沒有被設定的話,預設值就會是 undefined ,那這個時候就跟一般使用時沒什麼差別,如果有更動……

1
2
3
4
5
6
7
8
9
10
11
12
13
let obj = {};
Object.defineProperty(obj, 'name', {
get() {
return 'This is getting called'
},
set(){
this._name ='this is setting'
}
})

console.log(obj.name)

// This is getting called

get 跟 set 在屬性描述器的設定與前面幾個描述器有一個細微的差異,描述起可以分為兩種 : 若在定義屬性時,描述器上有 GetSet 的設定的話,這個描述器就會被稱做 存取描述器 ( Accessor Descriptor ),反之,則被稱做 資訊描述器 ( Data Descriptor )。而對存取描述器而言,它的 valuewritable 值會被忽略,而會以被設定的 getset 函式內容為主

This

This 這個東西很像 python 裡的 self ,但又差了一點……

我們來看一下在 JS 裡面的 This ,this 是一個特殊的關鍵字,讓我們可以很方便的從執行環境裡取得外部的物件,用另一種說法就是, this 可以讓我們在呼叫函式時,透過不同方式決定它要指向哪一個新物件。 this 會在每個執行環境堆疊裡面出現,不過沒有足夠了解的話,就會出現 this 指向錯誤的物件之類等不如預期的狀況,所以我們在使用前一定要弄清楚 this 在檯面下的運作方式。

注意 : 如何呼叫在哪裡呼叫 這兩件事情都會造成 this 的指向不同。

而this有四種綁定方式,所謂的 this 綁定 ( Binding ) ,或稱 繫結,指的是指向哪一個物件,而 this 大致上有四種綁定形式……

預設的綁定

前面有說過 this 的指向,攸關於一個函式是怎麼被呼叫的。我們來看看一般類型的函式

1
2
3
4
5
6
7
8
9
function test(){
console.log(this.a)
}

let a = 'rex'

test();

// undefined

之前有說過在函式裡的 this 涉及到全域,但感覺並不然,除了全域物件 window ,另一個指向 window 的這個物件的 this 也會跟著產生。

1
2
3
4
5
6
7
8
function test(a){
this.a = a
console.log(this.a)
}

let a = 'rex'

test(a);

所以還是要把 a 指回去給函式裡喔

隱含的綁定

從字面上來看就是不太確定的綁定。當我們呼叫一個函式時,如果不是直接呼叫,而是透過物件裡面的一個屬性,那麼就等於是在給這個函式一個背景資訊, JS 就會知道你呼叫的是該物件裡的函式,於是就會把 this 指向你所用的這個函式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function test(){
console.log(this.msg)
}

let obj = {
msg : 'a message',
test : test,
}

obj.test()
test()

// a message
// undefined

這邊我們先宣告了 test ,然後再把這個函式指派給了物件 obj 裡的 test 屬性,之後再呼叫了 obj 裡面的這個 test ,透過 this 我們也拿到了 obj 裡面的 msg 內容了。

而相對的,當我們在全域時使用 test 函式,所使用的就會跟 預設的this 綁定,但因為全域物件,也就是 window 底下並沒有 msg 這個變數,所以會出現 undefined 的結果。

明確的綁定

在 JS 裡面所有的函式都有 callapply 可以使用,他們用來指定綁住物件的 this 。

他們的第一個參數都是指定 this 所指向的物件,而第二個以後的參數則是要傳入該函式的參數,apply 是以陣列方式決定傳入函式的參數順序, call 則是直接以第二個參數後的數量及順序來決定,不過不管使用哪一個方式都能明確的綁定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function test(){
console.log(this.name)
}

let user = {
name : 'rex'
}

test()
test.call(user)
test.apply(user)

// undefined
// rex
// rex

透過 callapply 就可以透過 this 來抓到 user 裡的屬性值了

硬綁定

硬綁定 ( Hard Binding )其實是明確綁定的一種變化,用來確保某個函式的 this 每次被呼叫的時候都與目標物件綁定,而不會因為利用 call 或 apply 錯誤地修改了一個函式地 this 而發生了預期之外的結果。

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
28
29
30
31
32
33
34
35
function say(){
console.log(this.msg)
}

function test(){
console.log("this is test = ", this);
say.call(user);
}

let user = {
msg : 'I am a message',
}

console.log(test())
console.log(test.call({}));

// this is test = <ref *1> Object [global] {
// global: [Circular *1],
// clearInterval: [Function: clearInterval],
// clearTimeout: [Function: clearTimeout],
// setInterval: [Function: setInterval],
// setTimeout: [Function: setTimeout] {
// [Symbol(nodejs.util.promisify.custom)]: [Function (anonymous)]
// },
// queueMicrotask: [Function: queueMicrotask],
// clearImmediate: [Function: clearImmediate],
// setImmediate: [Function: setImmediate] {
// [Symbol(nodejs.util.promisify.custom)]: [Function (anonymous)]
// }
// }
// I am a message
// undefined
// this is test = {}
// I am a message
// undefined

從註解地這一大串我們可以看到 test 函式達成了硬綁定的目的。可以看到因為真正取用物件屬性的 name 函式 say 外面其實多包了一層 test ,並固定使用 call 來將 say 函式的 this 直接綁定成 user 這個物件,所以即使外層函式 test 再怎麼用 call 來綁定 this ,裡面的主要函式,也就是 say 被呼叫時取用的 this 值,也不會到影響,這就是硬綁定的概念。

我們可以看到上面那個連結的 bind

嘿,關於硬綁定其實不需要自己處理,使用 bind 可以確保這個函式的 this 不會因為任何其他的 call 或 apply 而被修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let obja = {
a : 'testa'
}

function test(){
console.log(this)
}
let bindTest = test.bind(obja)

console.log(bindTest)
console.log(bindTest.call(obja, 'test'))

// [Function: bound test]
// { a: 'testa' }
// undefined

new 與建構函式的綁定

前面有講過建構函式,在這裡面的 this 與前面幾種 this 的行為相比又更特殊。且比較沒有關聯。

一般來說 new 建構子搭配呼叫時,會有以下神奇的事情發生 :

  • 會有一個全新的物件被創建出來
  • 這個新建構的物件帶有 prototype 連結
  • 這個新建構的物件會被設為那個函式呼叫的 this
  • 除非該函式提供了自己的替代物件,不然這個以 new 呼叫的函式呼叫會自動回傳這個新建構的物件。

目前的主流是 class ,幾乎沒有人會直接用 new 了吧….大概 ?

箭頭函式裡的 this

現在我們回過頭來看箭頭函式,與一般函式上的 this 綁定行為不太一樣。當我們使用一般的函式呼叫時, this 的綁定規則都會根據前面講過的幾種綁定方式來進行綁定,不過在箭頭函式哩,沿用官方的說法,它是 沒有 this 綁定的

為甚麼 ? 一個簡單的解釋是,在箭頭函式裡的 this ,與箭頭函式外面的 this 綁定是相同的,也就是說箭頭函式裡的 this 綁定會根據這個箭頭函式的程式碼實際位置,亦依語彙環境而變

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let obj = {
func : () => {
console.log("this is normal function.", this);
},
arrowFunc : () => {
console.log("this is arrow function.", this);
}
}

console.log(obj.func());
console.log(obj.arrowFunc());

// this is normal function. {}
// undefined
// this is arrow function. {}
// undefined

當我們呼叫 obj 物件上的一般函式時,所使用的 this 綁定是透過隱含綁定的方式來達成,不過同樣的事情可能就不會發生在箭頭函式內…..

如果我們執行了上面這段程式碼,你會發現這個箭頭函式內的 this 在呼叫時不會綁定到 obj 物件本身,而是全域物件......,這就是因為箭頭函式沒有 this 綁定的關係造成的。

這個現象很適合用在防止 this 綁定會隱含失去的現象發生。