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