JavaScript面向对象编程
随着HTML5标准的对象成熟和在移动开发领域的大规模使用,JavaScript正成为Web开发领域最热门的编程开发语言,而且随着NodeJS等技术的对象发展,JavaScript的编程应用也从传统前端开发领域延伸到了服务器端开发。但同时需要注意的对象是,我们项目中的编程JavaScript代码规模也变得越来越大和更加复杂。这就要求开发人员能够编写高效且可维护的对象JavaScript代码,虽然JavaScript不像Java那样对面向对象设计有那么好的编程支持,但我们可以通过在JavaScript中应用这些面向对象的对象设计模式,来使我们写出更优秀的编程JavaScript代码。
在这篇教程中,对象你将学习基于JavaScript的编程面向对象编程。其中的对象代码示例是基于EcmaScript 5(JavaScript的标准定义)来实现。
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视图中,构造器,对象实例成员,网站模板静态成员和内部函数都能被识别,并在Outline视图中显示出来。
实例成员 - Instance Members
通过"new"关键字可以创建一个实例对象,而实例成员(变量或方法)能够通过这个实例对象来访问。实例成员可以通过"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静态成员 - Static Members
JavaScript中并不直接支持静态成员。你可以通过构造器来创建静态成员。静态成员不允许通过"this"关键字直接访问。
公共静态成员
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();私有静态成员
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同样没有对接口的直接支持。你可以通过下面代码中实现的机制来定义接口。
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
如果你希望在全局范围内只创建一个某一类型的示例,那么你可以有下面两种方式来实现一个单例。云服务器
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"); //创建对象 - Object Creation
通过new关键字创建
可以使用"new"关键字来创建内置类型或用户自定义类型的实例对象,它会先创建一个空的实例对象,然后再调用构造函数来给这个对象的成员变量赋值,从而实现对象的初始化。
//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通过字面量直接创建
通过字面量创建对象非常简单和直接,同时你还可以创建嵌套对象。
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中没有对私有字段的直接支持,但你可以通过构造器来实现它。首先将变量在构造函数中定义为私有的,任何需要使用到这个私有字段的方法都需要定义在构造函数中,这样你就可以通过这些共有方法来访问这个私有变量了。
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
私有方法也被称作内部函数,往往被定义在构造体中,从外部无法直接访问它们。
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如果私有内部函数被实例化并被构造函数返回,那么它将可以从外部被调用。
function Outer(){ return new Inner(); //private inner function Inner(){ this.sayHello = function(){ console.log("Hello"); } } } (new Outer()).sayHello(); // Hello特权方法 - Privileged Methods
原型方法中的一切都必须是公共的,因此它无法调用类型定义中的私有变量。通过在构造函数中使用"this."声明的函数称为特权方法,它们能够访问私有字段,并且可以从外部调用。香港云服务器
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
原型方法是公共的,所有与之关联的对象或方法也都是公共的。
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中实现继承。其中"原型继承"——使用原型机制实现继承的方法,是最常用的。如下面示例:
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当一个成员字段或函数被访问时,会首先搜索这个对象自身的成员。如果没有找到,那么会搜索这个对象对应的原型对象。如果在原型对象中仍然没有找到,那么会在它的父对象中查找成员和原型。这个继承关系也被成为 "原型链"。下面这张图就反映了原型链的继承关系。
模块化 - Modularization
当我们的项目中,自定义的对象类型越来越多时,我们需要更有效地组织和管理这些类定义,并控制他们的可见性,相互依赖关系以及加载顺序。"命名空间"和"模块"能够帮助我们很好地解决这个问题。(EcmaScript 6已经实现了模块系统,但因它还没有被所有浏览器实现,此处我们仍以ES5为例来进行说明)
命名空间 - Namespaces
JavaScript中并没有命名空间的概念。我们需要通过对象来创建命名空间,并将我们定义的对象类型放入其中。
//create namespacevar myLib = { }; myLib.myPackage = { };//Register types to namespacemyLib.myPackage.MyType1 = MyType1; myLib.myPackage.MyType2 = MyType2;模块 - Modules
模块被用来将我们的JavaScript代码分解到包中。模块可以引用其他模块或将自己定义的对象类型对外暴露,以供其他模块使用。同时它能够用来管理模块间的依赖关系,并按照我们指定的顺序进行加载。目前有一些第三方库可以用来实现模块的管理。
下面的例子中,我们在模块里定义新的类型,并且引用其他模块并将自身的公共类型对外暴露。
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。它们会在运行时被创建和抛出。所有的异常都是"unchecked"。一个普通的对象也可以被用作一个异常,并在throw语句中抛出。因此,我们可以创建自己定义的异常对象,并且在程序中捕获它们进行处理。一个异常处理的***实践是,扩展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【本文是专栏作者“陈逸鹤”的原创文章,如需转载请联系作者本人(微信公众号:techmask)】
戳这里,看该作者更多好文