封裝
封裝資料
在許多語言的物件系統中,封裝資料是由語法解析來實現的,這些語言也許提供了 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 中沒有類的概念,這句話我們已經重複過很多次了。但剛才不是明明呼叫了new
Person()嗎?
在這裡 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.prototype
,A
的prototype
指向自身的構造器,A.prototype = obj;
將引用指向了obj
,所以在程式碼執行完後,物件a的原型指向obj,雖然a本身沒有name屬性,但原型上擁有name屬性
總結
現在,讓我們來總結一下js建立物件的幾種方式:
- 物件字面量
var obj = {};
複製程式碼
- 通過物件構造器建立
var Co = function(){};
var obj = new Co();
複製程式碼
- 通過Object.create();建立(ES5以後版本支援)
var Co = function(){};
var obj = Object.create(Co);
複製程式碼
- 通過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
複製程式碼
上面的幾種建立物件的方式有本質的區別,這裡先不做詳細說明,後續學完作用域和閉包後再統一說明。