关于javascript里面数组的sort方法

介绍

Array.sort([compareFunction])

对Array里面的元素进行升序排序

关于排序的规则

默认的排序

如果compareFunction没有提供的时候,默认把所有Array里面的元素转换为字符串,然后取第一个字符,比较它们的Unicode值,进行正序排序。

这个转为字符串再比较第一个字符的Unicode值,很关键,很多数字的比较,经常就是这样出错的。


[1,2,3].sort();  // ==> [1,2,3]  正确

[9, 80].sort();  // ==> [9, 80]  错误

[9, 80].sort();  // ==> [80, 9]  正确

上面例子中,数组[9, 80]进行sort的时候,先转为为字符串 “9”, “80”, 再比较第一个字符, “9”和”8″

根据这个比较结果进行排序,所以最后80会在9前面,而不是按照它们的数值大小进行排序的。

想自行测试的,可以看看这个demo(http://www.icondownloader.com/demo/sort-stable.html]

compareFunction的排序

进行sort的时候,如果compareFunction提供了,会往compareFunction传两个数组的元素(类似compareFunction(a, b))

取compareFunction的返回值,如果小于0,则表明a小于b;大于0,则a大于b

最后对结果进行升序排序,如果a小于b,则[a, b]; 如果a大于b 则[b, a]

这里有个技巧,在排序的时候,如果想实现降序排序,可以在compareFuction里面实现类似:return !(a-b);

sort stable

关于排序,有个stable的说法,也就是,排序完成之后,对于值相同的元素,在排序结束之后,能否保留其在排序之前的顺序。

sort stable

具体的概念可以参考维基百科https://en.wikipedia.org/wiki/Sorting_algorithm#Stability

目前的主流浏览器对stable的sort的支持情况如下:

Browser Sort is stable Sort is unstable
Firefox 8 all lengths
Safari 5.1.1 all lengths
Opera 11.52 all lengths
Internet Explorer 6, 7, 8 all lengths
Internet Explorer 9 all lengths
Android 2.3 Browser all lengths
Chrome 15 .. 17.0.942.0 length <= 10 length > 10

可以参考此网站,检测浏览器的sort情况http://ofb.net/~sethml/is-sort-stable.html

sort的效率

可以尝试着对compare function在执行的时候,进行一个计数。这样可以知道每次sort,总共执行了多少次compare function。

我这边写了个demo,有兴趣的可以在自己本机的各个浏览器上测试看看。http://www.icondownloader.com/demo/sort-compare-function-call-times.html

这边我贴出我自己Mac OSX Yosemite 10.10.4下的浏览器的运行结果

## chrome 版本 42.0.2311.135 (64-bit)
Array length is: 5 and sort call compare 7 times;
Array length is: 10 and sort call compare 23 times;
Array length is: 11 and sort call compare 22 times;
Array length is: 12 and sort call compare 19 times;
Array length is: 13 and sort call compare 27 times;
Array length is: 14 and sort call compare 25 times;
Array length is: 15 and sort call compare 24 times;
Array length is: 20 and sort call compare 29 times;
Array length is: 50 and sort call compare 80 times;
Array length is: 100 and sort call compare 206 times;
Array length is: 1000 and sort call compare 1996 times;
Array length is: 10000 and sort call compare 20206 times;

## Safari 版本 8.0.7 (10600.7.7)
Array length is: 5 and sort call compare 8 times;
Array length is: 10 and sort call compare 23 times;
Array length is: 11 and sort call compare 28 times;
Array length is: 12 and sort call compare 31 times;
Array length is: 13 and sort call compare 35 times;
Array length is: 14 and sort call compare 40 times;
Array length is: 15 and sort call compare 44 times;
Array length is: 20 and sort call compare 65 times;
Array length is: 50 and sort call compare 232 times;
Array length is: 100 and sort call compare 568 times;
Array length is: 1000 and sort call compare 8996 times;
Array length is: 10000 and sort call compare 123583 times;

## Firefox (Developer Edition) 40.0a2 (2015-05-29)
Array length is: 5 and sort call compare 6 times;
Array length is: 10 and sort call compare 26 times;
Array length is: 11 and sort call compare 24 times;
Array length is: 12 and sort call compare 33 times;
Array length is: 13 and sort call compare 48 times;
Array length is: 14 and sort call compare 48 times;
Array length is: 15 and sort call compare 37 times;
Array length is: 20 and sort call compare 70 times;
Array length is: 50 and sort call compare 245 times;
Array length is: 100 and sort call compare 574 times;
Array length is: 1000 and sort call compare 8600 times;
Array length is: 10000 and sort call compare 114046 times;

从上面的数据可以看出两个结论:

  1. chrome的执行效率明显高于Safari和Firefox。但是chrome的sort是unstable的,而Safari和Firefox是stable的。是不是可以认为,因为chrome不需要考虑stable,所以提高了执行效率。

  2. 可以看出随着数组长度的增加,比较的次数是指数增加的

用map来提高提高sort的性能

从上面的结论2可以知道,在数组长度很长的时候,compareFunction的调用次数是很多的,这个时候,提高compareFunction的效率就很有必要性了。

现在咱们构建一个数组

var arr = [],
    arrLen = 1000,
    i = 0;

function makeWord(){
    var word = [],
        words = ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z'],
        wordsLength = words.length,
        arrMinLen = 3,
        arrLen = arrMinLen + Math.floor( Math.random()*9 ),

        i = 0;

    for (i = 0; i < arrLen; i++){
        word.push( words[Math.floor( Math.random()*wordsLength )] );
    }

    return word.join('');
}

for (i = 0; i < arrLen; i++) {
    arr.push( makeWord() );
}

然后比较一下它们小写字母状态下的排序

// 未优化
arr.sort(function(a, b){
    return +(a.toLowerCase() > b.toLowerCase()) || +(a.toLowerCase() === b.toLowerCase())-1;
});

// 优化后

// 用一个临时数组来保存位置和计算后的数值
var mapped = list.map(function(el, i) {
  return { index: i, value: el.toLowerCase() };
})

// 排序这个已经计算后的临时数组
mapped.sort(function(a, b) {
  return +(a.value > b.value) || +(a.value === b.value) - 1;
});

// 根据位置信息 对应映射生成一个排序后的数组
var result = mapped.map(function(el){
  return list[el.index];
});
// 不支持map的时候的兼容方法

var tmpArr = [],
    result = [],
    i = 0,
    len = arr.length;

for (i = 0; i < len; i++) {
    tmpArr[i] = {index: i, value: arr[i].toLowerCase()};
}

tmpArr.sort(function(a, b) {
  return +(a.value > b.value) || +(a.value === b.value) - 1;
});

for (i = 0; i < len; i++) {
    result[i] = arr[tmpArr[i].index];
}

具体的执行效果可以看看jsperf里面的这个地址http://jsperf.com/arraysortperform

参考文章:
+ https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
+ https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Grammar_and_types#Unicode
+ https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#String_literals
+ http://ofb.net/~sethml/is-sort-stable.html
+ http://stackoverflow.com/questions/3026281/array-sort-sorting-stability-in-different-browsers

编写可维护的JavaScript-语句和表达式

关于for-in循环的一些使用上应该注意的细节

matainable javascript : For-In 循环

本系列的文章都是在阅读《编写可维护的JavaScript》——Nicbolas C. Zakas 的基础上做的一些个人总结

for-in 循环

### 遍历对象

对对象使用for in循环的时候,一般情况下,都应该使用hasOwnProperty()方法来进行过滤:



var prop,
    obj = {
        a: "aaa",
        b: "bbb"
    };

for (prop in obj) {
    if (obj.hasOwnProperty(prop) {
        console.log("Property name is " + prop);
        console.log("Property value is " + obj[prop]);
    }
}


如果的确需要查询原型链,这个时候应当补充注释

var prop,
    obj = {
        a: "aaa",
        b: "bbb"
    };

for (prop in obj) { // 包含对原型链的遍历
    console.log("Property name is " + prop);
    console.log("Property value is " + obj[prop]);
}

### 不要使用for in 来遍历数组


// 不好的用法
var values = [ 1, 2, 3, 4, 5, 6, 7],
    i;
    
for (i in values) {
    console.log(values[i]);
}


不使用for in来遍历数组,是因为这样会造成一些潜在的错误,同时会有性能上面的影响。

潜在的错误:


var arr = [ 'a', 'b', 'c'],
    i;

arr.test = 'ddd';

for ( i in arr ){
    console.log(i,arr[i]);
}

/** 输出内容
 * 0 a 
 * 1 b 
 * 2 c 
 * test tset 
 * /


可见for in循环会把用户附加在数组上的一些自定义属性输出,这很可能会导致一些潜在的错误,因为这段代码的作者的目的是需要的数组成员的数据

性能问题(摘自JavaScript秘密花园):

由于 for in 循环会枚举原型链上的所有属性,唯一过滤这些属性的方式是使用 hasOwnProperty 函数, 因此会比普通的 for 循环慢上好多倍。

做个测试:


var arr = [],
    i,
    a,
    max = 100000,
    timeStart,
    timeEnd;

// 生成一个长度100000的数组
for (i = 0; i < max; i++) {
    arr[i] = i;
}

/**
 * for 循环
 */
timeStart = new Date().valueOf();
for (i = 0; i < max; i++) {
    // 无意义的一些操作
    a = arr[i];
}
timeEnd = new Date().valueOf();

console.log(timeEnd - timeStart);

/**
 * for in 循环
 */
timeStart = new Date().valueOf();
for (i in arr) {
    // 无意义的一些操作
    a = arr[i];
}
timeEnd = new Date().valueOf();

console.log(timeEnd - timeStart);



在数组长度比较小的时候,两者差距不大
甚至有的时候for in还会快一点
但是随着数组长度越来越大,两者的差距也就越来越明显

下面是一串本机的测试数据(Mac Chrome 37.0.2062.122)

数组长度 for for-in
10 0 0
1000 0 1
10000 1 6
100000 2 46
1000000 24 331
10000000 207 4362

虽然在实际项目中,数组的长度不会那么长,但是能提高一点效率总是好的

被flash执行的js方法,书写的时候,需要注意的

特别是在IE6下

字符串的拼接

不要用var str = “aaa”+”bbb” 会报错

arr.push arr.join 测试也是会报错

推荐 var str = ”;str += ‘aaa’; str += ‘bbb’;

不要注释

注释会影响解析,可能导致 缺少’}’ 之类的bug

关于返回值

不要类似直接return ‘a’ 这样直接返回一个临时创建的字符而应该是 var str = ‘not’; return not;

这些说法没有严谨的进行验证,只是在最近和flash进行交互的时候,在ie下遇到错误,总是查找不到,最后一点点的回滚代码测试错误的时候,发现的。也没法描述其中的原理是什么。仅供参考

2014年6月5日补充

以上情况只适用于这种情况:使用swfobject.js来加载flash,会以flashvar的形式给flash传递一个js的回调函数。出问题的原因是:swfobject.js会把这个js的回调函数的代码复制然后写入标签的flashvar属性中。这种复制会导致下面的问题。

关于阻止表单提交

详细讲解了在不同的注册事件方法下,如何去阻止表单的提交,甚至扩散开来说是阻止浏览器的默认行为/动作

起因

今天在公司忙活着一个项目,涉及到表单提交,要求在表单提交之前判断几个input的值是否为空,如果是,则阻止表单提交。

我的做法是给form添加了一个submit事件绑定,然后判断,如果有input的值为空则return false 阻止提交。判断input值是否为空,这一块走的正常,可是在判断有空值的时候,表单还是提交了,很是奇怪。

上网看了看大家的一些做法,也都是return false.为啥到我这边就不管用了?后来仔细看了下别人代码和自己的区别,这才找到了原因。

表单提交前判断的常用做法

一般来说,js要实现提交前的判断,有以下三种常见做法:

  1. 监听form标签的submit事件
  2. 监听input[type=submit]按钮的click事件
  3. 监听input[type=button]或者任何指定作为提交按钮的标签的任意事件,在条件符合之后调用form标签的submit()方法

一下是一个简单的例子:

<!DOCTYPE HTML>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>demo-form</title>
</head>
<body>
<form action="answer.php" id="form1" method="post">
    <input type="text" value="" id="txt">
    <input type="submit" value="提交" id="sbtn">
    <input type="button" value="按钮" id="btn">
</form>
<script type="text/javascript">
    var d = document,
        form1 = d.getElementById('form1'),
        txt = d.getElementById('txt'),
        sbtn = d.getElementById('sbtn'),
        btn = d.getElementById('btn');

        // 检测文本框内容是否为空
        function checkIsNull(){
            if(txt.value === ""){
                return false;
            }else{
                return true;
            }
        }
</script>
</body>
</html>

 

对于第三种方法,实现阻止表单提交最简单,只要条件不符合就不调用form标签的submit()方法即可

        btn.addEventListener('click',function(e){
            if(checkIsNull()){
                form1.submit();
            }else{
                alert('文本框内容不能为空');
            }
        });

 

对于第二种方法和第一种方法,就有比较多的可能性了

直接绑定到标签属性上

如果是使用直接绑定到标签属性上的方法,则第一和第二种方法都可以简单的使用return false;的方法去阻止表单提交。

        form1.onsubmit = function(e){
            if(checkIsNull()){
                return true;
            }else{
                alert('文本框内容不能为空');
                return false;
            }
        };

 

        sbtn.onclick = function(e){
            if(checkIsNull()){
                return true;
            }else{
                alert('文本框内容不能为空');
                return false;
            }
        };

 

这种直接注册到标签属性上的方法,方法内部的return false;就会直接作用到整个事件上,这一点我是在下面这个文章里面看到的,有兴趣的同学可以看看,传送门:由表单中onsubmit=”return false;”想到的

可是这种事件注册有个缺点就是没有办法给同一个标签的同一个事件注册多个回调函数函数,这个在某些情况下还是不行的。然后就是通过addEventListener(IE下面是attachEvent)

通过addEventListener/attachEvent注册事件

通过addEventListener方法注册的事件的回调函数,假设是:

form.addeventListener('submit',function doSubmit(e){
    return false;
},false);

 

其实就类似于:

form.onsubmit = function(e){
    doSubmit();
}

 

所以doSubmit()内部的return false;是没办法影响到整个事件的浏览器默认动作

这个时候,在非IE浏览器下,回调函数函数可以通过传入的event,调用该event对象的preventDefault()方法阻止浏览器的默认动作;如果是在IE浏览器下,则是通过window.event.returnValue = false;方法来阻止浏览器的默认动作。(这段内容参考的网站,传送门:js阻止浏览器的默认行为以及停止事件冒泡(用JQuery实现回车提交,兼容IE、FF浏览器)

所以可行的代码如下:

        function doSubmit(e){
            if(checkIsNull()){
                return true;
            }else{
                alert('文本框内容不能为空');
                //如果提供了事件对象,则这是一个非IE浏览器 
                if(e && e.preventDefault){
                    //阻止默认浏览器动作(W3C)
                    e.preventDefault();
                }else{
                    //IE中阻止函数器默认动作的方式 
                    window.event.returnValue = false;
                }
            }
        }

        // 判断是否支持IE的注册事件方法,如果支持则是IE浏览器,使用attachEvent进行事件注册
        if(sbtn.attachEvent){
            // attachEvent不支持捕捉,所以只有两个参数
            sbtn.attachEvent('click',doSubmit)
        }else{
            sbtn.addEventListener('click',duSubmit,false);
        }

 

form标签的submit事件也是类似的,就不重复写了

总结

return false;是能够阻止表单提交的,不过只能针对直接注册事件到标签属性上面的情况。对于addEventListener/attachEvent注册的事件,则需要通过event.preventDefaule()或window.event.returnValue=false的方式阻止表单的提交。针对其他浏览器默认行为,也大体上类似

JavaScript中文字符进行大小的比较

JavaScript究竟是通过什么方式比较中文或者说非英文以及数字的字符的大小的,是通过转义成Unicode编码然后比较它们的吗?

最近在看《JavaScript权威指南(第六版)》,里面有个例子,是说的

"a" < "b"     // => true

然后我就想了,那中文字符可以进行这样的比较吗,然后就在Chrome的控制台里面测试了一下

"大" < "小"     // => true
"小" < "大"     // => false

那它们是如何比较的?

书里面有这样一句话:

Unicode允许使用多种方法对同一个字符进行编码。比如,字符“é”可以使用Unicode字符u00E9表示,也可以使用普通的ASCII字符e跟随一个语调符u0301。在文本编辑器中,这两种编码的显示结果一模一样,但它们的二进制编码表示是不一样的,在计算机里也不相等。Unicode标准为所有字符定义了一个首选的编码格式,并给出了一个标准化的处理方式将文本转换为一种适合比较的标准格式,JavaScript会认为它正在解析的程序代码已经是这种标准格式,不会再对其标识符、字符串或正则表达式作标准化处理。

对这句话不是很理解,看的很晕。是说的在JavaScript解析之前,已经有程序把它的所有代码转变成了另一种格式的编码吗,而且还不是二进制的

后来偶然看到有encodeURICompent()这样个函数,一般是用来处理URI里面的数据,把一些特殊字符和中文等等进行十六进制编码,以便在不同平台上面数据都能很好使用

然后我就试了下

encodeURIComponent("大");        // =>%E5%A4%A7
encodeURIComponent("小");        // =>%E5%B0%8F

很明显,转化成十六进制之后,”小”的值是大于”大”的

我想,是不是在JavaScript解析之前,浏览器就根据Unicode标准用类似的方式把代码进行了编码,所以等到JavaScript解析的时候,比较”大”>”小”,实际上是类似于在比较”%E5%A4%A7″>”%E5%B0%8F”

不知道有人能跟我讲解一下这方面的知识不,感激不尽

正则表达式对中文的判断

如何用正则表达式去判断是否中文字符,以及大致的原理

今天工作上面需要对一个表单进行验证,需要判断中文。在网上看到了很多的方式

1. GBK (GB2312/GB18030)
x00-xff  GBK双字节编码范围
x20-x7f  ASCII
xa1-xff  中文
x80-xff  中文

2. UTF-8 (Unicode)
u4e00-u9fa5 (中文)
x3130-x318F (韩文
xAC00-xD7A3 (韩文)
u0800-u4e00 (日文)
ps: 韩文是大于[u9fa5]的字符

 

x是十六进制,u是Unicode编码,这种理解对吗

两者之间有什么区别和联系呢

 

2012年8月15日补充

JavaScript的字符串是使用Unicode编码的,每个字符占用两个字节。编码的方式是Unicode 16,具体是Little-Endian还是Big-Endian则要看操作系统了。所以在针对JavaScript的字符串使用正则时,针对中文使用u4e00-u9fa5

参考文章:http://demon.tw/programming/javascript-unicode-utf-8.html

关于Unicode 16的Little-Endian和Big-Endian,可以参看维基百科:https://zh.wikipedia.org/wiki/%E5%AD%97%E8%8A%82%E5%BA%8F

getElementsByTagName不是document的专利

一说到getElementsByTagName,我们总是第一时间想到document.getElementsByTagName,可事实上拥有这个方法的对象远不止document这一个

一直以来,但凡涉及到getElementById,getElementsByTagName,总是会习惯性的在前面加上document对象。就像昨天写的一个博客里面,有个关于清空指定id的后代节点中input type=”text”的内容的函数(传送门)。当我通过document.getElementById()获取指定的ID的对象之后,就开始很傻的一步步的遍历每个节点,看它是不是input type=“text”的;还看改节点有没有子节点,有的话,有几个,然后再考虑是否遍历查找。

代码如下:

 /**
  * [clearUserInput 根据元素的ID找到其子节点(包括子节点的子节点)
中的input元素并且type="text",将其value设为空]
  * @param  {[type]} id [DOM对象或者字符串表示元素的id值]
  * @return {[type]}    [description]
  */
 function clearUserInput(id){
     //参数校验
     if(id === undefined){consoloe.log("参数必须");return 1;}//判断参数是否存在
     id = ((typeof id) === "object")?id:document.getElementById(id);//获取对象
     if(id === null){consoloe.log("未知对象"); return 2;}//typeof null === "object"

     var childrens = id.childNodes,len=childrens.length; i=0;
     for(i;i<len;i++){
         if(childrens[i] === undefined){return;}//到达边界
         if(childrens[i].nodeType === 3){continue;}
         if(childrens[i].childNodes.length !== 0){
             arguments.callee(childrens[i]);//循环遍历        
         }
         if(childrens[i].nodeName === "INPUT" && childrens[i].type === "text"){
             childrens[i].value = "";//内容设为空
         }
     }
 }

 

最后,功能上面实现了,可是每次点击清除的时候,整个浏览器和卡机了差不多…..遍历的计算次数太多了

后来我偶然一下突然想了,既然document对象有getElementsByTagName()之类的方法,那其他的节点和document对象理论上应该是没差的,那它们是不是也应该有这些方法?

我就测试了下,果然一个div对象是有getElementsByTagName()的方法,可是没有getElementById()的方法

元素类型 节点类型
元素 1
属性 2
文本 3
注释 8
文档 9

document.nodeType == 9

一般的HTML标签,如div,p等等,他们的DOM对象的nodeType == 1

就我所知的,nodeType == 1的DOM对象仅有getElementsByTagName()的方法

之后我根据这个修改了一下代码

 /**
  * [clearUserInput 根据元素的ID找到其子节点(包括子节点的子节点)中的input元素并且type="text",将其value设为空]
  * @param  {[type]} id [DOM对象或者字符串表示元素的id值]
  * @return {[type]}    [description]
  */
 function clearUserInput(id){
     //参数校验

     //判断参数是否存在
     if(id === undefined){return 1;}
     //获取对象
     id = ((typeof id) === "object")?id:document.getElementById(id);
     //typeof null === "object"
     if(id === null){return 2;}
     //找到后代节点中的所有input标签
     var inputs = id.getElementsByTagName("input"),
     len=inputs.length, i=0;
     for(i;i<len;i++){
         //到达边界
         if(inputs[i] === undefined){return 3;}
         if(inputs[i].nodeName === "INPUT" && inputs[i].type === "text"){
             inputs[i].value = "";//内容设为空
         }
     }
 }

效率果然显著提升