下面是JS的一個面試題
var a = new Object;
var b = new Object;
var c = new Object;
c[a] = a;
c[b] = b;
//輸出false
alert(c[a] === b);
//輸出true
alert(c[a] === b);
答案已在注釋中指出。此處分析原因,主要是要理解JS創建對象的過程。
JS中有兩種創建對象的方式,一種是通過new運算符,還有一種是通過字面量的方式。
一、利用字面量
ECMA標準語法如下:
Ecma 262 10.1.5
ObjectLiteral :
{ }
{ PropertyNameAndValueList }
PropertyNameAndValueList :
PropertyName : AssignmentExpression
PropertyNameAndValueList , PropertyName : AssignmentExpression
PropertyName :
Identifier
String Literal
Numeric Literal
根據描述,如果創建的不是空對象,而是一個帶有Name和Value的對象,那么Name可以是JS中的標識符、字符串或者數字。具體的創建過程是可以描述成:
(1)var obj = {} 就等于var obj = new Object()
The production ObjectLiteral : {} is evaluated as follows:
- Create a new object as if by the expression new Object().
- Return Result(1).
(2)var obj = { PropertyNameAndValueList }
如果是這種帶了鍵值對的對象,首先還是調用new Object()來創建一個新的對象,然后會計算PropertyName、AssignmentExpression,利用GetValue方法獲取AssignmentExpression的值,最后調用被新創建對象的[[Put]] 方法(obj的put方法是內部方法,外部無法調用),具體細節如下:
The production ObjectLiteral : { PropertyNameAndValueList } is evaluated as follows:
- Evaluate PropertyNameAndValueList.
- Return Result(1);
The production PropertyNameAndValueList : PropertyName : AssignmentExpression is evaluated as follows:
- Create a new object as if by the expression new Object().
- Evaluate PropertyName.
- Evaluate AssignmentExpression.
- Call GetValue(Result(3)).
- Call the [[Put]] method of Result(1) with arguments Result(2) and Result(4).
- Return Result(1).
這里的GetValue和[[Put]]方法都可以暫且不管,因為它們對于程序員并不可見。進一步看一下Evaluate PropertyName的過程:
The production PropertyName : Identifier is evaluated as follows:
- Form a string literal containing the same sequence of characters as the Identifier.
- Return Result(1).
The production PropertyName : StringLiteral is evaluated as follows:
- Return the value of the StringLiteral.
The production PropertyName : NumericLiteral is evaluated as follows:
- Form the value of the NumericLiteral.
- Return ToString(Result(1)).
可以發現,在利用字面量創建對象的時候:如果屬性的name用JS中的標識符表示,那么name會被轉成值相同的字符串;如果屬性的name是number,那么會調用ToString來計算該number的字符串表示,這兒的ToString也是JS內部的方法。
二、利用new Object()
ECMA標準語法如下:
new Object ( [ value ] )
When the Object constructor is called with no arguments or with one argument value, the following steps are taken:
- If value is not supplied, go to step 8.
- If the type of value is not Object,go to step 5.
- If the value is a native ECMAScript object, do not create a new object but simply return value.
- If the value is a host object, then actions are taken and a result is returned in an implementation-dependent manner that may depend on the host object.
- If the type of value is String, return To Object(value).
- If the type of value is Boolean, return To Object(value).
- If the type of value is Number, return To Object(value).
- (The argument value was not supplied or its type was Null or Undefined.)Create a new native ECMAScript object.
The [[Prototype]] property of the newly constructed object is set to the Object prototype object.
The [[Class]] property of the newly constructed object is set to "Object".
The newly constructed object has no [[Value]] property.Return the newly created native object
很顯然,**如果是不傳參數,那么會創建一個 native ECMAScript object,隨后會給這個object添加一系列的內部屬性 **,比如將 [[Prototype]]設置為Object prototype object(即Object.prototype),將[[Class]]設置為字符串“Object”。注意,在Object.prototype中已經包含了一些方法:
- toString ( )
- toLocaleString ( )
- valueOf ( )
- hasOwnProperty (V)
- isPrototypeOf (V)
- propertyIsEnumerable (V)
利用new Object新創建的對象不會有除了上面之外別的方法。
對象的屬性訪問、賦值
對JS中的對象進行屬性訪問同樣也是兩種形式,一種是利用“[ ]”運算符,還有一種是利用“.”運算符。即:
CallExpression. Identifier 或CallExpression[ Expression ]
注意就是利用“.”的時候,只能后接一個合法的Identifie。但是如果是利用“[ ]”卻可以包含一個表達式進來,本文剛開始的題目中就是利用這種形式去訪問obj的屬性。其實CallExpression. Identifier也會按照CallExpression[ <Identifier-string>]去執行。
CallExpression[ Expression ] is evaluated as follows:
- Evaluate CallExpression.
- Call GetValue(Result(1)).
- Evaluate Expression.
- Call GetValue(Result(3)).
- Call ToObject(Result(2)).
- Call ToString(Result(4)).
- Return a value of type Reference whose base object is Result(5) and whose property name is Result(6).
尤其要注意第6行, 所有方括號中的Expression的值要經過ToString這一步。
對于賦值,具體的計算過程如下:
The production AssignmentExpression : LeftHandSideExpression = AssignmentExpression is evaluated as follows:
- Evaluate LeftHandSideExpression.
- Evaluate AssignmentExpression.
- Call GetValue(Result(2)).
- Call PutValue(Result(1), Result(3)).
CallExpression就是一種 LeftHandSideExpression。
題目詳解
下面來拆分題目,逐行來推敲具體的執行過程,首先是三條創建對象的語句:
//創建一個native Ecmascript object
//[[Prototype]]指向Object.prototype
//[[Class]]設為"Object"
var a = new Object;
//同a
var b = new Object;
//同a
var c = new Object;
注意,一個obj里并非只有 [[Prototype]]和[[Class]]兩個內部屬性,具體的內部屬性以及內部方法可以參考Ecma262的8.6章節。
在創建完對象后,又是兩條屬性賦值語句。
//c[a]會按照CallExpression[ Expression ] 的7個步驟來計算,
//其中的GetValue較為復雜,可以參考http://www.w3help.org/zh-cn/causes/SD9028的注釋2
//注意第6步,ToString(a)會調用到ToPrimitive(a),進而調用a的[[DefaultValue]]方法,具體參考Ecma規范
//這里最終會調用到a.toString方法,根據Object.prototype.toString的描述,會返回[object Object]
//即最終相當于c["[object Object]"]=a;
c[a]=a;
//即最終相當于c["[object Object]"]=b;
c[b]=b;
alert(c[a]===a);
稍微變個形
var a = {};
var b = {};
var c = {};
c.a=a;
c.b=b;
alert(c[a]===a);
你可能會立馬想到,c.a是怎么處理的,很顯然這是利用了“.”運算符訪問屬性,而不是“[ ]”。根據上面的描述, CallExpression. Identifier會按照CallExpression[ <Identifier-string>]去執行,那么這里的c.a就相當于c["a"]。因此最后結果肯定是true了?
真的是這樣么?別忘了,在alert語句中還有c[a]的存在,如前文所述,c[a]完全就相當于c["[object Object]"],因此這段代碼相當于:
var a = {};
var b = {};
var c = {};
c["a"]=a;
c["b"]=b;
alert(c["[object Object]"]===a);
真是相當的惡心....
再來變一變
這次c的屬性直接在聲明里寫好了。
var a = {};
var b = {};
var c = {
a:a,
b:b
};
alert(c.a===a);
這回是利用了字面量創建對象的方式。根據上面的描述,如果鍵值對的name是一個合法的JS標識符,那么name就是將該標識符變成直接字符串,簡單來說,就是兩邊加上引號。
因此,上面這段代碼相當于:
var a = {};
var b = {};
var c = {};
c["a"]=a;
c["b"]=b;
alert(c.a===a);
終于輸出了true....