Javascript筆記:使用prototype chain(原型鍊)來達成繼承效果

Posted by Kubeguts on 2017-09-13

先知道個keyword:

  • __proto__: javascript自己定義的變數,用來實現inheritance效果,有點類似像link list node的概念,連結其他的 prototype

javascript記憶體管理的配置:

  • class儲存在 global底下
  • class的prototype放置在heap (即class底下的各種variable和function)
  • instance(ex var b = new B()) 的b儲存在 callstack(記憶體RAM裡面)

slogan:在自己的scope裡面找不到要的函式或變數就跟自己的__proto__要!

繼承範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
var Car = function(){
//constructor

this.brand = "default";
var _wheels

// private method
var initialWheels = function () {
_wheels = 4
}

this.getNumberofWheel = function(){
return this.brand;
}
}

var Benz = function(brandName) {
//constructor
Car.apply(this, argument) //等於呼叫 super(args);
//argument為 var Benz = function(args...); 的 args們 = [brandName, wheel]

// 這一行就是把 Benz.prototype.__proto__ = Car.prototype接起來;
// 即Benz class inheritance Car
// 疑問:上面敘述需和Ben確認一下 那這樣為什麼下面又得自己做Benz.prototype = Object.create(Car.prototype) ??
// 解答:若沒有做Car.apply (即super()) 動作,這樣子類別會無法將接受的arguments傳遞給從parent複製過來的函式

this.brand = brandName; //初始化車品牌的名稱


}
// javascript ES5得自己綁定繼承關係,用__proto__這個keyword
// 建立父類別實體 設定繼承關係:

Benz.prototype = Object.create(Car.prototype)
//上面即是做了Benz.prototype.__proto__ = Car.prototype
//使Benz class繼承Car class(Benz extends Car)

Benz.prototype.constructor = Benz
//Benz的constructor指向Benz class
//將Benz的constructor綁定到自己身上,
//才能在Benz new出一個物件時作初始化動作(即呼叫Benz =function函式{})
//var benz = new Benz("Benz") 初始化該車子名稱:"Benz";

var benz = new Benz()
// benz.__proto__ = Benz.prototype
// 宣告一個物件叫 benz

console.log(benz.numberOfWheels())
// 會先搜尋benz.prototype有沒有numberOfWheels()方法
// 沒有==>那就找benz.__proto__ = Benz.prototype;
// Benz.prototype就會找到 numberOfWheels()的方法!

benz.numberOfWheels() 有此方法
每次呼叫Car.apply(this, arguments)時"複製"到子類別的物件上
初始化物件時較慢,在run time想要動態改變numberOfWheels()的實作時
無法影響已經創建的子類別或父類別instances.

以下為初始化較有效率的寫法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var Car = function () {
// constructor

// public property
this.wheels = 4
// pulic property
this.brand = 'default'
}

// 這種寫法不支援存取private property
// 因為每個物件都可以直接存取掛在parent.prototype上的property

// 但可以在run time修改此Car.prototype.numberOfWheels()的實作
// 就可以透過prototype chain更改所有相關的instance功能 因為instance共用prototype chain上的方法
Car.prototype.numberOfWheels = function () {
return this.wheels
}

var Benz = function (brandName) {
Car.apply(this, arguments) // 複製繼承父類別public property
this.brand = 'brandName';
this.getBrand = function () {
return this.brand
}
}
...

numberOfWheels該方法已經被掛在 Car.prototype上,
所以並不像Car.apply上直接複製一份給子class們,而是子class去共用Car.prototype.numberOfWheels 這個方法。

  • 優點:節省初始化的時間和提升效率
  • 缺點:這種寫法不支援存取private property

javascript 類別、prototype以及reference中的__proto__所指向的關係示意圖:

圖片作者: Ben大大
javascript_prototype_chain

假設 B class extend A;
let b = new B(); // b.__proto__ = B.prototype
let a = new A(); // a.__proto__ = A.prototype
let o = new Object(); o.__proto__ = Object.prototype
//對應到圖片左邊的b、a、o.

(先從圖片最左上角開始看
那 var b = new B();
b想要使用繼承A的 getValue() 函式,
那麼就得先找自己的 b.prototype 沒有的話 找 b.__proto__ = B.prototype

b.__proto__也沒有的話(即B.prototype)也沒有,
那麼找 B.prototype.__proto__ == A.prototype
A.prototype就會找到getValue()的函式!

如果又沒找到,那就最後找A.prototype.__proto__ == Object.prototype
若沒有那就找 Object.prototype.__proto__ == null 即找無該函式,compile告知發生錯誤);

Prototype inheritance 的好處

  • Suitable in loosely typed environments, no need to define explicit types.
  • Makes it incredibly easy to implement singleton pattern (compare JavaScript and Java in this regard, and you’ll know what I am talking about).

(why? 因為prototype inheritance提供??? 請看補充二)

  • Provides ways of applying a method of an object in the context of a different object, adding and replacing methods dynamically from an object etc. (things which are not possible in a strongly typed languages).

Prototype inheritance的壞處

  • No easy way of implementing private variables. Its possible to implement private vars using Crockford’s wizardry using closures, but its definitely not as trivial as using private variables in say Java or C#.
    (因為__prpto__會指向prototype的所有成員!故無法像class inheritance那樣避免繼承到有private關鍵字的變數,故得用closure手法來避免繼承到_private變數)

Reference:
prototype based vs. class based inheritance

補充ㄧ:Function.prototype.apply

fun.apply(thisArg, [argsArray])
// 將一連串參數用array包起來:[argsArray],丟給fun函式處理

範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function theFunction(name, profession) {
console.log("My name is " + name + " and I am a " + profession + ".");
}
// call function

function callFn(name, profession) {
theFunction.apply(this, [name, profession]);
}
theFunction('小will', '工程師');
callFn('大Will', '小廢廢');

// 輸出:
// My name is 小will and I am a 工程師.
// My name is 大Will and I am a 小廢廢.

補充二:用Closure實作Singleton模式

閉包觀念連結:==> 閉包是什麼??

先看範例程式碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var UniverseN;
(function(){
var instance;
UniverseN = function UniverseN(){
if(instance){
return instance;
}
instance = this;

this.start_time = 0;
this.bang = 'Big';
};
}());

var uni9 = new UniverseN();
var uni10 = new UniverseN();

console.log(uni9 === uni10); //true

UniverseN.prototype.inEverything = true;

可以看出若利用了Prototype inheritance的特性,,將IIFE函式掛在UniverseN.prototype.IIFE上,使得之後宣告的物件不會在複製一份IIFE出來出來,讓IIFE只跑一次!
這樣就可以達成一個class (UniverseN) 只能宣告一次的物件 (uni9)。 如果在宣告出 uni10,他們都還是指向同一樣的instance(uni9.__proto__ == uni10__proto__)

Reference:
JavaScript Design Pattern - Singleton 單體模式