【简明教程】JavaScript 面向对象编程

1,558 阅读10分钟
原文链接: www.jianshu.com

随着HTML5标准的成熟和在移动开发领域的大规模使用,JavaScript正成为Web开发领域最热门的开发语言,而且随着NodeJS等技术的发展,JavaScript的应用也从传统前端开发领域延伸到了服务器端开发。但同时需要注意的是,我们项目中的JavaScript代码规模也变得越来越大和更加复杂。这就要求开发人员能够编写高效且可维护的JavaScript代码,虽然JavaScript不像Java那样对面向对象设计有那么好的支持,但我们可以通过在JavaScript中应用这些面向对象的设计模式,来使我们写出更优秀的JavaScript代码。

在这篇教程中,你将学习基于JavaScript的面向对象编程。其中的代码示例仍然基于EcmaScript 5(JavaScript的标准定义)来实现。

Java与JavaScript的比对

Java JavaScript
静态类型 动态类型
使用类,接口和枚举来定义类型 使用函数和原型来定义类型
在运行时类型无法改变 类型可以在运行时变更
需要给所有变量声明类型(强类型校验) 声明变量时不需要指定类型 (弱类型校验)
构造器是特殊的方法 构造器也是一个函数,与其他函数没有区别
类和对象是不同的实体 包括构造器,函数原型在内的一切都是对象
支持静态方法和实例 不直接支持静态方法和实例
通过抽象类和接口支持抽象类型 不直接支持抽象类型
通过private,package,protected,public定义对象的作用域 只支持public成员
提供丰富的继承机制 通过原型实现继承
支持方法级的重写和重载机制 不直接支持重写和重载
提供丰富的反射特性 具有一些反射特性
通过包提供模块化支持 没有直接的模块化支持

对象类型定义- 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); // Fluffy
console.log(cat.numLegs); // 4
console.log(cat.say()); // Meow
console.log(cat.eat()); // Eating

静态成员 - Static Members

JavaScript中并不直接支持静态成员。你可以通过构造器来创建静态成员。静态成员不允许通过"this"关键字直接访问。

公共静态成员

function Factory(){
}

// public static method
Factory.getType = function (){
    return "Object Factory";
};

// public static field
Factory.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 Title
console.log(firstBook.getNumOfBooks()); // 1

var secondBook = new Book("0-85131-041-9", "Second Title");
console.log(firstBook.title); // First Title
console.log(secondBook.title); // Second Title
console.log(firstBook.getNumOfBooks()); // 2
console.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 X11
delete 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"); // test
Logger.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); // Scooby
console.log(dog.owner.name); // Mike
console.log(dog.bark()); // Woof

通过字面量直接创建

通过字面量创建对象非常简单和直接,同时你还可以创建嵌套对象。

var dog = {
  name:"Scoobyî",
  owner:{
    name:"Mike"
  },
  bark:function(){
    return "Woof";
  }
};
console.log(dog.name); // Scooby
console.log(dog.owner.name); // Mike
console.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()); // 0
console.log(customer.addOrder(2000)); // 2000
console.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
 // 0
console.log(customer.type); // GOLD
console.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 field
customer1.country = "A Country";
customer1.report(); // Customer 1
                     // 10
                     // NORMAL
                     // A Country

var customer2 = new Customer("Customer 2",20);
customer2.promoteType();
console.log(customer2.type); // SILVER
console.log(customer1.type); // NORMAL

公共方法 - Public Methods

原型方法是公共的,所有与之关联的对象或方法也都是公共的。

function Customer(){
  // public method
  this.shipOrder = function(shipAmount){
     return shipAmount;
  };
}

// public method
Customer.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 method
customer.findType = function(){
   return "NORMAL";
};

console.log(customer.addOrder(25,75)); // 100
console.log(customer.shipOrder(50)); // 50
console.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");
}

//inheritance
Child.prototype = new Parent();// parent
Child.prototype.constructor = Child;

//lets add extented functions
Child.prototype.extensionMethod = function(){
  return "child’s " + this.parentData;
};

//override inherited functions
Child.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
 // child
console.log(child.extensionMethod()); //child’s parent data
console.log(child.parentData); //parent data
console.log(child.parentMethod()); //parent method
console.log(child.overrideMethod()); //Invoking from child test
overriden parent method
console.log(child.parentPrivate()); // parent private data
console.log(child instanceof Parent); //true
console.log(child instanceof Child); //true

当一个成员字段或函数被访问时,会首先搜索这个对象自身的成员。如果没有找到,那么会搜索这个对象对应的原型对象。如果在原型对象中仍然没有找到,那么会在它的父对象中查找成员和原型。这个继承关系也被成为 "原型链"。下面这张图就反映了原型链的继承关系。


模块化 - Modularization

当我们的项目中,自定义的对象类型越来越多时,我们需要更有效地组织和管理这些类定义,并控制他们的可见性,相互依赖关系以及加载顺序。"命名空间"和"模块"能够帮助我们很好地解决这个问题。(EcmaScript 6已经实现了模块系统,但因它还没有被所有浏览器实现,此处我们仍以ES5为例来进行说明)

命名空间 - Namespaces

JavaScript中并没有命名空间的概念。我们需要通过对象来创建命名空间,并将我们定义的对象类型放入其中。

//create namespace
var myLib = {};
myLib.myPackage = {};
//Register types to namespace
myLib.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

编写组件 - Components

JavaScriipt允许开发人员通过编写组件来向HTML元素添加行为。下面是一个典型的应用场景,将JavaScript对象绑定到了DOM元素上。

1-定义JavaScript组件

function InputTextNumberComponent(domElement) {
  this.initialize(domElement);
}

InputTextNumberComponent.prototype.initialize =
  function(domElement) {
    domElement.onchange = function() {
      //just a format
      domElement.value = "-" +domElement.value + " -";
    };
    domElement.jsComponent = this; //Expando property
    this.domElement = domElement;
  };
InputTextNumberComponent.prototype.resetValue = function() {
  this.domElement.value = "";
};

2-定义一个CSS样式用于与JavaScript组件写作控制HTML元素


  .inputTextNumber { text-align:right; }

HTML元素的定义如下

2-当页面加载时(或DOM元素都已准备就绪),检测HTML元素并创建JavaScript组件

window.onload = function() {
  var inputTextNumbers = document.getElementsByClassName("inputTextNumber");
  for (var i = 0; i < inputTextNumbers.length; i++) {
    var myComp = new InputTextNumberComponent(
      inputTextNumbers.item(i));
  }
};

稀土掘金联合编辑:技匠,以上内容欢迎大家分享到朋友圈/微博等,但未经授权,请勿转载。谢谢大家!