JavaScript与面向对象程序设计的实现

 2016年06月27日    207     声明


JavaScript是一种基于对象的语言,基于对象编程被认为是面向对象编程的子集。JavaScript支持面向对象编程,并提供了强大灵活的 OOP 语言能力。本文将探讨JavaScript中使用基于对象原型编程的方式,实现面向对象编程中的一些类似的概念。

  1. 1. 面向对象与基于对象
  2. 2. JavaScript对面向对象编程的实现

1. 面向对象与基于对象

1.1 面向对象编程

面向对象编程(Object-oriented programming,简写:OOP)是一种用抽象的方式创建现实世界中对象模型的编程模式。对象指的是类的实例,它面向对象编程设计将对象作为程序的基本单元,并将程序和数据封装其中,以提高软件的重用性、灵活性和扩展性。

面向对象编程是通过一系列对象及对象的相互协作实现软件功能的软件设计方式。在传统面向过程的编程思想中,一个程序只是一些函数的集合,或简单的计算机指令列表;在面向对象中,每个对象能够接收消息,处理数据和发送消息给其他对象,每个对象都可以看作是一个拥有清晰角色的、具有独立功能的逻辑处理单元。

面向对象程序设计的目的在于提高程序的灵活性和可维护性,凭借其对模块化的重视,面向对象的代码开发更加简单。


面向对象中的概念

在面向对象编程中有一些重要的概念(术语),这些概念是面向对象思想的重要体现。如,借助命名空间实现模块化与功能的分离、利用继承实现代码重用和功能的扩展特性。

面向对象中的一些概念简单介绍如下:

Namespace - 命名空间

命名空间是一系列类的合集,其主要目的是避免类和方法重名问题。允许开发人员在一个独特, 应用相关的名字的名称下捆绑所有功能的容器。

Class - 类

定义了一个事物的抽象特点。类的定义包含了数据的形式以及对数据的操作,也可以认为它是对象的属性和方法的模板定义。

Object - 对象

对象是一个类的实例,可以认为其是现实世界中具体事务的抽像。

Property - 属性

属性是对对象特征的描述,如:颜色

Method - 方法

方法是对对象行为能力的描述,如:行走。

Constructor - 构造函数

对象被初始化时调用的方法,一般会在构造函数中进行一些对象的初始化设置。构造函数名通常与类名一致。

Abstraction - 抽像

抽像是是简化复杂的现实问题的途径,通过对对象属性、方法的继承,模拟出现实对象的模型并找到最合适的类定义。

Inheritance - 继承

一个类(子类)可以继承另一个类(父类)的特征,并添加一些自有的特征。

Encapsulation - 封装

封装把一系列数据和相关的方法绑定在一起使用的方法,并通过接口提供只有特定类的对象可以访问的方法。

Polymorphism - 多态

多态是指由继承而产生的相关的不同的类,其对象对同一消息会做出不同的响应。

继承、封装、多态是面向对象中的三大主要特征。


1.2 原型编程

基于原型的编程(prototype-based programming)又被称为原型程序设计、原型编程,它是面向对象编程的子系统和一种方式。基于原型的编程不是面向对象编程中体现的风格,其行为重用(在基于类的语言中也称为继承)是通过装饰它作为原型的现有对象实现的。这种模式也被称为弱类化原型化、或基于实例的编程

在基于类的语言中,提倡使用一个关注分类和类之间关系开发模型。与此相对,原型编程更提倡关注一系列对象实例的行为,而后才关心如何将这些对象划分到最近的使用方式相似的原型对象,而不是分成类。因此,很多基于原型的系统提倡运行时原型的修改,而只有极少数基于类的面向对象系统(如:Smalltalk)允许类在程序运行时被修改。


2. JavaScript对面向对象编程的实现

2.1 命名空间

命名空间是一个容器,它允许开发人员在一个独特的,特定于应用程序名称下捆绑所有的功能。在JavaScript中,命名空间是一个包含方法,属性,对象的对象

创建一个JavaScript命名空间很简单:一个全局对象创建后,所有的变量,方法等都会成为该对象的属性。在浏览器环境中,创建的对象默认是全局对象,使用命名空间可以最大程度地减少应用程序中的命名冲突。

如,创建一个名为 ITBILU 的全局对象:

// 全局命名空间
var ITBILU = ITBILU || {};

在上面操作中,我们首先检查是否已全局定义全局对象 ITBILU。如果是,则使用已定义的对象,否则新建一个全局对象,用于封装变量、方法、对象等。

在全局对象中,还可以创建子对象,即:子命名空间:

// 子命名空间
ITBILU.event = {};

创建命名空间后,就可以在命名空间或子命名空间中添加变量、方法、函数等:

// 给普通方法和属性创建一个ITBILU.commonMethod的容器
ITBILU.commonMethod = {
  regExForName: "",  // 定义名称的正则验证规则
  regExForPhone: "", // 定义电话的正则验证规则
  validateName: function(name){
    // 对名字的操作,可以使用 this.regExForname 
    // 访问regExForName变量
  },
  validatePhoneNo: function(phoneNo){
    // 对电话号码的操作
  }
}

// 对象(子命名空间)和方法一起声明
ITBILU.event = {
  addListener: function(el, type, fn) {
    // 代码
  },
  removeListener: function(el, type, fn) {
    // 代码
  },
  getEvent: function(e) {
    // 代码
  }

  // 还可以添加其他的属性和方法
}

//使用addListner方法的写法:
ITBILU.event.addListener("yourel", "type", callback);


2.2 类

JavaScript是一种基于原型的语言,它没有其它静态语言(如:Java和C++)中的class类声明关键字。在JavaScript中,声明类和声明一个函数一样:

function Person() { } 
// 或使用函数表达式
var Person = function(){ }

在上面操作中,我们创建了一个名为Person类,同时该函数还是一构造函数。

注意:在ES6(ECMAScript 2015)标准中,新增了class(类语句和类表达式),但只是一种对象模板的形式,其本质上还是原型。


2.3 对象 - 类实例

JavaScript创建类实例与其它面向对象语言类似,创建新实例的语法格式为:new obj

如,创建上面定义的Person实例:

function Person() { }
var person1 = new Person();
var person2 = new Person();

这样就创建了两对象(Person实例):person1、person2。


2.4 构造函数(构造器)

构造函数是指在类实例化时被调用的函数,一般与类名相同。 在JavaScript,函数名(类名)就可以作为构造函数使用,每个声明的函数都可以在实例化时被调用执行。构造函数常用于给对象的属性赋值或者为调用函数做准备。

如,在Person类的构造函数中加一个日志打印操作:

function Person() {
  console.log('Person初始化完成');
}

var person1 = new Person();
var person2 = new Person();


2.5 属性 - 对象属性

属性就是类中包含的变量,每一个对象实例同样可以有多个属性。不同于Java等静态面向对象语言,为了正确的继承,原型继承中的属性应该被定义在类的原型属性 (函数)中。

在JavaScript中,this是对当前对象的引用,可以使用this关键字调用类中的属性,而在实例中读/写其属性的语法:InstanceName.Property

下面,我们为Person类增加一个属性:

function Person(firstName) {
  this.firstName = firstName;
  console.log('Person初始化完成');
}

var person1 = new Person('王二小');
var person2 = new Person('张大牛');
// 在实例中访问属性
console.log(person1.firstName);  // '王二小''
console.log(person2.firstName);  // '张大牛'


2.6 方法 - 对象方法

方法与属性很相似,不同的是方法以函数的形式定义。在JavaScript中,定义一个实例方法需要将其添加到原型链上,即:prototype属性上。prototype属性是继承自Object,所有JavaScript对象中都存在。

如,Person类添加一个sayName属性:

function Person(firstName) {
  this.firstName = firstName;
  console.log('Person初始化完成');
}
// 定义实例方法
Person.prototype.sayName = function() {
  console.log(this.firstName);
};

var person1 = new Person('王二小');
var person2 = new Person('张大牛');
// 在实例中调用方法
person1.sayName();  // '王二小''
person2.sayName();  // '张大牛'

在JavaScript中方法是一个绑定到对象中的普通函数,这意味着方法可以在其上下文之外被调用。示例如下:

function Person(firstName) {
  this.firstName = firstName;
  console.log('Person初始化完成');
}

Person.prototype.sayName = function() {
  console.log(this.firstName);
};

var person1 = new Person('王二小');
var helloFunction = person1.sayName;
person1.sayName();   // 王二小
helloFunction()      // undefined,严格模式下 TypeError
helloFunction.call(person1);  // undefined
console.log(helloFunction === person1.sayName); // true

callFunction类中的方法,通过call(或apply)方法可以指定对象的作用域上调用函数。


2.7 继承

通过一个或多个类创建另一个新类的方式叫做继承,但Javascript中只支持单继承。被创建的类叫做子类,用于创建类的类叫做父类(或基类、超类)。

在Javascript中,继承通过赋予子类一个父类的实例来实现。在支持ES5标准的运行环境中,Object.create实现继承:

如,我们可以像下面这样创建一个继承自Person的子类Student,并为其添加一些自有属性:

// 定义Person构造器
function Person(firstName) {
  this.firstName = firstName;
}

// 在Person.prototype中加入方法
Person.prototype.walk = function(){
  console.log("我会走");
};
Person.prototype.sayName = function(){
  console.log("我是 " + this.firstName);
};

// 定义Student构造器
function Student(firstName, subject) {
  // 调用父类构造器, 确保"this"在调用过程中设置正确
  Person.call(this, firstName);

  // 初始化Student类的特有属性
  this.subject = subject;
};

/* 
 通过Object.create方法将Person.prototype继承到Student.prototype
 注意: 常见的错误是使用"new Person()"来创建Student.prototype
 这样做的并不正确,最重要的一点是我们在实例化时
 不能赋予Person类任何的firstName参数。
 调用Person的正确位置如下,我们从Student中来调用它
*/
Student.prototype = Object.create(Person.prototype);

// 设置"constructor" 属性指向Student
Student.prototype.constructor = Student;

// 替换 sayName 方法
Student.prototype.sayName = function(){
  console.log("我是 " + this.firstName + "。我在学 " + this.subject);
};

// 加入"sayGoodBye" 方法
Student.prototype.sayGoodBye = function(){
  console.log("Goodbye!");
};

// 测试实例:
var student1 = new Student("王二小", "小学数学");
student1.sayName();    // 我是 王二小。我在学 小学数学
student1.walk();       // 我会走
student1.sayGoodBye(); // Goodbye!

// 检查实例是否正常
console.log(student1 instanceof Person);  // true 
console.log(student1 instanceof Student); // true

对于不支持Object.create方法的运行环境,可以使用以下补丁:

function createObject(proto) {
  function ctor() { }
  ctor.prototype = proto;
  return new ctor();
}

// 使用
Student.prototype = createObject(Person.prototype);


2.8 封装

在上面示例中,Student类并不知道父类中的walk()方法是如何实现的,但仍然可以使用它。对Student类来说,这就叫做封装。而子类并不需要继承所有方法,或方法实现不一至,就可以在子类中对方法进行改写。


2.9 多态

就像所有定义在原型属性内部的方法和属性一样,不同的类可以定义具有相同名称的方法,方法只作用于所在的类中,并且这仅在两个类不是父子关系时成立,这就是多态关系。

如,定义一个同样继承自PersonWorker类,并定义一个sayGoodBye方法,该方法与Student类中的实现并不一样:

function Worker(firstName, subject) {
  Person.call(this, firstName);
};

Worker.prototype = Object.create(Person.prototype);
Worker.prototype.constructor = Worker;
// 加入"sayGoodBye" 方法
Worker.prototype.sayGoodBye = function(){
  console.log("我是个工人,Goodbye!");
};


2.10 抽像

抽象是工作问题中通用部分进行建模的一种机制,并通过继承(具体化)或组合来实现复杂问题的简单化。 JavaScript通过继承实现具体化,并通过指定类的实例为其他对象的属性值来实现对象的组合。

如,在JavaScriptFunction类继承自Object类,这是典型的具体化。而Function.prototype的属性是一个Object实例,这是典型的组合。

var foo = function(){};
console.log(foo instanceof Function); // true
cpnsole.log(foo.prototype instanceof Object); // true