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中其實有不少種實現方式的。只不過要對傳入的參數進行判斷以實現多種的調用方式。

操做比較常規,這裏就再也不贅述了。能夠參考個人另外一篇文章忍者級別的函數操做

相關文章
相關標籤/搜索