下面是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....