分类
JavaScript

深入理解JavaScript系列(5):强大的原型和原型链

# 前言

JavaScript 不包含传统的类继承模型,而是使用 prototypal 原型模型。

虽然这经常被当作是 JavaScript 的缺点被提及,其实基于原型的继承模型比传统的类继承还要强大。实现传统的类继承模型是很简单,但是实现 JavaScript 中的原型继承则要困难的多。

由于 JavaScript 是唯一一个被广泛使用的基于原型继承的语言,所以理解两种继承模式的差异是需要一定时间的,今天我们就来了解一下原型和原型链。

# 原型

10年前,我刚学习JavaScript的时候,一般都是用如下方式来写代码:

```js
var decimalDigits = 2,
tax = 5;

function add(x, y) {
return x + y;
}

function subtract(x, y) {
return x - y;
}

//alert(add(1, 3));

```

通过执行各个function来得到结果,学习了原型之后,我们可以使用如下方式来**美化**一下代码。

## **原型使用方式1:**

在使用原型之前,我们需要先将代码做一下小修改:

```js
var Calculator = function (decimalDigits, tax) {
this.decimalDigits = decimalDigits;
this.tax = tax;
};
```

然后,通过给Calculator对象的prototype属性赋值**对象字面量**来设定Calculator对象的原型。

```js
Calculator.prototype = {
add: function (x, y) {
return x + y;
},

subtract: function (x, y) {
return x - y;
}
};
//alert((new Calculator()).add(1, 3));
```

这样,我们就可以new Calculator对象以后,就可以调用add方法来计算结果了。

## **原型使用方式2:**

第二种方式是,在赋值原型prototype的时候使用function立即执行的表达式来赋值,即如下格式:

`Calculator.prototype = function () { } ();`

它的好处在前面的帖子里已经知道了,就是可以封装私有的function,通过return的形式暴露出简单的使用名称,以达到public/private的效果,修改后的代码如下:

```js
Calculator.prototype = function () {
add = function (x, y) {
return x + y;
},

subtract = function (x, y) {
return x - y;
}
return {
add: add,
subtract: subtract
}
} ();

//alert((new Calculator()).add(11, 3));
```

同样的方式,我们可以new Calculator对象以后调用add方法来计算结果了。

# 再来一点

## **分步声明:**

上述使用原型的时候,有一个限制就是一次性设置了原型对象,我们再来说一下如何分来设置原型的每个属性吧。

```js
var BaseCalculator = function () {
//为每个实例都声明一个小数位数
this.decimalDigits = 2;
};

//使用原型给BaseCalculator扩展2个对象方法
BaseCalculator.prototype.add = function (x, y) {
return x + y;
};

BaseCalculator.prototype.subtract = function (x, y) {
return x - y;
};
```

首先,声明了一个BaseCalculator对象,构造函数里会初始化一个小数位数的属性decimalDigits,然后通过原型属性设置2个function,分别是add(x,y)和subtract(x,y),当然你也可以使用前面提到的2种方式的任何一种,我们的主要目的是看如何将BaseCalculator对象设置到真正的Calculator的原型上。

```js
var BaseCalculator = function() {
this.decimalDigits = 2;
};

BaseCalculator.prototype = {
add: function(x, y) {
return x + y;
},
subtract: function(x, y) {
return x - y;
}
};
```

创建完上述代码以后,我们来开始:

```js
var Calculator = function () {
//为每个实例都声明一个税收数字
this.tax = 5;
};

Calculator.prototype = new BaseCalculator();
```

我们可以看到Calculator的原型是指向到BaseCalculator的一个实例上,目的是让Calculator集成它的add(x,y)和subtract(x,y)这2个function,还有一点要说的是,由于它的原型是BaseCalculator的一个实例,所以不管你创建多少个Calculator对象实例,他们的原型指向的都是同一个实例。

```js
var calc = new Calculator();
alert(calc.add(1, 1));
//BaseCalculator 里声明的decimalDigits属性,在 Calculator里是可以访问到的
alert(calc.decimalDigits);
```

上面的代码,运行以后,我们可以看到因为Calculator的原型是指向BaseCalculator的实例上的,所以可以访问他的decimalDigits属性值,那如果我不想让Calculator访问BaseCalculator的构造函数里声明的属性值,那怎么办呢?这么办:

```js
var Calculator = function () {
this.tax= 5;
};

Calculator.prototype = BaseCalculator.prototype;
```

通过将BaseCalculator的原型赋给Calculator的原型,这样你在Calculator的实例上就访问不到那个decimalDigits值了,如果你访问如下代码,那将会提升出错。

```js
var calc = new Calculator();
alert(calc.add(1, 1));
alert(calc.decimalDigits);
```

## **重写原型:**

在使用第三方JS类库的时候,往往有时候他们定义的原型方法是不能满足我们的需要,但是又离不开这个类库,所以这时候我们就需要重写他们的原型中的一个或者多个属性或function,我们可以通过继续声明的同样的add代码的形式来达到覆盖重写前面的add功能,代码如下:

```js
//覆盖前面Calculator的add() function
Calculator.prototype.add = function (x, y) {
return x + y + this.tax;
};

var calc = new Calculator();
alert(calc.add(1, 1));

```

这样,我们计算得出的结果就比原来多出了一个tax的值,但是有一点需要注意:那就是重写的代码需要放在最后,这样才能覆盖前面的代码。

# 原型链

在将原型链之前,我们先上一段代码:

```js
function Foo() {
this.value = 42;
}
Foo.prototype = {
method: function() {}
};

function Bar() {}

// 设置Bar的prototype属性为Foo的实例对象
Bar.prototype = new Foo();
Bar.prototype.foo = 'Hello World';

// 修正Bar.prototype.constructor为Bar本身
Bar.prototype.constructor = Bar;

var test = new Bar() // 创建Bar的一个新实例

// 原型链
test [Bar的实例]
Bar.prototype [Foo的实例]
{ foo: 'Hello World' }
Foo.prototype
{method: ...};
Object.prototype
{toString: ... /* etc. */};

```

上面的例子中,test 对象从 Bar.prototype 和 Foo.prototype 继承下来;因此,它能访问 Foo 的原型方法 method。同时,它也能够访问那个定义在原型上的 Foo 实例属性 value。需要注意的是 new Bar() 不会创造出一个新的 Foo 实例,而是重复使用它原型上的那个实例;因此,所有的 Bar 实例都会共享相同的 value 属性。

## **属性查找:**

当查找一个对象的属性时,JavaScript 会向上遍历原型链,直到找到给定名称的属性为止,到查找到达原型链的顶部 - 也就是 Object.prototype - 但是仍然没有找到指定的属性,就会返回 undefined,我们来看一个例子:

```js
function foo() {
this.add = function (x, y) {
return x + y;
}
}

foo.prototype.add = function (x, y) {
return x + y + 10;
}

Object.prototype.subtract = function (x, y) {
return x - y;
}

var f = new foo();
alert(f.add(1, 2)); //结果是3,而不是13
alert(f.subtract(1, 2)); //结果是-1
```

通过代码运行,我们发现subtract是安装我们所说的向上查找来得到结果的,但是add方式有点小不同,这也是我想强调的,就是属性在查找的时候是先查找自身的属性,如果没有再查找原型,再没有,再往上走,一直插到Object的原型上,所以在某种层面上说,用 for in语句遍历属性的时候,效率也是个问题。

还有一点我们需要注意的是,我们可以赋值任何类型的对象到原型上,但是不能赋值原子类型的值,比如如下代码是无效的:

```js
function Foo() {}
Foo.prototype = 1; // 无效
```

## **hasOwnProperty函数:**

hasOwnProperty是Object.prototype的一个方法,它可是个好东西,他能判断一个对象是否包含自定义属性而不是原型链上的属性,因为hasOwnProperty 是 JavaScript 中唯一一个处理属性但是不查找原型链的函数。

```js
// 修改Object.prototype
Object.prototype.bar = 1;
var foo = {goo: undefined};

foo.bar; // 1
'bar' in foo; // true

foo.hasOwnProperty('bar'); // false
foo.hasOwnProperty('goo'); // true
```

只有 hasOwnProperty 可以给出正确和期望的结果,这在遍历对象的属性时会很有用。 没有其它方法可以用来排除原型链上的属性,而不是定义在对象自身上的属性。

但有个恶心的地方是:JavaScript 不会保护 hasOwnProperty 被非法占用,因此如果一个对象碰巧存在这个属性,就需要使用外部的 hasOwnProperty 函数来获取正确的结果。

```js
var foo = {
hasOwnProperty: function() {
return false;
},
bar: 'Here be dragons'
};

foo.hasOwnProperty('bar'); // 总是返回 false

// 使用{}对象的 hasOwnProperty,并将其上下为设置为foo
{}.hasOwnProperty.call(foo, 'bar'); // true

```

当检查对象上某个属性是否存在时,hasOwnProperty 是唯一可用的方法。同时在使用 for in loop 遍历对象时,推荐总是使用 hasOwnProperty 方法,这将会避免原型对象扩展带来的干扰,我们来看一下例子:

```js
// 修改 Object.prototype
Object.prototype.bar = 1;

var foo = {moo: 2};
for(var i in foo) {
console.log(i); // 输出两个属性:bar 和 moo
}
```

我们没办法改变for in语句的行为,所以想过滤结果就只能使用hasOwnProperty 方法,代码如下:

```js
// foo 变量是上例中的
for(var i in foo) {
if (foo.hasOwnProperty(i)) {
console.log(i);
}
}
```

这个版本的代码是唯一正确的写法。由于我们使用了 hasOwnProperty,所以这次只输出 moo。如果不使用 hasOwnProperty,则这段代码在原生对象原型(比如 Object.prototype)被扩展时可能会出错。

总结:推荐使用 hasOwnProperty,不要对代码运行的环境做任何假设,不要假设原生对象是否已经被扩展了。

# 总结

原型极大地丰富了我们的开发代码,但是在平时使用的过程中一定要注意上述提到的一些注意事项。

参考内容:http://bonsaiden.github.com/JavaScript-Garden/zh/

分类
JavaScript

深入理解JavaScript系列(4):立即调用的函数表达式

# 前言

大家学JavaScript的时候,经常遇到自执行匿名函数的代码,今天我们主要就来想想说一下自执行。

在详细了解这个之前,我们来谈了解一下“自执行”这个叫法,本文对这个功能的叫法也不一定完全对,主要是看个人如何理解,因为有的人说立即调用,有的人说自动执行,所以你完全可以按照你自己的理解来取一个名字,不过我听很多人都叫它为“自执行”,但作者后面说了很多,来说服大家称呼为“立即调用的函数表达式”。

本文英文原文地址:http://benalman.com/news/2010/11/immediately-invoked-function-expression/

# 什么是自执行?

在JavaScript里,任何function在执行的时候都会创建一个执行上下文,因为为function声明的变量和function有可能只在该function内部,这个上下文,在调用function的时候,提供了一种简单的方式来创建自由变量或私有子function。

```js
// 由于该function里返回了另外一个function,其中这个function可以访问自由变量i
// 所有说,这个内部的function实际上是有权限可以调用内部的对象。

function makeCounter() {
// 只能在makeCounter内部访问i
var i = 0;

return function () {
console.log(++i);
};
}

// 注意,counter和counter2是不同的实例,分别有自己范围内的i。

var counter = makeCounter();
counter(); // logs: 1
counter(); // logs: 2

var counter2 = makeCounter();
counter2(); // logs: 1
counter2(); // logs: 2

alert(i); // 引用错误:i没有defind(因为i是存在于makeCounter内部)。
```

很多情况下,我们不需要makeCounter多个实例,甚至某些case下,我们也不需要显示的返回值,OK,往下看。

## **问题的核心**

当你声明类似function foo(){}或var foo = function(){}函数的时候,通过在后面加个括弧就可以实现自执行,例如foo(),看代码:

```js
// 因为想下面第一个声明的function可以在后面加一个括弧()就可以自己执行了,比如foo(),
// 因为foo仅仅是function() { /* code */ }这个表达式的一个引用

var foo = function(){ /* code */ }

// ...是不是意味着后面加个括弧都可以自动执行?

function(){ /* code */ }(); // SyntaxError: Unexpected token (
//
```

上述代码,如果甚至运行,第2个代码会出错,因为在解析器解析全局的function或者function内部function关键字的时候,默认是认为function声明,而不是function表达式,如果你不显示告诉编译器,它默认会声明成一个缺少名字的function,并且抛出一个语法错误信息,因为function声明需要一个名字。

## **旁白:函数(function),括弧(paren),语法错误(SyntaxError)**

有趣的是,即便你为上面那个错误的代码加上一个名字,他也会提示语法错误,只不过和上面的原因不一样。在一个表达式后面加上括号(),该表达式会立即执行,但是在一个语句后面加上括号(),是完全不一样的意思,他的只是分组操作符。

```js
// 下面这个function在语法上是没问题的,但是依然只是一个语句
// 加上括号()以后依然会报错,因为分组操作符需要包含表达式

function foo(){ /* code */ }(); // SyntaxError: Unexpected token )

// 但是如果你在括弧()里传入一个表达式,将不会有异常抛出
// 但是foo函数依然不会执行
function foo(){ /* code */ }( 1 );

// 因为它完全等价于下面这个代码,一个function声明后面,又声明了一个毫无关系的表达式:
function foo(){ /* code */ }

( 1 );
```

你可以访问[ECMA-262-3 in detail. Chapter 5\. Functions](http://dmitrysoshnikov.com/ecmascript/chapter-5-functions/#question-about-surrounding-parentheses) 获取进一步的信息。

# 自执行函数表达式

要解决上述问题,非常简单,我们只需要用大括弧将代码的代码全部括住就行了,因为JavaScript里括弧()里面不能包含语句,所以在这一点上,解析器在解析function关键字的时候,会将相应的代码解析成function表达式,而不是function声明。

```js
// 下面2个括弧()都会立即执行

(function () { /* code */ } ()); // 推荐使用这个
(function () { /* code */ })(); // 但是这个也是可以用的

// 由于括弧()和JS的&&,异或,逗号等操作符是在函数表达式和函数声明上消除歧义的
// 所以一旦解析器知道其中一个已经是表达式了,其它的也都默认为表达式了
// 不过,请注意下一章节的内容解释

var i = function () { return 10; } ();
true && function () { /* code */ } ();
0, function () { /* code */ } ();

// 如果你不在意返回值,或者不怕难以阅读
// 你甚至可以在function前面加一元操作符号

!function () { /* code */ } ();
~function () { /* code */ } ();
-function () { /* code */ } ();
+function () { /* code */ } ();

// 还有一个情况,使用new关键字,也可以用,但我不确定它的效率
// http://twitter.com/kuvos/status/18209252090847232

new function () { /* code */ }
new function () { /* code */ } () // 如果需要传递参数,只需要加上括弧()
```

上面所说的括弧是消除歧义的,其实压根就没必要,因为括弧本来内部本来期望的就是函数表达式,但是我们依然用它,主要是为了方便开发人员阅读,当你让这些已经自动执行的表达式赋值给一个变量的时候,我们看到开头有括弧(,很快就能明白,而不需要将代码拉到最后看看到底有没有加括弧。

## **用闭包保存状态**

和普通function执行的时候传参数一样,自执行的函数表达式也可以这么传参,因为闭包直接可以引用传入的这些参数,利用这些被lock住的传入参数,自执行函数表达式可以有效地保存状态。

```js
// 这个代码是错误的,因为变量i从来就没被locked住
// 相反,当循环执行以后,我们在点击的时候i才获得数值
// 因为这个时候i才真正获得值
// 所以说无论点击哪个链接,最终显示的都是I am link #10(如果有10个a元素的话)

var elems = document.getElementsByTagName('a');

for (var i = 0; i < elems.length; i++) { elems[i].addEventListener('click', function (e) { e.preventDefault(); alert('I am link #' + i); }, 'false'); } // 这个是可以用的,因为他在自执行函数表达式闭包内部 // i的值作为locked的索引存在,在循环执行结束以后,尽管最后i的值变成了a元素总数(例如10) // 但闭包内部的lockedInIndex值是没有改变,因为他已经执行完毕了 // 所以当点击链接的时候,结果是正确的 var elems = document.getElementsByTagName('a'); for (var i = 0; i < elems.length; i++) { (function (lockedInIndex) { elems[i].addEventListener('click', function (e) { e.preventDefault(); alert('I am link #' + lockedInIndex); }, 'false'); })(i); } // 你也可以像下面这样应用,在处理函数那里使用自执行函数表达式 // 而不是在addEventListener外部 // 但是相对来说,上面的代码更具可读性 var elems = document.getElementsByTagName('a'); for (var i = 0; i < elems.length; i++) { elems[i].addEventListener('click', (function (lockedInIndex) { return function (e) { e.preventDefault(); alert('I am link #' + lockedInIndex); }; })(i), 'false'); } ``` 其实,上面2个例子里的lockedInIndex变量,也可以换成i,因为和外面的i不在一个作用域,所以不会出现问题,这也是匿名函数+闭包的威力。 ## **自执行匿名函数和立即执行的函数表达式区别** 在这篇帖子里,我们一直叫自执行函数,确切的说是自执行匿名函数(Self-executing anonymous function),但英文原文作者一直倡议使用立即调用的函数表达式(Immediately-Invoked Function Expression)这一名称,作者又举了一堆例子来解释,好吧,我们来看看: ```js // 这是一个自执行的函数,函数内部执行自身,递归 function foo() { foo(); } // 这是一个自执行的匿名函数,因为没有标示名称 // 必须使用arguments.callee属性来执行自己 var foo = function () { arguments.callee(); }; // 这可能也是一个自执行的匿名函数,仅仅是foo标示名称引用它自身 // 如果你将foo改变成其它的,你将得到一个used-to-self-execute匿名函数 var foo = function () { foo(); }; // 有些人叫这个是自执行的匿名函数(即便它不是),因为它没有调用自身,它只是立即执行而已。 (function () { /* code */ } ()); // 为函数表达式添加一个标示名称,可以方便Debug // 但一定命名了,这个函数就不再是匿名的了 (function foo() { /* code */ } ()); // 立即调用的函数表达式(IIFE)也可以自执行,不过可能不常用罢了 (function () { arguments.callee(); } ()); (function foo() { foo(); } ()); // 另外,下面的代码在黑莓5里执行会出错,因为在一个命名的函数表达式里,他的名称是undefined // 呵呵,奇怪 (function foo() { foo(); } ()); ``` 希望这里的一些例子,可以让大家明白,什么叫自执行,什么叫立即调用。 *注:arguments.callee在[ECMAScript 5 strict mode](https://developer.mozilla.org/en/JavaScript/Strict_mode#Differences_in_functions)里被废弃了,所以在这个模式下,其实是不能用的。* ## **最后的旁白:Module模式** 在讲到这个立即调用的函数表达式的时候,我又想起来了Module模式,如果你还不熟悉这个模式,我们先来看看代码: ```js // 创建一个立即调用的匿名函数表达式 // return一个变量,其中这个变量里包含你要暴露的东西 // 返回的这个变量将赋值给counter,而不是外面声明的function自身 var counter = (function () { var i = 0; return { get: function () { return i; }, set: function (val) { i = val; }, increment: function () { return ++i; } }; } ()); // counter是一个带有多个属性的对象,上面的代码对于属性的体现其实是方法 counter.get(); // 0 counter.set(3); counter.increment(); // 4 counter.increment(); // 5 counter.i; // undefined 因为i不是返回对象的属性 i; // 引用错误: i 没有定义(因为i只存在于闭包) ``` 关于更多Module模式的介绍,请访问我的上一篇帖子:深入理解JavaScript系列(3):全面解析Module模式 。

分类
JavaScript

深入理解JavaScript系列(3):全面解析Module模式

# 简介

Module模式是JavaScript编程中一个非常通用的模式,一般情况下,大家都知道基本用法,本文尝试着给大家更多该模式的高级使用方式。

首先我们来看看Module模式的基本特征:

1. 模块化,可重用
2. 封装了变量和function,和全局的namaspace不接触,松耦合
3. 只暴露可用public的方法,其它私有方法全部隐藏

关于Module模式,最早是由YUI的成员Eric Miraglia在4年前提出了这个概念,我们将从一个简单的例子来解释一下基本的用法(如果你已经非常熟悉了,请忽略这一节)。

# 基本用法

先看一下最简单的一个实现,代码如下:

```js
var Calculator = function (eq) {
//这里可以声明私有成员

var eqCtl = document.getElementById(eq);

return {
// 暴露公开的成员
add: function (x, y) {
var val = x + y;
eqCtl.innerHTML = val;
}
};
};
```

我们可以通过如下的方式来调用:

```js
var calculator = new Calculator('eq');
calculator.add(2, 2);
```

大家可能看到了,每次用的时候都要new一下,也就是说每个实例在内存里都是一份copy,如果你不需要传参数或者没有一些特殊苛刻的要求的话,我们可以在最后一个}后面加上一个括号,来达到自执行的目的,这样该实例在内存中只会存在一份copy,不过在展示他的优点之前,我们还是先来看看这个模式的基本使用方法吧。

## **匿名闭包**

匿名闭包是让一切成为可能的基础,而这也是JavaScript最好的特性,我们来创建一个最简单的闭包函数,函数内部的代码一直存在于闭包内,在整个运行周期内,该闭包都保证了内部的代码处于私有状态。

```js
(function () {
// ... 所有的变量和function都在这里声明,并且作用域也只能在这个匿名闭包里
// ...但是这里的代码依然可以访问外部全局的对象
}());
```

注意,匿名函数后面的括号,这是JavaScript语言所要求的,因为如果你不声明的话,JavaScript解释器默认是声明一个function函数,有括号,就是创建一个函数表达式,也就是自执行,用的时候不用和上面那样在new了,当然你也可以这样来声明:

`(function () {/* 内部代码 */})();`

不过我们推荐使用第一种方式,关于函数自执行,我后面会有专门一篇文章进行详解,这里就不多说了。

## **引用全局变量**

JavaScript有一个特性叫做隐式全局变量,不管一个变量有没有用过,JavaScript解释器反向遍历作用域链来查找整个变量的var声明,如果没有找到var,解释器则假定该变量是全局变量,如果该变量用于了赋值操作的话,之前如果不存在的话,解释器则会自动创建它,这就是说在匿名闭包里使用或创建全局变量非常容易,不过比较困难的是,代码比较难管理,尤其是阅读代码的人看着很多区分哪些变量是全局的,哪些是局部的。

不过,好在在匿名函数里我们可以提供一个比较简单的替代方案,我们可以将全局变量当成一个参数传入到匿名函数然后使用,相比隐式全局变量,它又清晰又快,我们来看一个例子:

```js
(function ($, YAHOO) {
// 这里,我们的代码就可以使用全局的jQuery对象了,YAHOO也是一样
} (jQuery, YAHOO));
```

现在很多类库里都有这种使用方式,比如jQuery源码。

不过,有时候可能不仅仅要使用全局变量,而是也想声明全局变量,如何做呢?我们可以通过匿名函数的返回值来返回这个全局变量,这也就是一个基本的Module模式,来看一个完整的代码:

```js
var blogModule = (function () {
var my = {}, privateName = "博客园";

function privateAddTopic(data) {
// 这里是内部处理代码
}

my.Name = privateName;
my.AddTopic = function (data) {
privateAddTopic(data);
};

return my;
} ());
```

上面的代码声明了一个全局变量blogModule,并且带有2个可访问的属性:blogModule.AddTopic和blogModule.Name,除此之外,其它代码都在匿名函数的闭包里保持着私有状态。同时根据上面传入全局变量的例子,我们也可以很方便地传入其它的全局变量。

# 高级用法

上面的内容对大多数用户已经很足够了,但我们还可以基于此模式延伸出更强大,易于扩展的结构,让我们一个一个来看。

## **扩展**

Module模式的一个限制就是所有的代码都要写在一个文件,但是在一些大型项目里,将一个功能分离成多个文件是非常重要的,因为可以多人合作易于开发。再回头看看上面的全局参数导入例子,我们能否把blogModule自身传进去呢?答案是肯定的,我们先将blogModule传进去,添加一个函数属性,然后再返回就达到了我们所说的目的,上代码:

```js
var blogModule = (function (my) {
my.AddPhoto = function () {
//添加内部代码
};
return my;
} (blogModule));
```

这段代码,看起来是不是有C#里扩展方法的感觉?有点类似,但本质不一样哦。同时尽管var不是必须的,但为了确保一致,我们再次使用了它,代码执行以后,blogModule下的AddPhoto就可以使用了,同时匿名函数内部的代码也依然保证了私密性和内部状态。

## **松耦合扩展**

上面的代码尽管可以执行,但是必须先声明blogModule,然后再执行上面的扩展代码,也就是说步骤不能乱,怎么解决这个问题呢?我们来回想一下,我们平时声明变量的都是都是这样的:

`var cnblogs = cnblogs || {} ;`

这是确保cnblogs对象,在存在的时候直接用,不存在的时候直接赋值为{},我们来看看如何利用这个特性来实现Module模式的任意加载顺序:

```js
var blogModule = (function (my) {

// 添加一些功能

return my;
} (blogModule || {}));
```

通过这样的代码,每个单独分离的文件都保证这个结构,那么我们就可以实现任意顺序的加载,所以,这个时候的var就是必须要声明的,因为不声明,其它文件读取不到哦。

## **紧耦合扩展**

虽然松耦合扩展很牛叉了,但是可能也会存在一些限制,比如你没办法重写你的一些属性或者函数,也不能在初始化的时候就是用Module的属性。紧耦合扩展限制了加载顺序,但是提供了我们重载的机会,看如下例子:

```js
var blogModule = (function (my) {
var oldAddPhotoMethod = my.AddPhoto;

my.AddPhoto = function () {
// 重载方法,依然可通过oldAddPhotoMethod调用旧的方法
};

return my;
} (blogModule));
```

通过这种方式,我们达到了重载的目的,当然如果你想在继续在内部使用原有的属性,你可以调用oldAddPhotoMethod来用。

## **克隆与继承**

```js
var blogModule = (function (old) {
var my = {},
key;

for (key in old) {
if (old.hasOwnProperty(key)) {
my[key] = old[key];
}
}

var oldAddPhotoMethod = old.AddPhoto;
my.AddPhoto = function () {
// 克隆以后,进行了重写,当然也可以继续调用oldAddPhotoMethod
};

return my;
} (blogModule));
```

这种方式灵活是灵活,但是也需要花费灵活的代价,其实该对象的属性对象或function根本没有被复制,只是对同一个对象多了一种引用而已,所以如果老对象去改变它,那克隆以后的对象所拥有的属性或function函数也会被改变,解决这个问题,我们就得是用递归,但递归对function函数的赋值也不好用,所以我们在递归的时候eval相应的function。不管怎么样,我还是把这一个方式放在这个帖子里了,大家使用的时候注意一下就行了。

## **跨文件共享私有对象**

通过上面的例子,我们知道,如果一个module分割到多个文件的话,每个文件需要保证一样的结构,也就是说每个文件匿名函数里的私有对象都不能交叉访问,那如果我们非要使用,那怎么办呢? 我们先看一段代码:

```js
var blogModule = (function (my) {
var _private = my._private = my._private || {},

_seal = my._seal = my._seal || function () {
delete my._private;
delete my._seal;
delete my._unseal;

},

_unseal = my._unseal = my._unseal || function () {
my._private = _private;
my._seal = _seal;
my._unseal = _unseal;
};

return my;
} (blogModule || {}));
```

任何文件都可以对他们的局部变量_private设属性,并且设置对其他的文件也立即生效。一旦这个模块加载结束,应用会调用 blogModule._seal()"上锁",这会阻止外部接入内部的_private。如果这个模块需要再次增生,应用的生命周期内,任何文件都可以调用_unseal() ”开锁”,然后再加载新文件。加载后再次调用 _seal()”上锁”。

## **子模块**

最后一个也是最简单的使用方式,那就是创建子模块

```js
blogModule.CommentSubModule = (function () {
var my = {};
// ...

return my;
} ());
```

尽管非常简单,我还是把它放进来了,因为我想说明的是子模块也具有一般模块所有的高级使用方式,也就是说你可以对任意子模块再次使用上面的一些应用方法。

# 总结

上面的大部分方式都可以互相组合使用的,一般来说如果要设计系统,可能会用到松耦合扩展,私有状态和子模块这样的方式。另外,我这里没有提到性能问题,但我认为Module模式效率高,代码少,加载速度快。使用松耦合扩展允许并行加载,这更可以提升下载速度。不过初始化时间可能要慢一些,但是为了使用好的模式,这是值得的。

参考文章:

http://yuiblog.com/blog/2007/06/12/module-pattern/
http://www.adequatelygood.com/2010/3/JavaScript-Module-Pattern-In-Depth

分类
JavaScript

深入理解JavaScript系列(2):揭秘命名函数表达式

# 前言

网上还没有发现有人对命名函数表达式进去重复深入的讨论,正因为如此,网上出现了各种各样的误解,本文将从原理和实践两个方面来探讨JavaScript关于命名函数表达式的优缺点。

简单的说,命名函数表达式只有一个用户,那就是在Debug或者Profiler分析的时候来描述函数的名称,也可以使用函数名实现递归,但很快你就会发现其实是不切实际的。当然,如果你不关注调试,那就没什么可担心的了,否则,如果你想了解兼容性方面的东西的话,你还是应该继续往下看看。

我们先开始看看,什么叫函数表达式,然后再说一下现代调试器如何处理这些表达式,如果你已经对这方面很熟悉的话,请直接跳过此小节。

# 函数表达式和函数声明

在ECMAScript中,创建函数的最常用的两个方法是函数表达式和函数声明,两者期间的区别是有点晕,因为ECMA规范只明确了一点:函数声明必须带有标示符(Identifier)(就是大家常说的函数名称),而函数表达式则可以省略这个标示符:

  函数声明:

   function 函数名称 **(**参数:可选**){** 函数体 **}**

  函数表达式:

  function 函数名称(可选)**(**参数:可选**){** 函数体 **}**

所以,可以看出,如果不声明函数名称,它肯定是表达式,可如果声明了函数名称的话,如何判断是函数声明还是函数表达式呢?ECMAScript是通过上下文来区分的,如果function foo(){}是作为赋值表达式的一部分的话,那它就是一个函数表达式,如果function foo(){}被包含在一个函数体内,或者位于程序的最顶部的话,那它就是一个函数声明。

```js
function foo(){} // 声明,因为它是程序的一部分
var bar = function foo(){}; // 表达式,因为它是赋值表达式的一部分

new function bar(){}; // 表达式,因为它是new表达式

(function(){
function bar(){} // 声明,因为它是函数体的一部分
})();
```

还有一种函数表达式不太常见,就是被括号括住的(function foo(){}),他是表达式的原因是因为括号 ()是一个分组操作符,它的内部只能包含表达式,我们来看几个例子:

```js
function foo(){} // 函数声明
(function foo(){}); // 函数表达式:包含在分组操作符内

try {
(var x = 5); // 分组操作符,只能包含表达式而不能包含语句:这里的var就是语句
} catch(err) {
// SyntaxError
}
```

你可以会想到,在使用eval对JSON进行执行的时候,JSON字符串通常被包含在一个圆括号里:eval('(' + json + ')'),这样做的原因就是因为分组操作符,也就是这对括号,会让解析器强制将JSON的花括号解析成表达式而不是代码块。

```js
try {
{ "x": 5 }; // "{" 和 "}" 做解析成代码块
} catch(err) {
// SyntaxError
}

({ "x": 5 }); // 分组操作符强制将"{" 和 "}"作为对象字面量来解析
```

表达式和声明存在着十分微妙的差别,首先,函数声明会在任何表达式被解析和求值之前先被解析和求值,即使你的声明在代码的最后一行,它也会在同作用域内第一个表达式之前被解析/求值,参考如下例子,函数fn是在alert之后声明的,但是在alert执行的时候,fn已经有定义了:

```js
alert(fn());

function fn() {
return 'Hello world!';
}

```

另外,还有一点需要提醒一下,函数声明在条件语句内虽然可以用,但是没有被标准化,也就是说不同的环境可能有不同的执行结果,所以这样情况下,最好使用函数表达式:

```js
// 千万别这样做!
// 因为有的浏览器会返回first的这个function,而有的浏览器返回的却是第二个

if (true) {
function foo() {
return 'first';
}
}
else {
function foo() {
return 'second';
}
}
foo();

// 相反,这样情况,我们要用函数表达式
var foo;
if (true) {
foo = function() {
return 'first';
};
}
else {
foo = function() {
return 'second';
};
}
foo();
```

函数声明的实际规则如下:

*函数声明*只能出现在*程序*或*函数体*内。从句法上讲,它们 不能出现在Block(块)({ ... })中,例如不能出现在 if、while 或 for 语句中。因为 Block(块) 中只能包含Statement语句, 而不能包含*函数声明*这样的源元素。另一方面,仔细看一看规则也会发现,唯一可能让*表达式*出现在Block(块)中情形,就是让它作为*表达式语句*的一部分。但是,规范明确规定了*表达式语句*不能以关键字function开头。而这实际上就是说,*函数表达式*同样也不能出现在Statement语句或Block(块)中(因为Block(块)就是由Statement语句构成的)。

# 函数语句

在ECMAScript的语法扩展中,有一个是函数语句,目前只有基于Gecko的浏览器实现了该扩展,所以对于下面的例子,我们仅是抱着学习的目的来看,一般来说不推荐使用(除非你针对Gecko浏览器进行开发)。

1.一般语句能用的地方,函数语句也能用,当然也包括Block块中:

```js
if (true) {
function f(){ }
}
else {
function f(){ }
}
```

2.函数语句可以像其他语句一样被解析,包含基于条件执行的情形

```js
if (true) {
function foo(){ return 1; }
}
else {
function foo(){ return 2; }
}
foo(); // 1
// 注:其它客户端会将foo解析成函数声明
// 因此,第二个foo会覆盖第一个,结果返回2,而不是1
```

3.函数语句不是在变量初始化期间声明的,而是在运行时声明的——与函数表达式一样。不过,函数语句的标识符一旦声明能在函数的整个作用域生效了。标识符有效性正是导致函数语句与函数表达式不同的关键所在(下一小节我们将会展示命名函数表达式的具体行为)。

```js
// 此刻,foo还没用声明
typeof foo; // "undefined"
if (true) {
// 进入这里以后,foo就被声明在整个作用域内了
function foo(){ return 1; }
}
else {
// 从来不会走到这里,所以这里的foo也不会被声明
function foo(){ return 2; }
}
typeof foo; // "function"
```

不过,我们可以使用下面这样的符合标准的代码来模式上面例子中的函数语句:

```js
var foo;
if (true) {
foo = function foo(){ return 1; };
}
else {
foo = function foo() { return 2; };
}
```

4.函数语句和函数声明(或命名函数表达式)的字符串表示类似,也包括标识符:

```js
if (true) {
function foo(){ return 1; }
}
String(foo); // function foo() { return 1; }
```

5.另外一个,早期基于Gecko的实现(Firefox 3及以前版本)中存在一个bug,即函数语句覆盖函数声明的方式不正确。在这些早期的实现中,函数语句不知何故不能覆盖函数声明:

```js
// 函数声明
function foo(){ return 1; }
if (true) {
// 用函数语句重写
function foo(){ return 2; }
}
foo(); // FF3以下返回1,FF3.5以上返回2

// 不过,如果前面是函数表达式,则没用问题
var foo = function(){ return 1; };
if (true) {
function foo(){ return 2; }
}
foo(); // 所有版本都返回2
```

再次强调一点,上面这些例子只是在某些浏览器支持,所以推荐大家不要使用这些,除非你就在特性的浏览器上做开发。

# 命名函数表达式

函数表达式在实际应用中还是很常见的,在web开发中友个常用的模式是基于对某种特性的测试来伪装函数定义,从而达到性能优化的目的,但由于这种方式都是在同一作用域内,所以基本上一定要用函数表达式:

```js
// 该代码来自Garrett Smith的APE Javascript library库(http://dhtmlkitchen.com/ape/)
var contains = (function() {
var docEl = document.documentElement;

if (typeof docEl.compareDocumentPosition != 'undefined') {
return function(el, b) {
return (el.compareDocumentPosition(b) & 16) !== 0;
};
}
else if (typeof docEl.contains != 'undefined') {
return function(el, b) {
return el !== b && el.contains(b);
};
}
return function(el, b) {
if (el === b) return false;
while (el != b && (b = b.parentNode) != null);
return el === b;
};
})();
```

提到命名函数表达式,理所当然,就是它得有名字,前面的例子var bar = function foo(){};就是一个有效的命名函数表达式,但有一点需要记住:这个名字只在新定义的函数作用域内有效,因为规范规定了标示符不能在外围的作用域内有效:

```js
var f = function foo(){
return typeof foo; // foo是在内部作用域内有效
};
// foo在外部用于是不可见的
typeof foo; // "undefined"
f(); // "function"
```

既然,这么要求,那命名函数表达式到底有啥用啊?为啥要取名?

正如我们开头所说:给它一个名字就是可以让调试过程更方便,因为在调试的时候,如果在调用栈中的每个项都有自己的名字来描述,那么调试过程就太爽了,感受不一样嘛。

# 调试器中的函数名

如果一个函数有名字,那调试器在调试的时候会将它的名字显示在调用的栈上。有些调试器(Firebug)有时候还会为你们函数取名并显示,让他们和那些应用该函数的便利具有相同的角色,可是通常情况下,这些调试器只安装简单的规则来取名,所以说没有太大价格,我们来看一个例子:

```js
function foo(){
return bar();
}
function bar(){
return baz();
}
function baz(){
debugger;
}
foo();

// 这里我们使用了3个带名字的函数声明
// 所以当调试器走到debugger语句的时候,Firebug的调用栈上看起来非常清晰明了
// 因为很明白地显示了名称
baz
bar
foo
expr_test.html()
```

通过查看调用栈的信息,我们可以很明了地知道foo调用了bar, bar又调用了baz(而foo本身有在expr_test.html文档的全局作用域内被调用),不过,还有一个比较爽地方,就是刚才说的Firebug为匿名表达式取名的功能:

```js
function foo(){
return bar();
}
var bar = function(){
return baz();
}
function baz(){
debugger;
}
foo();

// Call stack
baz
bar() //看到了么?
foo
expr_test.html()
```

然后,当函数表达式稍微复杂一些的时候,调试器就不那么聪明了,我们只能在调用栈中看到问号:

```js
function foo(){
return bar();
}
var bar = (function(){
if (window.addEventListener) {
return function(){
return baz();
};
}
else if (window.attachEvent) {
return function() {
return baz();
};
}
})();
function baz(){
debugger;
}
foo();

// Call stack
baz
(?)() // 这里可是问号哦
foo
expr_test.html()
```

另外,当把函数赋值给多个变量的时候,也会出现令人郁闷的问题:

```js
function foo(){
return baz();
}
var bar = function(){
debugger;
};
var baz = bar;
bar = function() {
alert('spoofed');
};
foo();

// Call stack:
bar()
foo
expr_test.html()
```

这时候,调用栈显示的是foo调用了bar,但实际上并非如此,之所以有这种问题,是因为baz和另外一个包含alert('spoofed')的函数做了引用交换所导致的。

归根结底,只有给函数表达式取个名字,才是最委托的办法,也就是使用**命名函数表达式**。我们来使用带名字的表达式来重写上面的例子(注意立即调用的表达式块里返回的2个函数的名字都是bar):

```js
function foo(){
return bar();
}
var bar = (function(){
if (window.addEventListener) {
return function bar(){
return baz();
};
}
else if (window.attachEvent) {
return function bar() {
return baz();
};
}
})();
function baz(){
debugger;
}
foo();

// 又再次看到了清晰的调用栈信息了耶!
baz
bar
foo
expr_test.html()
```

OK,又学了一招吧?不过在高兴之前,我们再看看不同寻常的JScript吧。

# JScript的Bug

比较恶的是,IE的ECMAScript实现JScript严重混淆了命名函数表达式,搞得现很多人都出来反对命名函数表达式,而且即便是最新的一版(IE8中使用的5.8版)仍然存在下列问题。

下面我们就来看看IE在实现中究竟犯了那些错误,俗话说知已知彼,才能百战不殆。我们来看看如下几个例子:

**例1:函数表达式的标示符泄露到外部作用域**

```js
var f = function g(){};
typeof g; // "function"

```

上面我们说过,命名函数表达式的标示符在外部作用域是无效的,但JScript明显是违反了这一规范,上面例子中的标示符g被解析成函数对象,这就乱了套了,很多难以发现的bug都是因为这个原因导致的。

*注:IE9貌似已经修复了这个问题*

**例2:将命名函数表达式同时当作函数声明和函数表达式**

```js
typeof g; // "function"
var f = function g(){};
```

特性环境下,函数声明会优先于任何表达式被解析,上面的例子展示的是JScript实际上是把命名函数表达式当成函数声明了,因为它在实际声明之前就解析了g。

这个例子引出了下一个例子。
**例3:命名函数表达式会创建两个截然不同的函数对象!**

```js
var f = function g(){};
f === g; // false

f.expando = 'foo';
g.expando; // undefined
```

看到这里,大家会觉得问题严重了,因为修改任何一个对象,另外一个没有什么改变,这太恶了。通过这个例子可以发现,创建2个不同的对象,也就是说如果你想修改f的属性中保存某个信息,然后想当然地通过引用相同对象的g的同名属性来使用,那问题就大了,因为根本就不可能。

再来看一个稍微复杂的例子:

**例4:仅仅顺序解析函数声明而忽略条件语句块**

```js
var f = function g() {
return 1;
};
if (false) {
f = function g(){
return 2;
};
}
g(); // 2
```

这个bug查找就难多了,但导致bug的原因却非常简单。首先,g被当作函数声明解析,由于JScript中的函数声明不受条件代码块约束,所以在这个很恶的if分支中,g被当作另一个函数function g(){ return 2 },也就是又被声明了一次。然后,所有“常规的”表达式被求值,而此时f被赋予了另一个新创建的对象的引用。由于在对表达式求值的时候,永远不会进入“这个可恶if分支,因此f就会继续引用第一个函数function g(){ return 1 }。分析到这里,问题就很清楚了:假如你不够细心,在f中调用了g,那么将会调用一个毫不相干的g函数对象。

你可能会文,将不同的对象和arguments.callee相比较时,有什么样的区别呢?我们来看看:

```js
var f = function g(){
return [
arguments.callee == f,
arguments.callee == g
];
};
f(); // [true, false]
g(); // [false, true]
```

可以看到,arguments.callee的引用一直是被调用的函数,实际上这也是好事,稍后会解释。

还有一个有趣的例子,那就是在不包含声明的赋值语句中使用命名函数表达式:

```js
(function(){
f = function f(){};
})();
```

按照代码的分析,我们原本是想创建一个全局属性f(注意不要和一般的匿名函数混淆了,里面用的是带名字的生命),JScript在这里捣乱了一把,首先他把表达式当成函数声明解析了,所以左边的f被声明为局部变量了(和一般的匿名函数里的声明一样),然后在函数执行的时候,f已经是定义过的了,右边的function f(){}则直接就赋值给局部变量f了,所以f根本就不是全局属性。

了解了JScript这么变态以后,我们就要及时预防这些问题了,首先**防范标识符泄漏带外部作用域**,其次,应该永远**不引用被用作函数名称的标识符**;还记得前面例子中那个讨人厌的标识符g吗?——如果我们能够当g不存在,可以避免多少不必要的麻烦哪。因此,关键就在于始终要通过f或者arguments.callee来引用函数。如果你使用了命名函数表达式,那么应该只在调试的时候利用那个名字。最后,还要记住一点,一定要把**命名函数表达式声明期间错误创建的函数清理干净**。

对于,上面最后一点,我们还得再解释一下。

# JScript的内存管理

知道了这些不符合规范的代码解析bug以后,我们如果用它的话,就会发现内存方面其实是有问题的,来看一个例子:

```js
var f = (function(){
if (true) {
return function g(){};
}
return function g(){};
})();
```

我们知道,这个匿名函数调用返回的函数(带有标识符g的函数),然后赋值给了外部的f。我们也知道,命名函数表达式会导致产生多余的函数对象,而该对象与返回的函数对象不是一回事。所以这个多余的g函数就死在了返回函数的闭包中了,因此内存问题就出现了。这是因为if语句内部的函数与g是在同一个作用域中被声明的。这种情况下 ,除非我们显式断开对g函数的引用,否则它一直占着内存不放。

```js
var f = (function(){
var f, g;
if (true) {
f = function g(){};
}
else {
f = function g(){};
}
// 设置g为null以后它就不会再占内存了
g = null;
return f;
})();
```

通过设置g为null,垃圾回收器就把g引用的那个隐式函数给回收掉了,为了验证我们的代码,我们来做一些测试,以确保我们的内存被回收了。

测试

测试很简单,就是命名函数表达式创建10000个函数,然后把它们保存在一个数组中。等一会儿以后再看这些函数到底占用了多少内存。然后,再断开这些引用并重复这一过程。下面是测试代码:

```js
function createFn(){
return (function(){
var f;
if (true) {
f = function F(){
return 'standard';
};
}
else if (false) {
f = function F(){
return 'alternative';
};
}
else {
f = function F(){
return 'fallback';
};
}
// var F = null;
return f;
})();
}

var arr = [ ];
for (var i=0; i
```

通过运行在Windows XP SP2中的任务管理器可以看到如下结果:

IE6:

without `null`: 7.6K -> 20.3K
with `null`: 7.6K -> 18K

IE7:

without `null`: 14K -> 29.7K
with `null`: 14K -> 27K

如我们所料,显示断开引用可以释放内存,但是释放的内存不是很多,10000个函数对象才释放大约3M的内存,这对一些小型脚本不算什么,但对于大型程序,或者长时间运行在低内存的设备里的时候,这是非常有必要的。

关于在Safari 2.x中JS的解析也有一些bug,但介于版本比较低,所以我们在这里就不介绍了,大家如果想看的话,请仔细查看英文资料。

# SpiderMonkey的怪癖

大家都知道,命名函数表达式的标识符只在函数的局部作用域中有效。但包含这个标识符的局部作用域又是什么样子的吗?其实非常简单。在命名函数表达式被求值时,会**创建一个特殊的对象**,该对象的唯一目的就是保存一个属性,而这个属性的名字对应着函数标识符,属性的值对应着那个函数。这个对象会被注入到当前作用域链的前端。然后,被“扩展”的作用域链又被用于初始化函数。

在这里,有一点十分有意思,那就是ECMA-262定义这个(保存函数标识符的)“特殊”对象的方式。标准说**“像调用new Object()表达式那样”**创建这个对象。如果从字面上来理解这句话,那么这个对象就应该是全局`Object`的一个实例。然而,只有一个实现是按照标准字面上的要求这么做的,这个实现就是SpiderMonkey。因此,在SpiderMonkey中,扩展`Object.prototype`有可能会干扰函数的局部作用域:

```js
Object.prototype.x = 'outer';

(function(){

var x = 'inner';

/*函数foo的作用域链中有一个特殊的对象——用于保存函数的标识符。这个特殊的对象实际上就是{ foo: }。
当通过作用域链解析x时,首先解析的是foo的局部环境。如果没有找到x,则继续搜索作用域链中的下一个对象。下一个对象
就是保存函数标识符的那个对象——{ foo: },由于该对象继承自Object.prototype,所以在此可以找到x。
而这个x的值也就是Object.prototype.x的值(outer)。结果,外部函数的作用域(包含x = 'inner'的作用域)就不会被解析了。 */

(function foo(){

alert(x); // 提示框中显示:outer

})();
})();
```

不过,更高版本的SpiderMonkey改变了上述行为,原因可能是认为那是一个安全漏洞。也就是说,“特殊”对象不再继承Object.prototype了。不过,如果你使用Firefox 3或者更低版本,还可以“重温”这种行为。

另一个把内部对象实现为全局Object对象的是黑莓(Blackberry)浏览器。目前,它的*活动对象*(Activation Object)仍然继承Object.prototype。可是,ECMA-262并没有说*活动对象*也要“像调用new Object()表达式那样”来创建(或者说像创建保存NFE标识符的对象一样创建)。 人家规范只说了*活动对象*是规范中的一种机制。

那我们就来看看黑莓里都发生了什么:

```js
Object.prototype.x = 'outer';

(function(){

var x = 'inner';

(function(){

/*在沿着作用域链解析x的过程中,首先会搜索局部函数的活动对象。当然,在该对象中找不到x。
可是,由于活动对象继承自Object.prototype,因此搜索x的下一个目标就是Object.prototype;而
Object.prototype中又确实有x的定义。结果,x的值就被解析为——outer。跟前面的例子差不多,
包含x = 'inner'的外部函数的作用域(活动对象)就不会被解析了。 */

alert(x); // 显示:outer

})();
})();

```

不过神奇的还是,函数中的变量甚至会与已有的`Object.prototype`的成员发生冲突,来看看下面的代码:

```js
(function(){

var constructor = function(){ return 1; };

(function(){

constructor(); // 求值结果是{}(即相当于调用了Object.prototype.constructor())而不是1

constructor === Object.prototype.constructor; // true
toString === Object.prototype.toString; // true

// ……

})();
})();
```

要避免这个问题,要避免使用Object.prototype里的属性名称,如toString, valueOf, hasOwnProperty等等。

JScript解决方案

```js
var fn = (function(){

// 声明要引用函数的变量
var f;

// 有条件地创建命名函数
// 并将其引用赋值给f
if (true) {
f = function F(){ }
}
else if (false) {
f = function F(){ }
}
else {
f = function F(){ }
}

// 声明一个与函数名(标识符)对应的变量,并赋值为null
// 这实际上是给相应标识符引用的函数对象作了一个标记,
// 以便垃圾回收器知道可以回收它了
var F = null;

// 返回根据条件定义的函数
return f;
})();
```

最后我们给出一个应用上述技术的应用实例,这是一个跨浏览器的addEvent函数代码:

```js
// 1) 使用独立的作用域包含声明
var addEvent = (function(){

var docEl = document.documentElement;

// 2) 声明要引用函数的变量
var fn;

if (docEl.addEventListener) {

// 3) 有意给函数一个描述性的标识符
fn = function addEvent(element, eventName, callback) {
element.addEventListener(eventName, callback, false);
}
}
else if (docEl.attachEvent) {
fn = function addEvent(element, eventName, callback) {
element.attachEvent('on' + eventName, callback);
}
}
else {
fn = function addEvent(element, eventName, callback) {
element['on' + eventName] = callback;
}
}

// 4) 清除由JScript创建的addEvent函数
// 一定要保证在赋值前使用var关键字
// 除非函数顶部已经声明了addEvent
var addEvent = null;

// 5) 最后返回由fn引用的函数
return fn;
})();
```

# 替代方案

其实,如果我们不想要这个描述性名字的话,我们就可以用最简单的形式来做,也就是在函数内部声明一个函数(而不是函数表达式),然后返回该函数:

```js
var hasClassName = (function(){

// 定义私有变量
var cache = { };

// 使用函数声明
function hasClassName(element, className) {
var _className = '(?:^|\\s+)' + className + '(?:\\s+|$)';
var re = cache[_className] || (cache[_className] = new RegExp(_className));
return re.test(element.className);
}

// 返回函数
return hasClassName;
})();
```

显然,当存在多个分支函数定义时,这个方案就不行了。不过有种模式貌似可以实现:那就是提前使用函数声明来定义所有函数,并分别为这些函数指定不同的标识符:

```js
var addEvent = (function(){

var docEl = document.documentElement;

function addEventListener(){
/* ... */
}
function attachEvent(){
/* ... */
}
function addEventAsProperty(){
/* ... */
}

if (typeof docEl.addEventListener != 'undefined') {
return addEventListener;
}
elseif (typeof docEl.attachEvent != 'undefined') {
return attachEvent;
}
return addEventAsProperty;
})();
```

虽然这个方案很优雅,但也不是没有缺点。第一,由于使用不同的标识符,导致丧失了命名的一致性。且不说这样好还是坏,最起码它不够清晰。有人喜欢使用相同的名字,但也有人根本不在乎字眼上的差别。可毕竟,不同的名字会让人联想到所用的不同实现。例如,在调试器中看到attachEvent,我们就知 道`addEvent`是基于`attachEvent`的实现。当 然,基于实现来命名的方式也不一定都行得通。假如我们要提供一个API,并按照这种方式把函数命名为inner。那么API用户的很容易就会被相应实现的 细节搞得晕头转向。

要解决这个问题,当然就得想一套更合理的命名方案了。但关键是不要再额外制造麻烦。我现在能想起来的方案大概有如下几个:

```
'addEvent', 'altAddEvent', 'fallbackAddEvent'
// 或者
'addEvent', 'addEvent2', 'addEvent3'
// 或者
'addEvent_addEventListener', 'addEvent_attachEvent', 'addEvent_asProperty'
```

另外,这种模式还存在一个小问题,即增加内存占用。提前创建N个不同名字的函数,等于有N-1的函数是用不到的。具体来讲,如果`document.documentElement` 中包含`attachEvent`,那么`addEventListener` 和`addEventAsProperty`则根本就用不着了。可是,他们都占着内存哪;而且,这些内存将永远都得不到释放,原因跟JScript臭哄哄的命名表达式相同——这两个函数都被“截留”在返回的那个函数的闭包中了。

不过,增加内存占用这个问题确实没什么大不了的。如果某个库——例如Prototype.js——采用了这种模式,无非也就是多创建一两百个函数而已。只要不是(在运行时)重复地创建这些函数,而是只(在加载时)创建一次,那么就没有什么好担心的。

# WebKit的displayName

WebKit团队在这个问题采取了有点儿另类的策略。介于匿名和命名函数如此之差的表现力,WebKit引入了一个“特殊的”`displayName`属性(本质上是一个字符串),如果开发人员为函数的这个属性赋值,则该属性的值将在调试器或性能分析器中被显示在函数“名称”的位置上。[Francisco Tolmasky详细地解释了这个策略的原理和实现](http://www.alertdebugging.com/2009/04/29/building-a-better-javascript-profiler-with-webkit/)。

# 未来考虑

将来的ECMAScript-262第5版(目前还是草案)会引入所谓的**严格模式(strict mode)**。开启严格模式的实现会禁用语言中的那些不稳定、不可靠和不安全的特性。据说出于安全方面的考虑,`arguments.callee`属性将在严格模式下被“封杀”。因此,在处于严格模式时,访问`arguments.callee`会导致`TypeError`(参见ECMA-262第5版的10.6节)。而我之所以在此提到严格模式,是因为如果在基于第5版标准的实现中无法使用`arguments.callee`来执行递归操作,那么使用命名函数表达式的可能性就会大大增加。从这个意义上来说,理解命名函数表达式的语义及其bug也就显得更加重要了。

```js
// 此前,你可能会使用arguments.callee
(function(x) {
if (x return 1;
return x * arguments.callee(x - 1);
})(10);

// 但在严格模式下,有可能就要使用命名函数表达式
(function factorial(x) {
if (x return 1;
return x * factorial(x - 1);
})(10);

// 要么就退一步,使用没有那么灵活的函数声明
function factorial(x) {
if (x return 1;
return x * factorial(x - 1);
}
factorial(10);
```

# 致谢

>***理查德· 康福德(Richard Cornford)***,是他率先[解释了JScript中命名函数表达式所存在的bug](http://groups.google.com/group/comp.lang.javascript/msg/5b508b03b004bce8)。理查德解释了我在这篇文章中提及的大多数bug,所以我强烈建议大家去看看他的解释。我还要感谢***Yann-Erwan Perio***和***道格拉斯·克劳克佛德(Douglas Crockford)***,他们早在2003年就在[comp.lang.javascript论坛中提及并讨论NFE问题了](http://groups.google.com/group/comp.lang.javascript/msg/03d53d114d176323)。

>***约翰-戴维·道尔顿(John-David Dalton)***对“最终解决方案”提出了很好的建议。

>***托比·兰吉***的点子被我用在了“替代方案”中。

>***盖瑞特·史密斯(Garrett Smith)***和***德米特里·苏斯尼科(Dmitry Soshnikov)***对本文的多方面作出了补充和修正。

>英文原文:http://kangax.github.com/nfe/

>参考译文:[连接访问](http://www.cn-cuckoo.com/main/wp-content/uploads/2009/12/named-function-expressions-demystified.html#jscript-memory-management) (SpiderMonkey的怪癖之后的章节参考该文)

分类
JavaScript

深入理解JavaScript系列(1):编写高质量JavaScript代码的基本要点

> 才华横溢的[Stoyan Stefanov](http://www.phpied.com/),在他写的由[O’Reilly](http://oreilly.com/)初版的新书[《JavaScript Patterns》](http://amzn.to/93szK7)(JavaScript模式)中,我想要是为我们的读者贡献其摘要,那会是件很美妙的事情。具体一点就是编写高质量JavaScript的一些要素,例如避免全局变量,使用单变量声明,在循环中预缓存length(长度),遵循代码阅读,以及更多。

>此摘要也包括一些与代码不太相关的习惯,但对整体代码的创建息息相关,包括撰写API文档、执行同行评审以及运行JSLint。这些习惯和最佳做法可以帮助你写出更好的,更易于理解和维护的代码,这些代码在几个月或是几年之后再回过头看看也是会觉得很自豪的。

# 书写可维护的代码(Writing Maintainable Code )

软件bug的修复是昂贵的,并且随着时间的推移,这些bug的成本也会增加,尤其当这些bug潜伏并慢慢出现在已经发布的软件中时。当你发现bug 的时候就立即修复它是最好的,此时你代码要解决的问题在你脑中还是很清晰的。否则,你转移到其他任务,忘了那个特定的代码,一段时间后再去查看这些代码就 需要:

* 花时间学习和理解这个问题
* 化时间是了解应该解决的问题代码

还有问题,特别对于大的项目或是公司,修复bug的这位伙计不是写代码的那个人(且发现bug和修复bug的不是同一个人)。因此,必须降低理解代 码花费的时间,无论是一段时间前你自己写的代码还是团队中的其他成员写的代码。这关系到底线(营业收入)和开发人员的幸福,因为我们更应该去开发新的激动 人心的事物而不是花几小时几天的时间去维护遗留代码。

另一个相关软件开发生命的事实是,读代码花费的时间要比写来得多。有时候,当你专注并深入思考某个问题的时候,你可以坐下来,一个下午写大量的代码。

你的代码很能很快就工作了,但是,随着应用的成熟,还会有很多其他的事情发生,这就要求你的进行进行审查,修改,和调整。例如:

* bug是暴露的
* 新功能被添加到应用程序
* 程序在新的环境下工作(例如,市场上出现新想浏览器)
* 代码改变用途
* 代码得完全从头重新,或移植到另一个架构上或者甚至使用另一种语言

由于这些变化,很少人力数小时写的代码最终演变成花数周来阅读这些代码。这就是为什么创建可维护的代码对应用程序的成功至关重要。

可维护的代码意味着:

* 可读的
* 一致的
* 可预测的
* 看上去就像是同一个人写的
* 已记录

# 最小全局变量(Minimizing Globals)

JavaScript通过函数管理作用域。在函数内部声明的变量只在这个函数内部,函数外面不可用。另一方面,全局变量就是在任何函数外面声明的或是未声明直接简单使用的。

每个JavaScript环境有一个全局对象,当你在任意的函数外面使用this的时候可以访问到。你创建的每一个全部变量都成了这个全局对象的属 性。在浏览器中,方便起见,该全局对象有个附加属性叫做window,此window(通常)指向该全局对象本身。下面的代码片段显示了如何在浏览器环境 中创建和访问的全局变量:

```js
myglobal = "hello"; // 不推荐写法
console.log(myglobal); // "hello"
console.log(window.myglobal); // "hello"
console.log(window["myglobal"]); // "hello"
console.log(this.myglobal); // "hello"
```

# 全局变量的问题

全局变量的问题在于,你的JavaScript应用程序和web页面上的所有代码都共享了这些全局变量,他们住在同一个全局命名空间,所以当程序的两个不同部分定义同名但不同作用的全局变量的时候,命名冲突在所难免。

web页面包含不是该页面开发者所写的代码也是比较常见的,例如:

* 第三方的JavaScript库
* 广告方的脚本代码
* 第三方用户跟踪和分析脚本代码
* 不同类型的小组件,标志和按钮

比方说,该第三方脚本定义了一个全局变量,叫做result;接着,在你的函数中也定义一个名为result的全局变量。其结果就是后面的变量覆盖前面的,第三方脚本就一下子嗝屁啦!

因此,要想和其他脚本成为好邻居的话,尽可能少的使用全局变量是很重要的。在书中后面提到的一些减少全局变量的策略,例如命名空间模式或是函数立即自动执行,但是要想让全局变量少最重要的还是始终使用var来声明变量。

由于JavaScript的两个特征,不自觉地创建出全局变量是出乎意料的容易。首先,你可以甚至不需要声明就可以使用变量;第二,JavaScript有隐含的全局概念,意味着你不声明的任何变量都会成为一个全局对象属性。参考下面的代码:

```js
function sum(x, y) {
// 不推荐写法: 隐式全局变量
result = x + y;
return result;
}
```

此段代码中的`result`没有声明。代码照样运作正常,但在调用函数后你最后的结果就多一个全局命名空间,这可以是一个问题的根源。

经验法则是始终使用var声明变量,正如改进版的sum()函数所演示的:

```js
function sum(x, y) {
var result = x + y;
return result;
}
```

另一个创建隐式全局变量的反例就是使用任务链进行部分var声明。下面的片段中,`a`是本地变量但是`b`确实全局变量,这可能不是你希望发生的:

```js
// 反例,勿使用
function foo() {
var a = b = 0;
// ...
}
```

此现象发生的原因在于这个从右到左的赋值,首先,是赋值表达式`b = 0`,此情况下b是未声明的。这个表达式的返回值是0,然后这个0就分配给了通过var定义的这个局部变量a。换句话说,就好比你输入了:

`var a = (b = 0);`

如果你已经准备好声明变量,使用链分配是比较好的做法,不会产生任何意料之外的全局变量,如:

```js
function foo() {
var a, b;
// ... a = b = 0; // 两个均局部变量
}
```

> 然而,另外一个避免全局变量的原因是可移植性。如果你想你的代码在不同的环境下(主机下)运行,使用全局变量如履薄冰,因为你会无意中覆盖你最初环境下不存在的主机对象(所以你原以为名称可以放心大胆地使用,实际上对于有些情况并不适用)。

# 忘记var的副作用(Side Effects When Forgetting var)

隐式全局变量和明确定义的全局变量间有些小的差异,就是通过`delete`操作符让变量未定义的能力。

* 通过var创建的全局变量(任何函数之外的程序中创建)是不能被删除的。
* 无var创建的隐式全局变量(无视是否在函数中创建)是能被删除的。

这表明,在技术上,隐式全局变量并不是真正的全局变量,但它们是全局对象的属性。属性是可以通过`delete`操作符删除的,而变量是不能的:

```js
// 定义三个全局变量
var global_var = 1;
global_novar = 2; // 反面教材
(function () {
global_fromfunc = 3; // 反面教材
}());

// 试图删除
delete global_var; // false
delete global_novar; // true
delete global_fromfunc; // true

// 测试该删除
typeof global_var; // "number"
typeof global_novar; // "undefined"
typeof global_fromfunc; // "undefined"
```

在ES5严格模式下,未声明的变量(如在前面的代码片段中的两个反面教材)工作时会抛出一个错误。

# 访问全局对象(Access to the Global Object)

在浏览器中,全局对象可以通过`window`属性在代码的任何位置访问(除非你做了些比较出格的事情,像是声明了一个名为window的局部变量)。但是在其他环境下,这个方便的属性可能被叫做其他什么东西(甚至在程序中不可用)。如果你需要在没有硬编码的`window`标识符下访问全局对象,你可以在任何层级的函数作用域中做如下操作:

```js
var global = (function () {
return this;
}());
```

这种方法可以随时获得全局对象,因为其在函数中被当做函数调用了(不是通过`new`构造),`this`总 是指向全局对象。实际上这个病不适用于ECMAScript 5严格模式,所以,在严格模式下时,你必须采取不同的形式。例如,你正在开发一个JavaScript库,你可以将你的代码包裹在一个即时函数中,然后从 全局作用域中,传递一个引用指向this作为你即时函数的参数。

# 单var形式(Single var Pattern)

在函数顶部使用单var语句是比较有用的一种形式,其好处在于:

* 提供了一个单一的地方去寻找功能所需要的所有局部变量
* 防止变量在定义之前使用的逻辑错误
* 帮助你记住声明的全局变量,因此较少了全局变量//zxx:此处我自己是有点晕乎的…
* 少代码(类型啊传值啊单线完成)

单var形式长得就像下面这个样子:

```js
function func() {
var a = 1,
b = 2,
sum = a + b,
myobject = {},
i,
j;
// function body...
}
```

您可以使用一个var语句声明多个变量,并以逗号分隔。像这种初始化变量同时初始化值的做法是很好的。这样子可以防止逻辑错误(所有未初始化但声明的变量的初始值是`undefined`)和增加代码的可读性。在你看到代码后,你可以根据初始化的值知道这些变量大致的用途,例如是要当作对象呢还是当作整数来使。

你也可以在声明的时候做一些实际的工作,例如前面代码中的`sum = a + b`这个情况,另外一个例子就是当你使用DOM(文档对象模型)引用时,你可以使用单一的var把DOM引用一起指定为局部变量,就如下面代码所示的:

```js
function updateElement() {
var el = document.getElementById("result"),
style = el.style;
// 使用el和style干点其他什么事...
}
```

# 预解析:var散布的问题(Hoisting: A Problem with Scattered vars)

JavaScript中,你可以在函数的任何位置声明多个var语句,并且它们就好像是在函数顶部声明一样发挥作用,这种行为称为 hoisting(悬置/置顶解析/预解析)。当你使用了一个变量,然后不久在函数中又重新声明的话,就可能产生逻辑错误。对于JavaScript,只 要你的变量是在同一个作用域中(同一函数),它都被当做是声明的,即使是它在var声明前使用的时候。看下面这个例子:

```js
// 反例
myname = "global"; // 全局变量
function func() {
alert(myname); // "undefined"
var myname = "local";
alert(myname); // "local"
}
func();
```

在这个例子中,你可能会以为第一个alert弹出的是”global”,第二个弹出”loacl”。这种期许是可以理解的,因为在第一个alert 的时候,myname未声明,此时函数肯定很自然而然地看全局变量myname,但是,实际上并不是这么工作的。第一个alert会弹 出”undefined”是因为myname被当做了函数的局部变量(尽管是之后声明的),所有的变量声明当被悬置到函数的顶部了。因此,为了避免这种混 乱,最好是预先声明你想使用的全部变量。

上面的代码片段执行的行为可能就像下面这样:

```js
myname = "global"; // global variable
function func() {
var myname; // 等同于 -> var myname = undefined;
alert(myname); // "undefined"
myname = "local";
alert(myname); // "local"}
func();
```

> 为了完整,我们再提一提执行层面的稍微复杂点的东西。代码处理分两个阶段,第一阶段是变量,函数声明,以及正常格式的参数创建,这是一个解析和进入上下文 的阶段。第二个阶段是代码执行,函数表达式和不合格的标识符(为声明的变量)被创建。但是,出于实用的目的,我们就采用了”hoisting”这个概念, 这种ECMAScript标准中并未定义,通常用来描述行为。

# for循环(for Loops)

在`for`循环中,你可以循环取得数组或是数组类似对象的值,譬如`arguments`和`HTMLCollection`对象。通常的循环形式如下:

```js
// 次佳的循环
for (var i = 0; i // 使用myarray[i]做点什么
}
```

这种形式的循环的不足在于每次循环的时候数组的长度都要去获取下。这回降低你的代码,尤其当`myarray`不是数组,而是一个`HTMLCollection`对象的时候。

`HTMLCollections`指的是DOM方法返回的对象,例如:

```js
document.getElementsByName()
document.getElementsByClassName()
document.getElementsByTagName()
```

还有其他一些`HTMLCollections`,这些是在DOM标准之前引进并且现在还在使用的。有:

```js
document.images: 页面上所有的图片元素
document.links : 所有a标签元素
document.forms : 所有表单
document.forms[0].elements : 页面上第一个表单中的所有域
```

集合的麻烦在于它们实时查询基本文档(HTML页面)。这意味着每次你访问任何集合的长度,你要实时查询DOM,而DOM操作一般都是比较昂贵的。

这就是为什么当你循环获取值时,缓存数组(或集合)的长度是比较好的形式,正如下面代码显示的:

```js
for (var i = 0, max = myarray.length; i // 使用myarray[i]做点什么
}
```

这样,在这个循环过程中,你只检索了一次长度值。

在所有浏览器下,循环获取内容时缓存`HTMLCollections`的长度是更快的,2倍(Safari3)到190倍(IE7)之间。//zxx:此数据貌似很老,仅供参考

注意到,当你明确想要修改循环中的集合的时候(例如,添加更多的DOM元素),你可能更喜欢长度更新而不是常量。

伴随着单var形式,你可以把变量从循环中提出来,就像下面这样:

```js
function looper() {
var i = 0,
max,
myarray = [];
// ...
for (i = 0, max = myarray.length; i // 使用myarray[i]做点什么
}
}
```

这种形式具有一致性的好处,因为你坚持了单一var形式。不足在于当重构代码的时候,复制和粘贴整个循环有点困难。例如,你从一个函数复制了一个循环到另一个函数,你不得不去确定你能够把`i`和`max`引入新的函数(如果在这里没有用的话,很有可能你要从原函数中把它们删掉)。

最后一个需要对循环进行调整的是使用下面表达式之一来替换`i++`。

```js
i = i + 1
i += 1
```

JSLint提示您这样做,原因是`++`和`–-`促进了“过分棘手(excessive trickiness)”。//zxx:这里比较难翻译,我想本意应该是让代码变得更加的棘手
如果你直接无视它,JSLint的`plusplus`选项会是`false`(默认是default)。

还有两种变化的形式,其又有了些微改进,因为:

* 少了一个变量(无max)
* 向下数到0,通常更快,因为和0做比较要比和数组长度或是其他不是0的东西作比较更有效率

```js
//第一种变化的形式:

var i, myarray = [];
for (i = myarray.length; i–-;) {
// 使用myarray[i]做点什么
}

//第二种使用while循环:

var myarray = [],
i = myarray.length;
while (i–-) {
// 使用myarray[i]做点什么
}
```

这些小的改进只体现在性能上,此外JSLint会对使用i–-加以抱怨。

# for-in循环(for-in Loops)

`for-in`循环应该用在非数组对象的遍历上,使用`for-in`进行循环也被称为“枚举”。

从技术上将,你可以使用`for-in`循环数组(因为JavaScript中数组也是对象),但这是不推荐的。因为如果数组对象已被自定义的功能增强,就可能发生逻辑错误。另外,在for-in中,属性列表的顺序(序列)是不能保证的。所以最好数组使用正常的for循环,对象使用for-in循环。

有个很重要的`hasOwnProperty()`方法,当遍历对象属性的时候可以过滤掉从原型链上下来的属性。

思考下面一段代码:

```js
// 对象
var man = {
hands: 2,
legs: 2,
heads: 1
};

// 在代码的某个地方
// 一个方法添加给了所有对象
if (typeof Object.prototype.clone === "undefined") {
Object.prototype.clone = function () {};
}
```

在这个例子中,我们有一个使用对象字面量定义的名叫man的对象。在man定义完成后的某个地方,在对象原型上增加了一个很有用的名叫 clone()的方法。此原型链是实时的,这就意味着所有的对象自动可以访问新的方法。为了避免枚举man的时候出现clone()方法,你需要应用`hasOwnProperty()`方法过滤原型属性。如果不做过滤,会导致clone()函数显示出来,在大多数情况下这是不希望出现的。

```js
// 1.
// for-in 循环
for (var i in man) {
if (man.hasOwnProperty(i)) { // 过滤
console.log(i, ":", man[i]);
}
}
/* 控制台显示结果
hands : 2
legs : 2
heads : 1*/
// 2.
// 反面例子:
// for-in loop without checking hasOwnProperty()
for (var i in man) {
console.log(i, ":", man[i]);
}
/*
控制台显示结果
hands : 2
legs : 2
heads : 1
clone: function()
*/
```

另外一种使用`hasOwnProperty()`的形式是取消Object.prototype上的方法。像是:

```js
for (var i in man) {
if (Object.prototype.hasOwnProperty.call(man, i)) { // 过滤
console.log(i, ":", man[i]);
}
}
```

其好处在于在man对象重新定义hasOwnProperty情况下避免命名冲突。也避免了长属性查找对象的所有方法,你可以使用局部变量“缓存”它。

```js
var i, hasOwn = Object.prototype.hasOwnProperty;
for (i in man) {
if (hasOwn.call(man, i)) { // 过滤
console.log(i, ":", man[i]);
}
}
```

> 严格来说,不使用`hasOwnProperty()`并不是一个错误。根据任务以及你对代码的自信程度,你可以跳过它以提高些许的循环速度。但是当你对当前对象内容(和其原型链)不确定的时候,添加`hasOwnProperty()`更加保险些。

格式化的变化(通不过JSLint)会直接忽略掉花括号,把if语句放到同一行上。其优点在于循环语句读起来就像一个完整的想法(每个元素都有一个自己的属性”X”,使用”X”干点什么):

```js
// 警告: 通不过JSLint检测
var i, hasOwn = Object.prototype.hasOwnProperty;
for (i in man) if (hasOwn.call(man, i)) { // 过滤
console.log(i, ":", man[i]);
}
```

# (不)扩展内置原型((Not) Augmenting Built-in Prototypes)

扩增构造函数的prototype属性是个很强大的增加功能的方法,但有时候它太强大了。

增加内置的构造函数原型(如Object(), Array(), 或Function())挺诱人的,但是这严重降低了可维护性,因为它让你的代码变得难以预测。使用你代码的其他开发人员很可能更期望使用内置的 JavaScript方法来持续不断地工作,而不是你另加的方法。

另外,属性添加到原型中,可能会导致不使用hasOwnProperty属性时在循环中显示出来,这会造成混乱。

因此,不增加内置原型是最好的。你可以指定一个规则,仅当下面的条件均满足时例外:

* 可以预期将来的ECMAScript版本或是JavaScript实现将一直将此功能当作内置方法来实现。例如,你可以添加ECMAScript 5中描述的方法,一直到各个浏览器都迎头赶上。这种情况下,你只是提前定义了有用的方法。
* 如果您检查您的自定义属性或方法已不存在——也许已经在代码的其他地方实现或已经是你支持的浏览器JavaScript引擎部分。
* 你清楚地文档记录并和团队交流了变化。

如果这三个条件得到满足,你可以给原型进行自定义的添加,形式如下:

```js
if (typeof Object.protoype.myMethod !== "function") {
Object.protoype.myMethod = function () {
// 实现...
};
}
```

# switch模式(switch Pattern)

你可以通过类似下面形式的switch语句增强可读性和健壮性:

```js
var inspect_me = 0,
result = '';
switch (inspect_me) {
case 0:
result = "zero";
break;
case 1:
result = "one";
break;
default:
result = "unknown";
}
```

这个简单的例子中所遵循的风格约定如下:

* 每个case和switch对齐(花括号缩进规则除外)
* 每个case中代码缩进
* 每个case以break清除结束
* 避免贯穿(故意忽略break)。如果你非常确信贯穿是最好的方法,务必记录此情况,因为对于有些阅读人而言,它们可能看起来是错误的。
* 以default结束switch:确保总有健全的结果,即使无情况匹配。

# 避免隐式类型转换(Avoiding Implied Typecasting )

JavaScript的变量在比较的时候会隐式类型转换。这就是为什么一些诸如:false == 0 或 “” == 0 返回的结果是true。为避免引起混乱的隐含类型转换,在你比较值和表达式类型的时候始终使用===和!==操作符。

```js
var zero = 0;
if (zero === false) {
// 不执行,因为zero为0, 而不是false
}

// 反面示例
if (zero == false) {
// 执行了...
}
```

还有另外一种思想观点认为==就足够了===是多余的。例如,当你使用typeof你就知道它会返回一个字符串,所以没有使用严格相等的理由。然而,JSLint要求严格相等,它使代码看上去更有一致性,可以降低代码阅读时的精力消耗。(“==是故意的还是一个疏漏?”)

# 避免(Avoiding) eval()

如果你现在的代码中使用了eval(),记住该咒语“eval()是魔鬼”。此方法接受任意的字符串,并当作JavaScript代码来处理。当有 问题的代码是事先知道的(不是运行时确定的),没有理由使用eval()。如果代码是在运行时动态生成,有一个更好的方式不使用eval而达到同样的目 标。例如,用方括号表示法来访问动态属性会更好更简单:

```js
// 反面示例
var property = "name";
alert(eval("obj." + property));

// 更好的
var property = "name";
alert(obj[property]);
```

使用eval()也带来了安全隐患,因为被执行的代码(例如从网络来)可能已被篡改。这是个很常见的反面教材,当处理Ajax请求得到的JSON 相应的时候。在这些情况下,最好使用JavaScript内置方法来解析JSON相应,以确保安全和有效。若浏览器不支持JSON.parse(),你可 以使用来自JSON.org的库。

同样重要的是要记住,给setInterval(), setTimeout()和Function()构造函数传递字符串,大部分情况下,与使用eval()是类似的,因此要避免。在幕后,JavaScript仍需要评估和执行你给程序传递的字符串:

```js
// 反面示例
setTimeout("myFunc()", 1000);
setTimeout("myFunc(1, 2, 3)", 1000);

// 更好的
setTimeout(myFunc, 1000);
setTimeout(function () {
myFunc(1, 2, 3);
}, 1000);
```

使用新的Function()构造就类似于eval(),应小心接近。这可能是一个强大的构造,但往往被误用。如果你绝对必须使用eval(),你 可以考虑使用new Function()代替。有一个小的潜在好处,因为在新Function()中作代码评估是在局部函数作用域中运行,所以代码中任何被评估的通过var 定义的变量都不会自动变成全局变量。另一种方法来阻止自动全局变量是封装eval()调用到一个即时函数中。

考虑下面这个例子,这里仅`un`作为全局变量污染了命名空间。

```js
console.log(typeof un); // "undefined"
console.log(typeof deux); // "undefined"
console.log(typeof trois); // "undefined"

var jsstring = "var un = 1; console.log(un);";
eval(jsstring); // logs "1"

jsstring = "var deux = 2; console.log(deux);";
new Function(jsstring)(); // logs "2"

jsstring = "var trois = 3; console.log(trois);";
(function () {
eval(jsstring);
}()); // logs "3"

console.log(typeof un); // number
console.log(typeof deux); // "undefined"
console.log(typeof trois); // "undefined"
```

另一间eval()和Function构造不同的是eval()可以干扰作用域链,而Function()更安分守己些。不管你在哪里执行 Function(),它只看到全局作用域。所以其能很好的避免本地变量污染。在下面这个例子中,eval()可以访问和修改它外部作用域中的变量,这是 Function做不来的(注意到使用Function和new Function是相同的)。

```js
(function () {
var local = 1;
eval("local = 3; console.log(local)"); // logs "3"
console.log(local); // logs "3"
}());

(function () {
var local = 1;
Function("console.log(typeof local);")(); // logs undefined
}());
```

# parseInt()下的数值转换(Number Conversions with parseInt())

使用parseInt()你可以从字符串中获取数值,该方法接受另一个基数参数,这经常省略,但不应该。当字符串以”0″开头的时候就有可能会出问 题,例如,部分时间进入表单域,在ECMAScript 3中,开头为”0″的字符串被当做8进制处理了,但这已在ECMAScript 5中改变了。为了避免矛盾和意外的结果,总是指定基数参数。

```js
var month = "06",
year = "09";
month = parseInt(month, 10);
year = parseInt(year, 10);
```

此例中,如果你忽略了基数参数,如parseInt(year),返回的值将是0,因为“09”被当做8进制(好比执行 parseInt( year, 8 )),而09在8进制中不是个有效数字。

替换方法是将字符串转换成数字,包括:

```js
+"08" // 结果是 8
Number("08") // 8
```

这些通常快于parseInt(),因为parseInt()方法,顾名思意,不是简单地解析与转换。但是,如果你想输入例如“08 hello”,parseInt()将返回数字,而其它以NaN告终。

# 编码规范(Coding Conventions)

建立和遵循编码规范是很重要的,这让你的代码保持一致性,可预测,更易于阅读和理解。一个新的开发者加入这个团队可以通读规范,理解其它团队成员书写的代码,更快上手干活。

许多激烈的争论发生会议上或是邮件列表上,问题往往针对某些代码规范的特定方面(例如代码缩进,是Tab制表符键还是space空格键)。如果你是 你组织中建议采用规范的,准备好面对各种反对的或是听起来不同但很强烈的观点。要记住,建立和坚定不移地遵循规范要比纠结于规范的细节重要的多。

# 缩进(Indentation)

代码没有缩进基本上就不能读了。唯一糟糕的事情就是不一致的缩进,因为它看上去像是遵循了规范,但是可能一路上伴随着混乱和惊奇。重要的是规范地使用缩进。

一些开发人员更喜欢用tab制表符缩进,因为任何人都可以调整他们的编辑器以自己喜欢的空格数来显示Tab。有些人喜欢空格——通常四个,这都无所谓,只要团队每个人都遵循同一个规范就好了。这本书,例如,使用四个空格缩进,这也是JSLint中默认的缩进。

什么应该缩进呢?规则很简单——花括号里面的东西。这就意味着函数体,循环 (do, while, for, for-in),if,switch,以及对象字面量中的对象属性。下面的代码就是使用缩进的示例:

```js
function outer(a, b) {
var c = 1,
d = 2,
inner;
if (a > b) {
inner = function () {
return {
r: c - d
};
};
} else {
inner = function () {
return {
r: c + d
};
};
}
return inner;
}
```

# 花括号{}(Curly Braces)

花括号(亦称大括号,下同)应总被使用,即使在它们为可选的时候。技术上将,在in或是for中如果语句仅一条,花括号是不需要的,但是你还是应该总是使用它们,这会让代码更有持续性和易于更新。

想象下你有一个只有一条语句的for循环,你可以忽略花括号,而没有解析的错误。

```js
// 糟糕的实例
for (var i = 0; i < 10; i += 1) alert(i); ``` 但是,如果,后来,主体循环部分又增加了行代码? ```js // 糟糕的实例 for (var i = 0; i < 10; i += 1) alert(i); alert(i + " is " + (i % 2 ? "odd" : "even")); ``` 第二个alert已经在循环之外,缩进可能欺骗了你。为了长远打算,最好总是使用花括号,即时值一行代码: ```js // 好的实例 for (var i = 0; i < 10; i += 1) { alert(i); } ``` if条件类似: ```js // 坏 if (true) alert(1); else alert(2); // 好 if (true) { alert(1); } else { alert(2); } ``` # 左花括号的位置(Opening Brace Location) 开发人员对于左大括号的位置有着不同的偏好——在同一行或是下一行。 ```js if (true) { alert("It's TRUE!"); } //或 if (true) { alert("It's TRUE!"); } ``` 这个实例中,仁者见仁智者见智,但也有个案,括号位置不同会有不同的行为表现。这是因为分号插入机制(semicolon insertion mechanism)——JavaScript是不挑剔的,当你选择不使用分号结束一行代码时JavaScript会自己帮你补上。这种行为可能会导致麻 烦,如当你返回对象字面量,而左括号却在下一行的时候: ```js // 警告: 意外的返回值 function func() { return // 下面代码不执行 { name : "Batman" } } ``` 如果你希望函数返回一个含有name属性的对象,你会惊讶。由于隐含分号,函数返回undefined。前面的代码等价于: ```js // 警告: 意外的返回值 function func() { return undefined; // 下面代码不执行 { name : "Batman" } } ``` 总之,总是使用花括号,并始终把在与之前的语句放在同一行: ```js function func() { return { name : "Batman" }; } ``` > 关于分号注:就像使用花括号,你应该总是使用分号,即使他们可由JavaScript解析器隐式创建。这不仅促进更科学和更严格的代码,而且有助于解决存有疑惑的地方,就如前面的例子显示。

# 空格(White Space)

空格的使用同样有助于改善代码的可读性和一致性。在写英文句子的时候,在逗号和句号后面会使用间隔。在JavaScript中,你可以按照同样的逻辑在列表模样表达式(相当于逗号)和结束语句(相对于完成了“想法”)后面添加间隔。

适合使用空格的地方包括:

* for循环分号分开后的的部分:如`for (var i = 0; i < 10; i += 1) {...}` * for循环中初始化的多变量(i和max):`for (var i = 0, max = 10; i < max; i += 1) {...}` * 分隔数组项的逗号的后面:`var a = [1, 2, 3];` * 对象属性逗号的后面以及分隔属性名和属性值的冒号的后面:`var o = {a: 1, b: 2};` * 限定函数参数:`myFunc(a, b, c)` * 函数声明的花括号的前面:`function myFunc() {}` * 匿名函数表达式function的后面:`var myFunc = function () {};` 使用空格分开所有的操作符和操作对象是另一个不错的使用,这意味着在`+, -, *, =, <, >, <=, >=, ===, !==, &&, ||, +=`等前后都需要空格。

```js
// 宽松一致的间距
// 使代码更易读
// 使得更加“透气”
var d = 0,
a = b + 1;
if (a && b && c) {
d = a % c;
a += d;
}

// 反面例子
// 缺失或间距不一
// 使代码变得疑惑
var d = 0,
a = b + 1;
if (a&&b&&c) {
d=a % c;
a+= d;
}

```

最后需要注意的一个空格——花括号间距。最好使用空格:

* 函数、if-else语句、循环、对象字面量的左花括号的前面({)
* else或while之间的右花括号(})

空格使用的一点不足就是增加了文件的大小,但是压缩无此问题。

> 有一个经常被忽略的代码可读性方面是垂直空格的使用。你可以使用空行来分隔代码单元,就像是文学作品中使用段落分隔一样。

### 命名规范(Naming Conventions)

另一种方法让你的代码更具可预测性和可维护性是采用命名规范。这就意味着你需要用同一种形式给你的变量和函数命名。

下面是建议的一些命名规范,你可以原样采用,也可以根据自己的喜好作调整。同样,遵循规范要比规范是什么更重要。

### 以大写字母写构造函数(Capitalizing Constructors)

JavaScript并没有类,但有new调用的构造函数:
`var adam = new Person(); `

因为构造函数仍仅仅是函数,仅看函数名就可以帮助告诉你这应该是一个构造函数还是一个正常的函数。

命名构造函数时首字母大写具有暗示作用,使用小写命名的函数和方法不应该使用new调用:

`function MyConstructor() {...}`
`function myFunction() {...}`

# 分隔单词(Separating Words)

当你的变量或是函数名有多个单词的时候,最好单词的分离遵循统一的规范,有一个常见的做法被称作“驼峰(Camel)命名法”,就是单词小写,每个单词的首字母大写。

对于构造函数,可以使用大驼峰式命名法(upper camel case),如`MyConstructor()`。对于函数和方法名称,你可以使用小驼峰式命名法(lower camel case),像是`myFunction(), calculateArea()`和`getFirstName()`。

要是变量不是函数呢?开发者通常使用小驼峰式命名法,但还有另外一种做法就是所有单词小写以下划线连接:例如,`first_name, favorite_bands,` 和 `old_company_name`,这种标记法帮你直观地区分函数和其他标识——原型和对象。

ECMAScript的属性和方法均使用Camel标记法,尽管多字的属性名称是罕见的(正则表达式对象的lastIndex和ignoreCase属性)。

# 其它命名形式(Other Naming Patterns)

有时,开发人员使用命名规范来弥补或替代语言特性。

例如,JavaScript中没有定义常量的方法(尽管有些内置的像Number, MAX_VALUE),所以开发者都采用全部单词大写的规范来命名这个程序生命周期中都不会改变的变量,如:

```js
// 珍贵常数,只可远观
var PI = 3.14,
MAX_WIDTH = 800;
```
还有另外一个完全大写的惯例:全局变量名字全部大写。全部大写命名全局变量可以加强减小全局变量数量的实践,同时让它们易于区分。

另外一种使用规范来模拟功能的是私有成员。虽然可以在JavaScript中实现真正的私有,但是开发者发现仅仅使用一个下划线前缀来表示一个私有属性或方法会更容易些。考虑下面的例子:
```js
var person = {
getName: function () {
return this._getFirst() + ' ' + this._getLast();
},

_getFirst: function () {
// ...
},
_getLast: function () {
// ...
}
};
```
在此例中,`getName()`就表示公共方法,部分稳定的API。而`_getFirst()`和`_getLast()`则表明了私有。它们仍然是正常的公共方法,但是使用下划线前缀来警告person对象的使用者这些方法在下一个版本中时不能保证工作的,是不能直接使用的。注意,JSLint有些不鸟下划线前缀,除非你设置了noman选项为:false。

下面是一些常见的`_private`规范:

* 使用尾下划线表示私有,如`name_`和`getElements_()`
* 使用一个下划线前缀表`_protected`(保护)属性,两个下划线前缀表示`__private` (私有)属性
* Firefox中一些内置的变量属性不属于该语言的技术部分,使用两个前下划线和两个后下划线表示,如:`__proto__`和`__parent__`。

# 注释(Writing Comments)

你必须注释你的代码,即使不会有其他人向你一样接触它。通常,当你深入研究一个问题,你会很清楚的知道这个代码是干嘛用的,但是,当你一周之后再回来看的时候,想必也要耗掉不少脑细胞去搞明白到底怎么工作的。

很显然,注释不能走极端:每个单独变量或是单独一行。但是,你通常应该记录所有的函数,它们的参数和返回值,或是任何不寻常的技术和方法。要想到注 释可以给你代码未来的阅读者以诸多提示;阅读者需要的是(不要读太多的东西)仅注释和函数属性名来理解你的代码。例如,当你有五六行程序执行特定的任务, 如果你提供了一行代码目的以及为什么在这里的描述的话,阅读者就可以直接跳过这段细节。没有硬性规定注释代码比,代码的某些部分(如正则表达式)可能注释 要比代码多。

> 最重要的习惯,然而也是最难遵守的,就是保持注释的及时更新,因为过时的注释比没有注释更加的误导人。

# 关于作者(About the Author )

Stoyan Stefanov是Yahoo!web开发人员,多个O'Reilly书籍的作者、投稿者和技术评审。他经常在会议和他的博客[www.phpied.com](http://www.zhangxinxu.com/wordpress/2010/10/%e7%bf%bb%e8%af%91-%e9%ab%98%e8%b4%a8%e9%87%8fjavascript%e4%bb%a3%e7%a0%81%e4%b9%a6%e5%86%99%e5%9f%ba%e6%9c%ac%e8%a6%81%e7%82%b9/www.phpied.com)上发表web开发主题的演讲。Stoyan还是smush.it图片优化工具的创造者,YUI贡献者,雅虎性能优化工具YSlow 2.0的架构设计师。

本文转自:http://www.zhangxinxu.com/wordpress/?p=1173

英文原文:http://net.tutsplus.com/tutorials/javascript-ajax/the-essentials-of-writing-high-quality-javascript/

分类
性能优化

前端性能优化

关于 性能优化 是个大的面,这篇文章主要涉及到 前端 的几个点,如 前端性能优化 的流程、常见技术手段、工具等。

提及 前端性能优化 ,大家应该都会想到 雅虎军规,本文会结合 雅虎军规 融入自己的了解知识,进行的总结和梳理

首先,我们先来看看 雅虎军规35 条。

  1. 尽量减少 HTTP 请求个数——须权衡
  2. 使用 CDN(内容分发网络)
  3. 为文件头指定 Expires 或 Cache-Control ,使内容具有缓存性。
  4. 避免空的 src 和 href
  5. 使用 gzip 压缩内容
  6. 把 CSS 放到顶部
  7. 把 JS 放到底部
  8. 避免使用 CSS 表达式
  9. 将 CSS 和 JS 放到外部文件中
  10. 减少 DNS 查找次数
  11. 精简 CSS 和 JS
  12. 避免跳转
  13. 剔除重复的 JS 和 CSS
  14. 配置 ETags
  15. 使 AJAX 可缓存
  16. 尽早刷新输出缓冲
  17. 使用 GET 来完成 AJAX 请求
  18. 延迟加载
  19. 预加载
  20. 减少 DOM 元素个数
  21. 根据域名划分页面内容
  22. 尽量减少 iframe 的个数
  23. 避免 404
  24. 减少 Cookie 的大小
  25. 使用无 cookie 的域
  26. 减少 DOM 访问
  27. 开发智能事件处理程序
  28. <link>代替 @import
  29. 避免使用滤镜
  30. 优化图像
  31. 优化 CSS Spirite
  32. 不要在 HTML 中缩放图像——须权衡
  33. favicon.ico要小而且可缓存
  34. 保持单个内容小于25K
  35. 打包组件成复合文本

如对 雅虎军规 的具体细则内容不是很了解,可自行去各搜索引擎 ,搜索 雅虎军规 了解详情。

压缩 合并

对于 前端性能优化 自然要关注 首屏 打开速度,而这个速度,很大因素是花费在网络请求上,那么怎么减少网络请求的时间呢?

  • 减少网络请求次数
  • 减小文件体积
  • 使用 CDN 加速

所以 压缩、合并 就是一个解决方案,当然可以用 gulpwebpackgrunt 等构建工具 压缩、合并

JS、CSS 压缩 合并

例如:gulp js、css 压缩、合并代码如下

//压缩、合并js
gulp.task('scripts', function () {
    return gulp.src([
        './public/lib/fastclick/lib/fastclick.min.js',
        './public/lib/jquery_lazyload/jquery.lazyload.js',
        './public/lib/velocity/velocity.min.js',
        './public/lib/velocity/velocity.ui.min.js',
        './public/lib/fancybox/source/jquery.fancybox.pack.js',
        './public/js/src/utils.js',
        './public/js/src/motion.js',
        './public/js/src/scrollspy.js',
        './public/js/src/post-details.js',
        './public/js/src/bootstrap.js',
        './public/js/src/push.js',
        './public/live2dw/js/perTips.js',
        './public/live2dw/lib/L2Dwidget.min.js',
        './public/js/src/love.js',
        './public/js/src/busuanzi.pure.mini.js',
        './public/js/src/activate-power-mode.js'
    ]).pipe(concat('all.js')).pipe(minify()).pipe(gulp.dest('./public/dist/'));
});

// 压缩、合并 CSS 
gulp.task('css', function () {
    return gulp.src([
        './public/lib/font-awesome/css/font-awesome.min.css',
        './public/lib/fancybox/source/jquery.fancybox.css',
        './public/css/main.css',
        './public/css/lib.css',
        './public/live2dw/css/perTips.css'
    ]).pipe(concat('all.css')).pipe(minify()).pipe(gulp.dest('./public/dist/'));
});

然后,再把 压缩、合并JS、CSS 放入 CDN , 看看效果如何

如图:* 压缩、合并 且放入 CND 之后的效果 *

首页请求速度(js)

首页请求速度(css)

以上是 lishaoy.net 清除缓存后的 首页 请求速度。

可见,请求时间是 4.59 s ,总请求个数 51 , 而 js 的请求个数是 8css 的请求个数是 3 (其实就 all.css 一个,其它 2 个是 Google浏览器加载的), 而没使用 压缩、合并 时候,请求时间是 10 多秒,总请求个数有 70 多个,js 的请求个数是 20 多个 ,对比请求时间 性能 提升 1倍

如图:有缓存下的首页效果

首页请求速度(缓存)

基本都是秒开

Tips:在 压缩、合并 后,单个文件控制在 25 ~ 30 KB左右,同一个域下,最好不要多于5个资源

图片压缩、合并

例如:gulp 图片压缩代码如下

//压缩image
gulp.task('imagemin', function () {
    gulp.src('./public/**/*.{png,jpg,gif,ico,jpeg}')
        .pipe(imagemin())
        .pipe(gulp.dest('./public'));
});

图片的合并可以采用 CSS Spirite,方法就是把一些小图用 PS 合成一张图,用 css 定位显示每张图片的位置

.top_right .phone {
    background: url(../images/top_right.png) no-repeat 7px -17px;
    padding: 0 38px;
}

.top_right .help {
    background: url(../images/top_right.png) no-repeat 0 -47px;
    padding: 0 38px;
}

然后,把 压缩 的图片放入 CDN , 看看,效果如何

首页请求速度(images)

可见,请求时间是 1.70 s ,总请求个数 50 , 而 img 的请求个数是 15 (这里因为首页都是大图,就没有合并,只是压缩了) ,但是,效果很好 ,从 4.59 s 缩短到 1.70 s, 性能又提升一倍。

再看看有缓存情况如何

首页请求速度(images 缓存)

请求时间是 1.05 s ,有缓存和无缓存基本差不多

Tips:大的图片在不同终端,应该使用不同分辨率,而不应该使用缩放(百分比)

整个 压缩、合并 (js、css、img) 再放入 CDN ,请求时间从 10 多秒 ,到最后的 1.70 s ,性能提升 5 倍多,可见,这个操作必要性。

缓存

缓存会根据请求保存输出内容的副本,例如 页面、图片、文件,当下一个请求来到的时候:如果是相同的URL,缓存直接使 用本地的副本响应访问请求,而不是向源服务器再次发送请求。因此,可以从以下 2 个方面提升性能。

  • 减少相应延迟,提升响应时间
  • 减少网络带宽消耗,节省流量

我们用两幅图来了解下浏览器的 缓存机制

浏览器第一次请求

第一次请求

浏览器再次请求

再次请求

从以上两幅图中,可以清楚的了解浏览器 缓存 的过程。
首次访问一个 URL ,没有 缓存 ,但是,服务器会响应一些 header 信息,如:expires、cache-control、last-modified、etag 等,来记录下次请求是否缓存、如何缓存。
再次访问这个 URL 时候,浏览器会根据首次访问返回的 header 信息,来决策是否缓存、如何缓存。
我们重点来分析下第二幅图,其实是分两条线路,如下

  • 第一条线路: 当浏览器再次访问某个 URL 时,会先获取资源的 header 信息,判断是否命中强缓存 (cache-control和expires) ,如命中,直接从缓存获取资源,包括响应的 header 信息 (请求不会和服务器通信) ,也就是 强缓存 ,如图

强缓存

  • 第二条线路: 如没有命中 强缓存 ,浏览器会发送请求到服务器,请求会携带第一次请求返回的有关缓存的 header 信息 (Last-Modified/If-Modified-Since和Etag/If-None-Match) ,由服务器根据请求中的相关 header 信息来比对结果是否协商缓存命中;若命中,则服务器返回新的响应 header 信息更新缓存中的对应 header 信息,但是并不返回资源内容,它会告知浏览器可以直接从缓存获取;否则返回最新的资源内容,也就是 协商缓存

现在,我们了解到浏览器缓存机制分为 强缓存、协商缓存,再来看看他们的区别

缓存策略获取资源形式状态码发送请求到服务器
强缓存从缓存取200(from memory cache)否,直接从缓存取
协商缓存从缓存取304(not modified)是,通过服务器来告知缓存是否可用

强缓存

与强缓存相关的 header 字段有两个:

expires

expires: 这是 http1.0 时的规范,它的值为一个绝对时间的 GMT 格式的时间字符串,如 Mon, 10 Jun 2015 21:31:12 GMT ,如果发送请求的时间在 expires 之前,那么本地缓存始终有效,否则就会发送请求到服务器来获取资源

cache-control

cache-control: max-age=number ,这是 http1.1 时出现的 header 信息,主要是利用该字段的 max-age 值来进行判断,它是一个相对值;资源第一次的请求时间和 Cache-Control 设定的有效期,计算出一个资源过期时间,再拿这个过期时间跟当前的请求时间比较,如果请求时间在过期时间之前,就能命中缓存,否则未命中, cache-control 除了该字段外,还有下面几个比较常用的设置值:

  • no-cache: 不使用本地缓存。需要使用缓存协商,先与服务器确认返回的响应是否被更改,如果之前的响应中存在 ETag ,那么请求的时候会与服务端验证,如果资源未被更改,则可以避免重新下载。
  • no-store: 直接禁止游览器缓存数据,每次用户请求该资源,都会向服务器发送一个请求,每次都会下载完整的资源。
  • public: 可以被所有的用户缓存,包括终端用户和 CDN 等中间代理服务器。
  • private: 只能被终端用户的浏览器缓存,不允许 CDN 等中继缓存服务器对其缓存。

Tips:如果 cache-control 与 expires 同时存在的话,cache-control 的优先级高于 expires

Last-Modified 或者 Etag ,则后续请求会带上对应的请求字段 If-Modified-Since 或者 If-None-Match ,若响应头没有 Last-Modified 或者 Etag 字段,则请求头也不会有对应的字段。

Last-Modified 或者 Etag ,则后续请求会带上对应的请求字段 If-Modified-Since 或者 If-None-Match ,若响应头没有 Last-Modified 或者 Etag 字段,则请求头也不会有对应的字段。
Last-Modified 或者 Etag ,则后续请求会带上对应的请求字段 If-Modified-Since 或者 If-None-Match ,若响应头没有 Last-Modified 或者 Etag 字段,则请求头也不会有对应的字段。
Last-Modified 或者 Etag ,则后续请求会带上对应的请求字段 If-Modified-Since 或者 If-None-Match ,若响应头没有 Last-Modified 或者 Etag 字段,则请求头也不会有对应的字段。

协商缓存

协商缓存都是由浏览器和服务器协商,来确定是否缓存,协商主要通过下面两组 header 字段,这两组字段都是成对出现的,即第一次请求的响应头带上某个字段 Last-Modified 或者 Etag ,则后续请求会带上对应的请求字段 If-Modified-Since 或者 If-None-Match ,若响应头没有 Last-Modified 或者 Etag 字段,则请求头也不会有对应的字段。

Last-Modified/If-Modified-Since

二者的值都是 GMT 格式的时间字符串,具体过程:

  • 浏览器第一次跟服务器请求一个资源,服务器在返回这个资源的同时,在 responeheader 加上 Last-Modified字段,这个 header 字段表示这个资源在服务器上的最后修改时间

  • 浏览器再次跟服务器请求这个资源时,在 requestheader 上加上 If-Modified-Since 字段,这个 header 字段的值就是上一次请求时返回的 Last-Modified 的值

  • 服务器再次收到资源请求时,根据浏览器传过来 If-Modified-Since 和资源在服务器上的最后修改时间判断资源是否有变化,如果没有变化则返回 304 Not Modified ,但是不会返回资源内容;如果有变化,就正常返回资源内容。当服务器返回 304 Not Modified 的响应时,response header 中不会再添加 Last-Modified的header ,因为既然资源没有变化,那么 Last-Modified 也就不会改变,这是服务器返回 304 时的 response header

  • 浏览器收到 304 的响应后,就会从缓存中加载资源

  • 如果协商缓存没有命中,浏览器直接从服务器加载资源时,Last-ModifiedHeader 在重新加载的时候会被更新,下次请求时,If-Modified-Since 会启用上次返回的Last-Modified

Etag/If-None-Match

这两个值是由服务器生成的每个资源的唯一标识字符串,只要资源有变化就这个值就会改变;其判断过程与 Last-Modified、If-Modified-Since 类似,与 Last-Modified 不一样的是,当服务器返回 304 Not Modified 的响应时,由于 ETag 重新生成过,response header 中还会把这个 ETag 返回,即使这个 ETag 跟之前的没有变化。

Tips:Last-Modified与ETag是可以一起使用的,服务器会优先验证ETag,一致的情况下,才会继续比对Last-Modified,最后才决定是否返回304。

Service Worker

什么是 Service Worker

Service Worker 本质上充当Web应用程序与浏览器之间的代理服务器,也可以在网络可用时作为浏览器和网络间的代理。它们旨在(除其他之外)使得能够创建有效的离线体验,拦截网络请求并基于网络是否可用以及更新的资源是否驻留在服务器上来采取适当的动作。他们还允许访问推送通知和后台同步API。

Service worker 可以解决目前离线应用的问题,同时也可以做更多的事。 Service Worker 可以使你的应用先访问本地缓存资源,所以在离线状态时,在没有通过网络接收到更多的数据前,仍可以提供基本的功能(一般称之为 Offline First)。这是原生APP 本来就支持的功能,这也是相比于 web app ,原生 app 更受青睐的主要原因。

再来看看 service worker 能做些什么:

  • 后台消息传递
  • 网络代理,转发请求,伪造响应
  • 离线缓存
  • 消息推送
  • … …

本文主要以(lishaoy.net)资源缓存为例,阐述下 service worker如何工作

生命周期

service worker 初次安装的生命周期,如图

no-shadow

从上 图可知,service worker 工作的流程:
1. 安装: service worker URL 通过 serviceWorkerContainer.register() 来获取和注册。
2. 激活:service worker 安装完成后,会接收到一个激活事件(activate event)。 onactivate 主要用途是清理先前版本的 service worker 脚本中使用的资源。
3. 监听: 两种状态
- 终止以节省内存;
- 监听获取 fetch 和消息 message 事件。
4. 销毁: 是否销毁由浏览器决定,如果一个 service worker 长期不使用或者机器内存有限,则可能会销毁这个 worker

Tips:激活成功之后,在 Chrome 浏览器里,可以访问 chrome://inspect/#service-workers和 chrome://serviceworker-internals/ 可以查看到当前运行的service worker ,如图 。

service worker

现在,我们来写个简单的例子

注册 service worker

要安装 service worker ,你需要在你的页面上注册它。这个步骤告诉浏览器你的 service worker 脚本在哪里。

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js').then(function(registration) {
    // Registration was successful
    console.log('ServiceWorker registration successful with scope: ',    registration.scope);
  }).catch(function(err) {
    // registration failed :(
    console.log('ServiceWorker registration failed: ', err);
  });
}

上面的代码检查 service worker API 是否可用,如果可用,service worker /sw.js 被注册。如果这个 service worker 已经被注册过,浏览器会自动忽略上面的代码。

激活 service worker

在你的 service worker 注册之后,浏览器会尝试为你的页面或站点安装并激活它。
install 事件会在安装完成之后触发。install 事件一般是被用来填充你的浏览器的离线缓存能力。你需要为 install 事件定义一个 callback ,并决定哪些文件你想要缓存.

// The files we want to cache
var CACHE_NAME = 'my-site-cache-v1';
var urlsToCache = [
  '/',
  '/css/main.css',
  '/js/main.js'
];

self.addEventListener('install', function(event) {
  // Perform install steps
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(function(cache) {
        console.log('Opened cache');
        return cache.addAll(urlsToCache);
      })
  );
});

在我们的 install callback 中,我们需要执行以下步骤:
- 开启一个缓存
- 缓存我们的文件
- 决定是否所有的资源是否要被缓存

上面的代码中,我们通过 caches.open 打开我们指定的 cache 文件名,然后我们调用 cache.addAll 并传入我们的文件数组。这是通过一连串 promise (caches.open 和 cache.addAll) 完成的。event.waitUntil 拿到一个 promise 并使用它来获得安装耗费的时间以及是否安装成功。

监听 service worker

现在我们已经将你的站点资源缓存了,你需要告诉 service worker 让它用这些缓存内容来做点什么。有了 fetch 事件,这是很容易做到的。

每次任何被 service worker 控制的资源被请求到时,都会触发 fetch 事件,我们可以给 service worker 添加一个 fetch 的事件监听器,接着调用 event 上的 respondWith() 方法来劫持我们的 HTTP 响应,然后你用可以用自己的方法来更新他们。

self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request);
  );
});

caches.match(event.request) 允许我们对网络请求的资源和 cache 里可获取的资源进行匹配,查看是否缓存中有相应的资源。这个匹配通过 urlvary header 进行,就像正常的 HTTP 请求一样。

那么,我们如何返回 request 呢,下面 就是一个例子

self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request)
      .then(function(response) {
        // Cache hit - return response
        if (response) {
          return response;
        }

        return fetch(event.request);
      }
    )
  );
});

上面的代码里我们定义了 fetch 事件,在 event.respondWith 里,我们传入了一个由 caches.match 产生的 promise.caches.match 查找 request 中被 service worker 缓存命中的 response
如果我们有一个命中的 response ,我们返回被缓存的值,否则我们返回一个实时从网络请求 fetch 的结果。

sw-toolbox

当然,我也可以使用第三方库,例如:lishaoy.net 使用了 sw-toolbox

sw-toolbox 使用非常简单,下面 就是 lishaoy.net 的一个例子

  "serviceWorker" in navigator ? navigator.serviceWorker.register('/sw.js').then(function () {
    navigator.serviceWorker.controller ? console.log("Assets cached by the controlling service worker.") : console.log("Please reload this page to allow the service worker to handle network operations.")
  }).catch(function (e) {
    console.log("ERROR: " + e)
  }) : console.log("Service workers are not supported in the current browser.")

以上是 注册 一个 service woker

"use strict";
(function () {
    var cacheVersion = "20180527";
    var staticImageCacheName = "image" + cacheVersion;
    var staticAssetsCacheName = "assets" + cacheVersion;
    var contentCacheName = "content" + cacheVersion;
    var vendorCacheName = "vendor" + cacheVersion;
    var maxEntries = 100;
    self.importScripts("/lib/sw-toolbox/sw-toolbox.js");
    self.toolbox.options.debug = false;
    self.toolbox.options.networkTimeoutSeconds = 3;

    self.toolbox.router.get("/images/(.*)", self.toolbox.cacheFirst, {
        cache: {
            name: staticImageCacheName,
            maxEntries: maxEntries
        }
    });

    self.toolbox.router.get('/js/(.*)', self.toolbox.cacheFirst, {
        cache: {
            name: staticAssetsCacheName,
            maxEntries: maxEntries
        }
    });
    self.toolbox.router.get('/css/(.*)', self.toolbox.cacheFirst, {
        cache: {
            name: staticAssetsCacheName,
            maxEntries: maxEntries
        }

    ......

    self.addEventListener("install", function (event) {
        return event.waitUntil(self.skipWaiting())
    });
    self.addEventListener("activate", function (event) {
        return event.waitUntil(self.clients.claim())
    })
})();

就这样搞定了 (具体的用法可以去 sw-toolbox 查看)

有的同学就问,service worker 这么好用,这个缓存空间到底是多大?其实,在 Chrome 可以看到,如图

storage quota

可以看到,大概有 30G ,我的站点只用了 183MB ,完全够用了

最后,来两张图

from ServiceWorker

Cache Storage

由于,文章篇幅过长,后续还会继续总结 架构 方面的优化,例如

  • bigpipe分块输出
  • bigrender分块渲染

以及,渲染 方面的优化,例如

  • requestAnimationFrame
  • well-change
  • 硬件加速 GPU

以及,性能测试工具,例如

  • PageSpeed
  • audits
分类
JavaScript

JavaScript常用工具方法封装

因为工作中经常用到这些方法,所有便把这些方法进行了总结。

JavaScript

1. type 类型判断

isString (o) { //是否字符串
    return Object.prototype.toString.call(o).slice(8, -1) === 'String'
}

isNumber (o) { //是否数字
    return Object.prototype.toString.call(o).slice(8, -1) === 'Number'
}

isBoolean (o) { //是否boolean
    return Object.prototype.toString.call(o).slice(8, -1) === 'Boolean'
}

isFunction (o) { //是否函数
    return Object.prototype.toString.call(o).slice(8, -1) === 'Function'
}

isNull (o) { //是否为null
    return Object.prototype.toString.call(o).slice(8, -1) === 'Null'
}

isUndefined (o) { //是否undefined
    return Object.prototype.toString.call(o).slice(8, -1) === 'Undefined'
}

isObj (o) { //是否对象
    return Object.prototype.toString.call(o).slice(8, -1) === 'Object'
}

isArray (o) { //是否数组
    return Object.prototype.toString.call(o).slice(8, -1) === 'Array'
}

isDate (o) { //是否时间
    return Object.prototype.toString.call(o).slice(8, -1) === 'Date'
}

isRegExp (o) { //是否正则
    return Object.prototype.toString.call(o).slice(8, -1) === 'RegExp'
}

isError (o) { //是否错误对象
    return Object.prototype.toString.call(o).slice(8, -1) === 'Error'
}

isSymbol (o) { //是否Symbol函数
    return Object.prototype.toString.call(o).slice(8, -1) === 'Symbol'
}

isPromise (o) { //是否Promise对象
    return Object.prototype.toString.call(o).slice(8, -1) === 'Promise'
}

isSet (o) { //是否Set对象
    return Object.prototype.toString.call(o).slice(8, -1) === 'Set'
}

isFalse (o) {
    if (!o || o === 'null' || o === 'undefined' || o === 'false' || o === 'NaN') return true
        return false
}

isTrue (o) {
    return !this.isFalse(o)
}

isIos () {
    var u = navigator.userAgent;
    if (u.indexOf('Android') > -1 || u.indexOf('Linux') > -1) {//安卓手机
        // return "Android";
        return false
    } else if (u.indexOf('iPhone') > -1) {//苹果手机
        // return "iPhone";
        return true
    } else if (u.indexOf('iPad') > -1) {//iPad
        // return "iPad";
        return false
    } else if (u.indexOf('Windows Phone') > -1) {//winphone手机
        // return "Windows Phone";
        return false
    }else{
        return false
    }
}

isPC () { //是否为PC端
    var userAgentInfo = navigator.userAgent;
    var Agents = ["Android", "iPhone",
                "SymbianOS", "Windows Phone",
                "iPad", "iPod"];
    var flag = true;
    for (var v = 0; v < Agents.length; v++) {
        if (userAgentInfo.indexOf(Agents[v]) > 0) {
            flag = false;
            break;
        }
    }
    return flag;
}

browserType(){
    var userAgent = navigator.userAgent; //取得浏览器的userAgent字符串
    var isOpera = userAgent.indexOf("Opera") > -1; //判断是否Opera浏览器
    var isIE = userAgent.indexOf("compatible") > -1 && userAgent.indexOf("MSIE") > -1 && !isOpera; //判断是否IE浏览器
    var isIE11 = userAgent.indexOf('Trident') > -1 && userAgent.indexOf("rv:11.0") > -1;
    var isEdge = userAgent.indexOf("Edge") > -1 && !isIE; //判断是否IE的Edge浏览器  
    var isFF = userAgent.indexOf("Firefox") > -1; //判断是否Firefox浏览器
    var isSafari = userAgent.indexOf("Safari") > -1 && userAgent.indexOf("Chrome") == -1; //判断是否Safari浏览器
    var isChrome = userAgent.indexOf("Chrome") > -1 && userAgent.indexOf("Safari") > -1; //判断Chrome浏览器

    if (isIE) {
        var reIE = new RegExp("MSIE (\\d+\\.\\d+);");
        reIE.test(userAgent);
        var fIEVersion = parseFloat(RegExp["$1"]);
        if(fIEVersion == 7) return "IE7"
        else if(fIEVersion == 8) return "IE8";
        else if(fIEVersion == 9) return "IE9";
        else if(fIEVersion == 10) return "IE10";
        else return "IE7以下"//IE版本过低
    }
    if (isIE11) return 'IE11';
    if (isEdge) return "Edge";
    if (isFF) return "FF";
    if (isOpera) return "Opera";
    if (isSafari) return "Safari";
    if (isChrome) return "Chrome";
}

checkStr (str, type) {
    switch (type) {
        case 'phone':   //手机号码
            return /^1[3|4|5|6|7|8|9][0-9]{9}$/.test(str);
        case 'tel':     //座机
            return /^(0\d{2,3}-\d{7,8})(-\d{1,4})?$/.test(str);
        case 'card':    //身份证
            return /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/.test(str);
        case 'pwd':     //密码以字母开头,长度在6~18之间,只能包含字母、数字和下划线
            return /^[a-zA-Z]\w{5,17}$/.test(str)
        case 'postal':  //邮政编码
            return /[1-9]\d{5}(?!\d)/.test(str);
        case 'QQ':      //QQ号
            return /^[1-9][0-9]{4,9}$/.test(str);
        case 'email':   //邮箱
            return /^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$/.test(str);
        case 'money':   //金额(小数点2位)
            return /^\d*(?:\.\d{0,2})?$/.test(str);
        case 'URL':     //网址
            return /(http|ftp|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&:/~\+#]*[\w\-\@?^=%&/~\+#])?/.test(str)
        case 'IP':      //IP
            return /((?:(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d))/.test(str);
        case 'date':    //日期时间
            return /^(\d{4})\-(\d{2})\-(\d{2}) (\d{2})(?:\:\d{2}|:(\d{2}):(\d{2}))$/.test(str) || /^(\d{4})\-(\d{2})\-(\d{2})$/.test(str)
        case 'number':  //数字
            return /^[0-9]$/.test(str);
        case 'english': //英文
            return /^[a-zA-Z]+$/.test(str);
        case 'chinese': //中文
            return /^[\u4E00-\u9FA5]+$/.test(str);
        case 'lower':   //小写
            return /^[a-z]+$/.test(str);
        case 'upper':   //大写
            return /^[A-Z]+$/.test(str);
        case 'HTML':    //HTML标记
            return /<("[^"]*"|'[^']*'|[^'">])*>/.test(str);
        default:
            return true;
    }

    // 严格的身份证校验
    isCardID(sId) {
        if (!/(^\d{15}$)|(^\d{17}(\d|X|x)$)/.test(sId)) {
            alert('你输入的身份证长度或格式错误')
            return false
        }
        //身份证城市
        var aCity={11:"北京",12:"天津",13:"河北",14:"山西",15:"内蒙古",21:"辽宁",22:"吉林",23:"黑龙江",31:"上海",32:"江苏",33:"浙江",34:"安徽",35:"福建",36:"江西",37:"山东",41:"河南",42:"湖北",43:"湖南",44:"广东",45:"广西",46:"海南",50:"重庆",51:"四川",52:"贵州",53:"云南",54:"西藏",61:"陕西",62:"甘肃",63:"青海",64:"宁夏",65:"新疆",71:"台湾",81:"香港",82:"澳门",91:"国外"};
        if(!aCity[parseInt(sId.substr(0,2))]) { 
            alert('你的身份证地区非法')
            return false
        }

        // 出生日期验证
        var sBirthday=(sId.substr(6,4)+"-"+Number(sId.substr(10,2))+"-"+Number(sId.substr(12,2))).replace(/-/g,"/"),
            d = new Date(sBirthday)
        if(sBirthday != (d.getFullYear()+"/"+ (d.getMonth()+1) + "/" + d.getDate())) {
            alert('身份证上的出生日期非法')
            return false
        }

        // 身份证号码校验
        var sum = 0,
            weights =  [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2],
            codes = "10X98765432"
        for (var i = 0; i < sId.length - 1; i++) {
            sum += sId[i] * weights[i];
        }
        var last = codes[sum % 11]; //计算出来的最后一位身份证号码
        if (sId[sId.length-1] != last) { 
            alert('你输入的身份证号非法')
            return false
        }

        return true
    }
}

2. Date

/**
 * 格式化时间
 * 
 * @param  {time} 时间
 * @param  {cFormat} 格式
 * @return {String} 字符串
 *
 * @example formatTime('2018-1-29', '{y}/{m}/{d} {h}:{i}:{s}') // -> 2018/01/29 00:00:00
 */
formatTime(time, cFormat) {
    if (arguments.length === 0) return null
    if ((time + '').length === 10) {
        time = +time * 1000
    }

    var format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}', date
    if (typeof time === 'object') {
        date = time
    } else {
        date = new Date(time)
    }

    var formatObj = {
        y: date.getFullYear(),
        m: date.getMonth() + 1,
        d: date.getDate(),
        h: date.getHours(),
        i: date.getMinutes(),
        s: date.getSeconds(),
        a: date.getDay()
    }
    var time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
        var value = formatObj[key]
        if (key === 'a') return ['一', '二', '三', '四', '五', '六', '日'][value - 1]
        if (result.length > 0 && value < 10) {
            value = '0' + value
        }
        return value || 0
    })
    return time_str
}

/**
 * 返回指定长度的月份集合
 * 
 * @param  {time} 时间
 * @param  {len} 长度
 * @param  {direction} 方向:  1: 前几个月;  2: 后几个月;  3:前后几个月  默认 3
 * @return {Array} 数组
 * 
 * @example   getMonths('2018-1-29', 6, 1)  // ->  ["2018-1", "2017-12", "2017-11", "2017-10", "2017-9", "2017-8", "2017-7"]
 */
getMonths(time, len, direction) {
    var mm = new Date(time).getMonth(),
        yy = new Date(time).getFullYear(),
        direction = isNaN(direction) ? 3 : direction,
        index = mm;
    var cutMonth = function(index) {
        if ( index <= len && index >= -len) {
            return direction === 1 ? formatPre(index).concat(cutMonth(++index)):
                direction === 2 ? formatNext(index).concat(cutMonth(++index)):formatCurr(index).concat(cutMonth(++index))
        }
        return []
    }
    var formatNext = function(i) {
        var y = Math.floor(i/12),
            m = i%12
        return [yy+y + '-' + (m+1)]
    }
    var formatPre = function(i) {
        var y = Math.ceil(i/12),
            m = i%12
        m = m===0 ? 12 : m
        return [yy-y + '-' + (13 - m)]
    }
    var formatCurr = function(i) {
        var y = Math.floor(i/12),
            yNext = Math.ceil(i/12),
            m = i%12,
            mNext = m===0 ? 12 : m
        return [yy-yNext + '-' + (13 - mNext),yy+y + '-' + (m+1)]
    }
    // 数组去重
    var unique = function(arr) {
        if ( Array.hasOwnProperty('from') ) {
            return Array.from(new Set(arr));
        }else{
            var n = {},r=[]; 
            for(var i = 0; i < arr.length; i++){
                if (!n[arr[i]]){
                    n[arr[i]] = true; 
                    r.push(arr[i]);
                }
            }
            return r;
        }
    }
    return direction !== 3 ? cutMonth(index) : unique(cutMonth(index).sort(function(t1, t2){
        return new Date(t1).getTime() - new Date(t2).getTime()
    }))
}

/**
 * 返回指定长度的天数集合
 * 
 * @param  {time} 时间
 * @param  {len} 长度
 * @param  {direction} 方向: 1: 前几天;  2: 后几天;  3:前后几天  默认 3
 * @return {Array} 数组
 *
 * @example date.getDays('2018-1-29', 6) // -> ["2018-1-26", "2018-1-27", "2018-1-28", "2018-1-29", "2018-1-30", "2018-1-31", "2018-2-1"]
 */
getDays(time, len, diretion) {
    var tt = new Date(time)
    var getDay = function(day) {
        var t = new Date(time)
        t.setDate(t.getDate() + day)
        var m = t.getMonth()+1
        return t.getFullYear()+'-'+m+'-'+t.getDate()
    }
    var arr = []
    if (diretion === 1) {
        for (var i = 1; i <= len; i++) {
            arr.unshift(getDay(-i))
        }
    }else if(diretion === 2) {
        for (var i = 1; i <= len; i++) {
            arr.push(getDay(i))
        }
    }else {
        for (var i = 1; i <= len; i++) {
            arr.unshift(getDay(-i))
        }
        arr.push(tt.getFullYear()+'-'+(tt.getMonth()+1)+'-'+tt.getDate())
        for (var i = 1; i <= len; i++) {
            arr.push(getDay(i))
        }
    }
    return diretion === 1 ? arr.concat([tt.getFullYear()+'-'+(tt.getMonth()+1)+'-'+tt.getDate()]) : 
        diretion === 2 ? [tt.getFullYear()+'-'+(tt.getMonth()+1)+'-'+tt.getDate()].concat(arr) : arr
}

/**
 * @param  {s} 秒数
 * @return {String} 字符串 
 *
 * @example formatHMS(3610) // -> 1h0m10s
 */
formatHMS (s) {
    var str = ''
    if (s > 3600) {
        str = Math.floor(s/3600)+'h'+Math.floor(s%3600/60)+'m'+s%60+'s'
    }else if(s > 60) {
        str = Math.floor(s/60)+'m'+s%60+'s'
    }else{
        str = s%60+'s'
    }
    return str
}

/*获取某月有多少天*/
getMonthOfDay (time) {
    var date = new Date(time)
    var year = date.getFullYear()
    var mouth = date.getMonth() + 1
    var days

    //当月份为二月时,根据闰年还是非闰年判断天数
    if (mouth == 2) {
        days = (year%4==0 && year%100==0 && year%400==0) || (year%4==0 && year%100!=0) ? 28 : 29
    } else if (mouth == 1 || mouth == 3 || mouth == 5 || mouth == 7 || mouth == 8 || mouth == 10 || mouth == 12) {
        //月份为:1,3,5,7,8,10,12 时,为大月.则天数为31;
        days = 31
    } else {
        //其他月份,天数为:30.
        days = 30
    }
    return days
}

/*获取某年有多少天*/
getYearOfDay (time) {
    var firstDayYear = this.getFirstDayOfYear(time);
    var lastDayYear = this.getLastDayOfYear(time);
    var numSecond = (new Date(lastDayYear).getTime() - new Date(firstDayYear).getTime())/1000;
    return Math.ceil(numSecond/(24*3600));
}

/*获取某年的第一天*/
getFirstDayOfYear (time) {
    var year = new Date(time).getFullYear();
    return year + "-01-01 00:00:00";
}

/*获取某年最后一天*/
getLastDayOfYear (time) {
    var year = new Date(time).getFullYear();
    var dateString = year + "-12-01 00:00:00";
    var endDay = this.getMonthOfDay(dateString);
    return year + "-12-" + endDay + " 23:59:59";
}

/*获取某个日期是当年中的第几天*/
getDayOfYear (time) {
    var firstDayYear = this.getFirstDayOfYear(time);
    var numSecond = (new Date(time).getTime() - new Date(firstDayYear).getTime())/1000;
    return Math.ceil(numSecond/(24*3600));
}

/*获取某个日期在这一年的第几周*/
getDayOfYearWeek (time) {
    var numdays = this.getDayOfYear(time);
    return Math.ceil(numdays / 7);
}

3. Array

/*判断一个元素是否在数组中*/
contains (arr, val) {
    return arr.indexOf(val) != -1 ? true : false;
}

/**
 * @param  {arr} 数组
 * @param  {fn} 回调函数
 * @return {undefined}
 */
each (arr, fn) {
    fn = fn || Function;
    var a = [];
    var args = Array.prototype.slice.call(arguments, 1);
    for(var i = 0; i < arr.length; i++) {
        var res = fn.apply(arr, [arr[i], i].concat(args));
        if(res != null) a.push(res);
    }
}

/**
 * @param  {arr} 数组
 * @param  {fn} 回调函数
 * @param  {thisObj} this指向
 * @return {Array} 
 */
map (arr, fn, thisObj) {
    var scope = thisObj || window;
    var a = [];
    for(var i = 0, j = arr.length; i < j; ++i) {
        var res = fn.call(scope, arr[i], i, this);
        if(res != null) a.push(res);
    }
    return a;
}

/**
 * @param  {arr} 数组
 * @param  {type} 1:从小到大   2:从大到小   3:随机
 * @return {Array}
 */
sort (arr, type = 1) {
    return arr.sort( (a, b) => {
        switch(type) {
            case 1:
                return a - b;
            case 2:
                return b - a;
            case 3:
                return Math.random() - 0.5;
            default:
                return arr;
        }
    })
}

/*去重*/
unique (arr) {
    if ( Array.hasOwnProperty('from') ) {
        return Array.from(new Set(arr));
    }else{
        var n = {},r=[]; 
        for(var i = 0; i < arr.length; i++){
            if (!n[arr[i]]){
                n[arr[i]] = true; 
                r.push(arr[i]);
            }
        }
        return r;
    }
    // 注:上面 else 里面的排重并不能区分 2 和 '2',但能减少用indexOf带来的性能,暂时没找到替代的方法。。。
    /* 正确排重
    if ( Array.hasOwnProperty('from') ) {
        return Array.from(new Set(arr))
    }else{
        var r = [], NaNBol = true
        for(var i=0; i < arr.length; i++) {
            if (arr[i] !== arr[i]) {
                if (NaNBol && r.indexOf(arr[i]) === -1) {
                    r.push(arr[i])
                    NaNBol = false
                }
            }else{
                if(r.indexOf(arr[i]) === -1) r.push(arr[i])
            }
        }
        return r
    }

     */
}

/*求两个集合的并集*/
union (a, b) {
    var newArr = a.concat(b);
    return this.unique(newArr);
}

/*求两个集合的交集*/
intersect (a, b) {
    var _this = this;
    a = this.unique(a);
    return this.map(a, function(o) {
        return _this.contains(b, o) ? o : null;
    });
}

/*删除其中一个元素*/
remove (arr, ele) {
    var index = arr.indexOf(ele);
    if(index > -1) {
        arr.splice(index, 1);
    }
    return arr;
}

/*将类数组转换为数组的方法*/
formArray (ary) {
    var arr = [];
    if(Array.isArray(ary)) {
        arr = ary;
    } else {
        arr = Array.prototype.slice.call(ary);
    };
    return arr;
}

/*最大值*/
max (arr) {
    return Math.max.apply(null, arr);
}

/*最小值*/
min (arr) {
    return Math.min.apply(null, arr);
}

/*求和*/
sum (arr) {
    return arr.reduce( (pre, cur) => {
        return pre + cur
    })
}

/*平均值*/
average (arr) {
    return this.sum(arr)/arr.length
}

4. String 字符串操作

/**
 * 去除空格
 * @param  {str}
 * @param  {type} 
 *       type:  1-所有空格  2-前后空格  3-前空格 4-后空格
 * @return {String}
 */
trim (str, type) {
    type = type || 1
    switch (type) {
        case 1:
            return str.replace(/\s+/g, "");
        case 2:
            return str.replace(/(^\s*)|(\s*$)/g, "");
        case 3:
            return str.replace(/(^\s*)/g, "");
        case 4:
            return str.replace(/(\s*$)/g, "");
        default:
            return str;
    }
}

/**
 * @param  {str} 
 * @param  {type}
 *       type:  1:首字母大写  2:首页母小写  3:大小写转换  4:全部大写  5:全部小写
 * @return {String}
 */
changeCase (str, type) {
    type = type || 4
    switch (type) {
        case 1:
            return str.replace(/\b\w+\b/g, function (word) {
                return word.substring(0, 1).toUpperCase() + word.substring(1).toLowerCase();

            });
        case 2:
            return str.replace(/\b\w+\b/g, function (word) {
                return word.substring(0, 1).toLowerCase() + word.substring(1).toUpperCase();
            });
        case 3:
            return str.split('').map( function(word){
                if (/[a-z]/.test(word)) {
                    return word.toUpperCase();
                }else{
                    return word.toLowerCase()
                }
            }).join('')
        case 4:
            return str.toUpperCase();
        case 5:
            return str.toLowerCase();
        default:
            return str;
    }
}

/*
    检测密码强度
*/
checkPwd (str) {
    var Lv = 0;
    if (str.length < 6) {
        return Lv
    }
    if (/[0-9]/.test(str)) {
        Lv++
    }
    if (/[a-z]/.test(str)) {
        Lv++
    }
    if (/[A-Z]/.test(str)) {
        Lv++
    }
    if (/[\.|-|_]/.test(str)) {
        Lv++
    }
    return Lv;
}

/*过滤html代码(把<>转换)*/
filterTag (str) {
    str = str.replace(/&/ig, "&");
    str = str.replace(/</ig, "<");
    str = str.replace(/>/ig, ">");
    str = str.replace(" ", "&nbsp;");
    return str;
}

5. Number

/*随机数范围*/
random (min, max) {
    if (arguments.length === 2) {
        return Math.floor(min + Math.random() * ( (max+1) - min ))
    }else{
        return null;
    }

}

/*将阿拉伯数字翻译成中文的大写数字*/
numberToChinese (num) {
    var AA = new Array("零", "一", "二", "三", "四", "五", "六", "七", "八", "九", "十");
    var BB = new Array("", "十", "百", "仟", "萬", "億", "点", "");
    var a = ("" + num).replace(/(^0*)/g, "").split("."),
        k = 0,
        re = "";
    for(var i = a[0].length - 1; i >= 0; i--) {
        switch(k) {
            case 0:
                re = BB[7] + re;
                break;
            case 4:
                if(!new RegExp("0{4}//d{" + (a[0].length - i - 1) + "}$")
                    .test(a[0]))
                    re = BB[4] + re;
                break;
            case 8:
                re = BB[5] + re;
                BB[7] = BB[5];
                k = 0;
                break;
        }
        if(k % 4 == 2 && a[0].charAt(i + 2) != 0 && a[0].charAt(i + 1) == 0)
            re = AA[0] + re;
        if(a[0].charAt(i) != 0)
            re = AA[a[0].charAt(i)] + BB[k % 4] + re;
        k++;
    }

    if(a.length > 1) // 加上小数部分(如果有小数部分)
    {
        re += BB[6];
        for(var i = 0; i < a[1].length; i++)
            re += AA[a[1].charAt(i)];
    }
    if(re == '一十')
        re = "十";
    if(re.match(/^一/) && re.length == 3)
        re = re.replace("一", "");
    return re;
}

/*将数字转换为大写金额*/
changeToChinese (Num) {
        //判断如果传递进来的不是字符的话转换为字符
        if(typeof Num == "number") {
            Num = new String(Num);
        };
        Num = Num.replace(/,/g, "") //替换tomoney()中的“,”
        Num = Num.replace(/ /g, "") //替换tomoney()中的空格
        Num = Num.replace(/¥/g, "") //替换掉可能出现的¥字符
        if(isNaN(Num)) { //验证输入的字符是否为数字
            //alert("请检查小写金额是否正确");
            return "";
        };
        //字符处理完毕后开始转换,采用前后两部分分别转换
        var part = String(Num).split(".");
        var newchar = "";
        //小数点前进行转化
        for(var i = part[0].length - 1; i >= 0; i--) {
            if(part[0].length > 10) {
                return "";
                //若数量超过拾亿单位,提示
            }
            var tmpnewchar = ""
            var perchar = part[0].charAt(i);
            switch(perchar) {
                case "0":
                    tmpnewchar = "零" + tmpnewchar;
                    break;
                case "1":
                    tmpnewchar = "壹" + tmpnewchar;
                    break;
                case "2":
                    tmpnewchar = "贰" + tmpnewchar;
                    break;
                case "3":
                    tmpnewchar = "叁" + tmpnewchar;
                    break;
                case "4":
                    tmpnewchar = "肆" + tmpnewchar;
                    break;
                case "5":
                    tmpnewchar = "伍" + tmpnewchar;
                    break;
                case "6":
                    tmpnewchar = "陆" + tmpnewchar;
                    break;
                case "7":
                    tmpnewchar = "柒" + tmpnewchar;
                    break;
                case "8":
                    tmpnewchar = "捌" + tmpnewchar;
                    break;
                case "9":
                    tmpnewchar = "玖" + tmpnewchar;
                    break;
            }
            switch(part[0].length - i - 1) {
                case 0:
                    tmpnewchar = tmpnewchar + "元";
                    break;
                case 1:
                    if(perchar != 0) tmpnewchar = tmpnewchar + "拾";
                    break;
                case 2:
                    if(perchar != 0) tmpnewchar = tmpnewchar + "佰";
                    break;
                case 3:
                    if(perchar != 0) tmpnewchar = tmpnewchar + "仟";
                    break;
                case 4:
                    tmpnewchar = tmpnewchar + "万";
                    break;
                case 5:
                    if(perchar != 0) tmpnewchar = tmpnewchar + "拾";
                    break;
                case 6:
                    if(perchar != 0) tmpnewchar = tmpnewchar + "佰";
                    break;
                case 7:
                    if(perchar != 0) tmpnewchar = tmpnewchar + "仟";
                    break;
                case 8:
                    tmpnewchar = tmpnewchar + "亿";
                    break;
                case 9:
                    tmpnewchar = tmpnewchar + "拾";
                    break;
            }
            var newchar = tmpnewchar + newchar;
        }
        //小数点之后进行转化
        if(Num.indexOf(".") != -1) {
            if(part[1].length > 2) {
                // alert("小数点之后只能保留两位,系统将自动截断");
                part[1] = part[1].substr(0, 2)
            }
            for(i = 0; i < part[1].length; i++) {
                tmpnewchar = ""
                perchar = part[1].charAt(i)
                switch(perchar) {
                    case "0":
                        tmpnewchar = "零" + tmpnewchar;
                        break;
                    case "1":
                        tmpnewchar = "壹" + tmpnewchar;
                        break;
                    case "2":
                        tmpnewchar = "贰" + tmpnewchar;
                        break;
                    case "3":
                        tmpnewchar = "叁" + tmpnewchar;
                        break;
                    case "4":
                        tmpnewchar = "肆" + tmpnewchar;
                        break;
                    case "5":
                        tmpnewchar = "伍" + tmpnewchar;
                        break;
                    case "6":
                        tmpnewchar = "陆" + tmpnewchar;
                        break;
                    case "7":
                        tmpnewchar = "柒" + tmpnewchar;
                        break;
                    case "8":
                        tmpnewchar = "捌" + tmpnewchar;
                        break;
                    case "9":
                        tmpnewchar = "玖" + tmpnewchar;
                        break;
                }
                if(i == 0) tmpnewchar = tmpnewchar + "角";
                if(i == 1) tmpnewchar = tmpnewchar + "分";
                newchar = newchar + tmpnewchar;
            }
        }
        //替换所有无用汉字
        while(newchar.search("零零") != -1)
            newchar = newchar.replace("零零", "零");
        newchar = newchar.replace("零亿", "亿");
        newchar = newchar.replace("亿万", "亿");
        newchar = newchar.replace("零万", "万");
        newchar = newchar.replace("零元", "元");
        newchar = newchar.replace("零角", "");
        newchar = newchar.replace("零分", "");
        if(newchar.charAt(newchar.length - 1) == "元") {
            newchar = newchar + "整"
        }
        return newchar;
    }

6. Http

/**
 * @param  {setting}
 */
ajax(setting){
    //设置参数的初始值
    var opts={
        method: (setting.method || "GET").toUpperCase(), //请求方式
        url: setting.url || "", // 请求地址
        async: setting.async || true, // 是否异步
        dataType: setting.dataType || "json", // 解析方式
        data: setting.data || "", // 参数
        success: setting.success || function(){}, // 请求成功回调
        error: setting.error || function(){} // 请求失败回调
    }

    // 参数格式化
    function params_format (obj) {
        var str = ''
        for (var i in obj) {
            str += i + '=' + obj[i] + '&'
        }
        return str.split('').slice(0, -1).join('')
    }

    // 创建ajax对象
    var xhr=new XMLHttpRequest();

    // 连接服务器open(方法GET/POST,请求地址, 异步传输)
    if(opts.method == 'GET'){
        xhr.open(opts.method, opts.url + "?" + params_format(opts.data), opts.async);
        xhr.send();
    }else{
        xhr.open(opts.method, opts.url, opts.async);
        xhr.setRequestHeader("Content-Type","application/x-www-form-urlencoded");
        xhr.send(opts.data);
    }

    /*
    ** 每当readyState改变时,就会触发onreadystatechange事件
    ** readyState属性存储有XMLHttpRequest的状态信息
    ** 0 :请求未初始化
    ** 1 :服务器连接已建立
    ** 2 :请求已接受
    ** 3 : 请求处理中
    ** 4 :请求已完成,且相应就绪
    */
    xhr.onreadystatechange = function() {
        if (xhr.readyState === 4 && (xhr.status === 200 || xhr.status === 304)) {
            switch(opts.dataType){
                case "json":
                    var json = JSON.parse(xhr.responseText);
                    opts.success(json);
                    break;
                case "xml":
                    opts.success(xhr.responseXML);
                    break;
                default:
                    opts.success(xhr.responseText);
                    break;
            }
        }
    }

    xhr.onerror = function(err) {
        opts.error(err);
    }
}

/**
 * @param  {url}
 * @param  {setting}
 * @return {Promise}
 */
fetch(url, setting) {
    //设置参数的初始值
    let opts={
        method: (setting.method || 'GET').toUpperCase(), //请求方式
        headers : setting.headers  || {}, // 请求头设置
        credentials : setting.credentials  || true, // 设置cookie是否一起发送
        body: setting.body || {},
        mode : setting.mode  || 'no-cors', // 可以设置 cors, no-cors, same-origin
        redirect : setting.redirect  || 'follow', // follow, error, manual
        cache : setting.cache  || 'default' // 设置 cache 模式 (default, reload, no-cache)
    }
    let dataType = setting.dataType || "json", // 解析方式  
        data = setting.data || "" // 参数

    // 参数格式化
    function params_format (obj) {
        var str = ''
        for (var i in obj) {
            str += `${i}=${obj[i]}&`
        }
        return str.split('').slice(0, -1).join('')
    }

    if (opts.method === 'GET') {
        url = url + (data?`?${params_format(data)}`:'')
    }else{
        setting.body = data || {}
    }

    return new Promise( (resolve, reject) => {
        fetch(url, opts).then( async res => {
            let data = dataType === 'text' ? await res.text() :
                dataType === 'blob' ? await res.blob() : await res.json() 
            resolve(data)
        }).catch( e => {
            reject(e)
        })
    })

}

7. DOM

$ (selector){ 
    var type = selector.substring(0, 1);
    if (type === '#') {
        if (document.querySelecotor) return document.querySelector(selector)
            return document.getElementById(selector.substring(1))

    }else if (type === '.') {
        if (document.querySelecotorAll) return document.querySelectorAll(selector)
            return document.getElementsByClassName(selector.substring(1))
    }else{
        return document['querySelectorAll' ? 'querySelectorAll':'getElementsByTagName'](selector)
    }
} 

/*检测类名*/
hasClass (ele, name) {
    return ele.className.match(new RegExp('(\\s|^)' + name + '(\\s|$)'));
}

/*添加类名*/
addClass (ele, name) {
    if (!this.hasClass(ele, name)) ele.className += " " + name;
}

/*删除类名*/
removeClass (ele, name) {
    if (this.hasClass(ele, name)) {
        var reg = new RegExp('(\\s|^)' + name + '(\\s|$)');
        ele.className = ele.className.replace(reg, '');
    }
}

/*替换类名*/
replaceClass (ele, newName, oldName) {
    this.removeClass(ele, oldName);
    this.addClass(ele, newName);
}

/*获取兄弟节点*/
siblings (ele) {
    console.log(ele.parentNode)
    var chid = ele.parentNode.children,eleMatch = []; 
    for(var i = 0, len = chid.length; i < len; i ++){ 
        if(chid[i] != ele){ 
            eleMatch.push(chid[i]); 
        } 
    } 
    return eleMatch;
}

/*获取行间样式属性*/
getByStyle (obj,name){
    if(obj.currentStyle){
        return  obj.currentStyle[name];
    }else{
        return  getComputedStyle(obj,false)[name];
    }
}

8. Storage 储存操作

class StorageFn {
    constructor () {
        this.ls = window.localStorage;
        this.ss = window.sessionStorage;
    }

    /*-----------------cookie---------------------*/
    /*设置cookie*/
    setCookie (name, value, day) {
        var setting = arguments[0];
        if (Object.prototype.toString.call(setting).slice(8, -1) === 'Object'){
            for (var i in setting) {
                var oDate = new Date();
                oDate.setDate(oDate.getDate() + day);
                document.cookie = i + '=' + setting[i] + ';expires=' + oDate;
            }
        }else{
            var oDate = new Date();
            oDate.setDate(oDate.getDate() + day);
            document.cookie = name + '=' + value + ';expires=' + oDate;
        }

    }

    /*获取cookie*/
    getCookie (name) {
        var arr = document.cookie.split('; ');
        for (var i = 0; i < arr.length; i++) {
            var arr2 = arr[i].split('=');
            if (arr2[0] == name) {
                return arr2[1];
            }
        }
        return '';
    }

    /*删除cookie*/
    removeCookie (name) {
        this.setCookie(name, 1, -1);
    }

    /*-----------------localStorage---------------------*/
    /*设置localStorage*/
    setLocal(key, val) {
        var setting = arguments[0];
        if (Object.prototype.toString.call(setting).slice(8, -1) === 'Object'){
            for(var i in setting){
                this.ls.setItem(i, JSON.stringify(setting[i]))
            }
        }else{
            this.ls.setItem(key, JSON.stringify(val))
        }

    }

    /*获取localStorage*/
    getLocal(key) {
        if (key) return JSON.parse(this.ls.getItem(key))
        return null;

    }

    /*移除localStorage*/
    removeLocal(key) {
        this.ls.removeItem(key)
    }

    /*移除所有localStorage*/
    clearLocal() {
        this.ls.clear()
    }

    /*-----------------sessionStorage---------------------*/
    /*设置sessionStorage*/
    setSession(key, val) {
        var setting = arguments[0];
        if (Object.prototype.toString.call(setting).slice(8, -1) === 'Object'){
            for(var i in setting){
                this.ss.setItem(i, JSON.stringify(setting[i]))
            }
        }else{
            this.ss.setItem(key, JSON.stringify(val))
        }

    }

    /*获取sessionStorage*/
    getSession(key) {
        if (key) return JSON.parse(this.ss.getItem(key))
        return null;

    }

    /*移除sessionStorage*/
    removeSession(key) {
        this.ss.removeItem(key)
    }

    /*移除所有sessionStorage*/
    clearSession() {
        this.ss.clear()
    }

}

9. Other 其它操作

/*获取网址参数*/
getURL(name){
    var reg = new RegExp("(^|&)"+ name +"=([^&]*)(&|$)");
    var r = decodeURI(window.location.search).substr(1).match(reg);
    if(r!=null) return  r[2]; return null;
}

/*获取全部url参数,并转换成json对象*/
getUrlAllParams (url) {
    var url = url ? url : window.location.href;
    var _pa = url.substring(url.indexOf('?') + 1),
        _arrS = _pa.split('&'),
        _rs = {};
    for (var i = 0, _len = _arrS.length; i < _len; i++) {
        var pos = _arrS[i].indexOf('=');
        if (pos == -1) {
            continue;
        }
        var name = _arrS[i].substring(0, pos),
            value = window.decodeURIComponent(_arrS[i].substring(pos + 1));
        _rs[name] = value;
    }
    return _rs;
}

/*删除url指定参数,返回url*/
delParamsUrl(url, name){
    var baseUrl = url.split('?')[0] + '?';
    var query = url.split('?')[1];
    if (query.indexOf(name)>-1) {
        var obj = {}
        var arr = query.split("&");
        for (var i = 0; i < arr.length; i++) {
            arr[i] = arr[i].split("=");
            obj[arr[i][0]] = arr[i][1];
        };
        delete obj[name];
        var url = baseUrl + JSON.stringify(obj).replace(/[\"\{\}]/g,"").replace(/\:/g,"=").replace(/\,/g,"&");
        return url
    }else{
        return url;
    }
}

/*获取十六进制随机颜色*/
getRandomColor () {
    return '#' + (function(h) {
        return new Array(7 - h.length).join("0") + h;
    })((Math.random() * 0x1000000 << 0).toString(16));
}

/*图片加载*/
imgLoadAll(arr,callback){
    var arrImg = []; 
    for (var i = 0; i < arr.length; i++) {
        var img = new Image();
        img.src = arr[i];
        img.onload = function(){
            arrImg.push(this);
            if (arrImg.length == arr.length) {
                callback && callback();
            }
        }
    }
}

/*音频加载*/
loadAudio(src, callback) {
    var audio = new Audio(src);
    audio.onloadedmetadata = callback;
    audio.src = src;
}

/*DOM转字符串*/
domToStirng(htmlDOM){
    var div= document.createElement("div");
    div.appendChild(htmlDOM);
    return div.innerHTML
}

/*字符串转DOM*/
stringToDom(htmlString){
    var div= document.createElement("div");
    div.innerHTML=htmlString;
    return div.children[0];
}

/**
 * 光标所在位置插入字符,并设置光标位置
 * 
 * @param {dom} 输入框
 * @param {val} 插入的值
 * @param {posLen} 光标位置处在 插入的值的哪个位置
 */
setCursorPosition (dom,val,posLen) {
    var cursorPosition = 0;
    if(dom.selectionStart){
        cursorPosition = dom.selectionStart;
    }
    this.insertAtCursor(dom,val);
    dom.focus();
    console.log(posLen)
    dom.setSelectionRange(dom.value.length,cursorPosition + (posLen || val.length));
}

/*光标所在位置插入字符*/
insertAtCursor(dom, val) {
    if (document.selection){
        dom.focus();
        sel = document.selection.createRange();
        sel.text = val;
        sel.select();
    }else if (dom.selectionStart || dom.selectionStart == '0'){
        let startPos = dom.selectionStart;
        let endPos = dom.selectionEnd;
        let restoreTop = dom.scrollTop;
        dom.value = dom.value.substring(0, startPos) + val + dom.value.substring(endPos, dom.value.length);
        if (restoreTop > 0){
            dom.scrollTop = restoreTop;
        }
        dom.focus();
        dom.selectionStart = startPos + val.length;
        dom.selectionEnd = startPos + val.length;
    } else {
        dom.value += val;
        dom.focus();
    }
}

CSS

1. pc-reset PC样式初始化

/* normalize.css */

html {
  line-height: 1.15;
  /* 1 */
  -ms-text-size-adjust: 100%;
  /* 2 */
  -webkit-text-size-adjust: 100%;
  /* 2 */
}

body {
  margin: 0;
}

article,
aside,
footer,
header,
nav,
section {
  display: block;
}

h1 {
  font-size: 2em;
  margin: 0.67em 0;
}

figcaption,
figure,
main {
  /* 1 */
  display: block;
}

figure {
  margin: 1em 40px;
}

hr {
  box-sizing: content-box;
  /* 1 */
  height: 0;
  /* 1 */
  overflow: visible;
  /* 2 */
}

pre {
  font-family: monospace, monospace;
  /* 1 */
  font-size: 1em;
  /* 2 */
}

a {
  background-color: transparent;
  /* 1 */
  -webkit-text-decoration-skip: objects;
  /* 2 */
}

abbr[title] {
  border-bottom: none;
  /* 1 */
  text-decoration: underline;
  /* 2 */
  text-decoration: underline dotted;
  /* 2 */
}

b,
strong {
  font-weight: inherit;
}

b,
strong {
  font-weight: bolder;
}

code,
kbd,
samp {
  font-family: monospace, monospace;
  /* 1 */
  font-size: 1em;
  /* 2 */
}

dfn {
  font-style: italic;
}

mark {
  background-color: #ff0;
  color: #000;
}

small {
  font-size: 80%;
}

sub,
sup {
  font-size: 75%;
  line-height: 0;
  position: relative;
  vertical-align: baseline;
}

sub {
  bottom: -0.25em;
}

sup {
  top: -0.5em;
}

audio,
video {
  display: inline-block;
}

audio:not([controls]) {
  display: none;
  height: 0;
}

img {
  border-style: none;
}

svg:not(:root) {
  overflow: hidden;
}

button,
input,
optgroup,
select,
textarea {
  font-family: sans-serif;
  /* 1 */
  font-size: 100%;
  /* 1 */
  line-height: 1.15;
  /* 1 */
  margin: 0;
  /* 2 */
}

button,
input {
  /* 1 */
  overflow: visible;
}

button,
select {
  /* 1 */
  text-transform: none;
}

button,
html [type="button"],

/* 1 */

[type="reset"],
[type="submit"] {
  -webkit-appearance: button;
  /* 2 */
}

button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
  border-style: none;
  padding: 0;
}

button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
  outline: 1px dotted ButtonText;
}

fieldset {
  padding: 0.35em 0.75em 0.625em;
}

legend {
  box-sizing: border-box;
  /* 1 */
  color: inherit;
  /* 2 */
  display: table;
  /* 1 */
  max-width: 100%;
  /* 1 */
  padding: 0;
  /* 3 */
  white-space: normal;
  /* 1 */
}

progress {
  display: inline-block;
  /* 1 */
  vertical-align: baseline;
  /* 2 */
}

textarea {
  overflow: auto;
}

[type="checkbox"],
[type="radio"] {
  box-sizing: border-box;
  /* 1 */
  padding: 0;
  /* 2 */
}

[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
  height: auto;
}

[type="search"] {
  -webkit-appearance: textfield;
  /* 1 */
  outline-offset: -2px;
  /* 2 */
}

[type="search"]::-webkit-search-cancel-button,
[type="search"]::-webkit-search-decoration {
  -webkit-appearance: none;
}

 ::-webkit-file-upload-button {
  -webkit-appearance: button;
  /* 1 */
  font: inherit;
  /* 2 */
}

details,

/* 1 */

menu {
  display: block;
}

summary {
  display: list-item;
}

canvas {
  display: inline-block;
}

template {
  display: none;
}

[hidden] {
  display: none;
}

/* reset */

html,
body,
h1,
h2,
h3,
h4,
h5,
h6,
div,
dl,
dt,
dd,
ul,
ol,
li,
p,
blockquote,
pre,
hr,
figure,
table,
caption,
th,
td,
form,
fieldset,
legend,
input,
button,
textarea,
menu {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

2. Phone-reset

/* normalize.css */

html {
  line-height: 1.15;
  /* 1 */
  -ms-text-size-adjust: 100%;
  /* 2 */
  -webkit-text-size-adjust: 100%;
  /* 2 */
}

body {
  margin: 0;
}

article,
aside,
footer,
header,
nav,
section {
  display: block;
}

h1 {
  font-size: 2em;
  margin: 0.67em 0;
}

figcaption,
figure,
main {
  /* 1 */
  display: block;
}

figure {
  margin: 1em 40px;
}

hr {
  box-sizing: content-box;
  /* 1 */
  height: 0;
  /* 1 */
  overflow: visible;
  /* 2 */
}

pre {
  font-family: monospace, monospace;
  /* 1 */
  font-size: 1em;
  /* 2 */
}

a {
  background-color: transparent;
  /* 1 */
  -webkit-text-decoration-skip: objects;
  /* 2 */
}

abbr[title] {
  border-bottom: none;
  /* 1 */
  text-decoration: underline;
  /* 2 */
  text-decoration: underline dotted;
  /* 2 */
}

b,
strong {
  font-weight: inherit;
}

b,
strong {
  font-weight: bolder;
}

code,
kbd,
samp {
  font-family: monospace, monospace;
  /* 1 */
  font-size: 1em;
  /* 2 */
}

dfn {
  font-style: italic;
}

mark {
  background-color: #ff0;
  color: #000;
}

small {
  font-size: 80%;
}

sub,
sup {
  font-size: 75%;
  line-height: 0;
  position: relative;
  vertical-align: baseline;
}

sub {
  bottom: -0.25em;
}

sup {
  top: -0.5em;
}

audio,
video {
  display: inline-block;
}

audio:not([controls]) {
  display: none;
  height: 0;
}

img {
  border-style: none;
}

svg:not(:root) {
  overflow: hidden;
}

button,
input,
optgroup,
select,
textarea {
  font-family: sans-serif;
  /* 1 */
  font-size: 100%;
  /* 1 */
  line-height: 1.15;
  /* 1 */
  margin: 0;
  /* 2 */
}

button,
input {
  /* 1 */
  overflow: visible;
}

button,
select {
  /* 1 */
  text-transform: none;
}

button,
html [type="button"],

/* 1 */

[type="reset"],
[type="submit"] {
  -webkit-appearance: button;
  /* 2 */
}

button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
  border-style: none;
  padding: 0;
}

button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
  outline: 1px dotted ButtonText;
}

fieldset {
  padding: 0.35em 0.75em 0.625em;
}

legend {
  box-sizing: border-box;
  /* 1 */
  color: inherit;
  /* 2 */
  display: table;
  /* 1 */
  max-width: 100%;
  /* 1 */
  padding: 0;
  /* 3 */
  white-space: normal;
  /* 1 */
}

progress {
  display: inline-block;
  /* 1 */
  vertical-align: baseline;
  /* 2 */
}

textarea {
  overflow: auto;
}

[type="checkbox"],
[type="radio"] {
  box-sizing: border-box;
  /* 1 */
  padding: 0;
  /* 2 */
}

[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
  height: auto;
}

[type="search"] {
  -webkit-appearance: textfield;
  /* 1 */
  outline-offset: -2px;
  /* 2 */
}

[type="search"]::-webkit-search-cancel-button,
[type="search"]::-webkit-search-decoration {
  -webkit-appearance: none;
}

 ::-webkit-file-upload-button {
  -webkit-appearance: button;
  /* 1 */
  font: inherit;
  /* 2 */
}

details,

/* 1 */

menu {
  display: block;
}

summary {
  display: list-item;
}

canvas {
  display: inline-block;
}

template {
  display: none;
}

[hidden] {
  display: none;
}

/* reset */

html,
body,
h1,
h2,
h3,
h4,
h5,
h6,
div,
dl,
dt,
dd,
ul,
ol,
li,
p,
blockquote,
pre,
hr,
figure,
table,
caption,
th,
td,
form,
fieldset,
legend,
input,
button,
textarea,
menu {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

html,
body {
  /* 禁止选中文本 */
  -webkit-user-select: none;
  user-select: none;
  font: Oswald, 'Open Sans', Helvetica, Arial, sans-serif
}

/* 禁止长按链接与图片弹出菜单 */

a,
img {
  -webkit-touch-callout: none;
}

/*ios android去除自带阴影的样式*/

a,
input {
  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}

input[type="text"] {
  -webkit-appearance: none;
}

3. 公共样式提取

/* 禁止选中文本 */
.usn{
    -webkit-user-select:none;
    -moz-user-select:none;
    -ms-user-select:none;
    -o-user-select:none;
    user-select:none;
}
/* 浮动 */
.fl { float: left; }
.fr { float: right; }
.cf { zoom: 1; }
.cf:after {
    content:".";
    display:block;
    clear:both;
    visibility:hidden;
    height:0;
    overflow:hidden;
}

/* 元素类型 */
.db { display: block; }
.dn { display: none; }
.di { display: inline }
.dib {display: inline-block;}
.transparent { opacity: 0 }

/*文字排版、颜色*/
.f12 { font-size:12px }
.f14 { font-size:14px }
.f16 { font-size:16px }
.f18 { font-size:18px }
.f20 { font-size:20px }
.fb { font-weight:bold }
.fn { font-weight:normal }
.t2 { text-indent:2em }
.red,a.red { color:#cc0031 }
.darkblue,a.darkblue { color:#039 }
.gray,a.gray { color:#878787 }
.lh150 { line-height:150% }
.lh180 { line-height:180% }
.lh200 { line-height:200% }
.unl { text-decoration:underline; }
.no_unl { text-decoration:none; }
.tl { text-align: left; }
.tc { text-align: center; }
.tr { text-align: right; }
.tj { text-align: justify; text-justify: inter-ideograph; }
.wn { /* 强制不换行 */
    word-wrap:normal;
    white-space:nowrap;
}
.wb { /* 强制换行 */
    white-space:normal;
    word-wrap:break-word;
    word-break:break-all;
}
.wp { /* 保持空白序列*/
    overflow:hidden;text-align:left;white-space:pre-wrap;word-wrap:break-word;word-break:break-all;
}
.wes { /* 多出部分用省略号表示 , 用于一行 */
    overflow:hidden;
    word-wrap:normal;
    white-space:nowrap;
    text-overflow:ellipsis;
}
.wes-2 { /* 适用于webkit内核和移动端 */
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    overflow: hidden;
} 
.wes-3 {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 3;
    overflow: hidden;
}
.wes-4 {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 4;
    overflow: hidden;
}

/* 溢出样式 */
.ofh { overflow: hidden; }
.ofs {overflow: scroll; }
.ofa {overflow: auto; }
.ofv {overflow: visible; }

/* 定位方式 */
.ps {position: static; }
.pr {position: relative;zoom:1; }
.pa {position: absolute; }
.pf {position: fixed; }

/* 垂直对齐方式 */
.vt {vertical-align: top; }
.vm {vertical-align: middle; }
.vb {vertical-align: bottom; }

/* 鼠标样式 */
.csd {cursor: default; }
.csp {cursor: pointer; }
.csh {cursor: help; }
.csm {cursor: move; }

/* flex布局 */
.df-sb {
    display:flex;
    align-items: center;
    justify-content: space-between;
}
.df-sa {
    display:flex;
    align-items: center;
    justify-content: space-around;
}

/* 垂直居中 */
.df-c {
    display: flex;
    align-items: center;
    justify-content: center;
}
.tb-c {
    text-align:center;
    display:table-cell;
    vertical-align:middle;
}
.ts-c {
    position: absolute;
    left: 50%; top: 50%;
    transform: translate(-50%, -50%);
}
.ts-mc {
    position: absolute;
    left: 0;right: 0;
    bottom: 0; top: 0;
    margin: auto;
}

/* 辅助 */
.mask-fixed-wrapper {
    width: 100%;
    height: 100%;
    position: fixed;
    left:0;top:0;
    background: rgba(0, 0, 0, 0.65);
    z-index: 999;
}
.bg-cover {
    background-size: cover;
    background-repeat: no-repeat;
    background-position: center center;
}
.bg-cover-all {
    background-size: 100% 100%;
    background-repeat: no-repeat;
    background-position: center center;
}

分类
JavaScript

JavaScript加载时间线

JS执行是单线程,并不是说整个浏览器都是单线程的,姑且就成为单线程吧
JS单线程的原因是为了避免多线程操作dom,引发的并发问题,dom属于基础数据,从多线程上讲,对它的操作要加事物,而js的操作最初就是为了操作dom,嗯,幸好是单线程的,总之一句话,凡是能够修改dom的一定得同步

客户端JS时间线

  1. 创建document对象,开始解析web页面。创建HTMLHtmlElement对象,添加到document中。这个阶段document.readyState = 'loading'
  2. 遇到link外部css,创建线程加载,并继续解析文档。并发
  3. 遇到script外部js,并且没有设置async、defer,浏览器创建线程加载,并阻塞,等待js加载完成并执行该脚本,然后继续解析文档。js拥有修改dom的能力-->domcument.write
  4. 遇到script外部js,并且设置有async、defter,浏览器创建线程加载,并继续解析文档。async属性的脚本,脚本加载完成后立即执行。defter丢置尾部。document.createElement('script')的方式动态插入script元素来模拟async属性,实现脚本异步加载和执行。
  5. 遇到img等,浏览器创建线程加载,并继续解析文档。并发
  6. 当文档解析完成,document.readyState ='interactive'
  7. 文档解析完成后,所有设置有defer的脚本会按照顺序执行。(注意与async的不同)
  8. document对象触发DOMContentLoaded事件,这也标志着程序执行从同步脚本执行阶段,转化为事件驱动阶段。
  9. 当所有async的脚本加载完成并执行后、img等加载完成后,document.readyState = 'complete',window对象触发load事件。
  10. 从此,以异步响应方式处理用户输入、网络事件等。
分类
HTML/CSS

HTML5如何使用SVG

代码优化永远是程序员亘古不变的需求,而合理的利用SVG图片来代替部分PNG/JPG等格式的图片则是前端优化重要的一环,既然是优化,那我们先来看看SVG图片都有哪些优势:

  • SVG 可被非常多的工具读取和修改(比如记事本)
  • SVG 与 JPEG 和 GIF 图像比起来,尺寸更小,且可压缩性更强。
  • SVG 是可伸缩的
  • SVG 图像可在任何的分辨率下被高质量地打印
  • SVG 可在图像质量不下降的情况下被放大
  • SVG 图像中的文本是可选的,同时也是可搜索的(很适合制作地图)
  • SVG 可以与 Java 技术一起运行
  • SVG 是开放的标准
  • SVG 文件是纯粹的 XML

#####几个SVG图片小例子:

#####我们来看一下第三个分享图标的代码:

<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
  <g stroke="#AAB0BA" fill="none" fill-rule="evenodd">
    <path d="M10.524 3.413v8.235" stroke-linejoin="round"/>
    <path d="M13.027 7.508c.813 0 1.678-.01 1.678-.01.449 0 .812.376.812.826l-.005 6.36a.819.819 0 0 1-.811.826H6.31a.822.822 0 0 1-.811-.826l.005-6.36c0-.456.36-.825.812-.825l1.689.006M8.373 5.111l2.143-2.09 2.143 2.07"/>
  </g>
</svg>

不了解SVG的同学现在一定一脸问号,就跟我第一次见他们一样,别着急,我们从基础看起。

什么是SVG?

SVG 是一种基于 XML 语法的图像格式,全称是可缩放矢量图(Scalable Vector Graphics)。其他图像格式都是基于像素处理的,SVG 则是属于对图像的形状描述,所以它本质上是文本文件,体积较小,且不管放大多少倍都不会失真。此外SVG 是万维网联盟的标准,SVG 与诸如 DOM 和 XSL 之类的 W3C 标准是一个整体。

怎么使用?

在 HTML5 中,您能够将 SVG 元素直接嵌入 HTML 页面中,例如上面的那颗小红心:

<body>
  <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="20" height="20" viewBox="0 0 20 20">
    <defs>
      <rect id="a" y="54" width="60" height="25" rx="1"/>
      <mask id="b" x="0" y="0" width="60" height="25" fill="#fff">
        <use xlink:href="#a"/>
    </mask>
    </defs>
    <g transform="translate(-9 -56)" fill="none" fill-rule="evenodd">
      <use stroke="#EDEEEF" mask="url(#b)" stroke-width="2" xlink:href="#a"/>
      <path d="M19.05 62.797c-.208-.268-1.776-2.188-3.629-1.725-.662.165-1.439.44-2.009 1.463-2.18 3.913 4.965 8.983 5.615 9.433V72l.023-.016.023.016v-.032c.65-.45 7.795-5.52 5.615-9.433-.57-1.023-1.347-1.298-2.009-1.463-1.853-.463-3.42 1.457-3.629 1.725z" fill="red"/>
    </g>
  </svg>
</body>

SVG 代码也可以写在一个以.svg结尾的文件中,然后用<img><object><embed><iframe>等标签插入网页。

<img src="search.svg">
<object id="object" data="search.svg" type="image/svg+xml"></object>
<embed id="embed" src="search.svg" type="image/svg+xml">
<iframe id="iframe" src="search.svg"></iframe>

CSS也可以使用svg

.logo {
  background: url(logo.svg);
}

SVG 文件还可以转为 BASE64 编码,然后作为 Data URI 写入网页。

<img src="data:image/svg+xml;base64,[data]">

SVG的语法

1. <svg>标签
SVG 代码都放在顶层标签<svg>之中。下面是一个例子。

<svg width="100%" height="100%">
  <circle id="mycircle" cx="50" cy="50" r="50" />
</svg>

<svg>的width属性和height属性,指定了 SVG 图像在 HTML 元素中所占据的宽度和高度。除了相对单位,也可以采用绝对单位(单位:像素)。如果不指定这两个属性,SVG 图像默认大小是300像素(宽) x 150像素(高)。

如果只想展示 SVG 图像的一部分,就要指定viewBox属性。

<svg width="100" height="100" viewBox="50 50 50 50">
  <circle id="mycircle" cx="50" cy="50" r="50" />
</svg>

<viewBox>属性的值有四个数字,分别是左上角的横坐标和纵坐标、视口的宽度和高度。上面代码中,SVG 图像是100像素宽 x 100像素高,viewBox属性指定视口从(50, 50)这个点开始。所以,实际看到的是右下角的四分之一圆。

注意,视口必须适配所在的空间。上面代码中,视口的大小是 50 x 50,由于 SVG 图像的大小是 100 x 100,所以视口会放大去适配 SVG 图像的大小,即放大了四倍。

如果不指定width属性和height属性,只指定viewBox属性,则相当于只给定 SVG 图像的长宽比。这时,SVG 图像的默认大小将等于所在的 HTML 元素的大小。

2. <circle>标签
<circle>标签代表圆形。

<svg width="300" height="180">
  <circle cx="30"  cy="50" r="25" />
  <circle cx="90"  cy="50" r="25" class="red" />
  <circle cx="150" cy="50" r="25" class="fancy" />
</svg>

上面的代码定义了三个圆。<circle>标签的cx、cy、r属性分别为横坐标、纵坐标和半径,单位为像素。坐标都是相对于<svg>画布的左上角原点。

class属性用来指定对应的 CSS 类。

.red {
  fill: red;
}

.fancy {
  fill: none;
  stroke: black;
  stroke-width: 3pt;
}

SVG 的 CSS 属性与网页元素有所不同。

fill:填充色
stroke:描边色
stroke-width:边框宽度

3. <line>标签
<line>标签用来绘制直线。

<svg width="300" height="180">
  <line x1="0" y1="0" x2="200" y2="0" style="stroke:rgb(0,0,0);stroke-width:5" />
</svg>

上面代码中,标签的x1属性和y1属性,表示线段起点的横坐标和纵坐标;x2属性和y2属性,表示线段终点的横坐标和纵坐标;style属性表示线段的样式。

4. <polyline>标签
<polyline>标签用于绘制一根折线。

<svg width="300" height="180">
  <polyline points="3,3 30,28 3,53" fill="none" stroke="black" />
</svg>

<polyline>的points属性指定了每个端点的坐标,横坐标与纵坐标之间与逗号分隔,点与点之间用空格分隔。

5. <rect>标签
<rect>标签用于绘制矩形。

<svg width="300" height="180">
  <rect x="0" y="0" height="100" width="200" style="stroke: #70d5dd; fill: #dd524b" />
</svg>

<rect>的x属性和y属性,指定了矩形左上角端点的横坐标和纵坐标;width属性和height属性指定了矩形的宽度和高度(单位像素)。

6. <ellipse>标签
<ellipse>标签用于绘制椭圆。

<svg width="300" height="180">
  <ellipse cx="60" cy="60" ry="40" rx="20" stroke="black" stroke-width="5" fill="silver"/>
</svg>

<ellipse>的cx属性和cy属性,指定了椭圆中心的横坐标和纵坐标(单位像素);rx属性和ry属性,指定了椭圆横向轴和纵向轴的半径(单位像素)。

7. <polygon>标签
<polygon>标签用于绘制多边形。

<svg width="300" height="180">
  <polygon fill="green" stroke="orange" stroke-width="1" points="0,0 100,0 100,100 0,100 0,0"/>
</svg>

<polygon>的points属性指定了每个端点的坐标,横坐标与纵坐标之间与逗号分隔,点与点之间用空格分隔。

8. <path>标签
<path>标签用于制路径。

<svg width="300" height="180">
<path d="
  M 18,3
  L 46,3
  L 46,40
  L 61,40
  L 32,68
  L 3,40
  L 18,40
  Z
"></path>
</svg>

<path>的d属性表示绘制顺序,它的值是一个长字符串,每个字母表示一个绘制动作,后面跟着坐标。

M:移动到(moveto)
L:画直线到(lineto)
Z:闭合路径
9. <text>标签
<text>标签用于绘制文本。

<svg width="300" height="180">
  <text x="50" y="25">肆客足球</text>
</svg>

<text>的x属性和y属性,表示文本区块基线(baseline)起点的横坐标和纵坐标。文字的样式可以用class或style属性指定。

10. <use>标签
<use>标签用于复制一个形状。

<svg viewBox="0 0 30 10" xmlns="http://www.w3.org/2000/svg">
  <circle id="myCircle" cx="5" cy="5" r="4"/>

  <use href="#myCircle" x="10" y="0" fill="blue" />
  <use href="#myCircle" x="20" y="0" fill="white" stroke="blue" />
</svg>

<use>的href属性指定所要复制的节点,x属性和y属性是<use>左上角的坐标。另外,还可以指定width和height坐标。

11. <g>标签
<g>标签用于将多个形状组成一个组(group),方便复用。

<svg width="300" height="100">
  <g id="myCircle">
    <text x="25" y="20">圆形</text>
    <circle cx="50" cy="50" r="20"/>
  </g>

  <use href="#myCircle" x="100" y="0" fill="blue" />
  <use href="#myCircle" x="200" y="0" fill="white" stroke="blue" />
</svg>

12. <defs>标签
<defs>标签用于自定义形状,它内部的代码不会显示,仅供引用。

<svg width="300" height="100">
  <defs>
    <g id="myCircle">
      <text x="25" y="20">圆形</text>
      <circle cx="50" cy="50" r="20"/>
    </g>
  </defs>

  <use href="#myCircle" x="0" y="0" />
  <use href="#myCircle" x="100" y="0" fill="blue" />
  <use href="#myCircle" x="200" y="0" fill="white" stroke="blue" />
</svg>

13. <pattern>标签
<pattern>标签用于自定义一个形状,该形状可以被引用来平铺一个区域。

<svg width="500" height="500">
  <defs>
    <pattern id="dots" x="0" y="0" width="100" height="100" patternUnits="userSpaceOnUse">
      <circle fill="#bee9e8" cx="50" cy="50" r="35" />
    </pattern>
  </defs>
  <rect x="0" y="0" width="100%" height="100%" fill="url(#dots)" />
</svg>

上面代码中,<pattern>标签将一个圆形定义为dots模式。patternUnits="userSpaceOnUse"表示<pattern>的宽度和长度是实际的像素值。然后,指定这个模式去填充下面的矩形。

14. <image>标签
<image>标签用于插入图片文件。

<svg viewBox="0 0 100 100" width="100" height="100">
  <image xlink:href="path/to/image.jpg"
    width="50%" height="50%"/>
</svg>

上面代码中,<image>xlink:href属性表示图像的来源。

15. <animate>标签
<animate>标签用于产生动画效果。

<svg width="500px" height="500px">
  <rect x="0" y="0" width="100" height="100" fill="#feac5e">
    <animate attributeName="x" from="0" to="500" dur="2s" repeatCount="indefinite" />
  </rect>
</svg>

上面代码中,矩形会不断移动,产生动画效果。

<animate>的属性含义如下。

attributeName:发生动画效果的属性名。
from:单次动画的初始值。
to:单次动画的结束值。
dur:单次动画的持续时间。
repeatCount:动画的循环模式。
可以在多个属性上面定义动画。

<animate attributeName="x" from="0" to="500" dur="2s" repeatCount="indefinite" />
<animate attributeName="width" to="500" dur="2s" repeatCount="indefinite" />

16. <animateTransform>标签
<animate>标签对 CSS 的transform属性不起作用,如果需要变形,就要使用标签。

<svg width="500px" height="500px">
  <rect x="250" y="250" width="50" height="50" fill="#4bc0c8">
    <animateTransform attributeName="transform" type="rotate" begin="0s" dur="10s" from="0 200 200" to="360 400 400" repeatCount="indefinite" />
  </rect>
</svg>

上面代码中,<animateTransform>的效果为旋转(rotate),这时from和to属性值有三个数字,第一个数字是角度值,第二个值和第三个值是旋转中心的坐标。from="0 200 200"表示开始时,角度为0,围绕(200, 200)开始旋转;to="360 400 400"表示结束时,角度为360,围绕(400, 400)旋转。

JavaScript 操作SVG

1. DOM操作
如果 SVG 代码直接写在 HTML 网页之中,它就成为网页 DOM 的一部分,可以直接用 DOM 操作。

<svg
  id="mysvg"
  xmlns="http://www.w3.org/2000/svg"
  viewBox="0 0 800 600"
  preserveAspectRatio="xMidYMid meet"
>
  <circle id="mycircle" cx="400" cy="300" r="50" />
<svg>

上面代码插入网页之后,就可以用 CSS 定制样式。

circle {
  stroke-width: 5;
  stroke: #f00;
  fill: #ff0;
}

circle:hover {
  stroke: #090;
  fill: #f8f8f8;
}

然后,可以用 JavaScript 代码操作 SVG。

var mycircle = document.getElementById('mycircle');

mycircle.addEventListener('click', function(e) {
  console.log('circle clicked - enlarging');
  mycircle.setAttribute('r', 60);
}, false);

上面代码指定,如果点击图形,就改写circle元素的r属性。

2. 获取 SVG DOM
使用<object><iframe><embed>标签插入 SVG 文件,可以获取 SVG DOM。

var svgObject = document.getElementById('object').contentDocument;
var svgIframe = document.getElementById('iframe').contentDocument;
var svgEmbed = document.getElementById('embed').getSVGDocument();

注意,如果使用标签插入 SVG 文件,就无法获取 SVG DOM。

3. 读取 SVG 源码
由于 SVG 文件就是一段 XML 文本,因此可以通过读取 XML 代码的方式,读取 SVG 源码。

<div id="svg-container">
  <svg
    xmlns="http://www.w3.org/2000/svg"
    xmlns:xlink="http://www.w3.org/1999/xlink"
    xml:space="preserve" width="500" height="440"
  >
    <!-- svg code -->
  </svg>
</div>

使用XMLSerializer实例的serializeToString()方法,获取 SVG 元素的代码。

var svgString = new XMLSerializer()
  .serializeToString(document.querySelector('svg'));

4. SVG 图像转为 Canvas 图像
首先,需要新建一个Image对象,将 SVG 图像指定到该Image对象的src属性。

var img = new Image();
var svg = new Blob([svgString], {type: "image/svg+xml;charset=utf-8"});

var DOMURL = self.URL || self.webkitURL || self;
var url = DOMURL.createObjectURL(svg);

img.src = url;

然后,当图像加载完成后,再将它绘制到<canvas>元素。

img.onload = function () {
  var canvas = document.getElementById('canvas');
  var ctx = canvas.getContext('2d');
  ctx.drawImage(img, 0, 0);
};

小结

SVG能做的远不止这些,利用SVG做的动画效果,文字效果我们以后给大家详细讲解,今天就先到这里吧。

console.log('右下角点好看呦')

###技术放肆聊QQ群:617413307 欢迎程序员朋友积极加群,共同进步
###技术放肆聊公众号,每日干货,最前沿的技术知识,扫描下方二维码关注:
技术放肆聊
####推一下自家APP
肆客足球 最新最全的足球资讯,最火爆的球迷社区,直播中超、欧洲赛事,全面丰富的数据统计,还有球星推特、Ins、社交动态,原汁原味,扫描下方二维码即可下载

分类
JavaScript

ES6十大常用特性

[toc]

以下是ES6排名前十的最佳特性列表(排名不分先后):

  • Default Parameters(默认参数) in ES6
  • Template Literals (模板文本)in ES6
  • Multi-line Strings (多行字符串)in ES6
  • Destructuring Assignment (解构赋值)in ES6
  • Enhanced Object Literals (增强的对象文本)in ES6
  • Arrow Functions (箭头函数)in ES6
  • Promises in ES6
  • Block-Scoped Constructs Let and Const(块作用域构造Let and Const)
  • Classes(类) in ES6
  • Modules(模块) in ES6

【备注 】这里只列出了10条比较常用的特性。并不是所有的浏览器都支持ES6模块,所以你需要使用一些像jspm去支持ES6模块。

1.Default Parameters(默认参数)

ES5:

var link = function (height, color, url) {  
    var height = height || 50;  
    var color = color || 'red';  
    var url = url || 'http://azat.co';  
    ...  
} 

ES6:直接写在参数里

var link = function(height = 50, color = 'red', url = 'http://azat.co') {  
  ...  
}

好处 节省了代码量。

2.Template Literals(模板对象)

在字符串里面输出变量

ES5:

var name = 'Your name is ' + first + ' ' + last + '.';  
var url = 'http://localhost:3000/api/messages/' + id;  

ES6:,使用新的语法 $ {NAME},并把它放在反引号里:

var name = 'Your name is ${first} ${last}.';
var url = 'http://loalhost:3000/api/messages/${id}';

好处: 这里的$ {NAME}直接当做字符串用,无需写加号

3.Multi-line Strings (多行字符串)

ES5:

var roadPoem = 'Then took the other, as just as fair,nt'  
    + 'And having perhaps the better claimnt'  
    + 'Because it was grassy and wanted wear,nt'  
    + 'Though as for that the passing therent'  
    + 'Had worn them really about the same,nt';  
var fourAgreements = 'You have the right to be you.n  
    You can only be you when you do your best.'; 

ES6: 反引号就可以啦!

var roadPoem = `Then took the other, as just as fair, 
    And having perhaps the better claim 
    Because it was grassy and wanted wear  
    Though as for that the passing theren 
    Had worn them really about the same,`;  
var fourAgreements = `You have the right to be you.n  
    You can only be you when you do your best.`; 

好处:直接一个反引号,将所有的字符串放进去即可,中介随意换行,好清爽!

4.Destructuring Assignment (解构赋值)

下边例子中,house 和 mouse是 key,同时 house 和 mouse 也是一个变量。

ES5:

var data = $('body').data(), // data has properties house and mouse  
    house = data.house,  
    mouse = data.mouse;  

以及在node.js中用ES5是这样:

var jsonMiddleware = require('body-parser').jsonMiddleware ;  
var body = req.body, // body has username and password  
username = body.username,  
password = body.password;  

ES6:

var {house,mouse} = $('body').data(); //we'll get house and mouse variables 
var {jsonMiddleware} = require('body-parser');
var {username,password} = req.body;

在数组中是这样的:

var [col1,col2] = $('.column'),
    [line1,line2,line3, ,line5] = file.split('n');

好处:使用{}省去了写对象的属性的步骤,当然这个{}中的变量是与对象的属性名字保持一致的情况下。

5.Enhanced Object Literals (增强的对象字面量)

使用对象文本可以做许多让人意想不到的事情!通过ES6,我们可以把ES5中的JSON变得更加接近于一个类。

下面是一个典型ES5对象文本,里面有一些方法和属性:

var serviceBase = {port: 3000, url: 'azat.co'},  
    getAccounts = function(){return [1,2,3]};  
var accountServiceES5 = {  
  port: serviceBase.port,  
  url: serviceBase.url,  
  getAccounts: getAccounts,  
  toString: function() {  
      return JSON.stringify(this.valueOf());  
  },  
  getUrl: function() {return "http://" + this.url + ':' + this.port},  
  valueOf_1_2_3: getAccounts()  
}  
如果我们想让它更有意思,我们可以用Object.create从serviceBase继承原型的方法:

var accountServiceES5ObjectCreate = Object.create(serviceBase)  
// Object.create() 方法创建一个拥有指定原型和若干个指定属性的对象。
var accountServiceES5ObjectCreate = {  
  getAccounts: getAccounts,  
  toString: function() {  
    return JSON.stringify(this.valueOf());  
  },  
  getUrl: function() {return "http://" + this.url + ':' + this.port},  
  valueOf_1_2_3: getAccounts()  
}  

ES6的对象文本中:既可以直接分配getAccounts: getAccounts,也可以只需用一个getAccounts

var serviceBase = {port: 3000, url: 'azat.co'},
getAccount = function(){return [1,2,3]};
var accountService = {
    __proto__: serviceBase, //通过proto设置属性
    getAccount, // 既可以直接分配getAccounts: getAccounts,也可以只需用一个getAccounts
    toString() { //这里将json形式改为函数形式 
        return JSON.stringify(super.valueOf()); 
        //调用super防范
    },  
    getUrl() {return "http://" + this.url + ':' + this.port},  
    [ 'valueOf_' + getAccounts().join('_') ]: getAccounts()  //使用动态key值(valueOf_1_2_3)此处将getAccounts()方法得到的数组[1,2,3]转化为字符串1_2_3
};
console.log(accountService);

好处:相当于直接将结果写进去,而不再必须 key:value

  • 将toString: function(){}这种json形式转变为 toString() {}这样的函数(类)的形式
  • 既可以直接分配getAccounts: getAccounts这样的json形式,也可以只需用一个getAccounts表达相同的意思

6.Arrow Functions in(箭头函数)

这些丰富的箭头是令人惊讶的因为它们将使许多操作变成现实,比如, 
以前我们使用闭包,this总是预期之外地产生改变,而箭头函数的迷人之处在于,现在你的this可以按照你的预期使用了,身处箭头函数里面,this还是原来的this。

ES5:

var _this = this;  
$('.btn').click(function(event){  
  _this.sendData();  
})  

ES6: 就不需要用 _this = this:

$('.btn').click((event) =>{  
  this.sendData();  
})  

再比如:

ES5:

var logUpperCase = function() {  
  var _this = this;   //this = Object {string: "ES6 ROCKS"}
  console.log('this指的是',this); //Object {string: "ES6 ROCKS"}
  console.log('_this指的是',_this);//Object {string: "ES6 ROCKS"}
  this.string = this.string.toUpperCase();  
  console.log(_this.string); //ES6 ROCKS  
  console.log(this.string);  //ES6 ROCKS
  return function () {  
    return console.log(_this.string); //ES6 ROCKS
    return console.log(_this.string); //如果return _this.string,将返回 undefined,因为

  }  
} 
logUpperCase.call({ string: 'ES6 rocks' })();

ES6:我们并不需要用_this浪费时间,现在你的this可以按照你的预期使用了,身处箭头函数里面,this还是原来的this

var logUpperCase = function() {  
  this.string = this.string.toUpperCase();//this还是原来的this  
  return () => console.log(this.string);  
}  
logUpperCase.call({ string: 'ES6 rocks' })();  

注意 只要你愿意,在ES6中=>可以混合和匹配老的函数一起使用。当在一行代码中用了箭头函数,它就变成了一个表达式。它将暗地里返回单个语句的结果。如果你超过了一行,将需要明确使用return。

ES5:

var ids = ['5632953c4e345e145fdf2df8','563295464e345e145fdf2df9'];  
var messages = ids.map(function (value) {  
  return "ID is " + value; // explicit return  
}); 

ES6:

var ids = ['5632953c4e345e145fdf2df8','563295464e345e145fdf2df9'];
var messages = ids.map((value)  => `ID is ${value}`); //implicit return 

好处: 
* 并不需要用_this浪费时间,现在你的this可以按照你的预期使用了,身处箭头函数里面,this还是原来的this。 
* => 可以代替function关键字,当在一行用了箭头函数,可以省去{},还可以省去return,它会暗地里返回的。

7.Promises

ES5:

setTimeout(function(){  
  console.log('Yay!');  
}, 1000);  

ES6: 我们可以用promise重写

var wait1000 = new Promise((resolve,reject)=> {
   setTimeout(resolve,1000);
}).then(()=> {
    console.log('Yay!'); 
});

如果我们有更多的嵌套逻辑在setTimeout()回调函数中,好处会明显一点: 
ES5:

setTimeout(function(){  
  console.log('Yay!');  
  setTimeout(function(){  
    console.log('Wheeyee!');  
  }, 1000)  
}, 1000);  

ES6: 我们可以用promise重写

var wait1000 = ()=> new Promise((resolve,reject)=>{ setTimeout(resolve,1000);});
wait1000()
    .then(function(){
        console.log('Yay!');  
        return wait1000()
    })
    .then(function(){
         console.log('Wheeyee!');  
    });

8 Block-Scoped(块作用域和构造let和const)

let是一种新的变量声明方式,它允许你把变量作用域控制在块级里面。我们用大括号定义代码块,在ES5中,块级作用域起不了任何作用:

function calculateTotalAmount (vip) {  
  var amount = 0;  
  if (vip) {  
    var amount = 1;  
  }  
  { // more crazy blocks!  
    var amount = 100;  
    {  
      var amount = 1000;  
    }  
  }    
  return amount;  
}  
console.log(calculateTotalAmount(true));  // 1000

ES6: 用let限制块级作用域

function calculateTotalAmount(vip){
    var amouont  = 0; // probably should also be let, but you can mix var and let
    if (vip) {  
        let amount = 1; // first amount is still 0  
    }   
    { // more crazy blocks!  
    let amount = 100; // first amount is still 0  
    {  
      let amount = 1000; // first amount is still 0  
    }  
  }    
  return amount;  
} 
console.log(calculateTotalAmount(true));  //0 因为块作用域中有了let。

谈到const,就更加容易了;它就是一个不变量,也是块级作用域就像let一样。

好处 : 我们用let限制块级作用域。而var是限制函数作用域。

9. Classes (类)

ES6没有用函数,而是使用原型实现类。我们创建一个类baseModel ,并且在这个类里定义了一个constructor 和一个 getName()方法:

class baseModel {  
    constructor(options, data) {// class constructor, 注意我们对options 和data使用了默认参数值。
        this.name = 'Base';  
        this.url = 'http://azat.co/api';  
        this.data = data;  
        this.options = options;  
   }  
    getName() { // class method  
        console.log(`Class name: ${this.name}`);  
    } 
    getUrl() { // class method  
         console.log(`Url: ${this.url}`);  
    }
}  

AccountModel 从类baseModel 中继承而来:

class AccountModel extends baseModel {  
    constructor(options, data) { 
    super({private: true}, ['32', '5242']); 
    this.url +='/accounts/';  
    }
    get accountsData() {
        return this.data;  
    }  
} 
// 调用
let accounts = new AccountModel(5);  
accounts.getName();  // Class name:  Base
console.log('Data is %s', accounts.accountsData); 
// Data is 32,5242 

//子类必须在constructor方法中调用super方法,否则新建实例时会报错。
//这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。
//如果不调用super方法,子类就得不到this对象。

【注意】: 
* 此处的继承中,子类必须在constructor方法中调用super方法,否则新建实例时会报错。 
* 此处的super方法继承了父类的所有方法,包括不在父类的constructor中的其他方法,当然可以改写父类方法,比如上述例子,继承了getName(),getUrl()方法,以及constructor()中的this.name等属性,同时改写了this.url,增加了accountsData,且新增的方法前边要加上get。 
* 子类调用super方法可以传入参数,对应constructor()函数的形参。

10. Modules (模块)

ES5导出:

module.exports = { port: 3000, getAccounts: function() { ... }}

ES6导出:

export var port = 3000;
export function getAccounts(url) { ...}

ES5导入:

var service = require('module.js');
console.log(service.port); // 3000

ES6导入:

我们需用import {name} from ‘my-module’语法

import {port, getAccounts} from 'module';
console.log(port); // 300

或者ES6整体导入:

import * as service from 'module';
console.log(service.port); // 3000

这里还有许多ES6的其它特性你可能会使用到,排名不分先后:

  • 全新的Math, Number, String, Array 和 Object 方法
  • 二进制和八进制数据类型
  • 默认参数不定参数扩展运算符
  • Symbols符号
  • tail调用
  • Generators (生成器)
  • New data structures like Map and Set(新的数据构造对像MAP和set)