1. 程式人生 > >JavaScript 建立自定義類和物件

JavaScript 建立自定義類和物件

JavaScript 建立類和物件的一般性套路

還記得嗎,面嚮物件語言的四大基本準則。

  • 抽象

  • 封裝

  • 繼承

  • 多型

JavaScript 語言滿足以上四點,所以完全能夠勝任面向物件程式設計的要求。一般來說,在 JavaScript 中要建立新的物件,通常有以下幾種辦法。

  • 物件字面量

  • 動態繫結方式

  • 工廠方式

  • 建構函式方式(我是說真正的函式,而不是隸屬於類程式碼塊裡面的建構函式)

  • 原型方式

  • 其他(指混合了以上三種方法的方案,也不可小覷)

你要知道 JavaScript 是一門相當靈活的程式語言,所以甚至不需要用到 class

關鍵字也能完美建立自定義物件。而且 JavaScript 是基於原型而不是基於類的程式語言,在 JavaScript 中是物件繼承物件而不是繼承類。以下例項大量引用來自 W3cSchool 的程式碼。

物件字面量

使用物件字面量建立物件在語法上極為簡單。它基於字典實現。

var person={"name":"Xiaoming","age":100,"sex":"male"};

如果你想要繫結函式的話。屬性名可以不加引號,但是如果是保留字就必須加上。

var person = {
    "name":"Xiaoming",
    "age":100,
    "sex":"male"
, "selfIntro":function(){ console.log("My name is "+this.name+" and I am "+age+" years old"); } };

使用這種方式可以建立一個單獨獨立的物件,而且當然具有複用性(封裝的特點)。但是缺陷是對比像 Java 這樣以類為基礎的語言,它完全沒有繼承的概念,也就是說是你雖然可以複用該物件卻不能大量生產該物件的同類型物件。

動態繫結

JavaScript 是一門動態的程式語言,這為類結構的書寫奠定了基調,畢竟類的本質就是封裝麼。在 JavaScript 中,有一個超好用的 object 內建物件,它是所有 JavaScript 物件的超類,你可以以它為基礎進行擴充套件,進而達到類結構的構建。

var oCar = new Object;
oCar.color = "blue";
oCar.doors = 4;
oCar.mpg = 25;
oCar.showColor = function() {
  alert(this.color);
};

如果你不介意程式碼變得又臭又長,還可以繼續寫(我能接著寫完這個螢幕)。但是這樣做和物件字面量一樣有一個致命的缺陷,就是它每次只能建立一個物件,卻不能達到批量建立物件的作用。

工廠函式

想要實現批量 JavaScript 物件的批量生產,你需要工廠函式來幫忙。本來函式就是一段程式碼的封裝體,這樣一來,只要以這段函式為模版構造物件,就能達到批量生產的效果了,而且這些新做出來物件全都具有相似的屬性和功能。

function createCar() {
  var oTempCar = new Object;
  oTempCar.color = "blue";
  oTempCar.doors = 4;
  oTempCar.mpg = 25;
  oTempCar.showColor = function() {
    alert(this.color);
  };
  return oTempCar;
}

var oCar1 = createCar();
var oCar2 = createCar();

看到了嗎,在函式內部定義了一個內部函式,但是從封裝類的角度看,這個函式的地位實際上和其他變數一樣,都是作為 createCar 物件的屬性罷了。注意到外層函式,也就是所謂的封裝類,它返回一個新的 oTempcar 物件,這才是最後兩行之所以能順利建立例項的關鍵。在這之後,你想建立多少由這個該物件為模版的物件都可以,如果你需要的話。有人說可以為函式新增引數,以提升新物件的可擴充套件性,完全沒問題,只要在外層函式引數呼叫處寫好即;有人說可以為了保持程式碼美觀整潔,還可以在外層函式外定義物件的方法屬性,同樣完全沒問題。這裡不再贅述。

建構函式

JavaScript 的建構函式可以單獨出現,這和包括 Python 在內的一些語言較為不同。這樣的設計我自認為較為小巧、靈活。建構函式和前面講的工廠函式的區別在於,建構函式重點在於構造,它存在之目的即在於建立模版,使用 this 關鍵字表明特點。最後建構函式沒有返回值,這一點和我們通常寫的以 class 字眼包裹的那種類倒有幾分神似。哦對了,為了迎合類的一般性標準,JavaScript 的建構函式,函式名最好大寫。

function Car(sColor,iDoors,iMpg) {
  this.color = sColor;
  this.doors = iDoors;
  this.mpg = iMpg;
  this.showColor = function() {
    alert(this.color);
  };
}

var oCar1 = new Car("red",4,23);
var oCar2 = new Car("blue",3,25);

再說一遍,建構函式沒有返回值(也沒有所謂的 return)。和工廠函式不同的是,工廠函式沒有建立物件,配合使用 thisnew關鍵字達到構造新物件的效果。

基於原型

JavaScript 是基於原型的程式語言。基於原型構建物件利用了prototype 屬性,它使您有能力向物件新增屬性和方法。或者你可以把 prototype 理解為外層函式的子物件,事實上確是如此(每個函式都有一個 prototype 子物件~)。prototype表示該函式的原型,也表示一個類的成員的集合。

function Car() {
}

Car.prototype.color = "blue";
Car.prototype.doors = 4;
Car.prototype.mpg = 25;
Car.prototype.showColor = function() {
  alert(this.color);
};

var oCar1 = new Car();
var oCar2 = new Car();

呼叫 new Car() 時,原型的所有屬性都被立即賦予要建立的物件,意味著所有 Car 例項存放的都是指向 showColor() 函式的指標。從語義上講,所有屬性看起來都屬於一個物件,因此解決了前面兩種方式存在的問題。

使用基於原型定義的物件有缺陷嗎?當然。參考上面的例子,相當於類的那個函式並沒有傳入任何引數,即使傳入了也用不了,因為之後用 prototype 進行繫結時是忽略那個函式之引數的。另外,如果遇到內層包括封裝體的話,一旦改變一個例項物件的該封裝體的某個屬性的值(有些繞口),以該模版建立的別的物件也會受到影響,同時改變它們的那個屬性的屬性值。下面是一個例子。

function Car() {
}

Car.prototype.color = "blue";
Car.prototype.doors = 4;
Car.prototype.mpg = 25;
Car.prototype.drivers = new Array("Mike","John");
Car.prototype.showColor = function() {
  alert(this.color);
};

var oCar1 = new Car();
var oCar2 = new Car();

oCar1.drivers.push("Bill");

alert(oCar1.drivers);   //輸出 "Mike,John,Bill"
alert(oCar2.drivers);   //輸出 "Mike,John,Bill"

原因在於,使用 prototype 繫結的是 new Array("Mike", "John"),它和函式封裝體一樣也是一個物件,也封裝類物件屬性。事實上,不管該模版建立了多少物件例項,它們只共享同一個 new Array("Mike", "John") 物件,之後不管通過那個物件改變了這個作為屬性的物件的值,其他物件會跟著改變。這和 Java 的靜態變數倒有些相像。

建構函式 + 基於原型

所謂取長補短,方能相得益彰。使用建構函式與基於原型兩者結合的方案構造物件,把變數屬性和方法屬性分離,將變數屬性寫在建構函式裡,將方法屬性,也就是函式屬性,用基於原型的思路搞定。

function Car(sColor,iDoors,iMpg) {
  this.color = sColor;
  this.doors = iDoors;
  this.mpg = iMpg;
  this.drivers = new Array("Mike","John");
}

Car.prototype.showColor = function() {
  alert(this.color);
};

var oCar1 = new Car("red",4,23);
var oCar2 = new Car("blue",3,25);

oCar1.drivers.push("Bill");

alert(oCar1.drivers);   //輸出 "Mike,John,Bill"
alert(oCar2.drivers);   //輸出 "Mike,John"

這樣做的直接好處就是,方法屬性在記憶體中只建立一次,避免了記憶體浪費。同時,因為建構函式的介入,又可以呼叫引數了,豈不美哉。我要說這樣創建出來的物件,才和像 Java 之類的強調類的程式語言寫出來的物件基本沒差了。當然,僅僅從實用性角度看。

動態的基於原型

上面講到,結合使用建構函式和基於原型,已經基本上可以實現像 Java 這樣的獨立的、個性化的物件構建了。但是有批評者認為,那種方法雖然實用卻不夠美觀,因為把方法和變數屬性分開了。_(:_」∠)_

這些專家認為如果結合使用建構函式和基於原型的話,勢必導致程式碼分散,也就是說會出現明明是完成一個封裝類的程式碼,卻出現在了兩個不同的程式碼塊中。這樣不和諧哦。那怎麼辦呢?又想節省記憶體,函式只加載一次。

function Car(sColor,iDoors,iMpg) {
  this.color = sColor;
  this.doors = iDoors;
  this.mpg = iMpg;
  this.drivers = new Array("Mike","John");

  if (typeof Car._initialized == "undefined") {
    Car.prototype.showColor = function() {
      alert(this.color);
    };

    Car._initialized = true;
  }
}

恍然大悟……原來是在建構函式內部做一次條件判斷,如果該方法沒有被初始化,就載入它,否則忽略之。說到底靠的還是 _initialized 這個小不點兒,實在是相見恨晚。動態的基於原型,其實就是以建構函式為主體,封裝住包含基於原型的條件判斷。

Object.getOwnPropertyNames 方法

Object.getOwnPropertyNames()方法返回一個數組,其中包含直接在給定物件上找到的所有屬性。暫且記住這個方法,留作後用。

const object1 = {
  a: 1,
  b: 2,
  c: 3
};

console.log(Object.getOwnPropertyNames(object1));
// expected output: Array ["a", "b", "c"]

我們可以用它進行分析物件原型的屬性所有情況。

console.log(Object.getOwnPropertyNames(Object.prototype));
// Log all properties belong to Object's prototype. 
/**
 * The properties belongs to Object's prototype.
 */
Array ["toString", "toLocaleString", "valueOf", "hasOwnProperty", "propertyIsEnumerable", "isPrototypeOf", "__defineGetter__", "__defineSetter__", "__lookupGetter__", "__lookupSetter__", "__proto__", "constructor"]

讓我們分析看看咱們自定義的類的原型之屬性。

  • 物件字面量
console.log(Object.getOwnPropertyNames(person.prototype));
// raise an error, since oCar does not have an prototype (undefined)

console.log(Object.getOwnPropertyNames(person));
// output: Array ["name", "age", "sex", "selfIntro"]
  • 無任何封裝的動態繫結
console.log(Object.getOwnPropertyNames(oCar.prototype));
// raise an error, since oCar does not have an prototype (undefined)

console.log(Object.getOwnPropertyNames(oCar);
// output: Array ["color", "doors", "mpg", "showColor"]
  • 使用工廠函式
console.log(Object.getOwnPropertyNames(createCar.prototype));
// output: Array ["constructor"]

console.log(Object.getOwnPropertyNames(createCar));
// output: Array ["arguments", "caller", "length", "name", "prototype"]
  • 使用建構函式
console.log(Object.getOwnPropertyNames(Car.prototype));
// output: Array ["constructor"]

console.log(Object.getOwnPropertyNames(Car.prototype));
// output: Array ["arguments", "caller", "length", "name", "prototype"]
  • 建構函式 + 基於原型
console.log(Object.getOwnPropertyNames(Car.property));
// raise an error, since oCar does not have an prototype (undefined)

console.log(Object.getOwnPropertyNames(Car));
 // Array ["arguments", "caller", "length", "name", "prototype"]
  • 基於原型建立
console.log(Object.getOwnPropertyNames(Car.property));
// raise an error, since oCar does not have an prototype (undefined)
console.log(Object.getOwnPropertyNames(Car));
// output: Array ["arguments", "caller", "length", "name", "prototype"]
  • 動態的基於原型
console.log(Object.getOwnPropertyNames(Car.property));
// raise an error, since oCar does not have an prototype (undefined)

console.log(Object.getOwnPropertyNames(Car));
// Array ["arguments", "caller", "length", "name", "prototype"]

可以看到,除了字面量物件和動態繫結外,只要和函式有關,Object.getOwnPropertyNames() 方法返回的都是相同的東西,即 Array ["arguments", "caller", "length", "name", "prototype"],請把握這一點,這也是所有函式的共性。(從這裡也體會到 JavaScript 中函式作為物件的強大了吧……)

class 關鍵字

class 關鍵字是 ES6 引入的最重要的一個特性。使用該特性可以寫出簡約風格的封裝類,使用 class 關鍵字的內部實現機制實際上是建構函式和基於原型結合的物件建立方案。畢竟這是一個語法糖~~

一段用建構函式與基於原型寫成的封裝類。

function User(name) {
  this.name = name;
}

User.prototype.sayHi = function() {
  alert(this.name);
}

let user = new User("John");
user.sayHi();

如果使用 class 關鍵字可以寫得更優雅。

class User {

  constructor(name) {
    this.name = name;
  }

  sayHi() {
    alert(this.name);
  }

}

let user = new User("John");
user.sayHi();

可以說,只要條件允許(瀏覽器支援的話),如果要寫封裝類就應該用 class 關鍵字。它不但糅合了之前方案的所有優點,而且寫起來也比較簡單。

同樣的,我們使用 Object.getOwnPropertyNames() 方法對自定義類進行測試。

console.log(Object.getOwnPropertyNames(User.property));
// Array ["constructor", "sayHi"]

console.log(Object.getOwnPropertyNames(User));
// output: Array ["length", "name", "prototype"]

如果使用 class 關鍵字來建立物件,從效果上完全等價於結合使用建構函式與基於原型進行物件的構建,這是針對構造出來的物件而言的。從上面的例子中我們看出,那個用來建立物件的模版(建構函式~嚴格類)擁有的屬性是不同的。且不看每個例子的第一塊,就看第二塊,你發現在基於建構函式建立物件的方案中,因為是函式,所以它們具有屬性 "arguments", "caller", "length", "name", "prototype",而這裡不再是函數了,是類,具有的屬性是不一樣的,分別是 "length", "name", "prototype"函式模版比類模版多了 argumentscaller 屬性。可以這樣理解,class 描述的類,在前者的基礎上封裝,同時捨去了一些和嚴格意義上的類無關的東西。想 argumentscaller 這樣作為屬性物件,雖然在原始碼中看不見,但卻是在執行時隱式宣告的。

使用 class 關鍵字建立物件,有幾點提請注意。

  • JavaScript 的 class 程式碼塊內只存在方法,包括一般方法和構造方法,不存在直接寫在程式碼塊中內的變數,但是你可以使用 prototype 打破這個限制如果非要不可的話

  • 能且只能用 new 關鍵字來呼叫constructor() 構造方法

  • 如果沒有顯式宣告 constructor(),JavaScript 會自動建立一個無引數構造方法 constructor() {}

  • 對於用 class 宣告的類而言,其自動使用 use strict(嚴格模式)

  • 為了更好的實現封裝,你自己實現需要 get(..)set(..) 方法

  • 為了實現類函式級別的方法,而不是將方法附加到 prototype 上,請使用 static 關鍵字(在 ES6 中和 class 一同被引入)

通常我們要使用某個封裝物件,不考慮複用性則優先考慮物件字面量。如果想要構建類體系,包括繼承(extends關鍵字在 ES6 和 class 一同引入)等特性的話,最好使用 class 關鍵字來建立物件。現在比較新的瀏覽器都支援。