前端学起来,简单入门面向对象编程

200 阅读6分钟

面向对象编程

1、两种编程风格——面向过程与面向对象

面向过程

例如,有个输入表单,分别输入手机号、姓名和密码。若要对它们进行输入校验。你可能会创建三个函数checkPhone、checkName、checkPassword分别实现校验功能。这是一个面向过程的实现方式,然而在这种方式中,你会发现无端地在页面中添加了很多全局变量,而且不利于别人重复使用。

面向对象

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

2、面向对象编程思想特点之一——包装

2.1 创建一个类

var Book = function(id, name, price) {
  this.id = id;
  this.name = name;
  this.price = price;
}
Book.prototype = {
  display: function() {
    console.log(`id为${this.id},书名为${this.name},价格为${this.price}元`);
  }
}

var newBook = new Book(101, 'js', 100);
newBook.display(); // id为101,书名为js,价格为100元

var otherBook = new Book(102, 'html', 50);
otherBook.display(); // id为102,书名为html,价格为50元

console.log(newBook.display === otherBook.display); // true

通过prototype继承的属性或者方法是每个对象通过prototype访问到,所以我们每次通过类创建一个新对象时这些属性和方法不会再次创建。

2.2 属性与方法封装

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.reset = function() {};

Book.prototype = {
  // 公有属性
  isJSBook: false,
  // 公有方法
  display: function() {}
}

2.3 将类的静态变量通过闭包实现

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

var Book = (function() {
  // 静态私有变量
  var bookNum = 0;
  // 静态私有方法
  function checkBook(name) {};
  
  // 创建类
  function _book(id, name, price) {
    // 私有属性
    var time = 0, history = [];
    // 私有方法
    function addTime(name) {
      time++;
      history.push(name);
    };
  
    // 特权方法
    this.getName = function() {};
    this.getPrice = function() {};
    this.setName = function(name) {
      addTime(name);
      this.name = name;
    };
    this.setPrice = function(price) {
      this.price = price;
    };
    this.getHistory = function() {
      return history;
    };
  
    // 对象公有属性
    this.id = id;
    // 对象公有方法
    this.copy = function() {};
    
    bookNum++;
    if (bookNum > 2) {
      throw new Error('仅生产2本书!');
    }
  
    // 构造器
    this.setName(name);
    this.setPrice(price);
	}

  // 类静态公有属性(对象不能访问)
  _book.isChinese = true;
  // 类静态公有方法(对象不能访问)
  _book.reset = function() {
    bookNum = 0;
  };

  // 构建原型
  _book.prototype = {
    // 静态公有属性
    isJSBook: false,
    // 静态公有方法
    display: function() {
      console.log(`id为${this.id},书名为${this.name},价格为${this.price}元`);
    }
  }
  
  // 返回类
  return _book;
})()

var book1 = new Book(101, 'js', 100);
book1.setName('js1');
book1.getHistory(); // ["js", "js1"]

var book2 = new Book(102, 'html', 50);
var book3 = new Book(103, 'css', 40); // Uncaught Error: 仅生产2本书!

Book.reset();
var newBook1 = new Book(104, 'node', 150); // id为104,书名为node,价格为150元

3、面向对象编程思想特点之一——继承

3.1 类式继承

function Father() {
  this.name = 'web';
  this.books = ['js', 'html'];
}
Father.prototype.getBooks = function() {
  return this.books;
}

function Child(cost) {
  this.cost = cost;
}
Child.prototype = new Father();
Child.prototype.getCost = function() {
  return this.cost;
}

var instance = new Child(100);
instance.getCost(); // 100
instance.getBooks(); // ["js", "html"]

console.log(instance instanceof Child); // true
console.log(instance instanceof Father); // true
console.log(Child instanceof Father); // false
console.log(Child.prototype instanceof Father); // true

instanceof是判断前面的对象是否是后面类(对象)的实例

instanceof原则就是前面的对象沿着隐式原型__proto__一直往下找,如果匹配到了后面类(对象)的显式原型prototype,那么返回true,否则返回false

类式继承

类式继承的缺点

  1. 由于子类通过其原型prototype对父类实例化,继承了父类。所以说父类中的共有属性要是引用类型,就会在子类中被所有实例共用,因此一个子类的实例更改子类实例从父类构造函数中继承来的共有属性就会直接影响其他子类,例如
var instance1 = new Child(100);
var instance2 = new Child(50);
console.log(instace2.books); //  ["js", "html"]
instance1.books.push('css');
console.log(instace2.books); //  ["js", "html", 'css'];
  1. 由于子类实现的继承是靠其原型prototype对父类的实例化实现的,因此在创建父类的时候,是无法向父类传递参数的,因而在实例化父类的时候也无法对父类构造函数内的属性进行初始化。
var instance1 = new Child(100);
var instance2 = new Child(50);
console.log(instace1.name, instace1.books); //  'web' ["js", "html"]
console.log(instace2.name, instace2.books); //  'web' ["js", "html"]
console.log(instace1.books === instace2.books); // true

可以看出,实例化的instance1和instance2中继承下来的属性name和books都是相同的,不能够刚开始实例化时对这两个属性进行自定义。

3.2 构造函数继承

构造函数继承就是来解决上述两个问题的。

function Father(name, books) {
  this.name = name;
  this.books = books;
}
Father.prototype.getBooks = function() {
  return this.books;
}

function Child(cost, name, books) {
  Father.call(this, name, books);
  /* 相当于执行了这两行代码
  * this.name = name;
  * this.books = books;
  */
  
  this.cost = cost;
}

var instance1 = new Child(100, 'web', ['js', 'html']);
var instance2 = new Child(50, 'ios', ['Objective-C']);
console.log(instance1.books === instance2.books); // false

但是好像还有问题,实例没有继承到Father的原型方法getBooks。因此引出了下面的组合继承。

3.3 组合继承

即构造函数继承与类式继承组合。

function Father(name, books) {
  this.name = name;
  this.books = books;
}
Father.prototype.getBooks = function() {
  return this.books;
}

function Child(cost, name, books) {
  Father.call(this, name, books);
  
  this.cost = cost;
}

Child.prototype = new Father();
Child.prototype.getCost = function() {
  return this.cost;
}
var instance1 = new Child(100, 'web', ['js', 'html']);
var instance2 = new Child(50, 'ios', ['Objective-C']);

console.log(instance1.getBooks()); // ["js", "html"]
console.log(instance2.getBooks()); // ["Objective-C"]

融合了类式继承和构造函数继承之后,取其精华,去其糟粕。

但是这种继承方案还不够完美,代码上可以看到,我们执行了Father.call(this, name, books);Child.prototype = new Father();,父类构造函数调用了两遍。而且实例原型多了用不上的属性。

conso.log(instance1);
{
  books: (2) ["js", "html"]
  cost: 100
  name: "web"
  __proto__: {
      books: undefined,
      name: undefined, // 这两个属性多余的
      getCost: ƒ (),
      ...
  }
}

3.4 原型式继承

function inheritObject(o) {
  function F() {};
  F.prototype = o;
  return new F();
}
var book = {
  name: 'web',
  books: ['js', 'html'],
  getBooks: function() {
    return this.books;
  }
}
var instance1 = inheritObject(book);
var instance2 = inheritObject(book);

console.log(instance2.books); // ['js', 'html']
instance1.books.push('node');
console.log(instance2.books); // ['js', 'html', 'node']
console.log(object.books); // ['js', 'html', 'node']

这种方式和类式继承一样,父类对象中的值类型的属性被复制,引用类型的属性被共用。

3.5 寄生式继承

var book = {
  name: 'web',
  books: ['js', 'html'],
  getBooks: function() {
    return this.books;
  }
}
function createBook(obj) {
  // 通过原型继承方式创建对象
  var o = inheritObject(obj);
  
  // 扩展新对象
  o.getName = function() {
    return this.name;
  }
  
  return o;
}

var instance1 = createBook(book);

3.6 寄生组合式继承

之前说组合继承的时候,实例的__proto__属性有多余用不上的属性。而寄生组合式继承可以解决这一问题,减少不必要的开销。组合继承与寄生组合式继承代码非常相像,只需改一处代码。

function Father(name, books) {
  this.name = name;
  this.books = books;
}
Father.prototype.getBooks = function() {
  return this.books;
}

function Child(cost, name, books) {
  Father.call(this, name, books);
  
  this.cost = cost;
}

// Child.prototype = new Father();

// ---寄生式继承-----------------
function inheritPrototype(child, father) {
  // 声明一个过渡函数对象
  function F() {};
	F.prototype = father.prototype;

	var p = new F();
  // p的constructor指向了father,修改为指向child
	p.constructor = child;
	child.prototype = p;
}

inheritPrototype(Child, Father);
// ---------------------

Child.prototype.getCost = function() {
  return this.cost;
}

var instance1 = new Child(100, 'web', ['js', 'html']);
var instance2 = new Child(50, 'ios', ['Objective-C']);

console.log(instance1.getBooks()); // ["js", "html"]
console.log(instance2.getBooks()); // ["Objective-C"]

conso.log(instance1);
{
  books: (2) ["js", "html"]
  cost: 100
  name: "web"
  __proto__: {
      getCost: ƒ (),
      // 没有了多余的属性
      ...
  }
}

可以看出,过渡函数对象F是一个纯净的对象,它只是作为一个中介,让Child能够继承Father的原型prototype里的方法,并没有执行Father的构造函数,开销比较少。

其实当时看到这里,有些许疑问,执行过渡函数对象F来替换Father,好像没有多大区别。仔细想想其实不然,如果项目应用足够大,Father有足够多的代码处理的话,这时就能够体现出寄生式继承的优势,因为可以完全避免执行Father构造函数里面的代码。

function Father(name, books) {
  	this.name = name;
  	this.books = books;
  
  	if (name === 'js') {
      this.difficultyLevel = 'hard';
    } else if (name === 'html') {
      this.difficultyLevel = 'easy';
    } else {
      // 其他
    }
  
  	// 更多处理
}

在这里插入图片描述

4、面向对象编程思想特点之一——多继承

4.1 单继承

function extend(target, source) {
  for (let property in source) {
    target[property] = source[property];
  }
  return target;
}

extend方法的实现就是对对象中的属性的一个复制过程(浅复制)

4.3 多继承

Object.prototype.mix = function () {
  var i = 0,
      len = arguments.length,
      arg;
  
  for (; i < len; i++) {
    arg = arguments[i];
    for (var property in arg) {
      this[property] = arg[property];
    }
  }
}

5、面向对象编程思想特点之一——多态

ECMAScript函数不能像传统意义上那样实现重载。而在其他语言(如Java)中,可以为一个函数编写两个定义,只要这两个定义的签名(接受的参数类型和数量)不同即可。ECMAScript函数没有签名,因为其参数是由包含零或多个值的数组来表示的。而没有函数签名,真正的重载就不能够实现。但可以通过对传入函数中参数的数量和类型进行检查并做不同的处理,来模仿重载。

function add10() {

	var arg = arguments,

			len = arg.length;

	switch(len) {
		case 0:
			return 10;
		case 1:
			return 10 + arg[0];
		case 2:
			return arg[0] + arg[1];
	}

}

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

本篇文章是看了《JavaScript设计模式》第一篇后,自己做的一些笔记和总结思考。看了该书作者张容铭的简介后,不得不佩服这是真大佬。

表情图

他2012年年底去百度实习,2015年出版了这本书。那岂不是还没有3年工作经验就有能力出书了。再次敬佩作者,我常常感到很菜而与大佬们格格不入。