你的位置:首页 > Java教程

[Java教程][Effective JavaScript 笔记]第5章:数组和字典


前言

这节里其实一直都在讨论对象这个在js中的万能的数据结构。对象可以表式为多种的形式,表示为字典和数组之间的区别。更多的我觉得这章讨论多的是一些对应实现功能的相关操作,有可能出现的bug以及如何避免来修复这些bug。比如下面会说到的for...in枚举属性的操作,可能因为对原型的一些操作,最终造成数据对象的操作的破坏。对于属性顺序有要求的如何处理,对类数组如何处理等。下面再一起一条一条回顾一下,这章里的主要内容。我觉得没必要讲的会一语带过,相比前面的几章,个人觉得这章的内容重点很少,注意几点就好,有几条只是强调一个知识点,有点繁琐。

第43条:使用Object的直接实例构造轻量级的字典

个人总结

这条里主要是强调在使用对象做为字典这种数据结构时,使用Object的直接实例来完成。
语法如下:

var obj=new Object();//或var obj1={};

上面这样做的好处是在列举对象的属性时,按字典的说法就是键值key的时候,可以通过for...in循环来处理。

自定义字典类对象

如果这里用的是自定义的对象在使用for...in循环时,把添加到原型对象中的方法也做为key值返回了,和最初的程序需求不相符。

数组对象字典

数组对象也存在着和自定义类对象相的问题,有许多的ES5的数组方法,在有些环境中无法支持。会使用polyfill来对相应方法进行添加。最后也会污染到for...in循环。
基于上面的两种方式可能出现的问题。使用对象的直接实例可以把可能出现的问题控制在可控的区域。

提示

  • 使用对象字面量构建轻量级字典

  • 轻量级字典应该是Object.prototype的直接子类,以使for...in循环免受原型污染

第44条:使用null原型以防止原型污染

个人总结

基于上一条讲的,for...in枚举属性的问题,可以使用ES5中的Object.create方法,创建一个原型为null的对象。语法如下:

var obj=Object.create(null);

对于不支持Object.create方法的环境,只能使用非标准属性__proto__来兼容。

var o={__proto__:null};o.instanceof Object;//false

可能有人会想下面的这种形式来解决相同的问题。但直接使用C.prototype=null,并不能完成。

function C(){}C.prototype=null;var c=new C();Object.getPrototypeOf(c) === null;//false

使用null为原型来防止原型污染,使用技术如下:

  • 在开发时优先使用ES5的Object.create(null)

  • 在不支持ES5但支持非标准的__proto__属性的,使用obj.proto=null;

  • 其他环境提示无法完成相关的功能

提示

  • 在ES5环境中,使用Object.create(null)创建的自由原型的空对象是不太容易被污染的

  • 在一些较老环境中,考虑使用{__proto__:null}

  • __proto__既不标准,也不可移植,并且可能未来不再支持

  • 不要使用__proto__名作字典的key,因为一些环境将其作为特殊的属性对待

第45条:使用hasOwnProperty方法以避免原型污染

个人总结

第43,44条中讲到for...in枚举属性时,一直受到原型对象污染,无法只获取对象自身的属性。可以使用hasOwnProperty方法来对自身属性进行判断。hasOwnProperty本身是Object.prototype中的方法。所有对象都会继续这个方法。

对象直接调用hasOwnProperty方法

直接使用当前对象调用hasOwnProperty方法,有一个问题就是当前的对象或原型链中没有原型对象对这个方法进行重新的定义,才能保重代码的正确性。

使用函数的call方法对Object.prototype.hasOwnProperty方法进行处理
var hasOwn=Object.prototype.hasOwnProperty;hasOwn.call(obj1);

可以保证hasOwnProperty在判断属性时的正确性。可以为需要的对象,把以上的判断封装在一个方法中方便调用。

对特殊的属性__proto__进行处理

使用记录状态的方法,来对非标准属性,进行代码兼容。防止环境不同,造成代码功能受到破坏。对于非定义的默认为false,详细代码到文章查看。

提示

  • 使用hasOwnProperty方法避免原型污染

  • 使用词法作用域和call方法避免覆盖hasOwnProperty方法

  • 考虑在封装hasOwnProperty测试样板代码的类中实现字典操作

  • 使用字典类避免将“__proto__”作为key来使用

第46条:使用数组而不要使用字典来存储有序集合

个人总结

因为字典的属性的读取是依赖于for...in循环的,而for...in循环在标准库中并没有规定相关属性的顺序,所以无法确切地知道属性输出的顺序。如果对于属性顺序的依赖性强,那么就不能使用字典来存储数据。而用数组存储,可以使用for循环,严格按照索引的顺序对数据进行处理。
本节有意思的是提到了浮点数的计算问题,浮点数的计算结果依赖于计算顺序,所以不能使用字典的形式也就是for...in循环的形式对各项进行加操作。解决办法是通过把浮点数转化为整数,然后把计算结果再转化为浮点数的方法,来解耦顺序依赖。

提示

  • 使用for...in循环来枚举对象的属性应当与顺序无关

  • 如果聚集运算字典中的数据,确保聚集操作与顺序无关

  • 使用数组而不是字典来存储有序集合

第47条:绝不要在Object.prototype中增加可枚举的属性

个人总结

还是和for...in循环有关,因为Object是所有js对象的根对象(Object.create(null)创建 的除外)。任何对象都是继承自Object.prototype对象的。对Object.prototype的操作,会影响到所有对象。如果非要添加方法或属性,可以使用Object.defineProperty方法,把属性或方法设置为不可枚举的。这样可以避免污染原型对象。

提示

  • 避免在Object.prototype中增加属性

  • 考虑编写一个函数代替Object.prototype中的方法

  • 如果确实需要在Object.prototype中增加属性,使用ES5中的Object.defineProperty方法将它们定义为不可枚举的属性。

第48条:避免在枚举期间修改对象

个人总结

还是和for...in循环有关,之前上面的已经讲过,for...in循环无法保证枚举属性的顺序。所以在修改对象属性时,可能枚举的是新的属性值,有可能是枚举的是旧的属性值。标准规定:如果被枚举的对象在枚举期间添加了新的属性,那么在枚举期间并不能保证新添加的属性能访问。
避免使用for...in循环执行这样的操作,使用其它替代的方案。比如自己管理循环控制,使用while循环来对对象进行相关操作。
本条中有相关有向图的遍历和查找相关的代码,到具体文章查看。

提示

  • 当使用for...in循环枚举一个对象的属性时,确保不要修改对象

  • 当迭代一个对象时,如果该对象的内容可能会在循环期间被改变,应用使用while循环或经典for循环来代替for...in循环

  • 为了在不断变化的数据结构中能够预测枚举,考虑使用一个有序的数据结构,例如数组,而不要使用字典对象

第49条:数组迭代要优先使用for循环而不是for...in循环

个人总结

for

for循环可以保证代码的执行过程的顺序,及可以根据索引去取确定的值。不会受其它因素的影响。

小知识点

对数组长度的存储,可以减少每次对数组长度的访问,提高循环的效率。

for(var i=0;i < arr.length;i++){}

其中var i=0;中执行一次,i < arr.length每次代码块执行前执行,i++代码块执行后执行。所以这里arr.length每一次都会去获取,使用下面这种方式更好:

for(var i=0,n=arr.length;i < n;i++){}
for...in

for...in循环可能会被原型链中的对象方法污染,无法保证其代码的正确性。

对比一下:

  • for循环清晰可控结果可预期

  • for...in循环不确定性强,容易被污染,顺序不可控

提示

  • 迭代数组的索引属性应当总是使用for循环而不是for...in循环

  • 考虑在循环之前将数组的长度存储在一个局部变量中以以避免重新计算数组长度

第50条:迭代方法优于循环

个人总结

得益于ES5里提供了功能强大的,众多的数组迭代方法,使得数组的处理更方便。主要包括filter,forEach,every,some,map等方法,具体详细可参阅对应的小节文章。
在使用for循环的时候,往往在边界条件设定错误,导致数组的溢出,或数组元素没被遍历的情况。对于这些问题都要小心去调试和对待,有时代码抽象层次高了之后,问题不容易被发现。
使用迭代方法,不用考虑边界条件的问题。主要考虑得是各个迭代方法的用法及它们之间的区别。利用不同的特性来完成个性化的需求。

通过猴子补丁给数组添加一些的新的迭代方法,比如这节里讲的takeWhile方法。
主要是功能,是提取出满足函数的数组的前几个元素。具体代码到文章里查看。

提示

  • 使用迭代方法替换for循环使得代码更可读,并且避免了重复循环控制逻辑

  • 使用自定义的迭代函数来抽象未被标准库支持的常见循环模式

  • 在需要提前终止的情况下,仍然推荐使用传统的for循环。some和every方法也可以用于提前退出

第51条:在类数组对象上利用通用的数组方法

个人总结

类数组对象中,大家经常使用的jQuery对象就是一个类数组的存在。也可以用这里的方法对它进行操作。如使用

[].slice.call($('a')).forEach(function(a){ a.style.display='none';})

这里只是举个例子,jquery里使用迭代方法的地方很多。
可以看看jquery里是怎样去完成类数组对象的构造的。主要是makeArray方法

//以下代码来自1.7.1版本function makeArray(array,results){  var ret = results || [];  if ( array != null ) {      var type = jQuery.type( array );//这里只是一个类型的判断   if ( array.length == null || type === "string" || type === "function" || type === "regexp" || jQuery.isWindow( array ) ) {    push.call( ret, array );   } else {    jQuery.merge( ret, array );//这里是主要的   }  }  return ret;}//merge方法function merge( first, second ) { var i = first.length,//并不要求第一个是数组对象,有.length属性就成   j = 0; if ( typeof second.length === "number" ) {//主要关注这里  for ( var l = second.length; j < l; j++ ) {   first[ i++ ] = second[ j ];  } } else {  while ( second[j] !== undefined ) {   first[ i++ ] = second[ j++ ];  } } first.length = i; return first;}

可以看出上面的代码jquery本身是一个对象,并没有元素数组。而是通过一些方法获取到选择器对就的页面元素的数组后,利用merge方法把这些数组元素加到jquery对象上,并设置其length属性的长度,从而把jquery对象变成一个类数组了。
通过这个也可以引出类数组的满足的两条契约:

  • 具有一个范围在0到2^32-1的整形length属性

  • length属性大于该对象的最大索引。索引是一个范围在0到2^32-1的整数,它的字符串表示是该对象中的一个key

提示

  • 对于类数组对象,通过提取方法对象并使用其call方法来复用通用的Array方法

  • 任意一个具有索引属性和恰当length属性的对象都可以使用通用的Array方法

第52条:数组字面量优于数组构造函数

个人总结

这个没什么好说的,就是各种对应的数组,对象,函数等有构造函数,又有字面量方式的,使用字面量的方式更好。
防止构造函数命名被覆盖,无法完成正确功能。防止由于参数传递不同的值造成了不一样的结果。就是能用字面量就用字面量

提示

  • 如果数组构造函数的第一个参数是数字则数组的构造函数行为是不同的

  • 使用数组字面量替代数组构造函数

总结

这一章前半的条目,都是绕怎么解决对象字典的属性列举问题。列举属性使用的for...in循环的各种问题,原型污染,顺序不确定,枚举过程中操作的响应问题。后半主要讨论数组的相关操作问题,迭代方法优于循环,字面量优于构造函数,类数组的处理方法。通过这章学习,可以很明确地区分字典对象和数组之间的联系和区别,同时它们也是两种不同的数据结构。字典是无序,散列的键值对;数组是有序,索引的数据集合。