第二章 面向对象编程

106 阅读10分钟

面向过程与面向对象

之前所写的 3 个对输入框中输入的数据校验功能方法,用了 3 个函数,这是一种面向过程的实现方式。

然而在这种方式中,你会无端地在页面中添加很多全局变量,而且不利于别人重复使用

一旦别人使用以前提供的方法,你就不能轻易地去修改这些方法,这不利于团队代码维护。

面向对象编程就是将你的需求抽象成一个对象,然后针对这个对象分析其特性(属性)和动作(方法)。这个对象我们称之为类。

封装

创建一个类

在 JavaScript 中创建一个类很容易,首先声明一个函数保存在一个变量里。

按照编程习惯一般将这个类的变量名首字母大写

然后在这个函数(类)的内部通过对 this 变量添加属性或者方法来实现对类添加属性或方法。例如:

var Book = function(id, bookname, price) { 
    this.id = id; 
    this.bookname = bookname; 
    this.price = price; 
} 
Book.prototype.display = function() { 
    // 展示这本书 
};

我们不能直接使用这个 Book 类,需要用 new 关键字来实例化(创建)新的对象。

var book = new Book(10, 'JavaScript设计模式', 50); 
console.log(book.bookname); // JavaScript设计模式

JavaScript 是一种基于原型 prototype 的语言,所以每创建一个对象时,它都有一个原型 prototype 用于指向其继承的属性、方法。

属性和方法封装

由于 JavaScript 的函数级作用域,声明在函数内部的变量以及方法在外界是访问不到的,通过此特性即可创建类的私有变量以及私有方法。

// 私有属性与私有方法,特权方法,对象公有属性和对象共有方法,构造器
var Book = function(id, name, price) {
	// 私有属性
  var num = 1;
  // 私有方法
  function checkId() {};

  // 特权方法
  this.getName = function () {};
  this.getPrice = function () {};
  this.setName = function () {};
  this.setPrice = function () {};

  // 对象公有属性
  this.id = id
  //对象公有方法
  this.copy = function() {};
  // 构造器
  this.setName(name);
  this.setPrice(price)
}

// 静态公有属性(对象不能访问)
Book.isChinese = true;
// 静态公有方法(对象不能访问)
Book.resetTime = function() {
	console.log('new Time')
}
Book.prototype = {
	// 公有属性
  isJSBook: false,
  // 公有方法
  display: function() {}
}

注:通过 this 创建的方法,不但可以访问这些对象的共有属性与共有方法,而且还能访问到类或对象自身的私有属性和私有方法。由于这些方法权利较大,所以称为特权方法

var b = new Book(11, 'JavaScript设计模式', 50);
conosle.log(b.num); // undefined
conosle.log(b.isJSBook); // false
conosle.log(b.id); // 11
conosle.log(b.isChinese); // undefined


console.log(Book.isChinese); // true
Book.resetTime(); // new Time

通过 new 关键字创建的对象实质是对新对象 this 的不断赋值,并将 prototype 指向类的 prototype 所指的对象。新对象的 prototype 和类的 prototype 指向的是同一个对象。

闭包实现

闭包是有权访问另一个函数作用域中变量的函数,即在一个函数内部创建另一个函数。

// 利用闭包实现静态变量
var Book = (function() {
	// 静态私有变量
	var bookNum = 0;
  // 静态私有方法
  function checkBook(name) {};

  // 创建类
  function _book(newId, newName, newPrice) {
  	var name, price;
    // 私有方法
    function checkID(id) {};
    // 特权方法
    this.getName = function () {};
    this.getPrice = function () {};
    this.setName = function () {};
    this.setPrice = function () {};

    // 对象公有属性
    this.id = newId
    //对象公有方法
    this.copy = function() {};
    bookNum++;
    if (bookNum > 100) {
    	throw new Error('我们仅出版 100 本书');
    }
    // 构造器
    this.setName(name);
    this.setPrice(price);
  }
  // 构建原型
  _book.prototype = {
  	// 静态公有属性
    isJSBook: false,
    // 静态公有方法
    display: function() {}
  }
  // 返回类
  return _book;
})();

创建对象的安全模式

var Book = function(title, time, type) {
	this.title = title;
  this.time = time;
  this.type = type;
}

var book = Book('JavaScript', 2014, 'js'); // 未使用 new 关键字

console.log(book); // undefined

console.log(window.title); // JavaScript
console.log(window.time); // 2014
console.log(window.type); // js

new 关键字的作业可以看作是对当前对象的 this 不停地赋值,然而例子中没有用 new,所以会直接执行这个函数。

而这个函数在全局作用域执行了,this 指向 window,所以属性会被添加到 window 上面。

添加检查如下:

var Book = function(title, time, type) {
  // 判断执行过程中 this 是否是当前这个对象
  if (this instanceof Book) {
  	this.title = title;
    this.time = time;
    this.type = type;
  } else {
  	return new Book(title, time, type)
  }

}

测试如下:

console.log(book); // Book
console.log(book.title); // JavaScript
console.log(book.time); // 2014
console.log(book.type); // js

console.log(window.title); // undefined
console.log(window.time); // undefined
console.log(window.type); // undefined

继承

子类的原型对象——类式继承

// 声明父类
function SuperClass() {
	this.superValue = true;
}
// 为父类添加共有方法
SuperClass.prototype.getSuperValue = function() {
	return this.superValue;
}
// 声明子类
function SubClass() {
	this.subValue = false;
}

// 继承父类
SubClass.prototype = new SuperClass();
// 为子类添加共有方法
SubClass.prototype.getSubValue = function() {
	return this.subValue;
}

类的原型对象的作用就是为类的原型添加共有方法,但类不能直接访问这些属性和方法,必须通过原型 prototype 来访问。

使用如下:

var instance = new SubClass();
console.log(instance.getSuperValue()); // true
console.log(instance.getSubValue()); // false

我们还可以通过 instanceof 来检测某个对象是否为某个类的实例。

console.log(instance instanceof SuperClass); // true
console.log(instance instanceof SubClass); // true
console.log(SubClass instanceof SuperClass); // false

console.log(SubClass.prototype instanceof SuperClass); // true

类式继承的缺点:

其一,由于子类通过其原型 prototype 对父类实例化,继承了父类。因此一个子类的实例更改子类原型从父类构造函数中继承来的共有属性就会直接影响到其他子类。如下:

function SuperClass() {
	this.books = [a, b , c];
}

function SubClass() {};
SubClass.prototype = new SuperClass();
var instance1 = new SubClass();
var instance2 = new SubClass();

console.log(instance2.books); // [a, b , c]
instance1.books.push('d');
console.log(instance2.books); // [a, b , c, d]

其二,由于子类实现的继承是靠其原型 prototype 对父类的实例化实现的,因此在创建父类的时候,是无法向父类传递参数的,因而在实例化父类的时候也无法对父类构造函数的属性进行初始化。

创建即继承——构造函数继承

function SuperClass(id) {
  // 引用类型共有属性
  this.books = ['JavaScript', 'html', 'css']
  // 值类型共有属性
  this.id = id;
}
// 父类声明原型方法
SuperClass.prototype.showBooks = function() {
	console.log(this.books);
}
// 声明子类
function SubClass(id) {
	// 继承父类
  SuperClass.call(this, id);
}
// 创建第一个子类的实例
var instance1 = new SubClass(10);
// 创建第二个子类的实例
var instance2 = new SubClass(11);

instance1.books.push('设计模式');
console.log(instance1.books); // ['JavaScript', 'html', 'css', '设计模式']
console.log(instance1.id); // 10
console.log(instance2.books); // ['JavaScript', 'html', 'css']
console.log(instance2.id); // 11

instance1.showBookss(); // TypeError

注意这里,SuperClass.call(this, id); 这条语句是构造函数式的精华

在子类中,对 SuperClass 调用这个方法就是将子类中的变量在父类中执行一遍,由于父类中是给 this 绑定属性的,因此子类自然也就继承了父类的共有属性。

将优点为我所用——组合式继承

类式继承是通过子类的原型prototype对父类实例化来实现的,构造函数式继承是通过在子类的构造函数作用环境中执行一次父类的构造函数来实现的,所以只要在继承中同时做到这两点即可,看下面的代码。

// 声明父类
function SuperClass(name){
  // 值类型共有属性
  this.name = name;
  // 引用类型共有属性
  this.books = ["html", "css", "JavaScript"];
}
// 父类原型共有方法
SuperClass.prototype.getName = function(){
  console.log(this.name);
};
// 声明子类
function SubClass(name, time){
  // 构造函数式继承父类name属性
  SuperClass.call(this, name);
  // 子类中新增共有属性
  this.time = time;
}
// 类式继承 子类原型继承父类
SubClass.prototype = new SuperClass();
// 子类原型方法
SubClass.prototype.getTIme = function(){
  console.log(this.time);
};

测试用例如下:

var instance1 = new SubClass("js book", 2014);
instance1.books.push("设计模式");
console.log(instance1.books);   // ["html", "css", "JavaScript", "设计模式"]
instance1.getName();       // js book
instance1.getTime();       // 2014

var instance2 = new SubClass("css book", 2013);
console.log(instance2.books);   // ["html", "css", "JavaScript"]
instance2.getName();       // css book
instance2.getTime();       // 2013

因为我们在使用构造函数继承时执行了一遍父类的构造函数,而在实现子类原型的类式继承时又调用了一遍父类构造函数。因此父类构造函数调用了两遍,所以这还不是最完美的方式。

洁净的继承者——原型式继承

2006 年道格拉斯·克罗克福德(Douglas Crockford)发表了一篇《JavaScript 中原型式继承》的文章。

Douglas Crockford是Web开发领域最知名的技术权威之一,曾任Yahoo! 资深JavaScript架构师,现任PayPal高级JavaScript架构师。

他的观点是,借助原型 prototype 可以根据已有的对象创建一个新的对象,同时不必创建新的自定义对象类型。

// 原型是继承
function inheritObject(o){
  // 声明一个过渡函数对象
  function F(){}
  // 过渡对象的原型继承父对象
  F.prototype = o;
  // 返回过渡对象的一个实例,该实例的原型继承了父对象
  return new F();
}

它是对类式继承的一个封装,其实其中的过渡对象就相当于类式继承中的子类,只不过在原型式中作为一个过渡对象出现的,目的是为了创建要返回的新的实例化对象。

测试代码如下:

var book = {
  name: "js book",
  alikeBook: ["css book", "html book"]
};

var newBook = inheritObject(book);
newBook.name = "ajax book";
newBook.alikeBook.push("xml book");

var otherBook = inheritObject(book);
otherBook.name = "flash book";
otherBook.alikeBook.push("as book");

console.log(newBook.name);      //ajax book
console.log(newBook.alikeBook);    //["css book", "html book", "xml book", "as book"]
console.log(otherBook.name);    //flash book
console.log(otherBook.alikeBook);  //["css book", "html book", "xml book", "as book"]
console.log(book.name);       //js book
console.log(book.alikeBook);     //["css book", "html book", "xml book", "as book"]

跟类式继承一样,父类对象book中的值类型的属性被复制,引用类型的属性被共用。

如虎添翼——寄生式继承

// 寄生式继承
// 声明基对象
var book = {
  name: "js book",
  alikeBook: ["css book", "html book"]
};
function createBook(obj){
  // 通过原型继承方式创建新对象
  var o = new inheritObject(obj);
  // 拓展新对象
  o.getName = function(){
    console.log(name);
  };
  // 返回拓展后的新对象
  return o;
}

其实寄生式继承就是对原型继承的第二次封装,并且在这第二次封装过程中对继承的对象进行了拓展,这样新创建的对象不仅仅有父类中的属性和方法而且还添加新的属性和方法。

终极继承者——寄生组合式继承

“嗯,之前我们学习了组合式继承,那时候我们将类式继承同构造函数继承组合使用,但是这种方式有一个问题,就是子类不是父类的实例,而子类的原型是父类的实例,所以才有了寄生组合式继承。但是你知道是哪两种模式的组合么?”

“寄生当然是寄生式继承,寄生式继承依托于原型继承,原型继承又与类式继承相像,另外一种就不应该是这些模式了,所以另外一种继承模式应该是构造函数继承了吧。当然,子类不是父类实例的问题是由于类式继承引起的。”

/**
 * 寄生式继承 继承原型
 * 传递参数 subClass  子类
 * 传递参数 superClass 父类
 **/
function inheritPrototype(subClass, superClass){
  // 复制一份父类的原型副本保存在变量中
  var p = inheritObject(superClass.prototype);
  // 修正因为重写子类原型导致子类的constructor属性被修改
  p.constructor = subClass;
  // 设置子类的原型
  subClass.prototype = p;
}

测试用例如下:

// 定义父类
function SuperClass(name){
  this.name = name;
  this.colors = ["red", "blue", "green"];
}
// 定义父类原型方法
SuperClass.prototype.getName = function(){
  console.log(this.name);
};
// 定义子类
function SubClass(name, time){
  // 构造函数式继承
  SuperClass.call(this, name);
  // 子类新增属性
  this.time = time;
}
// 寄生式继承父类原型
inheritPrototype(SubClass, SuperClass);
// 子类新增原型方法
SubClass.prototype.getTime = function(){
  console.log(this.time);
};
// 创建两个测试方法
var instance1 = new SubClass("js book", 2014);
var instance2 = new SubClass("css book", 2013);

instance1.colors.push("black");
console.log(instance1.colors);   //["red", "blue", "green", "black"]
console.log(instance2.colors);   //["red", "blue", "green"]
instance2.getName();         //css book
instance2.getTime();          //2013

“现在你明白了吧,其实这种方式继承如上所示,其中最大的改变就是对子类原型的处理,被赋予父类原型的一个引用,这是一个对象,因此这里有一点你要注意,就是子类再想添加原型方法必须通过 prototype 对象,通过点语法的形式一个一个添加方法了,否则直接赋予对象就会覆盖掉从父类原型继承的对象了。”

多态

多态,就是同一个方法多种调用方式

function add() {
	// 获取参数
  var arg = arguments, length = arg.length;
  switch(length) {
    case 0: // 没有参数
      return 10;
    case 1: // 1个参数
      return 10 + arg[0];
    case 2: // 2个参数
      return arg[0] + arg[1];
  }
}

// 测试用例
console.log(add()); // 10
console.log(add(5)); // 15
console.log(add(6, 7)); // 13