JavaScript面向对象编程一:类型系统

http://kodango.me/javascript-oop-type-system

类型系统

本文的类型分类依据来源于aimingoo的博客中关于JavaScript类型的几篇博客文章[1][2][3]

Javascript有两套类型系统:基础类型系统与对象类型系统。


前者使用typeof运算符识别,该运算符返回变量所属类型的名称,一般包括undefined、number、boolean、string、 object和function六种类型,其中object和function是引用类型,而其余的是值类型。这一套系统也是在JavaScript编程 中最常见也是最基础的。

后者以前者为基础,在object这一类型的基础上引申出来,这套系统中包含相对较多的对象类型,如Number、Boolean、String、 Array、Object、Function、Date等常用类型,还有许多属于这一类型的对象,在此就不一一例举了。该套系统的对象类型常用 instanceof运算符识别。在[1]中有一张JavaScript类型总览的总结图,本文最后也引用了这一张图,见"本文总结"部分。

除此之外,两套系统的部分类型之间还存在映射关系,例如基础类型系统中的number与对象类型系统中的Number类型。但是,映射关系并不代表 这几组类型是等价的,事实上,这些有映射关系的几组类型之间是有本质的不同的。对象类型中的String等类型都是Object类型的子类,在基础类型中 是属于object类型,因此是引用类型。

基础类型在.运算符或者[]运算符下会隐式地转换成对象类型,因此以下两种方式是等价的:

console.log('dango'.length);// 隐式转换,与以下语句等价  
console.log(Object('dango').length);

所以,不要以为基础系统中的string类型拥有indexOf等方法,这些方法是String类型的。

:注意几种表达方式的区别,string是基础类型系统中的字符串类 型,String类型是对象类型系统中的字符串封装类型,而单单String这种描述是指String类型的构造函数。String构造函数是 Function类型的实例,Function类型和String类型等均为Object类型的子类。

var str =newString('dango');  

console.log(str instanceofString);// 返回true  
console.log(StringinstanceofFunction);// 返回true  
console.log(FunctioninstanceofObject);// 返回true

aimingoo在他的博客文章[1]中给出一幅关于JavaScript类型的总览图,总结得非常到位,可以辅助理解,我将图放到"本文总结"部分。

类型判断

了解了类型的分类之后,摆在面前的另外一道难题是如何准确并有效地区分它们。所幸地是,JavaScript提供如何识别类型的方法,例如上文提到 的typeof和instanceof;但又不幸地是,没有一种方法可以完美地解决所有类型识别的问题。无论如何,了解几乎类型识别的方法总是有百利而无 一害的。我在stackoverflow的这个回答中发现一篇关于JavaScript类型判断的方法总结的文章[4]。该文章中指出四种不同的判断方法,分别是:

1. typeof
typeof运算符用于判断类型,高效但是功能有限,只能告诉你某个变量是否为基础类型中的值类型(如string、number等)或者引用类型(如 object或者function),但是无法区分属于object类型的不同对象类型,如String、Number、Date等。

2. instanceof
instanceof运算符解决typeof在对象类型识别上的局限性,能够确定某些变量具体属于哪种对象类型。该运算符与直接使用对象的 constructor属性来判断是差不多的,但是有一种情况除外,假如你人为地重新赋值该对象的constructor属性,instanceof依赖 可以正确地判断,而借助于constructor属性则会出错。

var str =newString('dango');  
console.log(str.constructor ===String);// true  
console.log(str instanceofString);// true  

str.constructor =Function;// 改变constructor属性  
console.log(str.constructor ===String);// false  
console.log(str instanceofString);// true

:在多窗口环境(frames, iframes)下,某个窗口无法识别来自另外一个窗口的变量的类型,原因是两个窗口相同名称的类型分别处于两个不同的作用域(window),是不同的类型。[5]

3. Object.prototype.toString
toString是定义在Object的原型对象上的内建方法,返回对象的字符串描述。对于内置的对象,例如String、Date等,该方法返回标准定义的字符串返回值: "[object XXX]",其中"XXX"是指类型的名称,例如:

var type =Object.prototype.toString();  
console.log(type.call(newDate());// "[object Date]"  
console.log(type.call([]));// "[object Array]"

但是该方法比起前面两种方法效率相对比较差,同时自定义的对象类型不一定适用。

:最好不要使用obj.toString()这种形式,因为对于具体的某种类型的对象,该方法可能会被用户覆盖。该方法同样适用于基础类型,因为基础类型会隐式地转换成相应的对象类型。

4. Duck typing

有时候,我们并不需要知道某个变量到底是属于哪种类型,而只需要判断该变量是否支持某种或者某几个属性或者方法。这种判断的方法类似于DOM中的特性检测,它也有一个比较专业的术语,叫做Duck typing[6]。在Wikipedia上是这么描述的:

"When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck

下面从jQuery的源码中摘录部分与类型判断相关的片断,准确地说是对象类型的判断。我们一段一段来,首先在jQuery中将Object.prototype.toString等核心方法保存:

// Save a reference to some core methods  
toString =Object.prototype.toString,  
hasOwn =Object.prototype.hasOwnProperty,

随后,将对象类型的toString结果保存到class2type对象中:

// [[Class]] -> type pairs
class2type ={};

jQuery.each("Boolean Number String Function Array Date RegExp Object".split(" "),function(i, name){
	class2type["[object "+ name +"]"]= name.toLowerCase();

定义基础的type函数,用于识别对象的类型:

type:function( obj ){return obj ==null?String( obj ):  
        class2type[ toString.call(obj)]||"object";},

该函数还是非常简单的,在此基础上同时定义了一些封装函数,例如isFunction或者isArray:

isFunction:function( obj ){return jQuery.type(obj)==="function";},  
  
isArray:Array.isArray ||function( obj ){return jQuery.type(obj)==="array";},

jQuery中还实现了一个特殊的函数isPlainObject(),它的作用是来判断一个对象是否为plain object,plain object是指用JSON形式定义的普通对象或者new Object()创建的简单对象,例如:

var obj ={ name:'dango',from:'china'};var obj2 =newObject(obj);

plain object是Object类型的实例,因此它的构造函数为Object,它指向(obj.__proto__属性)的原型对象为Object.prototype,同时。

jQuery.isPlainObject方法的代码如下所示,同时在代码中我将自己的一些理解以注释的形式标注了出来:
    isPlainObject:function( obj ){// Must be an Object.  // Because of IE, we also have to check the presence of the constructor property.  // Make sure that DOM nodes and window objects don't pass through, as well  // 确定obj类型为"object",同时不是DOM节点对象或者window全局对象。  if(!obj || jQuery.type(obj)!=="object"|| obj.nodeType || jQuery.isWindow( obj )){returnfalse;}try{// Not own constructor property must be Object  // 排除用new创建的对象(非new Object()),例如new String('d)等。  // 原因:  // a. {}或者new创建的对象的constructor属性都是继承自它的原型对象的。  // b. isPrototypeOf这个方法是Object.prototype引入的,任何继承自Object的子类增多可以通过原型链访问该属性。  // 因此可以排除原型对象或者new创建的Object子类对象。  if( obj.constructor &&!hasOwn.call(obj,"constructor")&&!hasOwn.call(obj.constructor.prototype,"isPrototypeOf")){returnfalse;}}catch( e ){// IE8,9 Will throw exceptions on certain host objects #9897  returnfalse;}// Own properties are enumerated firstly, so to speed up,  // if last one is own, then all properties are own.  // for .. in语句用于枚举一个对象的可枚举属性,包括继承的属性。  // 一些内置的属性是不事枚举的,例如继承自Object的toString等属性。  // 注:对象的属性是否可以枚举可以使用obj.propertyIsEnumerable(p)方法来判断。  var key;for( key in obj ){}// 如果对象为空或者对象的所有可枚举的属性均为非继承的属性  return key ===undefined|| hasOwn.call( obj, key );}

对于isPlainObject方法的最后一段代码我不是非常清楚,为什么要云遍历对象的属性?

可以试试:

console.log(jQuery.isPlainObject({}));// 返回true  
console.log(jQuery.isPlainObject(newDate()));// 返回false 

问题汇总

1. null是什么?

console.log(typeofnull);// "object"  
console.log(nullinstanceofObject);// false  

这说明null并不是Object类型或者其子类型,因此确实存在一个变量是对象(object),但却不是Object类型或者其子类型的实例。 因此要特别注意typeof在对象判断上的局限性,你无法确定他是不是某种对象,也就无法确定能否使用该对象的方法,这个时候就需要借助于 instanceof来识别变量的类型。

2. Object.__proto__是什么?为什么Object.__proto__ instanceof Function返回false?来自stackoverflow的问题
分成几个步骤来回答这两个问题:

(1) Object是一个构造函数,是Function类型的对象,因此:

Object.__proto__ === Function.prototype;  

(2) Object.__proto__是一个函数对象,可以通过上文所说的typeof运算符识别:

console.log(typeof Function.prototype); // "function" 

(3) Function.prototype是一个函数对象,但是显然地是一个对象无法从它本身继承,事实上Function.prototype最终继承自Object.prototype。

    console.log(Object.prototype.isPrototypeOf(Function.prototype)); // 返回true  

:某个对象的__proto__属性,即obj.__proto__是指向原型链上的构造函数的原型对象,该属性不是一个标准定义的属性,某些浏览器是不支持这个属性的。事实上,该属性应该是一个隐藏的不可见的属性。

关于Function.prototype或者Object.__proto__到底是什么,可以参考[7]

3. 为什么 Object.constructor===Object.constructor.constructor 返回true?来源自stackoverflow的问题

要回答这个问题,首先得了解等式两边到底指的是什么内容,同样分成几个步骤来回答:

(1) Object是一个构造函数,因此它是Function类型的一个实例。

(2) Object构造函数本身不拥有constructor属性,Object.constructor其实是从它的原型对象上继承的,问题2中说过,它的原型对象是Object.__proto__或者Function.prototype。因此事实上:

Object.constructor === Function.prototype.constructor; // 返回true  

Function.prototype.constructor是Function构造函数本身:

Function.prototype.constructor === Function;   // 返回true 

(3) Object.constructor同样也是Function类型的实例,因此它和Object拥有相同的原型对象,即:

Object.__proto__ === Object.constructor.__proto__; 

(4) 由(2)和(3)得出,

Object.__proto__.constructor === Object.constructor.__proto__.constructor
// 或者
Object.constructor === Object.constructor.constructor 

这个问题解释比较绕,理解起来可能有点困难,原问题的回答[8]给出了一幅非常形象地描绘了这些对象之间的关系的图,我将图放到"本文总结"部分

本文总结

总结部分,不打算说太多话,只引用上文提到过的两幅图,分别来自[1][8]:

JavaScript类型总览
JavaScript Object Layout
本文主要讲述了JavaScript中的类型系统以及类型识别两方面内容,这些内容都是后面要说的面向对象编程的基础,尤其是本文中第四部分中多次出现的constructor、__proto__或者prototype关键字,会在后面的博客文章中进一步解释。

posted @ 2012-12-26 15:54  haiwei.sun  阅读(262)  评论(0编辑  收藏  举报
返回顶部