JavaScript Object
物件導向
- 把情境(問題)描述為物件
- 人 & 香蕉
- 描述物件的屬性 & 方法
- 人 - 屬性
- 名子, 年齡, 有的物品
- 人 - 方法
- 打招呼, 走路, 接收物品
- 香蕉 - 屬性
- 名字, 價格, 味道
- 香蕉 - 方法
- 剝⽪, 攻擊人
- 人 - 屬性
- 操作讓物件彼此互動
- A 跟 B 是⼈,A 有⼀根香蕉
- A.打招呼( )
- A.給予( B , 香蕉)
- B -> 香蕉.剝⽪( )
- B.吃( 香蕉 )
- 「類別」可想像成是建構某特定物體的藍圖或模具,而「實體」就是按照這藍圖或模具製造出來的成品,並使用「建構子」來建立和初始化實體
- 「繼承」定義一個類別,其特性繼承於另外一個類別,稱它們為「父類別」與「子類別」,而子類別「繼承」了父類別的特性
- 「多型」是指子類別除了擁有自己的方法外,這個方法還能覆寫來特化父類別的同名方法,以賦予其更特殊的行為
new 關鍵字發生哪些事?
- 建立一個新的物件
- 將物件的
.__proto__
指向prototype
,形成原型鏈 - 將建構子的 this 指向 new 出來的新物件
- 回傳這個物件
新增 / 修改屬性
有什麼特性
- get & set 方法 - 模擬 Private 成員
- 使用物件字面值的方式定義屬性
- 透過這樣方式產生的物件,所有的屬性都是「公開成員」可隨意變動
- get 屬性讀取,不允許修改
1 | function Person( name, age, gender ){ |
- set 修改屬性更新
1 | this.setName = function(name){ |
屬性描述器 (Property descriptor)
是什麼
- 自己設定的屬性 稱為 屬性的特徵,而設定這些屬性特徵的函式稱為 屬性描述器
- 就算沒有屬性描述器,我們依然可以撰寫 JavaScript,但使用 屬性描述器 可以讓程式更為強健
有什麼特性
- 改善過去 constructor 函式與 ES6 Class 語法仍然是「一對一」關係的語法糖,只能宣告成員方法,無法宣告成員屬性
- 若要用 class 來模擬 private 透過 Object.defineProperty 加上 get、set 來處理
- 會直接對一個物件定義屬性或是修改現有的屬性。執行後會回傳定義完的物件
- 用來檢視屬性的特徵
- 可否寫入(writable)
- 可否配置(configurable)
- 可否列舉(enumerable)
- Object.defineProperty(obj, prop, descriptor)
- obj
- 要定義屬性的物件
- prop
- 要被定義或修改的屬性名字
- descriptor
- 要定義或修改物件敘述內容
- obj
- Object.getOwnPropertyDescriptor()
- 檢查物件屬性描述器的狀態
- 屬性描述器(Property descriptor)要透過 ES5 所提供的 Object.defineProperty() 來使用
- value: 屬性的值(唯一要設定的)
- writable: 定義屬性是否可以改變,如果是 false 那就是唯讀屬性(預設:false)
- enumerable: 定義物件內的屬性是否可以透過 for-in 語法來迭代(預設:false)
- configurable: 定義屬性是否可以被刪除、或修改屬性內的 writable、enumerable 及 configurable 設定(預設:false)
- get: 物件屬性的 getter function 回傳有效值 (預設:undefined)
- set: 物件屬性的 setter function 決定如何處理數據 (預設:undefined)
- 定義了 get 與 set 方法,表示要自行控制屬性的存取,那麼就不能再去定義 value 或 writable 的屬性描述
1 | var person = {} |
constructor、prototype、proto和原型鏈
Function, 對象(object)
- 對象由函數創建,函數都是 Function 實例對象
- Function 函數是 Person 函數的構造函數
- Function 函數同時是自己的構造函數
- Function 函數同樣是 Object 對象的構造函數
1 | function Person(){} |
建構式 (Constructor)
有什麼特性
- 有 function 就有 constructor
- 只能使用 function,不能使用箭頭函式
流程
- 產生一個新物件的 constructor 屬性設為 Person ,這個 Function 物件建立了一個 Person 建構式 (constructor) ,透過 new 關鍵字來建立各種實例物件
- 這個物件繼承 Person.prototype (Function 物件)
- person1 與 person2 是 Person 的實例對象,他們的 constructor 指指向創建它們的 Person 函數
- Person 是函數,同時也是 Function 的實例對象,它的 constructor 指向創建它的 Function 函數
- Function 函數,它是 JS 的內建對象,它的構造函數是它自己,所以內部 constructor 指向自己
1 | function Person(name,age){ |
prototype (原型對象)
- JS 並沒有對所建立的函式區分建構函式與一般的函式,所以只要是函式,就一定會有 prototype 屬性(除了語言內建的函式不會有這個屬性)
- prototype 存檔點一樣裡面放著許多屬性&方法(共用)
- function 自帶 prototype 存檔點
- 實例物件沒有 prototype(只會繼承上一個的 prototype 方法),因為實例物件沒有 function
- constructor 生成的實例對象,有一個缺點就是無法共享屬性和方法(佔記憶體空間)
- 因為沒有 class,所以它的繼承方法是透過 「原型」(prototype) 來進行實作
1 | // 設定相同方法 但不等於一樣並且佔據記憶體空間 |
- 透過「原型」繼承可以讓本來沒有某個屬性的物件去存取其他物件的屬性(可共享)
- 所有實例物件需要共享的屬性和方法,都放在 prototype 裡面
- 那些不需要共享的屬性和方法,就放在 constructor 裡面
1 | // 設定共用方法在 prototype 裡面 |
- new Person( ) 出來的多個實例中如果都有 constructor 屬性,並且都指向創建自己的構造函數,所以它們都各自佔據記憶體空間
- constructor 可以被當成一個共享屬性存放在 prototype 中,作用也依然是指向自己的 constructor
- 也就是默認 constructor 是被當做共享屬性放在它們的原型對像 prototype 中
- 如果是共享屬性 constructor,那將兩個實例其中一個屬性改了,為什麼第二個實例沒同步?
- 因為 person1.constructor = Function 改的並不是原型對像上的共享屬性 constructor,而是給實例 person1 加了一個 constructor 屬性
- 可隨意更改新增實例的 constructor 屬性,但無法通過一個 實例.constructor 找回創建自己的構造函數(之間沒有箭頭鏈接)
1 | function Person() {} |
_proto_
- 讓實例物件找到自己的原形對象,每個物件都有一個 _proto_ 內部屬性,幫助物件間指向它繼承而來的原型 prototype 物件
- _proto_ 這個內部屬性,它是一個存取器(accessor)屬性,意思是用 getter 和 setter 函式合成出來的屬性
- 用 new 的話必須要有 constructor ,所以 constructor 內的屬性會共享給實例
1 | // 創建自己的構造函數 |
- 實例對象._proto_.constructor = 創建自己的 constructor
- 實例對象._proto_ = 創建自己的 prototype(原型對象共用的東西都在裡面)
- 實例對象.prototype._proto_.constructor = 原型對象
- Object 不會像 Function 一樣指向自己的 protoype ,如果指向那就會進入死回圈
1 | console.log(person1.constructor) // ƒ Person() {} |
原型鏈 (prototype chain)
- 由 _proto_ 指向連接起來的結構,稱之為原型鏈(prototype chain),也就是原型繼承的整個連接結構(紅色箭頭)
- person -> function -> object -> nul
1 | console.log(Person.constructor) // ƒ Function() { [native code] } |
- prototype 的 constructor 很容易被更改
- 所有所謂繼承下來的屬性全都是共享屬性
1 | function GrandFather() { |
- 重新指向回自己的 prototype.constructor
1 | Son.prototype.constructor = Son |
名詞整理
含義 | 作用 | |
---|---|---|
constructor | 建立實例對象的構造函數 | 容易被更改、放在 prototype 中當共享屬性 |
prototype | 對象的原型對象 | 存放共享方法 / 屬性、節省記憶體、只要是 function 都有、實例對象沒有這屬性 |
_proto_ | 指向自己的原型對象、構成原型鍊、每個對象都有一個 _proto_ |
原型繼承(Prototype-based inheritance)
基礎
1 | function Energy(name,age){ |
繼承
1 | function Energy(name,age){ |
不指向回自己
- 在 prototype.constructor 底下建立實例,constructor 會指向繼承的 prototype,而原型鍊就不是在 Batman 下
- micky2 -> Energy -> Energy -> Object -> null
1 | let micky2 = new Batman() |
指向回自己
- 後在 prototype.constructor 底下建立實例,constructor 一樣指向繼承的 prototype,而原型鍊會在 Batman 下
- micky -> Batman -> Energy -> Object -> null
1 | // 指向回自己 |
類別的繼承(Classical inheritance) - class 語法糖前身
有什麼特性
- Object.create 沒有 constructor ,需要重新定義
- 模仿了傳統物件導向語言的類別方法,而達到繼承的功能
判斷
- 判斷某個物件是否存在於另一個物件
- 傳入對象將作為新建對象的原型
Object.create(prototype, descriptors)
- 有什麼特性
- 定義一個物件當作原型物件
- 原型物件建立另一個新的物件(過程可以加入其他的屬性)
- 不會執行建構函式,繼承 prototype 的方法&屬性
- 參數
- prototype
- 必需,要用作原型的對象,可以為 null(停止 prototype chain)
- descriptors
- 屬性描述器
- 可選,數據屬性包含 value,writable,enumerable,configurable 特性,未指定最後三個特性,則默認為 false
- 檢索或設置該值, 訪問器屬性包含 set 和/或 get 指定原型且可選擇性地指定屬性
- 屬性描述器
- prototype
isPrototypeOf
- prototype.isPrototypeOf(object)
- 判斷某個物件是否存在於另一個物件的原型鏈結中
instanceof
- object instanceof constructor
- 物件 instanceof 函式,回傳 boloon
- 檢查物件是否為指定的建構子所建立的實體
- instanceof 測試實例和原型鏈中出現過的 constructor function,如果存在就會返回 true
Object.keys
- 回傳一個 array,陣列中的各元素為直屬於 obj ,對應可列舉屬性名的字串
組合繼承流程
- 建立子類別,增加子類別的屬性/方法
- 選擇要繼承的父類別 prototype(方法自動繼承)
- 將子類別建立好在掛載到要的父類別 prototype 上
1 | // 父類別 |
兩種新增屬性的方式
- 用
call(thisAg,arg1,argN…)
強制指定參數帶入父類別屬性 定義新的建構子(SuperEnergy)
1 | function SuperEnergy(type,name,age,status,level){ |
- 直接新增子類別自己的屬性
1 | function SuperEnergy(name,type,status,level,age){ |
使用共同方法
- 將 SuperEnergy.prototype 建立在 Object.create(Energy) 上無法使用共同方法
- 所以要建立在 Object.create(Energy.prototype) 上
1 | // 掛載到父類別 prototype 上 |
原型鍊
- hulk -> SuperEnergy -> Energy -> Object -> null
1 | console.log(hulk.constructor) // ƒ SuperEnergy(name,type,status,level) |
物件繼承
1 | let Alien = { |
建立物件的語法
- 物件字面定義,相等於 new Object()
- const newObject = { }
- 使用 Object.create 方法
- const newObject = Object.create( proto )
- const newObject = Object.create( proto.prototype )
- ES6 類別定義,或是建構函式
- const newObject = new ConstructorFunc ( )
- const newObject = new ClassName ( )
類別宣告(class declaration)
- class 並非如其他物件導向語言般在宣告時期靜態的複製定義,而只是物件間的連結
- 執行時期變更父類別的方法或屬性,子類別與其實體都會受到影響
- class 語法無法宣告屬性,只能宣告方法,因此若想宣告屬性以追蹤共用狀態,就只能回歸到 prototype
- 為了優化效能,super 是在宣告時期靜態綁定的,在某些狀況下,綁定會失敗,因此必須手動綁定
- class 語法只能指定方法,不能設定屬性,這避免開發者誤將屬性(狀態或資料)放在類別中造成的共用問題
class
- 相當於建立共享方法,宣告自己的屬性
1 | class Energy{ |
類別繼承 - extends
- 把子類別掛載到父類別
- 相當於設定 prototype(_proto_)
- 原型鍊
- Energy -> Function -> Object -> null
- 呼叫父類別 - super
- 在子類別中,呼叫父類別的方法或屬性來使用
- 子類必須在 constructor 方法中調用 super 方法,否則新建實例時會報錯
- 這是因為子類自己的 this 對象,必須先通過父類的構造函數完成塑造,得到與父類同樣的實例屬性和方法,然後再對其進行加工,加上子類自己的實例屬性和方法,如果不調用 super 方法,子類就得不到 this 對象
- 靜態綁定 - 也就是父類別的方法或屬性不會隨不同子類別變化
1 | class Energy{ |
建立繼承實例
- 原型鍊
- SuperEnergy -> Energy -> Object -> null
1 | let hulk = new SuperEnergy('hulk',45,'crazy') |