寫此文目的是為了讓更多的程式員理解javascript的一些概念,對,是理解,而不是瞭解。我們已經瞭解得夠多了,該是向深入理解的方向靠攏的時候了。
為什麼這麼說,前些日子收到面試邀請,那就去試試唄,有幾年沒有面試過了吧。和面試官坐在沙發上,聊天式的他問我答,以下就是幾個javascript方面的問題:
請建立一個對象,包括幾個公有屬性,接下來是為對象建立一個公有方法,然後為對象建立幾個私人屬性,一個私人方法。
說實話,這幾個問題我默名其妙,要是他讓我用jquery寫個拖動外掛程式什麼的,我估計我能寫挺好,原生的javascript,暈,雖然我看過jquery源碼解讀,但這些基本概念要命。
本文的例子輸出使用如下方法,便於查看:
function dwn(s){document.write(s+"<br />");} function
從一開始接觸到js就感覺好靈活,每個人的寫法都不一樣,比如一個function就有N種寫法,如:
function showMsg(){}var showMsg = function(){}showMsg = function(){}
似乎沒有什麼區別,都是一樣的嘛,真的是一樣的嗎,大家看看下面的例子:
///------------------------------------------------------------------------------------------------//函數定義:命名函數(聲明式),匿名函數(引用式)//聲明式,定義代碼先於函數執行代碼被解析function t1(){dwn("t1");}t1();function t1(){ dwn("new t1");}t1();//引用式,在函數運行中進行動態解析var t1 = function(){ dwn("new new t1");}t1();var t1 = function(){dwn("new new new t1");}t1();//以上輸出:new t1,new t1,new new t1,new new new t1
可能想著應該是輸出t1,new t1,new newt1,new new new t1,結果卻並不是這樣,應該理解這句話:聲明式,定義代碼先於函數執行代碼被解析。
如果深入一步,應該說是scope鏈問題,實際上前面兩個方法等價於window.t1,可以理解為t1是window的一個公有屬性,被賦了兩次值,以最後一次賦值為最終值。
而後面兩個方法,可以理解為是t1是個變數,第四個方法的var去掉之後的結果仍然不會改變。
然而,當第四個方法改成function t1(){}這樣的聲明式時,結果變成了new new new t1,new new new t1,new new t1,new new t1前面兩個按照我的理解可以很好的理解為什麼是這個答案,第三個也可以理解,但是最後一個輸出讓我比較糾結。
另外匿名函數還有(function(){...})()這樣的寫法,最後一個括弧用於參數輸入。
還有var t1=new function(){..}這樣的聲明,實際上t1已經是一個對象了。
var t2 = new function(){var temp = 100; //私人成員this.temp = 200; //公有成員,這兩個概念會在第三點以後展開說明return temp + this.temp;}alert(typeof(t2)); //objectalert(t2.constructor()); //300
除此之外,還有使用系統內建函數對象來構建一個函數,例:
//這個位置加不加new結果都一樣,WHYvar t3 = new Function('var temp = 100; this.temp = 200; return temp + this.temp;'); alert(typeof(t3)); //functionalert(t3()); //300 建立對象
首先我們理解一下物件導向編程(Object-Oriented Programming,OOP),使用OOP技術,常常要使用許多代碼模組,每個模組都提供特定的功能,每個模組都是孤立的,甚至與其它模組完全獨立。這種模組化編程方法提供了非常大的多樣性,大大增加了代碼的重用機會。可以舉例進一步說明這個問題,假定電腦上的一個高效能應用程式是一輛一流賽車。如果使用傳統的編程技巧,這輛賽車就是一個單元。如果要改進該車,就必須替換整個單元,把它送回廠商,讓汽車專家升級它,或者購買一個新車。如果使用OOP技術,就只需從廠商處購買新的引擎,自己按照說明替換它,而不必用鋼鋸切割車體。
不過大部分的論點是,javascript並不是直接的物件導向的語言,但是通過類比可以做到很多物件導向語言才能做到的事,如繼承,多態,封裝,javascript都能幹(沒有做不到,只是想不到):
///-----------------------------------------------//以下三種構造對象的方法//new Object,執行個體化一個Objectvar a = new Object();a.x=1, a.y=2;//對象直接量var b = {x:1,y:2};//定義類型function Point(x,y){ //類似於C#中的類 this.x=x; this.y=y;}var p = new Point(1,2); //執行個體化類
第一種方法通過構造基本對象直接添加屬性的方法來實現,第二種和第一種差不多,可以看成是第一種方法的快捷標記法。第三種方法中,可以以"類"為基礎,創造多個類型相同的對象。
對象屬性的封裝(公有和私人)
以例子來說明:
function List(){//私人成員,在對象外無法訪問,如果此處無var聲明,則m_elements將變成全域變數,這樣外部是可以直接存取到的,如alert(m_elements[0]) var m_elements=[]; m_elements=Array.apply(m_elements,arguments); //此處類比getter,使用時alist.length;//等價於getName()方式:this.length=function(){return m_elements.length;},使用時alist.length();//公有屬性,可以通過"."運算子或下標來訪問this.length = { valueOf:function(){ return m_elements.length; }, toString:function(){ return m_elements.length; } } //公有方法,此方法使用得alert(alist)相當於alert(alist.toString())this.toString=function(){ return m_elements.toString();}//公有方法this.add=function(){m_elements.push.apply(m_elements,arguments);}//私人方法如下形式,這裡涉及到了閉包的概念,接下來繼續說明//var add=function()或function add()//{//m_elements.push.apply(m_elements,arguments);//}}var alist=new List(1,2,3);dwn(alist); //=alert(alist.toString()),輸出1,2,3dwn(alist.length); //輸出3alist.add(4,5,6); dwn(alist); //輸出1,2,3,4,5,6dwn(alist.length); //輸出6 屬性和方法的類型
javascript裡,對象的屬性和方法支援4種不同的類型:private property(私人屬性),dynamic public property(動態公有屬性),static public property/prototype property(靜態公有屬性或原型屬性),static property(靜態屬性或類屬性)。私人屬性對外界完全不具備訪問性,可以通過內部的getter和setter(都是類比);動態公有屬性外界可以訪問,每個對象執行個體持有一個副本,不會相互影響;原型屬性每個對象執行個體共用唯一副本;類屬性不作為執行個體的屬性,只作為類的屬性。
以下是例子:
///------------------------------------------------------------------------------------------------//動態公有類型,靜態公有類型(原型屬性)function myClass(){var p=100; //private propertythis.x=10; //dynamic public property}myClass.prototype.y=20; //static public property or prototype property,動態為myClass的原型添加了屬性,將作用於所有執行個體化了的對象,注意這裡用到了prototype,這是一個非常有用的東東//要想成為進階javascript階段,prototype和閉包必須得理解和適當應用myClass.z=30; //static propertyvar a=new myClass();dwn(a.p) //undefineddwn(a.x) //10dwn(a.y) //20a.x=20;a.y=40;dwn(a.x); //20dwn(a.y); //40delete(a.x); //刪除對象a的屬性xdelete(a.y); //刪除對象a的屬性ydwn(a.x); //undefineddwn(a.y); //20 靜態公有屬性y被刪除後還原為原型屬性ydwn(a.z); //undefined 類屬性無法通過對象訪問dwn(myClass.z); 原型(prototype)
這裡只講部分,prototype和閉包都不是幾句話都能講清楚的,如果這裡可以給你一些啟蒙,則萬幸矣。習語"照貓畫虎",這裡的貓就是原型,虎是類型,可以表示成:虎.prototype=某隻貓 or 虎.prototype=new 貓()。因為原型屬性每個對象執行個體共用唯一副本,所以當執行個體中的一個調整了一個原型屬性的值時,所有執行個體調用這個屬性時都將發生變化,這點需要注意。
以下是原型關係的類型鏈:
function ClassA(){}ClassA.prototype=new Object();function ClassB(){}ClassB.prototype=new ClassA();function ClassC(){}ClassC.prototype=new ClassB();var obj=new ClassC();dwn(obj instanceof ClassC); //truedwn(obj instanceof ClassB); //truedwn(obj instanceof ClassA); //truedwn(obj instanceof Object); //true//帶預設值的Point對象:function Point2(x,y){if (x) this.x=x;if (y) this.y=y;}//設定Point2對象的x,y預設值為0Point2.prototype.x=0;Point2.prototype.y=0;//p1是一個預設(0,0)的對象var p1=new Point2(); //可以寫成var p1=new Point2也不會出錯,WHY//p2賦值var p2=new Point2(1,2);dwn(p1.x+","+p1.y); //0,0dwn(p2.x+","+p2.y); //1,2delete對象的屬性後,原型屬性將回到初始化的狀態:function ClassD(){this.a=100;this.b=200;this.c=300}ClassD.prototype = new ClassD(); //將ClassD原有的屬性設為原型,包括其值ClassD.prototype.reset = function(){ //將非原型屬性刪除for (var each in this) { delete this[each]; }}var d = new ClassD();dwn(d.a); //100d.a*=2;d.b*=2;d.c*=2;dwn(d.a); //200dwn(d.b); //400dwn(d.c); //600d.reset(); //刪掉非原型屬性,所有回來原型dwn(d.a); //100dwn(d.b); //200dwn(d.c); //300 繼承
如果兩個類都是同一個執行個體的類型,那麼它們之間存在著某種關係,我們把同一個執行個體的類型之間的泛化關係稱為繼承。C#和JAVA中都有這個,具體的理解就不說了。
在javascript中,並不直接從方法上支援繼承,但是就像前面說的,可以類比。
方法可以歸納為四種:構造繼承法,原型繼承法,執行個體繼承法和拷貝繼承法。融會貫通之後,還有混合繼續法,這是什麼法,就是前面四種挑幾種混著來~
以下例子涉及到了apply,call和一些Array的用法:
構造繼續法例子
//定義一個Collection類型function Collection(size){this.size = function(){return size}; //公有方法,可以被繼承} Collection.prototype.isEmpty = function(){ //靜態方法,不能被繼承return this.size() == 0;} //定義一個ArrayList類型,它"繼承"Collection類型function ArrayList(){var m_elements = []; //私人成員,不能被繼承m_elements = Array.apply(m_elements, arguments);//ArrayList類型繼承Collection this.base = Collection; this.base.call(this, m_elements.length); this.add = function() { return m_elements.push.apply(m_elements, arguments); } this.toArray = function() { return m_elements; }}ArrayList.prototype.toString = function(){return this.toArray().toString();}//定義一個SortedList類型,它繼承ArrayList類型function SortedList(){//SortedList類型繼承ArrayListthis.base = ArrayList;this.base.apply(this, arguments);this.sort = function(){ var arr = this.toArray(); arr.sort.apply(arr, arguments); }}//構造一個ArrayListvar a = new ArrayList(1,2,3);dwn(a);dwn(a.size()); //a從Collection繼承了size()方法dwn(a.isEmpty); //但是a沒有繼承到isEmpty()方法//構造一個SortedListvar b = new SortedList(3,1,2);b.add(4,0); //b 從ArrayList繼承了add()方法dwn(b.toArray()); //b 從ArrayList繼承了toArray()方法b.sort(); //b 自己實現的sort()方法dwn(b.toArray());dwn(b);dwn(b.size()); //b從Collection繼承了size()方法
原型繼承法例子
//定義一個Point類型function Point(dimension){this.dimension = dimension;}//定義一個Point2D類型,"繼承"Point類型function Point2D(x, y){this.x = x;this.y = y;}Point2D.prototype.distance = function(){return Math.sqrt(this.x * this.x + this.y * this.y);}Point2D.prototype = new Point(2); //Point2D繼承了Point//定義一個Point3D類型,也繼承Point類型function Point3D(x, y, z){this.x = x;this.y = y;this.z = z;}Point3D.prototype = new Point(3); //Point3D也繼承了Point //構造一個Point2D對象var p1 = new Point2D(0,0);//構造一個Point3D對象var p2 = new Point3D(0,1,2);dwn(p1.dimension);dwn(p2.dimension);dwn(p1 instanceof Point2D); //p1 是一個 Point2Ddwn(p1 instanceof Point); //p1 也是一個 Pointdwn(p2 instanceof Point); //p2 是一個Point
以上兩種方法是最常用的。
執行個體繼承法例子
在說此法例子之前,說說構造繼承法的局限,如下:
function MyDate(){this.base = Date;this.base.apply(this, arguments);}var date = new MyDate();//undefined,date並沒有繼承到Date類型,所以沒有toGMTString方法alert(date.toGMTString);
核心對象的某些方法不能被構造繼承,原因是核心對象並不像我們自訂的一般對象那樣在建構函式裡進行賦值或初始化操作換成原型繼承法呢?,如下:
function MyDate(){}MyDate.prototype=new Date();var date=new MyDate();//'[object]'不是日期對象,仍然沒有繼承到Date類型!alert(date.toGMTString);
現在,換成執行個體繼承法:
function MyDate(){//instance是一個新建立的日期對象 var instance = new Date(); instance.printDate = function(){document.write("<p> "+instance.toLocaleString()+"</p> ");} //對instance擴充printDate()方法 return instance; //將instance作為建構函式的傳回值返回}var myDate = new MyDate();//這回成功輸出了正確的時間字串,看來myDate已經是一個Date的執行個體了,繼承成功dwn(myDate.toGMTString()); //如果沒有return instance,將不能以下標訪問,因為是私人對象的方法myDate.printDate();
拷貝繼承法例子
Function.prototype.extends = function(obj){for(var each in obj) {this.prototype[each] = obj[each]; //對對象的屬性進行一對一的複製,但是它又慢又容易引起問題//所以這種"繼承"方式一般不推薦使用}}var Point2D = function(){//……}Point2D.extends(new Point()){//……}
這種繼承法似乎是用得很少的。
混合繼承例子
function Point2D(x, y){this.x = x;this.y = y;}function ColorPoint2D(x, y, c){Point2D.call(this, x, y); //這裡是構造繼承,調用了父類的建構函式//從前面的例子看過來,這裡等價於//this.base=Point2D;//this.base.call(this,x,y);this.color = c;}ColorPoint2D.prototype = new Point2D(); //這裡用了原型繼承,讓ColorPoint2D以Point2D對象為原型