前言
JavaScript原型是JavaScript的精髓所在,理解了JavaScript原型對於我們理解JavaScript有着重大的意義。
一、構造對象引出的問題
1. 構造對象
在JavaScript中創建一個新的對象,可以如下:
function test(){}
var t1 = new test();
而上面的方法等價於如下:
function test(){}
var t1 = {}; //創建一個空的對象(此時t1指向這個新創建的對象)
t1.__proto__ = test.prototype; //將t1的內置對象指針指向test的prototype對象,t1的內置對象指針是無法直接訪問到的,但是內置對象指針所指向的對象的方法和屬性是可以被直接使用的
test.call(t1);
先創建一個空的對象(t1),然後將t1的內置對象指針指向test的prototype對象,最後調用test函數,並將該對象(t1)作爲test函數的this。
理解上面的三個步驟對於理解後面的代碼有着非常關鍵的意義,我相信會有很多人覺得讀到這理解了,但是當往後讀就會發現,他們的理解是不夠深入的,但沒有問題,到時記得再回來讀讀就好。(注:JavaScript中所有的obj對象都有__proto__屬性,但是該屬性無法被訪問,只有在new xxx的時候,會將其指向xxx的prototype對象)
注意:三個步驟的第一步,創建一個空的對象,假如t1是原本指向另外的對象的話,之後t1就會改爲指向這個新創建的對象了。例:
<script type="text/javascript">
var luo = new Object();
luo.name = "soldierluo";
luo.age = 23;
alert(luo.name+" is "+luo.age+" years old"); //此時的luo所指向的對象擁有兩個屬性
function person(){
this.salary = "33333";
}
luo = new person(); //這之後,luo所指向的對象替代了此前的luo所指向的對象(就是所luo的指向改變了)
alert(luo.name+" is "+luo.age+" years old"); //所有這時訪問不到原來的對象的屬性了
alert(luo.salary);
</script>
二、模擬繼承
我們可以通過call來模擬繼承,例:
<script type="text/javascript">
function person(name){
this.name = name;
this.say = function(){alert("i'm "+this.name)};
}
function worker(name, salary){
person.call(this,name);
this.salary = salary;
this.show = function(){alert(this.name+"'s salary is "+this.salary)};
}
var w1 = new worker("luo",10000);
var w2 = new worker("soldierluo",333333);
w1.say(); w1.show();
w2.say(); w2.show();
alert(w1.say==w2.say);
</script>
上面我們將person和worker看成兩個類,而worker類繼承了person類,這樣worker類就獲得了person類的屬性和方法。
而繼承的實現是通過在worker中調用person方法,並將worker的this傳遞給person,而不爲person的this,這樣,person和worker就擁有了同樣的this,從而實現了繼承。
但是。。。。。。有一個不小的問題——函數沒能複用
上面代碼的最後一句,返回的是false,這說明w1和w2對象使用的不是同一個say函數體。這是因爲say方法在w1和w2兩個對象中都有一份say函數體的拷貝。同一個類的對象各自擁有一套函數體顯然是一種浪費。
三、使用原型(prototype)杜絕浪費
1. JavaScript中所有function類型的對象都有一個prototype屬性,這個prototype屬性又指向一個object對象,所以我們可以任意給它添加屬性和方法。通過同一函數聲明的對象,在該函數的prototype屬性上的方法和屬性都是公用的,並且可以直接使用。例:
<script type="text/javascript">
function person(name){this.name = name;}
person.prototype.say = function(){alert("i'm "+this.name);}
var p1 = new person("luo");
var p2 = new person("soldier");
alert(p1.say==p2.say);
p1.say();p2.say();
</script>
返回結果爲true,說明p1和p2對象使用的是同一個say方法體,而prototype上的say方法也可以直接調用。
2. 將prototype應用於繼承,這樣繼承的類就可以使用同一個方法體了,例:
<script type="text/javascript">
function person(name){
this.name = name;
}
person.prototype.say = function(){alert("i'm "+this.name)};
function worker(name, salary){
person.call(this,name);
this.salary = salary;
this.show = function(){alert(this.name+"'s salary is "+this.salary);};
}
var w1 = new worker("luo",10000);
var w2 = new worker("soldierluo",333333);
w1.say();
w2.say();
alert(w1.say);
alert(w2.say);
</script>
執行上面的代碼,發現報錯,如果註釋掉w1.say();w2.say();,我們會驚奇地發現,w1.say和w2.say方法居然爲undefined,這真是奇怪了。
來看上面的代碼,worker類通過person.call(this,name);來繼承了person類,這樣它們就擁有了同樣的this,但person的say方法並不在this上,而在prototype上。那如何才能使用到person的prototype上的方法呢?最重要的一步:worker.prototype = new person();,通過這句,我們可以將worker的prototype與person的prototype關聯起來。當worker的prototype中找不到對應的屬性或方法時,它會根據上面的關聯找到person的prototype中去,這就是所謂的“原型鏈繼承”。
最終代碼如下:
<script type="text/javascript">
function person(name){
this.name = name;
}
person.prototype.say = function(){alert("i'm "+this.name)};
function worker(name, salary){
person.call(this,name);
this.salary = salary;
this.show = function(){alert(this.name+"'s salary is "+this.salary);};
}
worker.prototype = new person();
var w1 = new worker("luo",10000);
var w2 = new worker("soldierluo",333333);
w1.say();
w2.say();
</script>
分析上面的繼承,可以發現兩個最重要的點
1) 通過person.call(this);來實現this的繼承(或者叫擴展,我覺得更合適)
2) 通過worker.prototype=new person();(內部實現爲:worker.prototype.__proto__=person.prototype)來實現prototype的繼承(或者叫擴展,我覺得更適合)
四、閉包——原型擴展
微軟曾經使用了一種稱爲“閉包”的技術來模擬類,其大致模型如下:
<script type="text/javascript">
function person(name, age){
var name = name; //私有變量
var getName = function(){alert("my name is "+name);} //私有方法
this.age = age; //公共變量
this.say = function(){alert(name+" is "+this.age+" years old");} //公共方法
}
var p1 = new person("luo",23);
p1.say(); alert(p1.age); //調用公共方法、獲取公共變量
p1.age=100; alert(p1.age); //修改公共變量值
p1.say(); alert(p1.age); //調用公共方法、獲取公共變量
p1.getName(); alert(p1.name); //調用私有方法、獲取私有變量
</script>
上面可以看到,凡是通過var聲明的,不論是函數還是屬性,外部是無法訪問或更改的,而掛靠在this對象後的屬性和方法則可以在外部訪問和更改。通過這樣的屬性,也就模擬了面向對象中的私有和公有屬性,這就稱之爲“閉包”。
官方解釋(看起來比較頭痛):所謂的“閉包”,就是在構造函數體內定義另外的函數作爲目標對象的方法函數,而這個對象的方法函數反過來引用外層外層函數體中的臨時變量。這使得只要目標對象在生存期內始終能保持其方法,就能間接保持原構造函數體當時用到的臨時變量值。儘管最開始的構造函數調用已經結束,臨時變量的名稱也都消失了,但在目標對象的方法內卻始終能引用到該變量的值,而且該值只能通這種方法來訪問。即使再次調用相同的構造函數,但只會生成新對象和方法,新的臨時變量只是對應新的值,和上次那次調用的是各自獨立的。
五、給每一個對象設置一份方法是一種很大的浪費。還有,“閉包”這種間接保持變量值的機制,往往會給JavaSript的垃圾回收器製造難題。特別是遇到對象間複雜的循環引用時,垃圾回收的判斷邏輯非常複雜。無獨有偶,IE瀏覽器早期版本確實存在JavaSript垃圾回收方面的內存泄漏問題。再加上“閉包”模型在性能測試方面的表現不佳,微軟最終放棄了“閉包”模型,而改用“原型”模型。正所謂“有得必有失”嘛。“原型模型”例:
<script type="text/javascript">
function person(name, age){
this.name = name;
this.age = age;
}
person.prototype.say = function(){alert(this.name+" is "+this.age+" years old");}
function worker(name, age, salary){
person.call(this,name,age);
this.salary = salary;
}
worker.prototype = new person();
worker.prototype.show = function(){alert(this.name+"'s salary is "+this.salary);}
var w1 = new worker("luo",23,5000);
var w2 = new worker("soldier",32,50000);
w1.show(); w1.say();
w2.show(); w2.say();
</script>
這裏的原型模型其實就是上面說的模擬繼承,沒有私有變量,並且要分兩部分來定義類,顯得不夠簡介,不過對象的方法是共享的,並且不論是在垃圾回收還是性能方面都要優於閉包模型。