JavaScript框架设计
上QQ阅读APP看书,第一时间看更新

第3章 语言模块

1995年,Brendan Eich读完了所有在程序语言设计中曾经出现过的错误,自己又发明了一些更多的错误,然后用它们创造出了LiveScript。之后,为了紧跟Java语言的时髦潮流,它被重新命名为JavaScript。再然后,为了追随一种皮肤病的时髦名字,这语言又被命名为ECMAScript。

上面一段话出自博文《编程语言伪简史》,JavaScript受到最辛辣的嘲讽,可见,它在当时是多少不受欢迎的。抛开偏见,JavaScript 的确有许多不足之外,由于互联网的传播性及浏览器大战, JavaScript之父失去对此门语言的掌控权,即便他想修复这些Bug或推出某些新特,也要所有浏览器大厂都点头才行。IE6的市场独占性,打破了他的奢望。这个待到Chrome诞生,才有所改善。

但在IE6时期,浏览器提供的原生API的数量是极其贫乏的,因此各个框架都创造了许多方法以弥补这缺陷。视框架作者原来的语言背景不同,这些方法也是林林总总的。其中最杰出的代表是王者 Prototype.js,把 ruby 语言的那一套方式或范式搬过来,从底层促进了 JavaScript 的发展。ecma262v6添加那一堆字符串,数组方法,差不多就是改个名字而已。

即便是浏览器的API也不能尽信,尤其是IE6、IE7、IE8?到处是BUG,因此这也列入框架的工作范围。

本章主要是围绕着mass Framework的lang与lang_fix模块展开,可以到这里下载。

https://github.com/RubyLouvre/mass-Framework/blob/1.4/lang.js

https://github.com/RubyLouvre/mass-Framework/blob/1.4/lang_fix.js

3.1 字符串的扩展与修复

我发现脚本语言都对字符串特别关注,有关它的方法特别多。我把这些方法分为三大类。

第一类,与标签无关的实现: charAt、charCodeAt、concat、indexOf、lastIndexOf、localeCompare、match、replace,search、slice、split、substr、substring、toLocaleLowerCase、toLocaleUpperCase、toLowerCase、toUpperCase及从Object继承回来的方法,如toString、valueOf。

第二类,与标签有关的实现,都是对原字符串添加一对标签:anchor、big、blink、bold、fixed、fontcolor、italics、link、small、strike、sub、sup。

第三类是后来添加或未标准化的浏览器方法:trim、quote、toSource、trimLeft、trimRight。其中trim已经标准化,后四个是Firefox的私有实现。

我们再看ecma262v6(2012.6.15)打算要添加的方法:repeat、startsWith、endsWidth、contains。

再看伟大的Prototype.js添加的扩展:gsub、sub、scan、truncate、strip、stripTags、stripScripts、extractScripts、evalScripts、escapeHTML、unescapeHTML、parseQuery、toArray、succ、times、camelize、capitalize、underscore、dasherize、inspect、unfilterJSON、isJSON、evalJSON、include、startsWith、endsWith、empty、blank、interpolate。

其中gsub、sub、scan与正则相关,直接取自ruby的命名。

truncate是字符串截取,非常有用的方法,许多框架都有它的“微创新”。

strip即trim,已标准化。

stripTags去掉字符串中的标签对,非常有用。

stripScripts作为stripTags的补充,因为单单把script标签去掉,里面不该显示出来的script.text就暴露出来了。

extractScripts与evalScripts是抽取与执行字符串中的脚本,IE的innerHTML在某种情况下可以这样做,但其他浏览器不行,框架有责任屏蔽此差异性。

escapeHTML与unescapeHTML是对用户的输入输出操作进行转义,非常有用。

parseQuery基本上用于对URL的search部分进行操作,转换成对象,非常有用。

toArray原本也是ecma262v6打算要添加的方法,用于转换成数组,不过这个用户也易实现,因此被抛弃了。

succ是用于ObjectRange内部使用的。

times即ecma262v6的repeat方法。

Camelize、capitalize、underscore、dasherize这四个用于转换命名风格,非常有用。

inspect就是在两端加双引号,用于构建JSON,相当于Firefox的私有实现quote。

UnfilterJSON、isJSON、evalJSON与JSON相关。

include就是contains,与startsWith、endsWith成为ecma262v6的标准方法。

empty、blank是对空白进行判定,很简单的方法。

interpolate用于模板,其他框架通常称之为format或substitute。

Prototype.js这些有用的扩展会被其他框架抄去,我们查看哪些经常被抄,就知道哪些方法最有价值了。

Rirght.js 的字符串扩展:include、blank、camelize、capitalize、dasherize、empty、endsWith、evalScripts、extractScripts、includes、on、startsWith、stripScripts、stripTags、toFloat、toInt、trim、underscored。

Mootools的字符串扩展(只取原型扩展):test、contains、trim、clean、camelCase、hyphenate、capitalize、escapeRegExp、toInt、toFloat、hexToRgb、rgbToHex、substitute、stripScripts。

dojo的字符串扩展:rep、pad、substitute、trim。rep就是repeat方法.

EXT的字符串扩展:capitalize、ellipsis、escape、escapeRegex、format、htmlDecode、htmlEncode、leftPad、parseQueryString、trim、urlAppend。

qooxdoo的字符串扩展:format、hyphenate、pad、repeat、startsWidth、stripScripts、stripTags、toArray、trim、trimLeft、trimRight。

Tangram 的字符串扩展:decodeHTML、encodeHTML、escapeReg、filterFormat、format、formatColor、stripTags、toCamelCase、toHalfWidth、trim、wbr。

通过以上竞争对手分析,我在mass Framework为字符串添加如下扩展,各位写框架的朋友可以视自己的情况进行增减:contains、startsWith、endsWith、repeat、camelize、underscored、capitalize、stripTags、stripScripts、escapeHTML、unescapeHTML、escapeRegExp、truncate,wbr、pad。其中前四个 ecma262v6 的标准方法,接着九个发端于 Prototype.js 广受欢迎的工具方法,wbr 是来自Tangram,用于软换行,这出于汉语排版的需要。pad也是一个很常用的操作,被收纳。

下面是各种具体实现。

contains方法:判定一个字符串是否包含另一个字符串。常规思维,使用正则,但每次都要用new RegExp来构造,性能太差,转而使用原生字符串方法,如,ndexOf、lastIndexOf、 search。

      function contains(target, it) {
              return target.indexOf(it) != -1; //indexOf改成search,lastIndexOf也行得通
      }

在mootools的版本中,我看到它支持更多参数,估计目的是判定一个元素的className是否包含某个特定的class。众所周知,元素可以添加多个class,中间以空格隔开,使用mootools的contains就很方便检测包含关系了。

      function contains(target, str, separator) {
          return separator ?
                  (separator + target + separator).indexOf(separator + str + separator) > -1 :
                  target.indexOf(str) > -1;
      }

注,本章的所有工具函数都是以静态方法,将它们变成原型方法,我在结束这章时给一个函数变换方法。

startsWith方法:判定目标字符串是否位于原字符串的开始之处,可以说是contains方法的变种。

      //最后一参数是忽略大小写
      function startsWith(target, str, ignorecase) {
          var start_str = target.substr(0, str.length);
          return ignorecase ? start_str.toLowerCase() === str.toLowerCase() :
                  start_str === str;
      }

endsWith方法:与startsWith相反。

      //最后一参数是忽略大小写
      function endsWith(target, str, ignorecase) {
          var end_str = target.substring(target.length - str.length);
          return ignorecase ? end_str.toLowerCase() === str.toLowerCase() :
                  end_str === str;
      }

repeat方法:将一个字符串重复自身N次,如repeat("ruby", 2)得到rubyruby。

版本1:利用空数组的join方法。

      function repeat(target, n) {
          return (new Array(n + 1)).join(target);
      }

版本2:版本1的改良版,创建一个对象,拥有length属性,然后利用call方法去调用数组原型的join方法,省去创建数组这一步,性能大为提高。重复次数越多,两者对比越明显。另,之所以要创建一个带length属性的对象,是因为要调用数组的原型方法,需要指定call的第一个参数为类数组对象。而类数组对象的必要条件是其length属性的值为非负整数。

      function repeat(target, n) {
          return Array.prototype.join.call({
              length: n + 1
          }, target);
      }

版本3:版本2的改良版,利用闭包将类数组对象与数组原型的jion方法缓存起来,省得每次都重复创建与寻找方法。

      var repeat = (function() {
          var join = Array.prototype.join, obj = {};
          return function(target, n) {
              obj.length = n + 1;
              return join.call(obj, target);
          }
      })();

版本 4:从算法上着手,使用二分法,比如我们将 ruby 重复 5 次,其实我们在第二次已得rubyruby,那么3次直接用rubyruby进行操作,而不是用ruby。

      function repeat(target, n) {
          var s = target, total = [];
          while (n > 0) {
              if (n % 2 == 1)
                  total[total.length] = s;//如果是奇数
              if (n == 1)
                  break;
              s += s;
              n = n >> 1;//相当于将n除以2取其商,或说开2二次方
          }
          return total.join('');
      }

版本5:版本4的变种,免去创建数组与使用jion方法。它的悲剧之处在于它在循环中创建的字符串比要求的还长,需要回减一下。

      function repeat(target, n) {
          var s = target, c = s.length * n
          do {
              s += s;
          } while (n = n >> 1);
          s = s.substring(0, c);
          return s;
      }

版本6:版本4的改良版。

      function repeat(target, n) {
          var s = target, total = "";
          while (n > 0) {
              if (n % 2 == 1)
                  total += s;
              if (n == 1)
                  break;
              s += s;
              n = n >> 1;
          }
          return total;
      }

版本7:与版本6相近,不过递归在浏览器下好像都做了优化(包括IE6),与其他版本相比,属于上乘方案之一。

      function repeat(target, n) {
          if (n == 1) {
              return target;
          }
          var s = repeat(target, Math.floor(n / 2));
          s += s;
          if (n % 2) {
              s += target;
          }
          return s;
      }

版本8:可以说是一个反例,很慢,不过实际上它还是可行的,因此实际上没有人将n设成上百成千。

      function repeat(target, n) {
          return (n <= 0) ? "" : target.concat(repeat(target, --n));
      }

经测试,版本6在各浏览器的得分是最高的。

byteLen方法:取得一个字符串所有字节的长度。这是一个后端过来的方法,如果将一个英文字符插入数据库 char、varchar、text 类型的字段时占用一个字节,而一个中文字符插入时占用两个字节,为了避免插入溢出,就需要事先判断字符串的字节长度。在前端,如果我们要用户填空的文本,需要字节上的长短限制,比如发短信,也要用到此方法。随着浏览器普及对二进制的操作,这方法也越来越常用。

版本 1:假设字符串每个字符的 Unicode 编码均小于等于 255,byteLength 为字符串长度;再遍历字符串,遇到 Unicode 编码大于 255 时,为 byteLength 补加1。

      function byteLen(target) {
          var byteLength = target.length, i = 0;
          for (; i < target.length; i++) {
              if (target.charCodeAt(i) > 255) {
                  byteLength++;
              }
          }
          return byteLength;
      }

版本2:使用正则,并支持制定汉字的存储字节数。比如mysql存储汉字时,是用3个字节数的。

      function byteLen(target, fix) {
          fix = fix ? fix : 2;
          var str = new Array(fix + 1).join("-")
          return target.replace(/[^\x00-\xff]/g, str).length;
      }

truncate 方法:用于对字符串进行截断处理,当超过限定长度,默认添加三个点号或其他什么的。

      function truncate(target, length, truncation) {
          length = length || 30;
          truncation = truncation === void(0) ? '...' : truncation;
          return target.length > length ?
                  target.slice(0, length - truncation.length) + truncation : String(target);
      }

camelize方法:转换为驼峰风格。

      function camelize(target) {
          if (target.indexOf('-') < 0 && target.indexOf('_') < 0) {
              return target;//提前判断,提高getStyle等的效率
          }
          return target.replace(/[-_][^-_]/g, function(match) {
              return match.charAt(1).toUpperCase();
          });
      }

underscored方法:转换为下划线风格。

      function underscored(target) {
          return target.replace(/([a-z\d])([A-Z])/g, '$1_$2').
                  replace(/\-/g, '_').toLowerCase();
      }

dasherize方法:转换为连字符风格,亦即CSS变量的风格。

      function dasherize(target) {
          return underscored(target).replace(/_/g, '-');
      }

capitalize方法:首字母大写。

      function capitalize(target) {
          return target.charAt(0).toUpperCase() + target.substring(1).toLowerCase();
      }

stripTags方法:移除字符串中的html标签,但这方法有缺陷,如里面有script标签,会把这些不该显示出来的脚本也显示出来。在Prototype.js中,它与strip、stripScripts是一组方法。

      function stripTags(target) {
          return String(target || "").replace(/<[^>]+>/g, '');
      }

stripScripts 方法:移除字符串中所有的 script 标签。弥补 stripTags 方法的缺陷。此方法应在stripTags之前调用。

      function stripScripts(target) {
          return String(target || "").replace(/<script[^>]*>([\S\s]*?)<\/script>/img, '')
      }

escapeHTML 方法:将字符串经过 html 转义得到适合在页面中显示的内容,如将<替换为 &lt;。

      function escapeHTML(target) {
          return target.replace(/&/g, '&amp;')
                  .replace(/</g, '&lt;')
                  .replace(/>/g, '&gt;')
                  .replace(/"/g, "&quot;")
                  .replace(/'/g, "&#39;");
      }

unescapeHTML:将字符串中的 html 实体字符还原为对应字符。

      function unescapeHTML(target) {
          return  target.replace(/&quot;/g, '"')
                  .replace(/&lt;/g, '<')
                  .replace(/&gt;/g, '>')
                  .replace(/&amp;/g, "&") //处理转义的中文和实体字符
                  .replace(/&#([\d]+);/g, function($0, $1) {
              return String.fromCharCode(parseInt($1, 10));
          });
      }

escapeRegExp方法:将字符串安全格式化为正则表达式的源码。

      function escapeRegExp(target) {
          return target.replace(/([-.*+?^${}()|[\]\/\\])/g, '\\$1');
      }

pad方法:与trim相反,pad可以为字符串的某一端添加字符串。常见的用法如日历在月份前补零,因此也被称之为 fillZero。我在博客上收集许多版本的实现,在这里转换静态方法一并放出。

版本1:数组法,创建数组来放置填充物,然后再在右边起截取。

      function pad(target, n) {
          var zero = new Array(n).join('0');
          var str = zero + target;
          var result = str.substr(-n);
          return result;
      }

版本2:版本1的变种。

      function pad(target, n) {
          return Array((n + 1) - target.toString().split('').length).join('0') + target;
      }

版本3:二进制法。前半部分是创建一个含有n个零的大数,如(1<<5).toString(2),生成100000,(1<<8).toString(2)生成100000000,然后再截短。

      function pad(target, n) {
          return (Math.pow(10, n) + "" + target).slice(-n);
      }

版本4:Math.pow法,思路同版本3。

      function pad(target, n) {
          return ((1 << n).toString(2) + target).slice(-n);
      }

版本5:toFixed法,思路与版本3差不多,创建一个拥有n个零的小数,然后再截短。

      function pad(target, n) {
          return (0..toFixed(n) + target).slice(-n);
      }

版本6:创建一个超大数,在常规情况下是截不完的。

      function pad(target, n) {
          return (1e20 + "" + target).slice(-n);
      }

版本7:质朴长存法,就是先求得长度,然后一个个地往左边补零,加到长度为n为止。

      function pad(target, n) {
          var len = target.toString().length;
          while (len < n) {
              target = "0" + target;
              len++;
          }
          return target;
      }

版本8:也就是现在mass Framework使用的版本,支持更多参数,允许从左或从右填充,以及使用什么内容进行填充。

      function pad(target, n, filling, right, radix) {
          var num = target.toString(radix || 10);
          filling = filling || "0";
          while (num.length < n) {
              if (!right) {
                  num = filling + num;
              } else {
                  num += filling;
              }
          }
          return num;
      }

wbr方法:为目标字符串添加wbr软换行。不过需要注意的是,它并不是在每个字符之后都插入<wbr>字样,而是相当于在组成文本节点的部分中的每个字符后插入<wbr>字样。如aa<span>bb</span>cc,返回 a<wbr>a<wbr><span>b<wbr>b<wbr></span>c<wbr>c<wbr>。另外,在Opera下,浏览器默认css不会为wbr加上样式,导致没有换行效果,可以在css中加上wbr:after{ content: "\00200B" } 解决此问题。

      function wbr(target) {
          return String(target)
                  .replace(/(?:<[^>]+>)|(?:&#?[0-9a-z]{2,6};)|(.{1})/gi, '$&<wbr>')
                  .replace(/><wbr>/g, '>');
      }

format方法:在C语言中,有一个叫printf的方法,我们可以在后面添加不同的类型的参数嵌入到将要输出的字符串中。这是非常有用的方法,因为在JavaScript涉及大量这样的字符串拼接工作。如果涉及逻辑,我们可以用模板,如果轻量点,我们可以用这个方法。它在不同框架名字是不同的,Prototype.js叫interpolate, Base2叫format,mootools叫substitute。

      function format(str, object) {
          var array = Array.prototype.slice.call(arguments, 1);
          return str.replace(/\\?\#{([^{}]+)\}/gm, function(match, name) {
              if (match.charAt(0) == '\\')
                  return match.slice(1);
              var index = Number(name)
              if (index >= 0)
                  return array[index];
              if (object && object[name] !== void 0)
                  return  object[name];
              return  '';
          });
      }

它支持两种传参方法,如果字符串的占位符为0、1、2这样的非零整数形式,要求传入两个或两个以上的参数,否则就传入一个对象,键名为占位符。

      var a = format("Result is #{0},#{1}", 22, 33);
      alert(a);//"Result is 22,33"
      var b = format("#{name} is a #{sex}", {
          name: "Jhon",
          sex: "man"
      });
      alert(b);//"Jhon is a man"

quote 方法:在字符串两端添加双引号,然后内部需要转义的地方都要转义,用于接装 JSON的键名或模析系统中。

      //http://code.google.com/p/jquery-json/
      var escapeable = /["\\\x00-\x1f\x7f-\x9f]/g,
              meta = {
          '\b': '\\b',
          '\t': '\\t',
          '\n': '\\n',
          '\f': '\\f',
          '\r': '\\r',
          '"': '\\"',
          '\\': '\\\\'
      };
      function quote(target) {
          if (target.match(escapeable)) {
              return '"' + target.replace(escapeable, function(a) {
                  var c = meta[a];
                  if (typeof c === 'string') {
                      return c;
                  }
                  return '\\u' + ('0000' + c.charCodeAt(0).toString(16)).slice(-4)
              }) + '"';
          }
          return '"' + target + '"';
      }

当然,如果浏览器已经支持原生JSON,我们直接用JSON.stringify就行了,另,FF在JSON发明之前,就支持String.prototype.quote与String.quote方法,我们在使用quote之前判定浏览器是否内置这些方法。

字符串好像没有大的浏览器兼容问题,有的话是IE6、IE7 不支持用数组中括号取它的每一个字符,需要用charAt来取;IE6、IE7、IE8不支持垂直分表符,因此有如下hack。

      var isIE678= !+"\v1" ;

好了,我们来修复旧版本IE中的trim函数。这是一个很常用的操作,通常用于表单验证,我们需要把两端的空白去掉,清除“杂质”后,或转换数值进行范围验证,或进行空白验证,或字数验证……由于太常用,相应的实现也非常多。我们可以一起看看,顺便学习一下正则。

版本1:看起来不怎么样,动用了两次正则替换,实际速度非常惊人,主要得益于浏览器的内部优化。base2类库使用这种实现。在Chrome刚出来的年代,这实现是异常快的,但Chrome对字符串方法的疯狂优化,引起了其他浏览器的跟风。于是正则的实现再也比不了字符串方法了。一个著名的例子字符串拼接,直接相加比用Array做成的StringBuffer还快,而StringBuffer技术在早些年备受推崇!

      function trim(str) {
          return str.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
      }

版本 2:和版本 1 很相似,但稍慢一点,主要原因是它最先是假设至少存在一个空白符。Prototype.js使用这种实现,不过其名字为strip,因为Prototype的方法都是力求与Ruby同名。

      function trim(str) {
          return str.replace(/^\s+/, '').replace(/\s+$/, '');
      }

版本 3:截取方式取得空白部分(当然允许中间存在空白符),总共调用了四个原生方法。设计得非常巧妙,substring以两个数字作为参数。Math.max以两个数字作参数,search则返回一个数字。速度比上面两个慢一点,但基本比10之前的版本快!

      function trim(str) {
          return str.substring(Math.max(str.search(/\S/), 0),
                  str.search(/\S\s*$/) + 1);
      }

版本4:这个可以称得上版本2的简化版,就是利用候选操作符连接两个正则。但这样做就失去了浏览器优化的机会,比不上版本三。由于看来很优雅,许多类库都使用它,如jQuery与mootools。

      function trim (str) {
          return str.replace(/^\s+|\s+$/g, '');
      }

版本5:match如果能匹配到东西会返回一个类数组对象,原字符匹配部分与分组将成为它的元素。为了防止字符串中间的空白符被排除,我们需要动用到非捕获性分组(?:exp)。由于数组可能为空,我们在后面还要做进一步的判定。好像浏览器在处理分组上比较无力,一个字慢。所以不要迷信正则,虽然它基本上是万能的。

      function trim(str) {
          str = str.match(/\S+(?:\s+\S+)*/);
          return str ? str[0] : '';
      }

版本6:把符合要求的部分提供出来,放到一个空字符串中。不过效率很差,尤其是在IE6中。

      function trim(str) {
          return str.replace(/^\s*(\S*(\s+\S+)*)\s*$/, '$1');
      }

版本7:与版本6很相似,但用了非捕获分组进行了优点,性能效之有一点点提升。

      function trim(str) {
          return str.replace(/^\s*(\S*(?:\s+\S+)*)\s*$/, '$1');
      }

版本8:沿着上面两个的思路进行改进,动用了非捕获分组与字符集合,用?顶替了*,效果非常惊人。尤其在IE6中,可以用疯狂来形容这次性能的提升,直接秒杀FF3。

      function trim(str) {
          return str.replace(/^\s*((?:[\S\s]*\S)?)\s*$/, '$1');
      }

版本9:这次是用懒惰匹配顶替非捕获分组,在火狐中得到改善,IE没有上次那么疯狂。

      function trim(str) {
          return str.replace(/^\s*([\S\s]*?)\s*$/, '$1');
      }

版本 10:我只想说,搞出这个的人已经不是用厉害来形容,已是专家级别了。它先是把可能的空白符全部列出来,在第一次遍历中砍掉前面的空白,第二次砍掉后面的空白。全过程只用了indexOf与substring这个专门为处理字符串而生的原生方法,没有使用到正则。速度快得惊人,估计直逼内部的二进制实现,并且在IE与火狐(其他浏览器当然也毫无疑问)都有良好的表现。速度都是零毫秒级别的。PHP.js就收纳了这个方法。

      function trim(str) {
          var whitespace = ' \n\r\t\f\x0b\xa0\u2000\u2001\u2002\u2003\n\
        \u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u2028\u2029\u3000';
          for (var i = 0; i < str.length; i++) {
              if (whitespace.indexOf(str.charAt(i)) === -1) {
                  str = str.substring(i);
                  break;
              }
          }
          for (i = str.length - 1; i >= 0; i--) {
              if (whitespace.indexOf(str.charAt(i)) === -1) {
                  str = str.substring(0, i + 1);
                  break;
              }
          }
          return whitespace.indexOf(str.charAt(0)) === -1 ? str : '';
      }

版本11:实现10的字数压缩版,前面部分的空白由正则替换负责砍掉,后面用原生方法处理,效果不逊于原版,但速度都非常逆天。

      function trim(str) {
          str = str.replace(/^\s+/, '');
          for (var i = str.length - 1; i >= 0; i--) {
              if (/\S/.test(str.charAt(i))) {
                  str = str.substring(0, i + 1);
                  break;
              }
          }
          return str;
      }

版本12:版本10更好的改进版,注意说的不是性能速度,而是易记与使用方面。

      function trim(str) {
          var str = str.replace(/^\s\s*/, ""),
                  ws = /\s/,
                  i = str.length;
          while (ws.test(str.charAt(--i)))
              return str.slice(0, i + 1);
      }

版本13:原作者@ialeafs称它为trimChunge,通过字符的charCodeAt值来判定是否为空白,速度也非常逆天,它仅次于版本10,快于版本11、12,不过此版本能处理的空白很有限。

      function trim(str) {
          var m = str.length;
          for (var i = -1; str.charCodeAt(++i) <= 32; )
          for (var j = m - 1; j > i && str.charCodeAt(j) <= 32; j--)
          return str.slice(i, j + 1);
      }

但这还没有完。如果你经常翻看jQuery的实现,你就会发现jQuery1.4之后的trim实现,多出了一个对xA0的特别处理。这是Prototype.js的核心成员·kangax 的发现,IE或早期的标准浏览器在字符串的处理上都有BUG,把许多本属于空白的字符没有列为\s,jQuery在1.42中也不过把常见的不断行空白xA0修复掉,并不完整,因此最佳方案还是版本10。