xia的小窩

一起來coding和碼字吧

0%

js-物件與原型

物件導向是一種程式碼的撰寫風格,也是一種解決複雜問題的方法 ( 或稱為一種設計模式 )

物件導向的核心思想

在物件導向語言裡,分別有著 類別 ( class ) 與 物件( object ) 兩種概念。

類別 → 建築設計圖

物件 → 建築物

物件導向的繼承關係

在物件導向的繼承是指類別可以以另一個類別為基礎,在網上進行擴充、修改,這樣一來就可以以低成本的方式創造新的類別。

類別之間的繼承

我們必須要先了解傳統繼承的概念。在繼承的行為裡找出一個最主要的特點,透過被繼承的 後代類別 ( Child Class ) ,所產稱的物件,一開始就應該要直接具有 前代類別 ( Parent Class ) 的屬性跟方法。

我們假設 :

  • 我們有一個 Car 類別 跟 Breed 類別
  • 在 Car 類別的物件上有一個 getfast 方法
  • 在 Breed 類別的物件上有一個 getcar 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Car(action, height, race) {
this.action = action;
this.height = height;
this.race = race;
}

function Breed(firstname, lastname){
this.firstname = firstname;
this.lastname = lastname;
}

Car.prototype.getfast = function(){
return this.race
}

Breed.prototype.getcar = function(){
return this.firstname +this.lastname;
}

現在我們有了 Car 跟 Breed 兩種類別,不過這兩種類別是互不相干的分離狀態。在正式開始之前我們需要考慮…

  1. 類別之間原型物件的繼承 : 也就是要繼承的類別上的方法必須與被繼承的類別能夠共享。
  2. 類別之間與建構函式內容的繼承 : 只要繼承類別上的紀購函式的內容,或是定義的屬性,要與被繼承的類別能夠共享。

類別之間原型物件的繼承

因為透過 new 運算子生成物箭的時候,這兩個建構函式上都會有一個 prototype 物件,一般情況下他們各自為政,但是在處理繼承時我們必須同時考慮兩者之間的連結。

還記得原型鍊嗎? __proto__屬性可以往上一個物件尋找,至於原型物件之間要做到繼承就代表了 :

透過 後代類別 產生的物件,若 JavaScript 在這個物件內還是原型物件上都找不到這個屬性,會轉而往 前代類別 的原型物件尋找。

所以很顯然的,我們必須修改物件上的 __proto__ 連結,但前面也有提過一般不推薦使用這個屬性。在實際開發上也不推薦,因為會破壞物件的預設行為,所以我們必須用曖昧一點的方式……

1
Breed.prototype = Object.create(Car.prototype)

之前提過 JavaScript 的內箭方法 Object.create 修改了繼承物件 User 的 prototype 。

我們使用了 Object.create 來創造了一個全新的物件,而且把他的第一個參數傳入的物件當錯是這個新物件的原型參考。

之後我們可以發現 Breed 的原型物件,被我們修改成一個新的空物件,而這個物件的原型,正是指向 Car ,透過這樣的方式,我們就把繼承之間的原型鍊串起來了

在 JavaScript 裡面,一個函式被創造出來的時候,JavaScript 引擎會新增一個 prototype 屬性到這個函式上面,而若這個函式是一個建構函式,這個 prototype 的內容就正好會透過它所創造出來的物件原型做連結。

而在 prototype 屬性上,其實還有一個預設只回該函式的 constructor 屬性,這個 constructor 正好連結該建構函式本身,只是這個連結是可以被修改的,就像剛剛的 Object.create 來修改 Breed 的 prototype 連結之後,Breed 原來的 prototype 屬性裡面的 constructor 已經不見了,取而代之的只有一個空物件。

類別之間與建構函式內容的繼承

簡單來說就是 讓前代類別的內容出現在透過後代類別的建構函式所產生的物件上 。

1
2
3
4
5
function Breed(firstname, lastname, race, height) {
this.firstname = firstname;
this.lastname = lastname;
Car.call(this, race, height);
}

這邊我們呼叫了 Car 函式並且將 Breed 內的 this 透過 call 的方式綁定到 Car 上面,並當 Breed 透過 new 被呼叫的時候, JavaScript 會將 Breed 的 this 綁定到新生成的物件上,同時也會透過 this 來將 Car 的建構函式內定義的屬性新增到這個物件上。

這樣一來,前代類別的屬性設置就能夠和後代共用,而前面兩行定義的 firstname 跟 lastname ,也正好是 Breed 專屬,而 Car 也不會有這兩個資料屬性。

只有在透過 Car 建立物件時,才會讓這兩個類別產生連結。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Car(action, height, race) {
this.action = action;
this.height = height;
this.race = race;
}

function Breed(firstname, lastname, race, height) {
this.firstname = firstname;
this.lastname = lastname;
Car.call(this, race, height);
}

Breed.prototype = Object.create(Car.prototype)

Car.prototype.getfast = function(){
return this.race
}

Breed.prototype.getcar = function(){
return this.firstname +this.lastname;
}

class 語法糖

在 ES6 之後出現了 class 這個語法,讓我們以更接近物件導向的方式去撰寫 JavaScript

class 的基本用法

使用 class 宣告類別的寫法要使用比較多一點語法,但與建構函式不會相差太多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 建構函式
function User(name){
this.name = name;
}
let user1 = new User(name);
User.prototype.getname = function(){
return this.name;
}

// Class 宣告宣告
class User{
constructor(name){
this.name = name;
}
getname(){
return this.name;
}
}

我們在這裡使用了 class 宣告後,原本的建構函式內容還是一樣,只是被移動到了 constructor 函式裡面而已。

class 宣告式防呆機制

前面有提過,因為使用 new 運算子來搭配函式來創造實體的時候,基本上也算是一種函式呼叫,而且就算沒有加上 new 運算子,函式呼叫也還是有效。

1
2
3
4
5
6
7
8
9
10
11
12
class User{
constructor(name){
this.name = name;
}
getname(){
return this.name;
}
}

console.log(User())

// TypeError: Class constructor User cannot be invoked without 'new'

使用 class 來宣告的時候,則使用 new 呼叫的時候,才會有效。

1
2
3
4
let one = new User();
console.log(one)

// User { name: undefined }

透過 class 宣告來達成類別繼承

這時候需要用到 extends 關鍵字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class User{
constructor(name){
this.name = name;
}
getname(){
return this.name;
}
}

class player extends User{
constructor(name, password){
super(name)
this.password = password;
}
}

extends 後面接著 User 來表示這個類別繼承自 User ,另一個就是 player 的子類別裡的 constructor 函式內,呼叫了一個 super …..

這個 constructor 函式內的 super 函式代表了被 extends 的 User 建構函式,所以只要呼叫 super 就可以了。

透過 static 定義靜態方法

靜態方法是物件導向裡面的概念,它是一種只能由類別本身所取得的方法。

1
2
3
4
5
6
7
8
9
10
11
class User{
constructor(name){
this.name = name;
}
static getname(){
return 'rex'
}
}
console.log(User.getname())

// rex

class 建構子內的 super

裡面的 super 代表了前代類別,所以在後代類別建構子內一定要呼叫 super 才行。

如果想要有前代的函式,則可以直接加上 super 取用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class User{
constructor(name){
this.name = name;
}
getname(){
return this.name;
}
}

class player extends User{
constructor(name, password){
super(name)
this.password = password;
}
getname() {
return super.getname();
}
}

原型

這邊就從概念開始說起吧

原型的概念

每個 JavaScript 物件上都具有一個無法直接觀察到的內部屬性,這個 屬性就代表著該物件與另外一個物件的連結 ,也代表了物件的原型,在文件裡以 [[Prototype]] 來表示。

JavaScript 的物件在存取屬性時會有一個預設行為,就是當在一個物件上找不到某個屬性時,JavaScript就會嘗試 透過這個隱藏的 [[Prototype]] 屬性來看看這個物件連結到哪個原型物件

1
2
3
4
5
6
7
8
9
let a = {
name : 'obja'
}

let b = Object.create(a)

console.log(Object.getPrototypeOf(b))

// { name: 'obja' }

我們先來看看 Object.create , Object.create 主要是用來創造新的物件,跟 Object.assign 一樣,不過有一點差別是Object.create 不像 Object.assign 那樣可以接收並合併多個物件,在執行完這個方法後通常只會收到一個空的新物件,並且會以傳入的物件作為新產生物件的原型。

現在回來看上面這段程式碼,在裡面我們用了 a 作為基礎創造出另一個新物件 b ,在這個情況下 a 應該就會是 b 的原型。接下來我們就透過一個 JavaScript 在 ES5 之後提供的getPrototypeOf方法來取得物件的原型。

1
2
3
4
5
console.log(b)
console.log(b.name)

// {}
// obja

剛剛我們把 b 創造出來後,它就是一個空物件,現在我們使用 a 裡面的 name 屬性,就可以拿到所需要的數值。

現在我們可以確定 :

  • 原型的關係是一個物件與另外一個物件的參考。
  • 當 JavaScript 在一個物件內找不到某個屬性時,就會往原型物件嘗試去查找同樣的屬性。

建構函式的 prototype 屬性

前面在寫 this 綁定的時候我們講過,可以用 new 運算子搭配建構函式來創葬新物件,JavaScript 上的所有函式,包含建構函式上其實都會有一個 prototype 屬性,這個 prototype 屬性主要使用建構函式創造物件時,用來跟它所創造的物件的原型做連結。

也就是說,我們前面提到的每個物件上都會有一個內部隱藏的屬性 [[prototype]],當一個物件透過建構函式的方式產生時,它這上面的[[prototype]]屬性就會被跟建構函式上的 prototype 屬性連結在一起。

1
2
3
4
5
6
7
8
9
10
11
12
function test(user) {
this.user = user
}

const a = new test('rex')

let Oa = Object.getPrototypeOf(a)
let ma = test.prototype

console.log(Oa === ma)

// true

在這裡我們能夠知道 Object.getPrototypeOf 跟 建構函式上的 prototype 屬性是一樣的東西。

物件上的 __proto__屬性

如果有必要取得物件原型,透過 Object.getPrototype方法還是比較推薦的。

原型繼承

我們看了上面的說明,我們可以知道 透過 test 建構函式被創造出來的物件,都能透過原型來與建構函式上的 prototype 屬性產生連結。

假設我們有好幾個透過 test 建構函式被創造出來的物件 ( 使用 new ),我們知道 這些物件上的原型,都與 test 建構函式上的 prototype 屬性被連結在一起。既然如此,看起來我們可以透過改變這個 prototype 屬性的內容,而這些在裡面的內容,就可以用來讓這些被創造出來的物件共享。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function test(user) {
this.user = user
}

test.prototype.Cat = function(cat){
this.cat = cat
}

const user1 = new test('rex')
user1.Cat(80)

const user2 = new test('Dog')
user2.Cat(100)

console.log(user1)
console.log(user2)

// test { user: 'rex', cat: 80 }
// test { user: 'Dog', cat: 100 }

我們在建構函式 test 上的 prototype 屬性添加了一個可以修改 Cat 的函式,這樣一來所有之後透過這個建構函式創造出來的物件都能使用這個函式。

像這樣的建構函式能夠透過原型來讓所有能透過它創造的物件來想某個內容或方法的關係,在 JavaScript 內就是既成的關係,不過因為這種繼承與其他物件導向語言的繼承關係會有些不一樣,在一般的物件導向語言內,若提到繼承,通常只的是類別與類別之間的繼承,而在這裡我們見到的繼承,事實上是屬於類別與物件之間,因此也有人把這種繼承稱做原型式繼承 ( Prototyoe Inheritance )

原型鍊

原型鍊只是一種延伸 : 當 JavaScript 在一個物件內找不到某個屬性時,就會往原型物件嘗試去查找某個同樣的屬性。

如果我們以前一章的例子來看,會透過 __proto__ 屬性,去往上查找對應的原型屬性,而這個原型屬性因為本身也是物件,只要是物件就可能也會有自己的原型,以此類推,一直往上查找值到對應的屬性,或是找到翠號發現找不到……。而這樣一連串由原型組成的查找關係形成了一連串的鍊結,因次被稱做原型鍊。

這邊我們假設這是一個任意連結的直向箭頭。

  1. { name : ‘rex’ }
  2. __proto__
  3. __proto__
  4. null
  5. Cat

我們再舉一個例子

1
2
3
4
5
let arr = [];

console.log(arr.__proto__ === Array.prototype);

// true

全域物件 Array 本身就是陣列的建構函式。如果我們繼續往上推呢?

1
2
3
4
5
console.log(arr.__proto__.__proto__ === Object.prototype);
console.log(arr.__proto__.__proto__.__proto__)

// true
// null

如果找到 null 就代表已經查到底了