本文原文地址:http://msdn.microsoft.com/en-us/magazine/cc163419.aspx
翻译如下:
作者:Ray Djajadinata
不久我面试了一位在开发web应用方面拥有五年编程经验的开发者,她使用javascript已有4年半的时间的了,自认为在javascript方面拥有很强的技能。随后我发现她对javascript所知甚少,但我并没有因此怪她。实际上,javascript就是这样有趣,它很容易使人们产生自己很精通它的错觉,仅仅是因为他们熟悉C/C++或C#语言,或凭着自己先前的编程经验。
从某种程度上来说,该假设并不是毫无根据的。我们很容易使用javascript来做些简单的事情。入门的门槛很低,你并不需要知道多少东西,就能够开始用它编写代码,甚至一个毫无编程经验的人,就能够通过几个小时的学习为主页写一个有用的脚本。
实际上,我一直仅凭参考MSDN上关于DHTML的内容以及自己的C++/C#的编程经验,利用自己对javascript的一知半解过活,当我在实际工作做有关AJAX应用的时候,我才意识的我对javascript理解是多么肤浅。新的web应用复杂性以及交互性的特点要求我们需用完全不同的方法来编写javascript代码。这些才是真正javascript应用程序。我们在编写一次性脚本时一直采用的方法已完全不再有效。
为了使代码库便于管理和维护,面向对象程序设计方式在许多javascript库中非常流行。JavaScript支持面向对象程序设计,但跟Microsoft®.NET Framework中的C++/C#或Visual Basic 的实现方式有很大的不同。所以对那些一直使用这些语言的开发者来说,一开始在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"]);
我们也可以如下的方式定义在userObject 中定义lastLoginTime属性
var userObject = { "lastLoginTime": new Date() };
alert(userObject.lastLoginTime);
唯一的不同点是,javascript 对象/字典只接受字符键,而不像python那样接受哈希对象。上面的例子也显示出了javascript对象比C++/C#对象拥有更多的灵活性。从代码一,代码二可以看出,lastLoginTime并不需要事先声明,假如对象userObject没有该属性时,该属性会被添加到userObject对象中。当你意识到javascript对象仅是一个字典,这看上去并不惊讶。我们可以在任何时间往字典中增加键值。我们现在知道怎么去创建的对象的属性了。那对象中的方法我们该怎么去创建呢。javascript语言不同于C++/C#,为了能够理解javascript对象中方法,我们需要更进一步的认识javascript中的函数。
在许多程序语言中,函数和对象是无法放在一起比较的。然而在javascript中,它们之间的概念很模糊,在javascript中函数是一个关联可执行代码的对象。看看下面代码中的函数:
function func(x) {
alert(x);
}
func("blah");
这是我们经常定义函数的方式,你也可以按如下的方式定义该函数,下面的方式定义个匿名函数,并把它赋给了变量func。
var func = function(x) {
alert(x);
};
func("blah2");
甚至可以安照如下方式,使用Function构造器
var func = new Function("x", "alert(x);");
func("blah3");
上面展示了函数仅仅是一个支持函数调用操作的对象。最后一种方式是使用Function 构造器来定义一个函数,这种方式并不使用广泛。但它很有趣,我们可以看出,函数的主体仅仅是Function 构造器的一个String 类型的参数。这意味着你可以在运行期间构造任意的函数。
为了进一步的演示 函数就是一个对象,我们可以为其它对象那么为函数增加属性。代码如下
function sayHi(x) {
alert("Hi, " + x + "!");
}
sayHi.text = "Hello World!";
sayHi["text2"] = "Hello World... again.";
作为一个对象, 函数也能够被分配给一个变量,或作为一个形参给传递其它函数,或作为其它函数的返回值,或作为一个对象的属性,或作为一个数组中的元素,等等代码片段一提供了如上的例子。
// 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!
对于C++/C#开发者来说,在displayFullName函数中使用this关键字感觉到很熟悉,它指的调用该方法的对象(那些使用Visual Basic开发也应该很熟悉,在Visual Basic 中 使用"Me"来代替)。所以上面的例子中,displayFullName 中this的值是myDog对象。this的值并不是不变的,当调用一个不同的对象时,this的值也将会改变,并指向另一个对象,如代码片段2所示。
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!
在代码片段2中展示了另一种调用一个函数作为一个对象的方法的方式。我们要始终记住在javascript中,一个函数就是一个对象。每一个函数都有一个名叫call的方法,,该call方法使得该函数成为第一参数的中一个方法。即,无论哪一个对象作为第一个形参被传进call方法后,都将成为该函数内部的this。这在调用基类的构造器是,是一个有用的技术。随后我们将会看到。
需要记住的一件事,绝对不要调用那些归属于哪一个对象的函数,并且该函数还包含"this"关键字。如果你这样做,那么你会污染全局命名空间,因为在这个调用中,“this”对应着全局对象,这会在你的应该程序中造成严重的破环。例如,下面的脚本将会改变javascript全局函数 isNaN的行为。这种写法我绝对不推荐。
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));
在C#中,我们使用类来实例化对象。但是JavaScript 不同于C#,它没有类的概念。在下面章节中,我们将会充分得利用函数作为构造器与new关键字在一起来实现实例化对象。
对于javascript面向对象程序设计来说,有一件事很奇怪,JavaScript 并不像C# 或C++那样有类的概念。在C#中,但你声明一个对象。你可以像下面这样
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");
“new”关键字所做的事情很简单。首先它创建一个新的空对象。然后该函数紧跟着被执行,该新的对象设置为该函数中this对应的值。换句化说,上面代码跟下面的类似。
// create an empty object
var spot = {};
// call the function as a method of the empty object
DogConstructor.call(spot, "Spot");
我们从DogConstructor可以看出,调用该函数来初始化该对象,函数中的this关键字对应着调用期间的对象。这样的话,我们可以有一种方式来为对象创建一个模版。
无论我什么时候需要创建一个对象,我们只需要调用new关键字和构造函数,这样我们得到一个初始化的对象。看起来与c++/C#的类很相似,是吧! 实际上,在javascript语言中,构造函数的名字就是类似于c++/C#中类的名字。在上面的例子中,我们为该构造函数取名为Dog。
// 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");
在上面的例子中,我定义了一个名叫"name"的实例变量。这样的话,每一个使用Dog作为构造函数的对象都拥有实例变量name 的副本,这正是我们所希望的。每一个对象都需要拥有属性来保存自己的状态。但是当我们看下一行时,我们会发现每一个Dog的实例都拥有respondTo 方法的副本,这实在是一种浪费,我们只需要一个在所有对象实例中都能够共享的respondTo方法。我们可以通过在Dog函数外面定义respondTo来实现这一个目标。代码如下所示。
function respondTo() {
// respondTo definition
}
function Dog(name) {
this.name = name;
// attached this function as a method of the object
this.respondTo = respondTo;
}
这样的话,Dog的所有实例仅仅共享respondTo方法的一个实例。但是这种写法也有它的不足之处,假如方法很多的话,代码会变的难以维护。你的代码中会参杂着很多全局变量,尤其是你拥有很多"类"的时候,如果它们又拥有类似的方法名称,那会变得更糟糕。有一种更好的方法来解决该难题,那就是使用原型对象。下面的章节中,我们一起来讨论该话题
原型对象在Javascript面向对象编程是一个很重要的内容。在JavaScript中有一个这样的概念,每一个对象都是作为一个已经存在的对象(原型对象)的副本的而创建的。原型对象的所有属性和方法都够被该原型构造器所创建的对象拥有。当你按如下的方式创建一个新的Dog对象时,我们可以这样说,该对象中属性和方法都继承于原型。
var buddy = new Dog("Buddy");
buddy对象从自己原型中继承了属性和方法,但我们无法从上面的一行代码中得知该原型从哪里来。实际上,对象buddy的原型(prototyoe)来自构造函数的一个属性(换句话说,就是函数Dog)。
在javascript中,每一个函数都有一个名为“prototyoe”的属性,该属性对应着一个原型对象。该原型对象也有一个名为“constructor”的属性,"constructor"属性指向函数自己本身。这是一个循环引用。图一能够更好的展现该关系。
图一:每一个函数的原型都有一个constructor属性。
当一个函数(比如上面的Dog)使用new 操作符来创建一个对象的时候,被创建的对象就继承该函数原型的属性。从图一中,我们可以看到Dog.prototype对象有一个指向Dog函数的constructor属性。因此每一个Dog对象(即继承于Dog.prototype)都有一个指向Dog函数constructor属性。下面的代码将会证实这一点。构造函数,原型对象,以及由他们创建的对象之间的关系将在图二中展现。
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"));
图2 实例继承于原型
我们当中的一些人可能已经发现了这样的一个现象,我们在上面的代码中调用了hasOwnProperty 和isPrototypeOf方法,但这些方法来自哪里呢?它们并来源与Dog.prototype。事实上,像toString,toLoaclStrng ,valueOf这些方法,我们都可以通过Dog.prototype或者Dog的实例来调用,但是这些方法中任何一个都不来源于Dog.prototype。正如.Net Framework 有System.Object作为所有类的基类一样,JavaScript中有Ojbect.prototype作为所有原型的的终极原型(Object.prototype的原型为null)。
在这个例子中,记住Dog.prototype 是一个对象。它通过调用Ojbect 构造函数来创建,但这是不可见的。
Dog.prototype = new Object();
就像Dog的实例继承于Dog.prototype,Dog.prototype继承于Object.prototype。这使得Dog的所有实例都继承了Object.prototype的方法和属性。
图三:解析toString()方法的原型链
JavaScript动态的解析属性和方法调用的方式会导致一些后果:改变一个原型对象很容易在继承于该原型的对象中显示出来。如果我们在一个对象中定义了属性/方法 X 。该对象原型中的相同名字属性/方法将会被隐藏。例如,我们可以在Dog.prototype中定义一个toString方法来覆盖Object.prototype中的toString方法。改变只是单向的,从原型对象到它派生的对象。
下面的代码向我们显示了这种情况。下面的代码也解决了我们早先遇到的难题-----怎么解决不必要的方法。我们可以把一个方法放入原型对像中,使得所有继承该原型的对象能够共享该方法。在下面的例子中,rover对象和spot对象共享同一个getBreed方法,直到在spot对象中重写的getBreed方法(此处原文有错处)。这样sopt对象拥有了自己的getBreed()f方法,但是rover对象和继承于GreatDane的对象仍然共享由GreateDane.prototype对象创建的getBreed方法。
从原型继承
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());
有时候我们需要与类联系在一起的属性和方法,即静态属性和静态方法。JavaScript使的这个变得容易,因为函数就是对象,并且它的属性和方法能够随时添加。因为在JavaScript中 构造函数代表一个类,我们可以按照如下的方式向构造函数中添加静态方法和静态对象。
function DateTime() { }
// set static method now()
DateTime.now = function() {
return new Date();
};
alert(DateTime.now());
在JavaScript中调用静态方法的语法跟C#中的是一样的。这并不感到意外,因为构造函数的名字实际上和类的名字是一样的。现在我们知道了类,知道创建共有属性和方法,知道怎么创建的静态属性和和方法,我们还缺啥呢?对了,我们还需要私有属性,但是JavaScript并不支持私有属性。一个对象的所有属性和方法都够被访问,解决方法还是有的,但是我们必须理解闭包的概念
我并不是自愿学习JavaScript的,我不得不硬着头皮去学习它,不然在做一个ajax应用程序的时候,我会感到很被动。我自认为自己的水平已经很高了。当我改变自己最初的看法时,我发现JavaScript实际上是一门强大的,善于表达,简洁的语言。它甚至拥有目前许多流行语言刚开始支持的特性。
JavaScript最大的特点之一是它支持闭包。在C#中通过匿名方法来实现闭包。闭包是一种运行现象:当一个内部函数(在C#中,内部匿名方法)与外部函数中的局部变量绑定起来时会产生闭包。很显然,这并没有多大的意义,除非该内部函数和外部的某些变量关联起来。举一个例子会显的更清晰点。
假设我们要过滤一个数字序列,就是大于100的数字能够通过,其余的被过滤掉,我们可以按如下方式来实现。
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);
我们现在想要创建不同的过滤准则,比如大于300的才能通过,我们可能像这样改。
var greaterThan300 = filter(
function(x) { return (x > 300) ? true : false; },
someRandomNumbers);
然后,也许需要筛选大于 50、25、10、600 如此等等的数字,但作为一个聪明人,您会发现它们全部都有相同的谓词“greater than”,只有数字不同。因此,可以用类似下面的函数分开各个数字:
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));
现在介绍闭包如何帮助模拟私有成员。正常情况下,无法从函数以外访问函数内的本地变量。函数退出之后,由于各种实际原因,该本地变量将永远消失。但是,如果该本地变量被内部函数的闭包捕获,它就会生存下来。这一事实是模拟 JavaScript 私有属性的关键。假设有一个 Person 类:
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; };
}
参数 name 和 age 是构造函数 Person 中的局部变量。Person 返回时,name 和 age 应当永远消失。但是,它们被作为 Person 实例的方法而分配的四个内部函数捕获,实际上这会使 name 和 age 继续存在,但只能严格地通过这四个方法访问它们。因此,您可以:
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 }注意,这些私有成员与我们期望从 C# 中产生的私有成员略有不同。在 C# 中,类的公用方法可以访问它的私有成员。但在 JavaScript 中,只能通过在其闭包内拥有这些私有成员的方法来访问私有成员(由于这些方法不同于普通的公用方法,它们通常被称为特权方法)。因此,在 Person 的公用方法中,仍然必须通过私有成员的特权访问器方法才能访问私有成员:
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);
现在,如何创建从 Pet 派生的类 Dog 呢?在图 4 中可以看到,Dog 有另一个属性 breed,它改写了 Pet 的 toString 方法(注意,JavaScript 的约定是方法和属性名称使用 camel 大小写,而不是在 C# 中建议的 Pascal 大小写)。下面的代码 显示如何这样做。
// 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);
在 C++ 和 C# 中,命名空间用于尽可能地减少名称冲突。例如,在 .NET Framework 中,命名空间有助于将 Microsoft.Build.Task.Message 类与 System.Messaging.Message 区分开来。JavaScript 没有任何特定语言功能来支持命名空间,但很容易使用对象来模拟命名空间。如果要创建一个 JavaScript 库,则可以将它们包装在命名空间内,而不需要定义全局函数和类,如下所示:
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);如果看一下 Microsoft AJAX 库的源代码,就会发现库的作者使用了类似的技术来实现命名空间(请参阅静态方法 Type.registerNamespace 的实现)。有关详细信息,请参与侧栏“OOP 和 ASP.NET AJAX”。
function object(o) { function F() {} F.prototype = o; return new F(); }然后,由于 JavaScript 中的对象是可延展的,因此可以方便地在创建对象之后,根据需要用新字段和新方法增大对象。