2018年4月5日 星期四

[ JavaScript Gossip ] JavaScript 核心 : 定義物件 (函式 prototype 特性)

Source From Here 
前言 : 
在 建構式Constructor) 中看過一個例子 : 
  1. function Person(name, age) {  
  2.     this.name = name;  
  3.     this.age = age;  
  4.     this.toString = function() {  
  5.         return '[' + this.name + ', ' + this.age + ']';  
  6.     };  
  7. }  
Person 函式中使用了函式實字建立函式實例,並指定給 toString 特性,每一次呼叫建構式時,都會建立一次函式實例 : 
> function Person(name, age) {
> this.name = name;
> this.age = age;
> this.toString = function() {
> return '[' + this.name + ', ' + this.age + ']';
> };
> }

> var p1 = new Person('Justin', 35);
> var p2 = new Person('Momor', 32);
> p1.toString == p2.toString;
false // p1 的 toString() 與 p2 的 toString() 是不同物件!

但函式內容並沒有綁定特定資源. 為了節省記憶體,你也許可以這麼撰寫 : 
  1. function toString() {  
  2.     return '[' + this.name + ', ' + this.age + ']';  
  3. }  
  4.   
  5. function Person(name, age) {  
  6.     this.name = name;  
  7.     this.age = age;  
  8.     this.toString = toString;  
  9. }  
這可以解決重複建立函式實例的問題,但在全域範圍(物件)上多了個 toString 名稱. 如果你知道函式在定義時,都有個 prototype特性,則可以如下 : 
> function Person(name, age) {
> this.name = name;
> this.age = age;
> }

> Person.prototype.toString = function() {
> return '[' + this.name + ', ' + this.age + ']';
> };

> var p1 = new Person('Justin', 35);
> var p2 = new Person('Momor', 32);
> p1
[Justin, 35]
> p2
[Momor, 32]

使用 new 關鍵字時,JavaScript 會先建立一個空物件,接著設定物件的原型為函式的 prototype 特性所參考的物件,然後呼叫建構式並將所建立的空物件設為 this

JavaScript 在尋找特性名稱時,會先在實例上找尋有無特性,以上例而言,p1 上會有 name 與 age 特性,所以你可以直接取得對應的值。如果物件上沒有該特性,會到物件的原型上去尋找,以上例而言,p1 上沒有 toString 特 性,所以會到 p1 的原型上尋找,而 p1 的原型物件此時也就是 Person.prototype 所參考的物件,這個物件上有 toString 特性,所以可以 找到 toString 所參考的函式並執行. 

要注意的是,只有在查找特性,而物件上不具該特性時才會使用原型,如果你對物件設定某個特性,是直接在物件上設定了特性,而不是對原型設定了特性. 例如 : 
> function Some() {}
> Some.prototype.data = 10;
10
> var s = new Some();
> s.data; // 到 prototype 找到 data 特性.
10
> s.data = 20; // 設定物件 s 上的特性 data = 20
20
> s.data; // 參考到 s 物件上的特性 data
20
> Some.prototype.data;
10

在上例中你可以看到,你對 s 所參考的物件設定了 data 特性,但並不影響 Some.prototype.data 的值. 

你可以在任何時間點對函式的 prototype 新增特性,由於原型查找的機制,透過函式而建構的所有實例,都可以找到該特性,即使實例建立之後,特性才被添加到原型中. 例如 : 
> function Some() {}
> var s = new Some();
> print(s.data);
undefined
> Some.prototype.data = 10;
10
> print(s.data);
10

在 建構式Constructor) 中有提過,每個透過 new 建構的物件,都會有個 constructor 特性,參考至當初建構它的函式. 事實上,每個 Function 實例 建立時,都會在 Function實例 上以空物件建立 prototype,然後在空物件上設定constructor 特性,也因此每個 new 建構的物件,都可以找到 constructor 特性. 例如 : 
> function Some() {}
> Some.prototype.constructor;
function Some() {
}

每 個函式的實例,其 prototype 特性預設參考至 Object 的實例,實例上有個 constructor 特性。根據原型尋找原則,如果 prototype上也找不到,由於 prototype 是 Object 實例,也就是 prototype 的原型預設是參考至 Object.prototype,所以又會到 Object.prototype 上尋找,如果找到就使用,如果沒有找到就是 undefined,這就是 JavaScript 的原型鏈尋找特性機制. 例如 : 
> Object.prototype.xyz = 10;
10
> function Some() {}
> var s = new Some();
> s.xyz;
10
> s.__proto__ == Some.prototype;
true
> s.__proto__.__proto__ == Object.prototype; // 任何物件的 prototype 的 prototype 是 Object.prototype
true

上例中 __proto__ 是 Rhino 中一個非標準特性,可以取得物件建立時被設定的原型,預設就是建構式的 prototype 所參考的物件。雖然 Some 實例或 Some.prototype 都沒有定義 xyz,但根據原型鏈查找,最後在 Object.prototype 可以找到 xyz在Object.prototype上添加特性是非常不建議的,因為它會影響所有 JavaScript 中的物件,這邊只是為了示範原型鏈查找). 

你也可以使用 isPrototypeOf() 來確定物件是否為另一物件的原型. 例如 : 
> var arr = [];
> Array.prototype.isPrototypeOf(arr);
true
> Function.prototype.isPrototypeOf(Array);
true
> Object.prototype.isPrototypeOf(Array.prototype);
true

在列舉物件特性時,會循著原型鏈一路找出所有可列舉特性. delete 物件的某個特性時,則會循著原型鏈尋找第一個符合的特性並刪除. 

Supplement 
W3Schools - JavaScript Objects

沒有留言:

張貼留言

[ Py DS ] Ch2 - Introduction to NumPy (Part1)

Source From  Here   Preface   This chapter, along with Chapter 3, outlines techniques for effectively loading, storing, and manipulating i...