我們常說Javascript是一種面向對象的語言,那也就是說具有面向對象的一些基本特性。比如包含對象、類、屬性、方法以及構造函數等基本元素,很多人在想:JS類到底是什么玩意?其實很簡單,就是一個function,正所謂"簡單就是美"嘛。在自定義類的同時,我們也回顧一下JS基本的類:Math,Array,Object以及String等。
//定義JS類的兩種方式(注意這里是大寫開頭) function EmailMessage() { } var EmailMessage = function() { }
有類就有對象存在,同時構造函數也應運而生。常常在構造函數中使用this.**來訪問屬於當前對象的屬性或方法。對於由同一個類生成的多個對象之間是松耦合的,相互獨立。
//當創建對象時會觸發構造函數(這里無參) var EmailMessage = function() { alert("New message created."); } var myMessage = new EmailMessage(); // 輸出 "New message created." var anotherMessage = new EmailMessage(); // 輸出 "New message created."
當你想傳遞參數給對象時,就會使用帶參構造函數。
//帶參構造函數 var EmailMessage = function(message) { alert(message); } // 輸出 "Return to sender" var myMessage = new EmailMessage("Return to sender");
剛才講過,可以使用this來訪問屬性,其實在JS中還有一種方式來添加屬性: 利用prototype。不僅能添加屬性,還能添加方法。
//使用this來訪問當前對象的屬性和方法 var EmailMessage = function(subject) { this.subject = subject; this.send = function() { alert("Message '" + this.subject + "' sent!"); } } var myMessage = new EmailMessage("Check this out..."); var anotherMessage = new EmailMessage("Have you seen this before?"); //輸出屬性和方法 alert(myMessage.subject); // 輸出 "Check this out..." alert(anotherMessage.subject); // 輸出 "Have you seen this before?" myMessage.send();// 輸出 "Message 'Check this out...' sent!" anotherMessage.send();// 輸出 "Message 'Have you seen this before?' sent!"
利用prototype來添加屬性和方法(主要適合於對已有類進行功能擴展)。
var EmailMessage = function(subject) { this.subject = subject; } //使用prototype來添加方法 EmailMessage.prototype.send = function() { alert("Message '" + this.subject + "' sent!"); } var myMessage = new EmailMessage("Check this out..."); var anotherMessage = new EmailMessage("Have you seen this before?"); //輸出屬性和方法 alert(myMessage.subject); // 輸出 "Check this out..." alert(anotherMessage.subject); // 輸出 "Have you seen this before?" myMessage.send();// 輸出 "Message 'Check this out...' sent!" anotherMessage.send();// 輸出 "Message 'Have you seen this before?' sent!"
通常有朋友在講:要是想JS類只有一個對象存在時,即常說的單例模式如何實現呢?其實JS的內置類就包含了很多單例,如:Math等。在這里,我給出兩種實現方式。
var User = function() { this.username = ""; this.password = ""; this.login = function() { return true; } } // 創建User類的對象並存儲為相同的對象實例,而原始的類將被移除(可以看成起了個別名而已) User = new User(); // 使用單例對象來訪問對應的方法(類似於C#的靜態方法) User.login();
另外一種實現方式就是"自我初始化",即在類聲明時就立即執行,而該類就只包含一個對象。
var Inbox = new function() { this.messageCount = 0; this.refresh = function() { return true; } }(); //聲明就立即執行 Inbox.refresh();
接下來,我談一下在已有類的基礎上添加新的功能以實現擴展,我們稱之為"繼承"。注意以下代碼的高亮片段。
var EmailMessage = function(subject) { this.subject = subject; this.send = function() { alert("Message '" + this.subject + "' sent!"); } } // 創建一個新的空類 var EventInvitation = function() {}; // 繼承已有類EmailMessage的屬性和方法 EventInvitation.prototype = new EmailMessage(); // EventInvitation將構造函數設置為自身 EventInvitation.prototype.constructor = EventInvitation; // 重置設置已有的屬性subject EventInvitation.prototype.subject = "You are cordially invited to..."; // 創建EventInvitation的對象 var myEventInvitation = new EventInvitation(); // 輸出 "Message 'You are cordially invited to...' sent!" myEventInvitation.send();
在繼承已有類的同時,子類便獲取了父類的所有屬性和方法,自身只需要定義額外與自己相關的屬性和方法。我們來解釋一下封裝與多態的概念:所有封裝,即每個類只關注與自身相關的屬性和方法。所謂多態,即子類在包含父類同名屬性或方法時,而又不想繼承來自父類的同名元素,則可以重新定義該元素(如subject)。
var EmailMessage = function(subject) { this.subject = subject; this.send = function() { alert("Email message sent!"); } } // 繼承EmailMessage var EventInvitation = function() {}; EventInvitation.prototype = new EmailMessage("You are cordially invited to..."); EventInvitation.prototype.constructor = EventInvitation; // 重寫send方法 EventInvitation.prototype.send = function() { alert("Event invitation sent!"); } var myEmailMessage = new EmailMessage("A new email coming your way."); var myEventInvitation = new EventInvitation(); myEmailMessage.send(); // 輸出 "Email message sent!" myEventInvitation.send(); // 輸出 "Event invitation sent!"
現在假設子類需要重寫父類的同時,又需要調用父類的方法,我們來看怎么實現。
var EmailMessage = function(subject) { this.subject = subject; this.send = function() { alert("Email message sent!"); } } // 繼承EmailMessage var EventInvitation = function() {}; EventInvitation.prototype = new EmailMessage("You are cordially invited to..."); EventInvitation.constructor.prototype = EventInvitation; // 重寫send方法 EventInvitation.prototype.send = function() { alert("Event invitation sent!"); // 使用this.constructor.prototype來指向父類並執行同名方法 this.constructor.prototype.send.call(this); } var myEmailMessage = new EmailMessage("A new email coming your way."); var myEventInvitation = new EventInvitation(); myEmailMessage.send();// 輸出 "Email message sent!" myEventInvitation.send();// 輸出 "Event invitation sent!"、"Email message sent!"
從以上的講解中,我們不難發現this的大量應用,可能很多朋友也知道this代表當前執行的對象實例。我這里詳細解釋一下this的普遍意義和用法。給一段很簡單的代碼,大家試着想一想結果是什么?主要是搞清楚此時this到底代表什么?
var showSubject = function() { alert(this.subject); } showSubject();// 輸出 "undefined"
剛才提過,this代表當前執行的實例對象,可是現在並沒有像之前的代碼先定義一個類,然后定義對象,在使用this代表這個定義的對象。准確的解釋應該是: this代表其所在的作用域內類自身或正在執行的對象,若this超出類的作用域則代表全局對象window對象。故此時應該輸出undefined。接下來我們把這個function移植到已有類EmailMessage上。
var showSubject = function() { alert(this.subject); } showSubject();// 輸出"undefined" this.subject = "Global subject";// 設置全局屬性 showSubject();// 輸出 "Global subject" // 定義EmailMessage類 var EmailMessage = function(subject) { this.subject = subject; } // 將showSubject添加到EmailMessage,注意這里showSubject不含() EmailMessage.prototype.showSubject = showSubject; var myEmailMessage = new EmailMessage("I am the subject."); myEmailMessage.showSubject();// 輸出 "I am the subject.",因為現在this變成myEmailMessage showSubject();// 輸出"Global subject",因為此時this仍然為window EmailMessage.prototype.outputSubject = function() { //現在為EmailMessage添加新方法outputSubject來調用showSubject showSubject(); } myEmailMessage.outputSubject();// 輸出 "Global subject.",因為盡管添加了新方法,但this仍然是window
如果希望能強制切換當前引用的對象this,有兩種方法: call、apply。兩者的區別很小,前者傳遞的是參數列表,后者傳遞的是參數數組。
var showSubject = function() { alert(this.subject); } var setSubjectAndFrom = function(subject, from) { this.subject = subject; this.from = from; } //this代表全局對象window showSubject(); // 輸出"undefined" setSubjectAndFrom("Global subject", "miracle@cnblogs.com"); showSubject(); // Outputs "Global subject" //定義EmailMessage類 var EmailMessage = function() { this.subject = ""; this.from = ""; }; var myEmailMessage = new EmailMessage(); //call或apply將this從全局對象window切換到myEmailMessage setSubjectAndFrom.call(myEmailMessage, "New subject", "miracle@sina.com"); setSubjectAndFrom.apply(myEmailMessage, [ "New subject", "miracle@sina.com" ]); showSubject.call(myEmailMessage);// 輸出"New subject"
到此為止,我們所定義的屬性和方法,在類外部都能訪問。如果我們希望能將一些屬性和方法僅供類內部使用,即所謂的private變量,我們改如何實現呢?
var EmailMessage = function(subject) { // 公有的屬性和方法 this.subject = subject; this.send = function() { alert("Message sent!"); } // 私有的屬性和方法(使用var而不是this) var messageHeaders = ""; var addEncryption = function() { return true; } // 特權的屬性和方法(對外開放讀取接口但不能修改,類似於只讀) var messageSize = 1024; this.getMessageSize = function() { alert(messageSize); } }
接下來我們(在類外)開始使用這些變量,看看他們的表現如何?
var myEmailMessage = new EmailMessage("Save these dates..."); alert(myEmailMessage.subject); // 輸出 "Save these dates..." myEmailMessage.send(); // 輸出 "Message sent!" // 輸出"undefined"因為messageHeaders是私有屬性 alert(myEmailMessage.messageHeaders); // addEncryption()是私有方法,外部不能訪問因此拋出異常 try { myEmailMessage.addEncryption(); } catch (e) { alert("Method does not exist publicly!"); } // 輸出"undefined"因為messageSize是私有屬性 alert(myEmailMessage.messageSize); // 輸出"1024",特權屬性可通過方法在外部訪問 myEmailMessage.getMessageSize();
通過以上的學習,大家對面向對象的基礎知識點已經有所了解了把。接下來,我在最后簡單聊一下關於"對象字面量(Object Literal)"的知識。常聽別人談起這個概念,那對象字面量到底是什么呢?簡單的說:就是將一系列屬性和方法組合起來的集合體,可以用來創建單一對象(Singleton),創建類,設置函數輸入參數等。下面我來一一講解。首先,對象字面量是一個變量,然后將所有的屬性和方法以"鍵值對"的方式全部包含在{}中,這跟后面的系列JSON數據組織格式很相似。
var earth = { name: "Terra Firma", // 字符串 planet: true, // 布爾變量 moons: 1, // 整數 diameter: 12756.36, // 小數 oceans: ["Atlantic", "Pacific", "Indian", "Arctic", "Antarctic"], // 數組 poles: { // 嵌套對象字面量 north: "Arctic", south: "Antarctic" }, setDiameter: function(diameter) { // 函數 this.diameter = diameter; // 此時this代表earth } } // 注意:此處不再聲明 alert(earth.diameter); // 輸出 "12756.36" earth.setDiameter(12756.37); alert(earth.diameter); // 輸出 "12756.37"
同樣對象字面量也能創建類(將對象字面量映射到類的prototype上)。
var EmailMessage = function() {}; EmailMessage.prototype = { subject: "", from: "", send: function() { alert("Message sent!"); } } var myEmailMessage = new EmailMessage(); myEmailMessage.subject = "Come over for a party.." myEmailMessage.send(); // 輸出"Message sent!"
那對象字面量咋作為函數的輸入參數呢?很簡單,當我們有時傳遞的輸入參數過多時,而這些參數之間又彼此關聯時,我們不妨用對象字面量來作為輸入參數。
// 使用多個輸入參數 var sendEmail = function(to, from, subject, body) { alert("Message '" + subject + "' from '" + from + "' sent to '" + to + "'!"); } // 調用必須按照順序 sendEmail("miracle@cnblogs.com", "miracle.he@cnblogs.com", "Dinner this week?", ? "Do you want to come over for dinner this week? Let me know."); // 用對象字面量來作為輸入參數(將4個合成為1個) var sendEmail = function(message) { alert("Message '" + message.subject + "' from '" + message.from + "' sent to '" + message.to + "'!"); } // 此時調用不再區分順序,只要將對象字面量的屬性賦值即可 sendEmail({ from: 'miracle@cnblogs.com', to: 'miracle.he@cnblogs.com', subject: 'Dinner this week?', body: 'Do you want to come over for dinner this week? Let me know.' });
是不是覺得調用起來更加靈活和方便呢?再次回到上面的話題:變量作用域,其實這個在大家日常的實際項目經驗中應該更加引以重視?我們說了window在全局作用域中均有效,但是我們的JS程序如果過多或不當使用全局變量,導致全局作用域內存在很多全局變量,將使應用程序的安全性面了巨大挑戰,對於有些有心機的黑客來說,隨意讓別人獲取全局變量並做惡意的修改,將導致應用程序的崩潰。那如何才能有效避免呢?我的建議:可以采用私有變量對我們的私密數據加以保護,其次還可以采用命名空間來建立模塊層次以達到合理的保護。
// 這里MyCompany已經成為一個Singleton var MyCompany = new function(){ this.MyClient = { WebMail: function() { alert("Creating WebMail application..."); } }; }(); // 輸出 "Creating WebMail application..." var myWebMail = new MyCompany.MyClient.WebMail();
到此為止,關於JS面向對象的知識點就介紹到這里,以后的系列還將講解Javascript的性能調優以及測試框架的搭建。