分类
JavaScript

深入理解JavaScript系列(10):S.O.L.I.D五大原则之依赖倒置原则DIP

# 前言

本章我们要讲解的是S.O.L.I.D五大原则JavaScript语言实现的第5篇,依赖倒置原则LSP(The Dependency Inversion Principle )。

>英文原文:http://freshbrewedcode.com/derekgreer/2012/01/22/solid-javascript-the-dependency-inversion-principle/

# 依赖倒置原则

依赖倒置原则的描述是:

>A. High-level modules should not depend on low-level modules. Both should depend on abstractions.
高层模块不应该依赖于低层模块,二者都应该依赖于抽象

>B. Abstractions should not depend upon details. Details should depend upon abstractions.
抽象不应该依赖于细节,细节应该依赖于抽象

依赖倒置原则的最重要问题就是确保应用程序或框架的主要组件从非重要的底层组件实现细节解耦出来,这将确保程序的最重要的部分不会因为低层次组件的变化修改而受影响。

该原则的第一部分是关于高层模块和低层模块之间的耦合方式,在传统的分成架构中,高层模块(封装了程序的核心业务逻辑)总依赖于低层的一些模块(一些基础点)。当应用依赖倒置原则的时候,关系就反过来了。和高层模块依赖于低层模块不同,依赖倒置是让低层模块依赖于高层模块里定义的接口。举例来说,如果要给程序进行数据持久化,传统的设计是核心模块依赖于一个持久化模块的API,而根据依赖倒置原则重构以后,则是核心模块需要定义持久化的API接口,然后持久化的实现实例需要实现核心模块定义的这个API接口。

该原则的第二部分描述的是抽象和细节之间的正确关系。理解这一部分,通过了解C++语言比较有帮助,因为他的适用性比较明显。

不像一些静态类型的语言,C++没有提供一个语言级别的概念来定义接口,那类定义和类实现之间到底是怎么样的呢,在C++里,类通过头文件的形式来定义,其中定义了源文件需要实现的类成员方法和变量。因为所有的变量和私有方法都定义在头文件里,所以可以用来抽象以便和实现细节之前解耦出来。通过定只定义抽象方法来实现(C++里是抽象基类)接口这个概念用于实现类来实现。

# DIP and JavaScript

因为JavaScript是动态语言,所以不需要去为了解耦而抽象。所以抽象不应依赖于细节这个改变在JavaScript里没有太大的影响,但高层模块不应依赖于低层模块却有很大的影响。

在当静态类型语言的上下文里讨论依赖倒置原则的时候,耦合的概念包括语义(semantic)和物理(physical)两种。这就是说,如果一个高层模块依赖于一个低层模块,也就是不仅耦合了语义接口,也耦合了在底层模块里定义的物理接口。也就是说高层模块不仅要从第三方类库解耦出来,也需要从原生的低层模块里解耦出来。

为了解释这一点,想象一个.NET程序可能包含一个非常有用的高层模块,而该模块依赖于一个低层的持久化模块。当作者需要在持久化API里增加一个类似的接口的时候,不管依赖倒置原则有没有使用,高层模块在不重新实现这个低层模块的新接口之前是没有办法在其它的程序里得到重用的。

在JavaScript里,依赖倒置原则的适用性仅仅限于高层模块和低层模块之间的语义耦合,比如,DIP可以根据需要去增加接口而不是耦合低层模块定义的隐式接口。

为了来理解这个,我们看一下如下例子:

```js
$.fn.trackMap = function(options) {
var defaults = {
/* defaults */
};
options = $.extend({}, defaults, options);

var mapOptions = {
center: new google.maps.LatLng(options.latitude,options.longitude),
zoom: 12,
mapTypeId: google.maps.MapTypeId.ROADMAP
},
map = new google.maps.Map(this[0], mapOptions),
pos = new google.maps.LatLng(options.latitude,options.longitude);

var marker = new google.maps.Marker({
position: pos,
title: options.title,
icon: options.icon
});

marker.setMap(map);

options.feed.update(function(latitude, longitude) {
marker.setMap(null);
var newLatLng = new google.maps.LatLng(latitude, longitude);
marker.position = newLatLng;
marker.setMap(map);
map.setCenter(newLatLng);
});

return this;
};

var updater = (function() {
// private properties

return {
update: function(callback) {
updateMap = callback;
}
};
})();

$("#map_canvas").trackMap({
latitude: 35.044640193770725,
longitude: -89.98193264007568,
icon: 'http://bit.ly/zjnGDe',
title: 'Tracking Number: 12345',
feed: updater
});
```

在上述代码里,有个小型的JS类库将一个DIV转化成Map以便显示当前跟踪的位置信息。trackMap函数有2个依赖:第三方的Google Maps API和Location feed。该feed对象的职责是当icon位置更新的时候调用一个callback回调(在初始化的时候提供的)并且传入纬度latitude和精度longitude。Google Maps API是用来渲染界面的。

feed对象的接口可能按照装,也可能没有照装trackMap函数的要求去设计,事实上,他的角色很简单,着重在简单的不同实现,不需要和Google Maps这么依赖。介于trackMap语义上耦合了Google Maps API,如果需要切换不同的地图提供商的话那就不得不对trackMap函数进行重写以便可以适配不同的provider。

为了将于Google maps类库的语义耦合翻转过来,我们需要重写设计trackMap函数,以便对一个隐式接口(抽象出地图提供商provider的接口)进行语义耦合,我们还需要一个适配Google Maps API的一个实现对象,如下是重构后的trackMap函数:

```js
$.fn.trackMap = function(options) {
var defaults = {
/* defaults */
};

options = $.extend({}, defaults, options);

options.provider.showMap(
this[0],
options.latitude,
options.longitude,
options.icon,
options.title);

options.feed.update(function(latitude, longitude) {
options.provider.updateMap(latitude, longitude);
});

return this;
};

$("#map_canvas").trackMap({
latitude: 35.044640193770725,
longitude: -89.98193264007568,
icon: 'http://bit.ly/zjnGDe',
title: 'Tracking Number: 12345',
feed: updater,
provider: trackMap.googleMapsProvider
});
```

在该版本里,我们重新设计了trackMap函数以及需要的一个地图提供商接口,然后将实现的细节挪到了一个单独的googleMapsProvider组件,该组件可能独立封装成一个单独的JavaScript模块。如下是我的googleMapsProvider实现:

```js
trackMap.googleMapsProvider = (function() {
var marker, map;

return {
showMap: function(element, latitude, longitude, icon, title) {
var mapOptions = {
center: new google.maps.LatLng(latitude, longitude),
zoom: 12,
mapTypeId: google.maps.MapTypeId.ROADMAP
},
pos = new google.maps.LatLng(latitude, longitude);

map = new google.maps.Map(element, mapOptions);

marker = new google.maps.Marker({
position: pos,
title: title,
icon: icon
});

marker.setMap(map);
},
updateMap: function(latitude, longitude) {
marker.setMap(null);
var newLatLng = new google.maps.LatLng(latitude,longitude);
marker.position = newLatLng;
marker.setMap(map);
map.setCenter(newLatLng);
}
};
})();
```

做了上述这些改变以后,trackMap函数将变得非常有弹性了,不必依赖于Google Maps API,相反可以任意替换其它的地图提供商,那就是说可以按照程序的需求去适配任何地图提供商。

# 何时依赖注入?

有点不太相关,其实依赖注入的概念经常和依赖倒置原则混在一起,为了澄清这个不同,我们有必要来解释一下:

依赖注入是控制反转的一个特殊形式,反转的意思一个组件如何获取它的依赖。依赖注入的意思就是:依赖提供给组件,而不是组件去获取依赖,意思是创建一个依赖的实例,通过工厂去请求这个依赖,通过Service Locator或组件自身的初始化去请求这个依赖。依赖倒置原则和依赖注入都是关注依赖,并且都是用于反转。不过,依赖倒置原则没有关注组件如何获取依赖,而是只关注高层模块如何从低层模块里解耦出来。某种意义上说,依赖倒置原则是控制反转的另外一种形式,这里反转的是哪个模块定义接口(从低层里定义,反转到高层里定义)。

# 总结

这是五大原则的最后一篇了,在这5篇文字里我们看到了SOLID如何在JavaScript里实现的,不同的原则在JavaScript里通过不同的角度来说明的。(大叔注:其实大叔觉得虽然是有点不伦不类,但从另外一个层面上说,大体的原则在各种语言上其实还是一样的。)

分类
未分类

深入理解JavaScript系列(9):S.O.L.I.D五大原则之接口隔离原则ISP

# 前言

本章我们要讲解的是S.O.L.I.D五大原则JavaScript语言实现的第4篇,接口隔离原则ISP(The Interface Segregation Principle)。

>英文原文:http://freshbrewedcode.com/derekgreer/2012/01/08/solid-javascript-the-interface-segregation-principle/
注:这篇文章作者写得比较绕口,所以大叔理解得也比较郁闷,凑合着看吧,别深陷进去了

接口隔离原则的描述是:

>Clients should not be forced to depend on methods they do not use.
不应该强迫客户依赖于它们不用的方法。

当用户依赖的接口方法即便只被别的用户使用而自己不用,那它也得实现这些接口,换而言之,一个用户依赖了未使用但被其他用户使用的接口,当其他用户修改该接口时,依赖该接口的所有用户都将受到影响。这显然违反了开闭原则,也不是我们所期望的。

接口隔离原则ISP和单一职责有点类似,都是用于聚集功能职责的,实际上ISP可以被理解才具有单一职责的程序转化到一个具有公共接口的对象。

# JavaScript接口

JavaScript下我们改如何遵守这个原则呢?毕竟JavaScript没有接口的特性,如果接口就是我们所想的通过某种语言提供的抽象类型来建立contract和解耦的话,那可以说还行,不过JavaScript有另外一种形式的接口。在[Design Patterns – Elements of Reusable Object-Oriented Software](http://www.amazon.com/Design-Patterns-Elements-Reusable-Object-Oriented/dp/0201633612)一书中我们找到了接口的定义:

一个对象声明的任意一个操作都包含一个操作名称,参数对象和操作的返回值。我们称之为操作符的签名(signature)。
一个对象里声明的所有的操作被称为这个对象的接口(interface)。一个对象的接口描绘了所有发生在这个对象上的请求信息。

不管一种语言是否提供一个单独的构造来表示接口,所有的对象都有一个由该对象所有属性和方法组成的隐式接口。参考如下代码:

```js
var exampleBinder = {};
exampleBinder.modelObserver = (function() {
/* 私有变量 */
return {
observe: function(model) {
/* 代码 */
return newModel;
},
onChange: function(callback) {
/* 代码 */
}
}
})();

exampleBinder.viewAdaptor = (function() {
/* 私有变量 */
return {
bind: function(model) {
/* 代码 */
}
}
})();

exampleBinder.bind = function(model) {
/* 私有变量 */
exampleBinder.modelObserver.onChange(/* 回调callback */);
var om = exampleBinder.modelObserver.observe(model);
exampleBinder.viewAdaptor.bind(om);
return om;
};
```

上面的exampleBinder类库实现的功能是双向绑定。该类库暴露的公共接口是bind方法,其中bind里用到的关于change通知和view交互的功能分别是由单独的对象modelObserver和viewAdaptor来实现的,这些对象从某种意义上来说就是公共接口bind方法的具体实现。

尽管JavaScript没有提供接口类型来支持对象的contract,但该对象的隐式接口依然能当做一个contract提供给程序用户。

# ISP与JavaScript

我们下面讨论的一些小节是JavaScript里关于违反接口隔离原则的影响。正如上面看到的,JavaScript程序里实现接口隔离原则虽然可惜,但是不像静态类型语言那样强大,JavaScript的语言特性有时候会使得所谓的接口搞得有点不粘性。

## **堕落的实现**

在静态类型语言语言里,导致违反ISP原则的一个原因是堕落的实现。在Java和C#里所有的接口里定义的方法都必须实现,如果你只需要其中几个方法,那其他的方法也必须实现(可以通过空实现或者抛异常的方式)。在JavaScript里,如果只需要一个对象里的某一些接口的话,他也解决不了堕落实现这个问题,虽然不用强制实现上面的接口。但是这种实现依然违反了里氏替换原则。

```js
var rectangle = {
area: function() {
/* 代码 */
},
draw: function() {
/* 代码 */
}
};

var geometryApplication = {
getLargestRectangle: function(rectangles) {
/* 代码 */
}
};

var drawingApplication = {
drawRectangles: function(rectangles) {
/* 代码 */
}
};
```

当一个rectangle替代品为了满足新对象geometryApplication的getLargestRectangle 的时候,它仅仅需要rectangle的area()方法,但它却违反了LSP(因为他根本用不到其中drawRectangles方法才能用到的draw方法)。

## **静态耦合**

静态类型语言里的另外一个导致违反ISP的原因是静态耦合,在静态类型语言里,接口在一个松耦合设计程序里扮演了重大角色。不管是在动态语言还是在静态语言,有时候一个对象都可能需要在多个客户端用户进行通信(比如共享状态),对静态类型语言,最好的解决方案是使用[Role Interfaces](http://martinfowler.com/bliki/RoleInterface.html),它允许用户和该对象进行交互(而该对象可能需要在多个角色)作为它的实现来对用户和无关的行为进行解耦。在JavaScript里就没有这种问题了,因为对象都被动态语言所特有的优点进行解耦了。

## **语义耦合**

导致违反ISP的一个通用原因,动态语言和静态类型语言都有,那就是语义耦合,所谓语义耦合就是互相依赖,也就是一个对象的行为依赖于另外一个对象,那就意味着,如果一个用户改变了其中一个行为,很有可能会影响另外一个使用用户。这也违反单一职责原则了。可以通过继承和对象替代来解决这个问题。

## **可扩展性**

另外一个导致问题的原因是关于可扩展性,很多人在举例的时候都会举关于callback的例子用来展示可扩展性(比如ajax里成功以后的回调设置)。如果想这样的接口需要一个实现并且这个实现的对象里有很多熟悉或方法的话,ISP就会变得很重要了,也就是说当一个接口interface变成了一个需求实现很多方法的时候,他的实现将会变得异常复杂,而且有可能导致这些接口承担一个没有粘性的职责,这就是我们经常提到的胖接口。

# 总结

JavaScript里的动态语言特性,使得我们实现非粘性接口的影响力比静态类型语言小,但接口隔离原则在JavaScript程序设计模式里依然有它发挥作用的地方。

分类
JavaScript

深入理解JavaScript系列(8):S.O.L.I.D五大原则之里氏替换原则LSP

# 前言

本章我们要讲解的是S.O.L.I.D五大原则JavaScript语言实现的第3篇,里氏替换原则LSP(The Liskov Substitution Principle )。

>英文原文:http://freshbrewedcode.com/derekgreer/2011/12/31/solid-javascript-the-liskov-substitution-principle/

开闭原则的描述是:

Subtypes must be substitutable for their base types.
派生类型必须可以替换它的基类型。

在面向对象编程里,继承提供了一个机制让子类和共享基类的代码,这是通过在基类型里封装通用的数据和行为来实现的,然后已经及类型来声明更详细的子类型,为了应用里氏替换原则,继承子类型需要在语义上等价于基类型里的期望行为。

为了来更好的理解,请参考如下代码:

```js
function Vehicle(my) {
var my = my || {};
my.speed = 0;
my.running = false;

this.speed = function() {
return my.speed;
};
this.start = function() {
my.running = true;
};
this.stop = function() {
my.running = false;
};
this.accelerate = function() {
my.speed++;
};
this.decelerate = function() {
my.speed--;
}, this.state = function() {
if (!my.running) {
return "parked";
}
else if (my.running && my.speed) {
return "moving";
}
else if (my.running) {
return "idle";
}
};
}
```

上述代码我们定义了一个Vehicle函数,其构造函数为vehicle对象提供了一些基本的操作,我们来想想如果当前函数当前正运行在服务客户的产品环境上,如果现在需要添加一个新的构造函数来实现加快移动的vehicle。思考以后,我们写出了如下代码:

```js
function FastVehicle(my) {
var my = my || {};

var that = new Vehicle(my);
that.accelerate = function() {
my.speed += 3;
};
return that;
}
```

在浏览器的控制台我们都测试了,所有的功能都是我们的预期,没有问题,FastVehicle的速度增快了3倍,而且继承他的方法也是按照我们的预期工作。此后,我们开始部署这个新版本的类库到产品环境上,可是我们却接到了新的构造函数导致现有的代码不能支持执行了,下面的代码段揭示了这个问题:

```js
var maneuver = function(vehicle) {
write(vehicle.state());
vehicle.start();
write(vehicle.state());
vehicle.accelerate();
write(vehicle.state());
write(vehicle.speed());
vehicle.decelerate();
write(vehicle.speed());
if (vehicle.state() != "idle") {
throw "The vehicle is still moving!";
}
vehicle.stop();
write(vehicle.state());
};
```

根据上面的代码,我们看到抛出的异常是“The vehicle is still moving!”,这是因为写这段代码的作者一直认为加速(accelerate)和减速(decelerate)的数字是一样的。但FastVehicle的代码和Vehicle的代码并不是完全能够替换掉的。因此,FastVehicle违反了里氏替换原则。 

在这点上,你可能会想:“但,客户端不能老假定vehicle都是按照这样的规则来做”,里氏替换原则(LSP)的妨碍(译者注:就是妨碍实现LSP的代码)不是基于我们所想的继承子类应该在行为里确保更新代码,而是这样的更新是否能在当前的期望中得到实现。

上述代码这个case,解决这个不兼容的问题需要在vehicle类库或者客户端调用代码上进行一点重新设计,或者两者都要改。

# 减少LSP妨碍

那么,我们如何避免LSP妨碍?不幸的话,并不是一直都是可以做到的。我们这里有几个策略我们处理这个事情。

## **契约(Contracts)**

处理LSP过分妨碍的一个策略是使用契约,契约清单有2种形式:执行说明书(executable specifications)和错误处理,在执行说明书里,一个详细类库的契约也包括一组自动化测试,而错误处理是在代码里直接处理的,例如在前置条件,后置条件,常量检查等,可以从Bertrand Miller的大作《[契约设计](http://en.wikipedia.org/wiki/Design_by_contract)》中查看这个技术。虽然自动化测试和契约设计不在本篇文字的范围内,但当我们用的时候我还是推荐如下内容:

1. 检查使用测试驱动开发(Test-Driven Development)来指导你代码的设计
2. 设计可重用类库的时候可随意使用契约设计技术

对于你自己要维护和实现的代码,使用契约设计趋向于添加很多不必要的代码,如果你要控制输入,添加测试是非常有必要的,如果你是类库作者,使用契约设计,你要注意不正确的使用方法以及让你的用户使之作为一个测试工具。

## **避免继承**

避免LSP妨碍的另外一个测试是:如果可能的话,尽量不用继承,在Gamma的大作《[Design Patterns – Elements of Reusable Object-Orineted Software](http://www.amazon.com/Design-Patterns-Elements-Reusable-Object-Oriented/dp/0201633612)》中,我们可以看到如下建议:

>Favor object composition over class inheritance
尽量使用对象组合而不是类继承

有些书里讨论了组合比继承好的唯一作用是静态类型,基于类的语言(例如,在运行时可以改变行为),与JavaScript相关的一个问题是耦合,当使用继承的时候,继承子类型和他们的基类型耦合在一起了,就是说及类型的改变会影响到继承子类型。组合倾向于对象更小化,更容易想静态和动态语言语言维护。

# 与行为有关,而不是继承

到现在,我们讨论了和继承上下文在内的里氏替换原则,指示出JavaScript的面向对象实。不过,里氏替换原则(LSP)的本质不是真的和继承有关,而是行为兼容性。JavaScript是一个动态语言,一个对象的契约行为不是对象的类型决定的,而是对象期望的功能决定的。里氏替换原则的初始构想是作为继承的一个原则指南,等价于对象设计中的隐式接口。

举例来说,让我们来看一下Robert C. Martin的大作《[敏捷软件开发 原则、模式与实践](http://www.amazon.com/Software-Development-Principles-Patterns-Practices/dp/0135974445)》中的一个矩形类型:

## **矩形例子**

考虑我们有一个程序用到下面这样的一个矩形对象:

```js
var rectangle = {
length: 0,
width: 0
};
```

过后,程序有需要一个正方形,由于正方形就是一个长(length)和宽(width)都一样的特殊矩形,所以我们觉得创建一个正方形代替矩形。我们添加了length和width属性来匹配矩形的声明,但我们觉得使用属性的getters/setters一般我们可以让length和width保存同步,确保声明的是一个正方形:

```js
var square = {};
(function() {
var length = 0, width = 0;
// 注意defineProperty方式是262-5版的新特性
Object.defineProperty(square, "length", {
get: function() { return length; },
set: function(value) { length = width = value; }
});
Object.defineProperty(square, "width", {
get: function() { return width; },
set: function(value) { length = width = value; }
});
})();
```

不幸的是,当我们使用正方形代替矩形执行代码的时候发现了问题,其中一个计算矩形面积的方法如下:

```js
var g = function(rectangle) {
rectangle.length = 3;
rectangle.width = 4;
write(rectangle.length);
write(rectangle.width);
write(rectangle.length * rectangle.width);
};
```

该方法在调用的时候,结果是16,而不是期望的12,我们的正方形square对象违反了LSP原则,square的长度和宽度属性暗示着并不是和矩形100%兼容,但我们并不总是这样明确的暗示。解决这个问题,我们可以重新设计一个shape对象来实现程序,依据多边形的概念,我们声明rectangle和square,relevant。不管怎么说,我们的目的是要说里氏替换原则并不只是继承,而是任何方法(其中的行为可以另外的行为)。

# 总结

里氏替换原则(LSP)表达的意思不是继承的关系,而是任何方法(只要该方法的行为能体会另外的行为就行)。

分类
JavaScript

深入理解JavaScript系列(7):S.O.L.I.D五大原则之开闭原则OCP

# 前言

本章我们要讲解的是S.O.L.I.D五大原则JavaScript语言实现的第2篇,开闭原则OCP(The Open/Closed Principle )。

开闭原则的描述是:

>Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification.
软件实体(类,模块,方法等等)应当对扩展开放,对修改关闭,即软件实体应当在不修改的前提下扩展。

open for extension(对扩展开放)的意思是说当新需求出现的时候,可以通过扩展现有模型达到目的。而Close for modification(对修改关闭)的意思是说不允许对该实体做任何修改,说白了,就是这些需要执行多样行为的实体应该设计成不需要修改就可以实现各种的变化,坚持开闭原则有利于用最少的代码进行项目维护。

英文原文:http://freshbrewedcode.com/derekgreer/2011/12/19/solid-javascript-the-openclosed-principle/

# 问题代码

为了直观地描述,我们来举个例子演示一下,下属代码是动态展示question列表的代码(没有使用开闭原则)。

```js
// 问题类型
var AnswerType = {
Choice: 0,
Input: 1
};

// 问题实体
function question(label, answerType, choices) {
return {
label: label,
answerType: answerType,
choices: choices // 这里的choices是可选参数
};
}

var view = (function () {
// render一个问题
function renderQuestion(target, question) {
var questionWrapper = document.createElement('div');
questionWrapper.className = 'question';

var questionLabel = document.createElement('div');
questionLabel.className = 'question-label';
var label = document.createTextNode(question.label);
questionLabel.appendChild(label);

var answer = document.createElement('div');
answer.className = 'question-input';

// 根据不同的类型展示不同的代码:分别是下拉菜单和输入框两种
if (question.answerType === AnswerType.Choice) {
var input = document.createElement('select');
var len = question.choices.length;
for (var i = 0; i < len; i++) { var option = document.createElement('option'); option.text = question.choices[i]; option.value = question.choices[i]; input.appendChild(option); } } else if (question.answerType === AnswerType.Input) { var input = document.createElement('input'); input.type = 'text'; } answer.appendChild(input); questionWrapper.appendChild(questionLabel); questionWrapper.appendChild(answer); target.appendChild(questionWrapper); } return { // 遍历所有的问题列表进行展示 render: function (target, questions) { for (var i = 0; i < questions.length; i++) { renderQuestion(target, questions[i]); }; } }; })(); var questions = [ question('Have you used tobacco products within the last 30 days?', AnswerType.Choice, ['Yes', 'No']), question('What medications are you currently using?', AnswerType.Input) ]; var questionRegion = document.getElementById('questions'); view.render(questionRegion, questions); ``` 上面的代码,view对象里包含一个render方法用来展示question列表,展示的时候根据不同的question类型使用不同的展示方式,一个question包含一个label和一个问题类型以及choices的选项(如果是选择类型的话)。如果问题类型是Choice那就根据选项生产一个下拉菜单,如果类型是Input,那就简单地展示input输入框。 该代码有一个限制,就是如果再增加一个question类型的话,那就需要再次修改renderQuestion里的条件语句,这明显违反了开闭原则。 # 重构代码 让我们来重构一下这个代码,以便在出现新question类型的情况下允许扩展view对象的render能力,而不需要修改view对象内部的代码。 先来创建一个通用的questionCreator函数: ```js function questionCreator(spec, my) { var that = {}; my = my || {}; my.label = spec.label; my.renderInput = function () { throw "not implemented"; // 这里renderInput没有实现,主要目的是让各自问题类型的实现代码去覆盖整个方法 }; that.render = function (target) { var questionWrapper = document.createElement('div'); questionWrapper.className = 'question'; var questionLabel = document.createElement('div'); questionLabel.className = 'question-label'; var label = document.createTextNode(spec.label); questionLabel.appendChild(label); var answer = my.renderInput(); // 该render方法是同样的粗合理代码 // 唯一的不同就是上面的一句my.renderInput() // 因为不同的问题类型有不同的实现 questionWrapper.appendChild(questionLabel); questionWrapper.appendChild(answer); return questionWrapper; }; return that; } ``` 该代码的作用组合要是render一个问题,同时提供一个未实现的renderInput方法以便其他function可以覆盖,以使用不同的问题类型,我们继续看一下每个问题类型的实现代码: ```js function choiceQuestionCreator(spec) { var my = {}, that = questionCreator(spec, my); // choice类型的renderInput实现 my.renderInput = function () { var input = document.createElement('select'); var len = spec.choices.length; for (var i = 0; i < len; i++) { var option = document.createElement('option'); option.text = spec.choices[i]; option.value = spec.choices[i]; input.appendChild(option); } return input; }; return that; } function inputQuestionCreator(spec) { var my = {}, that = questionCreator(spec, my); // input类型的renderInput实现 my.renderInput = function () { var input = document.createElement('input'); input.type = 'text'; return input; }; return that; } ``` choiceQuestionCreator函数和inputQuestionCreator函数分别对应下拉菜单和input输入框的renderInput实现,通过内部调用统一的questionCreator(spec, my)然后返回that对象(同一类型哦)。 view对象的代码就很固定了。 ```js var view = { render: function(target, questions) { for (var i = 0; i < questions.length; i++) { target.appendChild(questions[i].render()); } } }; ``` 所以我们声明问题的时候只需要这样做,就OK了: ```js var questions = [ choiceQuestionCreator({ label: 'Have you used tobacco products within the last 30 days?', choices: ['Yes', 'No']   }), inputQuestionCreator({ label: 'What medications are you currently using?'   }) ]; ``` 最终的使用代码,我们可以这样来用: ```js var questionRegion = document.getElementById('questions'); view.render(questionRegion, questions); ``` ```js 重构后的最终代码 function questionCreator(spec, my) { var that = {}; my = my || {}; my.label = spec.label; my.renderInput = function() { throw "not implemented"; }; that.render = function(target) { var questionWrapper = document.createElement('div'); questionWrapper.className = 'question'; var questionLabel = document.createElement('div'); questionLabel.className = 'question-label'; var label = document.createTextNode(spec.label); questionLabel.appendChild(label); var answer = my.renderInput(); questionWrapper.appendChild(questionLabel); questionWrapper.appendChild(answer); return questionWrapper; }; return that; } function choiceQuestionCreator(spec) { var my = {}, that = questionCreator(spec, my); my.renderInput = function() { var input = document.createElement('select'); var len = spec.choices.length; for (var i = 0; i < len; i++) { var option = document.createElement('option'); option.text = spec.choices[i]; option.value = spec.choices[i]; input.appendChild(option); } return input; }; return that; } function inputQuestionCreator(spec) { var my = {}, that = questionCreator(spec, my); my.renderInput = function() { var input = document.createElement('input'); input.type = 'text'; return input; }; return that; } var view = { render: function(target, questions) { for (var i = 0; i < questions.length; i++) { target.appendChild(questions[i].render()); } } }; var questions = [ choiceQuestionCreator({ label: 'Have you used tobacco products within the last 30 days?', choices: ['Yes', 'No'] }), inputQuestionCreator({ label: 'What medications are you currently using?' }) ]; var questionRegion = document.getElementById('questions'); view.render(questionRegion, questions); ``` 上面的代码里应用了一些技术点,我们来逐一看一下: 1. 首先,questionCreator方法的创建,可以让我们使用[模板方法模式](http://en.wikipedia.org/wiki/Template_method_pattern)将处理问题的功能delegat给针对每个问题类型的扩展代码renderInput上。 2. 其次,我们用一个私有的spec属性替换掉了前面question方法的构造函数属性,因为我们封装了render行为进行操作,不再需要把这些属性暴露给外部代码了。 3. 第三,我们为每个问题类型创建一个对象进行各自的代码实现,但每个实现里都必须包含renderInput方法以便覆盖questionCreator方法里的renderInput代码,这就是我们常说的[策略模式](http://en.wikipedia.org/wiki/Strategy_pattern)。 通过重构,我们可以去除不必要的问题类型的枚举AnswerType,而且可以让choices作为choiceQuestionCreator函数的必选参数(之前的版本是一个可选参数)。 # 总结 重构以后的版本的view对象可以很清晰地进行新的扩展了,为不同的问题类型扩展新的对象,然后声明questions集合的时候再里面指定类型就行了,view对象本身不再修改任何改变,从而达到了开闭原则的要求。 另:懂C#的话,不知道看了上面的代码后是否和多态的实现有些类似?其实上述的代码用原型也是可以实现的,大家可以自行研究一下。

分类
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