破狼 Blog

Write less got more.

ngModel 值不更新/显示

angular中的$scope是页面(view)和数据(model)之间的桥梁,它链接了页面元素和model,也是angular双向绑定机制的核心。

而ngModel是angular用来处理表单(form)的最重要的指令,它链接了页面表单中的可交互元素和位于$scope之上的model,它会自动把ngModel所指向的model值渲染到form表单的可交互元素上,同时也会根据用户在form表单的输入或交互来更新此model值。

在源码中,model值的格式化、解析、验证都是由ngModel指令所对应的控制器ngModelController来实现的。

在笔者所维护的国内ng群中,经常被问到一个问题:

    为什么我的ng-model=“xxx”值不能在页面显示了呢?

对于ngModel的这类问题主要分为两类:

  • model值不满足表单验证条件,所以angular不会渲染它
  • 由于JavaScript特殊的原型链继承机制,对$scope中属性的赋值并不能更新到父$scope

在本节中,我们将会详细分析此类问题,借此深入剖析ngModel的工作原理。

验证引起的model值不显示

我们先来看一个修改商品数量的例子,要求为必须输入1-100的个数;

下面是对应的html代码:

<body class="container">
  <div ng-controller="DemoCtrl as demo">
   <div ng-form="form" class="form-horizontal">
      <div class="form-group" ng-class="{'has-error': form.amount.$invalid }">
      <label for="amount">Amount</label>
      <!-- 这个input将工作不正常 -->
    <input id="amount" name="amount" type="number" ng-model="demo.amount" class="form-control" placeholder="1 - 100" min="1" max="100"/>
    </div>
  </div>
   </div>
</body>

javascript代码:

angular.module("com.ngbook.demo", [])
    .controller("DemoCtrl", [function(){
    var vm = this;

    vm.amount = 0;

    return vm;
}]);

在代码中我们已经为ngModel变量amount赋值了整数“0”,可是界面显示效果仍然显示”1 – 100”的placeholder(如下图)。

ng-model绑定值不改变:验证

下面是关于angular number组件ngModel转换函数代码:

var NUMBER_REGEXP = /^\s*(\-|\+)?(\d+|(\d*(\.\d*)))\s*$/;

function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
    textInputType(scope, element, attr, ctrl, $sniffer, $browser);

    ctrl.$parsers.push(function(value) {
        var empty = ctrl.$isEmpty(value);
        if (empty || NUMBER_REGEXP.test(value)) {
            ctrl.$setValidity('number', true);
            return value === '' ? null : (empty ? value : parseFloat(value));
        } else {
            ctrl.$setValidity('number', false);
            return undefined;
        }
    });

    addNativeHtml5Validators(ctrl, 'number', numberBadFlags, null, ctrl.$$validityState);

    ctrl.$formatters.push(function(value) {
        return ctrl.$isEmpty(value) ? '' : '' + value;
    });

    if (attr.min) {
        var minValidator = function(value) {
            var min = parseFloat(attr.min);
            return validate(ctrl, 'min', ctrl.$isEmpty(value) || value >= min, value);
        };

        ctrl.$parsers.push(minValidator);
        ctrl.$formatters.push(minValidator);
    }

    if (attr.max) {
        var maxValidator = function(value) {
            var max = parseFloat(attr.max);
            return validate(ctrl, 'max', ctrl.$isEmpty(value) || value <= max, value);
        };

        ctrl.$parsers.push(maxValidator);
        ctrl.$formatters.push(maxValidator);
    }

    ctrl.$formatters.push(function(value) {
        return validate(ctrl, 'number', ctrl.$isEmpty(value) || isNumber(value), value);
    });
}

ngModel作为angular双向绑定中的重要组成部分,负责view控件交互数据到$scope上model的同步。当然这里存在一些差异,view上的显示和输入都是字符串类型,而在model上的数据则是有特定类型的,如常用的int、float、Date、Array、Object等。ngModel为了实现数据到model的类型转换,在ngModelController中提供了两个管道数组$formatters和$parsers,它们分别是将model的数据转换为view交互控件显示的值和将交互控件得到的view值转换为model数据,它们都是一个数组对象,在ngModel启动数据转换时,会以UNIX管道式传递执行这一些列的转换。我们也可以手动的添加$formatters和$parsers的转换函数(unshift、push),当然在这里也是做数据验证的最佳时机,能够转换意味应该是合法的数据。

在number组件代码中,我们清晰看见:依次添加了对数字验证转换、最小值合法性验证、最大值合法验证。首先会启动$parsers转换,如果在转换过程中出现不合法验证则会设置ngModelController.$setValidity验证错误,则返回undefined。对于model数据到交互控件显示,同样也会经过$formatters转换管道,对于没有通过验证的逻辑,同样也会ngModelController.$setValidity设置验证错误,返回undefined,因此这不合法的model数据不会显示在交互控件上。

原型链继承问题

JavaScript中每个对象都会链接到一个原型对象,并且他可以从中继承属性。即使通过字面量创建的对象也会链接到Object.prototype,它是JavaScript中的标配对象。JavaScript的原型链继承相对于其他语言常见的继承,是一种另类的继承,它是实施于对象上的动态继承方式,而非常见的实施与类型class之上的静态继承体系。JavaScript的这种继承方式很灵活,一个对象可以被多个对象继承,而且他们共享同一实例对象,但理解起来显得格外复杂,从JavaScript原型和原型链可以看出它的复杂性。在Javascript中,每个函数都有一个原型属性prototype指向自身的原型,而由这个函数创建的对象也有一个proto属性指向这个原型,而函数的原型是一个对象,所以这个对象也会有一个proto指向自己的原型,这样逐层深入直到Object对象的原型,这样就形成了原型链。下面的是JavaScript原型继承基础原型和原型链展示图。

javascript-原型继承

函数是由Function函数创建的对象,因此函数也有一个proto属性指向Function函数的原型。需要注意的是,真正形成原型链的是每个对象的proto属性,而不是函数的prototype属性。更多的内容关于原型和原型链的知识,请参考《Javascript模式》这本书。

JavaScript的原型链连接只在属性检索的时候才会启用,如果我们尝试去获取对象的某个属性值,但该对象没有此属性名,则JavaScript会试着从原型对象中获取该属性值。如果那个对象也没有该属性名,那么在继续从它的原型中寻找,依次类推,直到Object.prototype,如果仍然没有找到该属性值,则返回结果为undefined。不幸的是,这种原型链连接检索,只会在属性检索的的时候启用,并不会在更新属性值时启用,因此当我们对于基础类型(非引用对象上的属性,换句通俗的话来说,就是不会出现“.”运算符)的属性更新的时候,它并不能更新父对象的属性,替代方式是在自身对象上创建了该属性。这也是angular中对于基础类型的属性,不能在子controller中被修改的原因,导致在子controller中ngModel的更新并不会反应在父controller上。

下边是关于该问题的一个简化例子:

HTML:

<div ng-controller="ParentCtrl">
    <div class="form-group">
        <h4>Parent Controller:</h4>
        <pre></pre>
        <input type="text" ng-model="greet" class="form-control" />
    </div>
    <div ng-controller="ChildCtrl">
        <div class="form-group">
            <h4>Child controller:</h4>
            <pre></pre>
            <input type="text" ng-model="greet" class="form-control" />
        </div>
    </div>
</div>

JavaScript:

angular.module("com.ngbook.demo", [])
    .controller("ParentCtrl", ["$scope", function($scope) {

        $scope.greet = "hello angular!";

    }])
    .controller("ChildCtrl", angular.noop);

从初始化显示效果中,我们能看出子$scope之继承了来自父$scope的greet属性,都显示为”hello angular!“。如果我们尝试利用父controller提供了input控件改变父$scope的greet属性,你也能看见子controller区域的显示也会被及时更新。对于ngController默认会使用原型链继承其父对象的属性,所有的$scope的根$scope或称祖$scope是来自ngApp节点创建的$rootScope,换句话说,$rootScope是万物之源,所有的$scope都直接或者间接继承至它。

ngModel-controller继承

当我们尝试去改变输入框的greet属性的时,则发生了下面的情况:子controller区域发生了更新,父controller区域却无法更新。因为上面所说的JavaScript的原型链检索并不对更新启用,对于基础类型JavaScript在自身对象(这里是子$scope)上创建了一个同名的变量。你也想可以从下面angular调试插件batarang截图中看出来。一旦利用子controller的input控件修改了greet属性,再次之后我再次尝试修改父controller区域的greet属性,子controller区别不会在像初始化时候那样及时同步了,它们之间完全独立了,各自拥有了自己的greet属性。

ngModel-controller继承

batarang插件截图

ngModel-controller继承

经过上面的例子分析,相信作为读者的你已经能够理解这类由于继承链引用问题导致的ngModel不能更新问题了,请记住:这是JavaScript原型继承的issue,并不是angular的issue。

那么我们在子controller中如何更新父controller的属性值呢?这个问题已经很简单了,issue的问题在于没有启用原型链的检索,那么如果我们将ngModel的属性变为引用对象,换句话说:在ngModel的属性值中加了“.”,那么在JavaScript的原型链检索就会启动了。

HTML:

<div ng-controller="ParentCtrl">
    <div class="form-group">
        <h4>Parent Controller:</h4>
        <pre></pre>
        <input type="text" ng-model="vm.greet" class="form-control" />
    </div>
    <div ng-controller="ChildCtrl">
        <div class="form-group">
            <h4>Child controller:</h4>
            <pre></pre>
            <input type="text" ng-model="vm.greet" class="form-control" />
        </div>
    </div>
</div>

JavaScript:

angular.module("com.ngbook.demo", [])
    .controller("ParentCtrl", ["$scope", function($scope) {

        $scope.vm = {
            greet: "hello angular!"
        };

    }])
    .controller("ChildCtrl", angular.noop);

jsbin demo: http://jsbin.com/metufi/1/edit?html,js,output

这里在ngModel属性值多引入了“vm”变量,这个时候,不管我们尝试修改greet值,整个页面都会得到相应的同步。关于这个问题,作者更推荐使用angular 1.2后的controller as vm的方式解决,更多的信息请阅读《使用controller as vm方式.md》一节。

angular中的MVVM模式

在开始介绍angular原理之前,我们有必要先了解下mvvm模式在angular中运用。虽然在angular社区一直将angular统称为前端MVC框架,同时angular团队也称它为MVW(Whatever)框架,但angular框架整体上更接近MVVM模式。下面是Igor Minar发布在Google+ https://plus.google.com/+IgorMinar/posts/DRUAkZmXjNV的文章内容:

MVC vs MVVM vs MVP. What a controversial topic that many developers can spend hours and hours debating and arguing about.

For several years +AngularJS was closer to MVC (or rather one of its client-side variants), but over time and thanks to many refactorings and api improvements, it’s now closer to MVVM – the $scope object could be considered the ViewModel that is being decorated by a function that we call a Controller.

Being able to categorize a framework and put it into one of the MV* buckets has some advantages. It can help developers get more comfortable with its apis by making it easier to create a mental model that represents the application that is being built with the framework. It can also help to establish terminology that is used by developers.

Having said, I’d rather see developers build kick-ass apps that are well-designed and follow separation of concerns, than see them waste time arguing about MV* nonsense. And for this reason, I hereby declare AngularJS to be MVW framework – Model-View-Whatever. Where Whatever stands for “whatever works for you”.

Angular gives you a lot of flexibility to nicely separate presentation logic from business logic and presentation state. Please use it fuel your productivity and application maintainability rather than heated discussions about things that at the end of the day don’t matter that much.

在文中特别指出angular在多次的API重构和改善,它越来越接近于MVVM模式,$scope可以被认为是ViewModl,而Controller则是装饰、加工处理这个ViewModel的JavaScript函数。作者更希望大家关注于实现一个成功的,具有好的设计以及遵循“分离关注点”原则的应用程序,而不是去争论MV*,所以他将angular称为MVW框架,是什么并不重要,只要适合你的应用就行。

MVVM模式是Model-View-ViewMode(模型-视图-视图模型)模式的简称,其最早出现在微软的WPF和Silverlight框架中。MVVM模式利用框架内置的双向绑定技术对MVP(Model-View-Presenter)模式的变型,引入了专门的ViewModel(视图模型)来实现View和Model的粘合,让View和Model的进一步分离和解耦。MVVM模式的优势有如下四点:

  1. 低耦合:View可以独立于Model变化和修改,同一个ViewModel可以被多个View复用;并且可以做到View和Model的变化互不影响;
  2. 可重用性:可以把一些视图的逻辑放在ViewModel,让多个View复用;
  3. 独立开发:开发人员可以专注与业务逻辑和数据的开发(ViewModel),界面设计人员可以专注于UI(View)的设计;
  4. 可测试性:清晰的View分层,使得针对表现层业务逻辑的测试更容易,更简单。

下面是angular中关于MVVM模式的运用:

angular mvvm

在angular中MVVM模式主要分为四部分:

  1. View:它专注于界面的显示和渲染,在angular中则是包含一堆声明式Directive的视图模板。
  2. ViewModel:它是View和Model的粘合体,负责View和Model的交互和协作,它负责给View提供显示的数据,以及提供了View中Command事件操作Model的途径;在angular中$scope对象充当了这个ViewModel的角色;
  3. Model:它是与应用程序的业务逻辑相关的数据的封装载体,它是业务领域的对象,Model并不关心会被如何显示或操作,所以模型也不会包含任何界面显示相关的逻辑。在web页面中,大部分Model都是来自Ajax的服务端返回数据或者是全局的配置对象;而angular中的service则是封装和处理这些与Model相关的业务逻辑的场所,这类的业务服务是可以被多个Controller或者其他service复用的领域服务。
  4. Controller:这并不是MVVM模式的核心元素,但它负责ViewModel对象的初始化,它将组合一个或者多个service来获取业务领域Model放在ViewModel对象上,使得应用界面在启动加载的时候达到一种可用的状态。

View不能直接与Model交互,而是通过$scope这个ViewModel来实现与Model的交互。对于界面表单的交互,通过ngModel指令来实现View和ViewModel的同步。ngModelController包含$parsers和$formatters两个转换器管道,它们分别实现View表单输入值到Model数据类型转换和Model数据到View表单数据的格式化。对于用户界面的交互Command事件(如ngClick、ngChange等)则会转发到ViewModel对象上,通过ViewModel来实现对于Model的改变。然而对于Model的任何改变,也会反应在ViewModel之上,并且会通过$scope的“脏检查机制”($digest)来更新到View。从而实现View和Model的分离,达到对前端逻辑MVVM的分层架构。

angular中MVVM模式的实现,以领域Model为中心思维,遵循“分离关注点”设计原则,这也是与jQuery以DOM驱动的思维所不同之处。所以我们在做angular开发的时候应该谨记下面几点:

绝不要先设计你的页面,然后用DOM操作去改变它

在以往的jQuery开发中,我们会首先设计页面DOM结构,然后在利用jQuery来改变DOM结构或者实现动态交互效果。因为jQuery是为DOM驱动而设计的,对于拥有大量复杂的前端交互的项目,JavaScript的逻辑变得越来越臃肿,交互逻辑分散各处。

在MVVM模式下的angular开发中, 我们首先需要在脑子里挂着Model的弦。不能老想着“我有XXX这个DOM,我希望让它做XXX这种动态效果”,我们需要从要完成的目标开始思考我们需要或拥有怎么样的Model数据,然后设计我们的应用, 最后才是设计视图,并用$scope来粘合它们。

Directive不是封装jQuery代码的“天堂”

如上条所述,我们不能一开始就去想如何利用DOM操作的方法去实现应用目标,然后“冠冕堂皇”的写上一堆jQuery的代码,并将其封装到angular的directive中,最后不得不加上$scope.$apply()来通知angular你的ViewModel的改变,需要启动“脏检查机制”来更新你的改变到View。作者在多个客户项目中看见这种将Directive作为封装jQuery代码“天堂”的例子,其实对于这类问题,大部分情况下,我们都可以用很少了angular代码将其重构为真正的angular way。特别在ng社区经常看见在angular directive中利用jQuery的on方法绑定click、keydown、blur等事件的代码,大部分情况我们都能以对应的ng事件(ngClick、ngChange、ngBlur)来重构它们。

对于这类问题,首先我们应该尽量尝试复用angular的内置指令,以真正的angular way去思考我们的问题,请慎重的引入jQuery的DOM方法和操作。

关于angular MVVM模式的资料,你还可以参考视频:https://frontendmasters.com/courses/angularjs-mvc-mvvm-mvwhatever/#v=ypur7bfbcq

JavaScript函数柯里化

函数式

JavaScript是以函数为一等公民的函数式语言。函数在JavaScript中也是一个对象(继承制Function),函数也可以作为参数传递成函数变量。最近几年函数式也因为其无副作用的特性、透明性、惰性计算等在高并发,大数据领域火起来了。

JavaScript中也有如Underscore、lodash之类的函数式库,如lodash的使用方式:

1
2
3
4
5
6
7
var names = _.chain(users)
  .map(function(user){
    return user.user;
  })
  .join(" , ")
  .value();
console.log(names);

关于lodash更多内容请参考JavaScript工具库之Lodash.

柯里化

今天文章将以高阶函数中的柯里化方式来,看看JavaScript的函数式能力。

在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。这个技术由 Christopher Strachey 以逻辑学家 Haskell Curry 命名的,尽管它是 Moses Schnfinkel 和 Gottlob Frege 发明的。

在理论计算机科学中,柯里化提供了在简单的理论模型中比如只接受一个单一参数的lambda 演算中研究带有多个参数的函数的方式。

JavaScript的柯里化实现

下边的例子,我们将把柯里化方式泛化为接受任意个参数,直到声明的方法参数个数饱和才执行,所以根据参数个数可以有多种柯里化函数产生。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
(function(global) {
    var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m,
        FN_ARG_SPLIT = /,/,
        FN_ARG = /^\s*(_?)(\S+?)\1\s*$/,
        STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;

    var getArgLength = function(func) {
        var fnText = func.toString().replace(STRIP_COMMENTS, '');
        var argDecl = fnText.match(FN_ARGS);
        var params = [];
        argDecl[1].split(FN_ARG_SPLIT).forEach(function(arg) {
            arg.replace(FN_ARG, function(all, underscore, name) {
                params.push(name);
            });
        });
        return params.length;
    };

    var curryFunc = function(func, len) {
        len = len || getArgLength(func);
        var args = [];
        if (len === 0) {
            return func.apply(null);
        }

        return function() {
            [].push.apply(args, [].slice.apply(arguments));
            if (args.length >= len) {
                return func.apply(null, args);
            }

            return arguments.callee;
        };
    };

    global.curryFunc = curryFunc;
})(this);

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

console.log("result 1:", curryFunc(add)(1, 2)(3));
console.log("result 2:", curryFunc(add)(1)(2, 3));
console.log("result 3:", curryFunc(add)(1)(3)(2));

function add(x, y, z) {
    return x * y * z;
}

console.log("result 1:", curryFunc(add)(2, 4)(6));
console.log("result 2:", curryFunc(add)(2)(4, 6));
console.log("result 3:", curryFunc(add)(2)(6)(4));

function sayHello() {
    return "hello";
}
console.log(curryFunc(sayHello));

首先上面会利用正则来获取传入函数的参数个数。再返回一个函数的代理,每次的调用都会将传入参数缓存在args临时变量中,直到参数个数饱和才会立即执行。代码比较冗长,慢慢品味,当然有不足之处,也希望大家指出来。

前端HTML-CSS规范

目录

黄金定律

一个项目应该永远遵循同一套编码规范!

不管有多少人共同参与同一项目,一定要确保每一行代码都像是同一个人编写的。

HTML

语法

  • 用两个空格来代替制表符(tab) – 这是唯一能保证在所有环境下获得一致展现的方法。
  • 嵌套元素应当缩进一次(即两个空格)。
  • 对于属性的定义,确保全部使用双引号,绝不要使用单引号。
  • 不要在自闭合(self-closing)元素的尾部添加斜线 – HTML5 规范中明确说明这是可选的。
  • 不要省略可选的结束标签(closing tag)(例如,</li></body>)。
<!DOCTYPE html>
<html>
  <head>
    <title>Page title</title>
  </head>
  <body>
    <img src="images/company-logo.png" alt="Company">
    <h1 class="hello-world">Hello, world!</h1>
  </body>
</html>

HTML5 doctype

为每个 HTML 页面的第一行添加标准模式(standard mode)的声明,这样能够确保在每个浏览器中拥有一致的展现。

<!DOCTYPE html>
<html>
  <head>
  </head>
</html>

语言属性

根据 HTML5 规范:

强烈建议为 html 根元素指定 lang 属性,从而为文档设置正确的语言。这将有助于语音合成工具确定其所应该采用的发音,有助于翻译工具确定其翻译时所应遵守的规则等等。

更多关于 lang 属性的知识可以从 此规范 中了解。

这里列出了语言代码表

<html lang="zh-CN">
  <!-- ... -->
</html>

IE 兼容模式

IE 支持通过特定的 <meta> 标签来确定绘制当前页面所应该采用的 IE 版本。除非有强烈的特殊需求,否则最好是设置为 edge mode,从而通知 IE 采用其所支持的最新的模式。

阅读这篇 stack overflow 上的文章可以获得更多有用的信息。

<meta http-equiv="X-UA-Compatible" content="IE=Edge">

字符编码

通过明确声明字符编码,能够确保浏览器快速并容易的判断页面内容的渲染方式。这样做的好处是,可以避免在 HTML 中使用字符实体标记(character entity),从而全部与文档编码一致(一般采用 UTF-8 编码)。

<head>
  <meta charset="UTF-8">
</head>

引入 CSS 和 JavaScript 文件

根据 HTML5 规范,在引入 CSS 和 JavaScript 文件时一般不需要指定 type 属性,因为 text/csstext/javascript 分别是它们的默认值。

HTML5 spec links

<!-- External CSS -->
<link rel="stylesheet" href="code-guide.css">

<!-- In-document CSS -->
<style>
  /* ... */
</style>

<!-- JavaScript -->
<script src="code-guide.js"></script>

实用为王

尽量遵循 HTML 标准和语义,但是不要以牺牲实用性为代价。任何时候都要尽量使用最少的标签并保持最小的复杂度。

属性顺序

HTML 属性应当按照以下给出的顺序依次排列,确保代码的易读性。

  • class
  • id, name
  • data-*
  • src, for, type, href
  • title, alt
  • aria-*, role

class 用于标识高度可复用组件,因此应该排在首位。id 用于标识具体组件,应当谨慎使用(例如,页面内的书签),因此排在第二位。

<a class="..." id="..." data-modal="toggle" href="#">
  Example link
</a>

<input class="form-control" type="text">

<img src="..." alt="...">

布尔(boolean)型属性

布尔型属性可以在声明时不赋值。XHTML 规范要求为其赋值,但是 HTML5 规范不需要。

更多信息请参考 WhatWG section on boolean attributes

元素的布尔型属性如果有值,就是 true,如果没有值,就是 false。

如果一定要为其赋值的话,请参考 WhatWG 规范:

如果属性存在,其值必须是空字符串或 […] 属性的规范名称,并且不要再收尾添加空白符。

简单来说,就是不用赋值。

<input type="text" disabled>

<input type="checkbox" value="1" checked>

<select>
  <option value="1" selected>1</option>
</select>

减少标签的数量

编写 HTML 代码时,尽量避免多余的父元素。很多时候,这需要迭代和重构来实现。请看下面的案例:

<!-- Not so great -->
<span class="avatar">
  <img src="...">
</span>

<!-- Better -->
<img class="avatar" src="...">

JavaScript 生成的标签

通过 JavaScript 生成的标签让内容变得不易查找、编辑,并且降低性能。能避免时尽量避免。

CSS

语法

  • 用两个空格来代替制表符(tab) – 这是唯一能保证在所有环境下获得一致展现的方法。
  • 为选择器分组时,将单独的选择器单独放在一行。
  • 为了代码的易读性,在每个声明块的左花括号前添加一个空格。
  • 声明块的右花括号应当单独成行。
  • 每条声明语句的 : 后应该插入一个空格。
  • 为了获得更准确的错误报告,每条声明都应该独占一行。
  • 所有声明语句都应当以分号结尾。最后一条声明语句后面的分号是可选的,但是,如果省略这个分号,你的代码可能更易出错。
  • 对于以逗号分隔的属性值,每个逗号后面都应该插入一个空格(例如,box-shadow)。
  • 不要在 rgb()rgba()hsl()hsla()rect() 值的内部的逗号后面插入空格。这样利于从多个属性值(既加逗号也加空格)中区分多个颜色值(只加逗号,不加空格)。
  • 对于属性值或颜色参数,省略小于 1 的小数前面的 0 (例如,.5 代替 0.5-.5px 代替 -0.5px)。
  • 十六进制值应该全部小写,例如,#fff。在扫描文档时,小写字符易于分辨,因为他们的形式更易于区分。
  • 尽量使用简写形式的十六进制值,例如,用 #fff 代替 #ffffff
  • 为选择器中的属性添加双引号,例如,input[type="text"]只有在某些情况下是可选的,但是,为了代码的一致性,建议都加上双引号。
  • 避免为 0 值指定单位,例如,用 margin: 0; 代替 margin: 0px;

对于这里用到的术语有疑问吗?请参考 Wikipedia 上的 syntax section of the Cascading Style Sheets article

/* Bad CSS */
.selector, .selector-secondary, .selector[type=text] {
  padding:15px;
  margin:0px 0px 15px;
  background-color:rgba(0, 0, 0, 0.5);
  box-shadow:0px 1px 2px #CCC,inset 0 1px 0 #FFFFFF
}

/* Good CSS */
.selector,
.selector-secondary,
.selector[type="text"] {
  padding: 15px;
  margin-bottom: 15px;
  background-color: rgba(0,0,0,.5);
  box-shadow: 0 1px 2px #ccc, inset 0 1px 0 #fff;
}

声明顺序

相关的属性声明应当归为一组,并按照下面的顺序排列:

  1. Positioning
  2. Box model
  3. Typographic
  4. Visual

由于定位(positioning)可以从正常的文档流中移除元素,并且还能覆盖盒模型(box model)相关的样式,因此排在首位。盒模型排在第二位,因为它决定了组件的尺寸和位置。

其他属性只是影响组件的内部(inside)或者是不影响前两组属性,因此排在后面。

完整的属性列表及其排列顺序请参考 Recess

.declaration-order {
  /* Positioning */
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 100;

  /* Box-model */
  display: block;
  float: right;
  width: 100px;
  height: 100px;

  /* Typography */
  font: normal 13px "Helvetica Neue", sans-serif;
  line-height: 1.5;
  color: #333;
  text-align: center;

  /* Visual */
  background-color: #f5f5f5;
  border: 1px solid #e5e5e5;
  border-radius: 3px;

  /* Misc */
  opacity: 1;
}

不要使用 @import

<link> 标签相比,@import 指令要慢很多,不光增加了额外的请求次数,还会导致不可预料的问题。替代办法有以下几种:

  • 使用多个 <link> 元素
  • 通过 Sass 或 Less 类似的 CSS 预处理器将多个 CSS 文件编译为一个文件
  • 通过 Rails、Jekyll 或其他系统中提供过 CSS 文件合并功能

请参考 Steve Souders 的文章了解更多知识。

<!-- Use link elements -->
<link rel="stylesheet" href="core.css">

<!-- Avoid @imports -->
<style>
  @import url("more.css");
</style>

媒体查询(Media query)的位置

将媒体查询放在尽可能相关规则的附近。不要将他们打包放在一个单一样式文件中或者放在文档底部。如果你把他们分开了,将来只会被大家遗忘。下面给出一个典型的实例。

.element { ... }
.element-avatar { ... }
.element-selected { ... }

@media (min-width: 480px) {
  .element { ...}
  .element-avatar { ... }
  .element-selected { ... }
}

带前缀的属性

当使用特定厂商的带有前缀的属性时,通过缩进的方式,让每个属性的值在垂直方向对齐,这样便于多行编辑。

在 Textmate 中,使用 Text → Edit Each Line in Selection (⌃⌘A)。在 Sublime Text 2 中,使用 Selection → Add Previous Line (⌃⇧↑) 和 Selection → Add Next Line (⌃⇧↓)。

/* Prefixed properties */
.selector {
  -webkit-box-shadow: 0 1px 2px rgba(0,0,0,.15);
          box-shadow: 0 1px 2px rgba(0,0,0,.15);
}

单行规则声明

对于只包含一条声明的样式,为了易读性和便于快速编辑,建议将语句放在同一行。对于带有多条声明的样式,还是应当将声明分为多行。

这样做的关键因素是为了错误检测 – 例如,CSS 校验器指出在 183 行有语法错误。如果是单行单条声明,你就不会忽略这个错误;如果是单行多条声明的话,你就要仔细分析避免漏掉错误了。

/* Single declarations on one line */
.span1 { width: 60px; }
.span2 { width: 140px; }
.span3 { width: 220px; }

/* Multiple declarations, one per line */
.sprite {
  display: inline-block;
  width: 16px;
  height: 15px;
  background-image: url(../img/sprite.png);
}
.icon           { background-position: 0 0; }
.icon-home      { background-position: 0 -20px; }
.icon-account   { background-position: 0 -40px; }

简写形式的属性声明

在需要显示地设置所有值的情况下,应当尽量限制使用简写形式的属性声明。常见的滥用简写属性声明的情况如下:

  • padding
  • margin
  • font
  • background
  • border
  • border-radius

大部分情况下,我们不需要为简写形式的属性声明指定所有值。例如,HTML 的 heading 元素只需要设置上、下边距(margin)的值,因此,在必要的时候,只需覆盖这两个值就可以。过度使用简写形式的属性声明会导致代码混乱,并且会对属性值带来不必要的覆盖从而引起意外的副作用。

MDN(Mozilla Developer Network)上一片非常好的关于shorthand properties 的文章,对于不太熟悉简写属性声明及其行为的用户很有用。

/* Bad example */
.element {
  margin: 0 0 10px;
  background: red;
  background: url("image.jpg");
  border-radius: 3px 3px 0 0;
}

/* Good example */
.element {
  margin-bottom: 10px;
  background-color: red;
  background-image: url("image.jpg");
  border-top-left-radius: 3px;
  border-top-right-radius: 3px;
}

Less 和 Sass 中的嵌套

避免非必要的嵌套。这是因为虽然你可以使用嵌套,但是并不意味着应该使用嵌套。只有在必须将样式限制在父元素内(也就是后代选择器),并且存在多个需要嵌套的元素时才使用嵌套。

// Without nesting
.table > thead > tr > th {  }
.table > thead > tr > td {  }

// With nesting
.table > thead > tr {
  > th {  }
  > td {  }
}

注释

代码是由人编写并维护的。请确保你的代码能够自描述、注释良好并且易于他人理解。好的代码注释能够传达上下文关系和代码目的。不要简单地重申组件或 class 名称。

对于较长的注释,务必书写完整的句子;对于一般性注解,可以书写简洁的短语。

/* Bad example */
/* Modal header */
.modal-header {
  ...
}

/* Good example */
/* Wrapping element for .modal-title and .modal-close */
.modal-header {
  ...
}

class 命名

  • class 名称中只能出现小写字符和破折号(dashe)(不是下划线,也不是驼峰命名法)。破折号应当用于相关 class 的命名(类似于命名空间)(例如,.btn.btn-danger)。
  • 避免过度任意的简写。.btn 代表 button,但是 .s 不能表达任何意思。
  • class 名称应当尽可能短,并且意义明确。
  • 使用有意义的名称。使用有组织的或目的明确的名称,不要使用表现形式(presentational)的名称。
  • 基于最近的父 class 或基本(base) class 作为新 class 的前缀。
  • 使用 .js-* class 来标识行为(与样式相对),并且不要将这些 class 包含到 CSS 文件中。

在为 Sass 和 Less 变量命名是也可以参考上面列出的各项规范。

/* Bad example */
.t { ... }
.red { ... }
.header { ... }

/* Good example */
.tweet { ... }
.important { ... }
.tweet-header { ... }

选择器

  • 对于通用元素使用 class ,这样利于渲染性能的优化。
  • 对于经常出现的组件,避免使用属性选择器(例如,[class^="..."])。浏览器的性能会受到这些因素的影响。
  • 选择器要尽可能短,并且尽量限制组成选择器的元素个数,建议不要超过 3 。
  • 只有在必要的时候才将 class 限制在最近的父元素内(也就是后代选择器)(例如,不使用带前缀的 class 时 – 前缀类似于命名空间)。

扩展阅读:

/* Bad example */
span { ... }
.page-container #stream .stream-item .tweet .tweet-header .username { ... }
.avatar { ... }

/* Good example */
.avatar { ... }
.tweet-header .username { ... }
.tweet .avatar { ... }

代码组织

  • 以组件为单位组织代码段。
  • 制定一致的注释规范。
  • 使用一致的空白符将代码分隔成块,这样利于扫描较大的文档。
  • 如果使用了多个 CSS 文件,将其按照组件而非页面的形式分拆,因为页面会被重组,而组件只会被移动。
/*
 * Component section heading
 */

.element { ... }


/*
 * Component section heading
 *
 * Sometimes you need to include optional context for the entire component. Do that up here if it's important enough.
 */

.element { ... }

/* Contextual sub-component or modifer */
.element-heading { ... }

编辑器配置

将你的编辑器按照下面的配置进行设置,以避免常见的代码不一致和差异:

  • 用两个空格代替制表符(soft-tab 即用空格代表 tab 符)。
  • 保存文件时,删除尾部的空白符。
  • 设置文件编码为 UTF-8。
  • 在文件结尾添加一个空白行。

参照文档并将这些配置信息添加到项目的 .editorconfig 文件中。例如:Bootstrap 中的 .editorconfig 实例。更多信息请参考 about EditorConfig

前端JavaScript规范

JavaScript规范

目录

  1. 类型
  2. 对象
  3. 数组
  4. 字符串
  5. 函数
  6. 属性
  7. 变量
  8. 条件表达式和等号
  9. 注释
  10. 空白
  11. 逗号
  12. 分号
  13. 类型转换
  14. 命名约定
  15. 存取器
  16. 构造器
  17. 事件
  18. 模块
  19. jQuery
  20. ES5 兼容性
  21. HTML、CSS、JavaScript分离
  22. 使用jsHint
  23. 前端工具

类型

  • 原始值: 相当于传值(JavaScript对象都提供了字面量),使用字面量创建对象。

    • string
    • number
    • boolean
    • null
    • undefined
    var foo = 1,
        bar = foo;
    
    bar = 9;
    
    console.log(foo, bar); // => 1, 9
  • 复杂类型: 相当于传引用

    • object
    • array
    • function
    var foo = [1, 2],
        bar = foo;
    
    bar[0] = 9;
    
    console.log(foo[0], bar[0]); // => 9, 9

对象

  • 使用字面值创建对象

    // bad
    var item = new Object();
    
    // good
    var item = {};
  • 不要使用保留字 reserved words 作为键

    // bad
    var superman = {
      class: 'superhero',
      default: { clark: 'kent' },
      private: true
    };
    
    // good
    var superman = {
      klass: 'superhero',
      defaults: { clark: 'kent' },
      hidden: true
    };

数组

  • 使用字面值创建数组

    // bad
    var items = new Array();
    
    // good
    var items = [];
  • 如果你不知道数组的长度,使用push

    var someStack = [];
    
    
    // bad
    someStack[someStack.length] = 'abracadabra';
    
    // good
    someStack.push('abracadabra');
  • 当你需要拷贝数组时使用slice. jsPerf

    var len = items.length,
        itemsCopy = [],
        i;
    
    // bad
    for (i = 0; i < len; i++) {
      itemsCopy[i] = items[i];
    }
    
    // good
    itemsCopy = items.slice();
  • 使用slice将类数组的对象转成数组.

    function trigger() {
      var args = [].slice.apply(arguments);
      ...
    }

字符串

  • 对字符串使用单引号 ''(因为大多时候我们的字符串。特别html会出现")

    // bad
    var name = "Bob Parr";
    
    // good
    var name = 'Bob Parr';
    
    // bad
    var fullName = "Bob " + this.lastName;
    
    // good
    var fullName = 'Bob ' + this.lastName;
  • 超过80(也有规定140的,项目具体可制定)个字符的字符串应该使用字符串连接换行

  • 注: 如果过度使用,长字符串连接可能会对性能有影响. jsPerf & Discussion

    // bad
    var errorMessage = 'This is a super long error that was thrown because of Batman. When you stop to think about how Batman had anything to do with this, you would get nowhere fast.';
    
    // bad
    var errorMessage = 'This is a super long error that \
    was thrown because of Batman. \
    When you stop to think about \
    how Batman had anything to do \
    with this, you would get nowhere \
    fast.';
    
    
    // good
    var errorMessage = 'This is a super long error that ' +
      'was thrown because of Batman.' +
      'When you stop to think about ' +
      'how Batman had anything to do ' +
      'with this, you would get nowhere ' +
      'fast.';
  • 编程时使用join而不是字符串连接来构建字符串,特别是IE: jsPerf.

    var items,
        messages,
        length, i;
    
    messages = [{
        state: 'success',
        message: 'This one worked.'
    },{
        state: 'success',
        message: 'This one worked as well.'
    },{
        state: 'error',
        message: 'This one did not work.'
    }];
    
    length = messages.length;
    
    // bad
    function inbox(messages) {
      items = '<ul>';
    
      for (i = 0; i < length; i++) {
        items += '<li>' + messages[i].message + '</li>';
      }
    
      return items + '</ul>';
    }
    
    // good
    function inbox(messages) {
      items = [];
    
      for (i = 0; i < length; i++) {
        items[i] = messages[i].message;
      }
    
      return '<ul><li>' + items.join('</li><li>') + '</li></ul>';
    }

函数

  • 函数表达式:

    // 匿名函数表达式
    var anonymous = function() {
      return true;
    };
    
    // 有名函数表达式
    var named = function named() {
      return true;
    };
    
    // 立即调用函数表达式
    (function() {
      console.log('Welcome to the Internet. Please follow me.');
    })();
  • 绝对不要在一个非函数块里声明一个函数,把那个函数赋给一个变量。浏览器允许你这么做,但是它们解析不同。

  • 注: ECMA-262定义把定义为一组语句,函数声明不是一个语句。阅读ECMA-262对这个问题的说明.

    // bad
    if (currentUser) {
      function test() {
        console.log('Nope.');
      }
    }
    
    // good
    if (currentUser) {
      var test = function test() {
        console.log('Yup.');
      };
    }
  • 绝对不要把参数命名为 arguments, 这将会逾越函数作用域内传过来的 arguments 对象.

    // bad
    function nope(name, options, arguments) {
      // ...stuff...
    }
    
    // good
    function yup(name, options, args) {
      // ...stuff...
    }

属性

  • 当使用变量和特殊非法变量名时,访问属性时可以使用中括号(. 优先).

    var luke = {
      jedi: true,
      age: 28
    };
    
    function getProp(prop) {
      return luke[prop];
    }
    
    var isJedi = getProp('jedi');

变量

  • 总是使用 var 来声明变量,如果不这么做将导致产生全局变量,我们要避免污染全局命名空间。

    // bad
    superPower = new SuperPower();
    
    // good
    var superPower = new SuperPower();
  • 使用一个 var 以及新行声明多个变量,缩进4个空格。

    // bad
    var items = getItems();
    var goSportsTeam = true;
    var dragonball = 'z';
    
    // good
    var items = getItems(),
        goSportsTeam = true,
        dragonball = 'z';
  • 最后再声明未赋值的变量,当你想引用之前已赋值变量的时候很有用。

    // bad
    var i, len, dragonball,
        items = getItems(),
        goSportsTeam = true;
    
    // bad
    var i, items = getItems(),
        dragonball,
        goSportsTeam = true,
        len;
    
    // good
    var items = getItems(),
        goSportsTeam = true,
        dragonball,
        length,
        i;
  • 在作用域顶部声明变量,避免变量声明和赋值引起的相关问题。

    // bad
    function() {
      test();
      console.log('doing stuff..');
    
      //..other stuff..
    
      var name = getName();
    
      if (name === 'test') {
        return false;
      }
    
      return name;
    }
    
    // good
    function() {
      var name = getName();
    
      test();
      console.log('doing stuff..');
    
      //..other stuff..
    
      if (name === 'test') {
        return false;
      }
    
      return name;
    }
    
    // bad
    function() {
      var name = getName();
    
      if (!arguments.length) {
        return false;
      }
    
      return true;
    }
    
    // good
    function() {
      if (!arguments.length) {
        return false;
      }
    
      var name = getName();
    
      return true;
    }

条件表达式和等号

  • 合理使用 ===!== 以及 ==!=.
  • 合理使用表达式逻辑操作运算.
  • 条件表达式的强制类型转换遵循以下规则:

    • 对象 被计算为 true
    • Undefined 被计算为 false
    • Null 被计算为 false
    • 布尔值 被计算为 布尔的值
    • 数字 如果是 +0, -0, or NaN 被计算为 false , 否则为 true
    • 字符串 如果是空字符串 '' 则被计算为 false, 否则为 true
    if ([0]) {
      // true
      // An array is an object, objects evaluate to true
    }
  • 使用快捷方式.

    // bad
    if (name !== '') {
      // ...stuff...
    }
    
    // good
    if (name) {
      // ...stuff...
    }
    
    // bad
    if (collection.length > 0) {
      // ...stuff...
    }
    
    // good
    if (collection.length) {
      // ...stuff...
    }
  • 阅读 Truth Equality and JavaScript 了解更多

  • 给所有多行的块使用大括号

    // bad
    if (test)
      return false;
    
    // good
    if (test) return false;
    
    // good
    if (test) {
      return false;
    }
    
    // bad
    function() { return false; }
    
    // good
    function() {
      return false;
    }

注释

  • 使用 /** ... */ 进行多行注释,包括描述,指定类型以及参数值和返回值

    // bad
    // make() returns a new element
    // based on the passed in tag name
    //
    // @param <String> tag
    // @return <Element> element
    function make(tag) {
    
      // ...stuff...
    
      return element;
    }
    
    // good
    /**
     * make() returns a new element
     * based on the passed in tag name
     *
     * @param <String> tag
     * @return <Element> element
     */
    function make(tag) {
    
      // ...stuff...
    
      return element;
    }
  • 使用 // 进行单行注释,在评论对象的上面进行单行注释,注释前放一个空行.

    // bad
    var active = true;  // is current tab
    
    // good
    // is current tab
    var active = true;
    
    // bad
    function getType() {
      console.log('fetching type...');
      // set the default type to 'no type'
      var type = this._type || 'no type';
    
      return type;
    }
    
    // good
    function getType() {
      console.log('fetching type...');
    
      // set the default type to 'no type'
      var type = this._type || 'no type';
    
      return type;
    }
  • 如果你有一个问题需要重新来看一下或如果你建议一个需要被实现的解决方法的话需要在你的注释前面加上 FIXMETODO 帮助其他人迅速理解

    function Calculator() {
    
      // FIXME: shouldn't use a global here
      total = 0;
    
      return this;
    }
    function Calculator() {
    
      // TODO: total should be configurable by an options param
      this.total = 0;
    
      return this;
    }
  • 满足规范的文档,在需要文档的时候,可以尝试jsdoc.

空白

  • 缩进、格式化能帮助团队更快得定位修复代码BUG.
  • 将tab设为4个空格

    // bad
    function() {
    ∙∙var name;
    }
    
    // bad
    function() {var name;
    }
    
    // good
    function() {
    ∙∙∙∙var name;
    }
  • 大括号前放一个空格

    // bad
    function test(){
      console.log('test');
    }
    
    // good
    function test() {
      console.log('test');
    }
    
    // bad
    dog.set('attr',{
      age: '1 year',
      breed: 'Bernese Mountain Dog'
    });
    
    // good
    dog.set('attr', {
      age: '1 year',
      breed: 'Bernese Mountain Dog'
    });
  • 在做长方法链时使用缩进.

    // bad
    $('#items').find('.selected').highlight().end().find('.open').updateCount();
    
    // good
    $('#items')
      .find('.selected')
        .highlight()
        .end()
      .find('.open')
        .updateCount();
    
    // bad
    var leds = stage.selectAll('.led').data(data).enter().append('svg:svg').class('led', true)
        .attr('width',  (radius + margin) * 2).append('svg:g')
        .attr('transform', 'translate(' + (radius + margin) + ',' + (radius + margin) + ')')
        .call(tron.led);
    
    // good
    var leds = stage.selectAll('.led')
        .data(data)
      .enter().append('svg:svg')
        .class('led', true)
        .attr('width',  (radius + margin) * 2)
      .append('svg:g')
        .attr('transform', 'translate(' + (radius + margin) + ',' + (radius + margin) + ')')
        .call(tron.led);

逗号

  • 不要将逗号放前面

    // bad
    var once
      , upon
      , aTime;
    
    // good
    var once,
        upon,
        aTime;
    
    // bad
    var hero = {
        firstName: 'Bob'
      , lastName: 'Parr'
      , heroName: 'Mr. Incredible'
      , superPower: 'strength'
    };
    
    // good
    var hero = {
      firstName: 'Bob',
      lastName: 'Parr',
      heroName: 'Mr. Incredible',
      superPower: 'strength'
    };
  • 不要加多余的逗号,这可能会在IE下引起错误,同时如果多一个逗号某些ES3的实现会计算多数组的长度。

    // bad
    var hero = {
      firstName: 'Kevin',
      lastName: 'Flynn',
    };
    
    var heroes = [
      'Batman',
      'Superman',
    ];
    
    // good
    var hero = {
      firstName: 'Kevin',
      lastName: 'Flynn'
    };
    
    var heroes = [
      'Batman',
      'Superman'
    ];

分号

  • 语句结束一定要加分号

    // bad
    (function() {
      var name = 'Skywalker'
      return name
    })()
    
    // good
    (function() {
      var name = 'Skywalker';
      return name;
    })();
    
    // good
    ;(function() {
      var name = 'Skywalker';
      return name;
    })();

类型转换

  • 在语句的开始执行类型转换.
  • 字符串:

    //  => this.reviewScore = 9;
    
    // bad
    var totalScore = this.reviewScore + '';
    
    // good
    var totalScore = '' + this.reviewScore;
    
    // bad
    var totalScore = '' + this.reviewScore + ' total score';
    
    // good
    var totalScore = this.reviewScore + ' total score';
  • 对数字使用 parseInt 并且总是带上类型转换的基数.,如parseInt(value, 10)

    var inputValue = '4';
    
    // bad
    var val = new Number(inputValue);
    
    // bad
    var val = +inputValue;
    
    // bad
    var val = inputValue >> 0;
    
    // bad
    var val = parseInt(inputValue);
    
    // good
    var val = Number(inputValue);
    
    // good
    var val = parseInt(inputValue, 10);
    
    // good
    /**
     * parseInt was the reason my code was slow.
     * Bitshifting the String to coerce it to a
     * Number made it a lot faster.
     */
    var val = inputValue >> 0;
  • 布尔值:

    var age = 0;
    
    // bad
    var hasAge = new Boolean(age);
    
    // good
    var hasAge = Boolean(age);
    
    // good
    var hasAge = !!age;

命名约定

  • 避免单个字符名,让你的变量名有描述意义。

    // bad
    function q() {
      // ...stuff...
    }
    
    // good
    function query() {
      // ..stuff..
    }
  • 当命名对象、函数和实例时使用驼峰命名规则

    // bad
    var OBJEcttsssss = {};
    var this_is_my_object = {};
    var this-is-my-object = {};
    function c() {};
    var u = new user({
      name: 'Bob Parr'
    });
    
    // good
    var thisIsMyObject = {};
    function thisIsMyFunction() {};
    var user = new User({
      name: 'Bob Parr'
    });
  • 当命名构造函数或类时使用驼峰式大写

    // bad
    function user(options) {
      this.name = options.name;
    }
    
    var bad = new user({
      name: 'nope'
    });
    
    // good
    function User(options) {
      this.name = options.name;
    }
    
    var good = new User({
      name: 'yup'
    });
  • 命名私有属性时前面加个下划线 _

    // bad
    this.__firstName__ = 'Panda';
    this.firstName_ = 'Panda';
    
    // good
    this._firstName = 'Panda';
  • 当保存对 this 的引用时使用 self(python 风格),避免this issue.Angular建议使用vm(MVVM模式中view-model)

    // good
    function() {
      var self = this;
      return function() {
        console.log(self);
      };
    }

存取器

  • 属性的存取器函数不是必需的
  • 如果你确实有存取器函数的话使用getVal() 和 setVal(‘hello’),java getter、setter风格或者jQuery风格

  • 如果属性是布尔值,使用isVal() 或 hasVal()

    // bad
    if (!dragon.age()) {
      return false;
    }
    
    // good
    if (!dragon.hasAge()) {
      return false;
    }
  • 可以创建get()和set()函数,但是要保持一致

    function Jedi(options) {
      options || (options = {});
      var lightsaber = options.lightsaber || 'blue';
      this.set('lightsaber', lightsaber);
    }
    
    Jedi.prototype.set = function(key, val) {
      this[key] = val;
    };
    
    Jedi.prototype.get = function(key) {
      return this[key];
    };

构造器

  • 给对象原型分配方法,而不是用一个新的对象覆盖原型,覆盖原型会使继承出现问题。

    function Jedi() {
      console.log('new jedi');
    }
    
    // bad
    Jedi.prototype = {
      fight: function fight() {
        console.log('fighting');
      },
    
      block: function block() {
        console.log('blocking');
      }
    };
    
    // good
    Jedi.prototype.fight = function fight() {
      console.log('fighting');
    };
    
    Jedi.prototype.block = function block() {
      console.log('blocking');
    };
  • 方法可以返回 this 帮助方法可链。

    // bad
    Jedi.prototype.jump = function() {
      this.jumping = true;
      return true;
    };
    
    Jedi.prototype.setHeight = function(height) {
      this.height = height;
    };
    
    var luke = new Jedi();
    luke.jump(); // => true
    luke.setHeight(20) // => undefined
    
    // good
    Jedi.prototype.jump = function() {
      this.jumping = true;
      return this;
    };
    
    Jedi.prototype.setHeight = function(height) {
      this.height = height;
      return this;
    };
    
    var luke = new Jedi();
    
    luke.jump()
      .setHeight(20);
  • 可以写一个自定义的toString()方法,但是确保它工作正常并且不会有副作用。

    function Jedi(options) {
      options || (options = {});
      this.name = options.name || 'no name';
    }
    
    Jedi.prototype.getName = function getName() {
      return this.name;
    };
    
    Jedi.prototype.toString = function toString() {
      return 'Jedi - ' + this.getName();
    };

事件

  • 当给事件附加数据时,传入一个哈希而不是原始值,这可以让后面的贡献者加入更多数据到事件数据里而不用找出并更新那个事件的事件处理器

    // bad
    $(this).trigger('listingUpdated', listing.id);
    
    ...
    
    $(this).on('listingUpdated', function(e, listingId) {
      // do something with listingId
    });

    更好:

    // good
    $(this).trigger('listingUpdated', { listingId : listing.id });
    
    ...
    
    $(this).on('listingUpdated', function(e, data) {
      // do something with data.listingId
    });

模块

  • 这个文件应该以驼峰命名,并在同名文件夹下,同时导出的时候名字一致
  • 对于公开API库可以考虑加入一个名为noConflict()的方法来设置导出的模块为之前的版本并返回它
  • 总是在模块顶部声明 'use strict';,引入[JSHint规范](http://jshint.com/)

    // fancyInput/fancyInput.jsfunction(global) {
      'use strict';
    
      var previousFancyInput = global.FancyInput;
    
      function FancyInput(options) {
        this.options = options || {};
      }
    
      FancyInput.noConflict = function noConflict() {
        global.FancyInput = previousFancyInput;
        return FancyInput;
      };
    
      global.FancyInput = FancyInput;
    })(this);

jQuery

  • 对于jQuery对象以$开头,以和原生DOM节点区分。

    // bad
    var menu = $(".menu");
    
    // good
    var $menu = $(".menu");
  • 缓存jQuery查询

    // bad
    function setSidebar() {
      $('.sidebar').hide();
    
      // ...stuff...
    
      $('.sidebar').css({
        'background-color': 'pink'
      });
    }
    
    // good
    function setSidebar() {
      var $sidebar = $('.sidebar');
      $sidebar.hide();
    
      // ...stuff...
    
      $sidebar.css({
        'background-color': 'pink'
      });
    }
  • 对DOM查询使用级联的 $('.sidebar ul')$('.sidebar ul')jsPerf

  • 对有作用域的jQuery对象查询使用 find

    // bad
    $('.sidebar', 'ul').hide();
    
    // bad
    $('.sidebar').find('ul').hide();
    
    // good
    $('.sidebar ul').hide();
    
    // good
    $('.sidebar > ul').hide();
    
    // good (slower)
    $sidebar.find('ul');
    
    // good (faster)
    $($sidebar[0]).find('ul');
  • 每个页面只使用一次document的ready事件,这样便于调试与行为流跟踪。

    $(function(){
       //do your page init.  
    });
  • 事件利用jQuery.on从页面分离到JavaScript文件。

    // bad
    <a id="myLink" href="#" onclick="myEventHandler();"></a>
    
    // good
    <a id="myLink" href="#"></a>
    
    $("#myLink").on("click", myEventHandler);
    
  • 对于Ajax使用promise方式。

        // bad
        $.ajax({
            ...
            success : function(){
            },
            error : function(){
            } 
        })
    
        // good
        $.ajax({.
            ..
        }).then( function( ){
            // success
        }, function( ){
            // error
        })
  • 利用promise的deferred对象解决延迟注册问题。

    var dtd = $.Deferred(); // 新建一个deferred对象
      var wait = function(dtd){
        var tasks = function(){
          alert("执行完毕!");
          dtd.resolve(); // 改变deferred对象的执行状态
        };
        setTimeout(tasks,5000);
        return dtd;
      };
  • HTML中Style、以及JavaScript中style移到CSS中class,在HTML、JavaScript中引入class,而不是直接style。

ECMAScript 5兼容性

尽量采用ES5方法,特别数组map、filter、forEach方法简化日常开发。在老式IE浏览器中引入ES5-shim。或者也可以考虑引入underscorelodash 常用辅助库.
- 参考Kangax的 ES5 compatibility table
- JavaScript工具库之Lodash
- Babel-现在开始使用 ES6

HTML、CSS、JavaScript分离

  • 页面DOM结构使用HTML,样式则采用CSS,动态DOM操作JavaScript。不要混用在HTML中
  • 分离在不同类型文件,文件link。
  • HTML、CSS、JavaScript变量名都需要有业务价值。CSS以中划线分割的全小写命名,JavaScript则首字母小写的驼峰命名。
  • CSS可引入Bootstrap、Foundation等出名响应式设计框架。以及SASS、LESS工具书写CSS。
  • 对于CSS、JavaScript建议合并为单文件,减少Ajax的连接数。也可以引入AMD(Require.js)加载方式。
  • 对于内部大部分企业管理系统,可以尝试采用前端 MVC框架组织代码。如Angular、React + flux架构、Knockout等。
  • 对于兼容性可用Modernizr规范库辅助。

使用jsHint

  • 前端项目中推荐引入jshint插件来规范项目编码规范。以及一套完善的IDE配置。
  • 注意:jshint需要引入nodejs 工具grunt或gulp插件,建议企业级nodejs npm私服。

前端工具

  • 前端第三方JavaScript包管理工具bower(bower install jQuery),bower可以实现第三方库的依赖解析、下载、升级管理等。建议建立企业级bower私服。
  • 前端构建工具,可以采用grunt或者gulp工具,可以实现html、css、js压缩、验证、测试,文件合并、watch和liveload等所有前端任务。建议企业级nodejs npm私服。
  • 前端开发IDE: WebStorm( Idea )、Sublime为最佳 。项目组统一IDE。IDE统一配置很重要。

TypeScript - Modules(模块)

简介

在TypeScript中提供了module(模块)的方式管理和组织代码结构。这里覆盖了内部和外部的模块,以及在何时怎么使用它。当然也有一些高级话题如何使用外部模块以及如何组织公用模块。

第一步

让我们从下面的例子开始,这个例子将会贯通本文。首先我们写了一段字符串验证的例子,用来验证用户在web页面form表单输入的信息,或者是外部文件导出的数据信息。

单文件的验证逻辑

interface StringValidator {
    isAcceptable(s: string): boolean;
}

var lettersRegexp = /^[A-Za-z]+$/;
var numberRegexp = /^[0-9]+$/;

class LettersOnlyValidator implements StringValidator {
    isAcceptable(s: string) {
        return lettersRegexp.test(s);
    }
}

class ZipCodeValidator implements StringValidator {
    isAcceptable(s: string) {
        return s.length === 5 && numberRegexp.test(s);
    }
}

// Some samples to try
var strings = ['Hello', '98052', '101'];
// Validators to use
var validators: { [s: string]: StringValidator; } = {};
validators['ZIP code'] = new ZipCodeValidator();
validators['Letters only'] = new LettersOnlyValidator();
// Show whether each string passed each validator
strings.forEach(s => {
    for (var name in validators) {
        console.log('"' + s + '" ' + (validators[name].isAcceptable(s) ? ' matches ' : ' does not match ') + name);
    }
});

增加模块化

我们将会增加更多的验证逻辑,并且我们希望有个好的代码组织来避免全部的污染和命名冲突。所以更好的方式是给一个命名空间来 组织我们的对象在一个模块中。

在下面我们将把所有的验证逻辑移到一个叫‘Validation’的模块来组织我们的代码逻辑。因为我们希望interface和class在外部模块可见,所以我们加上了‘export’关键字导出成员。相反lettersRegexp和numberRegexp的逻辑是你不实现,我们并不希望暴露给外部模块。在文件最后的测试代码是放在外部模块,例如Validation.LettersOnlyValidator之类的。

模块化的Validators

module Validation {
    export interface StringValidator {
        isAcceptable(s: string): boolean;
    }

    var lettersRegexp = /^[A-Za-z]+$/;
    var numberRegexp = /^[0-9]+$/;

    export class LettersOnlyValidator implements StringValidator {
        isAcceptable(s: string) {
            return lettersRegexp.test(s);
        }
    }

    export class ZipCodeValidator implements StringValidator {
        isAcceptable(s: string) {
            return s.length === 5 && numberRegexp.test(s);
        }
    }
}

// Some samples to try
var strings = ['Hello', '98052', '101'];
// Validators to use
var validators: { [s: string]: Validation.StringValidator; } = {};
validators['ZIP code'] = new Validation.ZipCodeValidator();
validators['Letters only'] = new Validation.LettersOnlyValidator();
// Show whether each string passed each validator
strings.forEach(s => {
    for (var name in validators) {
        console.log('"' + s + '" ' + (validators[name].isAcceptable(s) ? ' matches ' : ' does not match ') + name);
    }
});

分离模块文件

随之项目代码的增加,我们希望能够分离文件,让项目更好的维护和开发。

在下面我们将分离我们的验证逻辑为多个文件。虽然分离在多个文件,但是我们仍然可以共享一个命名空间,因为需要告诉编译器文件之间的关系,所以我在文件开始加入了reference tags得标记。再对于我们的测试代码没有什么改变。

多文件的内部模块

Validation.ts
module Validation {
    export interface StringValidator {
        isAcceptable(s: string): boolean;
    }
}
LettersOnlyValidator.ts
/// <reference path="Validation.ts" />
module Validation {
    var lettersRegexp = /^[A-Za-z]+$/;
    export class LettersOnlyValidator implements StringValidator {
        isAcceptable(s: string) {
            return lettersRegexp.test(s);
        }
    }
}
ZipCodeValidator.ts
/// <reference path="Validation.ts" />
module Validation {
    var numberRegexp = /^[0-9]+$/;
    export class ZipCodeValidator implements StringValidator {
        isAcceptable(s: string) {
            return s.length === 5 && numberRegexp.test(s);
        }
    }
}
Test.ts
/// <reference path="Validation.ts" />
/// <reference path="LettersOnlyValidator.ts" />
/// <reference path="ZipCodeValidator.ts" />

// Some samples to try
var strings = ['Hello', '98052', '101'];
// Validators to use
var validators: { [s: string]: Validation.StringValidator; } = {};
validators['ZIP code'] = new Validation.ZipCodeValidator();
validators['Letters only'] = new Validation.LettersOnlyValidator();
// Show whether each string passed each validator
strings.forEach(s => {
    for (var name in validators) {
        console.log('"' + s + '" ' + (validators[name].isAcceptable(s) ? ' matches ' : ' does not match ') + name);
    }
});

如果存在多文件的依赖,我们需要保证编译器能加载各个文件。这里有两种方式做到:

第一种方式:可以利用 —out 连接多个输入文件让编译器编译成一个单一js文件()。这样编译器会自动根据 reference tags 自动组织多个文件编译成一个js文件。我们也可以组织个别的文件如:。

第二种方式:我们也可以采用预先文件的编辑来加载多个文件,我们可以使用script tag在web页面加载来控制文件顺序,例如:

#MyTestPage.html (excerpt)
<script src="Validation.js" type="text/javascript" />
<script src="LettersOnlyValidator.js" type="text/javascript" />
<script src="ZipCodeValidator.js" type="text/javascript" />
<script src="Test.js" type="text/javascript" />

JavaScript工具库之Lodash

你还在为JavaScript中的数据转换、匹配、查找等烦恼吗?一堆看似简单的foreach,却冗长无趣,可仍还在不停的repeat it!也许你已经用上了Underscore.js,不错,你已经进步很大一步了。然而今天我希望你能更进一步,利用lodash替换掉Underscore。

lodash一开始是Underscore.js库的一个fork,因为和其他(Underscore.js的)贡献者意见相左。John-David Dalton的最初目标,是提供更多“一致的跨浏览器行为……,并改善性能”。之后,该项目在现有成功的基础之上取得了更大的成果。最近lodash也发布了3.5版,成为了npm包仓库中依赖最多的库。它正在摆脱屌丝身份,成为开发者的常规选择之一。

现在我们所熟知的很多开源项目都已经使用或者转到了lodash阵营之上。比如JavaScript转译器Babel、博客平台Ghost,和项目脚手架工具Yeoman。特别Ghost是从Underscore迁移到了lodash,Ghost的创始人John O’Nolan对于此曾评价到:“这是一个非常明智的选择,它几乎完全是由我们开源开发社区推动的。我们发现lodash包含更多的功能,更好的性能、恰到好处地使用了semver,并且在Node.js社区(以及其他依赖)中越来越抢眼“。

lodash演练

lodash主要使用了延迟计算,使得lodash其性能远远超过Underscore。在lodash中延迟计算意味着在我们的链式方法在显示或隐式的value()调用之前是不会执行的。由于这种执行的延后,因此lodash可以进行shortcut fusion这样的优化,通过合并链式iteratee大大降低迭代的次数。从而大大提供其执行性能。

百说不如一练,下面我们以用户信息为例:

var users = [
  { 'user': 'barney',  'age': 36 },
  { 'user': 'fred',    'age': 40 },
  { 'user': 'pebbles', 'age': 18 }
];

1. 获取所有用户名字,并以”,“分割

var names = _.chain(users)
  .map(function(user){
    return user.user;
  })
  .join(" , ")
  .value();
console.log(names);

个人比较喜欢lodash延迟计算的现实value,以及JavaScript的函数式风格。在这里首先将users对象包装成为lodash对象,再map获取所有用户的名称,并最后利用join将用户名称以”,“连接在一起。注意这里只是一串方法链,如果你没有显样的调用value方法,使其立即执行的化,你将会得到如下的LodashWrapper延迟表达式:

 LodashWrapper {__wrapped__: LazyWrapper, __actions__: Array[1], __chain__: true, constructor: function, after: function…}

因为延迟表达式的存在,因此我们可以多次增加方法链,但这并不会被执行,所以不会存在性能的问题,最后知道我们需要使用的时候,使用value显式立即执行即可。

2. 获取最年轻的用户

 var youngest = _.chain(users)
  .min(function(user){
    return user.age;
  })
  .value();
console.log(youngest);

这里利用了lodash提供的min函数可以轻易的解决。

在这里博主还希望用另外一个方式解释lodash方法链的优化,上面的方法可以等价为下面的方式,以age排序的第一个user:

var youngest2 = _.chain(users)
  .sortBy("age")
  .map(function(user){
    console.log("map", user);
    return user;
  })
  .first()
  .value();
console.log(youngest2);

在这里博主多加了一个map作为log输出,如果你执行这行代码的时候,你会惊奇的看见这里只会有一个user的输出,这点可以证明在立即执行的时候lodash为我们的方法链做了可靠的优化;如果我们去掉first函数你则会看见有3个user对象的输出。

3. 获取最年长的用户

var oldest  = _.chain(users)
  .max(function(user){
    return user.age;
  })
  .value();

console.log(oldest );

这里则使用lodash的max函数。

4. 用户数组到用户Map的转换

在开发中我们经常会有把一堆数组形式的数据转换为Object形式的数组,便于根据属性key值查找,下面将以user对象来演示:

var userObj = _.chain(users)
  .map(function(user){
    return [user.user, user.age];
  })
  .zipObject()
  .value();
console.log(userObj);

利用lodash首先将user数组map为[key, value]的数组集合,最后利用zipObject将结果转换为Object对象,zipObject会利用结果集的第一项作为key,第二项作为value生产Object。

结尾

我们在这里展示知识lodash中很小一部分的API,正如随笔开始所说:lodash是为了提供更多“一致的跨浏览器行为……,并改善性能”API。所有的lodash API你可以在这里https://lodash.com/docs#matches查找。

本文的所演示的demo,你也可以在jsbin http://jsbin.com/xocixubaru/1/edit?html,js,output演示。

(转)Babel-现在开始使用 ES6

在 2 月 20 号 ECMAScript 第六版就正式推出了,这门语言一直保持稳定快速的发展而且新功能也在慢慢被现在主流的 JavaScript 引擎所接受。不过要想在浏览器端或者 Node 端直接运行 ES6 代码还得等上一些日子。幸好 TC39 (负责研究开发 EMCAScript 规格的组织) 做了大量工作让我们现在可以使用 ES6 中的大部分特性了。

代码转换

能够实现 ES6 到 ES5 的代码转换多亏了 Babel (以前叫 6to5) 以及 Traceur 之类的项目。这些转换器 (更准确地说是源代码到源代码的编译器) 可以把你写的符合 ECMAScript 6 标准的代码完美地转换为 ECMAScript 5 标准的代码,并且可以确保良好地运行在所有主流 JavaScript 引擎中。

我们这里目前在使用 Babel,主要是因为它对 ES6 的支持程度比其它同类更高,而且 Babel 拥有完善的文档和一个很棒的在线交互式编程环境

起步

在用 ES6 标准开始一个新项目的时候我们会建立一个目录结构来确保用 ES6 编写的代码能和编译出的 ES5 代码区分开。原始的 ES6 代码我们放在 src 目录下,而编译好的文件就是 lib 目录。这样的命名我们会在本文一直使用。(补充一点,lib 目录应该被加入 .gitignore 文件中)

安装 Babel

如果你还没安装 Babel 可以使用 npm 来安装:

npm install -g babel

Babel 一旦安装完成就可以开始编译你的 ES6 代码了。再确认一遍你已经在 src 目录放入了一些 ES6 文件,下面的命令将会把这个目录下所有 .es6, .es 和 .js 后缀的文件编译成符合 ES5 规范的代码到 lib 目录下:

babel -d lib/ src/

如果你想在文件有改动的时候自动完成这些编译工作可以使用这些常用的 JavaScript 构建工具:Grunt, GulpBrocolli.

给 ES6 标准库一个”腻子”

Babel 作为一个源到源的编译器不可能呈现所有 ES6 标准库中的新特性,例如 Map 和 Set 构造器和 Array 下的一些新方法。要使用这些我们需要一个”腻子”来填补这些不足。现在有很多 ES6 的腻子比如 core-js,它适用与 Node, io.js 和浏览器。

译者注: 本节原始标题为 Polyfilling the standard library,术语 polyfill 来自于一个家装产品Polyfilla:

Polyfilla 是一个英国产品,在美国称之为 Spackling Paste (刮墙的,在中国称为腻子)。记住这一点就行: 把旧的浏览器想象成为一面有了裂缝的墙.这些 polyfill 会帮助我们把这面墙的裂缝抹平,还我们一个更好的光滑的墙壁 (浏览器)

编写 ES6 代码

现在构建 ES6 代码的工具已经备齐了那我们就开始真正有趣的部分。我们不会过多着眼于某个新特性,如果你有需要可以阅读 Luke Hobanfeature list.

我们先在 src 目录下创建一个叫 person.es6 的文件:

import 'core-js/shim';

export default class Person {

  constructor( name ) {
    this.name = name;
  }

  sayHello() {
    return `Hello ${ this.name }!`;
  }

  sayHelloThreeTimes() {
    let hello = this.sayHello();
    return `${ hello } `.repeat(3);
  }
}

在这个很简单的例子中我们用了数个需要 Babel 来解决兼容性的语法,还有一个新的方法 String#repeat 须要由 core-js 处理。你可以用本文开头给出的 Babel 命令行代码或者用 REPL 得到运行结果。

发布到 npm

目前为止我们可以编写、编译和运行 ES6 代码,不过你也许还想把你的代码发布到 npm 上。你显然不能直接发布然后期望每个人都来自己编译一次。

幸好,npm 允许你在发布前用 prepublish script 选项来修改,这个特性在 CoffeeScript 项目中已经被广泛使用了。

现在把 package.json 文件加入到项目根目录中:

{
  "name": "person",
  "version": "0.1.0",
  "scripts": {
    "compile": "babel -d lib/ src/",
    "prepublish": "npm run compile"
  },
  "main": "lib/person.js",
  "dependencies": {
    "core-js": "^0.6.0"
  },
  "devDependencies": {
    "babel": "^4.6.0"
  }
}

注意这个 compile script 会直接运行你在右边提供 Babel 命令,这样你就可以直接运行 npm run compile 来编译而不需要键入文件目录了,而 prepublish script 会在你每次执行 npm publish 的时候自动运行。

还有就是为什么 Babel 会被加入 development dependencies 中,这样如果有人想参与这个项目就不用全局安装 Babel 了,npm 会把你项目下包含可执行文件的 node_modules 目录加入到系统环境变量 path 中。

.npmignore 文件

最后你需要确保发布的是编译出的文件而不是原始的 ES6 文件。如果你的项目根目录有 .gitignore 而没有 .npmignore 那 npm 就会自动忽略你项目中包含在 .gitignore 里所有的 文件和目录。添加 .npmignore 这样你发布的包里就是编译好的文件:

src/

总结

编写 ES6 代码并使用源到源的编译器如 Babel 或者 Traceur 来转换成标准 ES5 代码 使用 ES6 标准库腻子如 core-js 记得在发布到 npm 的时候添加 .npmignore 文件 你可以在我们的 update-couch-designs 项目中看到一个完整的例子,这个项目是我们用于更新和新建 CouchDB 设计文档的简单脚本。

原文:懂香—《现在开始使用 ES6》 翻译自 Using ES6 with npm today

(翻译)反射处理java泛型

当我们声明了一个泛型的接口或类,或需要一个子类继承至这个泛型类,而我们又希望利用反射获取这些泛型参数信息。这就是本文将要介绍的ReflectionUtil就是为了解决这类问题的辅助工具类,为java.lang.reflect标准库的工具类。它提供了便捷的访问泛型对象类型(java.reflect.Type)的反射方法。

本文假设你已经了解java反射知识,并能熟练的应用。如果还不了解java反射知识,那么你可以先移步到Oracel反射课程,这可能是你开始学习反射的好起点.

ReflectionUtil中包含以下几种功能:

  1. 通过Type获取对象class;
  2. 通过Type创建对象;
  3. 获取泛型对象的泛型化参数;
  4. 检查对象是否存在默认构造函数;
  5. 获取指定类型中的特定field类型;
  6. 获取指定类型中的特定method返回类型;
  7. 根据字符串标示获取枚举常量;
  8. ReflectionUtil下载地址.

通过Type获取对象class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private static final String TYPE_NAME_PREFIX = "class ";

public static String getClassName(Type type) {
    if (type==null) {
        return "";
    }
    String className = type.toString();
    if (className.startsWith(TYPE_NAME_PREFIX)) {
        className = className.substring(TYPE_NAME_PREFIX.length());
    }
    return className;
}

public static Class<?> getClass(Type type)
            throws ClassNotFoundException {
    String className = getClassName(type);
    if (className==null || className.isEmpty()) {
        return null;
    }
    return Class.forName(className);
}

方法ReflectionUtil#getClass(Type)实现了从java.lang.reflect.Type获取java.lang.Class对象名称。这里利用了Type的toString方法获取所在类型的class。如“class some.package.Foo”,截取后部分class名称,在利用Class.forName(String)获取class对象。

通过Type创建对象

1
2
3
4
5
6
7
8
public static Object newInstance(Type type)
        throws ClassNotFoundException, InstantiationException, IllegalAccessException {
    Class<?> clazz = getClass(type);
    if (clazz==null) {
        return null;
    }
    return clazz.newInstance();
}

方法ReflectionUtil#newInstance(Type type)实现根据Type构造对象实例。在这里输入的Type不能是抽象类、接口、数组类型、以及基础类型、Void否则会发生InstantiationException异常。

获取泛型对象的泛型化参数

首先假设我们有如下两个对象:

1
2
3
4
5
6
7
public abstract class Foo<T> {
    //content
}

public class FooChild extends Foo<Bar> {
    //content
}

怎么获取子类在Foo中传入的泛型Class类型呢?

比较常用的做法有以下两种:

强制FooChild传入自己的class类型(这也是比较常用的做法):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public abstract class Foo<T> {
    private Class<T> tClass;

    public Foo(Class<T> tClass) {
        this.tClass = tClass;
    }
    //content
}

public class FooChild extends Foo<Bar> {
    public FooChild() {
        super(FooChild.class);
    }
    //content
}

利用反射获取:

1
2
3
4
5
6
7
public static Type[] getParameterizedTypes(Object object) {
    Type superclassType = object.getClass().getGenericSuperclass();
    if (!ParameterizedType.class.isAssignableFrom(superclassType.getClass())) {
        return null;
    }
    return ((ParameterizedType)superclassType).getActualTypeArguments();
}

方法ReflectionUtil#getParameterizedTypes(Object)利用反射获取运行时泛型参数的类型,并数组的方式返回。本例中为返回一个T类型的Type数组。

为了Foo得到T的类型我们将会如下使用此方法:

1
2
3
4
...
Type[] parameterizedTypes = ReflectionUtil.getParameterizedTypes(this);
Class<T> clazz = (Class<T>)ReflectionUtil.getClass(parameterizedTypes[0]);
...

注意:

在java.lang.reflect.ParameterizedType#getActualTypeArguments() documentation:的文档中你能看见如下文字:

in some cases, the returned array can be empty. This can occur. if this type represents 
a non-parameterized type nested within a parameterized type.

当传入的对象为非泛型类型,则会返回空数组形式。

检查对象是否存在默认构造函数

1
2
3
4
5
6
7
8
9
public static boolean hasDefaultConstructor(Class<?> clazz) throws SecurityException {
    Class<?>[] empty = {};
    try {
        clazz.getConstructor(empty);
    } catch (NoSuchMethodException e) {
        return false;
    }
    return true;
}

方法ReflectionUtil#hasDefaultConstructor利用java.lang.reflect.Constructor检查是否存在默认的无参构造函数。

获取指定类型中的特定field类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static Class<?> getFieldClass(Class<?> clazz, String name) {
    if (clazz==null || name==null || name.isEmpty()) {
        return null;
    }
    name = name.toLowerCase();
    Class<?> propertyClass = null;
    for (Field field : clazz.getDeclaredFields()) {
        field.setAccessible(true);
        if (field.getName().equals(name)) {
            propertyClass = field.getType();
            break;
        }
    }
    return propertyClass;
}

在某些情况下你希望利用已知的类型信息和特定的字段名字想获取字段的类型,那么ReflectionUtil#getFieldClass(Class<?>, String)可以帮助你。ReflectionUtil#getFieldClass(Class<?>, String) 利用Class#getDeclaredFields()获取字段并循环比较java.lang.reflect.Field#getName()字段名称,返回字段所对应的类型对象。

获取指定类型中的特定method返回类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static Class<?> getMethodReturnType(Class<?> clazz, String name) {
    if (clazz==null || name==null || name.isEmpty()) {
        return null;
    }

    name = name.toLowerCase();
    Class<?> returnType = null;

    for (Method method : clazz.getDeclaredMethods()) {
        if (method.getName().equals(name)) {
            returnType = method.getReturnType();
            break;
        }
    }

    return returnType;
}

方法ReflectionUtil#getMethodReturnType(Class<?>, String)可以帮助你根据对象类型和方法名称获取其所对应的方法返回类型。ReflectionUtil#getMethodReturnType(Class<?>, String)利用Class#getDeclaredMethods()并以java.lang.reflect.Method#getName()比对方法名称,返回找到的方法的返回值类型(Method#getReturnType()).

根据字符串标示获取枚举常量

1
2
3
4
5
6
7
@SuppressWarnings({ "unchecked", "rawtypes" })
public static Object getEnumConstant(Class<?> clazz, String name) {
    if (clazz==null || name==null || name.isEmpty()) {
        return null;
    }
    return Enum.valueOf((Class<Enum>)clazz, name);
}

方法ReflectionUtil#getEnumConstant(Class<?>, String)为利用制定的枚举类型和枚举名称获取其对象。这里的名称必须和存在的枚举常量匹配。

ReflectionUtil下载地址

你可以从这里下载ReflectionUtil.java. 原英文版地址: http://qussay.com/2013/09/28/handling-java-generic-types-with-reflection/

SQL 行列倒置

SQL的的行列倒置已经不是新知识了,但在博主的技术咨询期间,仍发现其实有很多人并不了解这块,所以在此专门写一篇博客记录。本文将以Mysql为例,并以数据采集指标信息获取为例子。在下面的例子,你可以在sqlfiddle运行。

首先我们需要创建数据库Schema:

    CREATE TABLE Chart
        (`createTime` DateTime, `kpi` varchar(30), `field` varchar(30), `value` double);

    INSERT INTO Chart
        (`createTime`,`kpi`, `field`, `value`)
    VALUES
        ("2015-02-01 12:00:00", 'disk', 'disk', 20),
        ("2015-02-01 12:15:00", 'disk', 'disk', 30),
        ("2015-02-01 12:20:00", 'disk', 'disk', 25),
        ("2015-02-01 12:30:00", 'disk', 'disk', 25),
        ("2015-02-01 12:35:00", 'disk', 'disk', 25),
        ("2015-02-01 12:40:00", 'disk', 'disk', 25),

        ("2015-02-01 12:00:00", 'disk', 'disk-all', 20),
        ("2015-02-01 12:20:00", 'disk', 'disk-all', 30),
        ("2015-02-01 12:25:00", 'disk', 'disk-all', 25),
        ("2015-02-01 12:30:00", 'disk', 'disk-all', 25),
        ("2015-02-01 12:35:00", 'disk', 'disk-all', 25),
        ("2015-02-01 12:40:00", 'disk', 'disk-all', 25),
        ("2015-02-01 12:40:00", 'cpu', 'cpu-all', 25),
        ("2015-02-01 12:40:00", 'cpu', 'cpu', 25)
    ;

在这里字段分别代表:createTime = 数据采集时间,kpi = 数据采集指标,field = 作为指标的小类(一个kpi可以包含多个field),value = 采集的数据

当我们创建好了数据结构,下面因为我们希望获取出所有的 固定时间范围内的特定kpi的数据,注意因为可能一个kpi中的多个field,但是某些field漏采了部分时间的数据,所以这里我们需要补充异常点0. 并由于EChart这类图表库,希望我们输入的是横轴和纵轴为两个独立的数组对象表示。所以我们需要如下:

option = {
    ....

    xAxis : [
        {
            type : 'category',
            boundaryGap : false,
            data : ['周一','周二','周三','周四','周五','周六','周日']
        }
    ],
    yAxis : [
        {
            type : 'value',
            axisLabel : {
                formatter: '{value} °C'
            }
        }
    ],
    series : [
        {
            ....
            data:[11, 11, 15, 13, 12, 13, 10]
        },
        {
           ....
            data:[11, 11, 15, 13, 12, 13, 10]
        }
    ]
};

取出横轴比较容易,如下:

SELECT createTime,kpi, field, value FROM Chart WHERE kpi = 'disk' and (createTime BETWEEN '2015-02-01 12:00:00' AND '2015-02-01 12:25:00');

但是纵轴如果我们以同样方式取出,可能存在需要我们自动程序补值,并且需要保证每项数据和横轴对应,所以我们的程序处理会比较复杂,如下:

SELECT createTime,kpi, field, value FROM Chart WHERE kpi = 'disk' and (createTime BETWEEN '2015-02-01 12:00:00' AND '2015-02-01 12:25:00');

结果为:

createTime  kpi field   value
February, 01 2015 12:00:00  disk    disk    20
February, 01 2015 12:15:00  disk    disk    30
February, 01 2015 12:20:00  disk    disk    25
February, 01 2015 12:00:00  disk    disk-all    20
February, 01 2015 12:20:00  disk    disk-all    30
February, 01 2015 12:25:00  disk    disk-all    25

有没有其他方案更佳的呢?当然那就是本文要说的sql的倒置,如果我们能够把返回数据转换为如下:

field   ‘2015-02-01 12:00:00’   ‘2015-02-01 12:15:00’   ‘2015-02-01 12:20:00’   ‘2015-02-01 12:25:00’
disk         20                            30                     25                       0
disk-all     20                             0                     30                       25

那么程序就很好处理了。在上面我们已经能够取出所有的横轴数据并排序,接下来我们将可以很简单的做到行列倒置:如下:

SELECT field,
SUM(IF(createTime = '2015-02-01 12:00:00', value, 0)) as '2015-02-01 12:00:00',
SUM(IF(createTime = '2015-02-01 12:15:00', value, 0)) as '2015-02-01 12:15:00',
SUM(IF(createTime = '2015-02-01 12:20:00', value, 0)) as '2015-02-01 12:20:00',
SUM(IF(createTime = '2015-02-01 12:25:00', value, 0)) as '2015-02-01 12:25:00' 
FROM Chart
WHERE kpi = 'disk' and (createTime BETWEEN '2015-02-01 12:00:00' AND '2015-02-01 12:25:00')
GROUP BY field

这样返回数据满足我们的需求了。


下面我们来分析下这句SQL,

  1. 首先我们利用‘IF(createTime = ‘2015-02-01 12:00:00’, value, 0)’来处理插值,并对每行数据转为以时间为列数据,并可以利用IF来补’0‘,将会如下:

SQL:

SELECT field,
IF(createTime = '2015-02-01 12:00:00', value, 0) as '2015-02-01 12:00:00',
IF(createTime = '2015-02-01 12:15:00', value, 0) as '2015-02-01 12:15:00',
IF(createTime = '2015-02-01 12:20:00', value, 0) as '2015-02-01 12:20:00',
IF(createTime = '2015-02-01 12:25:00', value, 0) as '2015-02-01 12:25:00' 
FROM Chart
WHERE kpi = 'disk' and (createTime BETWEEN '2015-02-01 12:00:00' AND '2015-02-01 12:25:00');

结果为:

field   ‘2015-02-01 12:00:00’   ‘2015-02-01 12:15:00’   ‘2015-02-01 12:20:00’   ‘2015-02-01 12:25:00’
disk               20                       0                       0                       0
disk                0                       30                      0                       0
disk                0                       0                       25                      0
disk-all            20                      0                       0                       0
disk-all            0                       0                       30                      0
disk-all            0                       0                       0                       25
  1. 这下我们就可以利用sql的聚合函数sum和group by来聚合数据行:

SQL:

SELECT field,
SUM(IF(createTime = '2015-02-01 12:00:00', value, 0)) as '2015-02-01 12:00:00',
SUM(IF(createTime = '2015-02-01 12:15:00', value, 0)) as '2015-02-01 12:15:00',
SUM(IF(createTime = '2015-02-01 12:20:00', value, 0)) as '2015-02-01 12:20:00',
SUM(IF(createTime = '2015-02-01 12:25:00', value, 0)) as '2015-02-01 12:25:00' 
FROM Chart
WHERE kpi = 'disk' and (createTime BETWEEN '2015-02-01 12:00:00' AND '2015-02-01 12:25:00')
GROUP BY field

效果如上。

对于sql行列转置可以简述为分为两部分:

  1. 利用条件逻辑(mysql: IF, sql server: case … when(sql server 2005开始支持数据透视表pivot) ..)将 需要倒置的数据变为列。
  2. 利用聚合函数(sum、max、min…)group by 合并数据。这里需要注意max、min需要注意数据的边界,如存在负数且默认值采用0,那么max就会存在问题,所以一般sum是最安全的(任何数加0都不会改变结果);但对于特定场景max、min也是安全方案。

我们也可以将上面两次请求合并为一次,这就需要mysql的动态拼接,如下:

SELECT 
@time_sql := group_concat("SUM(IF(createTime = '", t.createTime, "', value, 0)) AS '" , t.createTime, "'")  
FROM (
 SELECT DISTINCT createTime FROM Chart ORDER BY createTime
) AS t;

 set @v_sql = CONCAT("SELECT field", IF(ISNULL(@time_sql) , " ", CONCAT(", ", @time_sql)) ," FROM Chart GROUP BY field");

prepare stmt from @v_sql; 
EXECUTE stmt;   
deallocate prepare stmt;