Javascript 物件導向式程式設計基礎講解
目錄
最近我與一位擁有五年開發 Web 應用程式經驗的軟體開發人員面談。她使用 JavaScript 的經驗已長達四年半的時間,對自己的 JavaScript 技巧有很高的評價,但後來我很快發現,她其實對 JavaScript 一知半解。不過,我並沒有因此而責怪她。這就是 JavaScript 令人意想不到的所在。許多使用者 (直到最近,包括我自己在內喔) 都以為自己很懂得這個語言,只因為他們知道 C/C++/C# 或之前已有一些程式設計經驗。
從某方面來說,這種假設並非完全毫無根據。使用 JavaScript 設計一些簡單的程式很容易。它的學習門檻很低;這個語言比較容許失誤,您不需要深入瞭解這個語言就可以使用它來設計程式。即使非程式設計師也可以在幾小時內上手,撰寫一些對首頁有用的指令碼。
事實上,直到最近我才發現,我一直靠著對 JavaScript 貧乏的認知,憑藉著 MSDN® DHTML 參考手冊和我的 C++/C# 使用經驗,在勉強應付著。直到我開始設計真正的 AJAX 應用程式之後,才瞭解自己是如此欠缺 JavaScript 技巧。新一代 Web 應用程式的複雜性和互動性,需要以完全不同的方法來撰寫 JavaScript 程式碼。這些需要真正的 JavaScript 應用程式功力!我們一直以來所撰寫的用完即丟指令碼已經不夠。
物件導向程式設計 (OOP) 是許多 JavaScript 程式庫常用的方法之一,使程式碼基底更容易管理及維護。JavaScript 支援 OOP,但它支援的方式與一般符合 Microsoft® .NET Framework 規格的語言 (如 C++、C# 或 Visual Basic®) 支援的方式大不相同,因此,已長久使用那些語言工作的開發人員,一開始會覺得使用 JavaScript 進行 OOP 的方式很奇怪,違反直覺。我撰寫這篇文章是為了深入探討 JavaScript 語言真正支援物件導向程式設計的方式,以及如何利用此一支援使用 JavaScript 有效進行物件導向開發。讓我們先來討論 (還有別的嗎?) 物件。
JavaScript 物件是字典
在 C++ 或 C# 中,當我們說到物件時,指的是類別或結構的執行個體。物件有不同的屬性和方法,視產生它們的範本 (也就是類別) 而定。JavaScript 物件不是這樣。在 JavaScript 中,物件只是名稱/值配對的集合 -- 您可以把 JavaScript 物件想像成含有字串索引鍵的字典。我們可以使用熟悉的 "."(點) 運算子或 "[]" 運算子 (處理字典時通常使用此種運算子) 來取得及設定物件的屬性。下列程式碼片段
var userObject = new Object();
userObject.lastLoginTime = new Date();
alert(userObject.lastLoginTime);
var userObject = {}; // equivalent to new Object()
userObject[「lastLoginTime」] = new Date();
alert(userObject[「lastLoginTime」]);
var userObject = { 「lastLoginTime」: new Date() };
alert(userObject.lastLoginTime);
JavaScript 函數是高級函數
function func(x) {
alert(x);
}
func(「blah」);
var func = function(x) {
alert(x);
};
func(「blah2」);
var func = new Function(「x」, 「alert(x);」);
func(「blah3」);
function sayHi(x) {
alert(「Hi, 「 + x + 「!」);
}
sayHi.text = 「Hello World!」;
sayHi[「text2」] = 「Hello World... again.」;
alert(sayHi[「text」]); // displays 「Hello World!」
alert(sayHi.text2); // displays 「Hello World... again.」
Figure 1 JavaScript 中的函數是高級函數
// assign an anonymous function to a variable
var greet = function(x) {
alert(「Hello, 「 + x);
};
greet(「MSDN readers」);
// passing a function as an argument to another
function square(x) {
return x * x;
}
function operateOn(num, func) {
return func(num);
}
// displays 256
alert(operateOn(16, square));
// functions as return values
function makeIncrementer() {
return function(x) { return x + 1; };
}
var inc = makeIncrementer();
// displays 8
alert(inc(7));
// functions stored as array elements
var arr = [];
arr[0] = function(x) { return x * x; };
arr[1] = arr[0](2);
arr[2] = arr[0](arr[1]);
arr[3] = arr[0](arr[2]);
// displays 256
alert(arr[3]);
// functions as object properties
var obj = { 「toString」 : function() { return 「This is an object.」; } };
// calls obj.toString()
alert(obj);
var myDog = {
「name」 : 「Spot」,
「bark」 : function() { alert(「Woof!」); },
「displayFullName」 : function() {
alert(this.name + 「 The Alpha Dog」);
},
「chaseMrPostman」 : function() {
// implementation beyond the scope of this article
}
};
myDog.displayFullName();
myDog.bark(); // Woof!
Figure 2 當物件變更時,「this」 也會跟著變更
function displayQuote() {
// the value of 「this」 will change; depends on
// which object it is called through
alert(this.memorableQuote);
}
var williamShakespeare = {
「memorableQuote」: 「It is a wise father that knows his own child.」,
「sayIt」 : displayQuote
};
var markTwain = {
「memorableQuote」: 「Golf is a good walk spoiled.」,
「sayIt」 : displayQuote
};
var oscarWilde = {
「memorableQuote」: 「True friends stab you in the front.」
// we can call the function displayQuote
// as a method of oscarWilde without assigning it
// as oscarWilde』s method.
//」sayIt」 : displayQuote
};
williamShakespeare.sayIt(); // true, true
markTwain.sayIt(); // he didn』t know where to play golf
// watch this, each function has a method call()
// that allows the function to be called as a
// method of the object passed to call() as an
// argument.
// this line below is equivalent to assigning
// displayQuote to sayIt, and calling oscarWilde.sayIt().
displayQuote.call(oscarWilde); // ouch!
alert(「NaN is NaN: 「 + isNaN(NaN));
function x() {
this.isNaN = function() {
return 「not anymore!」;
};
}
// alert!!! trampling the Global object!!!
x();
alert(「NaN is NaN: 「 + isNaN(NaN));
有建構函式但沒有類別
Dog spot = new Dog();
function DogConstructor(name) {
this.name = name;
this.respondTo = function(name) {
if(this.name == name) {
alert(「Woof」);
}
};
}
var spot = new DogConstructor(「Spot」);
spot.respondTo(「Rover」); // nope
spot.respondTo(「Spot」); // yeah!
var spot = new DogConstructor(「Spot」);
// create an empty object
var spot = {};
// call the function as a method of the empty object
DogConstructor.call(spot, 「Spot」);
// Think of this as class Dog
function Dog(name) {
// instance variable
this.name = name;
// instance method? Hmmm...
this.respondTo = function(name) {
if(this.name == name) {
alert(「Woof」);
}
};
}
var spot = new Dog(「Spot」);
function respondTo() {
// respondTo definition
}
function Dog(name) {
this.name = name;
// attached this function as a method of the object
this.respondTo = respondTo;
}
原型
var buddy = new Dog(「Buddy「);
var spot = new Dog(「Spot」);
// Dog.prototype is the prototype of spot
alert(Dog.prototype.isPrototypeOf(spot));
// spot inherits the constructor property
// from Dog.prototype
alert(spot.constructor == Dog.prototype.constructor);
alert(spot.constructor == Dog);
// But constructor property doesn』t belong
// to spot. The line below displays 「false」
alert(spot.hasOwnProperty(「constructor」));
// The constructor property belongs to Dog.prototype
// The line below displays 「true」
alert(Dog.prototype.hasOwnProperty(「constructor」));
Dog.prototype = new Object();
- 繼承的物件會立即看到對原型物件所做的變更,即使變更是發生在已建立繼承的物件之後。
- 如果您在物件中定義屬性/方法 X,相同名稱的屬性/方法將隱藏在該物件的原型中。例如,您可以在 Dog.prototype 中定義 toString 方法,來覆寫 Object.prototype 的 toString 方法。
- 變更僅從原型到其衍生物件單向進行,而不會反向進行。
function GreatDane() { }
var rover = new GreatDane();
var spot = new GreatDane();
GreatDane.prototype.getBreed = function() {
return 「Great Dane」;
};
// Works, even though at this point
// rover and spot are already created.
alert(rover.getBreed());
// this hides getBreed() in GreatDane.prototype
spot.getBreed = function() {
return 「Little Great Dane」;
};
alert(spot.getBreed());
// but of course, the change to getBreed
// doesn』t propagate back to GreatDane.prototype
// and other objects inheriting from it,
// it only happens in the spot object
alert(rover.getBreed());
靜態屬性和方法
function DateTime() { }
// set static method now()
DateTime.now = function() {
return new Date();
};
alert(DateTime.now());
Closure
Figure 8 根據述詞篩選元素
function filter(pred, arr) {
var len = arr.length;
var filtered = []; // shorter version of new Array();
// iterate through every element in the array...
for(var i = 0; i < len; i++) {
var val = arr[i];
// if the element satisfies the predicate let it through
if(pred(val)) {
filtered.push(val);
}
}
return filtered;
}
var someRandomNumbers = [12, 32, 1, 3, 2, 2, 234, 236, 632,7, 8];
var numbersGreaterThan100 = filter(
function(x) { return (x > 100) ? true : false; },
someRandomNumbers);
// displays 234, 236, 632
alert(numbersGreaterThan100);
var greaterThan300 = filter(
function(x) { return (x > 300) ? true : false; },
someRandomNumbers);
function makeGreaterThanPredicate(lowerBound) {
return function(numberToCheck) {
return (numberToCheck > lowerBound) ? true : false;
};
}
var greaterThan10 = makeGreaterThanPredicate(10);
var greaterThan100 = makeGreaterThanPredicate(100);
alert(filter(greaterThan10, someRandomNumbers));
alert(filter(greaterThan100, someRandomNumbers));
模擬私用屬性
function Person(name, age) {
this.getName = function() { return name; };
this.setName = function(newName) { name = newName; };
this.getAge = function() { return age; };
this.setAge = function(newAge) { age = newAge; };
}
var ray = new Person(「Ray」, 31);
alert(ray.getName());
alert(ray.getAge());
ray.setName(「Younger Ray」);
// Instant rejuvenation!
ray.setAge(22);
alert(ray.getName() + 「 is now 「 + ray.getAge() +
「 years old.」);
function Person(name, age) {
var occupation;
this.getOccupation = function() { return occupation; };
this.setOccupation = function(newOcc) { occupation =
newOcc; };
// accessors for name and age
}
Person.prototype.somePublicMethod = function() {
// doesn』t work!
// alert(this.name);
// this one below works
alert(this.getName());
};
繼承類別
// class Pet
function Pet(name) {
this.getName = function() { return name; };
this.setName = function(newName) { name = newName; };
}
Pet.prototype.toString = function() {
return 「This pet』s name is: 「 + this.getName();
};
// end of class Pet
var parrotty = new Pet(「Parrotty the Parrot」);
alert(parrotty);
Figure 10 從 Pet 類別衍生
// class Dog : Pet
// public Dog(string name, string breed)
function Dog(name, breed) {
// think Dog : base(name)
Pet.call(this, name);
this.getBreed = function() { return breed; };
// Breed doesn』t change, obviously! It』s read only.
// this.setBreed = function(newBreed) { name = newName; };
}
// this makes Dog.prototype inherits
// from Pet.prototype
Dog.prototype = new Pet();
// remember that Pet.prototype.constructor
// points to Pet. We want our Dog instances』
// constructor to point to Dog.
Dog.prototype.constructor = Dog;
// Now we override Pet.prototype.toString
Dog.prototype.toString = function() {
return 「This dog』s name is: 「 + this.getName() +
「, and its breed is: 「 + this.getBreed();
};
// end of class Dog
var dog = new Dog(「Buddy」, 「Great Dane」);
// test the new toString()
alert(dog);
// Testing instanceof (similar to the is operator)
// (dog is Dog)? yes
alert(dog instanceof Dog);
// (dog is Pet)? yes
alert(dog instanceof Pet);
// (dog is Object)? yes
alert(dog instanceof Object);
模擬命名空間
var MSDNMagNS = {};
MSDNMagNS.Pet = function(name) { // code here };
MSDNMagNS.Pet.prototype.toString = function() { // code };
var pet = new MSDNMagNS.Pet(「Yammer」);
var MSDNMagNS = {};
// nested namespace 「Examples」
MSDNMagNS.Examples = {};
MSDNMagNS.Examples.Pet = function(name) { // code };
MSDNMagNS.Examples.Pet.prototype.toString = function() { // code };
var pet = new MSDNMagNS.Examples.Pet(「Yammer」);
// MSDNMagNS.Examples and Pet definition...
// think 「using Eg = MSDNMagNS.Examples;」
var Eg = MSDNMagNS.Examples;
var pet = new Eg.Pet(「Yammer」);
alert(pet);
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
結論
隨 著互動式、仰賴用戶端之 AJAX 應用程式的擴增,JavaScript 很快成為 .NET 開發人員眾多利器當中最有用的工具之一。不過,其原型本質一開始可能會讓比較習慣 C++、C# 或 Visual Basic 等語言的開發人員不知所措。儘管一路走來出現不少挫折,我仍然覺得自己的 JavaScript 學習過程是一段很寶貴的經驗。如果本文可以幫助您更順利學習,我就感到很欣慰了,這也是我的目標。