Try   HackMD

JavaScript 的物件導向與原型繼承

古典和原型繼承

繼承表示一個物件取用另一個物件屬性或方法,只要了解其簡單觀念就好了,許多人在解釋這區塊時常會用各種火車、汽車等等例子來做舉例,但講師認為直接講清楚會比較簡顯易懂。

那古典繼承和原型繼承是什麼呢?古典繼承在 C#、Java 裡都有,而且非常熱門。

而古典繼承裡面有非常多方法可以用

  • friend
  • protected
  • private
  • interface
    但我們必須了解他才能夠知道該如何操作。

原型繼承呢?東西就簡單許多了

  • 彈性 (flexible)
  • 可擴充性 (extensible)
  • 簡單易懂 (east to understand)

古典和原型繼承各自都有他的好壞,所以並沒有一定,所以當有人在講繼承時,就是在講

一個物件取用另一個物件屬性或方法

瞭解原型

前言

JavaScript 用了原型繼承,所以代表有個叫作原型(prototype)的概念。

瞭解原型

首先我們知道物件可以有屬性和方法,然後我們可以使用點運算子取得屬性或方法,JavaScript 中所有物件、函數,都有原型屬性,而這個屬性會參考到另一個屬性通常被稱為 proto。

假設今天我們有一個 obj 的物件,底下有一個叫 prop1,所以我們可以透過 obj.prop1 來取得。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

JavaScript 中所有物件、函數,都有原型屬性。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

而這個屬性會參考到另一個物件,稱之為 proto,而 proto 也可以有屬性,例如 prop2,所以當我們要取用 prop2時就可以這樣寫 obj.prop2。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

而我們使用點運算子去取用 obj 中的 prop2 時,會找不到,所以他會往原型裡面找。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

那原型物件也可以指向到另一個物件。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

每個物件都可以有自己的原型,當這個 proto 裡面有一個 prop3,我們就可以用 obj.prop3 取得。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

而這過程就像一個鏈子,所以又稱為原型鏈 (prototype chain),但是不要把他跟範圍鏈搞混了,雖然很相似,可是範圍鏈是尋找可以取用的變數,但原型鏈是尋找屬性及方法。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

而一般來講這 proto 是隱藏起來的,所以我們才不用這樣撰寫 obj.proto.proto.prop3,只需要 obj.prop3 就好。

但是 JavaScript 中有一個很有趣的狀況,當若有第二個 obj 時,他可以指向同一個原型。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

所以當我們呼叫 obj2.prop2,他一樣會回傳相同位子。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

而以上這些就是原型及原型鏈的概念,只需要想簡單一點只是有一個特別的參考到我們的物件而已,那接下來我們直接來看點範例。

var person = { firstname: 'Default', lastname: 'Default', getFullName: function() { return this.firstname + ' ' + this.lastname; } } var john = { firstname: 'John', lastname: 'Doe', }

這邊我們有兩個物件,接下來我們要將 john 設定成原型,但以下範例千萬不要使用於現實中,這只是為了簡單理解觀念而已。

var person = { firstname: 'Default', lastname: 'Default', getFullName: function() { return this.firstname + ' ' + this.lastname; } } var john = { firstname: 'John', lastname: 'Doe', } // 千萬不要使用這種方式在真正的專案開發上,這只是為了理解原型而已。 john.__proto__ = person; console.log(john.getFullName());

為什麼不是抓到 Default?因為原型鏈的原因導致,所以點運算子會在 john 裡面找到 firstname,所以就會停下來不會再找了,接下來在加一點物件上去。

var person = { firstname: 'Default', lastname: 'Default', getFullName: function() { return this.firstname + ' ' + this.lastname; } } var john = { firstname: 'John', lastname: 'Doe', } // 千萬不要使用這種方式在真正的專案開發上,這只是為了理解原型而已。 john.__proto__ = person; console.log(john.getFullName()); var jane = { firstname: 'jane', } // 千萬不要使用這種方式在真正的專案開發上,這只是為了理解原型而已。 jane.__proto__ = person; console.log(jane.lastname); console.log(jane.getFullName());

所有東西都是物件(或純值)

前言

現在我們知道物件原型,接著我們可以深入瞭解到一件事情 JavaScript 所有東西都是物件或是純值

數值、布林、字串、函數、陣列、物件他們都有原型,除了基本物件(base object)。

所有東西都是物件(或純值)

讓我們從範例來瞭解,接下將會利用這三個東西 物件、函數、陣列來講解為什麼所有東西都有原型。

var a = { }; var b = function() { }; var c = [];

首先讓我們試著在瀏覽器輸入以上範例,然後再輸入 a.proto

我們會得到一個基本物件,這在原型鏈上非常底層,而這基本物件有屬性與方法。

那函數呢?我們試著輸入 b.proto

這就是所有函數的原型,所有我們建立的函數都有這個原型,當然也有相關的屬性與方法(你會看到熟悉的 apply、call、bind)。

接下來是陣列 c.proto

這就是一個原型陣列,我們也來看看它是否也有屬性與方法(這裡你也會看到許多熟悉的字眼)。

所以由上面這三個範例我們可以知道一件事情。

JavaScript 所有物件、所有陣列及所有函數都有原型。

那這邊再講一個好玩的問題,原型的原型是什麼?

是原型,所以我們要記得,原型鏈最底下的東西就是原型物件。

Reflection 與 Extend

前言

接下來將要講解另一個建立物件的函數很有趣很有效的東西,許多資料庫都會使用到,而這稱為 extend。

而通常我們都是用一個叫 Reflection 的東西來做到 extend。

Reflection: 一個物件可以看到自己的東西,然後改變自己的屬性和方法。

讓我們試著從範例來學習。

var person = { firstname: 'Default', lastname: 'Default', getFullName: function() { return this.firstname + ' ' + this.lastname; } } var john = { firstname: 'John', lastname: 'Doe', } // 千萬不要使用這種方式在真正的專案開發上,這只是為了理解原型而已。 john.__proto__ = person; for(var prop in john) { console.log(prop + ':' + john[prop]); }

這時候會看到一個很神奇的東西,getFullName(),這是因為 for in 會到外面取用屬性和方法的 不只在物件本身,還會到原型上找

那如果今天我們要

var person = { firstname: 'Default', lastname: 'Default', getFullName: function() { return this.firstname + ' ' + this.lastname; } } var john = { firstname: 'John', lastname: 'Doe', } // 千萬不要使用這種方式在真正的專案開發上,這只是為了理解原型而已。 john.__proto__ = person; for(var prop in john) { if(john.hasOwnProperty(prop)) { // hasOwnProperty 是原型物件的東西,我們可以透過這種方式來確定是否物件有這個屬性。 console.log(prop + ':' + john[prop]); } }

這樣 getFullName() 就沒有出現了,因為 john 的物件屬性並沒有 getFullName()。

而這個概念可以用來幫助我們做一些事情【補足原型繼承】,所以這邊試試看引入 underscore.js。

var newscript = document.createElement('script'); newscript.setAttribute('type','text/javascript'); newscript.setAttribute('src','https://underscorejs.org/underscore-min.js'); var john = { firstname: 'John', lastname: 'Doe', } var jane = { address: '111 Main St.', getFormFullName: function() { return this.lastname + ', ' + this.firstname; } } var jim = { getFirstName: function() { return firstname; } } _.extend(john, jane, jim); // 這樣做將會把物件結合起來 console.log(john);

這時候我們可以看到 john 有 jane 的物件地址、jim 物件的 getFirstName 函數,還有 jane 物件的 getFormalFullName()。

那這是怎麼做到的?首先先開啟 underscore.js,開啟之前記得千萬不要害怕開源的專案,這是一個非常好學習教材,我們試著找 extend 看看。

首先我們可以看到他建立了屬性或方法,而這個叫 createAssigner 的函數我們試著找看看。

我們可以發現 createAssigner 是一個函數,其中他接受了 keys 還有 defaults,然後回傳函數本身,我們可以看到這是一個閉包。

首先它會先取得傳入參數的長度

再來是包裝物件,他將物件包裝起來(避免參考問題) (((我猜)))

接下來就是當物件長度若小於兩筆,或是物件為空,就返回物件

接下來他會用 for 將所有物件跑一遍,因為陣列第一筆是 john ,所以起始會直接跳過 0 從 1 開始

所以因為經過 underscore.js 的處理, jim 就可以使用 getFormFullName()了。

var newscript = document.createElement('script'); newscript.setAttribute('type','text/javascript'); newscript.setAttribute('src','https://underscorejs.org/underscore-min.js'); var john = { firstname: 'John', lastname: 'Doe', } var jane = { address: '111 Main St.', getFormFullName: function() { return this.lastname + ', ' + this.firstname; } } var jim = { getFirstName: function() { return firstname; } } _.extend(john, jane, jim); // 這樣做將會把物件結合起來 console.log(john); john.getFormFullName();

tags: WierdJavascript