首页 > 代码库 > JavaScript 对象详解

JavaScript 对象详解

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),不要关注"对象的类是什么",而是关注”对象能做什么“