JavaScript Tips

本文是关于JavaScript的一些常用Tips。


结构

空白

var that = this;

上面的代码中,等号两端的空白字符是可以去掉的,但我们需要保留它们,不至于让that=this看起来像一个token。

在特定的位置加上空格有助于代码的可读性,以下位置必须加上空格:

更具体点来说,基本代码空白规则如下:

代码空白示例
if (num > 0) {
}

for (i = 0; i < len; i++) {
}

function setStyle(DOM, name, value) {
}

缩进

使用2个空格字符作为代码缩进,避免使用tab空白字符,因为tab空白字符在不同编辑器下可能会有不同的表现,使用空格字符作为缩进能很好的避免这种情况。主流编辑器都能够设置自动对tab进行替换,以方便代码编写。

行字符限制

每行的字符数建议不超过80个字符。 过长的程序行应在适当的地方断行,并保持断行后的代码整齐,同时断行不能破坏表达式本身的意义。

在逗号后面断行
// 断行示例:逗号后面断行
callFunction(
    expression1,
    expression2,
    expression3,
    expression4
);
在逻辑运算符前面断行
// 断行示例:逻辑运算符前面断行
if (condition1
    && condition2
    && condition3
) {
    callFunction();       
}
在+号前面断行
// 断行示例:+号前面断行
var str = 'hello world'
          + 'hello world2'
          + 'hello world3';
                          

不得出现以下情况:

// 最后一个结果很长,但不建议合并条件和第一分支
// 不要这么干
var result = condition ? resultA :
    thisIsAVeryVeryLongResult;

这种方法会导致语义上的分裂,即条件和分支在一行,另一分支在一行,没有按逻辑进行组织。

不要省略if、while、for、do的块

对于ifwhilefordo,应用{}将执行体包围,避免产生理解障碍。 主流的编辑器,以回车新起一行时,输入光标通常保留在上一行的缩进位置,所以编写时容易造成这样的情况:

容易造成理解歧义的块省略
if (condition)
    callFunction1();
    callFunction2();

命名

常用命名规则

根据不同类型的内容,采用不同的命名法:

命名同时还需要关注语义,如:

常量命名

目前主流浏览器中,JavaScript中无法使用constES6已经支持const)声明常量。如果要声明一个在程序运行阶段不会更改的变量(如配置项),命名规则为:每个字母大写,单词间以下划线分割。

常量命名示例
var IS_DEBUG_ENABLED = true; 

变量命名

局部变量与全局变量的命名均使用Camel命名法

变量命名示例
var foo = 'foo';

函数命名

JavaScript中,函数可以作为构造器用来实例化对象,也可以被调用。为了增加代码可读性,需要在命名上进行区分。

对于被调用的函数,使用Camel命名法
 

// 函数命名示例:调用函数  
function foo() {
  // ...
}
对于通过new来实例化对象的函数构造器,使用Pascal命名法
// 函数命名示例:函数构造器
function Dog(name) {
  this.name = name;
}                           

JavaScript已经提供了许多内置的函数构造器,如Object, RegExp等等。

对象命名

对象的命名规则与对象本身的职责有关。

注释

你必须为你的代码写注释,尽管除了你其他人不太可能去看你的代码。对于工程师来说,注释是一种编程意识,同时也是一种责任。 当你写代码时,可能你会觉得这段代码的意思是很明显的,但是当你过一段时间回过头来看这段代码时,你得花可能比写这段代码更多的时间去回想起它是做什么的。

当然也这也不是让你去为每个变量,每一个表达式都加上注释,但是,至少你也得为每个函数添加注释,让看代码的人知道这个函数是做什么的,它需要哪些类型的函数,而它的返回值类型又是什么,它里面又用到了哪些很Trick的技巧或是技术。

对于注释,最重要的也是最难坚持的习惯是保持注释最新,因为过时的注释会误导你,有时比没有注释还更糟糕。

文件描述

JavaScript文件顶部需要有文件描述的注释,其中包括项目名、作者、修改日期等信息。

注释示例:文件描述
/*!
 * jQuery JavaScript Library v1.9.0
 * http://jquery.com/
 *
 * Includes Sizzle.js
 * http://sizzlejs.com/
 *
 * Copyright 2005, 2012 jQuery Foundation, Inc. and other contributors
 * Released under the MIT license
 * http://jquery.org/license
 *
 * Date: 2013-1-14
 */

但是有一点需要注意,文件描述通常不用于生成API文档的。

函数与方法描述

函数和方法必须用注释描述其功能。

注释示例:函数与方法描述
/**
 * 发送一个ajax post请求
 *
 * @param {string} url 发送请求的url
 * @param {Object} jsonData 需要发送的数据
 * @param {Function} successFn 请求成功时触发,function(data, status)
 * @param {Function} failedFn 请求失败时触发,function(msg)
 */
function jsonPost(url, jsonData, successFn, failedFn) {
  $.ajax({
    type: 'POST',
    url: url,
    data: jsonData,
    dataType: 'json',
    success: function (data, textStatus, jqXHR) {
      var ret = data;

      //状态为0或者大于2,表示成功
      if (ret.status === 0 || ret.status > 2) {
          //调用成功函数,传递数据,同时传递状态码
          successFn(ret.data, ret.status, ret.msg);
      }
      //状态为1,表示失败
      else if (ret.status === 1) {
          if (failedFn) {
              failedFn(ret.msg);
          }
      }
      //状态为2,表示重定向至某个地址
      else if (ret.status === 2) {
          window.location = ret.data;
      }
    }
  }).fail(function(jqXHR, textStatus, errorThrown) {
    if (failedFn) {
      failedFn(errorThrown.message);
    }
  });
}

我们描述一个函数的时候,只描述做什么(What),不描述具体如何做(How)。

注释标签

常用的注释标签包括paramreturnprivateextend等。下表是JSDoc支持的部分标签.

JSDoc常用标签

标签 说明
@augments 指明类的基类,同@extends。建议使用@extends
@author 作者名
@class 用来给一个类提供描述(不描述构造函数)
@constant 指明常量
@constructor 描述一个类的构造函数
@constructs 指定一个函数作为构造函数
@default 描述变量的默认值
@deprecated 表示函数/类已不再提供
@description 说明的描述,与首行不带tag的描述同义
@event 描述事件
@example 提供使用示例,比如常用的调用方式
@extends 指明类的基类
@field 指明变量引用
@function 指明变量引用了一个函数(function类型)
@ignore 让jsdoc忽视改描述,不生成文档
@link 类似于@link标签,用于连接许多其它页面
@memberOf 指明成员所属类
@name 强迫生成文档时不适用默认名,指定一个名称
@namespace 描述一个对象为namespace
@param 描述一个函数的参数
@private 表示成员或类为私有,默认不生成文档
@property 在构造器上指明类的属性
@public 指明一个内部变量或成员为公有
@requires 描述依赖的资源
@return 描述一个函数的返回值
@returns 描述一个函数的返回值
@see 描述相关资源
@since 指明一个版本,表示该特性从该版本开始被支持
@throws 描述函数可能产生的错误
@type 指定函数的返回类型
@version 描述版本

操作描述

当算法或业务逻辑代码不容易被阅读时,我们可以用单行或多行注释进行内部描述。

注释示例:操作描述
  
if (condition1) { // 注释写在这里
  callFunction1(); // 注释写在这里
} else {
  callFunction2(); // 注释写在这里
}

/*
 * 注释写在这里
 */
for (var i = 0, len = list.length; i < len; i++) {}

条件

分支排列顺序

按执行频率排列分支的顺序。这样做有两个好处:

条件判断的陷阱

JavaScript中, false, null, 0, '', undefinedNaN都被当做false值。

而在执行==时,则会有一些让人难以理解的陷阱。

关于==的Tricks
  
(function () {
  var undefined;

  undefined == null; // true

  1 == true; // true
  2 == true; // false
  0 == false; // true
  0 == ''; // true
  NaN == NaN; // false
  +0 == -0; // true
  [] == false; // true
  [] == ![]; // true
})();

对于不同类型的==判断,有这样一些规则,顺序自上而下:

  1. 如果xy的类型相同,则使用===算法比对
  2. 如果xnullyundefined,则返回true,反之亦然
  3. 如果其中一个是Number类型,另外一个是String类型,那么需要先把String类型的转换为Number,然后再执行==
  4. 如果其中一个是Boolean类型,另外一个是非Boolean类型,那么需要先把Boolean类型转换为Number类型,然后再执行==
  5. 如果其中一个是String或是Number类型,另外一个是Object类型,那么需要先把Object类型转换为primitive的类型,然后再执行==
  6. 其它情况返回false

想了解更多,请参考JavaScript tricks 2: Use === instead of ==[3]

使用case代替if

在同时满足下面的条件时,建议使用case代替if

下面是一个简单的小例子。

使用case代替if例子
// 替换前,使用if:
if (type === 0) {
  call0();
} else if (type == 1) {
  call1();
} else {
  callOther();
}

// 替换后,使用case:
switch (type) {
case 0:
  call0();
  break;
case 1:
  call1();
  break;
default:
  callOther();
  break;
}
        

switch模式

在写switch语句时,你可以遵循该模式从而改进代码的可读性和鲁棒性。如下面的代码:

switch (type) {
case 0:
  call0();
  break;
case 1:
  call1();
  break;
default:
  callOther();
  break;
}

数据类型

JavaScript中,分为primitivenon-primitive值。一共有5种primitive类型,分别表示不同的primitive值。

所有其它的值都是non-primitive的,也就是Object类型,包括:ObjectFunctionArrayStringBooleanNumberMathDateRegExpError

typeof

我们可以通过typeof操作符检测某个值的类型:

typeof null; // 'object'
typeof undefined; // 'undefined'
typeof 0; // 'number' (`typeof NaN` is also 'number')
typeof true; // 'boolean'
typeof 'foo'; // 'string'
typeof {}; // 'object'
typeof function () {}; // 'function'
typeof []; // 'object'

typeof null是不应该返回'object'的,因为它并不是一个对象。 这是自从JavaScript第一版中就已经存在的一个错误,但是已经为时已晚,现在已经不能去修复它了,因为它会造成breaking change。更理想情况下是返回'null'

instanceof

我们可以用instanceof来检测对象的构造器。

value instanceof Constructor

如果它返回true,那么它就是由这个Constructor所创建的一个对象实例。

function Foo() {}
var f = new Foo();

f instanceof Foo; // true
f instanceof Object; // true
[] instanceof Array; // true

undefined instanceof Object; // false
null instanceof Object; // false

对于value instanceof Constructor这样的表达式,如果Constructor.prototypevalue的原型(proto)链里,那么就返回true

但是,由于instanceof遍历原型链的特性,可能产生如下问题:

instanceof的风险
 

function A() {
  this.testA = new Function();
}

function B() {
  this.testB = new Function();
}

var a = new A();
A.prototype = new B();

a instanceof A; // false

上面的例子中,instanceof认为a不是A的实例,因为Aprototype已经被更改。

实例化对象前,应完成类的prototype成员构建,否则可能导致不期望的判断结果。

类型检测的例子与思考

我们需要判断一个变量的类型的时候,需要考虑很多的可能性。

一个例子:判断变量a是否字符串。

最简单的:

typeof a === 'string'

可能a是字符串的包装类型,于是:

a.constructor === String

可是anullundefined时会报runtime error,于是:

typeof a === 'string' || a instanceof String

如果我们把String构造函数覆盖了,如function String(){},会判断出错,于是:

Object.prototype.toString.call(a) == '[object String]'

如果Object.prototype.toString再被覆盖了……这个时候,我们就没办法了。

简单类型与包装类型

JavaScript被许多其它编程语言影响过,其中之一就是Java,是JavaJavaScript也有了简单类型和包装类型。

那么,问题来了:

如果一个原生String类型没有属性,那么为什么`‘foo’.length可以正确执行返回字符串的长度呢?

那是因为JavaScript会自动地进行隐式装箱操作,也就是把原生类型转换为与它对应的包装类型。 在上面这个例子中,字符串值foo会转换为String对象,从而让你可以访问length属性。

String.prototype.returnMe = function () {
  return this;
}
var a = 'foo';
var b = a.returnMe();

a; //'foo' 
typeof a; //'string' (still a primitive)
b; //'foo'
typeof b; //'object'
简单类型与包装类型的差异
var str = '1',
var str2 = new String('1');

typeof str;  // string
typeof str2; // object
                

对于布尔、数值与字符串来说,应针对简单类型进行编程,包装类型的方法会自动通过隐式装箱调用。

使用简单类型
var str = 'Hello world!';
str.length;
                  

简单类型转换

Converting to String

将其它值转换为字符串,最常见的方法就是使用+运算符,因为+运算符中如果其中一个操作数是字符串,那么它就会执行字符串拼接。 所以,想要得到类型转换后的字符串值,最简单的方法就是将该值与一个空字符串拼接。如:

foo + ''

另外一个可选的方法就是String构造函数作为函数调用,即将待转换值作为参数传入它。

var string = String(foo);
# -3.14 -0 +0 1 3.14 314e-2 -Infinity +Infinity NaN
’’ + colValue ‘-3.14’ ‘0’ ‘0’ ‘1’ ‘3.14’ ‘3.14’ ‘-Infinity’ ‘+Infinity’ ‘NaN’
# undefined null true false {} [] function foo() {}
’’ + colValue ‘undefined’ ‘null’ ‘true’ ‘false’ ‘[object Object]’ ’’ ‘function foo(){}’

对象或是函数需要转换为字符串时,会默认调用它们的toString方法。注意,Object.prototype.toStringFunction.prototype.toString方法我们是可以任意覆盖的。

将函数转换为字符串的过程我们称之为函数反编译,一般我们可以通过fn.toString()String(fn)fn + ''实现,但是最后都是通过Function.prototype.toString[4]实现的。但是它并不一定会反射函数的源代码,因为它的实现与平台相关,结果并不总是一样的[5],特别是ES6中新出现的arrow function

Converting to Number

字符串到数值的转换,大家首先会想到的最好的方式一般是parseIntparseFloat,但是他们真的是最好的方式吗?

// 将字符串转移为数值(包括整数和浮点数)的不同方式
parseFloat('100')
+'100'
// 只用于整数
parseInt('100', 10)
'100'|0
'100' >> 0
'100' << 0
//Only works for positive numbers
'100' >>> 0

// ToInteger实现:http://es5.github.io/#x9.4
function ToInteger(x) {
  x = Number(x);
  return x < 0 ? Math.ceil(x) : Math.floor(x);
}

我们得到的结论:

parseInt并不是常常能正确的工作: #todo
[1, 2, 3, 4].map(parseInt); // [1, NaN, NaN, NaN]
parseInt('08'); // 0
parseInt('08', 10); //8
parseInt(1000000000000000000000.5, 10); // 1
Coverting Float to Integer

我们可以使用Math.floorMath.roundMath.ceil,又或是ToInteger方法。

对象(Object)

初始化

使用对象直接量{}初始化对象,不使用new Object

对象初始化
var obj = {
  num: 1,
  str: 'hello'
};

对象的最后一个成员后不要添加,号,否则在IE下会报语法错误。

类似的,初始化Array或是RegExp时,也应使用直接量。

ArrayRegExp初始化
var arr = [1, 2, 3, 4, 5, 6];
var regex = /^[a-z]+$/i;
                  

我们都知道在JavaScript中提供了二种方式定义正则表达式,一个是通过RegExp构造函数,另一个通过直接量。 RegExp构造函数产生的对象是在runtime时编译的,但是直接量却是在脚本加载时就已经编译了,所以它的性能相对会更好[6]。从另一方面来看,字面量更适合已经知道表达式构成时,而通过构造函数更适合动态构建正则表达式,如通过用户的输入。

成员访问

对象成员访问有两种方式:

如果成员名称为JavaScript保留字,则可以通过obj[expression]方式访问。其他时候,通过obj.identifier访问,该方式增加可读性,并且减少字符数。

删除成员

delete[7]关键字可以删除对象的成员。

使用delete删除对象成员
var GLOBAL_OBJECT = this; 

/* create global property via variable declaration; property has DontDelete */ 
var foo = 1; 

/* create global property via undeclared assignment; property has no DontDelete */ 
bar = 2; 

delete foo; // false 
typeof foo; // "number" 

delete bar; // true 
typeof bar; // "undefined"

原生对象污染

禁止扩展原生对象,如ObjectStringFunction等。

污染原生对象可能会带来冲突,比如两个开发者或者使用的两个库都扩展了Stringformat函数,则代码字面上后面的会覆盖前面的,导致不可预料的错误。 污染原生对象可能导致不期望的后果,如扩展了Object,则对任何一个对象的for...in都会遍历出这个扩展。

function Person(name) {
  this.name = name;
}
Person.prototype = {
  describe: function () {
    return 'Name: ' + this.name;
  }
};
var person = new Person('Jane');
for (var key in person) {
  console.log(key); // name, describe
}

Object.prototype.foo = 1;
for (var i in obj) {
  console.log(i); // foo
}

Object.getOwnPropertyDescriptor(Object.prototype, 'foo'); // Object {value: 1, writable: true, enumerable: true, configurable: true, foo: 1}

我们通过Object.getOwnPropertyDescriptor可以看到,fooenumerable属性是为true的,所以在for in遍历时我们会看到它。

函数(Function)

函数也是对象

JavaScript是支持First-class functions的,也就是说函数是一等公民

所谓第一等公民(first class),指的是函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值。

var print = function (i) {
  console.log(i);
};

[1, 2, 3].forEach(print);
print instanceof Object; //true

调用方式

函数可以以以下4种方式调用:

直接调用
foo('Hello world!');
方法调用
    
obj.foo('Hello world!');
构造器调用
 

new Foo('hello');
apply或call
Object.prototype.toString.call(arr); // '[object Array]'
                        

this指针

函数在被调用时,this指针是由调用者所决定的。

作用域(scope)

与熟知的绝大多数高级语言(c、c++、java…)不同,JavaScript变量的作用域是function域,不是block域。

变量的函数作用域
function myFunc() {
  for (var i = 0; i < 10; i++) {
    var num = i; // 生命周期是function的生命周期
  }

  num; // 9
}
myFunc();

for (var k in {a: 1, b: 2 }) {
  alert(k);
}
alert(k); // 尽管循环已经结束,但是变量`k`仍然在作用域中

函数在被调用的时候,会创建一个作用域。作用域可以看做是一个JavaScript对象,其中包括arguments属性,以及变量,函数声明和与形参同名的属性。

闭包

函数对象会引用父函数作用域对象。所以,我们能够通过闭包,让随后执行的函数使用当前父函数运行时的环境(除了thisarguments

简单的闭包
var myFunc = (function (num) {
  var me = this;

  return function () {
    num; // 2
    me == window; // true;
  };
})(2);
myFunc();

作用域泛滥

父函数作用域对象被子函数引用后,如果子函数包含引用,则父函数的作用域不会被回收。下面是一个作用域泛滥的例子,这个例子额外创建了lis.length个scope,并且他们都不会被释放:

作用域泛滥
function init() {
  var lis = ul.getElementsByTagName('li');
  _.forEach(lis, function (item) {
    item.onclick = function (e) {
      e = e || window.event;
      // ......
    };
  });
}
init();

闭包原则

如果闭包中需要使用DOM,闭包环境应引用DOM元素id,在需要使用的时候再获取。

闭包函数中使用DOM元素
function getHandler() {
  var DOMId = 'DOM';
  return function () {
    var DOM = document.getElementById(DOMId); // 获取DOM元素
  };
}

在mouseover事件或其他频繁执行的状态,如果多次获取同一个DOM对象会导致效率问题,可以在第一次持有DOM对象的引用,在使用完后释放。

字符串

字符串处理是前端最常见的操作,所以它很有可能成为性能瓶颈。

查找

字符串可以使用正则也可以使用String.prototype.indexOfString.prototype.search,从这个测试可以看出,这里的性能问题有很多种情况,对于我们关注的IE6而言,indexOf字符串和search正则性能是相同的,不够search字符串要比正则快,有可能是构建正则的开销导致,除了IE6、IE7,其它JS引擎的正则都不会慢于字符串,甚至快很多。

v8中的indexOf会根据待匹配的字符串特点来选择不同的算法,对于小于7个字符会使用线性查找,而大于等于7个字符则使用Boyer-Moore-Horspool

拼接

字符串拼接是一个相当消耗资源的操作,但是最好的做法是什么呢?过去常见的性能优化方法中都会提到使用数组push+join的方式来取代字符串的拼接,但是实际上是这样的么? 如果我们对拼接后的字符串不做任何操作,那么最新版的Chrome(40.0.2214.111 (64-bit))和Firefox(35.0.1)结果是一样的:+=操作符比+, String.prototype.concatArray.prototype.join都快,并且Array.prototype.join是最慢的[[8]](#[8]]。

但是,如果我们对拼接后的字符串进行操作,结果是不一样的[9]

v8中的实现类似JavaStringBuilder,内部也是用数组来实现的,但做了特殊优化,所以性能比JS数组快。 Firefox的方法是对于短字符串使用memcpy,长字符串使用Rope数据结构

总的来说,字符串拼接,应使用数组作为StringBuffer保存字符串片段,使用时调用join方法,尽量避免使用++=的方式拼接较长的字符串,每个字符串都会使用一个小的内存片段,过多的内存片段会影响性能。

绝大部分现代浏览器都对+=的字符串拼接进行了优化,但在IE6IE7中字符串拼接远远慢于push+join,同时IE6的使用率依然很高。

不提倡的拼接方式:+=
var str = '';
for (var i = 0, len = list.length; i < len; i++) {
  str += '<div>' + list[i] + '</div>';
}
DOM.innerHTML = str;
提倡的拼接方式:push+join
var str = [];
for (var i = 0, len = list.length; i < len; i++) {
  str.push('<div>' + list[i] + '</div>');
}
DOM.innerHTML = str.join('');
更好的拼接方式:push+join, 使用临时变量存储数组长度
var str = [];
var strLen = 0;

for (var i = 0, len = list.length; i < len; i++) {
  str[strLen++] = '<div>' + list[i] + '</div>';
}
DOM.innerHTML = str.join('');

字符串格式化

复杂的字符串拼接,应使用格式化的方式,提高代码可读性。

字符串格式化
// 让人难以理解的拼接:
var str = '<div id="' + id + '" class="' + className + '">' + html + '</div>';

// 使用格式化:
var tpl = '<div id="${0}" class="${1}">${2}</div>';
var str = jQuery.format(tpl,
  id, className, html
);

字符串常用实例方法

更多的可以参考这里

stringObject.charAt(position)

charAt方法返回字符串处于position位置的字符。如果position为负值或超出字符串长度,则返回空字符串。

'hello'.charAt(0); // 'h'
stringObject.charCodeAt(position)

charCodeAt方法返回字符串处于position位置字符的Unicode编码。如果position为负值或超出字符串长度,则返回NaN

  
'hello'.charCodeAt(0); // 104: 字符h的Unicode码,即ASCII码
                      
stringObject.indexOf(searchString, position)

indexOf方法在字符串内查找子字符串searchString的第一个匹配的位置,无匹配时返回-1,可选参数position可设置从指定位置开始查找。

var index = 'Hello World'.indexOf('l'); // 2

一般情况下,如果我们不需要searchString的索引值,那么我们也可以使用RegExp.prototype.test来检索字符串中指定的值,它返回truefalse

JSPerf: RegExp.prototype.test & String.prototype.indexOf: indexOf更快,可能是因为省去了构建正则表达式的开销。

stringObject.lastIndexOf(searchString, position)

lastIndexOf()方法与indexOf()方法类似,但是lastIndexOf()方法是从尾部向前查找。

var index = 'Hello World'.lastIndexOf('l'); // 9
stringObject.localeCompare(otherString)

localeCompare用本地特定的顺序来比较两个字符串,该方法与本机环境相关。如果字符串比otherString小,则返回小于0的数,则该方法返回大于 0 的数。如果两个字符串相等,或根据本地排序规则没有区别,该方法返回0。

该方法一般与数组的sort()方法结合使用,localeCompare()方法提供的比较字符串的方法,考虑了默认的本地排序规则。**ECMAscript **标准并没有规定如何进行本地特定的比较操作,它只规定该函数采用底层操作系统提供的排序规则。

['张学友', '刘德华', '郭富城', '黎明'].sort(function (a, b) {
  return a.localeCompare(b);
}); // ["郭富城", "黎明", "刘德华", "张学友"]
stringObject.match(regexp)

match()方法可在字符串内检索指定的值,或找到一个或多个正则表达式的匹配。 该方法类似indexOf()lastIndexOf(),但是它返回指定的值,而不是字符串的位置。

在全局检索模式下,match()既不提供与子表达式匹配的文本的信息,也不声明每个匹配子串的位置。如果您需要这些全局检索的信息,可以使用RegExp.exec()

如果regexp没有标志g,那么match()方法就只能在stringObject中执行一次匹配。如果没有找到任何匹配的文本,match()将返回null。否则,它将返回一个数组,其中存放了与它找到的匹配文本有关的信息。该数组的第0个元素存放的是匹配文本,而其余的元素存放的是与正则表达式的子表达式匹配的文本。除了这些常规的数组元素之外,返回的数组还含有两个对象属性。index属性声明的是匹配文本的起始字符在stringObject中的位置,input属性声明的是对stringObject的引用。

var html = '<html><head><title>title</title></head><body style="color:red;"><p>p</p></body></html>';
html.match(/<[^>/]+>/g); // ["<html>", "<head>", "<title>", "<body style="color:red;">", "<p>"]    

让我们来比较与RegExp相关的几个字符串方法(match, search, exec, test)的性能:JSPerf: RegExp方法

RegExp.prototype.execString.prototype.match快很多,但是那是因为他们的用途根本就不一样。在搜索子串时,RegExp.prototype.test相对更快,也许是因为它并不需要返回匹配子串的索引吧。

stringObject.replace(search, replaceValue)

replace()方法用于在字符串中用一些字符替换另一些字符,或替换一个与正则表达式匹配的子串,并返回一个新字符串。

'border_left_style'.replace('_', '-'); // 'border-left_style',只替换第一项
'border_left_style'.replace(/_/g, '-'); // 'border-left-style',替换所有项

replaceValue可以是字符串,也可以是函数。如果它是字符串,那么每个匹配都将由字符串替换。但是replaceValue中的$字符具有特定的含义[[10]],它说明从模式匹配得到的字符串将用于替换。

// 使用替换函数
'borderLeftStyle'.replace(/[A-Z]/g, function ($0) {
  return '-' + $0.toLowerCase();
});
stringObject.search(regexp)

search()方法用于检索字符串中指定的子字符串,或检索与正则表达式相匹配的子字符串,并返回stringObject中第一个与*regexp *相匹配的子串的起始位置。和stringObject.indexOf不同的是:

性能测试:String.prototype.search vs String.prototype.indexOf

很显示,indexOf肯定是最快,其次是search(string),最后是search(regexp)

  
'type="text"'.search(/"/); // 5
stringObject.slice(start, end)

slice()方法提取字符串的片断(从start到end,但不包括end),并在新的字符串中返回被提取的部分。 start参数为负值时,将与字符串的length相加,如果还是负数则为0。end参数可选,默认为字符串长度。如果指定一个end,处理规则和start参数一样。

'Hello world!'.slice(0, 5); // Hello
'Hello world!'.slice(-6, -1); // world

不要使用substring来截取字符串,它的功能和slice完全一样,只是不支持负数作为参数。

stringObject.split(separator, limit)

split()方法将字符串分割成数组,separator可以是一个字符串,或是一个正则表达式。limit参数可以限定分割的数量,这个参数不常用。

'127.0.0.1'.split('.'); // ['127', '0', '0', '1']
                      

IE下,如果split结果的第一项或最后一项是空字符串,会被直接省略掉。

stringObject.substr(start, length)

substrslice一样都可以用来截取字符串。不同的是substr第二个参数指定的是要截取的字符串长度。

####### string.substr

'Hello world!'.substr(6, 5); // world
stringObject.toLowerCase()

toLowerCase()方法将字符串转换为小写。

var str = 'Hello World!'.toLowerCase(); // 'hello world!'
stringObject.toUpperCase()

toUpperCase()方法将字符串转换为大写。

var str = 'Hello World!'.toUpperCase(); // 'HELLO WORLD!'

字符串常用静态方法

String.fromCharCode(code…)

fromCharCode()方法可以从一串参数中返回一个字符串,每个参数对应的是相应位置的Unicode编码

####### String.formCharCode

String.fromCharCode(20013, 22269); // 中国

数组

复制

数组的复制可以使用slice()方法,但是使用slice()方法是浅复制。如果数组中包含对象,则相应索引的对象引用是同一个对象[[11]]。

数组的浅复制
[1, 2, 3].slice(0); // [1, 2, 3]

var arr = [{}]
arr.slice(0)[0] == a[0]; // true

我们可以利用Array.apply消除数组中的hole。虽然hole在遍历数组或是判断使用in判断某个元素是否在数组中时都起作用,但是当你想输出hole的值时,却是undefined

['a', , 'b'].forEach(function (x) {
  console.log(x);
}); // 'a' 'b'
1 in ['a', , 'b']; // false
['a', , 'b'][1]; // undefined

// 消除hole
Array.apply(null, ['a', , 'b']); // [ 'a', undefined, 'b' ]

避免使用Array.apply的方法复制数组。当只有一个参数时,会被当作初始化的数组长度,导致不期望的结果。

删除

删除数组项

删除数组项可以使用splice()方法。

var arr = [1, 2, 3];
arr.splice(0, 1);
alert(arr); // [2, 3]
清空数组

我们可以设置数组的length0来清除数组的所有项。

var arr = [1, 2, 3];
arr.length = 0; // []

length被设置为小于当前长度时,下标大于或等于设置长度的项会被删除。

var arr = [1, 2, 3];
arr.length = 2; // [1, 2]

遍历

数组长度的变量保存

数组遍历时,应先将数组的length保存到临时变量中,避免在循环的每一步都读取数组的length

for (var i = 0, len = list.length; i < len; i++) {
  // ...
}

如果能保证数组所有项的ToBoolean不为false,可以使用for (var i = 0, obj; obj = list[i]; i++)进行遍历,这种遍历方式对性能有提高。

倒序遍历

当遍历的顺序无关时,建议使用倒序遍历。

var len = list.length;

while (len--) {
  var item = list[len];
}

排序

JavaScript中,可以使用sort()方法对数组进行排序。

在基于比较的排序情况下,不建议自己写排序。因为使用sort排序性能并不比自己写快速排序差,甚至更好。

数值排序

sort()方法默认会将数组每一项转换为字符串,如果要按数值排序,需要传递一个比较函数。

[3, 5, 2, 1].sort(function (a, b) {
  return a - b;
}); // [1, 2, 3, 5]
                      
中文排序

使用字符串的localeCompare()方法,可以对中文进行排序。localeCompare()方法的返回值类似于sort方法比较函数的约定,并且它是用本地特定的顺序来比较两个字符串,所以它依赖于本地环境。

['张学友', '刘德华', '郭富城', '黎明'].sort(function (a, b) {
  return a.localeCompare(b);
}); // ["郭富城", "黎明", "刘德华", "张学友"]

常用数组实例方法

更多的可以参考这里

concat(item…)

concat()方法返回一个新数组,并将参数附加在数组后面。如果参数是一个数组,则数组中的每一项会被分别添加。

var arr = [1, 2, 3];
var newArr = arr.concat([4, 5, 6], 7); // [1, 2, 3, 4, 5, 6, 7]
join(separator)

join()方法把数组构造成一个字符串,并以分隔符分隔。默认的分隔符为’,’。

var arr = ['Hello', 'world'];
arr.join(' '); // "Hello world"
pop()

pop()方法会移除数组最后一项,并返回该项。

var arr = [1, 2, 3];
var last = arr.pop(); // arr: [1, 2]; last: 3
push(item…)

push()方法向数组尾部添加一个或多个项,并返回操作后的数组长度。

pushpop配合可使数组像stack一样工作。

pushconcat有一些不同:

var arr = [1, 2, 3];
arr.push([4, 5, 6], 7); // [1, 2, 3, [4, 5, 6], 7]
shift()

shift()方法会移除数组第一项,并返回该项。

var arr = [1, 2, 3];
var first = arr.shift(); // arr: [2, 3]; first: 1
slice(start, end)

slice()方法会返回数组的浅复制,索引从start开始到end(不包括end)。 如果startend是负数,解析器会试图把他们与数组的length相加,使其成为非负数。

var arr = [1, 2, 3, 4];
arr.slice(1, 3); // [2, 3]
sort(compareFunction)

sort()方法用于对数组的元素进行排序,默认将按字母顺序对数组中的元素进行排序,说得更精确点,是按照字符编码的顺序进行排序。

请注意,数组在原数组上进行排序,不生成副本。

####### array.sort:默认按字符串排序

[1, 4, 8, 10].sort(); // [1, 10, 4, 8]

另外,我们也可以传递一个比较函数。该函数对数组中的两项进行比较,相等时返回0,如果想要第二个参数排在前面,则返回一个正数。

[7, 4, 6, 2].sort(function (a, b) {
  return b - a;
}); // [7, 6, 4, 2]
                      
splice(start, deleteCount, item…)

splice()方法最大的作用是移除数组的项,同时还能在删除项的位置插入新的项,然后返回被删除的项。

var arr = [1, 2, 3, 4, 5, 6];
arr.splice(1, 2, 7); // [1, 7, 4, 5, 6]
unshift(item…)

unshift()方法向数组开始部分添加一个或多个项。

unshiftpop配合可使数组像Queue一样工作。

var arr = [3, 4, 5];
arr.unshift(1, 2); // [1, 2, 3, 4, 5]
                      

面向对象

静态类

通常我们使用静态类封装同一类型或同一业务相关的功能。静态类在JavaScript里使用频率远高于可实例化的类。静态类在JavaScript里表现为Object

声明静态类
var Util = {
  formatDate: function (date) {},
  encodeHTML: function (source) {}
};
                  

使用IIFE(immediately invoked function expression)可以达到变量私有的效果,该方法可以提高JavaScript代码压缩的效果。因为局部变量是可以被压缩的,而全局变量以及对象成员是无法被压缩的。

var Util = function () {
  var format = "yyyy-MM-dd"; // 私有变量,不对外暴露

  return {
    formatDate: function (date) {},

    encodeHTML: function (source) {}
  };
}();
                  

继承

使用原型继承

在继承的实现上,通常我们利用JavaScript的语言特性,使用原型继承。

原型继承的优点是:

原型继承的缺点是:

function Animal(name) {
 this.name = name;
}

Animal.prototype = {
 jump: function () {
   alert('animal ' + this.name + ' jump');
 }
};

function Dog(name) {
 Animal.call(this, name);
}

Dog.prototype = new Animal();

Dog.prototype.jump = function () {
 alert('dog ' + this.name + ' jump');
};
                  
原型继承的风险

由于原型对象的成员被所有实例共享,所以编码是我们应该遵守这样的原则:原型对象只包含程序不会修改的成员,如方法函数或配置项。

下面是一个简单的风险例子:

function ListBase() {
  this.container = [];
}

ListBase.prototype = {
  push: function (item) {
    this.container.push(item);
  },

  alert: function () {
    alert(this.container);
  }
};

function List() {}

List.prototype = new ListBase();

var list1 = new List();
var list2 = new List();

list1.push(1);
list2.push(2);
list2.alert(); // 1,2

DOM

由于DOM对象不是JavaScript原生对象,所以我们在使用DOM对象的时候,可能会面临各种各样的麻烦:性能、浏览器兼容性、内存泄露等。

获取元素

获取单个元素

通常,我们使用document.getElementById()方法来获取DOM元素,避免使用document.all

document.getElementById()方法是标准方法,兼容所有浏览器。但是,IE浏览器会混淆元素的idname属性,document.getElementById()可能获得不期望的元素。在对元素的idname属性的命名需要非常小心,应使用不同的命名法。下面是一个name与id冲突的例子:

<input type='text' name='test'>
<div id='test'></div>

<!-- ie6下为INPUT -->
<button onclick='alert(document.getElementById('test').tagName)'></button>
获取元素集合
getElementsByTagName

getElementsByTagName(tagName)方法可以根据标签名获得元素下的子元素,包含非直接附属的子元素。我们可以指定tagName参数为_*_来获得元素的所有子元素。

<!-- DOM操作实时反映到结果集合中 -->
<body>
<div></div>
<span></span>
&gt;script&lt;var elements = document.body.getElementsByTagName('*');
alert(elements[0].tagName); // div

var div = elements[0];
var u = document.createElement('u');

document.body.insertBefore(u, div);
alert(elements[0].tagName); // u,实时反应到结果中
</script>
</body>
childNodes

childNodes属性可以获取控件的直接子节点集合,但是集合中包括文本、注释、属性等类型的节点。

children

children属性可以获取控件的直接子元素集合,但是集合中只包含元素节点,并不包括文本节点等等。

以上方法或属性的结果并不直接引用DOM元素,而是对索引进行读取,所以DOM结构的改变会实时反映到结果中。使用了getElementsByTagName方法需要小心DOM操作的时序性。

样式读取

浏览器不兼容性的重灾区。

通过style属性,我们不能读取embeded, linked或者是imported的样式,但是我们又偶尔需要去获取这些非inline样式,MicrosoftW3C提供了不同的方法去访问这些非inline样式 。

style

通过style属性可以给元素设置inline样式。

The general rule is that all dashes are removed from the CSS property names, and that the character after a dash becomes uppercase. Thus margin-left becomes marginLeft, text-decoration becomes textDecoration, and border-left-style becomes borderLeftStyle. 许多CSS样式名称里面都会包含-,比如font-size。在JavaScript中,-是不用于属性名称中的,所以,一般的原则是去掉所有的-,然后-后面的第一个字符变为大写,如margin-left变为 marginLeft。另外,别忘记添加CSS单位,如px,因为如果没有它,浏览器会不知道如何解析它,所以它什么都不会做。

HTML元素的style属性仅仅提供访问元素的内联样式。

currentStyle

Microsoft是使用currentStyle属性,它与style属性几乎一样,除了它支持访问元素的所有样式,并不限于inline样式,因此它是真正反应了作用于元素上的实时样式,另外,它是只读的。

var x = document.getElementById('test'); 
alert(x.currentStyle.color);
window.getComputedStyle()

W3C的解决方案是window.getComputedStyle()方法, 它与currentStyle差不多,但是它的语法更复杂:

var x = document.getElementById('test'); 
alert(window.getComputedStyle(x,null).color);

getComputedStyle()方法始终返回的是计算后的pixel值,尽管它的原始值可能是使用的em或是百分比%。

样式设置

className

JavaScript允许你动态地更改元素的classid属性,之后浏览器会自动更新元素的样式。幸运的是该方式兼容所有浏览器。

element.className = 'class';

性能

提高DOM操作的性能有一个主要原则:减少DOM操作,减少浏览器的Reflow。

减少DOM操作

举一个简单的例子:构建一个列表。我们可以用两种方式:

第一种方法看起来比较标准,但是每次循环都会对DOM进行操作,性能极低。在这里推荐使用第二种方法。

减少浏览器的Reflow

下面一些场景会触发浏览器的reflow:

// 一个频繁触发reflow的例子
var parent = document.getElementById('parent');
var elements = parent.getElementsByTagName('li');
var len = elements.length;
var i;

for (i = 0, ; i < len; i++) {
  elements[i].style.width = parent.offsetWidth + 'px';
}

在这个例子中,循环的每一步都读取了parent元素的offsetWidth,所以触发Reflow进行重新渲染了len次。这个例子中,更好的做法是读取一次,较少Reflow

// 一个频繁触发reflow的例子的改进  
var parent = document.getElementById('parent');
var elements = parent.getElementsByTagName('li');
var width = parent.offsetWidth;
var len = elements.length;
var i;

for (i = 0, ; i < len; i++) {
  elements[i].style.width = width + 'px';
}

更多关于Layout/Reflow

DOM事件

浏览器差异性

IE与标准浏览器在事件处理上有很明显的差异,这些差异为浏览器端开发带来了很大的麻烦。

EventArgument

IE通过window.event获取EventArgument;标准浏览器通过监听器的第一个参数获取EventArgumentEventArgument对象本身也存在差异性,最典型的是IE通过srcElement属性获得触发事件的元素,标准浏览器则是target属性。

事件触发

IE的事件触发为冒泡模型,而标准浏览器的触发为捕获-冒泡模型。

监听方法

IE使用attachEvent()方法添加监听器;标准浏览器使用addEventListener()方法添加事件监听器(IE9开始支持它)。

添加事件处理的方法

通常,为一个DOM元素添加事件处理,可以使用3种方法:

内联模型

在内联事件模型中,事件处理器是作为HTML元素的属性添加的。如下所示:

<p>Hey <a href="http://www.example.com" onclick="triggerAlert('Joe'); return false;">Joe!</p>

如果想了解更多,请参考这里

传统模型

在传统事件模型中,事件处理器是通过JavaScript脚本添加或删除的。与内联模型相同的是,每一个事件一次只能绑定一个事件处理器。

element.onclick = doSomething; //添加事件处理器
element.onclick = null; // 移除事件处理器

如果想了解更多,请参考这里

高级事件模型:事件监听器

标准的addEventListener()方法提供了两种时间触发模型:冒泡和捕获。可以通过第三个参数指定。IE的attachEvent()方法只支持冒泡的事件触发。所以为了保持一致性,通常addEventListener()方法的第三个参数都为false

事件类型中,标准浏览器不需要添加on,如click;IE需要添加为onclick

IE通过下使用attachEvent()挂载事件监听器,当事件被触发时,函数的this指针始终指向window,而不是触发事件的元素本身。

// 添加事件处理:添加监听器
function listener(e) {}

if ($.browser.ie) {
  element.attachEvent('onclick', listener);
} else {
  element.addEventListener('click', listener, false);
}
                            

添加事件原则

拼接html时,可以使用标签内联的方法添加事件

有时候我们需要用JavaScript拼接html字符串。这个时候我们面对的不是DOM对象,可以使用标签内联的方式添加事件。标签内联的方法可以不用关心事件的释放。

标签内联的方式使得我们丧失了对事件控制的权利。我们没有办法获得触发事件的target,没有办法停止事件冒泡,没有办法停止默认行为。我们唯一能做的就是使用this把当前DOM元素传递给处理函数。

运行环境可控时,使用传统模型的方法添加事件

这个时候,JavaScript运行的页面环境全部是由我们控制的,我们不担心会有其他的JavaScript脚本覆盖我们挂载的event handler。

对于大多数项目,使用这种方法代替流行的addEventListener,因为该方式会获得更高的性能(如document.body.onmousemove的拖拽处理)

对于页面级别的事件管理,使用添加监听器的方法

有时我们需要在点击页面时关闭所有的浮动层。类似的情况向body添加监听器更容易实现,相互之间无影响。

对于元素级别事件挂载:有的时候一个项目由多人开发,大家为了不覆盖相互对同一个元素的事件处理而添加监听器。但这种情况大多是因为对交互的分析、事件的管理没有很好的规划。

针对第三方环境时,使用添加监听器的方法

添加监听器的方法能够不覆盖之前对响应元素添加的监听器。不过监听器之间的处理顺序无法保证。

开发人员需要持有监听器函数的引用,在适当时候(元素释放、页面卸载等)卸载添加的监听器。

页面加载与代码构建

页面加载

通常我们在body标签结束之前加载外部JavaScript。在这个位置加载JavaScript的好处是:

加载外部JavaScript
<body>
  <div class='container'>...</div>
  <script type='text/javascript' src='my.js'></script>
</body>
                  

script标签中必选的属性为type和src*。

下面的一些属性虽然常用,但部分已不推荐:

deferasync

HTML的<script>标签允许你定义JavaScript代码何时可以开始执行。

这是>script<标签默认的行为,脚本执行的时候HTML的解析会暂停。对于延迟性高的服务器和大型的脚本来说,这也就意味着页面的显示会被推迟。

与正常执行相对,延迟执行也就是会延迟脚本的执行直到HTML的解析完成。这样做的好处是对于你的脚本来说,此时DOM是可以访问了的。然而,并不是所有浏览器都支持它,所以不要单纯地依赖于它。

如果你并不在乎脚本何时会执行,那么异步模式是最好的选择。因为对于异步执行来说,HTML的解析会继续,而脚本一旦下载成功,就会开始解析并执行脚本。Google Analytics就是使用的这种方式。

Asynchronous and deferred JavaScript execution explained

分模块与分文件开发

通常,一个项目中我们可能设计公共模块与业务模块划分,开发时我们会划分模块,并分成多个JavaScript文件。

关于模块划分,有如下建议:

关于文件划分:开发时,我们可以使用一个JavaScript文件来装载要用到的JavaScript文件(如build.js)。这样做的好处是,在提测前构建的时候,无需修改相应的html或模板。

开发时外部JavaScript装载
document.write('<script type="text/javascript" src="/src/UIBase.js"></script>');
document.write('<script type="text/javascript" src="/src/UIManager.js"></script>');
document.write('<script type="text/javascript" src="/src/ui/MonthView.js"></script>');
document.write('<script type="text/javascript" src="/src/ui/Calendar.js"></script>');
document.write('<script type="text/javascript" src="/src/ui/Link.js"></script>');
document.write('<script type="text/javascript" src="/src/ui/Button.js"></script>');
document.write('<script type="text/javascript" src="/src/ui/TextInput.js"></script>');
document.write('<script type="text/javascript" src="/src/ui/BaseBox.js"></script>');
document.write('<script type="text/javascript" src="/src/ui/CheckBox.js"></script>');
document.write('<script type="text/javascript" src="/src/ui/RadioBox.js"></script>');
// ......
                  

切勿以document.write输出静态js引用的方式上线!在IE下,document.write无法保证脚本加载的时序性。经过测试,我们认为对于同域的静态资源,时序型可以保证,所以开发时可以采用这种方式。

自动构建

提测时,我们需要一些脚本来自动将这些文件的具体内容打包到这个文件中,并且进行压缩。这个过程避免手工。想想,如果手工合并文件,是一件多麻烦的事情,在每个提测前可能都要干一遍……

我们可以使用shellantwscriptpython等脚本进行合并。下面是一个简单的shell构建脚本的例子,这个例子中build.js会被打包压缩成build-c.js,为了避免文件被覆盖,需要手工备份build.js。当然我们也可以不备份而从svn中恢复,但那样我们需要解决svn冲突。

下面是一个简单的自动构建的shell脚本示例:

  
cd ..

# merge
cat assets/build.js | 
    awk -F'"' '/src="[^"]+.js"/{print $4}' |
    cut -c 2- |
    xargs cat 1>assets/build-all.js
    
# compress
java -jar ~/local/ecui.jar assets/build-all.js --mode 2 --charset utf-8 -o assets/build-c.js
                  

当然,我们可以会考虑使用现在比较流行的一些build工具,如GruntGulp又或是Webpack

第三方JavaScript

有时候,我们开发的JavaScript是为了给第三方页面提供服务的(广告、开放api…),我们的js会被运行在各种各样的页面环境中。相比环境完全可控的js开发,我们需要关注一些额外的东西:

全局变量冲突

由于环境的未知性,所有暴露的全局变量都可能产生危险,我们应该使用function隔离作用域。

(function () {
 var i = 0;
})();

对于提供API而必须暴露的全局变量,首先减少暴露的个数,以1个为宜。通过挂载到window的方式暴露。

(function () {
  function BaiduClass() {}

  window.BaiduClass = BaiduClass;
})();
         

字符编码

第三方页面使用的字符集编码是无法确定的,可能是GBKUTF-8等等。外联的js如果编码与页面编码不一致,可能导致问题。

解决办法:将ASCII大于127的字符(如中文字符),使用Unicode进行转码,保证代码中不包含ASCII大于127的字符。

字符编码转换
// 原始代码
var str = '中国';

// unicode转码后:
var str = '\u4e2d\u56fd';
                  

Unicode转码能带来字符编码的安全性。但是对于脚本执行环境编码可控的页面,不建议进行Unicode转码。因为会给js文件增加额外的字节大小。

DOM的创建

在第三方页面中创建DOM元素,有两种可能:与用户页面本身DOM结构有关或无关。

与用户页面DOM结构有关的情况,比如需要在引入脚本的位置创建元素时,使用document.write()
// 与用户页面DOM结构有关时,使用document.write创建元素
document.write('<div>myHTML</div>');
                          
与用户页面DOM结构无关的情况,比如绘制一个提供服务的浮动层时,应在用户页面load完成后,使用createElement创建:
 

// 与用户页面DOM结构无关时,在用户页面load完成后,使用createElement创建
$.ready(function () {
 var div = document.createElement('div');
 document.body.appendChild(div);
});
                       

由于window.onload事件的触发时机是页面完全加载(包括图片等资源),所以建议使用DOMContentLoaded事件,在DOM树在页面中构建完成后即可创建元素。该事件非所有浏览器都支持,详细方案见$.ready。

绝对定位的元素应创建于body标签下。切忌创建在未知的当前区域,有可能因为处于表格内部或其他绝对定位元素内,导致定位错误。

参考

参考书籍

Related Posts

Xin(Khalil) Zhang 22 February 2015
blog comments powered by Disqus