JavaScript面向對象編程
隨著HTML5標準的成熟和在移動開發(fā)領域的大規(guī)模使用,JavaScript正成為Web開發(fā)領域最熱門的開發(fā)語言,而且隨著NodeJS等技術的發(fā)展,JavaScript的應用也從傳統(tǒng)前端開發(fā)領域延伸到了服務器端開發(fā)。但同時需要注意的是,我們項目中的JavaScript代碼規(guī)模也變得越來越大和更加復雜。這就要求開發(fā)人員能夠編寫高效且可維護的JavaScript代碼,雖然JavaScript不像Java那樣對面向對象設計有那么好的支持,但我們可以通過在JavaScript中應用這些面向對象的設計模式,來使我們寫出更優(yōu)秀的JavaScript代碼。
在這篇教程中,你將學習基于JavaScript的面向對象編程。其中的代碼示例是基于EcmaScript 5(JavaScript的標準定義)來實現(xiàn)。
Java與JavaScript的比對
對象類型定義- Object Type
- function MyType(){ if (!(this instanceof MyType)) throw new Error("Constructor can’t be called as a function");
- }var myInstance = new MyType();
- MyType(); // Error: Constructor can’t be called as a function
在Eclipse的JavaScript視圖中,構造器,實例成員,靜態(tài)成員和內部函數(shù)都能被識別,并在Outline視圖中顯示出來。
實例成員 - Instance Members
通過"new"關鍵字可以創(chuàng)建一個實例對象,而實例成員(變量或方法)能夠通過這個實例對象來訪問。實例成員可以通過"this"關鍵字,原型(prototype),構造器或Object.defineProperty來定義。
- function Cat(name){ var voice = "Meow"; this.name = name; this.say = function(){ return voice;
- }
- }
- Cat.prototype.eat = function(){ return "Eating";
- }var cat = new Cat("Fluffy");Object.defineProperty(cat, "numLegs",{value: 4,writable:true,enumerable:true,configurable:tr
- ue});console.log(cat.name); // Fluffyconsole.log(cat.numLegs); // 4console.log(cat.say()); // Meowconsole.log(cat.eat()); // Eating
靜態(tài)成員 - Static Members
JavaScript中并不直接支持靜態(tài)成員。你可以通過構造器來創(chuàng)建靜態(tài)成員。靜態(tài)成員不允許通過"this"關鍵字直接訪問。
公共靜態(tài)成員
- function Factory(){
- }// public static methodFactory.getType = function (){ return "Object Factory";
- };// public static fieldFactory.versionId = "F2.0";
- Factory.prototype.test = function(){ console.log(this.versionId); // undefined
- console.log(Factory.versionId); // F2.0
- console.log(Factory.getType()); // Object Factory}var factory = new Factory();
- factory.test();
私有靜態(tài)成員
- var Book = (function () { // private static field
- var numOfBooks = 0; // private static method
- function checkIsbn(isbn) { if (isbn.length != 10 && isbn.length != 13) throw new Error("isbn is not valid!");
- } function Book(isbn, title) {
- checkIsbn(isbn); this.isbn = isbn; this.title = title;
- numOfBooks++; this.getNumOfBooks = function () { return numOfBooks;
- }
- } return Book;
- })();var firstBook = new Book("0-943396-04-2", "First Title");console.log(firstBook.title); // First Titleconsole.log(firstBook.getNumOfBooks()); // 1var secondBook = new Book("0-85131-041-9", "Second Title");console.log(firstBook.title); // First Titleconsole.log(secondBook.title); // Second Titleconsole.log(firstBook.getNumOfBooks()); // 2console.log(secondBook.getNumOfBooks()); // 2
抽象類型 - Abstract Types
JavaScript是一個弱類型語言,所以當你聲明一個變量時,不需要指定它的類型。這就減弱了對于像接口這樣的抽象類型的依賴。但有時候,你仍然希望使用抽象類型來將一些共有的功能放在一起,并采用繼承的機制,讓其他類型也具有相同的功能,你可以參考下面的示例:
- (function(){ var abstractCreateLock = false; // abstract type
- function BaseForm(){ if(abstractCreateLock) throw new Error("Can’t instantiate BaseForm!");
- }
- BaseForm.prototype = {};
- BaseForm.prototype.post = function(){ throw new Error("Not implemented!");
- } function GridForm(){
- }
- GridForm.prototype = new BaseForm();
- abstractCreateLock = true;
- GridForm.prototype.post = function(){ // ...
- return "Grid is posted.";
- } window.BaseForm = BaseForm; window.GridForm = GridForm;
- })();var myGrid = new GridForm();console.log(myGrid.post()); // Grid is posted.var myForm = new BaseForm(); // Error: Can’t instantiate BaseForm!
接口 - Interfaces
JavaScript同樣沒有對接口的直接支持。你可以通過下面代碼中實現(xiàn)的機制來定義接口。
- var Interface = function (name, methods) { this.name = name; // copies array
- this.methods = methods.slice(0);
- };
- Interface.checkImplements = function (obj, interfaceObj) { for (var i = 0; i < interfaceObj.methods.length; i++) { var method = interfaceObj.methods[i]; if (!obj[method] || typeof obj[method] !=="function")
- thrownewError("Interfacenotimplemented! Interface: " + interfaceObj.name + " Method: " + method);
- }
- };var iMaterial = new Interface("IMaterial", ["getName", "getPrice"]);function Product(name,price,type){
- Interface.checkImplements(this, iMaterial); this.name = name; this.price = price; this.type = type;
- }
- Product.prototype.getName = function(){ return this.name;
- };
- Product.prototype.getPrice = function(){ return this.price;
- };var firstCar = new Product("Super Car X11",20000,"Car");console.log(firstCar.getName()); // Super Car X11delete Product.prototype.getPrice;var secondCar = new Product("Super Car X12",30000,"Car"); // Error: Interface not implemented!
單例對象 - Singleton Object
如果你希望在全局范圍內只創(chuàng)建一個某一類型的示例,那么你可以有下面兩種方式來實現(xiàn)一個單例。
- var Logger = {
- enabled:true,
- log: function(logText){ if(!this.enabled) return; if(console && console.log) console.log(logText); else
- alert(logText);
- }
- }
- 或者
- function Logger(){
- }
- Logger.enabled = true;
- Logger.log = function(logText){ if(!Logger.enabled) return; if(console && console.log) console.log(logText); else
- alert(logText);
- };
- Logger.log("test"); // testLogger.enabled = false;
- Logger.log("test"); //
創(chuàng)建對象 - Object Creation
通過new關鍵字創(chuàng)建
可以使用"new"關鍵字來創(chuàng)建內置類型或用戶自定義類型的實例對象,它會先創(chuàng)建一個空的實例對象,然后再調用構造函數(shù)來給這個對象的成員變量賦值,從而實現(xiàn)對象的初始化。
- //or var dog = {};//or var dog = new MyDogType();var dog = new Object();
- dog.name = "Scooby";
- dog.owner = {};
- dog.owner.name = "Mike";
- dog.bark = function(){ return "Woof";
- };console.log(dog.name); // Scoobyconsole.log(dog.owner.name); // Mikeconsole.log(dog.bark()); // Woof
通過字面量直接創(chuàng)建
通過字面量創(chuàng)建對象非常簡單和直接,同時你還可以創(chuàng)建嵌套對象。
- var dog = {
- name:"Scoobyî",
- owner:{
- name:"Mike"
- },
- bark:function(){ return "Woof";
- }
- };console.log(dog.name); // Scoobyconsole.log(dog.owner.name); // Mikeconsole.log(dog.bark()); // Woof
成員作用域 - Scoping
私有字段 - Private Fields
在JavaScript中沒有對私有字段的直接支持,但你可以通過構造器來實現(xiàn)它。首先將變量在構造函數(shù)中定義為私有的,任何需要使用到這個私有字段的方法都需要定義在構造函數(shù)中,這樣你就可以通過這些共有方法來訪問這個私有變量了。
- function Customer(){ // private field
- var risk = 0; this.getRisk = function(){ return risk;
- }; this.setRisk = function(newRisk){
- risk = newRisk;
- }; this.checkRisk = function(){ if(risk > 1000) return "Risk Warning"; return "No Risk";
- };
- }
- Customer.prototype.addOrder = function(orderAmount){ this.setRisk(orderAmount + this.getRisk()); return this.getRisk();
- };var customer = new Customer();console.log(customer.getRisk()); // 0console.log(customer.addOrder(2000)); // 2000console.log(customer.checkRisk()); // Risk Warning
私有方法 - Private Methods
私有方法也被稱作內部函數(shù),往往被定義在構造體中,從外部無法直接訪問它們。
- function Customer(name){ var that = this; var risk = 0; this.name = name; this.type = findType(); // private method
- function findType() { console.log(that.name); console.log(risk); return "GOLD";
- }
- }
或者
- function Customer(name){ var that = this; var risk = 0; this.name = name; // private method
- var findType = function() { console.log(that.name); console.log(risk); return "GOLD";
- }; this.type = findType();
- }var customer = new Customer("ABC Customer"); // ABC Customer
- // 0console.log(customer.type); // GOLDconsole.log(customer.risk); // undefined
如果私有內部函數(shù)被實例化并被構造函數(shù)返回,那么它將可以從外部被調用。
- function Outer(){ return new Inner(); //private inner
- function Inner(){ this.sayHello = function(){ console.log("Hello");
- }
- }
- }
- (new Outer()).sayHello(); // Hello
特權方法 - Privileged Methods
原型方法中的一切都必須是公共的,因此它無法調用類型定義中的私有變量。通過在構造函數(shù)中使用"this."聲明的函數(shù)稱為特權方法,它們能夠訪問私有字段,并且可以從外部調用。
- function Customer(orderAmount){ // private field
- var cost = orderAmount / 2; this.orderAmount = orderAmount; var that = this; // privileged method
- this.calculateProfit = function(){ return that.orderAmount - cost;
- };
- }
- Customer.prototype.report = function(){ console.log(this.calculateProfit());
- };var customer = new Customer(3000);
- customer.report(); // 1500
公共字段 - Public Fields
公共字段能夠被原型或實例對象訪問。原型字段和方法被所有實例對象共享(原型對象本身也是被共享的)。當實例對象改變它的某一個字段的值時,并不會改變其他對象中該字段的值,只有直接使用原型對象修改字段,才會影響到所有實例對象中該字段的值。
- function Customer(name,orderAmount){ // public fields
- this.name = name; this.orderAmount = orderAmount;
- }
- Customer.prototype.type = "NORMAL";
- Customer.prototype.report = function(){ console.log(this.name); console.log(this.orderAmount); console.log(this.type); console.log(this.country);
- };
- Customer.prototype.promoteType = function(){ this.type = "SILVER";
- };var customer1 = new Customer("Customer 1",10);// public fieldcustomer1.country = "A Country";
- customer1.report(); // Customer 1
- // 10
- // NORMAL
- // A Countryvar customer2 = new Customer("Customer 2",20);
- customer2.promoteType();console.log(customer2.type); // SILVERconsole.log(customer1.type); // NORMAL
公共方法 - Public Methods
原型方法是公共的,所有與之關聯(lián)的對象或方法也都是公共的。
- function Customer(){ // public method
- this.shipOrder = function(shipAmount){ return shipAmount;
- };
- }// public methodCustomer.prototype.addOrder = function (orderAmount) { var totalOrder = 0; for(var i = 0; i < arguments.length; i++) {
- totalOrder += arguments[i];
- } return totalOrder;
- };var customer = new Customer();// public methodcustomer.findType = function(){ return "NORMAL";
- };console.log(customer.addOrder(25,75)); // 100console.log(customer.shipOrder(50)); // 50console.log(customer.findType()); // NORMAL
繼承 - Inheritance
有幾種方法可以在JavaScript中實現(xiàn)繼承。其中"原型繼承"——使用原型機制實現(xiàn)繼承的方法,是最常用的。如下面示例:
- function Parent(){ var parentPrivate = "parent private data"; var that = this; this.parentMethodForPrivate = function(){ return parentPrivate;
- }; console.log("parent");
- }
- Parent.prototype = {
- parentData: "parent data",
- parentMethod: function(arg){ return "parent method";
- },
- overrideMethod: function(arg){ return arg + " overriden parent method";
- }
- }function Child(){ // super constructor is not called, we have to invoke it
- Parent.call(this); console.log(this.parentData); var that = this; this.parentPrivate = function(){ return that.parentMethodForPrivate();
- }; console.log("child");
- }//inheritanceChild.prototype = new Parent();// parentChild.prototype.constructor = Child;//lets add extented functionsChild.prototype.extensionMethod = function(){ return "child’s " + this.parentData;
- };//override inherited functionsChild.prototype.overrideMethod = function(){ //parent’s method is called
- return "Invoking from child" + Parent.prototype.
- overrideMethod.call(this, " test");
- };var child = new Child();// parent// parent data
- // childconsole.log(child.extensionMethod()); //child’s parent dataconsole.log(child.parentData); //parent dataconsole.log(child.parentMethod()); //parent methodconsole.log(child.overrideMethod()); //Invoking from child testoverriden parent methodconsole.log(child.parentPrivate()); // parent private dataconsole.log(child instanceof Parent); //trueconsole.log(child instanceof Child); //true
當一個成員字段或函數(shù)被訪問時,會首先搜索這個對象自身的成員。如果沒有找到,那么會搜索這個對象對應的原型對象。如果在原型對象中仍然沒有找到,那么會在它的父對象中查找成員和原型。這個繼承關系也被成為 "原型鏈"。下面這張圖就反映了原型鏈的繼承關系。
模塊化 - Modularization
當我們的項目中,自定義的對象類型越來越多時,我們需要更有效地組織和管理這些類定義,并控制他們的可見性,相互依賴關系以及加載順序。"命名空間"和"模塊"能夠幫助我們很好地解決這個問題。(EcmaScript 6已經(jīng)實現(xiàn)了模塊系統(tǒng),但因它還沒有被所有瀏覽器實現(xiàn),此處我們仍以ES5為例來進行說明)
命名空間 - Namespaces
JavaScript中并沒有命名空間的概念。我們需要通過對象來創(chuàng)建命名空間,并將我們定義的對象類型放入其中。
- //create namespacevar myLib = {};
- myLib.myPackage = {};//Register types to namespacemyLib.myPackage.MyType1 = MyType1;
- myLib.myPackage.MyType2 = MyType2;
模塊 - Modules
模塊被用來將我們的JavaScript代碼分解到包中。模塊可以引用其他模塊或將自己定義的對象類型對外暴露,以供其他模塊使用。同時它能夠用來管理模塊間的依賴關系,并按照我們指定的順序進行加載。目前有一些第三方庫可以用來實現(xiàn)模塊的管理。
下面的例子中,我們在模塊里定義新的類型,并且引用其他模塊并將自身的公共類型對外暴露。
- Module.define("module1.js",
- ["dependent_module1.js","dependent_module2.js",...], function(dependentMod1, dependentMod2) {//IMPORTS
- //TYPE DEFINITIONS
- function ExportedType1(){ // use of dependent module’s types
- var dependentType = new dependentMod1.DependentType1();
- ...
- } function ExportedType2(){
- }
- ... // EXPORTS
- return { ExportedType1: ExportedType1, ExportedType2:ExportedType2,...};
- });//To use a module (can work asynchronously or synchronously):Module.use(["module1.js"], function(aModule){
- console.log("Loaded aModule!"); var AType = aModule.AnExportedType; var atype1Instance = new AType();
- });
自定義異常 - Custom Exceptions
JavaScript中有一些內部定義的異常,如Error、TypeError和SyntaxError。它們會在運行時被創(chuàng)建和拋出。所有的異常都是"unchecked"。一個普通的對象也可以被用作一個異常,并在throw語句中拋出。因此,我們可以創(chuàng)建自己定義的異常對象,并且在程序中捕獲它們進行處理。一個異常處理的***實踐是,擴展JavaScript中標準的Error對象。
- function BaseException() {}
- BaseException.prototype = new Error();
- BaseException.prototype.constructor = BaseException;
- BaseException.prototype.toString = function() { // note that name and message are properties of Error
- return this.name + ":"+this.message;
- };function NegativeNumberException(value) { this.name = "NegativeNumberException"; this.message = "Negative number!Value: "+value;
- }
- NegativeNumberException.prototype = new BaseException();
- NegativeNumberException.prototype.constructor = NegativeNumberException;function EmptyInputException() { this.name = "EmptyInputException"; this.message = "Empty input!";
- }
- EmptyInputException.prototype = new BaseException();
- EmptyInputException.prototype.constructor = EmptyInputException;var InputValidator = (function() { var InputValidator = {};
- InputValidator.validate = function(data) { var validations = [validateNotNegative, validateNotEmpty]; for (var i = 0; i < validations.length; i++) { try {
- validations[i](data);
- } catch (e) { if (e instanceof NegativeNumberException) { //re-throw
- throw e;
- } else if (e instanceof EmptyInputException) { // tolerate it
- data = "0";
- }
- }
- }
- }; return InputValidator; function validateNotNegative(data) { if (data < 0) throw new NegativeNumberException(data)
- } function validateNotEmpty(data) { if (data == "" || data.trim() == "") throw new EmptyInputException();
- }
- })();try {
- InputValidator.validate("-1");
- } catch (e) { console.log(e.toString()); // NegativeNumberException:Negative number!Value: -1
- console.log("Validation is done."); // Validation is done.}
自定義事件 - Custom Events
自定義事件能夠幫助我們減小代碼的復雜度,并且有效地進行對象之間的解耦。下面是一個典型的自定義事件應用模式:
- function EventManager() {}var listeners = {};
- EventManager.fireEvent = function(eventName, eventProperties) { if (!listeners[eventName]) return; for (var i = 0; i < listeners[eventName].length; i++) {
- listeners[eventName][i](eventProperties);
- }
- };
- EventManager.addListener = function(eventName, callback) { if (!listeners[eventName])
- listeners[eventName] = [];
- listeners[eventName].push(callback);
- };
- EventManager.removeListener = function(eventName, callback) { if (!listeners[eventName]) return; for (var i = 0; i < listeners[eventName].length; i++) { if (listeners[eventName][i] == callback) { delete listeners[eventName][i]; return;
- }
- }
- };
- EventManager.addListener("popupSelected", function(props) { console.log("Invoked popupSelected event: "+props.itemID);
- });
- EventManager.fireEvent("popupSelected", {
- itemID: "100"}); //Invoked popupSelected event: 100
【本文是51CTO專欄作者“陳逸鶴”的原創(chuàng)文章,如需轉載請聯(lián)系作者本人(微信公眾號:techmask)】