1 创建对象的方法
最常见的创建对象方法是通过new调用构造函数,此外还有一个方法就是使用Object.create(),将一个原型对象作为参数传递给Object.create也可以创建一个继承了其属性的新对象。但是和使用new创建对象还有一点差别,那就是构造函数不会被调用。所以如果使用这种方法创建一个foo新对象,其foo.f是undefined的:
function Foo(z) {
this.f = z;
}
Foo.prototype.add = function(x, y) {
return x + y;
}
var foo = Object.create(Foo.prototype);
console.log(foo.add); // ==> [Function]
console.log(foo.f); // ==> undefined
那如果自己再补充调用一次构造函数,是不是就可以了呢?
Foo.call(foo, 7);
console.log(foo.f); // ==> 7
真的可以!不过这样的调用方式没有任何方便性可言,所以还是推荐使用new更加方便。
创建一个连toString()和valueOf()这样的基础属性都没有的空对象可以使用:
var obj = Object.create(null);
另外,下面三行语句是等效的。
var obj = {};
var obj = new Object();
var obj = Object.create(Object.prototype);
2 内置对象的创建
犀牛书(第六版P204~P205)说:"构造函数就是用来‘构造新对象’的,它必须通过关键字new调用如果将构造函数用做普通函数的话,往往不会正常工作"。其实对于自定义类型的对象,这个结果是明确的,因为Foo函数没有写返回值所以会返回undefined
var foo = Foo(7);
console.log(foo); // ==> undefined
但是对于内置对象的实验的结果很有意思,竟然正确的创建了一个Array对象,后来通过查询ECMAScript规范发现,不同的对象直接调用构造函数
其结果是不一致的,对于Array在ECMAScript 5(15.4.1节)是这么说的"Create and return a new Array object exactly as if the standard built-in constructor Array was used in anew expression with the same arguments",而对于Date在ECMAScript5(15.9.2.1节)是这么说的"A String is created and returned as if by the expression (new Date()).toString()",对于Array()来说等效于"new Array()"而"Date()"只是返回一个代表当前时间的字符串, 这两个对象的行为不一样!
var a = new Array();
var b = Array();
var c = new Date();
var d = Date();
Object.prototype.toString.call(a); // ==>'[object Array]'
Object.prototype.toString.call(b); // ==>'[object Array]'
Object.prototype.toString.call(c); // ==>'[object Date]'
Object.prototype.toString.call(d); // ==>'[object String]'
所以,还是不要单独调用构造函数了,因为返回的对象类型并没有统一的规则,容易出错。
3 包装对象
JavaScript里的值类型可以在必要的时候自动变成对象,这非常类似Java/C#里的装箱/拆箱特性,字符串/数字/布尔值都可以在必要的时候转化成对应的对象String/Number/Boolean,相当于系统帮助我们调用了new String(s)。同样的,JavaScript会在必要时将包装对象转换成原始值。注意新建的只是一个临时对象,临时对象用后即丢弃,如果为这个临时对象的属性赋值会被忽略!
console.log("abcdef".length); // ==> 6
var s = "test";
s.x = 4;
var t = s.x;
console.log(s.x); // ==> undefined
4 对象的类型
熟悉了静态语言的朋友一定不会喜欢JavaScript的类型系统,JavaScript里虽有三种方法(详见下面的方法一,方法二,方法三)能确定对象的类型,但却没有一个是完美的解决方案。下面的Foo和Bar是用作例子程序的两个类型。他们使用不同的方式为原型添加属性,Foo单纯的添加属性,而Bar是新建了一个对象直接量然后赋值给原型。
function Foo(z) {
this.f = z;
}
Foo.prototype.add = function (x, y) {
return x + y;
}
var foo = new Foo(7);
function Bar(y) {
this.b = y;
}
Bar.prototype = {
add: function (x, y) {
return x + y;
}
}
var foo = new Foo(7);
var bar = new Bar(7);
方法一 instanceof运算符
先看看instanceof运算符在不同情况下的表现吧
foo instanceof Foo // ==> true
bar instanceof Bar // ==> true
<html>
<script>
var cwindow = window.open();
var carray = cwindow.Array(5);
var r1 = carray instanceof Array
var r2 = carray instanceof child.Array
alert("Array:" + r1 + "\nchild.Array:" + r2);
</script>
</html>
<pre name="code" class="html">
<!-- Array:false -->
<!-- child.Array:true -->
可以看到,instanceof不受原型对象的影响,对于对象直接量当作原型的情况也可以准确识别出其类型,但是第三个例子,来自不同窗口的另一个Array被当作不同的对象对待了。
缺点:
1 只能回答a的类型是不是A,无法回答a的类型是什么。也就是说必须事先知道要检测的对象的类型。
2 无法跨窗口访问
方法二 constructor属性
foo.constructor === Foo // ==> true
bar.constructor === Bar // ==> false
<html>
<script>
var cwindow = window.open();
var carray = new cwindow.Array(5);
var r1 = (carray.constructor === Array);
var r2 = (carray.constructor === cwindow.Array);
alert("Array:" + r1 + "\nchild.Array:" + r2);
</script>
</html>
<pre name="code" class="html">
<!-- Array:false -->
<!-- child.Array:true -->
如果简单使用对象的constructor属性来判断类型是最不靠谱的一种方法,采用覆盖原型方法创建的对象无法通过constructor属性判别属性,跨窗口访问也不被支持
缺点:
1 依赖constructor属性
2 只能回答a的类型是不是A,无法回答a的类型是什么。也就是说必须事先知道要检测的对象的类型。
3 无法跨窗口访问
方法三 构造函数的名字
这种方法的思路是:通过对象的constructor属性访问到对象的构造函数,在构造函数上调用toString()方法获得构造函数的实现,然后直接从函数实现里截取函数名(正则表达式),于是我们在预先不知道对象类型的前提下成功获取了其类型。同时,在跨窗口访问时也可以正常使用,因为两个窗口的构造函数名字是一样的。因为这种方法同方法二一样使用了对象的constructor属性,其弊端就是一旦对象的原型是通过覆盖的方式生成的就会失效。
foo.constructor.toString().match(/function\s*([^(]*)\(/)[1]; // ==>'Foo'
bar.constructor.toString().match(/function\s*([^(]*)\(/)[1]; // ==>'Object'
<html>
<script>
var cwindow = window.open();
var carray = new cwindow.Array(5);
var array = new Array(5);
var r1 = (array.constructor.toString().match(/function\s*([^(]*)\(/)[1]);
var r2 = (carray.constructor.toString().match(/function\s*([^(]*)\(/)[1]);
alert("Array:" + r1 + "\nchild.Array:" + r2);
</script>
</html>
<!-- Array:Array -->
<!-- child.Array:Array -->
缺点:
1 依赖constructor属性
总结三种判别类型的方法,两种方法(方法二 方法三)都与constructor属性有关,唯一不受constructor属性影响的方法(方法1)无法实现跨窗口判别类型,总之没有一种完美实现的类型判断方法,这或许也是在暗示我们,没必要过分关心JavaScript对象的类型,我们研究类型是为了淡化类型,因为JavaScript更适合使用鸭式辨型(duck typing),不要关注"对象的类是什么",而是关注”对象能做什么“