javascript設計模式與開發實踐(二)- 封裝和原型模式

jasonlive發表於2019-02-28

封裝

封裝資料

在許多語言的物件系統中,封裝資料是由語法解析來實現的,這些語言也許提供了 private、
public、protected 等關鍵字來提供不同的訪問許可權。例如:java

但在js裡面,並沒有提供這些關鍵字的支援,我們只能通過作用域來模擬實現封裝性。(ES6 let除外)

var myTest = (function() {
    var _name =`jason`;
    return {
        getName: function(){
            return _name;
        }
    }
})()
console.log(myTest.getName()); // 輸出:jason
console.log(_name); // 輸出:_name is not defined
複製程式碼

這裡,我們定義了一個自執行函式(什麼叫自執行函式?參考這篇文章),函式內部定義了一個變數_name,函式return一個物件出去(也叫模組模式),向外部暴露了一個getName函式,這裡的_name變數就被封裝在了myTest函式內部。

上文說的是封裝資料,在js裡,不光資料可以封裝,還可以封裝實現、封裝型別,封裝變化等,封裝使得物件之間的耦合變得鬆散,物件之間只通過暴露的 API 介面來通訊。

原型模式和基於原型繼承的JavaScript物件系統

在java中,物件必須由類建立而來,而在js中,物件卻是通過原型繼承的方式得來的,在原型程式設計的思想中,類並不是必需的,物件未必需要從類中建立而來, 一個物件是通過克隆另外一個物件所得到的。

原型模式不單是一種設計模式,也被稱為一種程式設計泛型。

使用克隆的原型模式

從設計模式的角度講,原型模式是用於建立物件的一種模式,如果我們想要建立一個物件, 一種方法是先指定它的型別,然後通過類來建立這個物件。原型模式選擇了另外一種方式,我們 不再關心物件的具體型別,而是找到一個物件,然後通過克隆來建立一個一模一樣的物件。
原型模式的實現關鍵,是語言本身是否提供了 clone 方法。ECMAScript 5 提供了 Object.create 方法,可以用來克隆物件。程式碼如下:

var Plane = function(){ 
    this.blood = 100;
    this.attackLevel = 1;
    this.defenseLevel = 1;
};
var plane = new Plane();
plane.blood = 500;
plane.attackLevel = 10;
plane.defenseLevel = 7;
var clonePlane = Object.create( plane );
console.log( clonePlane ); // 輸出:Object {blood: 500, attackLevel: 10,defenseLevel: 7}

在不支援 Object.create 方法的瀏覽器中,則可以使用以下程式碼:

Object.create = Object.create || function( obj ){
    var F = function(){};
    F.prototype = obj;
    return new F(); 
}
複製程式碼

原型程式設計範型的一些規則

  • 所有的資料都是物件。
  • 要得到一個物件,不是通過例項化類,而是找到一個物件作為原型並克隆它。
  • 物件會記住它的原型。
  • 如果物件無法響應某個請求,它會把這個請求委託給它自己的原型。

JavaScript中的原型繼承

這裡我們來根據上面的正規化來整理一下js中遵循的規則

所有的資料都是物件

在js中,除了undefined之外,所有資料都是物件,number、boolean、string 這幾種基本型別資料也可以通過“包裝類”的方式變成物件型別資料來處理。那麼根據原型規則,這些物件一定有個根物件,這個物件就是Object.prototype,Object.prototype 物件是一個空的 物件。我們在 JavaScript 遇到的每個物件,實際上都是從 Object.prototype 物件克隆而來的, Object.prototype 物件就是它們的原型。

var obj1 = new Object();
var obj2 = {};

console.log( Object.getPrototypeOf( obj1 ) === Object.prototype ); // 輸出:true
console.log( Object.getPrototypeOf( obj2 ) === Object.prototype ); // 輸出:true
複製程式碼
要得到一個物件,不是通過例項化類,而是找到一個物件作為原型並克隆它

js中克隆是引擎內部實現的,我們不用去關心他是如何實現的,我們只要知道使用var obj1 = new Object()或者var obj2 = {},引擎就會從Object.prototype克隆一個物件出來。

接下來我們看看如何使用new運算子得到一個物件

var Person = function(name) {
    this.name = name;
    this.getName = function() {
        return this.name;
    }
}

var p = new Person(`jason`);

console.log(p.name);
console.log(p.getName());
console.log(Object.getPrototypeOf(p) === Person.prototype); // 輸出true
複製程式碼

在 JavaScript 中沒有類的概念,這句話我們已經重複過很多次了。但剛才不是明明呼叫了newPerson()嗎?

在這裡 Person 並不是類,而是函式構造器,JavaScript 的函式既可以作為普通函式被呼叫, 7 也可以作為構造器被呼叫。當使用 new 運算子來呼叫函式時,此時的函式就是一個構造器。 用
new 運算子來建立物件的過程,實際上也只是先克隆 Object.prototype 物件,再進行一些其他額 外操作的過程。

物件會記住它的原型

JavaScript 給物件提供了一個名為__proto__的隱藏屬性,某個物件的__proto__屬性預設會指 向它的構造器的原型物件,即{Constructor}.prototype。
我們通過程式碼來驗證:

var objA = {
    name: `jason`
}

console.log(objA.__proto__ === Object.prototype); //true
複製程式碼

再來

var objB = function(age) {
    this.age = age;
}

var b = new objB();

console.log(b.__proto__ === objB.prototype); //true
複製程式碼

實際上,__proto__就是物件跟“物件構造器的原型”聯絡起來的紐帶 切記這句話,對未來理解js原型鏈很有幫助。

如果物件無法響應某個請求,它會把這個請求委託給它的構造器的原型

雖然 JavaScript 的物件最初都是由 Object.prototype 物件克隆而來的,但物件構造器的原型並不僅限於 Object.prototype 上,可以動態指向其他物件。

var obj = { name: `sven` };
var A = function(){};

A.prototype = obj;
var a = new A();
console.log(a.__proto__ === obj); //true
console.log(a.name); // 輸出:sven
複製程式碼

上面的程式碼中,第一行和第二行本沒有任何關聯,obj是個物件字面量建立的物件,A是個空方法,在第三行程式碼執行之前,obj__proto__指向Object.prototypeAprototype指向自身的構造器,A.prototype = obj;將引用指向了obj,所以在程式碼執行完後,物件a的原型指向obj,雖然a本身沒有name屬性,但原型上擁有name屬性

總結

現在,讓我們來總結一下js建立物件的幾種方式:

  1. 物件字面量
var obj = {};
複製程式碼
  1. 通過物件構造器建立
var Co = function(){};
var obj = new Co();
複製程式碼
  1. 通過Object.create();建立(ES5以後版本支援)
var Co = function(){};
var obj = Object.create(Co);
複製程式碼
  1. 通過class建立(ES6以後版本支援)
class An{
    constructor(name){
        this.name = name;
    }
    
    getName() {
        return this.name;
    }
}

class Dog extends Animal { 
    constructor(name) {
        super(name); 
    }
    speak() {
        return "woof";
    } 
}

var dog = new Dog("Scamp");
console.log(dog.getName()); // Scamp
console.log(dog.speak()); // woof
複製程式碼

上面的幾種建立物件的方式有本質的區別,這裡先不做詳細說明,後續學完作用域和閉包後再統一說明。

相關文章