javascript中的數據類型、Object與Function

轉自http://blog.csdn.net/baiduforum/article/details/5381869 格式略有改動

1. 數據類型

      javascript中包含6種數據類型:undefined、null、string、number、boolean和object。其中,前5 種是原始數據類型,object是對象類型。

object類型中包括Object、Function、String、Number、Boolean、Array、Regexp、Date、 Globel、Math、Error,以及宿主環境提供的object類型。

2.類型判斷

      常在javascript中進行類型判斷主要通過3種方式:typeof、instanceof、constructor。

2.1 typeof

      typeof操作可能返回的類型爲undefined、object、number、string、function、boolean。但是會有一些情況並不能完全判斷準確。比如typeof new String('')的值爲object。
2.2 constructor

有時候我們可能會很偷懶的使用a.constructor == String進行類型判斷,但是constructor其實是不靠譜的東西。因爲當我們調用a.constructor的時候,內部操作其實是 ToObject(a).prototype.constructor(ToObject是什麼,看下文分解)。

看下面一段代碼就能明白:

String.prototype.constructor = Number;
 alert('test'.constructor == String);
 //Result:false
// 或者
function MyClass()
 {
 

}
 

MyClass.prototype = {};
 

alert((new MyClass).constructor == MyClass);
 //Result:false

而且,通過constructor並不能判斷出對象實例類型的繼承關係。因爲javascript的繼承其實是通過原型鏈實現的(原型鏈是什麼,看下文分解)。

另外,null.constructor會拋出運行時的TypeError,所以使用constructor除了不靠譜外,還可能伴隨着異常的風險。

2.3 

instanceof

例子:a instanceof String

關於object類型的判斷,使用instanceof判斷是比較靠譜的方法。instanceof所做的事情是,先取出類型對象(String) 的prototype成員(String.prototype),然後和要判斷類型的對象(a)的原型鏈中的對象逐個比較。當發現是一個對象的時候返回 true,原型鏈中當前節點是null的時候返回false。

類型判斷示例:判斷一個變量是否是字符串類型



function isString(str)
 {
 
    return (typeof str == 'string' || str instanceof String);
 

}

3.類型轉換


ecma262中描述了以下幾種類型轉換的操作:(還有其他的比如ToInt32等,這裏就不列了)

  • ToNumber:轉換成number型
  • ToString:轉換成string型
  • ToBoolean:轉換成boolean型
  • ToObject:轉換成object型
  • ToPrimitive:轉換成原始類型

每種操作都描述了從什麼類型轉換成該類型的映射。比如上文的'a'.constructor中,就包含解析器使用ToObject將‘a’轉換成 object的一個隱式操作。

這裏想要主要說的是ToPrimitive。ToPrimitive用於轉換成原始數據類型。當要轉換的量已經是原始類型時,會直接返回。如果要轉換的是一個Object,那會調用[[DefaultValue]]方法做轉換。([[DefaultValue]]是什麼,下文分解)該方法可以傳入一個hint參數,說明需要將Object轉換成字符串或數字。如果要轉換成字符串,則調用Object的toString方法,如果要轉換成數字,則調用 Object的valueOf方法。具體在運行時什麼時候應該轉換成什麼類型,請參考ecma262中關於expression的描述部分。


4.Object

除了5種原始類型外,一切都是Object,包括Object、Function、Array等等,他們的實例和構造器,都是Object。那 Object是一個什麼東西呢?

Object是一個:無序的成員集合

它是一個集合,說明它包含0-n個成員。而它是無序的。

每一個成員由以下3個部分組成:名稱、值、特徵集合

下面的代碼中:

var obj = {'key': 'value'};

key就是成員名稱,value就是值,obj這個Object從代碼上看起來包含了一個成員,注意,是從代碼上看而已。這裏我們不去深究它先。

那特徵集合是個什麼東西呢?

javascript的對象成員可能包含下面幾種特徵的0個或多個:ReadOnly、DontEnum、DontDelete、 Internal。

  • ReadOnly:擁有這個特徵的成員是不能被程序修改的。
  • DontEnum:擁有這個特徵的成員是不能被for in遍歷的。
  • DontDelete:擁有這個特徵的成員是不能被delete操作刪除的。
  • Internal:代表這個成員是內部成員。通常內部成員不能被程序以任何方式訪問,但是有些javascript的引擎實現將它以特殊方式暴露,使得可以訪問對象的某些內部成員。

一個對象的Internal成員以[[xxxx]]的方式來表示。

下面列一些和本博有關的的Object可能包含的internal成員。

  • [[Class]]:表示該對象的類型。比如function Object的[[Class]]成員的值是"Function"
  • [[Get]](PropertyName):獲取對象的屬性值。
  • [[DefaultValue]] (Hint):用於ToPrimitive進行類型轉換時調用。hint參數可能的值爲"string"或"number"
  • [[Prototype]]:[[Prototype]]成員實現了javascript中所謂的“原型鏈”。一個對象的[[Prototype]]成員可能是object對象,或者是null。只有Object.[[prototype]]爲null,其他任何對象的[[Prototype]]成員都是一個Object
  • [[Call]]:function Object特有的成員,在函數被調用的時候,就是調用的[[Call]]。
  • [[Construct]]:function Object特有的成員,在函數作爲構造器,被new操作符用於創建對象的時候,就是調用的[[Construct]]。
  • [[Scope]]:[[Prototype]]成員實現了javascript中所謂的“作用域鏈”。

5. function Object的創建過程


解析器在遇到function declaration或者function expression的時候,會創建一個function Object。步驟大致如下:

  1. 解析形參和函數體
  2. 創建一個native ECMAScript Object:F
  3. 設置F的[[Class]]、[[Prototype]]、[[Call]]、[[Construct]]、[[Scope]]、length屬性
  4. 創建一個new Object():O
  5. 設置O的constructor屬性爲F
  6. 設置F的prototype屬性爲O

在這個創建過程裏,要說明的幾點是:

  1. 步驟3中F的[[Prototype]]被設置爲Function.prototype
  2. 用戶自定義的function,都會同時具有[[Call]]和[[Construct]]這兩個internal屬性
  3. 解析器會自動給每一個function Object初始化一個prototype成員。而F.prototype.constructor == F,所以,當我們沒有重新定義這個F的prototype成員的時候,F的實例的constructor成員是靠譜的。因爲(new F).constructor其實方位的是F.prototype.constructor,而解析器默認初始化給你的 F.prototype.constructor就是F。
  4. 關於[[scope]]和作用域鏈的問題,下文分解

還要提的一點是,function declaration和function expression是不一樣的

function declaration:

function fn() {}
 

function expression:

var a = function () {};
 

function () {};
 

6.原型鏈


首先要澄清的一點是,我們通常會使用myfunction.prototype的方式進行原型擴展,所以我們在聽到“原型鏈”這個詞的時候,會覺得這裏的“原型”指的是myfunction.prototype。其實不是,“原型”指的是對象的[[Prototype]]。當然,對象的 [[Prototype]]就是其真實構造器當前的prototype成員對象。

上文中有提過,一個我們通過程序創建的function Object,一定會包含[[Call]]和[[Construct]]這2個internal成員。它們做了什麼事情呢?

[[Call]]:

  1. Establish a new execution context using F's FormalParameterList, the passed arguments list, and the this value as described in 10.2.3.
  2. Evaluate F's FunctionBody.
  3. Exit the execution context established in step 1, restoring the previous execution context.
  4. If Result(2). type is throw then throw Result(2). value.
  5. If Result(2). type is return then return Result(2). value.
  6. (Result(2). type must be normal.) Return undefined.

[[Construct]]:

  1. Create a new native ECMAScript object.
  2. Set the [[Class]] property of Result(1) to "Object".
  3. Get the value of the prototype property of F.
  4. If Result(3) is an object, set the [[Prototype]] property of Result(1) to Result(3).
  5. If Result(3) is not an object, set the [[Prototype]] property of Result(1) to the original Object prototype object as described in 15.2.3.1.
  6. Invoke the [[Call]] property of F, providing Result(1) as the this value and providing the argument list passed into [[Construct]] as the argument values.
  7. If Type(Result(6)) is Object then return Result(6).
  8. Return Result(1).

一切都很清楚了。當我們創建一個對象,也就是我們new的時候,調用的是function Object的[[Construct]]成員方法。在上面的描述中,3、4步描述了[[Prototype]]成員的創建過程,就是構造器的 prototype成員。

好的,那回到之前,我們使用obj.property來獲取obj對象的屬性的時候,其實調用的是obj對象的internal方法 [[Get]]。那我們看看[[Get]]方法調用做了哪些事情:

  1. If O doesn't have a property with name P, go to step 4.
  2. Get the value of the property.
  3. Return Result(2).
  4. If the [[Prototype]] of O is null, return undefined.
  5. Call the [[Get]] method of [[Prototype]] with property name P.
  6. Return Result(5).

可以看出來,當我們獲取對象obj的某個成員的時候,會在obj對象自身成員裏查找是否存在該成員。如果不包含,則到obj. [[Prototype]]這個對象中查找名字成員,如果還不存在,則到obj.[[Prototype]].[[Prototype]]這個對象裏找,直到某個[[Prototype]]是null爲止。查找的過程就是一個順藤摸瓜的事情,這個藤就是我們所謂的“原型鏈”。

我不想說太多原型鏈和繼承之間的關係與實現,這方面的資料在網絡上已經太多太多。我只想把原型鏈脫光了告訴大家,原型鏈是什麼。

7.函數調用過程與作用域鏈

講到作用域鏈,就要扯到函數的調用。當我們有一個函數

function fn(param) {}

我們去調用它

fn(1);

這個時候解析器爲我們做了什麼呢?

有一定經驗的javascript工程師也許會用過arguments、用過閉包、知道作用域,這一切的一切,都和execution context有關。

當我們進入一個函數調用的時候,解析器會爲我們創建一個活動對象(Activation Object ),假設這裏把這個活動對象叫做ac(爲什麼不叫ao呢,因爲喜歡c)。然後做下面的事情:

  1. 初始化arguments對象,並將它添加到這個ac中。這個時候,對象ac就擁有了一個name爲arguments的成員。這裏arguments初始化過程就不具體說了,感興趣的可以看ecma262的章節10.1.8。
  2. 解析形參,並使用函數調用時傳遞的參數初始化。在上面的調用例子fn(1)中,這個時候,ac就擁有了一個name爲param的成員,這個成員的值爲 1。
  3. 對function declaration進行初始化,爲所有FunctionBody中的function declaration,創建function Object,並添加到對象ac中作爲ac的成員。在這一步,假設ac中已經包含了同名屬性,會被覆蓋掉。
  4. 對var聲明進行初始化,爲所有var聲明,在對象ac中創建同名成員,並初始化爲undefined。在這一步,假設ac中已經包含了同名屬性,不會被覆蓋掉。
  5. 初始化作用域鏈,並將這個作用域鏈與當前的執行上下文相關聯。這個作用域鏈是一個鏈式列表,最前段是進入函數調用時初始化出來的活動對象ac,然後後面跟着的是該函數的[[scope]]的成員。[[scope]]是個什麼東西呢,就是這個鏈。假如函數體中有創建function Object,叫做innerFn,那innerFn的[[scope]]成員,就是這個作用域鏈。當innerFn被調用時,會初始化新的活動對象,新的作用域鏈。新的作用域鏈就是初始化自這個新的活動對象和innerFn的[[scope]]。

那scope chain是什麼作用呢?看下面的描述,來自10.1.4

During execution, the syntactic production PrimaryExpression : Identifier is evaluated using the following algorithm:

  1. Get the next object in the scope chain. If there isn't one, go to step 5.
  2. Call the [[HasProperty]] method of Result(1), passing the Identifier as the property name.
  3. If Result(2) is true, return a value of type Reference whose base object is Result(1) and whose property name is the Identifier.
  4. Go to step 1.
  5. Return a value of type Reference whose base object is null and whose property name is the Identifier. 可以看出,我們在訪問一個變量的時候,其實是從和當前執行上下文相關的作用域鏈中查找成員。

在程序正常在全局下的函數,其[[scope]]成員的值是global object,所以無論任何調用,在作用域鏈的尾端,一定會是global object。在瀏覽器宿主環境下,就是window。

8.調用過程中的this

在函數的調用中,this是個什麼東西,又是由什麼決定的呢?在ecma262中,這是個比較繞的東西,其描述散落在世界各地。

首先,在10.2.3中告訴我們: The caller provides the this value. If the this value provided by the caller is not an object (note that null is not an object), then the this value is the global object. 我們可以知道,caller可以提供給我們this。如果沒有提供,則this爲global object。問題又來了,caller是怎麼提供this的?

在11.2.3中,找到如下關於Function calls的描述:The production CallExpression : MemberExpression Arguments is evaluated as follows:

  1. Evaluate MemberExpression.
  2. Evaluate Arguments, producing an internal list of argument values (see 11.2.4).
  3. Call GetValue(Result(1)).
  4. If Type(Result(3)) is not Object, throw a TypeError exception.
  5. If Result(3) does not implement the internal [[Call]] method, throw a TypeError exception.
  6. If Type(Result(1)) is Reference, Result(6) is GetBase(Result(1)). Otherwise, Result(6) is null.
  7. If Result(6) is an activation object, Result(7) is null. Otherwise, Result(7) is the same as Result(6).
  8. Call the [[Call]] method on Result(3), providing Result(7) as the this value and providing the list Result(2) as the argument values.
  9. Return Result(8).

從步驟6、7中可以看出來,如果MemberExpression的結果是一個Reference的話,提供的this應該是 GetBase(Reference),否則是空。步驟7中還有描述了6的結果是活動對象的情況,我們這裏忽略。 又有疑問了,Reference?Reference是什麼,GetBase又是什麼?

我們在8.7中,找到了Reference的答案。這裏的描述比較長,我只摘了可以滿足我們需要的一段: A Reference is a reference to a property of an object. A Reference consists of two components, the base object and the property name.

The following abstract operations are used in this specification to access the components of references:

GetBase(V). Returns the base object component of the reference V.

GetPropertyName(V). Returns the property name component of the reference V.

已經很明顯了,一個Reference必須引用一個對象的一個屬性。所以我們通過obj.method()來調用的時候,obj.method這個表達式生成了一箇中間態的Reference,這個Reference的base object就是obj,所以GetBase的結果就是obj,於是obj被caller提供作this

我曾經看到很多文章,舉了類似obj.method()這樣的調用例子,認爲obj就是caller,來解釋這番話:

The caller provides the this value. If the this value provided by the caller is not an object (note that null is not an object), then the this value is the global object.

這其實是說不通的。

caller絕不可能是obj,否則被attachEvent的函數或對象方法,他們運行時的this就解釋不通了。 所以,通過我們自己代碼調用的函數,caller由腳本引擎執行控制所決定;在瀏覽器宿主環境通過事件觸發的,caller由瀏覽器控制的行爲所決定。


9.關於原型鏈的補充——原型鏈會不會是圓形鏈


這個問題是telei同學提出的。答案是:不會

回頭看看[[Construct]]的步驟,我們可以發現,創建一個對象obj時,obj.[[prototype]]成員被賦予其構造器的 prototype成員。但是當構造器的prototype成員被指向爲另外一個對象的引用時,obj.[[prototype]]依然是其構造器的前 prototype對象。

描述代碼如下:(註釋裏是說明)

function A(){
        this.testA = new Function();
 

}
 

function B(){
 

        this.testB = new Function();
 

}
 

 

var a = new A(); 

 

B.prototype = a;
 

//a.[[prototype]] == {};(不是真的等,{}表示的是Function A初始的prototype object。下同)
 

 

var b = new B();
 

//b.[[prototype]] == a;
 

//b.[[prototype]].[[prototype]] == a.[[prototype]] == {};
 

 

A.prototype = b;
 

 

var a2 = new A();
 

//a2.[[prototype]] == b;
 

//a2.[[prototype]].[[prototype]] == b.[[prototype]] == a;
 

//a2.[[prototype]].[[prototype]].[[prototype]] == b.[[prototype]].[[prototype]] == a.[[prototype]] == {};
 

 

//最後測試一下,很搞笑的
 

alert(a instanceof A);

最後特殊的解釋:好吧,上面代碼的最後出現了很搞笑的事情,合乎語言的實現,但不合乎正常以及不正常地球人的邏輯。 我們知道,a對象是被A構造器創建出來的,所以a是A的實例。 但是,上面類型判斷那裏有講,instanceof是通過構造器prototype成員與對象原型鏈的比較來判斷的。所以當對象a被創建後,如果創建它的構造器的prototype發生了變化,a就和他媽(構造器)沒任何關係了。 看到這裏,你確定你還想要在實例化對象後,修改構造器的prototype成另外一個對象嗎?


發佈了46 篇原創文章 · 獲贊 14 · 訪問量 34萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章