JavaScript面向對象程序設計基礎
- 2019 年 12 月 25 日
- 筆記
一、對象
JavaScript的簡單數據類型包括數字、字符串、布爾值、null值和undefined值。其他所有的值都是對象。數字、字符串和布爾值「貌似」對象,因為它們擁有方法,但它們是不可變的。JavaScript中的對象是可變的鍵控集合(Keyed collections)。在JavaScript中,數組時對象,函數是對象,正在表達式是對象,當然,對象自然也是對象。 對象是屬性的集合,每一個屬性具有一個名稱和一個值。數學的名字可以是包括空字符串在內的任意字符串。屬性值可以是除undefined值之外的任何值。
JavaScript里的對象是無類型的(class-free)。它對新屬性的名字和值是沒有限制的。對象適合用於彙集和管理數據。對象可以保護其他對象,所以它們可以很容易的表示成樹狀或圖形結構。
1、創建(Create)
可以採用兩種方法來實例化對象。
第一種方法是使用new關鍵字
1 |
var myObject = new Object(); |
---|
new 關鍵字調用構造函數,或者更通用的說法是構造器。構造器將初始化新創建的對象。下面的代碼演示了一個名為Zombie(殭屍)對象的構造器,構造器初始化該對象的name屬性,然後使用new關鍵字實例化一個Zombie對象。this關鍵字用於引用當前對象,不能對它進行賦值,但可以將this關鍵字的值賦給另外一個變量。
12345 |
//構造函數function Zombie( name ) { this.name = name;}var smallZombie = new Zombie( "Booger"); |
---|
使用對象字面量創建對象 另外一種實例化新對象的方法更加方便:使用對象字面量,這種方式創建的對象更像其他語言中的散列(hash)或關聯數組。
12345 |
var myObject = { };var webSite= { "url": "www.mybry.com", "siteName": "讀你"}; |
---|
在屬性列表的最後一項值的末尾,請不要使用結尾逗號。不同的瀏覽器對此的解析並不一致。
2、檢索(Retrieval)
既可以使用方括號來訪問對象的屬性,也可以使用點操作符來訪問。下面的代碼示例了者兩種方式:
12345 |
webSite['url'];"www.mybry.com"webSite.siteName;"讀你" |
---|
採用方括號方式訪問屬性時,可以使用JavaScript的關鍵字作為屬性名,但不推薦這樣做,使用點操作符方式訪問屬性時則不能使用。使用點操作符方式,代碼更加簡短。JSLint鼓勵開發人員使用點操作符方式。因為屬性也可以是對象,可以在任意層次上嵌套對象。
下面假設有一個父親father對象,他有自己的名字name和年齡age,他有兩個兒子childrenOne和childrenTwo,兩個孩子當然也是對象,也有名字name個年齡age,然後我們訪問孩子childrenOne的名字:
123456789101112131415 |
var father = { 'name': 'zhangsan', 'age': 50, 'childrenOne': { 'name': 'zhangsi', 'age': 22 }, 'childrenTwo': { 'name': 'zhangwu', 'age': 12 }};console.log(father.childrenOne.name);//輸出:zhangsi |
---|
JavaScript是一種動態的語言,因此更新一個屬性只需要重新對屬性賦值即可。要從對象中移除一個屬性,只需要使用delete操作符。刪除一個不存在的屬性並不會造成任何危險。要遍歷對象的所有屬性,可以使用for…in循環,比如下面的代碼塊:
123456789101112 |
var obj1 = { 'properties1': 1, 'properties2': 2};var i;for( i in obj1 ){ console.log( i );}properties1properties2 |
---|
3、原型(Prototype)
JavaScript使用原型繼承(prototype inheritance),對象直接從其他對象繼承,從而創建新的對象。簡而言之,對象繼承了另外一個對象的屬性。JavaScript中沒有類,這是與Java和C#等語言相比一個較大的差別。原型就是其他對象的模型(model)。
每個對象都連接到一個原型對象,並且它可以從中繼承屬性。所有通過對象字面量創建的對象都連接到Object.prototypr,它是JavaScript中的標配對象。
當你創建一個新對象時,你可以選擇某個對象作為它的原型。JavaScript提供的實現機制雜亂而複雜,但其實可以被明顯地簡化。我們將給Object對象增加一個create方法。這個方法創建一個使用原型對象作為其原型的新對象。
123456789101112131415161718192021222324252627282930 |
if (typeof Object.beget !== 'function') { Object.create = function (o) { var F = function () { }; F.prototype = o; return new F(); };}var another = Object.create( webSite );console.log(another);“` 在firebug中輸出如下結果,正是一個對象。原型連接在更新時是不起作用的。當我們對某個對象做出改變時,不會觸及該對象的原型,原型連接只有在檢索值的時候才會被用到。如果我們嘗試去獲取對象的某個屬性值,但該對象沒有此屬性名,那麼JavaScript會試着從原型對象中獲取屬性值。如果那個原型對象也沒有該屬性,那麼再從他的原型中尋找,以此類推,直到該過程最後到達終點Object.prototype。如果想要的屬性完全不存在與原型鏈中,那麼結果就是undefined值。這個過程稱為委託。原型關係是一種動態關係。如果我們添加一個新的屬性到原型中,該屬性會立即對所有基於該原型創建的對象可見。(更多關於原型鏈的內容後續文章將會介紹)### 4、引用(Reference)引用是一個指向對象實例位置的指針。Object是引用類型,由於所有對象都是通過引用傳遞的,,它們永遠不會被複制:“`jsvar x = myObject;x.nickname = '這世間唯有夢想和好姑娘不可辜負~~~';var nick = myObject.nickname;//因為x和stooge是指向同一個對象的引用,所以nick為『zhangsan』console.log(nick); //這世間唯有夢想和好姑娘不可辜負~~~//因為a、b和c每個都引用一個不同的空對象var a = { }, b = { }, c = { };//a、b和c都引用同一個空對象a = b = c = { }; |
---|
修改綁定於一個原型的屬性,將修改基於該原型的所有其他對對象的原型。
對象是自知的,或者說對象知道自己的屬性。要檢查某個屬性是否存在,可以使用hasOwnProperty()方法,該方法將返回一個布爾值。
5、減少全局變量污染
很多程序員認為,應該避免使用全局變量。有一些辦法可以避免擾亂全局名稱空間,一種辦法是使用單個全局變量作為頂層對象,包含程序或框架中的所有方法和屬性。按照慣例,名稱空間的字母全部大寫,但值得注意的是常量通常也大寫格式。
1234567 |
ZOMBIE_GENERATOR = {};ZOMBIE_GENERATOR.Zombies = { smallZombie : 'Booger', largeZombie : 'Bruce'} |
---|
採用上面的代碼定義名稱空間之後,Zombies就不會與全局名稱空間中的任何其他變量衝突。另外一種減少衝突的方式是使用閉包。
6、反射(Reflection)
檢查對象並確定對象有什麼屬性時很容易的事情,只要試着去檢索該屬性並驗證取得值。typeof操作符對確定屬性的類型很有幫助:
1234 |
typeof father.name; // 'string'typeof father.age; // 'number'typeof father.childrenOne; // 'object'typeof father.xxx; // 'undefined' |
---|
請注意原型鏈中任何屬性都會產生值:
12 |
typeof father.toString; // 'function'typeof father.constructor; // 'function' |
---|
有兩種方法去處理掉這些不需要的屬性。第一個是讓你的程序做檢查並丟棄掉值為函數的屬性。一般來說,當你想讓對象在運行時動態獲取自身信息時,你關注更多的時數據,而你應該意識到一些值可能是函數。
另一個方法時使用hasOwnProperty方法,如果對象擁有獨有的屬性,它會返回true。hasOwnProperty方法不會檢查原型鏈。
12 |
father.hasOwnProperty('name'); // truefather.hasOwnProperty('constructor'); // false |
---|
7、枚舉(Enumeraton)
for...in
語句可以用來遍歷一個對象中的所有屬性名。該枚舉過程將會列出所有的屬性——包括函數和你可能不關心的原型中的屬性——所以有必要過濾那些你不想要的值。最為常用的過濾時hasOwnPropery方法,一級使用typeof來排除函數。
屬性名出現的順序是不確定的,因此要對任何可能出現的順序有所準備。如果你想要確保屬性以特定的順序出現,最好的辦法就是完全避免使用for in語句,而是創建一個數組,在其中以正確的順序包含屬性名:
123456789 |
var properties = [ 'first-name', 'middle-name', 'last-name', 'profession'];for(var i = 0; i < properties.length; i++){ console.info(properties[i]);} |
---|
通過使用for而不是for in,可以得到我們想要的屬性,而不必擔心可能發掘出的原型鏈中的屬性,並且我們按正確的順序取得了它們的值。
二、函數
函數是一個代碼塊,它封裝了一系列的JavaScript語句。函數可以接受參數,也可以返回值。如果一個函數沒有返回一個特定的值,則它返回一個undefined值。下面的代碼示例定義了一個沒有返回值的函數。該函數依然執行了函數體中的操作,將變了x的值乘以2,但由於沒有使用return語句返回值,因此在控制台中輸出函數的返回值時,返回值為undefined。
12345 |
var x = 2;function calc( ){ x = x * 2;}console.log( calc() );// undefined |
---|
這正是函數有趣的地方,與Java不同,JavaScript中的函數時第一類對象。這意味着可以像處理其他JavaScript對象一樣處理JavaScript函數。可以將函數賦予給一個變量,或者保存在另外一個數據結構中(比如一個數組或對象);可以將函數作為參數傳遞給其他函數;可以將函數作為另一個函數的返回值;函數還可以採用匿名函數的形式:即根本沒有綁定於函數名標識符的函數。下面代碼定義了一個函數表達式,並將其賦值給一個變量。
123456 |
var calc = function(x){ return x * 2;}calc(5);// 10 |
---|
給函數名使用圓括號,將執行該函數並返回函數的返回值,而不是返回對函數的引用。
1234567 |
var calc = function(x){ return x * 2;}var calcValue = calc( 5 );console.log( calcValue );// 10 |
---|
第一類函數的另外兩個特性時非常重要的。第一個特性就是將函數作為參數傳遞給其他函數。第二個特性就是匿名函數。下面代碼演示了JavaScript中函數的重要特性。reporter函數接收一個函數作為參數,並輸出執行該參數函數的返回值。另外兩個例子則演示了匿名函數。第一個函數是一個根本不包含任何語句的匿名函數,但根據前面的介紹,該函數將返回一個undefined。第二個函數時一個返回字符串的匿名函數。
123456789101112 |
function reporter( fn ) { console.log( "這個返回值是你傳遞過來的函數:" + fn() );}reporter( function(){}); //這個返回值是你傳遞過來的函數:undefinedreporter( function() { return "這是一個簡單的字符串" });//這個返回值是你傳遞過來的函數:這是一個簡單的字符串function calc(){ return 2 * 4;}reporter( calc );// 這個返回值是你傳遞過來的函數:8 |
---|
這是你會看到控制台還輸出了一個undefined值,這是reporter函數本身的返回值,也就是說執行任何函數本身,它都會返回一個undefined值。
關於匿名函數,一個特別重要的變體就是立即調用的函數表達式,或稱為自執行匿名函數。無論稱為「立即調用的函數表達式」還是「自執行匿名函數」,這一模式的本質就是將一個函數表達式包裝在一對圓括號中,然後立即調用該函數。這一技巧非常簡單,將函數表達式包裝在一對圓括號中,將迫使JavaScript引擎將function(){}塊識別為一個函數表達式,而不是一個函數語句的開始。下面的代碼示例描述了這一模式,在這個簡單的例子中,函數表達式接受兩個值並簡單地將二者相加。
1234 |
(function( x,y ){ console.log( x+y );})( 5,6);// 11 |
---|
由於這樣的函數表達式將被立即調用,因此該模式用於確保代碼塊的執行上下文按照預期的效果執行,這是這種模式最重要的用於之一。通過將參數傳入函數,在執行時就可以捕獲函數所創建閉包中的變量。閉包就是這樣的一個函數:它處在一個函數體中,並引用了函數體當前執行上下文中的變量。閉包是一個極為強大的功能,下面的代碼描述了閉包的基本應用。在下面的例子中還引入了JavaScript函數另外一個有趣的特性,即在函數中可以將另外一個函數作為返回值返回。
在下面的例子中,將自執行匿名函數賦給一個變量message。message返回另外一個函數,該函數只是簡單的輸出變量x的值。有趣的是,當我們把變量x的初始值作為參數傳入函數時,可以在函數執行時所創建的閉包中捕獲變量x的值。無論在外部作用域中的x的值發生了什麼變化,閉包將記住函數執行時變量x的值。
1234567891011 |
var x = 42;console.log(x); // 42var message = (function ( x ){ return function(){ console.log( "x is " + x); }})( x );message(); //x is 42x = 12;console.log( x ); // 12message(); //x is 42 |
---|
即使只介紹了這個簡單的例子,也應該看到JavaScript函數的強大功能。
三、作用域和閉包
當討論作用域時,考慮定義變量的位置和變量的生存期時非常重要的。作用域指的是在什麼地方可以訪問該變量。在JavaScript中,作用域維持在函數級別,而並非塊級別。因此,參數以及使用var關鍵字定義的變量,僅在當前函數中可見。
除了不能訪問this關鍵字和參數之外,嵌套函數可以訪問外部函數中定義的變量。這一機制時通過閉包來實現的,它是指:即使在外部函數結束執行之後,內部嵌套的函數繼續保持它對外部函數的引用。閉包還有助於減少名稱空間的衝突。
每次調用一個包裹的函數時,雖然函數的代碼並沒有改變,但是JavaScript將為每一次調用創建一個新的作用域。下面的代碼說明了這一行為。
123456789101112131415 |
function getFunction(value){ return function(value){ return value; }}var a = getFunction(), b = getFunction(), c = getFunction();console.log(a(0)); // 0console.log(b(1)); // 1console.log(c(2)); // 2console.log(a === b); // false |
---|
當定義一個獨立函數時(即不綁定於任何對象)時,this關鍵字綁定雨全局名稱空間。作為一個最直接的結果,當在一個方法內創建一個內部函數時,內部函數的this關鍵字將綁定於全局名稱空間,而不是綁定於該方法。為了解決這一問題,可以將包裹方法的this關鍵字簡單的賦值給一個名為that的中間變量。
123456789101112131415161718 |
obj = {};obj.method = function(){ var that = this; this.counter = 0; var count = function(){ that.counter += 1; console.log(that.counter); } count(); count(); console.log(this.counter);}obj.method();// 輸出:122 |
---|
四、訪問級別
在JavaScript中並沒有官方的訪問級別語法,JavaScript沒有類似於Java語言中的private或protected這樣的訪問級別關鍵字。默認情況下,對象中所有的成員都是公開和可訪問的。但在JavaScript中可以實現與私有或專有屬性類似的訪問級別效果。要實現私有方法或屬性,請使用閉包。
1234567891011121314 |
function TimeMachine(){ //私有成員 var destination = 'ShangHai CAOHEJING'; //公有成員 this.getDestination = function(){ return destination; };}var zhangsan = new TimeMachine();console.log(zhangsan.getDestination());// ShangHai CAOHEJINGconsole.log(zhangsan.destination);//undefined |
---|
getDestination方法是一個專有方法,它可以訪問TimeMachine(時間機器)中的私有成員。另外,變量destination 是「私有」的,它只能通過專有方法getDestination進行訪問。
五、模塊
於私有和專有訪問級別類似,JavaScript沒有內置的包語法。模塊模式是一種簡單而流行的方式,用於創建自包含的、模塊化的代碼。要創建一個模塊,只需要聲明一個名稱空間、將有關函數綁定在該名稱空間,並定義私有成員和專有成員即可,下面將使用模塊重寫TimeMachine對象。
123456789101112131415161718192021222324252627282930313233343536373839404142434445 |
//創建名稱空間對象TIMEMACCHINE = {};TIMEMACCHINE.createZhangsan = (function(){ //私有成員 var destination = ''; var model = ''; var fuel = ''; //公有訪問方法 return { //設置器 setDestination: function(dest){ this.destination = dest; }, setModel: function(model){ this.model = model; }, setFuel: function(fuel){ this.fuel = fuel; }, //訪問器 getDestination: function(){ return this.destination; }, getModel: function(){ return this.model; }, getFuel: function(){ return this.fuel; }, //其他成員 toString : function(){ console.log(this.getModel() + ' – Fuel Type:' + this.getFuel() + ' – Headed:' + this.getDestination()); } };})();var myTimeMachine = TIMEMACCHINE.createZhangsan;myTimeMachine.setModel('順豐號');myTimeMachine.setFuel('鈈');myTimeMachine.setDestination('漕河涇');myTimeMachine.toString();// 順豐號 – Fuel Type:鈈 – Headed:漕河涇 |
---|
該模塊具有一個工廠函數,它返回一個帶有public/privateAPI的對象。
六、數組
數組是一種特殊類型的對象,他作為一個有序的值的集合,這些值稱為數組元素。在一個數組內,每一個元素都具有一個索引,或稱為一個數字位置。在一個數組中可以存儲相同類型或不同類型的元素。不要求數組元素全都具有相同的數據類型。於對象和函數類似,數組也可以任意嵌套。
與普通對象一樣,可以採用兩種方式來創建數組。第一種方法就是使用Array()構造函數:
123 |
var array1 = new Array(); //空數組array1[0] = 1; //在索引為0 的位置添加一個數組元素array1[1] = 'a string'; //在索引為1的位置添加一個字符串 |
---|
更常用的是第二種方式,即使用一個數組字面量來創建數組,它由一對方括號括起來,其中包含了0個或多個一逗號分隔的值。
1 |
var niceArray = [1, 2, 3,]; |
---|
在數組字面量中混合使用對象字面量,可以提供非常強大的結構,他是JavaScript對象表示法JSON的基礎。JSON是一種流行的數據交換格式,它可以用於多種語言而不僅僅時JavaScript語言。
1234567 |
var myData = { 'root' : { 'numbers' : [1, 2, 3], 'letters' : ['a', 'b', 'c'], 'mirepoix' : ['tomatoes', 'carrots', 'potatoes'] }} |
---|
數組具有一個length屬性,它的值總是等於數組中元素的個數減11。在數組中添加新元素將改變數組length屬性。要從數組中移除元素,請使用delete操作符。
123456 |
var list = [1, '2', 'blah', {}];delete list[0];console.log(list);// [undefined, "2", "blah", Object {}]console.log(list.length);// 4 |
---|
刪除一個元素並不會改變數組的長度,只是在數組中留下一個undefined值的元素(空元素)。
七、擴展類型
JavaScript支持將方法和其他屬性綁定到內置類型。比如字符串、數值和數組類型。於其他任何對象類似,String對象也具有prototype屬性。開放人員可以為String對象擴充一些便利的方法。例如,String沒有將字符串false或true轉換為布爾值的方法。開放人員可以使用下面的代碼 為String對象添加該功能:
123456789 |
String.prototype.boolean = function(){ return "true" == this;}var t = 'true'.boolean();var f = 'false'.boolean();console.info(t); //trueconsole.info(f); //false |
---|
顯然,每次想為制定類型添加方法時反覆輸入prototype顯得有點累贅》以下提供一段簡潔的代碼,他使用一個名為method的方法擴充了Function.prototype。下面就是該方法的代碼。
1234 |
Function.prototype.method = function(name,func){ this.prototype[name] = func; return this;} |
---|
接下來就可以重寫String的boolean方法:
1234 |
String.method('boolean',function(){ return "true" == this;});"true".boolean(); |
---|
對於編寫工具方法庫並將其包含在項目中,這一技術非常有用。
八、JavaScript最佳實踐
下面列出了在進行JavaScript開發時,一些應該注意或應該避免的事項:
- 使用parseInt()函數將字符串轉換為整數,但請確保總是指定基數。更好的辦法時使用一元操作符(+)將字符串轉化為數值。 好的寫法:parseInt(「020」,10); //轉化為十進制而不是八進制 更好的寫法:console.log(+ 「010」); //簡單而又高效
- 使用等同(===)操作符來比較兩個值。避免意料之外的類型轉換。
- 在在定義對象字面量時,在最後一個值的結尾不要使用逗號。例如: 12345var o = { "p1" : 1, "p2" : 2, //非常糟糕!}
- eval()函數可以接收一個字符串,並將其視為JavaScript代碼執行。應該限制eval()函數的使用,它很容易在代碼中引入各種各樣嚴重的安全問題。
- 使用分號作為語句的結尾,當精簡代碼時這特別有用。
- 應該避免使用全局變量。應該總是使用var關鍵字來聲明變量。將代碼包裹在匿名函數中,以避免命名衝突,請使用模塊來阻止代碼。
- 對函數名使用首字母大寫表示該函數將作為new操作符的構造函數,但請不要對其他任何函數的函數名使用首字母大寫。
- 不要使用with語句。
- 在循環中創建函數應該謹慎小心。這種方式非常低效。
最佳摘自Cesar Otero,Rob Larsen《jQuery高級編程》