本文摘自:人民邮电出版社异步图书《JavaScript框架设计(第2版)》
试读本书: www.epubit.com.cn/book/detail…
敲重点:
活动规则:试读样章,评论区留言说一下你对本书的一些感想,同时关注异步社区专栏,并留言你想要得到的图书。
活动时间:即日起-9月10日(活动奖项公告在9月11日)
赠书数量:1本 先到先得!
备注:可以选本书作为奖品也可以选择其他图书
更多好书可以来人邮社异步社区查看,申请下期活动:www.epubit.com.cn/
第2章 语言模块
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。早期出现的各种“JS库”,例如远古的prototype、中古的mootools,到近代的jQuery,再到大规模、紧封装的YUI和Extjs,很大的一个目标就是为了填“兼容性”这个“大坑”。
在avalon2中,就提供了许多带compact命名的模块,它们就是专门用于修复古老浏览器的兼容性问题。此外,本章也介绍了一些非常底层的知识点,能让读者更熟悉这门语言。
2.1 字符串的扩展与修复
笔者发现脚本语言都对字符串特别关注,有关它的方法特别多。笔者把这些方法分为三大类,如图2-1所示。
图2-1
显然以前,总是想着通过字符串生成标签,于是诞生了一些方法,如anchor、big、blink、bold、fixed、fontcolor、italics、link、small、strike、sub及sup。
剩下的就是charAt、charCodeAt、concat、indexOf、lastIndexOf、localeCompare、match、replace,search、slice、split、substr、substring、toLocaleLowerCase、toLocaleUpperCase、toLowerCase、toUpperCase及从Object继承回来的方法,如toString、valueOf。
鲜为人知的是,数值的toString有一个参数,通过它可以转换为进行进制的数值,如图 2-2所示。
图2-2
但相对于其他语言,JavaScript的字符串方法可以说是十分贫乏的,因此后来的ES5、ES6又加上了一堆方法。
即便这样,也很难满足开发需求,比如说新增的方法就远水救不了近火。因此各大名库都提供了一大堆操作字符串的方法。我综合一下Prototype、mootools、dojo、EXT、Tangram、RightJS的一些方法,进行比较去重,在mass Framework为字符串添加如下扩展:contains、startsWith、endsWith、repeat、camelize、underscored、capitalize、stripTags、stripScripts、escapeHTML、unescapeHTML、escapeRegExp、truncate、wbr、pad,写框架的读者可以视自己的情况进行增减,如图2-3所示。其中前4个是ECMA262V6的标准方法;接着9个发端于Prototype.js广受欢迎的工具方法;wbr则来自Tangram,用于软换行,这是出于汉语排版的需求。pad也是一个很常用的操作,已被收录,如图2-3所示。
图2-3
到了另一个框架avalon2,笔者的方法也有用武之地,或者改成avalon的静态方法,或者作为ECMA262V6的补丁模块,或者作为过滤器(如camelize、truncate)。
各种方法实现如下。
contains 方法:判定一个字符串是否包含另一个字符串。常规思维是使用正则表达式。但每次都要用new RegExp来构造,性能太差,转而使用原生字符串方法,如indexOf、lastIndexOf、search。
function contains(target, it) {
//indexOf改成search,lastIndexOf也行得通
return target.indexOf(it) != -1;
}复制代码
在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;
}复制代码
2.1.1 repeat
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的改良版。利用闭包将类数组对象与数组原型的join方法缓存起来,避免每次都重复创建与寻找方法。
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在各浏览器的得分是最高的。
2.1.2 byteLen
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;
}复制代码
版本3:来自腾讯的解决方案。腾讯通过多子域名+postMessage+manifest离线proxy页面的方式扩大localStorage的存储空间。在这个过程中,我们需要知道用户已经保存了多少内容,因此就必须编写一个严谨的byteLen方法。
/** 复制代码
www.alloyteam.com/2013/12/js-…
计算字符串所占的内存字节数,默认使用UTF-8的编码方式计算,也可制定为UTF-16 UTF-8 是一种可变长度的 Unicode 编码格式,使用1~4个字节为每个字符编码
000000 - 00007F(128个代码) 0zzzzzzz(00-7F) 1个字节
000080 - 0007FF(1920个代码) 110yyyyy(C0-DF) 10zzzzzz(80-BF) 2个字节 000800 - 00D7FF
00E000 - 00FFFF(61440个代码) 1110xxxx(E0-EF) 10yyyyyy 10zzzzzz 3个字节
010000 - 10FFFF(1048576个代码) 11110www(F0-F7) 10xxxxxx 10yyyyyy 10zzzzzz 4个字节
注: Unicode在范围 D800-DFFF 中不存在任何字符 {@link href="zh.wikipedia.org/wiki/UTF-8"…
UTF-16 大部分使用2个字节编码,编码超出 65535 的使用4个字节 000000 - 00FFFF 2个字节
010000 - 10FFFF 4个字节
{@link href="zh.wikipedia.org/wiki/UTF-16…
@param {String} str @param {String} charset utf-8, utf-16
@return {Number} /
function byteLen(str, charset){
var total = 0,
charCode,
i,
len;
charset = charset ? charset.toLowerCase() : '';
if(charset === 'utf-16' || charset === 'utf16'){
for(i = 0, len = str.length; i < len; i++){
charCode = str.charCodeAt(i);
if(charCode <= 0xffff){
total += 2;
}else{
total += 4;
}
}
}else{
for(i = 0, len = str.length; i < len; i++){
charCode = str.charCodeAt(i);
if(charCode <= 0x007f) {
total += 1;
}else if(charCode <= 0x07ff){
total += 2;
}else if(charCode <= 0xffff){
total += 3;
}else{
total += 4;
}
}
}
return total;
}复制代码
truncate方法:用于对字符串进行截断处理。当超过限定长度,默认添加3个点号。
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标签。比如,我们需要实现一个HTMLParser,这时就要处理option元素的innerText问题。此元素的内部只能接受文本节点,如果用户在里面添加了span、strong等标签,我们就需要用此方法将这些标签移除。在Prototype.js中,它与strip、stripScripts是一组方法。
var rtag = /<\w+(\s+("[^"]"|'[^']'|[^>])+)?>|<\/\w+>/gi
function stripTags(target) {
return String(target || "").replace(rtag, '');
}复制代码
stripScripts 方法:移除字符串中所有的script标签。弥补stripTags方法的缺陷。此方法应在stripTags之前调用。
function stripScripts(target) {
return String(target || "").replace(/