JavaScript 物件與原型鍊
JavaScript 是一門 “物件導向” 的程式語言,但物件彼此是透過 “原型” 來繼承屬性,在介紹原型鍊之前,會先透過 “串聯” Cascade 與物件來了解背後的觀念
Cascade 串聯
在 javascript 的函式 function 中,如果沒有設定 return 的值,那麼預設就會回傳 undefined,而 function 中的 this 會指向 “呼叫這個函式的物件” ,如果將 return 的值設為 this,就可以達到串聯 Cascade 的方式,也是 jQuery 可以串聯許多方法背後的觀念
一般寫法
以下為一個運算的範例
1 | var count = function (num){ |
接著透過 new 來新增物件,要注意的是,如果沒有 new,該新增變數的值就會是函式 count 回傳的值,也就是預設的 undefined
1 | var a = new count(100); |
建立物件後,就可以呼叫物件內的函式,又稱為方法 Method
1 | a.add(100); |
Cascade 的方式
一般的寫法如上,在呼叫物件中的多個方法 Method 時,需要個別呼叫 ; 如果將原物件中的方法都加上 return this,回傳 this
1 | var count = function (num){ |
這時候就會發現可以將多個方法 Method 在一行程式碼一起呼叫
1 | var a = new count(100); |
拆開來看的話,a.add(100) 在運算完後會 return this,其中 this 就是屬性 num 變成 200 的 a 物件,而不是預設的 undefined,所以就可以接著呼叫下一個方法 Method
這就是 jQuery 常見的方法鍊
1 | $('div').addClass('is-active').removeClass('is-hide'); |
物件 Object
型別 Type
在 javascript 中,主要區分為兩大型別,基本型別 Primitives 與 物件型別 Object,除了基本型別外,都可以歸類為物件型別
基本型別
- 字串
string - 數字
number - 布林
boolean - 空值
null - 未定義
undefined - ES6 新增的
stymbol
自訂物件
在 javascript 中,有兩種方式可以自訂物件
使用
new關鍵字來建立1
2var person = new Object();
preson.name = 'Jerry';使用大括號
{ }直接建立新物件1
2
3var person = {
name: "Jerry"
}
建構式 Constructor
javascript 不像其他物件導向的語言具有 class 語法,但可以透過建構式來建立各種對應的物件,要注意的是建構式是使用 “函式” function 來建立物件樣板,不能使用 ES6 的 “箭頭函式”
function中的this代表物件的屬性可以透過參數來傳入數值
使用關鍵字
new來建立新物件1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function Person( name, age, gender ){
this.name = name;
this.age = age;
this.gender = gender;
this.greeting = function (){
console.log('Hello!' + this.name);
}
};
var Jerry = new Person( 'Jerry', 24, 'male' );
Jerry.greeting(); // 'Hello! Jerry'
var Tom = new Person('Tom', 18, 'female');
Tom.greeting(); // 'Hello! Tom'將建構式拆開來看,其實是先建立一個空物件
{ },再透過call方法將this指向這個物件,並傳入參數1
2var Jerry = {};
Person.call(Jerry, 'Jerry', 24, 'male');透過建構式建立的物件,還是可以透過
.來存取屬性1
2
3console.log(Jerry.age); // 24
Jerry.age = 18;
console.log(Jerry.age); // 18
屬性描述器 Property descriptor
從 ES5 開始,我們可以透過新的物件模型來靠至物件屬性的存取、刪除、列舉等。這些特殊的屬性就稱為 “屬性描述器” Property descriptor
屬性描述器分為六種 :
value: 屬性的值writable: 定義屬性是否可以改變,如果是false就是唯讀屬性enumerable: 定義屬性是否可以透過for-in語法來迭代configurable: 定義屬性是否可以被刪除、或修改屬性的writable、enumerable和configurable設定get: 屬性的getter function,預設為undefinedset: 屬性的setter function,預設為undefined
這些 “屬性描述器” 都必須透過 ES5的 Object.defineProperty 來處理
定義屬性 Object.defineProperty 與 查詢屬性描述 Object.getOwnPropertyDescriptor
Object.defineProperty(object, property, descriptor): 用來定義物件屬性的描述1
2
3
4
5
6
7var Person = {};
Object.defineProperty(Person, 'name', {value: 'Jerry'});
// resault
var Person = {
name: 'Jerry'
}當然也可以一次定義多個屬性描述
1
2
3
4
5
6Object.definedProperty(person, 'name', {
value: 'Jerry',
writable: true,
enumerable: true,
configurable: true
});Object.getOwnPropertyDescriptor(Object, Property): 用來檢查物件屬性描述器的狀態1
2Object.getOwnPropertyDescriptor(Person, 'name');
// {value: 'Jerry', writable: false, enumerable: false, configuarable: false}
要注意的是,使用兩種物件屬性的定義方式在 writable、enumerable 和 configurable 這三種屬性描述器上會有不同的預設值,使用 Object.definedProperty 定義的物件屬性的預設值會是 false,直接定義物件的預設值則會是 true
1 | var person = {}; |
屬性描述器 get 與 set
我們可以透過 屬性描述器 get 與 set 來追蹤物件屬性的更新,以下範例透過屬性 _name_ 來作為 name 屬性的封裝,物件屬性更新時會執行 set,讀取物件屬性則會執行 get,且在自定義 get 和 set 後,就無法再透過 Object.defineProperty 來更改屬性描述器,並將其他屬性描述器設定為 false
1 | var person = {}; |
基本型別包裹器 Primitive Wrapper
物件型別
上文提到,除了基本型別以外都可以歸類在物件型別,而在物件型別中,又可以細分為以下幾種 “建構器” Constructor
- String()
- Number()
- Boolean()
- Array()
- Object
- Function
- RegExp
- Date
- Error
- Symbol
以上這些建構器都可以透過關鍵字 new 來建立對應的物件
基本型別包裹器 Primitive Wrapper
物件中的 “變數” 稱為 “屬性”,而物件中的 “函式” 稱為 “方法”,但在基本型別中卻有 “屬性” 和 “方法” 可以呼叫
1 | var str = 'Hello'; |
因為在存取字串 String、數字 Number 和布林 Boolean 的那一刻會被 “自動轉型” 為該類別的物件,這就是基本型別包裹器 Primitive Wrapper
當我們在呼叫基本型別的屬性和方法時,背後的原理是透過對應的物件建構器包裝成 “物件”,再回傳對應的屬性後銷毀並回復成基本型別
1 | var str = new String('Hello'); |
基本型別並沒有 “屬性” 和 “方法”,在設定完後仍會回傳 undefined
1 | var str = 'Hello'; |
物件與原型鍊 Object & Prototype Chain
JavaScript 是一門物件導向的程式語言,它的繼承方法是透過 “原型” Prototype 來操作
原型鍊 Prototype Chain
原型繼承的基本概念就是讓沒有該屬性的物件去存取其他物件的屬性,原型物件是物件的內部屬性,無法直接存取,通常直接標示為 [[prototype]]
Object.setPrototypeOf(“繼承者”, “原型”)
透過 Object.setPrototypeOf() 可以指定物件之間的原型關係,第一個參數為繼承者物件,第二個為要繼承的原型物件
1 | var person = {}; |
延續上方範例,同一物件無法指定兩個原型物件,如果繼承超過一個原型物件,只會繼承最後一個
1 | var age = { |
這時候就需要 “原型鍊” 的概念了,延續上方範例,先讓 person2 去繼承 age,再讓 person 繼承 person2
1 | var person = {}; |
最頂層原型物件 Object.prototype
當我們嘗試存取不存在的物件屬性時,就會自動往該物件的 “原型物件” [[prototype]] 去尋找,順著原型鍊一直找,直到 “最頂層原型物件” Object.prototype
最頂層原型物件 Object.prototype 是所有物件的起源,我們以往呼叫卻未定義的 “方法” Method 就是透過 “原型鍊” 繼承自這個 “最頂層原型物件” Object.prototype 的方法
1 | Object.prototype.hasOwnPrototype() |
建構式與原型
因為函式也是物件的一種,所以也可以透過 prototype 來擴充每個透過這個函式所建構出來的物件
1 | var Person = function (){}; |
這裡一一解析以上的結果以釐清觀念
p1: 直接呼叫Person這個函式,所以就會是這個函式回傳的結果,因為沒有設定 return ,自然也就是預設的 undefinedp2: 透過Person這個函式物件所建構出來的 “物件”p2.sayHello(): 函式被建立時,都會有原型物件,當我們透過這個函式建構一個新物件時,這個函式的原型物件就會被當作新物件的 “原型”Person.sayHello(): 因為Person本身是一個函式,所以並不會存取物件屬性
如果新建構出來的物件實體和原型具有相同的 “屬性” 和 “方法” 時,會優先存取自己的
1 | var Person = function (){ |
繼承判斷
透過 hasOwnProperty() 可以判斷該屬性是物件本身所有還是繼承而來的
1 | var Person = function (){ |
物件的 __proto__ 屬性
從上方的 建構式與原型 中可以知道,當透過函式建構出一個新的物件時,這個新物件的原型會自動指向函式的原型物件 prototype
在 JavaScript 中,所有物件都有 __proto__ 這個特殊屬性,這個屬性會指向這個物件的原型物件 ; 在 ES5 中,也可以透過 Object.getPropertyOf() 這個方法來取得物件的原型物件
1 | var Person = function(name){ |
要注意的是,當一個函是被建立的時候,會產生一個 prototype 屬性,但 prototype “並非” 這個函式的原型物件,而是透過這個函式所建構的新物件才會將原型指向函式的 prototype 屬性
1 | var Person = function (sayHello){ |
物件與原型繼承
前面都是透過建構式來介紹,但直接透過物件來繼承是比較常見的做法
第一種是上方所提到的 Object.setPropertyOf()
1
Object.setPropertyOf(p, Person);
第二種是透過
Object.create()來建立一個新物件1
2
3
4
5
6
7
8
9
10var Person = {
name: 'Jerry',
sayHello: function (){
return "Hi! " + this.name
}
}
var p = Object.create(Person);
console.log(p.sayHello()); // "Hi! Jerry"
而 Object.create() 背後的原理是將物件作為參數帶入,先建立一個新函式,再將這個函式的原型指向這個物件,最後回傳一個 new 的新物件
1 | Object.create = function(proto){ |
proto & prototype
總結一下 __proto__ & prototype 的關係,JavaScript 的內建物件 Array``、Function 等,這些物件的 prototype 屬性本身也是一個物件,而這些屬性實際上都是繼承自最頂層物件 Object.prototype
1 | console.log(Object.getPrototypeOf(Function.prototype) === Object.prototype); // true |
由此可知下層物件的 __proto__ 屬性都會指向上層物件的 prototype 屬性,這就是 JavaScript 的繼承關係