JavaScript设计模式之面向对象编程

此篇总结与《JavaScript设计模式》github地址 YOU-SHOULD-KNOW-JSjavascript

封装

建立一个类

在JavaScript中建立一个对象很容易,首先声明一个函数保存在一个变量里。按编程习惯通常将这个变量名的首字母大写。而后内部经过this变量来添加属性或者方法来实现对类添加属性和行为。css

var Book = function(id,bookname,price) {
  this.id = id;
  this.bookename = bookname;
  this.price = price;
}
复制代码

固然,咱们也能够经过在类的原型上添加属性和方法。有两种方式:html

Book.prototype.display = function() {
  //展现展现这本书
}

//或者
Book.prototype = {
    display:function() {
      //展现这本书
    }
}
复制代码

这样,咱们就将咱们所须要的方法和属性都封装到咱们封装的Book类里面了,当使用这些功能和方法的时候,咱们不能直接使用这些类,而是须要使用关键字new来实例化新的对象。java

var book = new Book(10,'JavaScript设计模式',20);
console.log(book.bookname);
复制代码

注意,经过this添加的属性和方法是在当前对象上添加的,然而JavaScript是一种基于原型的语言,因此每建立一个对象时,他都有一个prototype用于指向其继承的属性、方法。这样经过prototype继承的方法并非对象自身的,因此在使用这些方法时,须要经过prototype一层一层往上查找。node

简单的说,经过this定的属性和方法是该对象自身拥有的,因此咱们每次经过类建立的一个新对象时,this执行的属性和方法都会获得相应的建立,而经过prototype继承的属性和方法是每个对象经过prototype访问到的。因此咱们每一次经过类建立一个新对象时,这些属性和方法不会再次建立。react

如上图,实例的__proto__属性执行原型。原型的constructor属性指向构造函数。当建立一个函数或者对象时都会为其建立一个原型对象prototype,在prototype对象中,又会像函数中建立this同样建立一个constructor属性,这个属性指向的就是拥有整个原型对象的函数或者对象。

属性与方法封装

因为JavaScript是函数级做用域,申明在函数内部的变量或者方法在外部是访问不到的,经过此特性便可建立类的私有变量和私有方法。然而在函数内部经过this建立的属性和方法,在类建立对象时,每一个对象自身都拥有一份,而且在外部访问到。所以用this建立的属性能够看作是对象的共有属性和共有方法。而经过this建立的方法不但能够访问这些对象的共有属性和共有方法,还能够访问类自身的私有属性和私有方法,这权利比较大,因此咱们称之为特权方法。 在建立对象时,咱们可使用这些特权方法来初始化实例对象的一些属性,所以这些在建立对象时,调用的特权方法能够看作是类的构造器jquery

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);
}
复制代码

经过new关键字建立新对象时,因为类外面经过点语法添加的属性和方法没有执行到,因此新建立的对象中没法获取他们,当时能够经过类来使用。所以咱们称之为静态共有属性和静态共有方法。而经过类的prototype建立的属性和方法在类的实例中能够经过this访问到的(新建立对象的__ptoto__指向类的原型所指的对象),因此咱们将prototype中的属性和方法称之为共有属性和方法git

//静态的共有属性和方法,对象不能访问
Book.isChinese = true;
Book.setTime = function() {
  console.log('new time');
}
Book.prototype  = function() {
    //共有属性和方法
  isBook:true;
  display = function() {
    
  }
}
复制代码

经过new关键字建立的对象,其实是对新对象this的不断赋值,并将prototype指向类的prototype所指向的对象,而类的构造函数外面的经过点语法添加的属性和方法不会添加到新建立的对象上去。github

闭包的实现

有时候咱们常常将类的静态变量经过闭包来实现ajax

var Book = (function() {
   //静态私有变量、静态私有方法
 var bookNum = 0;
 function checkBook(name) {
   
 }
 //返回构造函数
 return function(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.setNmae(name);
   this.setPrice(price)
 }
})()
Book.prototype =  {
   //静态共有属性、静态公有方法
 isJsBook:false,
 display:function() {
   
 }
}
复制代码

闭包就是有权访问另一个函数做用域中变量的函数,即在一个函数内部建立另一个函数。咱们将这个闭包做为建立对象的构造函数,这样,它既是闭包又是可实例化对象的函数,便可访问到类做用域中的变量。可是在闭包外部添加原型属性和方法看上去彷佛脱离闭包这个类,因此我们能够用下面的方式来搞一搞

var Book = (function() {
  //静态私有变量
  var bookNum = 0;
  //静态私有方法
  function checkBook(name) {
    console.log(name);
  }
  function _book(newId,newName,newPrice) {
    //私有变量
    var name,price;
    //私有方法
    function checkId(id) {
      console.log(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() {
        console.log('display')
      }
  }
  return _book;
})()
复制代码

继承

类式继承

function SuperClass() {
  this.superValue = true;
}
SuperClass.prototype.getSuperValue = function() {
  return this.superValue;
}

function SubClass() {
  this.subValue = false;
}
SubClass.prototype = new SubClass();

SubClass.prototype.getSubValue = function() {
  return this.subValue;
}
复制代码

继承很是简单,就是声明两个类而已,不过类式继承须要将第一个类的实例赋值给第二个类的原型,由于类的原型对象做用就是为类的原型添加共有方法,可是类不能直接访问这些属性和方法,必须经过原型prototype来访问。而咱们实例化一个父类的时候,新建立的对象复制了父类构造函数的属性和方法并将原型__proto__指向父类的原型对象,这样就拥有了父类原型对象的属性和方法,而且这个新建立的对象能够直接访父类原型对象上的属性和方法。并且新建立的对象不只仅能够访问父类原型上的属性和方法,一样能够访问父类构造函数中复制的属性和方法。将这个对象赋值给子类的原型,那么这个子类的原型一样能够访问父类原型上的属性和方法与从父类构造函数中复制的属性和方法。

另外,咱们能够经过instanceof来检测某个对象是否为某个类的实例

var instance = new SubClass();

console.log(instance instanceof SuperClass)
console.log(instance instanceof SubClass)
console.log(SubClass instanceof SuperClass)
复制代码

关于结果你们能够自行尝试。注意,instanceof是判断对象是不是后面类的实例,它并不表示两者的继承。

console.log(SubClass.prototype instanceof SuperClass);
复制代码

可是这种类式继承有两个缺点,其一,因为子类经过其原型prototype对父类实例化,继承了父类,因此说父类中若是共有属性是引用类型,就会在子类中被全部的实例所共享,所以一个子类的实例更改子类原型从父类构造函数中继承的共有属性就会直接影响到其余的子类。

function SuperClass() {
  this.books = ['js','css'];
}
function SubClass() {}
SubClass.prototype = new SuperClass();

var instance1  = new SubClass();
var instance2 = new SubClass();
console.log(instance2.books);
instance1.books.push('html');
console.log(instance1.books,instance2.books)
复制代码

其二,因为子类实现的继承是靠其原型prototype对父类进行实例化实现的,所以在建立父类的时候,是没法向父类传递参数的。于是在实例化父类的时候也没法对父类构造函数内的属性进行初始化

构造函数继承

直接看代码

function SuperClass(id) {
  this.books = ['js','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('html');
console.log(instance1)
console.log(instance2)
instance1.showBooks();//TypeError
复制代码

如上,SuperClass.call(this,id)固然就是构造函数继承的核心语句了,因为call这个方法能够更改函数的做用环境,所以在子类中,对superClass调用这个方法就是将子类中的变量在父类中执行一遍。因为父类中给this绑定属性,所以子类天然也就继承父类的共有属性。因为这种类型的继承没有涉及到原型prototype,因此父类的原型方法天然不会被子类继承,而若是想被子类继承,就必须放到构造函数中,这样建立出来的每个实例都会单独的拥有一份而不能共用,这样就违背了代码复用的原则,因此综合上述两种,咱们提出了组合式继承方法

组合继承

类式继承是经过子类原型prototype对父类实例化实现的,构造函数继承是经过在子类的构造函数做用环境中执行一次父类的构造函数来实现的。

function SuperClass(name) {
  this.name = name; 
  this.books = ['Js','CSS'];
}
SuperClass.prototype.getBooks = function() {
    console.log(this.books);
}
function SubClass(name,time) {
  SuperClass.call(this,name);
  this.time = time;
}
SubClass.prototype = new SuperClass();

SubClass.prototype.getTime = function() {
  console.log(this.time);
}
复制代码

如上,咱们就解决了以前说到的一些问题,可是是否是从代码看,仍是有些不爽呢?至少这个SuperClass的构造函数执行了两遍就感受很是的不妥。

原型式继承

原型式继承大体的实现方式是这个样子的

function inheritObject(o) {
    //申明一个过渡对象
  function F() { }
  //过渡对象的原型继承父对象
  F.prototype = o;
  //返回过渡对象的实例,该对象的原型继承了父对象
  return new F();
}
复制代码

其实这种方式和类式继承很是的类似,他只是对类式继承的一个封装,其中的过渡对象就至关于类式继承的子类,只不过在原型继承中做为一个普通的过渡对象存在,目的是为了建立要返回的新的实例对象。

var book = {
    name:'js book',
    likeBook:['css Book','html book']
}
var newBook = inheritObject(book);
newBook.name = 'ajax book';
newBook.likeBook.push('react book');
var otherBook = inheritObject(book);
otherBook.name = 'canvas book';
otherBook.likeBook.push('node book');
console.log(newBook,otherBook);
复制代码

如上代码咱们能够看出,原型式继承和类式继承一个样子,对于引用类型的变量,仍是存在子类实例共享的状况。

因此,咱们还有下面的寄生式继承

寄生式继承

直接看代码

var book = {
    name:'js book',
    likeBook:['html book','css book']
}
function createBook(obj) {
    //经过原型方式建立新的对象
  var o = new inheritObject(obj);
  // 拓展新对象
  o.getName = function(name) {
    console.log(name)
  }
  // 返回拓展后的新对象
  return o;
}
复制代码

其实寄生式继承就是对原型继承的拓展,一个二次封装的过程,这样新建立的对象不只仅有父类的属性和方法,还新增了别的属性和方法。

寄生组合式继承

回到以前的组合式继承,那时候咱们将类式继承和构造函数继承组合使用,可是存在的问题就是子类不是父类的实例,而子类的原型是父类的实例,因此才有了寄生组合式继承。

而寄生组合式继承是寄生式继承和构造函数继承的组合。可是这里寄生式继承有些特殊,这里他处理不是对象,而是类的原型。

function inheritPrototype(subClass,superClass) {
    // 复制一份父类的原型副本到变量中
  var p = inheritObject(superClass.prototype);
  // 修正由于重写子类的原型致使子类的constructor属性被修改
  p.constructor = subClass;
  // 设置子类原型
  subClass.prototype = p;
}
复制代码

组合式继承中,经过构造函数继承的属性和方法都是没有问题的,因此这里咱们主要探究经过寄生式继承从新继承父类的原型。咱们须要继承的仅仅是父类的原型,不用去调用父类的构造函数。换句话说,在构造函数继承中,咱们已经调用了父类的构造函数。所以咱们须要的就是父类的原型对象的一个副本,而这个副本咱们能够经过原型继承拿到,可是这么直接赋值给子类会有问题,由于对父类原型对象复制获得的复制对象p中的constructor属性指向的不是subClass子类对象,所以在寄生式继承中要对复制对象p作一次加强,修复起constructor属性指向性不正确的问题,最后将获得的复制对象p赋值给子类原型,这样子类的原型就继承了父类的原型而且没有执行父类的构造函数。

function inheritPrototype(subClass,superClass) {
    // 复制一份父类的原型副本到变量中
  var p = inheritObject(superClass.prototype);
  // 修正由于重写子类的原型致使子类的constructor属性被修改
  p.constructor = subClass;
  // 设置子类原型
  subClass.prototype = p;
}
function inheritObject(o) {
    //申明一个过渡对象
  function F() { }
  //过渡对象的原型继承父对象
  F.prototype = o;
  //返回过渡对象的实例,该对象的原型继承了父对象
  return new F();
}
function SuperClass(name) {
  this.name = name;
  this.books=['js book','css book'];
}
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('React','2017/11/11')
var instance2 = new SubClass('Js','2018/22/33');

instance1.books.push('test book');

console.log(instance1.books,instance2.books);
instance2.getName();
instance2.getTime();
复制代码

这种方式继承其实如上图所示,其中最大的改变就是子类原型中的处理,被赋予父类原型中的一个引用,这是一个对象,所以有一点你须要注意,就是子类在想添加原型方法必须经过prototype.来添加,不然直接赋予对象就会覆盖从父类原型继承的对象了。

多继承

因为JavaScript中的继承是经过原型链来实现的,只有一条原型链,因此理论上来讲是实现不了继承多个父类的。可是咱们能够经过一些小技巧,来实现一个相似的多继承

var extend = function(target,source) {
  // 遍历源对象中的属性
  for(var property in source){
      //将源对象中的属性复制到目标对象中
      target[property] = source[property];
  }
  //返回目标对象
  return target;
}
复制代码

固然,此处咱们实现的这是浅复制,对于引用类型的它仍是无能为力的。jquery中实现了深复制,就是将源对象中的引用类型的属性再执行一遍extend方法而实现。这里咱们实现的比较简单。

var book = {
    name:'javascript 设计模式',
    alike:['css','html']
}
var another = {
    color:'blue'
};
extend(another,book);
console.log(another.name);
console.log(another.alike);
another.alike.push('React');
another.name = '设计模式'console.log(another,book);
复制代码

上面是实现一个对象的赋值,固然,多继承,也就是在外层多套一个循环的事情了,这里就不在赘述了

多态

多态,其实就是同一个方法多种的调用方式,在JavaScript中其实有不少种实现方式的。只不过要对传入的参数进行判断以实现多种的调用方式。

操做比较常规,这里就再也不赘述了。能够参考个人另外一篇文章忍者级别的函数操做

相关文章
相关标签/搜索