天天看點

JavaScript. The core.

轉載:http://ued.ctrip.com/blog/?p=2795

最近讀到了一篇JavaScript的文章,覺得不錯。原本的中文翻譯又404,是以我開始邊讀邊翻譯。這篇主要就是介紹js裡面的一些非常基本但是又很重要的概念。

原文位址:http://dmitrysoshnikov.com/ecmascript/javascript-the-core/

翻譯備注:因為有些英文翻譯成中文會更加難懂,是以一些術語或者必要的地方,都是中文在前,後面緊随中括号中的英文。希望能讓大家更好的讀懂。

全文分為10個主要部分

  1. An object
  2. A prototype chain
  3. Constructor
  4. Execution context stack
  5. Execution context
  6. Variable object
  7. Activation object
  8. Scope chain
  9. Closures
  10. This value
  11. Conclusion

本篇是ECMA-262-3 in detail系列的概述。每個章節都有一個更詳細的内容連結,如果你覺得某個章節解釋的不夠過瘾,可以繼續讀一下每個章節對應的詳細内容連結。

适合的讀者:有經驗的開發員,專業前端人員。

我們首先來看一下對象[Object]的概念,這也是ECMASript中最基本的概念。

An object

ECMAScript, 是一門進階抽象面向對象的語言,用以處理Objects. 當然,也有原生類型,但是必要時,也需要轉換成object.

An object is acollection of propertiesand has asingle prototype object. The prototype may be either an object or the

null

value.

Object是一個屬性的集合,并且都擁有一個單獨的原型對象[prototype object]. 這個原型對象[prototype object]可以是一個object或者null值。

讓我們舉一個Object的例子。首先我們要清楚,一個Object的prototype是一個内部的[[prototype]]屬性的引用。不過一般來說,我們會使用____ 下劃線來代替雙括号,例如__prototype__

看這段代碼

varfoo={
 x:10,
 y:20
};      

如上結構有兩個顯式的屬性[explicit own properties]和一個自帶隐式的 __proto__ 屬性[implicit __proto__ property],也就是foo的prototype.

JavaScript. The core.

這個property有什麼用處呢?我們來看一個原型鍊[prototype chain]的例子。

A prototype chain

Prototype對象也是對象類型的,也會有自己的prototypes。如果這個prototype仍然存在一個非空的prototype,那麼這樣一直搜尋下去,就形成了一個原型鍊[prototype chain]。

A prototype chain is afinitechain of objects which is used to implementedinheritanceandshared properties.

原型鍊是由一系列用來繼承和共享屬性的對象組成有限的對象鍊。

考慮這樣的一個情況。

有兩個對象,隻有一小部分的方法或者屬性不同,其餘的都是一樣的。很明顯,在一個好的設計模式中,我們會需要重用那部分相同的,而不是在每個對象中重複定義那些相同的方法或者屬性。在基于類[class-based]的系統中,這些重用部分被稱為類的繼承 – 相同的部分放入class A,然後class B和class C從A繼承,并且擁有各自的獨特的東西。

ECMAScript沒有類的概念。但是,重用[reuse]這個理念沒什麼不同(某些方面,甚至比class-更加靈活),可以由prototype chain原型鍊來實作。這種繼承被稱為delegation based inheritance-基于繼承的委托,或者更通俗一些,叫做原型繼承。

上述的Classes A,B,C,在ECMAScript中可以建立三個對象:a,b,c. 由a來負責b和c相同的部分,而b和c則負責各自不同的獨特的東西。

var a = {
  x: 10,
  calculate: function (z) {
    return this.x + this.y + z
  }
};

var b = {
  y: 20,
  __proto__: a
};

var c = {
  y: 30,
  __proto__: a
};

// call the inherited method
b.calculate(30); // 60
c.calculate(40); // 80      

這樣看上去是不是很簡單啦。b和c可以使用a中定義的calculate方法,這就是有原型鍊來[prototype chain]實作的。

我們來看一下原理:如果在對象b中找不到calculate方法(也就是對象b中沒有這個calculate屬性), 那麼就會沿着原型鍊開始找。如果這個calculate方法在b的prototype中沒有找到,那麼就會沿着原型鍊找到a的prototype,一直周遊完整個原型鍊。記住,一旦找到,就傳回第一個找到的屬性或者方法。是以,第一個找到的屬性成為繼承屬性。如果周遊完整個原型鍊,仍然沒有找到,那麼就會傳回undefined.

注意一點,this這個值在一個繼承機制中,仍然是指向它原本屬于的對象,而不是從原型鍊上找到它時,它所屬于的對象。例如,以上的例子,this.y是從b和c中擷取的,而不是a。當然,你也發現了this.x是從a取的,因為是通過原型鍊機制找到的。

如果一個對象的prototype沒有顯示的聲明過或者說定義過,那麼__prototype__的預設值就是object.prototype, 而object.prototype也會有一個__prototype__, 這個就是原型鍊的終點了,被設定為null.

下面的圖示就是表示了上述a,b,c的關系

JavaScript. The core.

我們再考慮一種情況。有時候,我們需要對象使用相同的或者相似的結構(例如相同的屬性集),但是不同的值。在這種情況下,我們可以考慮使用構造函數[constructor function],是一種特定結構構造的對象。

Constructor

除了可以構造特定對象,構造函數[constructor]還有一個有用的功能 – 為一個建立對象建立一個原型對象。這個原型對象放置在構造函數的原型[ConstrutorFunction.prototype]中。

我們用構造函數重寫一下上述的例子。

// a constructor function
function Foo(y) {
  // which may create objects
  // by specified pattern: they have after
  // creation own "y" property
  this.y = y;
}

// also "Foo.prototype" stores reference
// to the prototype of newly created objects,
// so we may use it to define shared/inherited
// properties or methods, so the same as in
// previous example we have:

// inherited property "x"
Foo.prototype.x = 10;

// and inherited method "calculate"
Foo.prototype.calculate = function (z) {
  return this.x + this.y + z;
};

// now create our "b" and "c"
// objects using "pattern" Foo
var b = new Foo(20);
var c = new Foo(30);

// call the inherited method
b.calculate(30); // 60
c.calculate(40); // 80

// let's show that we reference
// properties we expect

console.log(

  b.__proto__ === Foo.prototype, // true
  c.__proto__ === Foo.prototype, // true

  // also "Foo.prototype" automatically creates
  // a special property "constructor", which is a
  // reference to the constructor function itself;
  // instances "b" and "c" may found it via
  // delegation and use to check their constructor

  b.constructor === Foo, // true
  c.constructor === Foo, // true
  Foo.prototype.constructor === Foo // true

  b.calculate === b.__proto__.calculate, // true
  b.__proto__.calculate === Foo.prototype.calculate // true

);      

上述的代碼是如下的關系

JavaScript. The core.

上述的圖示可以看出來,每一個object都有一個prototype. 構造函數Foo也擁有自己的__proto__, 也就是Function.prototype, 而Function.prototype的__proto__指向了Object.prototype. 是以,Foo.prototype隻是一個顯式的屬性,也就是b和c的__proto__屬性。

針對這個内容更完整和詳細的可以在第七章ES3系列中找到。有兩個部分:Chapter 7.1. OOP. The general theory(各種OOP的原理和它們與ECMAScript的對比),還有Chapter 7.2. OOP. ECMAScript implementation(闡述如何将OOP的概念引入到ECMAScript中)

現在,我們已經了解了基本的object原理,那麼我們接下去來看看ECMAScript裡面的程式執行環境[runtime program execution]. 這就是通常稱為的“執行上下文堆棧”[execution context stack]。每一個元素都可以抽象的了解為object。你也許發現了,沒錯,在ECMAScript中,幾乎處處都能看到object的身影。

Execution context stack

在ECMASscript中的代碼有三種類型:global, function和eval。

每一種代碼的執行都需要依賴自身的上下文。當然global的上下文可能涵蓋了很多的function和eval的執行個體。函數的每一次調用,都會進入函數執行中的上下文,并且來計算函數中變量等的值。eval函數的每一次執行,也會進入eval執行中的上下文,判斷應該從何處擷取變量的值。

注意,一個function可能産生無限的上下文環境,因為一個函數的調用(甚至遞歸)都産生了一個新的上下文環境。

function foo(bar) {}

// call the same function,
// generate three different
// contexts in each call, with
// different context state (e.g. value
// of the "bar" argument)

foo(10);
foo(20);
foo(30);      

一個執行上下文可以激活另一個上下文,就好比一個函數調用了另一個函數(或者全局的上下文調用了一個全局函數),然後一層一層調用下去。邏輯上來說,這種實作方式是棧,我們可以稱之為上下文堆棧。

例如A上下文激活了B的上下文,那麼A稱為caller,B稱為callee. 一個callee同時也可能是另一個callee的caller。(例如一個全局上下文中的function又一次調用了它的内部函數。)

當一個caller激活了一個callee,那麼這個caller就會暫停它自身的執行,然後将控制權交給這個callee. 于是這個callee被放入堆棧,稱為進行中的上下文[running/active execution context]. 當這個callee的上下文結束之後,會把控制權再次交給它的caller,然後caller會在剛才暫停的地方繼續執行。在這個caller結束之後,會繼續觸發其他的上下文。一個callee可以用return或者例外退出(exit with an exception)結束它自身的上下文.

所有的ECMAScript的程式執行都可以看做是一個執行上下文堆棧[execution context (EC) stack]。堆棧的頂部就是處于激活狀态的上下文。

JavaScript. The core.

當一段程式開始時,會先進入全局執行上下文環境[global execution context], 這個也是堆棧中最底部的元素。此全局程式會開始初始化,初始化生成必要的對象[objects]和函數[functions]. 在此全局上下文執行的過程中,它可能會激活一些方法(當然是已經初始化過的),然後進入他們的上下文環境,然後将新的元素加入堆棧。在這些初始化都結束之後,這個系統會等待一些事件(例如使用者的滑鼠點選等),會觸發一些方法,然後進入一個新的上下文環境。

在這個圖示中,一些方法的上下文稱為EC1,全局的上下文稱為 Global EC,我們看一下堆棧形式,示範了從全局上下文進入和推出EC1的過程。

JavaScript. The core.

這就是ECMAScript的執行系統如何管理代碼執行的。

關于上下文環境的更多内容,可以在Chapter 1. Execution context中找到。

就像我們剛剛講過的,堆棧中每一個執行的上下文可以看做一個對象[object]。接街區,我們将看看它的結構和執行它的代碼時狀态類型(或者說屬性)。

Execution context

一個執行的上下文可以抽象的了解為object。每一個執行的上下文都有一系列的屬性(我們稱為上下文狀态),他們用來追蹤關聯代碼的執行進度。這個圖示就是一個context的結構

JavaScript. The core.

當然除了這三個必要的屬性,一個上下文環境也會根據情況依賴其他的屬性(狀态)。

讓我們仔細來看看這三個屬性

Variable object

Avariable objectis ascope of datarelated with the execution context. It’s a special object associated with the context and which storesvariablesandfunction declarationsare being defined within the context.

一個VO[variable object]是關聯上下文執行環境的一系列資料。它是與上下文關系密切的一個特殊對象,存儲了在上下文中定義的變量和函數聲明。

注意:函數表達式[function expression](而不是函數聲明[function declarations])是不包含在VO[variable object]裡面的。

Variable Object是一個抽象的概念,某個方面來講,它表示使用不同的object.以下簡稱VO. 例如,在global上下文中,variable object也是全局對象[global object]。(這就是我們可以通過全局對象的屬性來指向全局變量)。

讓我們看看下面例子中的全局執行上下文情況。

var foo = 10;

function bar() {} // function declaration, FD
(function baz() {}); // function expression, FE

console.log(
  this.foo == foo, // true
  window.bar == bar // true
);

console.log(baz); // ReferenceError, "baz" is not defined      

如下圖所示

JavaScript. The core.

可以看到,函數baz是一個函數表達式,是以它不在VO裡面,是以會有一個ReferenceError的提示,因為此時是在全局環境下讀取,也就是在函數表達式外部讀取的。

注意,和其他的語言不同(例如c/c++),在ECMAScript中,隻有函數functions會建立一個新的作用域。在一個函數中定義的變量和内部函數在外部是無法擷取的,同時也不會影響全局的變量。

使用eval的話,我們通常會進入一個新的eval上下文環境。Eval會使用global VO,也可以使用caller的VO。

那麼函數和它們的VO的情況呢,在一個函數的上下文中,一個VO代表了激活變量[activation object],以下簡稱AO.

Activation object

當一個函數被調用激活之後,這個稱為AO(activation object)的特殊對象就被建立了.它涵蓋了普通的參數和一個特殊的arguments對象(這是一個參數的映射表,并且有索引屬性)。我們也可以認為:AO實際上就是函數上下文的VO.

例如,函數的VO實際上就是簡單的VO,但是除了這些變量和函數定義,還有參數和arguments對象,這些就成為AO.

考慮下面的情況

function foo(x, y) {
  var z = 30;
  function bar() {} // FD
  (function baz() {}); // FE
}

foo(10, 20);      

那麼AO的圖示如下所示:

JavaScript. The core.

同樣道理,function expression不在AO的行列。

對于這個AO的詳細内容可以通過Chapter 2. Variable object找到

我們接下去将第三個主要的對象。衆所周知,在ECMAScript中,我們會用到内部函數[inner functions],在這些内部函數中,我們可能會引用它的父函數變量,或者全局的變量。我們把這些變量對象成為上下文作用域對象[scope object of the context]. 類似于上面讨論的原型鍊[prototype chain],我們在這裡稱為作用域鍊[scope chain]。

Scope chain

作用域鍊就是在代碼上下文中,搜尋辨別符的過程中遇到的一系列對象

這個原理和原型鍊很類似,如果這個變量在自己的作用域中沒有,那麼它會尋找父級的,直到最頂層。

标示符[Identifiers]可以了解為變量名稱、函數聲明和普通參數。例如,當一個函數在自身函數體内需要引用一個變量,但是這個變量并沒有在函數内部聲明(或者也不是某個參數名),那麼這個變量就可以稱為自由變量[free variable].那麼我們搜尋這些自由變量就需要用到作用域鍊。

在一般情況下,一個作用域鍊包括父級變量VO、函數自身的VO和AO。不過,有些情況下也會包含其它的對象,例如在執行期間,動态加入作用域鍊中的—例如with或者catch語句。

當處理一個變量,作用域鍊是會從AO開始搜尋,然後一直搜尋到頂端。和原型鍊非常類似。

var x = 10;

(function foo() {
  var y = 20;
  (function bar() {
    var z = 30;
    // "x" and "y" are "free variables"
    // and are found in the next (after
    // bar's activation object) object
    // of the bar's scope chain
    console.log(x + y + z);
  })();
})();      

我們假設作用域鍊的對象關聯是通過一個叫做__parent__的屬性,它是指向作用域鍊的下一個對象。這可以在Rhino Code中測試一下這種流程,這種技術也确實在ES5環境中實作了(有一個稱為outer連結).當然也可以用一個簡單的資料來模拟這個模型。使用__parent__的概念,我們可以把上面的代碼示範成如下的情況。(是以,父級變量是被存在函數的[[Scope]]屬性中的)

JavaScript. The core.

在代碼執行過程中,如果使用with或者catch語句就會改變作用域鍊。而這些對象都是一些簡單對象,他們也會有原型鍊。這樣的話,作用域鍊會從兩個次元來搜尋。

  1. 首先在原本的作用域鍊
  2. 每一個連結點的作用域的鍊(如果這個連結點是有prototype的話)

我們再看下面這個例子

Object.prototype.x = 10;

var w = 20;
var y = 30;

// in SpiderMonkey global object
// i.e. variable object of the global
// context inherits from "Object.prototype",
// so we may refer "not defined global
// variable x", which is found in
// the prototype chain

console.log(x); // 10

(function foo() {

  // "foo" local variables
  var w = 40;
  var x = 100;

  // "x" is found in the
  // "Object.prototype", because
  // {z: 50} inherits from it

  with ({z: 50}) {
    console.log(w, x, y , z); // 40, 10, 30, 50
  }

  // after "with" object is removed
  // from the scope chain, "x" is
  // again found in the AO of "foo" context;
  // variable "w" is also local
  console.log(x, w); // 100, 40

  // and that's how we may refer
  // shadowed global "w" variable in
  // the browser host environment
  console.log(window.w); // 20

})();      

我們就會有如下結構圖示。注意,在我們去搜尋__parent__之前,首先會去__proto__的連結中。

JavaScript. The core.

注意,不是所有的全局對象都是由Object.prototype繼承而來的。上述圖示的情況可以在SpiderMonkey中測試。

我們會搜尋作用域鍊來尋找需要的變量。剛才也有提過,在一個上下文結束之後,它的狀态和自身會銷毀掉。同時,這個内部函數會傳回到父級函數中。也許,這個傳回函數稍後會再激活另一個上下文。如何保持一個自由變量仍然處于被激活中呢?理論上來說,有一個概念可以解決這個問題,稱為閉包[Closure].它也是和作用域鍊有直接關系的。

Closure

在ECMAScript中,function[函數]是基本等級[first-class]的對象。這就意味着,函數可以作為另一個函數的參數(在這種情況下,稱之為”funargs”, 也就是”functional arguments”的縮寫).接收funargs的函數稱為進階函數[higher-order function], 或者類似于數學中的運算符[operator]. 同樣,函數也可以作為另一個函數的傳回。把函數作為傳回值的函數稱為以函數為值的函數[function valued functions](或者functions with functional value)

關于上述的funargs和functional value,會引發一個叫做”Funarg problem”(或者”A problem of a functional argument)”。為了解決這個問題,我們引入了“閉包”。我們下面來詳細讨論這兩個問題(在ECMAScript中的要解決這兩個問題,需要用到函數的一個[[Scope]]的屬性)。

首先”funarg problem”的一個類型就是自下而上[”upward funarg problem”]. 當一個函數作為另一個函數的傳回值時,并且使用了自由變量[free variable]的時候會發生。即便它的父級上下文環境已經結束了,它可以引用父級的變量,。這個内部函數在建立時就會将父級的作用域鍊儲存在自己的作用域[[Scope]]中。當函數運作時,上下文環境的作用域量是由活躍變量[activation object]和它[[Scope]]屬性組合而成。

Scope chain = Activation object + [[Scope]]      

請再次注意這個很重要的點 – 在函數建立期間[creation moment],函數會将父級的作用域鍊儲存起來,因為随後調用這個函數的時候使用的已經儲存的作用域鍊來搜尋變量。

請看下面的函數

function foo() {
  var x = 10;
  return function bar() {
    console.log(x);
  };
}

// "foo" returns also a function
// and this returned function uses
// free variable "x"

var returnedFunction = foo();

// global variable "x"
var x = 20;

// execution of the returned function
returnedFunction(); // 10, but not 20      

這種形式的作用域稱為靜态作用域[static/lexical scope]。上面的x變量就是在函數bar的[[Scope]]中搜尋到的。理論上來說,也會有動态作用域[dynamic scope], 也就是上述的x被解釋為20,而不是10. 但是EMCAScript不使用動态作用域。

“funarg problem”的另一個類型就是自上而下[”downward funarg problem”].在這種情況下,父級的上下會存在,但是在判斷一個變量值的時候會有多義性。也就是,這個變量究竟應該使用哪個作用域。是在函數建立時的作用域呢,還是在執行時的作用域呢?為了避免這種多義性,可以采用閉包,也就是使用靜态作用域。

請看下面的例子

// global "x"
var x = 10;

// global function
function foo() {
  console.log(x);
}

(function (funArg) {

  // local "x"
  var x = 20;

  // there is no ambiguity,
  // because we use global "x",
  // which was statically saved in
  // [[Scope]] of the "foo" function,
  // but not the "x" of the caller's scope,
  // which activates the "funArg"

  funArg(); // 10, but not 20

})(foo); // pass "down" foo as a "funarg"      

從上述的情況,我們似乎可以斷定,在語言中,使用靜态作用域是閉包的一個強制性要求。不過,在某些語言中,會提供動态和靜态作用域的結合,可以允許開發員選擇哪一種作用域。但是在ECMAScript中,隻采用了靜态作用域。是以ECMAScript完全支援使用[[Scope]]的屬性。我們可以給閉包得出如下定義

Aclosureis a combination of a code block (in ECMAScript this is a function) and statically/lexically saved all parent scopes. Thus, via these saved scopes a function may easily refer free variables.

閉包是一系列代碼塊(在ECMAScript中是函數),并且靜态儲存所有父級的作用域。通過這些儲存的作用域來搜尋到函數中的自由變量。

注意,其實每一個函數都在建立期間儲存[[Scope]],是以理論上來說,在ECMAScript中所有的函數都是閉包。

還有一個很重要的點,幾個函數可能含有相同的父級作用域(這是一個很普遍的情況,例如有好幾個内部或者全局的函數)。在這種情況下,在[[Scope]]中存在的變量是會共享的。一個閉包中變量的變化,也會影響另一個閉包的。

function baz() {
  var x = 1;
  return {
    foo: function foo() { return ++x; },
    bar: function bar() { return --x; }
  };
}

var closures = baz();

console.log(
  closures.foo(), // 2
  closures.bar()  // 1
);      

上述代碼可以用這張圖來表示

JavaScript. The core.

如果有這個特性的話,在一個循環中建立多個函數就會有關聯了。在循環中建立函數,我們可能會遇到意想不到的情況,因為所有的函數都會共享同一個變量了。所有的函數都有同一個[[Scope]], 并且都指向了最後一次指派的count。

var data = [];

for (var k = 0; k < 3; k++) {
  data[k] = function () {
    alert(k);
  };
}

data[0](); // 3, but not 0
data[1](); // 3, but not 1
data[2](); // 3, but not 2      

有幾種方法可以解決種鴿問題。一種就是在作用域鍊中提供一個新增的對象,例如增加一個函數。

var data = [];

for (var k = 0; k < 3; k++) {
  data[k] = (function (x) {
    return function () {
      alert(x);
    };
  })(k); // pass "k" value
}

// now it is correct
data[0](); // 0
data[1](); // 1
data[2](); // 2      

如果對閉包和它的應用有興趣的,可以去看看Chapter 6. Closures.

如果對作用域鍊想要有更深傳入連結接的,可以去看看Chapter 4. Scope chain。

接下去我們讨論下一個章節,也就是上下文環境的最後一個屬性– this.

This value

A

this

value is a special object which is related with the execution context. Therefore, it may be named as acontext object(i.e. an object in which context the execution context isactivated).

this适合執行的上下文環境息息相關的一個特殊對象。是以,它也可以稱為上下文對象[context object]

任何對象在上下文環境中都可以使用this。我需要澄清一個誤區,在一些描述中總是認為this是一個變量對象的屬性。請記住

a

this

value is aproperty of the execution context, butnota property of the variable object.

this是執行上下文環境的一個屬性,而不是某個變量對象的屬性

這個特點很重要,因為和變量不同,this是沒有一個類似搜尋變量的過程。當你在代碼中使用了this,這個 this的值就直接從執行的上下文中擷取了,而不會從作用域鍊中搜尋。this的值隻取決中進入上下文時的情況。

順便說一句,和ECMAScript不同,Python有一個self的參數,和this的情況差不多,但是可以在執行過程中被改變。在ECMAScript中,是不可以給this指派的,因為,還是那句話,this不是變量。

在global context(全局上下文)中,this的值就是指全局這個對象,這就意味着,this值就是這個變量本身。

var x = 10;

console.log(
  x, // 10
  this.x, // 10
  window.x // 10
);      

在函數上下文[function context]中,this會可能會根據每次的函數調用而成為不同的值.this會由每一次caller提供,caller是通過調用表達式[call expression]産生的(也就是這個函數如何被激活調用的)。例如,下面的例子中foo就是一個callee,在全局上下文中被激活。下面的例子就表明了不同的caller引起this的不同。

// the code of the "foo" function
// never changes, but the "this" value
// differs in every activation

function foo() {
  alert(this);
}

// caller activates "foo" (callee) and
// provides "this" for the callee

foo(); // global object
foo.prototype.constructor(); // foo.prototype

var bar = {
  baz: foo
};

bar.baz(); // bar

(bar.baz)(); // also bar
(bar.baz = bar.baz)(); // but here is global object
(bar.baz, bar.baz)(); // also global object
(false || bar.baz)(); // also global object

var otherFoo = bar.baz;
otherFoo(); // again global object      

要更深入的了解問什麼每次函數調用的時候,this都會改變,請看Chapter 3. This。

Conslusion

到這裡,我們基本上有一個簡要的回顧。要詳細的了解這一系列,需要一本書來闡述。我們沒有涉及兩個主要的概念:functions(function declaration 和 function expression)和evaluation strategy.這兩個内容可以從Chapter 5. Functions and Chapter 8. Evaluation strategy.

If you have comments, questions or additions, I’ll be glad to discuss them in comments.

Good luck in studying ECMAScript!

Written by: Dmitry A. Soshnikov

Published on: 2010-09-02

Translated by: feifeipan

最後聲明:如果翻譯的内容有不妥或者需要改進的地方,請大家多多指出。

本文作者:小灰灰 轉載請注明來自:攜程UED