本系列均参考了大量网上相关的内容,并基于此总结并归纳,作为个人笔记,也供同样与我一样初涉JavaScript面向对象编程的同学一同学习讨论。

前言

网上有非常多的介绍JavaScript类型的内容,关于类型的分类众说纷纭,各执一词,一些矛盾的观点往往会让我们感到非常困惑。后来我仔细想了 想,与其在这种泥苦苦挣扎,还不如就近找一根救命稻草抓住。当然,这种观点可能比较激进,但是有时候确实需要做些取舍与选择。选择一种你觉得相对比较正 确,并且可以接受的答案。

类型系统

本文的类型分类依据来源于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 = new String('dango');  

console.log(str instanceof String);        // 返回true  
console.log(String instanceof Function);   // 返回true  
console.log(Function instanceof Object);   // 返回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 = new String('dango');  
console.log(str.constructor === String); // true  
console.log(str instanceof String);  // true  

str.constructor = Function;  // 改变constructor属性  
console.log(str.constructor === String);  // false  
console.log(str instanceof String); // 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(new Date());   // "[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 = new Object(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 ) ) {  
            return false;  
        }  
      
        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") ) {  
                return false;  
            }  
        } catch ( e ) {  
            // IE8,9 Will throw exceptions on certain host objects #9897  
            return false;  
        }  
      
        // 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(new Date()));  // 返回false 

问题汇总

1. null是什么?

console.log(typeof null);  // "object"  
console.log(null instanceof Object);  // 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关键字,会在后面的博客文章中进一步解释。

由于近期都在搭建新博客,这一系列的内容学习可能要延后了。