记一次关于js数组类型判断及js类型判断的细节探索

2,560 阅读9分钟

一、前言

众所周知,js是门“动态”、“弱类型”编程语言,这意味着在js中可以任性定义变量,同时,“任性”也意味着需常在项目开发中对变量做类型判断,曾几何时,对数组变量的类型判断是件很痛苦的事情,开发人员想出多种方案来对数组做出准确的类型判断,但效果不佳,直到ES5标准“入主中原”,判断数组类型有了标准的isArray()官方利剑,才降伏了数组类型判断这条恶龙,世间得一清,但在此之前开发者是如何判断数组类型的?判断数组类型为何会如此玄学?为何要判断数组类型?带着这些疑问,吾跋山涉水,探寻各方资料,整理消化后遂成此文,以记之。

二、判断js数组类型为何麻烦?

1、语言本身的“缺陷”

js是门“动态”“弱类型”编程语言,这意味着js在定义和使用变量时可以“任性”,在ES6之前,我们定义变量一般使用“var”来定义:

var name = 'jack';
name = 20;
name = ['aa'];

在上述例子中,name变量初始定义为字符串类型,而后变为数字类型,最后摇身一变成为数组类型,这种任性摇摆的特性就是其“动态”特性,在java中我们定义一个字符串变量须如此定义:String name = 'jack',java通过一个String前缀“显式的”、“强制的”指定name变量为字符串类型,之后不得对该name变量进行类型变换(如果执行name = 22将会报type类型转换错误),但js采用的是弱类型定义方案,在定义变量时使用var声明了一个变量,弱化了类型前缀的限制,并没强制锁死变量类型,之后可以随意更改其类型。动态弱类型这种声明变量的方案用起来可以随性而为,无须顾虑太多,随性的代码书写如若不加管制必将招致灾难性的代码bug。

2、js类型判断的“不足”

其实动态弱类型的语言特性并不是决定js判断数组类型麻烦的必然原因,js语言因为历史原因,其创造者在开发之初将其定位为简单的网页小助手语言,为了轻巧、快速的完成小任务开发选择了“动态弱类型”的语言方案,PHP亦为动态弱类型语言,但在处理类型判断时,PHP用一个gettype()方法可以轻松、精准的搞定(PHP作为世界上世界上最好的语言还是有、东西的),PHP有gettype()这枚银弹,js有吗,嗯,算有吧,js最常用的是用typeof操作符来获取数据类型,看typeof这个名字是不是感觉很厉害?感觉会跟PHP一样轻松简单?但随后你会发现:typeof操作符是个很局限的类型获取方案,用它对基本数据类型做判断还算过得去,但在涉及到引用类型判断这种细活时就显得很low了(本以为是个王者,没想到是个青铜)。

三、判断js数组类型的几个“方案”

1、typeof操作符方案(Pass)

前言:typeof操作符本应是解决js类型判断的合适方案,奈何负了众望。

众所周知,js分两大数据类型:基本数据类型和引用数据类型,typeof操作符可以对两大数据类型做出基本的判断,我本以为typeof对基本数据类型做判断是可以的,但后来发现其实是有问题的,比如用typeof判断基本数据类型null:typeof null结果就是“object”(关于typeof null为object的问题属历史遗留问题,可查阅相关资料了解详细原因),typeof在判断基础数据类型时尚有问题,更别说用来判断子孙繁多的引用类型了,typeof在判断引用类型时一刀切的统统返回object

var obj = {};
var arr = [];
var map = new Map();

typeof obj; // object
typeof arr; // object
typeof map; //object

在上面的例子中可以看到typeof在判断{}时返回object'、[]亦返回object`,数组和对象的判断结果根本没区别!所以用typeof来判断数组类型的判断,pass!

2、instanceof运算符方案(存在缺陷)

instanceof是js用来判断继承关系的运算符(js基于原型链实现继承,故instanceof判断的就是对应的类是否存在于变量的原型链上),根据这个特性可以如此来判断数组类型:

['a'] instanceof Array; // true

打印数组['a']可以看到如下结果: 从打印的结果可以看到Array存在于数组['a']的原型链上,故['a'] instanceof Array === true;,利用instanceof的这个特性可以判断数组类型,但是instanceof运算符有个弊端,就是如果跨越frame会存在问题:

var iframe = document.createElement('iframe');
document.body.append(iframe);
var FrameArray = window.frames[window.frames.length-1].Array;
var array = new FrameArray();
console.log(array instanceof Array); // false

跨越frame导致的判断失误属于意料之中,也很好理解,前面说过instanceof运算符是用来判断继承关系的(判断是否存在血统连接关系),原型链好比家族里面的派系链,不同的frame相当于不同的家族,在同一个家族中同一派系上的族人存在着链接关系,但如若家族不同(frame不同),则派系链则更不可能相同了。所以利用instanceof运算符判断数组类型的方案,pass!

3、Object.prototype.toString()方案

instanceof运算符属于血统继承性判断,这种判断是基于实物的纽带性判断,在早前判断数组类型时很多类库时采用该方案(如早期的jquery),但不同frame导致instanceof出现的局限性不得不让开发者放弃该方案,转而寻求更合理的方案,这时候开发者想到既然这种血统继承性判断有弊端,那有没有含蓄而深入点的方案呢?嗯,有请 Object.prototype.toString()方法:

console.log(Object.prototype.toString.call([])); // "[object Array]"

该方案能获取到变量的“类目名”,在js中万物皆为对象,万物皆有“类目名”,每个变量、对象、数组等都有一个唯一的类目名(这个类目名类似于人类给各类动植物起的“学名”),该方案通过获取目标变量的类目名([object Array])进行判断,如果类目名一致则证明目标变量为数组类型:

var a = [];
Object.prototype.toString.call(a) === "[object Array]"; // true

关于“Object.prototype.toString()”这个方案,在这里我来好好叨叨:

  • *1、为何要用Object原型上的toString()方法?*我们都知道js的引用类上都存在着toString()方法,然而每个引用类都对toString()方法做了自我实现,比如数组调用toString()方法会产生如下结果:
['aa', 'bb'].toString(); // "aa,bb"
  • *2、Object.prototype.toString()方法是何方神圣?*该方法可用来获取变量的类目名,通过该方法可获取到变量的“[[class]]”属性值,这个属性值是js的造物主给所有的类定义的类目名(也可以佐证js中万物皆是对象的说法,详情可看Ecma详情定义)
Object.prototype.toString.call([]); // "[object Array]"
Object.prototype.toString.call({}); // "[object Object]"
Object.prototype.toString.call(''); // "[object String]"
Object.prototype.toString.call(1); // "[object Number]"
Object.prototype.toString.call(function f(){}); // "[object Function]"
Object.prototype.toString.call(); // "[object Undefined]"
Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call(document); // "[object HTMLDocument]"
  • 3、如何记忆用Object.prototype.toString()方法来判断数组类型的方案?我以前记该方案时总易记错,后来我这样理解:该方法获取的是变量的“类目名”,类目名这种肯定属于顶级属性,不能随便得之,而Object属于所有类的顶级类,这种顶级属性肯定归属于顶级类,而为了享用这种顶级属性,就须用到原型链上顶级方法:Object.prototype.toString(),一般的“凡物”变量肯定不能随便享用顶级方法的,为能享之,须借神器:call()、apply()方法

4、Array.isArray()方案

isArray()方法是ES5标准规定的判断数组类型的标准方法,虽然Object.prototype.toString()方法可用来判断数组类型,但未免显得有点hack,又因自家typeof类型操作符给予厚望,辱没众望,如果随便更改typeof的返回结果势必会导致天下大乱,instanceof运算符又存在不同frame的局限性难堪大任,ES5不得不亡羊补牢的设计了isArray()方法来“增量”的解决数组判断难题。

四、为何要判断数组类型?

前面说过js属于动态弱类型语言,可能某个变量用着用着就莫名其妙的变了类型(自己不小心更改类型,引入的第三方代码库,因为同名变量改变了类型),如果你设想的是某个变量为数组类型,但因某个逻辑变成了基本类型,这时如果调用数组的方法注定会报错,凡此种种导致的问题,数不胜数,具体的问题实践多了懂得就懂。

五、结语

近几年前端项目愈发复杂庞大,为更好的构建高性能的前端项目,诞生了“react、angular、vue”等数据驱动型解决方案,大量的数据、大量的组件和类对数据类型的判断需求愈发频繁,但因为js动态弱类型语言特性,加之其类型判断的坑爹性,所以各路开发者希望完善和升级js,在ES6标准中,新的const变量定义方案能很好的应对变量动态性问题,微软开发的“typescript”能够实现强类型变量定义,可应对弱类型定义问题。这些方案极大的减少了早期js变量任性定义带来的各种问题,虽然判断数组类型在未来开发中可能会成为历史云烟,但理解其相关的基础和历史演变却是一件很【浪漫】的事情,因为在理解了它的相关坑爹性和进化史有助于我们更好的思考和优化。爱之深,责之切,希望js能在未来变得更加锋利可靠,也希望少为一些坑爹特性而想出一些hack方案(额,比如——>Object.prototype.toString()方法)。

六、参考资料