本系列均参考了大量网上相关的内容,并基于此总结并归纳,作为个人笔记,也供同样与我一样初涉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中的类型系统以及类型识别两方面内容,这些内容都是后面要说的面向对象编程的基础,尤其是本文中第四部分中多次出现的constructor、__proto__或者prototype关键字,会在后面的博客文章中进一步解释。
由于近期都在搭建新博客,这一系列的内容学习可能要延后了。
楼主,好关电脑吃宵夜啦~
楼主,吃不吃宵夜啊