日期:2014-05-16  浏览次数:20405 次

Javascript对象真经

Javascript对象真经
2011年01月07日
  UI前端2011-01-07 16:12:40阅读1评论0  字号:大中小 订阅 之所以用这样猛的一个名字,只是想表达我是想尽力的去讲透Javascript中对象的来龙去脉。既然是我尽力而不是你、他,就必然受 我的知识结构的限制,所以如果有不妥不对的地方,请诚心指出,好让我参考学习。注意:以下文中我将使用Js代替Javascript,同时请多分耐心因为 文章有点长。
  首先要明白的是:Js是使用是原型继承来实现对象系统的。那什么是这个原型呢?在Js中,其实原型也是一个对象实例,这点很关键,也就是说原型本身就具有了很多可读取的属性和可调用的方法,而不像一些基于类继承来实现的对象系统的语言(比如:Java)中的类,类不必持有这些,只要描叙一个蓝图就行。但就Javascript的语言和对象系统的实现来讲,对象实例并没有原型,而只有构造器才有原型。
  那原型继承的实质是什么呢?其实就是复制,对,就是形成的新对象实例从原型复制出对应的信息。不过谈到复制,很多大侠自然就会联想到它的性能问题,那我要说的是,目前Js引擎基本都是采用了读遍历。
  
  我们看上面的图片,这是在形成对象时,由于我们并没有实际的需求去改变继承自的对象原型的属性,所以我们只要在系统中指明obj1和obj2等同于 它们的原型,那么读取的时候,顺着指针去读原型即可。但当真正要写入一些属性时,Js的做法,和一般的写复制就有点不同了。情况就如下图所示。
  
  整个过程,并没有完全像写复制那样克隆一个完全的原型对象实例的拷贝,而是创建了一张对象实例自己以后维护的成员表,这个列表记录obj1发生了修 改的成员名、值与类型。其实上面有说过,原型也是一个对象实例,那就意味着它自身也要维护一个这样的表格,那这张表格和原型维护的表格就必然有可能冲突, 于是就引出了要遵循的读遍历规则: 查找成员时,先读对象实例自己维护的表;
  如果没有找到指定的成员,就要遍历整个原型链,直到原型为空的对象({}),或者到找到该成员为止。
  这里我要讲一个问题,就是null和{}的区别。我们知道null也可以“理解为”是对象类型的,typeof null返回object可以说明这一点。它虽然可以“理解为”对象类型,但完全可以当成对象中的另类,它有几点特殊: 对象是彻底的空值的,这么说的含义是它连内置初始话的方法和属性都没有一个(如:toString),也没有原型(prototype);
  null是Js中的保留关键字;
  也可以参与运算,其值会自动转换为0、”null”或者false其中的一个。
  而我说的空的对象({})是指一个有原型,有内置属性的纯净的对象。当然你for … in循环遍历,无法读取任何属性,因为都是内置的。而且Object构造器的原型其实还是空的对象{},空的对象的constructor属性所指的构造函数还会是Object。 [javascript]
  js> typeof null
  object
  js> Object.prototype
  [object Object]
  js> Object.constructor
  function Function() {
  [native code]
  }
  js> Object.prototype.constructor
  function Object() {
  [native code]
  }
  js> Object.prototype.constructor.prototype.constructor === Object
  true
  [/javascript]
  Js构造对象的利器其实就是一个“改造后”的函数,名叫构造器。我们可以这样理解,一个普通的函数并没有prototype这个属性(有也是多余的),只有当实实在在要利用prototype属性时才会去创建一个空的对象({})指向它,而且这个原型实例创建后的,其constructor属性总是先默认赋值为当前的函数。这个地方很关键,一定要加深理解,这里放上一段代码来加深理解: [javascript]
  js> function MyObject() {}
  js> var obj = new MyObject()
  js> obj.constructor //实例本身的constructor属性默认为MyObject
  function MyObject() {
  }
  js> MyObject.prototype.constructor //注意构造器函数原型(prototype)对象实例的constructor属性默认也为这个构造器函数MyObject
  function MyObject() {
  }
  js> MyObject.prototype //原型为一个空的对象
  [object Object]
  [/javascript]
  讲到了这里,就为我要说的两个原型链做完铺垫了。深呼吸,闭上你的眼睛,放松一下吧。先看下下面的美图。  
  
  
  这幅图还配套了代码呢。 [javascript]
  function MyObject() {
  this.constructor = arguments.callee; //正确维护constructor,以便回溯外部原型链
  }
  MyObject.prototype = new Object(); //人为构建外部原型链
  function MyObjectEx() {
  this.constructor = arguments.callee; //正确维护constructor,以便回溯外部原型链
  }
  MyObjectEx.prototype = new MyObject(); //人为构建外部原型链
  obj1 = new MyObjectEx();
  obj2 = new MyObjectEx();
  print(obj1.constructor === MyObjectEx); //true
  print(MyObjectEx.prototype instanceof MyObject); //true
  print(MyObjectEx.prototype.constructor === MyObject); //true
  print(MyObject.prototype instanceof Object); //true
  print(MyObject.prototype.constructor === Object); //true
  //完成了所有的回溯
  print(obj1.constructor.prototype.constructor.proto type.constructor === Object); //true
  [/javascript]
  图虽然很好的回溯了我们开发人员构造的这样一个原型链,但其实看上面的代码就知道,是我们开发人员是要下很多功夫的。我来重点讲解下。前面我提到过要重点关注构造器函数原型(prototype)对象实例的constructor属性默认也为这个构造器函数,那如果我只是随意的改变一个构造器函数的原型属性,那就必然出现断链的情况。下面的代码很好的说明了这个情况: [javascript]
  function MyObject() {}
  function MyObjectEx() {}
  var newprototype = new MyObject();
  MyObjectEx.prototype = newprototype; //本质就是我们常用的MyObjectEx.prototype = new MyObject();
  obj1 = new MyObject();
  obj2 = new MyObjectEx();
  print(obj1.constructor === obj2.constructor) //true 问题就出现了,从obj2无法回溯到MyObjectEx构造器了
  [/javascript]
  由于obj2.constructor === MyObjectEx.prot