給程式設計師看的Javascript攻略 - Prototype (上)

古鐵發表於2017-09-04

給程式設計師看的Javascript攻略 - Prototype (上)

原文發表在: holmeshe.me , 本文是漢化重製版。

本系列在 Medium掘金上同步連載。

還記得早先用ajax胡亂做專案的時候踩過好多坑,然後對JS留下了“非常詭異”的印象。最近換了一個工作,工作語言就是JS。然後發現這個語言真不得了,前面後面都能幹,基本成了全棧的同義詞。所以剛好就趁這個機會系統學習一下這個語言。因為是給程式設計師看的,這個系列不會講基本的“if-else”,迴圈,或者物件導向。相反,我會關注差異,希望能給您在Pull Request裡走查程式碼式的學習體驗!

prototype, 仿版OOP

一句話定義:prototype 是JS引擎在執行時,給每個類建立的一個單例模板,該類的物件都通過把這個模板在記憶體裡拷貝一份來建立。prototype用一種非常規的方式實現了OOP,我把它稱為仿版表示它不一樣,而並沒有任何貶低的意思。

打上碼,我們先感受下prototype有多不一樣:

function inherits(ChildClass, ParentClass) {
  function IntermediateClass() {}
  IntermediateClass.prototype = ParentClass.prototype;
  ChildClass.prototype = new IntermediateClass;
  ChildClass.prototype.constructor = ChildClass;
  return ChildClass;
};複製程式碼

這是一個用prototype實現繼承的示例,現在看不懂沒關係,因為我會在下篇詳細討論基於prototype的繼承。

學這個有用不?

上篇裡面講道,ES6已經在JS裡定義了一套更加標準的OOP機制。而且基於ES6的程式碼是可以被轉譯成ES5的語法,所以新標準在應用中可以達到老標準同等的相容性。這個是說prototype不用學了嗎?我認為不一定。ES5仍然在已有的程式碼中大規模存在著,如果在AngularReactVue的程式碼庫下面執行這段命令:

grep -R prototype *|wc -l複製程式碼

結果分別是286,405和756處。所以對於我而言,哪怕是以理解這1447行基礎程式碼為目的,也要把prototype當做必修課。尤其要注意,這三個框架我是按字母順序拍(緊張打錯字,排)的先後次序?。

玩轉prototype

在下面的例子中,我們首先定義一個建構函式(如果您不熟悉JS語境下的建構函式,請⬅到我的上一篇文章)然後我們基於這個建構函式建立物件來檢查他們的prototype

打上碼:

var MyClass = function (){};
var obj = new MyClass;

alert(obj.__proto__ == MyClass.prototype); //=>true
alert(obj.prototype == MyClass.prototype); //=>false
// if the object's prototype does not equal to the class' prototype, what is it then
alert(obj.prototype); //=>undefined

// you can change the definition of a class on the run
MyClass.prototype.a_member = "abc";
// and the object's structure is changed as well
alert(obj.a_member); //=>abc
alert(obj.__proto__.a_member); //=>abc複製程式碼

執行結果:

true
false
undefined
abc
abc複製程式碼

初步觀察,我們可以得到以下印象:

  1. 只有類(class),或者說建構函式才包含prototype。並且這個prototype可以通過生成物件例項的__proto__訪問的到;
  2.  prototype__proto__ 其實是一個硬幣的正反面,它們同時指向的一個實體,就是本篇開頭所說的那個單例
  3. 如果類的prototype在執行時改變,這個變更會傳遞給所有例項。

更進一步,如果在執行時改變某個例項的__proto__,這個改變會向上改變類的prototype(類定義),進而會級聯更新所有其它同型別例項的結構。

打上碼:

var MyClass = function (){};
var obj1 = new MyClass;
var obj2 = new MyClass;
obj1.__proto__.a_member = 'abc';

// an instance can change the definition of the class
alert(MyClass.prototype.a_member); //Ooooops...
// so all other instances are affected
alert(obj2.a_member);複製程式碼

執行結果:

abcabc複製程式碼

這是一個作死的操作,因為誰都受不了變來變去的類宣告。所以我建議把__proto__想象成常量,別去動就好了。

但是,如果你實在想在執行時動定義,那就動例項本身吧,因為動例項沒有潛規則:

var MyClass = function (){};
var obj1 = new MyClass;
var obj2 = new MyClass;

obj1.a_member = 'abc'; // 動物件的定義

// 不打緊
alert(MyClass.prototype.a_member);
// 不打緊
alert(obj2.a_member);複製程式碼

執行結果:

abc
abc複製程式碼

從另一方面說,你也可以直接動類的定義,不但沒有副作用,還能碰巧讓JavaScript支援static成員變數(或函式):

var MyClass = function (){};
var obj1 = new MyClass;
var obj2 = new MyClass;
MyClass.a_member = 'abc'; // 動類的定義

// 不打緊
alert(MyClass.prototype.a_member);

// 不打緊
alert(obj1.a_member);
alert(obj2.a_member);

// the member has been effectively added
alert(MyClass.a_member);複製程式碼

執行結果:

undefined
undefined
undefined
abc複製程式碼

所以用好了這也可以是個神操作,主要是由於

一切皆物件

在JavaScript裡,一切皆物件。這一切包括,函式,建構函式(類),物件例項,prototype以及__proto__,等等。我們來用碼證明一下:

var MyClass = function (){  this.a = "a";  this.b = "b";};
var obj = new MyClass;var arry = [];
function f1() {
  alert("something");
};

alert(MyClass instanceof Object);
alert(MyClass.prototype instanceof Object);
alert(MyClass.__proto__ instanceof Object);
alert(obj instanceof Object);
alert(obj.__proto__ instanceof Object);
alert(obj instanceof Object);
alert(arry instanceof Object);
alert(f1 instanceof Object);
alert(f1.prototype instanceof Object);
alert(f1.__proto__ instanceof Object);複製程式碼

執行結果:

true(合唱)複製程式碼

更進一步說,物件可以細分為一等物件普通物件。普通物件就是一般的變數了,它們可以在執行時被建立,被改,被銷燬或者被賦值給其它變數。而一等物件則是“柏金版”的普通物件,是解鎖了會員特權的。所以,除了“被建立,被改,被銷燬或者被賦值給其它變數”以外,它還能被(作為普通函式)呼叫,還能(作為建構函式)建立例項。這些多出來的特權是不是可以從另一個方面解釋為啥要多給一等物件一個prototype呢?你怎麼看?

打上碼:

var MyClass = function () {
  this.a = "a";
  this.b = "b";
};

var obj = new MyClass;
function f1() {
  alert("something");
};

// MyClass is a first class object so...
alert(MyClass.prototype); // it has prototype and
alert(MyClass.__proto__); // __proto__

// obj is an object so...
alert(obj.prototype);
// it does not have prototype but
alert(obj.__proto__); // it has __proto__

// f1 is a first class object so...
alert(f1.prototype); // it has prototype and
alert(f1.__proto__); // __proto__複製程式碼

執行結果:

[object Object]
function () {}
undefined
[object Object]
[object Object]
function () {}複製程式碼

除了。。。

例外一

我剛剛講了一切皆物件嗎?

var str = "whatever";
alert(str instanceof Object);
alert(str.__proto__ instanceof Object);複製程式碼

執行結果:

false
true複製程式碼

從上碼(沒寫錯)可以看出,儘管字串常量不是物件,它也有一個__proto__。所以有兩種可能性,要麼字串也是物件(明顯不是),要麼我上面講的都是錯的。

其實沒錯啦。這裡是由於一種叫自動包裝的機制起了作用,所以基本資料型別被自動包裝成物件,然後呼叫相應的成員。

打上碼:

var str = "whatever";
alert(str.indexOf("tever"));複製程式碼

執行結果

3複製程式碼

同理,字串常量在執行時被自動包裝成String()物件,然後呼叫indexOf()成員函式。這個機制在其它基本資料型別(int, float)上面也有效哦。

例外二

好像很完美的樣子,那再試試下面這段碼:

var obj = { a:"abc", b:"bcd"};

alert(obj instanceof Object);
alert(obj.__proto__);
alert(obj.__proto__ instanceof Object);複製程式碼

執行結果:

true
[object Object]
false複製程式碼

顯然,從前兩條的結果看 var objobj.__proto__ 都是物件。但是當我想確認一下的時候,obj.__proto__又不是了。我好像成了一個亂寫自己都不懂的東西的博主了,但是如果您有答案的話,不妨在下面留言。其實如果沒有也不打緊,因為__proto__本來也是非標準用法。

下一篇我會討論一個難點,基於 prototype 鏈的繼承。

好了,今天先寫到這。如果您覺得這篇不錯,可以關注我的公眾號 - 全棧獵人。

給程式設計師看的Javascript攻略 - Prototype (上)

也可以去Medium上隨意啪啪啪我的其他文章。感謝閱讀!?


相關文章