JavaScript面向?qū)ο蠛喗?/h1>

前言

JavaScript 的核心是支持面向?qū)ο蟮模瑫r它也提供了強(qiáng)大靈活的 OOP 語言能力。本文從對面向?qū)ο缶幊痰慕榻B開始,帶您探索 JavaScript 的對象模型,最后描述?JavaScript 當(dāng)中面向?qū)ο缶幊痰囊恍└拍睢?/p>

面向?qū)ο缶幊?/h1>

面向?qū)ο缶幊淌怯贸橄蠓绞絼?chuàng)建基于現(xiàn)實世界模型的一種編程模式。它使用先前建立的范例,包括模塊化,多態(tài)和封裝幾種技術(shù)。今天,許多流行的編程語言(如Java,JavaScript,C#,C+ +,Python,PHP,Ruby 和 Objective-C)都支持面向?qū)ο缶幊蹋∣OP)。

相對于「一個程序只是一些函數(shù)的集合,或簡單的計算機(jī)指令列表。」的傳統(tǒng)軟件設(shè)計觀念而言,面向?qū)ο缶幊炭梢钥醋魇鞘褂靡幌盗袑ο笙嗷f(xié)作的軟件設(shè)計。?在 OOP 中,每個對象能夠接收消息,處理數(shù)據(jù)和發(fā)送消息給其他對象。每個對象都可以被看作是一個擁有清晰角色或責(zé)任的獨(dú)立小機(jī)器。

面向?qū)ο蟪绦蛟O(shè)計的目的是在編程中促進(jìn)更好的靈活性和可維護(hù)性,在大型軟件工程中廣為流行。憑借其對模塊化的重視,面向?qū)ο蟮拇a開發(fā)更簡單,更容易理解,相比非模塊化編程方法, 它能更直接地分析, 編碼和理解復(fù)雜的情況和過程。

術(shù)語

Namespace

命名空間允許開發(fā)人員在一個獨(dú)特,應(yīng)用相關(guān)的名字的名稱下捆綁所有功能的容器。

Class 類

定義對象的特征。它是對象的屬性和方法的模板定義。

Object 對象

類的一個實例。

Property 屬性

對象的特征,比如顏色。

Method 方法

對象的能力,比如行走。

Constructor 構(gòu)造函數(shù)

對象初始化的瞬間,被調(diào)用的方法。通常它的名字與包含它的類一致。

Inheritance 繼承

一個類可以繼承另一個類的特征。

Encapsulation 封裝

一種把數(shù)據(jù)和相關(guān)的方法綁定在一起使用的方法。

Abstraction 抽象

結(jié)合復(fù)雜的繼承,方法,屬性的對象能夠模擬現(xiàn)實的模型。

Polymorphism 多態(tài)

多意為「許多」,態(tài)意為「形態(tài)」。不同類可以定義相同的方法或?qū)傩浴?/p>

原型編程

基于原型的編程不是面向?qū)ο缶幊讨畜w現(xiàn)的風(fēng)格,且行為重用(在基于類的語言中也稱為繼承)是通過裝飾它作為原型的現(xiàn)有對象的過程實現(xiàn)的。這種模式也被稱為弱類化,原型化,或基于實例的編程。

原始的(也是最典型的)基于原型語言的例子是由大衛(wèi)·安格爾和蘭德爾·史密斯開發(fā)的。然而,弱類化的編程風(fēng)格近來變得越來越流行,并已被諸如JavaScript,Cecil,NewtonScript,IO,MOO,REBOL,Kevo,Squeak(使用框架操縱Morphic組件),和其他幾種編程語言采用。

JavaScript面向?qū)ο缶幊?/h1>

命名空間

命名空間是一個容器,它允許開發(fā)人員在一個獨(dú)特的,特定于應(yīng)用程序的名稱下捆綁所有的功能。在JavaScript中,命名空間只是另一個包含方法,屬性,對象的對象。

注意:需要認(rèn)識到重要的一點(diǎn)是:與其他面向?qū)ο缶幊陶Z言不同的是,Javascript中的普通對象和命名空間在語言層面上沒有區(qū)別。這點(diǎn)可能會讓JavaScript初學(xué)者感到迷惑。

創(chuàng)造的JavaScript命名空間背后的想法很簡單:一個全局對象被創(chuàng)建,所有的變量,方法和功能成為該對象的屬性。使用命名空間也最大程度地減少應(yīng)用程序的名稱沖突的可能性。

我們來創(chuàng)建一個全局變量叫做 MYAPP

// 全局命名空間
varMYAPP = MYAPP || {};

在上面的代碼示例中,我們首先檢查MYAPP是否已經(jīng)被定義(是否在同一文件中或在另一文件)。如果是的話,那么使用現(xiàn)有的MYAPP全局對象,否則,創(chuàng)建一個名為MYAPP的空對象用來封裝方法,函數(shù),變量和對象。

我們也可以創(chuàng)建子命名空間:

// 子命名空間
MYAPP.event = {};

下面是用于創(chuàng)建命名空間和添加變量,函數(shù)和方法的代碼寫法:

// 給普通方法和屬性創(chuàng)建一個叫做MYAPP.commonMethod的容器
MYAPP.commonMethod = {

??regExForName:?"",?// 定義名字的正則驗證

??regExForPhone:?"",?// 定義電話的正則驗證

??validateName:?function(name){

????// 對名字name做些操作,你可以通過使用“this.regExForname”

????// 訪問regExForName變量

??},

??validatePhoneNo:?function(phoneNo){

????// 對電話號碼做操作

??}

}

// 對象和方法一起申明

MYAPP.event = {

????addListener:?function(el, type, fn) {

????//? 代碼

????},

???removeListener:?function(el, type, fn) {

????// 代碼

???},

???getEvent:?function(e) {

???// 代碼

???}

???// 還可以添加其他的屬性和方法

}

//使用addListener方法的寫法:

MYAPP.event.addListener("yourel",?"type", callback);

標(biāo)準(zhǔn)內(nèi)置對象

JavaScript有包括在其核心的幾個對象,例如,Math,Object,Array和String對象。下面的例子演示了如何使用Math對象的random()方法來獲得一個隨機(jī)數(shù)。

1console.log(Math.random());

注意:這里和接下來的例子都假設(shè)名為console.log的方法全局有定義。console.log實際上不是 JavaScript 自帶的。

查看JavaScript 參考:全局對象了解 JavaScript 內(nèi)置對象的列表。

JavaScript 中的每個對象都是Object對象的實例且繼承它所有的屬性和方法。

自定義對象

JavaScript 是一種基于原型的語言,它沒類的聲明語句,比如 C+ + 或 Java 中用的。這有時會對習(xí)慣使用有類申明語句語言的程序員產(chǎn)生困擾。相反,JavaScript可用方法作類。定義一個類跟定義一個函數(shù)一樣簡單。在下面的例子中,我們定義了一個新類Person。

functionPerson() { }

// 或

varPerson =?function(){ }

對象(類的實例)

我們使用new?obj?創(chuàng)建對象obj的新實例, 將結(jié)果(obj 類型)賦值給一個變量方便稍后調(diào)用。

在下面的示例中,我們定義了一個名為Person的類,然后我們創(chuàng)建了兩個Person的實例(person1andperson2)。

functionPerson() { }

varperson1 =?newPerson();

varperson2 =?newPerson();

注意:有一種新增的創(chuàng)建未初始化實例的實例化方法,請參考Object.create

構(gòu)造器

在實例化時構(gòu)造器被調(diào)用 (也就是對象實例被創(chuàng)建時)。構(gòu)造器是對象中的一個方法。 在JavaScript中函數(shù)就可以作為構(gòu)造器使用,因此不需要特別地定義一個構(gòu)造器方法,每個聲明的函數(shù)都可以在實例化后被調(diào)用執(zhí)行。

構(gòu)造器常用于給對象的屬性賦值或者為調(diào)用函數(shù)做準(zhǔn)備。 在本文的后面描述了類中方法既可以在定義時添加,也可以在使用前添加。

在下面的示例中,Person類實例化時構(gòu)造器調(diào)用一個alert函數(shù)。

functionPerson() {

??alert('Person instantiated');

}

varperson1 =?newPerson();

varperson2 =?newPerson();

屬性 (對象屬性)

屬性就是 類中包含的變量;每一個對象實例有若干個屬性. 為了正確的繼承,屬性應(yīng)該被定義在類的原型屬性 (函數(shù))中。

可以使用 關(guān)鍵字this調(diào)用類中的屬性, this是對當(dāng)前對象的引用。 從外部存取(讀/寫)其屬性的語法是:InstanceName.Property; 這與C++,Java或者許多其他語言中的語法是一樣的 (在類中語法this.Property常用于set和get屬性值)

在下面的示例中,我們?yōu)槎xPerson類定義了一個屬性firstName并在實例化時賦初值。

functionPerson(firstName) {

??this.firstName = firstName;

??alert('Person instantiated');

}

varperson1 =?newPerson('Alice');

varperson2 =?newPerson('Bob');

// Show the firstName properties of the objects

alert('person1 is '+ person1.firstName);?// alerts "person1 is Alice"

alert('person2 is '+ person2.firstName);?// alerts "person2 is Bob"

方法(對象屬性)

方法與屬性很相似, 不同的是:一個是函數(shù),另一個可以被定義為函數(shù)。 調(diào)用方法很像存取一個屬性,? 不同的是add()在方法名后面很可能帶著參數(shù)。為定義一個方法, 需要將一個函數(shù)賦值給類的prototype屬性; 這個賦值給函數(shù)的名稱就是用來給對象在外部調(diào)用它使用的。

在下面的示例中,我們給Person類定義了方法sayHello(),并調(diào)用了它。

functionPerson(firstName) {

??this.firstName = firstName;

}

Person.prototype.sayHello =?function() {

??alert("Hello, I'm "+?this.firstName);

};

varperson1 =?newPerson("Alice");

varperson2 =?newPerson("Bob");

// call the Person sayHello method.

person1.sayHello();?// alerts "Hello, I'm Alice"

person2.sayHello();?// alerts "Hello, I'm Bob"

在JavaScript中方法通常是一個綁定到對象中的普通函數(shù), 這意味著方法可以在其所在context之外被調(diào)用。 思考下面示例中的代碼:

functionPerson(firstName) {

??this.firstName = firstName;

}

Person.prototype.sayHello =?function() {

??alert("Hello, I'm "+?this.firstName);

};

varperson1 =?newPerson("Alice");

varperson2 =?newPerson("Bob");

varhelloFunction = person1.sayHello;

person1.sayHello();?????????????????????????????????// alerts "Hello, I'm Alice"

person2.sayHello();?????????????????????????????????// alerts "Hello, I'm Bob"

helloFunction();????????????????????????????????????// alerts "Hello, I'm undefined" (or fails with a TypeError in strict mode)

console.log(helloFunction === person1.sayHello);??????????// logs true

console.log(helloFunction === Person.prototype.sayHello);?// logs true

helloFunction.call(person1);????????????????????????// logs "Hello, I'm Alice"

如上例所示, 所有指向sayHello函數(shù)的引用,包括person1,Person.prototype, 和helloFunction等, 均引用了相同的函數(shù).

在調(diào)用函數(shù)的過程中,this的值取決于我們怎么樣調(diào)用函數(shù).??在通常情況下,我們通過一個表達(dá)式person1.sayHello()來調(diào)用函數(shù):即從一個對象的屬性中得到所調(diào)用的函數(shù)。此時this被設(shè)置為我們?nèi)〉煤瘮?shù)的對象(即person1)。這就是為什么person1.sayHello()使用了姓名“Alice”而person2.sayHello()使用了姓名“bob”的原因。

然而我們使用不同的調(diào)用方法時,this的值也就不同了。當(dāng)從變量helloFunction()中調(diào)用的時候,this就被設(shè)置成了全局對象 (在瀏覽器中即window)。由于該對象 (非常可能地) 沒有firstName屬性, 我們得到的結(jié)果便是"Hello, I'm undefined". (這是松散模式下的結(jié)果, 在嚴(yán)格模式中,結(jié)果將不同(此時會產(chǎn)生一個error)。?但是為了避免混淆,我們在這里不涉及細(xì)節(jié)) 。另外,我們可以像上例末尾那樣,使用Function#call(或者Function#apply)顯式的設(shè)置this的值。

更多有關(guān)信息請參考Function#callandFunction#apply

繼承

創(chuàng)建一個或多個類的專門版本類方式稱為繼承(Javascript只支持單繼承)。?創(chuàng)建的專門版本的類通常叫做子類,另外的類通常叫做父類。 在Javascript中,繼承通過賦予子類一個父類的實例并專門化子類來實現(xiàn)。在現(xiàn)代瀏覽器中你可以使用Object.create實現(xiàn)繼承。

JavaScript 并不檢測子類的prototype.constructor(見Object.prototype), 所以我們必須手動申明它。

在下面的例子中, 我們定義了Student類作為Person類的子類。之后我們重定義了sayHello()方法并添加了sayGoodBye() 方法。

// 定義Person構(gòu)造器

functionPerson(firstName) {

??this.firstName = firstName;

}

// 在Person.prototype中加入方法

Person.prototype.walk =?function(){

??alert("I am walking!");

};

Person.prototype.sayHello =?function(){

??alert("Hello, I'm "+?this.firstName);

};

// 定義Student構(gòu)造器

functionStudent(firstName, subject) {

??// 調(diào)用父類構(gòu)造器, 確保(使用Function#call)"this" 在調(diào)用過程中設(shè)置正確

??Person.call(this, firstName);

??// 初始化Student類特有屬性

??this.subject = subject;

};

// 建立一個由Person.prototype繼承而來的Student.prototype對象.

// 注意: 常見的錯誤是使用 "new Person()"來建立Student.prototype.

// 這樣做的錯誤之處有很多, 最重要的一點(diǎn)是我們在實例化時

// 不能賦予Person類任何的FirstName參數(shù)

// 調(diào)用Person的正確位置如下,我們從Student中來調(diào)用它

Student.prototype = Object.create(Person.prototype);?// See note below

// 設(shè)置"constructor" 屬性指向Student

Student.prototype.constructor = Student;

// 更換"sayHello" 方法

Student.prototype.sayHello =?function(){

??console.log("Hello, I'm "+?this.firstName +?". I'm studying "+?this.subject +?".");

};

// 加入"sayGoodBye" 方法

Student.prototype.sayGoodBye =?function(){

??console.log("Goodbye!");

};

// 測試實例:

varstudent1 =?newStudent("Janet",?"Applied Physics");

student1.sayHello();???// "Hello, I'm Janet. I'm studying Applied Physics."

student1.walk();???????// "I am walking!"

student1.sayGoodBye();?// "Goodbye!"

// Check that instanceof works correctly

console.log(student1?instanceofPerson);??// true

console.log(student1?instanceofStudent);?// true

對于 “ Student.prototype = Object.create(Person.prototype); ” 這一行,在不支持Object.create方法的老 JavaScript 引擎中,可以使用一個"polyfill"(又名"shim",查看文章鏈接),或者使用一個 function 來獲得相同的返回值,就像下面:

functioncreateObject(proto) {

????functionctor() { }

????ctor.prototype = proto;

????returnnewctor();

}

// Usage:

Student.prototype = createObject(Person.prototype);

更多相關(guān)信息請參考?Object.create,連接中還有一個老JavaScript引擎的兼容方案(shim)。

封裝

在上一個例子中,Student類雖然不需要知道Person類的walk()方法是如何實現(xiàn)的,但是仍然可以使用這個方法;Student類不需要明確地定義這個方法,除非我們想改變它。?這就叫做封裝,對于所有繼承自父類的方法,只需要在子類中定義那些你想改變的即可。

抽象

抽象是允許模擬工作問題中通用部分的一種機(jī)制。這可以通過繼承(具體化)或組合來實現(xiàn)。

JavaScript通過繼承實現(xiàn)具體化,通過讓類的實例是其他對象的屬性值來實現(xiàn)組合。

JavaScript Function 類繼承自O(shè)bject類(這是典型的具體化) 。Function.prototype的屬性是一個Object實例(這是典型的組合)。

varfoo =?function(){};

console.log(?'foo is a Function: '+ (foo?instanceofFunction) );??????????????????// logs "foo is a Function: true"

console.log(?'foo.prototype is an Object: '+ (foo.prototype?instanceofObject) );?// logs "foo.prototype is an Object: true"

多態(tài)

就像所有定義在原型屬性內(nèi)部的方法和屬性一樣,不同的類可以定義具有相同名稱的方法;方法是作用于所在的類中。并且這僅在兩個類不是父子關(guān)系時成立(繼承鏈中,一個類不是繼承自其他類)。


原文:JavaScript面向?qū)ο蠛喗?/a>