面向對象的JavaScript基本知識指南大全
譯文由于jQuery和MooTools等精心開發的庫,JavaScript已成為前端開發的基礎。不過,我們要留意這些優秀庫中所運用的較高級概念,這點極其重要。原因何在?因為作為Web開發人員,對待學習最新的編程趨勢和試圖把那些趨勢推向極致,我們必須予以一視同仁。要不然,Web開發領域就不會出現創新。所以,我們不妨花點時間來了解JavaScript面向對象編程的基本知識,包括類、繼承和范圍。
類
在我們學習如何把類實施到代碼中之前,不妨討論一下類是什么、為什么有必要學習/使用類。
正如Java文檔聲明的那樣:“類是用來創建一個個對象的藍圖。”這藍圖就像造房子過程中所用的實際藍圖。建造人員使用藍圖來評估房子有什么樣的屬性,房子會有什么樣的功能。類是表示對象屬性的一種很方便的方式,無論這對象是房子、汽車還是人。當存在的某個對象不止一個時,類就變得特別有用。
比如說,我們不使用類來比較一下兩個實際的對象。這體現了程序思考過程,而不是面向對象的思考過程。我們將描述一個名叫Rob的男子和一個名為Emillee的小女孩。我們必須假定我們對人體一無所知,因為我們沒有藍圖(類)可供使用。
Rob:
1. Rob在身體的上部有兩個橢圓形的結構,相隔幾英寸。這些橢圓形結構有一個黑色背景,中間是棕色。
2. Rob有兩個與地面相對平行的結構,似乎表明了人體中最垂直的部分,這仍是身體基部的一部分。
3. Rob有兩個附屬物,從另外兩個附屬物延伸過來。這些似乎可用來抓取物件。它們似乎比較大。
4. Rob高度約6英尺。
5. Rob無意識地吸入氧,把氧轉換成二氧化碳。
Emilee:
1. Emillee在身體的上部有兩個橢圓形的結構,相隔幾英寸。這些橢圓形結構有一個黑色背景,中間是藍色。
2. Emillee有兩個與地面相對平行的結構,似乎表明了人體中最垂直的部分,這仍是身體基部的一部分。
3. Emillee有兩個附屬物,從另外兩個附屬物延伸過來。這些似乎可用來抓取物件。它們似乎比較小。
4. Emillee高度約1.5英尺。
5. Emillee無意識地吸入氧,把氧轉換成二氧化碳。
單單描述一個人的1)眼睛、2)肩膀、3)雙手、4)身高和5)呼吸行為就有大量的工作要做。要注意:我們不得不兩次給出幾乎一模一樣的看法,因為我們沒有藍圖可供使用。雖然描述兩個人不是太費勁,但是如果我們想要描述100個人、1000個人或者100萬個人,怎么辦?肯定有一種更高效的方法來描述有著類似屬性的對象:這正是類的亮點。
我們不妨使用面向對象的理念,重新考慮前一個例子。由于我們描述的是男子和小女孩,我們知道他們都是人類。所以不妨先為人類創建一個簡單的藍圖。
人類:
1. 身體的上部有兩個橢圓形的結構。這些橢圓形結構有一個黑色背景,中間顏色不一樣。我們稱之為眼睛。
2. 有兩個與地面相對平行的結構,似乎表明了人體中最垂直的部分,這仍是身體基部的一部分。我們稱之為肩膀。
3. 有兩個附屬物,從另外兩個附屬物延伸過來。這些似乎可用來抓取物件。它們的大小不一樣。我們稱之為雙手。
4. 視年齡及其他因素而定,高度不一樣。我們稱之為身高。
5. 無意識地吸入氧,并把氧轉換成二氧化碳。我們稱之為呼吸。
于是我們已聲明,人類的屬性是,他們有眼睛,有肩膀,有雙手,有身高。我們還已聲明,這些屬性可能不一樣。定義了人類的藍圖后,并且聲明了Rob和Emillee是人類后,我們可以將已經知道的關于人類的屬性運用到Rob和Emillee。
Rob是人類。
1. Rob有棕色的眼睛
2. Rob有肩膀
3. Rob有大大的雙手
4. Rob身高6英寸
5. Rob會呼吸
Emillee是人類。
1. Emillee有藍色的眼睛
2. Emillee有肩膀
3. Emillee有小小的雙手
4. Emillee身高1.5英尺
5. Emillee會呼吸
我們只要明確聲明Rob和Emillee是人類,就可以把與人類有關的屬性和功能直接運用到Rob和Emillee。這讓我們可以避免重新定義身體的所有部位,同時讓我們可以高效地描述這兩個對象之間的重要區別。
下面是關于類及對象(名為類的實例)的幾個例子,以便你明白兩者之間的關系。
類Student(學生)
◆ 屬性:年級、年齡、出生日期和學生身份標志(SSID)
◆ 功能:計算年級平均成績、查看缺課情況、更新操行評語
類Employee(員工)
◆ 屬性:雇主身份識別號(EIN)、小時工資、聯系號碼、保險
◆ 功能:設定薪水、查看工作效率和獲取簡歷
類Computer(電腦)
◆ 屬性:處理器、主機、顯示器
◆ 功能:開機、關機和重啟
好了,我們已明白了類背后的概念,不妨把所知道的東西運用到JavaScript。與包括PHP和C++在內的語言不一樣,JavaScript沒有類數據類型。不過,如果我們借助JavaScript的靈活性,就很容易使用函數來模擬類。
我們以前面一個例子為例,使用類來表示學生。
創建一個類時,你必須做兩件事:必須知道這個類有什么屬性/函數(又叫方法);你需要用一個值來初始化屬性。
- function Student(name, gender, age, grade, teacher)
- {
- this.name = name;
- this.gender = gender;
- this.age = age;
- this.grade = grade;
- this.teacher = teacher;
- }
- var bob = new Student("bob", "male", 15, 10, "Marlow");
- alert(bob.age); //輸出15
- var susan = new Student("susan", "female", 10, 5, "Gresham");
- alert(susan.gender); //輸出 'female'
我們可以從這個例子中看出,類的實例使用新的運算符來進行初始化。類的屬性和方法使用. (dot)運算符來訪問。所以為了獲得名為bob的Student類實例的屬性年齡,我們只要使用bob.age。同樣,我們創建了Student類的一個實例,把它分配給susan。為了獲得susan的性別,我們只要使用susan.gender。類在代碼可讀性方面帶來了巨大的好處:你不需要有任何編程經驗,就能推斷出bob.age是bob的年齡。
不過,前一個例子有兩個不好(但很容易修復)的缺點。
1)任何語句都可以訪問類屬性
2)參數必須以一定的次序來傳遞
#p#
確保屬性值的私有性
請注意:在前一個例子中,我們只要調用bob.age,就能獲得bob.age的值。此外,我們可以在程序中任何地方將bob.age設成自己喜歡的任何值。
- var bob = new Student("bob", "male", 15, 10, "Marlow");
- alert(bob.age); //輸出15
- bob.age = 9;
- alert(bob.age); //輸出9;
看起來沒有害處,是不是?那么請考慮這個例子。
- var bob = new Student("bob", "male", 15, 10, "Marlow");
- alert(bob.age); //輸出15
- bob.age = -50;
- alert(bob.age); //輸出-50;
我們看到了年齡是負值:這在邏輯上不一致。我們只要使用私有變量(private variable)這個概念,就可以防止諸如此類的問題、確保數據的完整性。私有變量是只能在類本身里面訪問的變量。雖然JavaScript再次沒有用于確保變量私有性的保留字,但是JavaScript為我們提供了創造同樣效果的工具。
- function Student(name, gender, age, grade, teacher)
- {
- var studentName = name;
- var studentGender = gender;
- var studentGrade = grade;
- var studentTeacher = teacher;
- var studentAge = age;
- this.getAge = function()
- {
- return studentAge;
- };
- this.setAge = function(val)
- {
- studentAge = Math.abs(val); //使用絕對值,確保年齡是正值
- };
- }
- var bob = new Student("bob", "male", 15, 10, "Marlow");
- alert(bob.studentAge); //未定義,因為年齡在類定義中受私有保護
- alert(bob.getAge()); //輸出15
- bob.setAge(-20);
- alert(bob.getAge()); //輸出20
通過使用變量聲明,而不是直接為類賦予屬性,我們保護了年齡數據的完整性。由于JavaScript使用了函數范圍,我們的類里面聲明的變量在該類外面是無法訪問的,除非由該類里面的函數明確返回。方法this.getAge將學生年齡返回到調用環境,它名為訪問器方法(Accessor method)。訪問器方法返回屬性的值,那樣該值就可以在類的外面使用,而不影響類里面的值。按照約定,訪問器方法的前綴通常是“get”這個字。方法this.setAge名為更改器方法(Mutator method)。其目的是,改變屬性的值,保護完整性。
我們已看到了在類里面使用訪問器方法和更改器方法來保護數據完整性的好處。不過,為每個屬性創建訪問器方法帶來了極其冗長的代碼。
- function Student(name, gender, age, grade, teacher)
- {
- var studentName = name;
- var studentGender = gender;
- var studentGrade = grade;
- var studentTeacher = teacher;
- var studentAge = age;
- this.getName = function()
- {
- return studentName;
- };
- this.getGender = function()
- {
- return studentGender;
- };
- this.getGrade = function()
- {
- return studentGrade;
- };
- this.getTeacher = function()
- {
- return studentTeacher;
- };
- this.getAge = function()
- {
- return studentAge;
- };
- this.setAge = function(val)
- {
- studentAge = Math.abs(val); //使用絕對值,確保年齡是正值
- };
- }
- var bob = new Student("bob", "male", 15, 10, "Marlow");
- alert(bob.studentGender); //未定義,因為性別在類定義中受私有保護
- alert(bob.getGender()); //輸出 'male'
教我C++的教授總是說:“如果你發現自己反復輸入相同的代碼,這表明你的做法不對。”的確,有更高效的方法可以為每個屬性創建訪問器方法。此外,這種機制還不需要以特定次序來調用函數參數。
動態創建的訪問器方法
這個演示來自John Resig所寫的《專業JavaScript技巧》一書(我強烈建議各位讀一讀。前三章就非常值得一讀)。
- function Student( properties )
- {
- var $this = this; //將類范圍存儲到名為$this的變量中
- //迭代處理對象的屬性
- for ( var i in properties )
- {
- (function(i)
- {
- // 動態創建訪問器方法
- $this[ "get" + i ] = function()
- {
- return properties[i];
- };
- })(i);
- }
- }
- // 創建一個新的用戶對象實例,并傳遞屬性的對象
- var student = new Student(
- {
- Name: "Bob",
- Age: 15,
- Gender: "male"
- });
- alert(student.name); //因屬性是私有的而未定義
- alert(student.getName()); //輸出 "Bob"
- alert(student.getAge()); //輸出15
- alert(student.getGender()); //輸出 "male"
通過實施這個技巧,我們不但確保自己的屬性是私有的,而且不需要按次序來指定參數。下面的類實例化都相同:
- var student = new Student(
- {
- Name: "Bob",
- Age: 15,
- Gender: "male"
- });
- var student = new Student(
- {
- Age: 15,
- Name: "Bob",
- Gender: "male"
- });
- var student = new Student(
- {
- Gender: "male",
- Age: 15,
- Name: "Bob"
- });
#p#
繼承
在這篇文章中,我使用“類”這個術語極其寬松。如前所述,JavaScript沒有類實體,但是后面仍可以跟類的模式。JavaScript與其他面向對象語言的區別主要在于繼承模型。C++和Java體現了基于類的繼承或傳統繼承。另一方面,JavaScript體現了原型繼承(Prototypal Inheritance)。在其他面向對象語言中,類是一個實際的數據類型,表示創建對象的藍圖。在JavaScript中,雖然我們可以使用函數來模擬對象藍圖,但是它們實際上本身就是對象。然后,這些對象用作其他對象的模型(又叫原型),可以參閱文章《JavaScript原型繼承》(http://www.webreference.com/programming/javascript/prototypal_inheritance/index.html)。
運用原型繼承這個概念讓我們得以創建“子類”,即繼承另一個對象的屬性的對象。如果我們想使用另一個對象的稍有一些變動的方法,這就變得極其有用。
以類Employee(員工)為例。假設我們有兩種類型的員工:一種基于薪水,一種基于傭金。這些員工類型會有許多相似的屬性。比如說,不管某員工通過傭金獲得收入還是通過薪水獲得收入,該員工都有名稱。不過,對基于傭金的員工和基于薪水的員工來說,收入方式完全不一樣。下面這個例子體現了這個概念:
- function Worker()
- {
- this.getMethods = function(properties, scope)
- {
- var $this = scope; //將類范圍存儲到名為$this的變量中
- //迭代處理對象的屬性
- for ( var i in properties )
- {
- (function(i)
- {
- // 動態創建訪問器方法
- $this[ "get" + i ] = function()
- {
- return properties[i];
- };
- //動態地創建一個分析整數,并確保是正值的更改器方法。
- $this[ "set" + i ] = function(val)
- {
- if(isNaN(val))
- {
- properties[i] = val;
- }
- else
- {
- properties[i] = Math.abs(val);
- }
- };
- })(i);
- }
- };
- }
- // CommissionWorker "子類"和WageWorker "子類"
- //繼承Worker的屬性和方法。
- CommissionWorker.prototype = new Worker();
- WageWorker.prototype = new Worker();
- function CommissionWorker(properties)
- {
- this.getMethods(properties, this);
- //計算收入
- this.getIncome = function()
- {
- return properties.Sales * properties.Commission;
- }
- }
- //要求有下列屬性:薪水、每周小時數、每年周數
- function WageWorker(properties)
- {
- this.getMethods(properties, this);
- //計算收入
- this.getIncome = function()
- {
- return properties.Wage * properties.HoursPerWeek * properties.WeeksPerYear;
- }
- }
- var worker = new WageWorker(
- {
- Name: "Bob",
- Wage: 10,
- HoursPerWeek: 40,
- WeeksPerYear: 48
- });
- alert(worker.wage); //未定義。薪水是私有屬性。
- worker.setWage(20);
- alert(worker.getName()); //輸出 "Bob"
- alert(worker.getIncome()); //輸出 38,400 (20*40*48)
- var worker2 = new CommissionWorker(
- {
- Name: "Sue",
- Commission: .2,
- Sales: 40000
- });
- alert(worker2.getName()); //輸出 "Sue"
- alert(worker2.getIncome()); //輸出8000(2% 乘40,000)
前一個例子中最重要的兩個語句是:
- CommissionWorker.prototype = new Worker();
- WageWorker.prototype = new Worker();
這聲明,對新的CommissionWorker或新的WageWorker對象的每個實例而言,Worker的屬性和方法將傳遞到那些新對象。如果需要的話,這些方法和屬性可以在“子類”定義里面被覆蓋寫入。
范圍
JavaScript體現了所謂的函數范圍。這意味著,函數中聲明的變量在函數(變量來自該函數)外面最初是無法訪問的。不過,在語句塊中(如條件語句),可以對調用環境進行變量聲明或改動。
- var car = "Toyota";
- if(car == "Toyota")
- {
- car = "Toyota - We never stop...and you won't either.";
- }
- alert(car); //輸出Toyota——我們從未停上,你也如此。
- car = "Toyota"; //將汽車設回成原始值。
- function makeFord(car)
- {
- car = "Ford";
- }
- makeFord(car);
- alert(car); //輸出"Toyota",因為汽車在函數范圍中已改動。
不過,如果你想要改動值的函數,可以將對象作為參數來傳遞,并改動對象的屬性。
- var car = new Object();
- car.brand = "Toyota"
- function makeFord(car)
- {
- car.brand = "Ford";
- }
- makeFord(car);
- alert(car.brand); //輸出“Ford”
這名為“通過調用”傳遞,將值傳遞給函數。只有你在類里面創建方法,又知道對象含有什么屬性,我一般才會建議采用通過調用傳遞。
現在你已經掌握了運用到JavaScript的面向對象的基本知識。運用這些原則,就可以為你在將來的開發項目簡化代碼。
原文:http://www.1stwebdesigner.com/design/object-oriented-basics-javascript/
【編輯推薦】