利用jsdoc和idea加速javascript开发(一)

来源:互联网 发布:淘宝退货地址 编辑:程序博客网 时间:2024/05/02 00:58
当项目里的js代码到达一定规模后, js的开发会变成一个很头痛的问题, 由于没有静态类型语言的类型支持,很少有IDE能够对js代码提供有效的代码补全,方法和参数错误检查等功能. 想要在几万行js代码弄清楚某一个变量应该是什么类型, 有什么方法, 方法的参数是什么, 返回值是什么是件很令人头痛的事情. 

幸好Intellij IDEA提供了对jsdoc的支持, 使我们可以更方便的编写js代码. 有时候我觉得IDEA对js的支持简直到了逆天的程度, 很少有IDE对js的支持能做到这么好. 

我们先来看一个简单的例子, 下面的js程序 
Js代码  收藏代码
  1. var doSomething = function(str, num){  
  2.     return {name : str, age : num};  
  3. };  
  4.   
  5. doSomething(1, "terry");  
  6. doSomething("terry", 1);  


上面的程序在运行期间是不会出错的.因为javascript是没有严格的类型检查. 
但实际上,我们希望name属性是个字符串, 而age属性是一个数值类型. 这样我们的业务逻辑才有意义. 如果不是很清楚业务逻辑的开发人员调用doSomething方法时不小心用了doSomething(1, "terry"); 可能直到项目运行过程中发现页面上名字变成了数字, 或者年龄计算出错时才能发现这个问题. 

同样是因为js的弱类型检查机制, 使得IDE也无法知道什么样的调用才算合理的. 所以即便是Intellij IDEA也会如下图的样子, 没有任何提醒. 

 

事实上, 只有doSomething方法的作者本人才知道如何检查调用的方式正确的正确性. 幸运的是当我们在方法上加上正确的jsdoc以后, IDEA就会知道什么样的调用是合理的, 至少在参数的类型上是知道的. 所有加了jsdoc的IDEA会像下图的样子给出提醒 

 

怎么样? 很不错是吧! 错误发现的越早, 修正错误的成本越低, 我想这是大家都认同的事实. 

其实IDEA通过jsdoc对js程序的支持远不止这一个, 我们可以来看一下加上哪些jsdoc能让IDEA更好的为我们服务. 

首先说一下jsdoc. 在javascript领域里没有想javadoc那样统一的标准, 也有好几种注释方式, 只能说jsdoc作为被使用较多和IDE支持较好, 成为了事实上标准. 但是每个IDE对jsdoc的支持程度也不仅相同. 

关于jsdoc详细信息可以参见: http://usejsdoc.org/index.htm 
在上面的网站上有详细的jsdoc的书写方法和生成jsdoc文档的工具的使用方法. 很多项目构建工具, 如maven, 也同样有生成jsdoc的支持. 

Intellij IDEA对jsdoc的支持也不是100%的, 它目前支持的其实是Google Closure中的jsdoc变种, 但基本上和标准的jsdoc一致, 加入了一些类似泛型的支持(后面会讲到).相关信息参加这里:https://developers.google.com/closure/compiler/docs/js-for-compiler 

下面我们会用"加入jsdoc之前"和"加入jsdoc之后"的区别来逐一看看IDEA对js到的支持. 由于这些特性在IDEA的文档里也没有明确指出, 基本上都是我试出来的, 所以可能会有遗漏的地方. 

1. 函数的参数声明 

上面的例子中已经看到了. 
标签: @param {Type} varname Description 
其实把varname放到{Type}前面IDEA也可以正确解析. 

Type可以是JavaScript里面的原生类型,如String, Number. 也可以是自定义类型. 

如果参数可以是可选的: @param {Type} [varname] Description 

如果某个参数不是可选的, 但调用时没有指定值, IDEA提示错误, 如下: 

 

num属性指定了可选以后则不会提示错误 

 

在函数提示上也可以清楚的看到, num参数是可选的 

 

通过@param {Number} [num=18]  a number parameter 可以指定默认值, 但是这只是在注释上使阅读更加清楚, 但IDEA并没有对这个支持 

也可以用Google Closure的可选参数声明: 
@param {Number=} num  a number parameter 

2. 返回类型 
在上面的例子中我们也通过@returns {{name: String, age: Number}}标签给返回值加了类型定义. 

定义了返回值类型以后, 如果在函数体里实际返回的和声明的不一致, IDEA会给出提示: 
需要的是一个有name和age属性的对象, 但是返回的是一个String 

 

即便返回的是一个自定义对象, IDEA也会检查其中的属性是否满足要求: 


 

在满足了要求的前提下, 增加别的属性是可以的: 

 

缺少属性是不可以的: 

 

如果某个属性是可选的, 可以用{Type=}的方式声明: 

 

在声明了返回值以后, IDEA会自动把类型信息加入代码补全功能, 在上面的例子中,如果我们把函数调用的返回值赋给val4, 则val.的自动提示会是这样: 

 

如果函数没有返回值, 不要加上@return标签 

如果返回的结果值不可能为null, 则可以用@return {!String}, 如果在函数体里返回了null, IDEA会给出提示 

 

如果函数返回值有可能是null, 最好在@return上声明出来@return {!String|null} 
这样调用者会在自己的程序里加上null判断, 遗憾的是IDEA并没有对这个给予有效的提示, 如果能提供null校验提示就更好的. IDEA对Java是有类似的支持的. 

3. 类型声明 

回到上面的doSomething函数的例子: 
Js代码  收藏代码
  1. /** 
  2.  * doSomething is a function 
  3.  * @param {String} str  a string parameter 
  4.  * @param {Number} num  a number parameter 
  5.  * @returns {{name: String, age: Number}} 
  6.  */  
  7. var doSomething = function(str, num){  
  8.     return {name : str, age : num + 1};  
  9. };  

返回值的类型是一个复杂类型{{name: String, age: Number}}, 只有两个属性时还好, 代码不算多, 如果在多处使用copy一下成本不算太高. 如果属性比较多, 并且多处使用, 也就是说将来有可能到处修改那就麻烦了. 对于这种场景我们可以考虑使用两种方式: 定义真实的js类型, 定义虚拟的js类型. 

定义真实js类型的方式有很多种, 这里是比较简单的一种: 

Js代码  收藏代码
  1. /** 
  2.  * 
  3.  * @constructor 
  4.  */  
  5. var Employee = function(){  
  6.     /** @type String */  
  7.     this.name = "";  
  8.     /** @type Number */  
  9.     this.age = 1;  
  10. };  
  11.   
  12. var emp = new Employee();  
  13. emp.name = "Terry";  
  14. emp.age = 18;  


定义真实js类型的好处是, 运行期有类型信息, 可以通过构造函数进行复杂的参数控制. 但是当我们项目里有大量的充当VO功能的类型时定义真实类型就会很麻烦. 而且不灵活. 

这时候定义虚拟类型会更加方便, 运行期不需要把这些定义告诉浏览器, 事实上浏览器也不关心. 虚拟类型是给IDE用的. 下面我们定义一个Manager类型: 

Js代码  收藏代码
  1. /** 
  2.  * Manager virtual Type 
  3.  * @typedef {Object} Manager 
  4.  * @property {String} name 
  5.  * @property {Number} age 
  6.  * @property {Array.<Employee>} employees 
  7.  */  
  8.   
  9. // var manager = new Manager(); 错误写法  
  10. /** @type Manager */  
  11. var manager = {  
  12.     name : "Terry",  
  13.     age : 28,  
  14.     employees : [  
  15.         {  
  16.             name : "Losyn",  
  17.             age : 18  
  18.         },  
  19.         {  
  20.             name : "Bruce",  
  21.             age : 18  
  22.         }  
  23.     ]  
  24. };  


注意因为只是注释, 浏览器并不知道Manager这个东西, 所以如果执行var manager = new Manager();是会出错的. 

我们可以把Manager声明为函数的参数,返回值, 或函数内部的变量的类型. 

Js代码  收藏代码
  1. /** 
  2.  * Employee virtual Type 
  3.  * @typedef {Object} Employee 
  4.  * @property {String} name 
  5.  * @property {Number} age 
  6.  */  
  7.   
  8. /** 
  9.  * Manager virtual Type 
  10.  * @typedef {Object} Manager 
  11.  * @property {String} name 
  12.  * @property {Number} age 
  13.  * @property {Array.<Employee>} employees 
  14.  */  
  15.   
  16. /** 
  17.  * a function that a manager can make a order 
  18.  * @param {Manager} manager 
  19.  */  
  20. var makeOrder = function(manager){  
  21.   
  22. };  


IDEA会给出友好的代码补全功能: 

 

声明变量类型 

Js代码  收藏代码
  1. /** @type Manager */  
  2. var manger = $.parseJSON("{...}");  


IDEA同样会给出友好的代码补全功能: 

 

甚至数组中的元素的类型也能给出提示 


 




4. 类型表达式 

在上一篇的例子中我们看到可以用@param指定参数的类型, 用@return指定返回值的类型, 用@type指定变量的类型. 下面我们详细的介绍一下指定类型时的表达式可以是什么样子的. 

首先类型表达是需要用两个花括号"{...}"包起来, 简单的表达式不用{}也可以, 但为了统一风格, 建议都用{}. 你在代码中对参数, 返回值, 变量的类型表达式写的越精确, 越详细, IDEA对js的支持就越好. 

类型名 
指定类型的最简单方式 

  • {boolean}
  • {Window}
  • {goog.ui.Menu}


集合类型 
类似于Java里的泛型, 指定元素的类型 
  • {Array.<String>} 字符串数组
  • {Object.<String,Number>} 对象的key是字符串, 值是数值, 类似于Map


联合类型 
指定类型是A或B 
{(number|boolean)} 

记录(Record)类型 
相当于匿名类, 指定类型中应该有的属性及属性类型, 里面那层{}是语法的一部分 
  • Array.<{length}> Array中的元素需要有一个length属性
  • {{myNum: number, myObject}} 指明, 一个对象有一个数值类型的myNum属性, 和一个任意类型的myObject属性


可为null的类型 
指明一个类型为A或null, 其实所有的类型(除了function,number, string, boolean)默认都是可以为null的, 即便前面不加问号. 
  • {?Number}
  • {?Shape}


不可为null的类型 
指明一个类型为A并且不为null. function,number, string, boolean默认不可为null. 
  • {!Object}


函数类型 
指明一个对象是函数, 并指明参数类型 
  • {function(string, boolean)}


函数返回类型 
指明一个对象是函数, 并指明返回值类型(也可以同时指明参数) 
  • {function(): number}
  • {function(string, boolean): number}



函数中this的类型 
指明一个对象是函数,并且在函数的context中this指向什么类型 
  • {function(this:goog.ui.Menu, string)}


例如下面的例子: 

Js代码  收藏代码
  1. /** 
  2.  * 
  3.  * @param {function(this:HTMLTableElement, Number)} f 
  4.  * @param {String} otherData 
  5.  */  
  6. var doSomething5 = function(f, otherData){  
  7.   
  8. };  


在调用doSomething5方法的匿名函数时, 首先我们可以看到函数声明提示 

 

函数中的num变量被识别出是Number类型 

 

函数中的this被识别出是HTMLTableElement 

 

这里面我没有用自定义类型作为this的类型是因为我用IDEA 13.1.2中是乎不支持自定义类型, 不知道是不是Bug 

函数中被new出来的类型 
指明一个对象是函数,如果在这个函数前用new,会被创造出来的对象的类型 
  • {function(new:goog.ui.Menu, string)} var menu = new func("str");

很遗憾, IDEA是乎没有找到相关的支持 

可变长度的参数的类型 
指明某一个参数的个数不定 
  • {function(string, ...[number]): number} 第一个参数是string, 后面可以任何个Number类型的参数


Js代码  收藏代码
  1. /** 
  2.  * 
  3.  * @param {function(string, ...[number])} f 
  4.  * @param {String} otherData 
  5.  */  
  6. var doSomething7 = function(f, otherData){  
  7.     f("terry");  
  8.     f("terry", 1);  
  9.     f("terry", 1, 2, 3);  
  10.     f("terry", 1, 2, 3, 4);  
  11. };  


 

可变长度的参数的类型 (@param) 
指明某一个参数的个数不定 
  • @param {...number} var_args 可以任何个Number类型的参数


Js代码  收藏代码
  1. /** 
  2.  * 
  3.  * @param {...number} args 
  4.  */  
  5. var doSomething8 = function (args) {  
  6.   
  7. };  


调用参数值类型不正确是会有提示 

 

可选参数的类型 
指明某一个参数是可选的 
  • @param {number=} opt_argument


Js代码  收藏代码
  1. /** 
  2.  * 
  3.  * @param {Number} age 
  4.  * @param {String=} name 
  5.  */  
  6. var doSomething9 = function (age, name) {  
  7.   
  8. };  


 

任意类型 
{*} 

不确定类型 
{?} 
IDEA没有特殊支持 

5. 类声明 

定义接口和实现 

总所周知,JavaScript是没有比较完善的面向对象支持, 所以从语言层面是实现接口和实现是比较困难的. 但是我们可以通过jsdoc让IDE知道这种关系并提供一些错误校验. 
主要标签@interface声明一个类是接口, @implements声明一个类是实现了哪个接口, 接口和类都可以通过@extends声明继承关系. 

Java代码  收藏代码
  1. /** 
  2.  * A shape. 
  3.  * @interface 
  4.  */  
  5. function Shape() {  
  6. }  
  7. Shape.prototype.draw = function () {  
  8. };  
  9.   
  10. /** 
  11.  * A polygon. 
  12.  * @interface 
  13.  * @extends {Shape} 
  14.  */  
  15. function Polygon() {  
  16. }  
  17. Polygon.prototype.getSides = function () {  
  18. };  


这里我们声明了一个接口Shape, 一个接口Polygon继承于Shape. 
如果我们想要直接实例化这个接口Idea会提示错误: 


 

一个类要实现某个接口但是却没有实现其所有方法, Idea也会给予提示: 

 

上面说getSides方法没有实现, 当我们把这个方法加上以后错误提示就会消失: 


 

并且我们可以看到, 在行号的右侧出现了继承关系的图标, 点击图标可以在接口与实现之间跳转. 

继承 

标记了@extends标签的类, 在Idea里的代码补全里可以给出父类的方法: 
 

批量添加方法 
很多时候我们并不是直接用prototype的方式一个一个的向类里添加方法, 而是一些第三方类型的类型支持功能创建类型. 如著名的John Resig'Class类(http://blog.buymeasoda.com/understanding-john-resigs-simple-javascript-i/)

Js代码  收藏代码
  1. /** 
  2.  * Person类 
  3.  * @class 
  4.  * @extends {Class} 
  5.  */  
  6. var Person = Class.extend(  
  7.     {  
  8.         init: function (car) {  
  9.             /** @type {Car} */  
  10.             this.car = car;  
  11.         },  
  12.   
  13.         canDrive: function () {  
  14.             return this.car.size > 10;  
  15.         }  
  16.     }  
  17. );  

其中init是构造函数, canDrive是成员函数.上面这种写法,IDEA并不能识别init和canDrive. 
我们需要在extend函数的参数值之前用@lends来指定这种关系.@constructs指明那个函数是构造函数.注意@lends后面用的是Person.prototype, 因为其实我们是把函数加到prototype里. 

Js代码  收藏代码
  1. /** 
  2.  * Person类 
  3.  * @class 
  4.  * @extends {Class} 
  5.  */  
  6. var Person = Class.extend(  
  7.     /** @lends Person.prototype */  
  8.     {  
  9.         /** 
  10.          * 构造函数 
  11.          * @constructs 
  12.          * @param car {Car} 
  13.          */  
  14.         init: function (car) {  
  15.             /** @type {Car} */  
  16.             this.car = car;  
  17.         },  
  18.   
  19.         /** 
  20.          * 能否驾驶 
  21.          * 
  22.          * @return {Boolean} 
  23.          */  
  24.         canDrive: function () {  
  25.             return this.car.size > 10;  
  26.         }  
  27.     }  
  28. );  


尝试调用new Person时, IDEA能把init当构造函数, 并且指明需要一个Car类型的参数. 
 

同时Person的实例中也有相应的方法提示 
 

6. 成员函数 

Module模式是javascript里常用的一种设计模式.(http://www.adequatelygood.com/JavaScript-Module-Pattern-In-Depth.html

当我们要声明Module的成员函数时,需要用@memberOf指定一个方法数据某个Module. 而Module本身需要声明为@namespace. 

Java代码  收藏代码
  1. /** @namespace */  
  2. var Tools = {};  
  3.   
  4. /**  
  5.  * @memberof Tools  
  6.  * @param {String} str 
  7.  **/  
  8. var hammer = function(str) {  
  9. };  
  10.   
  11. Tools.hammer = hammer;  


其实不写@namespace和@memberof时, IDEA也能很好的解析两者的关系. 因为IDEA看到了 
Tools.hammer = hammer;这样显示的调用. 两者的关系没有通过这么明显的方式表示出来, IDEA就无法正确的给出代码补全了. 
 

加了jsdoc的情况 
 

JavaScript没有对成员函数的访问控制支持, 也就是没有类似Java中的private, protected, public等修饰符. 通过jsdoc可以指定这种访问控制. 
 
可以看到, 当尝试访问Tools.sickle方法时,Idea会给出提示. 

7. 模板方法 

模板方法是Google Closure独有的jsdoc支持, Idea对这个功能提供了很好的支持, 真是让人倍感惊喜. 模板方法具体是什么我就不解释了, 熟悉Java,C++, C#中的泛型的同学看一眼就知道. 

下面是一个Foo类, 这个类的set的参数,get的返回值是可以在实例化是确定. 
Java代码  收藏代码
  1. /** 
  2.  * @constructor 
  3.  * @template T 
  4.  */  
  5. Foo = function () {  
  6. };  
  7. /** @return {T} */  
  8. Foo.prototype.get = function () {  
  9. };  
  10.   
  11. /** @param {T} t */  
  12. Foo.prototype.set = function (t) {  
  13. };  
  14.   
  15. /** @type {Foo.<FooBean>} */  
  16. var foo = new Foo();  

例如上面的最后一句, 我们可以确定, foo的set和get方法的类型为FooBean. 
这是Idea的代码补全功能会自动的把FooBean最为set和get的类型,并给出提示 

错误提示 
 
代码补全 
 

我用的Intellij Idea的版本是13.1.2, 我不能保证上面写的就是Idea对jsdoc的所有支持, 上面的标签也不是jsdoc的全部标签. 似乎Idea的每次升级都会对js的支持更加丰富, 期待以后的版本中有更加完善的jsdoc支持. 






0 0