怎样写出简洁的TypeScript项目

Posted SEvent的博客

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了怎样写出简洁的TypeScript项目相关的知识,希望对你有一定的参考价值。

TypeScript是javascript语言的超集,使用它编写完成后可编译成为JS在浏览器上运行。选择使用TS来替代JS的原因是前者在后者基础上提供了类型检测以及更加全面的面向对象编程手段,使得我们编写的代码更加易读且质量更高。那么如何写出更加简洁易读且高质量的TS代码呢?下面是根据本人多年经验总结的一些点,如有需要补充及纠正的可以在评论区写出。

变量声明

  • 变量命名需遵循“易懂”的原则,起名时多打几个字可以为你再次浏览代码省去大量的理解时间。

 
   
   
 
  1. let num;//坏

  2. let numberOfStudents;//好

另外,变量命名需遵循以小写字母开头的驼峰式命名方式。

  • 使用常量const来定义一些配置项,注意,const类型的变量需要使用全大写字母加下划线来命名,这样使你一眼就知道该变量是const类型。使用const可以避免在编写代码时误修改变量的值

 
   
   
 
  1. for(let i=0; i<365; i++)

  2. {

  3. //坏例子,不知道该循环是想干嘛,且出现多次该循环的情况下,一旦修改循环终结条件,即365这个值,需要找到每个循环出现的地方并一个个修改365这个值,一是工作量打大,二是容易发生遗漏

  4. }

  5. const DAY_OF_A_YEAR = 365;//好例子,使用常量明确了365这个值的意义,且修改常量的值可以一次性地改掉项目中所有循环的终结条件

  6. DAY_OF_A_YEAR += 1;//使用const还能避免项目中对其的误修改。此代码会报错说不能修改常量

  7. for(let i=0; i<DAY_OF_A_YEAR; i++)

  8. {

  9. //一眼就看出来该循环是在遍历一年中的每一天

  10. }

  • 使用枚举Enum来定义选项,定义后可以受益于VS CODE这个IDE下相当直观的代码提示。值得注意的是,Enum也是一种常量,它的值也没法被修改。

 
   
   
 
  1. var ALIGN_LEFT=0, ALIGN_RIGHT=1;//JS中常用方式,使用统一前缀来将一组变量关联起来

  2. var ALIGN = {LEFT:0, RIGHT:1};//JS中另一种常用方式,较上一种更加直观

  3. enum ALIGN{LEFT, RIGHT};//TS中可以使用枚举来定义,与上面那种用法差不多,但是好处是代码提示更加直观

  • 类Class中的共有(public)变量需以小写字母开头,受保护(protected)变量需以dollar符号($)开头,私有(prvate)变量需以下划线(_)开头

 
   
   
 
  1. class C{

  2.    public publicVar;

  3.    protected $protectedVar;

  4.    private _privateVar;

  5.    protected $protectedFuc(){}

  6.    private _privateFuc(){}

  7. }

这么做的好处,一方面可以让程序员一眼就知道成员变量/方法的可访问状态,另一方面在VS CODE等IDE的代码“大纲”视图下可以将所有成员变量/方法排放得非常整齐便于查找。

代码位置

分散在项目中各处的代码就像零乱散落在地面的零件,将它们分类整合后放置可以很方便地找到它们。


  • 类Class中,public类型方法/变量需放在最上方,因为它被浏览得最频繁,其次是protected的以及private的。如果有某些protected的方法,如init之类的频繁需要浏览和修改的也可放在偏前的位置,总之,放置在前还是后全根据该变量/方法被浏览/修改的频繁程度而定。


  • 常量const按关联性进行放置。

 
   
   
 
  1. enum PLAYER_STATE{SIT=1, READY, PLAYING, WATCH};//玩家状态

  2. class PlayerVO{

  3.    static PLAYER_STATE = PLAYER_STATE;//因为其仅与PlayerVO这个类有关系,因此放在PlayerVO中

  4. }

若有一些常量是全局都会用到的,则需要单独新建一个地方将它们收集起来

 
   
   
 
  1. //Definition.ts

  2. namespace core.Definition{

  3.    /** 卡牌花色 */

  4.    export enum CARD_SUIT{HEART='heart', SPADE='spade', DIAMOND='diamond', CLUB='club', E='e', F='f'};

  5.    /** 卡牌花色对应颜色 */

  6.    export let CARD_SUIT_COLOR = {heart:'0', spade:'1', diamond:'0', club:'1', e:'1', f:'0'};

  7.    /** 座位 */

  8.    export enum SEAT {

  9.        BOTTOM, RIGHT, TOP, LEFT

  10.    };

  • 不同属性的文件放到不同的文件夹中,属性可分为“核心类(core)”“视图类(view)”“数据类(vo)”等等

 
   
   
 
  1. view|-game|-PlayerView.ts

  2.    |     |-ChatView.ts

  3.    |

  4.    |-lobby|-RoomView.ts

  5.    |      |-MallView.ts

  6.    |

  7.    |-scene|-GameScene.ts

  8.           |-LobbyScene.ts

  9. core|-Defination.ts

  10.    |-EventManager.ts

  11.    |-NetworkManager.ts

  • 按代码功能分类排放。

 
   
   
 
  1. //GameUtils.ts

  2. namespace core.GameUtils{

  3.    //== 时间操作 ==//

  4.    export function getDateFromString(str:string){){}

  5.    export function getRelativeDay(targetDate:Date){}

  6.    //每个功能区之间需以两个以上的空行隔开,看起来更加清晰

  7.    //== 对象操作 ==//

  8.    export const OBJ_NUM = 10;//常量不一定要放在最上方位置,按其功能放置到相应区域即可

  9.    export function cloneObject(obj){}

  10.    export function getKeyByValue(obj, value){}

  11. }

  12. //LobbyScene.ts

  13. export class LobbyScene implements core.IGuidePlayer{

  14.    constructor(){}

  15.    protected init(){}

  16.    destroy(){}

  17.    private _updateView(){}

  18.    //下面是实现的core.IGuidePlayer接口中定义的两个方法,需要放在一起,此时不必遵循public方法必须出现在最前位置的限制

  19.    playGuide(){}

  20.    stopGuide(){}

  21. }

代码封装

  • 使用getter/setter封装

 
   
   
 
  1. class UserVO{

  2.    head:string;

  3. }

  4. function onGetData(data)

  5. {

  6.    let u = new UserVO();

  7.    u.head = data ? data : '1.jpg';

  8. }

  9. function onGetData2(data)

  10. {

  11.    let u = new UserVO();

  12.    u.head = data.head ? data.head : '1.jpg';

  13. }

按照上面的代码写法,需要在外部每次设置UserVO实例的head属性时对设置的值进行判断处理,这样会多次出现重复代码且难以管理,此时使用setter进行改写

 
   
   
 
  1. class UserVO{

  2.    private _head:string;

  3.    set head(v:string){

  4.        this._head = v ? v : '1.jpg';

  5.    }

  6.    get head():string{

  7.        return this._head;

  8.    }

  9. }

  10. function onGetData(data)

  11. {

  12.    let u = new UserVO();

  13.    u.head = data;

  14. }

  15. function onGetData2(data)

  16. {

  17.    let u = new UserVO();

  18.    u.head = data.head;

  19. }

一般来说,getter的使用大多数情况下只是return一个private的变量值,因为在取变量值前做一些处理的需求不太常见,如果真要做一些处理,大可以用一个以"get"开头的方法来实现。不过我认为某些某些时候使用getter比使用'get'开头的方法更加直观一些

 
   
   
 
  1. class CardList{

  2.    private _data:string[];

  3.    setData(v:string){

  4.        this._data = v.split(',');

  5.    }

  6.    getLength():number{

  7.        return this._data.length;

  8.    }

  9.    get length():number{

  10.        return this._data.length;

  11.    }

  12.    //个人觉得,使用cardList.length比cardList.getLength()更加直观一些

  13. }

使用getter有时候还可以确保返回的数据是最新的,防止程序员漏更新某个变量的情况出现

 
   
   
 
  1. class CardList{

  2.    data:string[];

  3.    dataLength:number;

  4. }

  5. let cl = new CardList();

  6. cl.data = ['1','2','3','4'];

  7. cl.dataLength = c1.data.length;

  8. cl.data.pop();

  9. console.log(c1.dataLength);//输出为4,但实际上是3

  10. //使用getter替代的方案

  11. class CardList{

  12.    data:string[];

  13.    private _dataLength:number;

  14.    get dataLength():number{

  15.        return data.length;

  16.    }

  17.    //这样每次使用cl.length拿到的都是准确的数据,但是会产生一些执行getter方法内代码的额外开销

  18. }

  • class封装
    我们通常使用class来封装一个种类的代码块,以此来节省代码量,如下:

 
   
   
 
  1. let rec1 = {x:10, y:10, width:10, height:10};

  2. let rec2 = {x:10, y:10, width:20, height:20};

  3. let rec1Right = rec1.x + rec1.width;

  4. let rec1Bottom = rec1.y + rec1.height;

  5. let rec2Right = rec2.x + rec2.width;

  6. let rec2Bottom = rec2.y + rec2.height;

  7. //封装为Rectangle类

  8. class Rectangle{

  9.    x:number;

  10.    y:number;

  11.    width:number;

  12.    height:number;

  13.    get right():number{

  14.        return this.x + this.width;

  15.    }

  16.    get bottom():number{

  17.        return this.y + this.height;

  18.    }

  19. }

  20. let rec1 = new Rectangle();

  21. rec1.x = 10; rec1.y = 10; rec1.width = 10; rec1.height = 10;

  22. let rec1Right = rec1.right;

  23. let rec1Bottom = rec1.bottom;

将代码块封装为class可以让一些业务在class内部进行处理,外部无需关注实现细节,只需要将精力花在业务方面即可,且便于修改和拓展功能。比如说刚才的代码中要把Rectangle类的x属性更名为left,只需要使用VS CODE等IDE的重命名功能直接将x更名为left,IDE便会将项目中所有使用了rect.x的代码改为rect.left,非常快速以及彻底(这个替换过程一般是先找到修改名字的属性位于哪个类里面,然后再去找这个类的所有实例位置,最后将这些实例使用到该属性的代码做相应更名编辑)。
在封装类的时候需要遵循的原则是类要尽可能地精简,尽可能地拆分为不能拆分了为止,下面这个类就还有拆分的空间

 
   
   
 
  1. class GameScene{

  2.    player1Name:Label;

  3.    player1Money:Label;

  4.    player2Name:Label;

  5.    player2Money:Label;

  6.    setPlayerInfo(playerIndex, playerInfo)

  7.    {

  8.        this['player' + playerIndex + 'Name'].text = playerInfo.name;

  9.        this['player' + playerIndex + 'Money'].text = playerInfo.money;

  10.    }

  11. }

  12. //执行拆分

  13. class GameScene{

  14.    player1:PlayerControl;

  15.    player2:PlayerControl;

  16.     setPlayerInfo(playerIndex, playerInfo)

  17.    {

  18.        this['player' + playerIndex].data = playerInfo;

  19.    }

  20. }

  21. class PlayerControl{

  22.    nameLabel:Label;

  23.    moneyLabel:Label;

  24.    private _data;

  25.    set data(v){

  26.        this._data = v;

  27.        if(v)

  28.        {

  29.            this.nameLabel.text = v.name;

  30.            this.moneyLabel.text = v.money;

  31.        }

  32.    }

  33.    get data(){

  34.        return this._data;

  35.    }

  36. }

通俗一点讲这个原则叫做“分权制”,能分权的地方就分权,分权后我不再过问,你管好你自己我管好我自己即可,此时,父(如上例中的GameScene)与子(如上例中的PlayerControl)之间应保持相互独立,由父来调控各个子的活动(如上例中GameScene::setPlayerInfo方法中所做的事情),子最好不要对父有任何直接操纵的行为

 
   
   
 
  1. class PlayerControl{

  2.    init(){

  3.        this.button.on('click', this, this._onClick);

  4.    }

  5.    private _onClick(){

  6.        this.parent.doSomething();//直接操纵父是不推荐的,强耦合的行为

  7.    }

  8. }

子直接操纵父是强耦合的行为,因为子并不确定父有哪些方法,而且就算你现在知道,但是一旦父的方法名变更了也就会出现错误。子与祖辈交互的推荐方式是以下三种:

  1. 派发事件

  2. 执行回调函数

  3. 通过中间人来协调

 
   
   
 
  1. class PlayerControl extends EventDispatcher{

  2.    onBtnClick:Function;

  3.    private _onClick(){

  4.        this.emit('btnClick');//方法一,派发事件

  5.        this.onBtnClick && this.onBtnClick();//方法二,执行回调

  6.        core.EventManager.instance().dispatchEvent('btnClick');//方法三,通过中间人协调。此处的中间人是一个可以自派发/侦听的单例

  7.    }

  8. }

  9. class GameScene{

  10.    player:PlayerControl;

  11.    init(){

  12.        this.player = new PlayerControl();

  13.        this.player.on('btnClick', this, this.doSomething);//方法一,侦听事件

  14.        this.player.onBtnClick = this.doSomething.bind(this);//方法二,设置回调

  15.        core.EventManager.instance().addEventListener('btnClick', this, this.doSomething);//方法三,通过中间人协调

  16.    }

  17. }

相对的,父也不应该管得太多太细,否则就会显得代码冗余且耦合度不够低

 
   
   
 
  1. class GameScene{

  2.    player1:PlayerControl;

  3.    player2:PlayerControl;

  4.     setPlayerInfo(playerIndex, playerInfo)

  5.    {

  6.        let player = this['player' + playerIndex];

  7.        //管太细了,应该由PlayerControl自己来处理内部各视图的更新操作并提供一个视图更新方法供父调用

  8.        player.nameLB.text = playerInfo.name;

  9.        player.moneyLB.text = playerInfo.money;

  10.    }

  11. }

  • 数据与视图进行分离 如果数据与视图未进行分离,那么每次数据更新的时候你都必须手动更新所有的视图,该过程会极其痛苦

 
   
   
 
  1. class PlayerControl{

  2.    nameLB:Label;

  3.    moneyLB:Label;

  4.    private _data;

  5.    set data(v){

  6.        this._data = v;

  7.        if(v)

  8.        {

  9.            this.nameLabel.text = v.name;

  10.            this.moneyLabel.text = v.money;

  11.        }

  12.    }

  13.    get data(){

  14.        return this._data;

  15.    }

  16. }

  17. let playerInfo = {name:'n', age:18};

  18. let player1:PlayerControl = new PlayerControl();

  19. let player2:PlayerControl = new PlayerControl();

  20. player1.data = player2.data = playerInfo;

  21. //发生数据变化

  22. playerInfo.name = 'a';

  23. //必须手动更新所有相关UI

  24. player1.nameLabel.text = playerInfo.name;

  25. player2.nameLabel.text = playerInfo.name;

在手动更新所有相关视图的过程中你很容易会发生更新不全的情况,因为此数据有可能用在了分布在项目中各个地方的多个实例中。
而且还有一个缺点是,在数据的使用处,如上例中的PlayerControl类的data属性里面,你无法得知这个data对象里有哪些字段/属性,这样就有可能出现拼写错误

 
   
   
 
  1. class PlayerControl{

  2.    set data(v){

  3.        this._data = v;

  4.        if(v)

  5.        {

  6.            this.nameLabel.text = v.nam;//name拼写错误成了nam

  7.            this.moneyLabel.text = v.mony;//money拼写错误成了mony

  8.        }

  9.    }

  10. }

因为这个data属性类型是any,故IDE无法得知里面有哪些字段/属性,也就无法为你在编写时提供代码提示也不会在你写错后提示错误。这种时候,将data抽象成一个数据类(Value Object Class)就迫在眉睫了。

 
   
   
 
  1. type PropertyChangeHandler = (newValue:any, oldValue?:any) => void;

  2. type PropertyChangeHandlerData = {handler:PropertyChangeHandler, context:any, justOnce:boolean};

  3. /** 可绑定的VO类。使用其中提供的propertyChange方法可以抛送属性变更事件;使用watchProperty方法可以侦听属性变更 */

  4. export class BindableVO{

  5.    /**

  6.     * 属性值发生变更时需要调用此方法来触发侦听器

  7.     * @param pName        发生变化的属性名称

  8.     * @param oldValue     发生变化的属性旧值

  9.     * @param newValue     发生变化的属性新值

  10.     */

  11.    propertyChange(pName:string, oldValue, newValue){

  12.        //实现细节略

  13.    }

  14.    /**

  15.     * 添加属性值变更侦听器

  16.     * @param pName      需要侦听的属性名称

  17.     * @param handler    侦听器

  18.     * @param context    侦听器里面的this指向

  19.     * @param justOnce   该侦听器是否只会执行一次

  20.     */

  21.    watchProperty(pName:string, handler:PropertyChangeHandler, context?:any, justOnce:boolean=false){

  22.        //实现细节略

  23.    }

  24.    /**

  25.     * 移除属性值变更侦听器

  26.     * @param pName      需要侦听的属性名称

  27.     * @param handler    侦听器,为空则移除全部侦听器

  28.     * @param context    侦听器里面的this指向

  29.     */

  30.    unwatchProperty(pName:string, handler:PropertyChangeHandler, context?){

  31.        //实现细节略

  32.    }

  33. }

  34. class PlayerVO extends BindableVO{

  35.    private _name:string;

  36.    set name(v:string){

  37.        if(v != this._name)

  38.        {

  39.            let old = this._name;

  40.            this._name = v;

  41.            //属性发生变化时会调用一些属性变化侦听函数

  42.            this.propertyChange('name', old, v);

  43.        }

  44.    }

  45.    get name():string{

  46.        return this._name;

  47.    }

  48. }

  49. class PlayerControl{

  50.    private _data:PlayerVO;//规定data的类型,这样就会有代码提示及错误检查

  51.    set data(v:PlayerVO){

  52.        if(this._data != v)

  53.        {

  54.            //去掉旧的数据变化侦听器

  55.            if(this._data)

  56.            {

  57.                this._data.unwatchProperty('name', this, this._updateView);

  58.            }

  59.            this._data = v;

  60.            //加上新的数据变化侦听器

  61.            if(v)

  62.            {

  63.                this._data.watchProperty('name', this, this._updateView);

  64.            }

  65.            //立即更新界面一次

  66.            this._updateView();

  67.        }

  68.    }

  69.    private _updateView(){

  70.        this.nameLabel.text = this._data ? this._data.name : '';

  71.    }

  72. }

上面的代码中,通过继承BindableVO,PlayerVO具备了数据绑定的能力,之后,在将一个PlayerVO实例设置到PlayerControl的data属性后,PlayerControl对其进行了数据和视图的绑定工作,一旦数据发生了变化,视图会自动进行更新,也就是说,一旦一个PlayerVO实例的name属性发生了变化,所有使用了它的PlayerControl都会自动更新视图,这样一来,我们就可以把精力集中在核心业务的处理上而不需要操心其他杂碎的视图更新工作了。

 
   
   
 
  1. class GameScene{

  2.    player1:PlayerControl;

  3.    player2:PlayerControl;

  4.    commonPlayer:PlayerVO;

  5.    init(){

  6.        this.commonPlayer = new PlayerVO();

  7.        this.player1.data = this.player2.data = this.commonPlayer;

  8.        this.commonPlayer.name = 'n';//此时this.player1和this.player2中的视图均会自动更新

  9.    }

  10. }

  • 通过继承来减少重复代码 如果几个类之间大部分相同,只有少许功能不同则可以考虑使用继承来有效节约代码量,如上面提到过的BindableVO和PlayerVO之间就用了继承的概念,使PlayerVO节约了数据绑定那一部分功能的代码。接下来再给出一个将一个复杂的类使用继承概念分拆成多个类的例子

 
   
   
 
  1. enum GAME_TYPE{NORMAL, MATCH}

  2. class GameScene{

  3.    type:GAME_TYPE;

  4.    isGameStart:boolean;

  5.    private _normalBtn:Button;

  6.    private _matchBtn:Button;

  7.    init(){

  8.        if(this.type == GAME_TYPE.NORMAL)

  9.        {

  10.            this._normalBtn = new Button();

  11.            this.addChild(this._normalBtn);

  12.            this._normalBtn.on('click', this, this.startGame);

  13.        }

  14.        else if(this.type == GAME_TYPE.MATCH)

  15.        {

  16.            this._matchBtn = new Button();

  17.            this.addChild(this._matchBtn);

  18.            this._matchBtn.on('click', this, this.startGame);

  19.        }

  20.    }

  21.    startGame(){

  22.        this.isGameStart = true;

  23.        if(this.type == GAME_TYPE.NORMAL)

  24.        {

  25.            console.log('NORMAL GAME');

  26.        }

  27.        else if(this.type == GAME_TYPE.MATCH)

  28.        {

  29.            console.log('MATCH GAME');

  30.        }

  31.    }

  32. }

  33. //==========拆分==========//

  34. //将各种游戏类别都具有的共同代码放在此类中

  35. class GameScene{

  36.    isGameStart:boolean;

  37.    init(){}

  38.    startGame(){

  39.        this.isGameStart = true;

  40.    }

  41. }

  42. class GameSceneNormal extends GameScene{

  43.    init(){

  44.        this._normalBtn = new Button();

  45.        this.addChild(this._normalBtn);

  46.        this._normalBtn.on('click', this, this.startGame);

  47.    }

  48.    startGame(){

  49.        super.startGame();//执行该方法在父类中的代码

  50.        //接下来实现差异化代码

  51.        console.log('NORMAL GAME');

  52.    }

  53. }

  54. class GameSceneMatch extends GameScene{

  55.    init(){

  56.        this._matchBtn = new Button();

  57.        this.addChild(this._matchBtn);

  58.        this._matchBtn.on('click', this, this.startGame);

  59.    }

  60.    startGame(){

  61.        super.startGame();//执行该方法在父类中的代码

  62.        //接下来实现差异化代码

  63.        console.log('MATCH GAME');

  64.    }

  65. }

  66. //根据游戏类别来实例化不同的GameScene类型

  67. let type = GAME_TYPE.NORMAL;

  68. let gameScene:GameScene = type === GAME_TYPE.NORMAL ? new GameSceneNormal() : new GameSceneMatch();

可以观察到,分拆后的代码看起来更加清晰,精简也容易维护

注释

注释不是越多越好,毕竟你不是在写一个API文档。尽可能地让变量/方法名一看就知道其含义

 
   
   
 
  1. function parseData(){}//难懂

  2. function transformObjectToDate(){}//易懂

  3. let flag;//难懂

  4. let hasFoundResult;//易懂

只有在变量/方法名无法清楚地描述功能时再考虑使用注释来说明清楚,比如一些方法的参数描述很重要

 
   
   
 
  1. /**

  2. * 遍历一个数组

  3. * @param ary   遍历对象

  4. * @param loopFuc 遍历回调函数。接受两个参数表示当前遍历对象及对象所在索引号,此回调函数若是返回一个非falsy的值则会中断遍历并使aryForEach方法返回此值

  5. * @param reverse 是否逆序遍历

  6. */

  7. function aryForEach(ary:any[], loopFuc:(elem:any, index:number) => any, reverse=false):any{}

在变量/方法的声明语句前面使用JSDoc的注释语法(/* 注释内容 */)来做注释可以使我们在鼠标移到变量/方法的名字上时能弹出注释文字,效果优于传统注释(//注释内容),使用它可以使我们不需要去到变量/方法的声明位置就能查看该变量/方法的含义及使用方法,节约了时间。
使用注释来做一些代码功能块之间的分割线效果也不错

 
   
   
 
  1. //================================================运行设备检查================================================//

  2. /** 是否运行在微信浏览器内 */

  3. export function isWeiXin():boolean{}

  4. //========================================= 数组操作 =========================================//

  5. /** 检查一个元素是否在数组中,若在则将其从数组中移除,否则添加进数组 */

  6. export function toggleArrayElem(elem, ary:any[]){}

或者你还可以利用注释来在代码中做一些标记/锚点,方便搜索

 
   
   
 
  1. //file1.ts

  2. function doSth(){

  3.    console.log('调试内容');//DEL 版本发布后需要删除

  4. }

  5. //file2.ts

  6. function doSth2(){

  7.    console.log('调试内容');//DEL 版本发布后需要删除

  8. }

这样,在版本发布后,只需要全局搜索“DEL”这个关键字即可找到所有需要删除的地方。需要注意的是,在Eclipse等IDE中,使用特殊的"//TODO"标记的注释可以被IDE理解成为“待办任务”并集中列出在“任务视图”窗口中,连搜索这步都直接省略了。

不写让人费解的代码

现在很多人为了所谓的“执行速度快”和“装逼”爱写一些让人难以理解的代码

 
   
   
 
  1. //=============难以理解的代码=============//

  2. //判断是否等于-1

  3. let ary = ['1','2'];

  4. //~为取反位运算符,~-1 == 0

  5. if(~ary.indexOf('3'))console.log('have 3');

  6. //取整

  7. let num = 1.2345;

  8. let intNum = num >> 0;//右移0位可去掉小数部分

  9. //类型转换为Boolean

  10. let str = 'false';

  11. let boolResult = !!str;

  12. //=============容易理解的代码=============//

  13. if(ary.indexOf('3') != -1)console.log('have 3');

  14. intNum = Math.floor(num);

  15. boolResult = Boolean(str);

在写复杂算法时,不要吝啬声明变量和方法,为了让代码看起来更易读,可以多命名一些变量和方法来代替注释,使代码整齐易懂

 
   
   
 
  1. //难懂的写法

  2. function isMatch(obj):boolean{

  3.    for(let i of nameAry)

  4.    {

  5.        if(i.name == obj.name)return true;

  6.    }

  7.    for(let i of ageAry)

  8.    {

  9.        if(i.age == obj.age)return true;

  10.    }

  11.    for(let i of moneyAry)

  12.    {

  13.        if(i.money == obj.money)return true;

  14.    }

  15.    return false;

  16. }

  17. //易懂的写法

  18. function isMatch(obj):boolean{

  19.    function isNameMatch(){

  20.        for(let i of nameAry)

  21.        {

  22.            if(i.name == obj.name)return true;

  23.        }

  24.        return false;

  25.    }

  26.    function isAgeMatch(){

  27.        for(let i of ageAry)

  28.        {

  29.            if(i.age == obj.age)return true;

  30.        }

  31.        return false;

  32.    }

  33.    function isMoneyMatch(){

  34.        for(let i of moneyAry)

  35.        {

  36.            if(i.money == obj.money)return true;

  37.        }

  38.        return false;

  39.    }

  40.    return isNameMatch() || isAgeMatch() || isMoneyMatch();

  41. }

大多数的情况下,程序员都不愿意看带有循环(while, for...in)的代码,因为嵌套较多也比较难理解

 
   
   
 
  1. let v = {value:1, key:'age'};

  2. let target;

  3. for(let i of ary1)

  4. {

  5.    for(let j of i.list)

  6.    {

  7.        for(let k of j.list)

  8.        {

  9.            //一旦循环体内代码复杂就会造成理解成本成倍上升

  10.            if(v.value == k.value && v.key == k.key)target = k;

  11.        }

  12.    }

  13. }

因此,建议将循环封装成函数,理解成本就低,而且还可以使用“return”语句提前结束代码执行

 
   
   
 
  1. let target = searchTarget(v);

  2. function searchTarget(keywords:{value:number, key:string}){

  3.    for(let i of ary1)

  4.    {

  5.        for(let j of i.list)

  6.        {

  7.            for(let k of j.list)

  8.            {

  9.                //使用return提前结束代码运行,可以有效节约垃圾时间

  10.                if(v.value == k.value && v.key == k.key)return k;

  11.            }

  12.        }

  13.    }

  14. }

利用Array的find(寻找第一个匹配元素),map(将数组中各元素进行某种转换),filter(过滤掉一些元素)等方法来替代元素遍历,使我们在节约代码的同时让人更加易于理解

判断条件时的代码节约

使用大量的if语句来进行判断不仅代码量大,让人看着也不舒服,当判断条件大于3个时使用switch语句更加清晰,有些时候还可以使用字典查询或数组搜索方式来进行判断

 
   
   
 
  1. let availableOptionsAry = [1,2,3,4,5,6,7,8,9];

  2. let availableOptionsDic = {1:true, 2:true, 3:true, 4:true, 5:true, 6:true, 7:true, 8:true, 9:true};

  3. function isOptionAvailable(o){

  4.    return o == 1 || o == 2 ||... //不推荐的写法,效果等同于多个if判断

  5.    return availableOptionsAry.indexOf(o) != -1;//数组搜索法

  6.    return availableOptionsDic[o] != undefined;//字典查询法

  7. }

  8. //一般来说,字典查询法不会用在简单地搜索true or false上面,而是会将大部分条件成立后的代码放在字典的value中

  9. let sceneList = {'lobby':view.scene.LobbyScene, 'game':view.scene.GameScene};

  10. function gotoScene(sceneName:string){

  11.    let scene;

  12.    //不推荐的写法

  13.    if(sceneName == 'lobby')scene = new view.scene.LobbyScene();

  14.    else if(sceneName == 'game')scene = new view.scene.GameScene();

  15.    //字典查询法

  16.    let theConstructor = sceneList[sceneName];

  17.    if(theConstructor)scene = new theConstructor();

  18.    //如果将sceneList中的value改成一些函数的引用,就可以在查找到sceneName对应的key时执行不同的函数

  19. }

重复代码整合

如果你的代码中出现了大量的重复代码,试着将其做一些整理,能缩写的缩写,能剥离成函数的剥离成函数

 
   
   
 
  1. //整理前

  2. let lvAry = [{lv:1, name:'白银会员'}, {lv:2, name:'黄金会员'}];

  3. let playerList = [{id:1, lv:1}, {id:2, lv:2}];

  4. function updateLv(id:string){

  5.    let player;

  6.    let lvName;

  7.    for(let i of playerList)

  8.    {

  9.        if(i.id === id){

  10.            player = i;

  11.            break;

  12.        }

  13.    }

  14.    if(player)

  15.    {

  16.        for(let i of lvAry)

  17.        {

  18.            if(i.lv === player.lv){

  19.                lvName = i.name;

  20.                break;

  21.            }

  22.        }

  23.    }

  24.    if(lvName){

  25.        view.popup.Alert.show('您的等级是:' + lvName, view.popup.Alert.Alert_OK);

  26.    }

  27.    else{

  28.        view.popup.Alert.show('找不到等级', view.popup.Alert.Alert_OK);

  29.    }

  30. }

  31. //整理后

  32. function updateLv(id:string){

  33.    let Alert = view.popup.Alert;

  34.    let player = getPlayerByID(id);

  35.    if(player)

  36.    {

  37.        let lvName = getLvNameByLv(player.lv);

  38.        let alertMsg = lvName ? '您的等级是:' + lvName : '找不到等级';

  39.        Alert.show(alertMsg, Alert.Alert_OK);

  40.    }

  41. }

  42. function getPlayerByID(id:string){

  43.    for(let i of playerList)

  44.    {

  45.        if(i.id === id){

  46.            return i;

  47.        }

  48.    }

  49. }

  50. function getLvNameByLv(lv:string){

  51.    for(let i of lvAry)

  52.    {

  53.        if(i.lv === lv){

  54.            return i;

  55.        }

  56.    }

  57. }

总结

一切优化的最终目的都是为了提高编码的效率,编码的效率表现在以下几个方面:

  1. 看代码时的理解成本

  2. 为了实现功能,在修改/增加代码时花的时间

  3. 代码稳定性,BUG出现频率 因此,一切能提高上诉几个点的代码优化方法都是有效的方法


以上是关于怎样写出简洁的TypeScript项目的主要内容,如果未能解决你的问题,请参考以下文章

编写代码片段的更简洁的方法

20个简洁的 JS 代码片段

20个简洁的 JS 代码片段

几点建议帮你写出简洁的JS代码

几点建议帮你写出简洁的JS代码

使用自定义卫语句写出更简洁的代码