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。步驟大緻如下:
- 解析形參和函數體
- 建立一個native ECMAScript Object:F
- 設定F的[[Class]]、[[Prototype]]、[[Call]]、[[Construct]]、[[Scope]]、length屬性
- 建立一個new Object():O
- 設定O的constructor屬性為F
- 設定F的prototype屬性為O
在這個建立過程裡,要說明的幾點是:
- 步驟3中F的[[Prototype]]被設定為Function.prototype
- 使用者自定義的function,都會同時具有[[Call]]和[[Construct]]這兩個internal屬性
- 解析器會自動給每一個function Object初始化一個prototype成員。而F.prototype.constructor == F,是以,當我們沒有重新定義這個F的prototype成員的時候,F的執行個體的constructor成員是靠譜的。因為(new F).constructor其實方位的是F.prototype.constructor,而解析器預設初始化給你的 F.prototype.constructor就是F。
- 關于[[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]]:
- Establish a new execution context using F's FormalParameterList, the passed arguments list, and the this value as described in 10.2.3.
- Evaluate F's FunctionBody.
- Exit the execution context established in step 1, restoring the previous execution context.
- If Result(2). type is throw then throw Result(2). value.
- If Result(2). type is return then return Result(2). value.
- (Result(2). type must be normal.) Return undefined.
[[Construct]]:
- Create a new native ECMAScript object.
- Set the [[Class]] property of Result(1) to "Object".
- Get the value of the prototype property of F.
- If Result(3) is an object, set the [[Prototype]] property of Result(1) to Result(3).
- 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.
- 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.
- If Type(Result(6)) is Object then return Result(6).
- Return Result(1).
一切都很清楚了。當我們建立一個對象,也就是我們new的時候,調用的是function Object的[[Construct]]成員方法。在上面的描述中,3、4步描述了[[Prototype]]成員的建立過程,就是構造器的 prototype成員。
好的,那回到之前,我們使用obj.property來擷取obj對象的屬性的時候,其實調用的是obj對象的internal方法 [[Get]]。那我們看看[[Get]]方法調用做了哪些事情:
- If O doesn't have a property with name P, go to step 4.
- Get the value of the property.
- Return Result(2).
- If the [[Prototype]] of O is null, return undefined.
- Call the [[Get]] method of [[Prototype]] with property name P.
- 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)。然後做下面的事情:
- 初始化arguments對象,并将它添加到這個ac中。這個時候,對象ac就擁有了一個name為arguments的成員。這裡arguments初始化過程就不具體說了,感興趣的可以看ecma262的章節10.1.8。
- 解析形參,并使用函數調用時傳遞的參數初始化。在上面的調用例子fn(1)中,這個時候,ac就擁有了一個name為param的成員,這個成員的值為 1。
- 對function declaration進行初始化,為所有FunctionBody中的function declaration,建立function Object,并添加到對象ac中作為ac的成員。在這一步,假設ac中已經包含了同名屬性,會被覆寫掉。
- 對var聲明進行初始化,為所有var聲明,在對象ac中建立同名成員,并初始化為undefined。在這一步,假設ac中已經包含了同名屬性,不會被覆寫掉。
- 初始化作用域鍊,并将這個作用域鍊與目前的執行上下文相關聯。這個作用域鍊是一個鍊式清單,最前段是進入函數調用時初始化出來的活動對象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:
- Get the next object in the scope chain. If there isn't one, go to step 5.
- Call the [[HasProperty]] method of Result(1), passing the Identifier as the property name.
- If Result(2) is true, return a value of type Reference whose base object is Result(1) and whose property name is the Identifier.
- Go to step 1.
- 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:
- Evaluate MemberExpression.
- Evaluate Arguments, producing an internal list of argument values (see 11.2.4).
- Call GetValue(Result(1)).
- If Type(Result(3)) is not Object, throw a TypeError exception.
- If Result(3) does not implement the internal [[Call]] method, throw a TypeError exception.
- If Type(Result(1)) is Reference, Result(6) is GetBase(Result(1)). Otherwise, Result(6) is null.
- If Result(6) is an activation object, Result(7) is null. Otherwise, Result(7) is the same as Result(6).
- Call the [[Call]] method on Result(3), providing Result(7) as the this value and providing the list Result(2) as the argument values.
- 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成另外一個對象嗎?
------------------- 切割線:我是結束前 ----------------------
好了,就寫這麼多吧,好久不碼那麼多字了…………
渴了,有沒有人請我喝飲料~~