JavaScript 中的物件導向程式設計

oschina發表於2016-09-10

介紹

JavaScript 是一個強大的物件導向程式語言,但是,並不像傳統的程式語言,它採用一個以原型為基礎的OOP模型,致使它的語法讓大多數開發人員看不懂。另外,JavaScript 也把函式作為首要的物件,這可能會給不夠熟悉這門語言的開發人員造成更大的困惑。那就是我們決定放在前面作為一個簡短前言進行介紹的原因,並且在 JavaScript 裡也可以用作物件導向程式設計的一個參考。

這個文件沒有提供一個物件導向程式設計的規則預覽,但有它們的介面概述。

名稱空間

隨著越來越多的第三方庫,框架和web依賴的出現,JavaScript發展中的名稱空間是勢在必行的,我們得儘量避免在全域性名稱空間的物件和變數的衝突。

不幸的是,JavaScript沒有提供支援名稱空間的編譯,但是我們可以使用物件來得到同樣結果。在JavaScript中我們有許多種模式來實現名稱空間介面,但是我們覆蓋巢狀的名稱空間,它在該領域是最常用的模式。

巢狀名稱空間

巢狀的名稱空間模式使用物件字面量來捆綁一個特定應用的特定名字的功能。

我們最初建立一個全域性物件,並且賦值給一個稱為MyApp的變數。

// global namespace
var MyApp = MyApp || {};

上述的語法會檢查MyApp是否已經被定義過。假如它已經被定義過,我們簡單地把它賦值給自己,但是,我們建立一個空的容器來裝載我們的函式和變數。

我們也可以使用相同技術來建立子名稱空間。例如:

// sub namespaces
MyApp.users = MyApp.user || {};

我們一旦啟動我們的容器,我們可以在(容器)內部定義我們的函式和變數,並且在全域性名稱空間呼叫它們,不需要冒著與現有定義衝突的風險。

// declarations

MyApp.users = {

	existingUsers: '', // variable in namespace

	renderUsersHTML: function() { // function in namespace

		// render html list of users

	}

};

// syntax for using functions within our namespace from the global scope

MyApp.users.renderUsersHTML();

在JavaScript命名模式的一個內部概述是由Goggle的Addy Osmani在Essential JavaScript Namespacing Patterns的文章中介紹的。假如你想探索不同的模式,這裡將是一個美好的起點。

物件

如果你寫過 JavaScript 程式碼,那你已經使用過物件了。JavaScript 有三種型別的物件:

原生物件

原生物件是語言規範的一部分,不管在什麼樣的執行環境下執行,原生物件都可用。原生物件包括:Array、Date、Math 和 parseInt 等。想了解所有原生物件,請參閱 JavaScript 內建物件參考

var cars = Array(); // Array is a native object

宿主物件

與原生物件不同,宿主物件是由 JavaScript 程式碼執行的環境建立。不同的環境環境建立有不同的宿主物件。這些宿主物件在多數情況下都允許我們與之互動。如果我們寫的是在瀏覽器(這是其中一種執行環境)上執行的程式碼,會有 window、document、location 和 history 等宿主物件。

document.body.innerHTML = 'Hello World!'; // document is a host object

// the document object will not be available in a 
// stand-alone environments such as Node.js

使用者物件

使用者物件(或植入物件)是在我們的程式碼中定義的物件,在執行的過程中建立。JavaScript 中有兩種方式建立自己的物件,下面詳述。

物件字面量

在前面演示建立名稱空間的時候,我們已經接觸到了物件字面量。現在來搞清楚物件字面量的定義:物件字面量是置於一對花括號中的,由逗號分隔的名-值對列表。物件字面量可擁有變數(屬性)和函式(方法)。像 JavaScript 中的其它物件一樣,它也可以作為函式的引數,或者返回值。

現在定義一個物件字面量並賦予一個變數:

// declaring an object literal

var dog = {

	// object literal definition comes here...

};

向這個物件字面量新增屬性和方法,然後在全域性作用域訪問:

// declaring an object literal

var dog = {

	breed: 'Bulldog', // object literal property

	bark: function() { // object literal method

		console.log("Woof!");

	},

};

// using the object

console.log( dog.breed ); // output Bulldog

dog.bark(); // output Woof!

這看起來和前面的名稱空間很像,但這並不是巧合。字面量物件最典型的用法就是把程式碼封裝起來,使之在一個封裝的包中,以避免與全域性作用域中的變數或物件發生衝突。由於類似的原因,它也常常用於向外掛或物件傳遞配置引數。

如果你熟悉設計模式的話,物件字面量在某種程度上來說就是單例,就是那種只有一個例項的模式。物件字面量先天不具備例項化和繼承的能力,我們接下來還得了解 JavaScript 中另一種建立自定義物件的方法。

建構函式

定義建構函式

函式是 JavaScript 一等公民,就是說其它實體支援的操作函式都支援。在 JavaScript 的世界,函式可以在執行時進行動態構造,可以作為引數,也可以作為其它函式的返回值,也可被賦予變數。而且,函式也可以擁有自己的屬性和方法。JavaScript 中函式的特性使之成為可以實體化和繼承的東西。

來看看怎麼用建構函式建立一個自定義的物件:

// creating a function

function Person( name, email ) {

	// declaring properties and methods using the (this) keyword

	this.name 	= name;
	this.email 	= email;

	this.sayHey = function() {

		console.log( "Hey, I’m " + this.name );

	};

}

// instantiating an object using the (new) keyword

var steve = new Person( "Steve", "steve@hotmail.com" );

// accessing methods and properties

steve.sayHey();

建立建構函式類似於建立普通函式,只有一點例外:用 this 關鍵字定義自發性和方法。一旦函式被建立,就可以用 new 關鍵字來生成例項並賦予變數。每次使用 new 關鍵字,this 都指向一個新的例項。

構建函式例項化和傳統物件導向程式語言中的通過類例項化並非完全不同,但是,這裡存在一個可能不易被察覺的問題。

當使用 new 關鍵字建立新物件的時候,函式塊會被反覆執行,這使得每次執行都會產生新的匿名函式來定義方法。這就像建立新的物件一樣,會導致程式消耗更多記憶體。這個問題在現代瀏覽器上執行的程式中並不顯眼。但隨著應用規則地擴大,在舊一點的瀏覽器、計算機或者低電耗裝置中就會出現效能問題。不過不用擔心,有更好的辦法將方法附加給建構函式(是不會汙染全域性環境的哦)。

方法和原型

前面介紹中提到 JavaScript 是一種基於原型的程式語言。在 JavaScript 中,可以把原型當作物件模板一樣來使用。原型能避免在例項化物件時建立多餘的匿名函式和變數。

在 JavaScript 中,prototype 是一個非常特別的屬性,可以讓我們為物件新增新的屬性和方法。現在用原型重寫上面的示例看看:

// creating a function

function Person( name, email ) {

	// declaring properties and methods using the (this) keyword

	this.name 	= name;
	this.email 	= email;

}

// assign a new method to the object’s prototype

Person.prototype.sayHey = function() {

	console.log( "Hey, I’m " + this.name );

}

// instantiating a new object using the constructor function

var steve = new Person( "Steve", "steve@hotmail.com" );

// accessing methods and properties

steve.sayHey();

這個示例中,不再為每個 Person 例項定義 sayHey 方法,而是通過原型模板在各例項中共享這個方法。

繼承性

通過原型鏈,原型可以用來例項繼承。JavaScript 的每一個物件都有原型,而原型是另外一個物件,也有它自己的原型,周而復始…直到某個原型物件的原型是 null——原型鏈到此為止。

在訪問一個方法或屬性的時候,JavaScript 首先檢查它們是否在物件中定義,如果不,則檢查是否定義在原型中。如果在原型中也沒找到,則會延著原型鏈一直找下去,直到找到,或者到達原型鏈的終端。

現在來看看程式碼是怎麼實現的。可以從上一個示例中的 Person 物件開始,另外再建立一個叫 Employee 的物件。

// Our person object

function Person( name, email ) {

	this.name 	= name;
	this.email 	= email;

}

Person.prototype.sayHey = function() {

	console.log( "Hey, I’m " + this.name );

}

// A new employee object

function Employee( jobTitle ) {

	this.jobTitle = jobTitle;

}

現在 Employee 只有一個屬性。不過既然員工也屬於人,我們希望它能從 Person 繼承其它屬性。要達到這個目的,我們可以在 Employee 物件中呼叫 Person 的建構函式,並配置原型鏈。

// Our person object

function Person( name, email ) {

	this.name 	= name;
	this.email 	= email;

}

Person.prototype.sayHey = function() {

	console.log( "Hey, I’m " + this.name );

}

// A new employee object

function Employee( name, email, jobTitle ) {

	// The call function is calling the Constructor of Person
	// and decorates Employee with the same properties

	Person.call( this, name, email );

	this.jobTitle = jobTitle;

}

// To set up the prototype chain, we create a new object using 
// the Person prototype and assign it to the Employee prototype

Employee.prototype = Object.create( Person.prototype );

// Now we can access Person properties and methods through the
// Employee object

var matthew = new Employee( "Matthew", "matthew@hotmail.com", "Developer" );

matthew.sayHey();

要適應原型繼承還需要一些時間,但是這一個必須熟悉的重要概念。雖然原型繼承模型常常被認為是 JavaScript 的弱點,但實際上它比傳統模型更強大。比如說,在掌握了原型模型之後建立傳統模型簡直就太容易了。

ECMAScript 6 引入了一組新的關鍵字用於實現 。雖然新的設計看起來與傳統基於類的開發語言非常接近,但它們並不相同。JavaScript 仍然基於原型。

相關文章