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高級編程》