JavaScript 代码的组织方式

@CSS魔法

2015. 06. 18.

  • 无组织
  • 命名空间
  • “模块”
  • 模块

(30~45 分钟)

无组织

(这里给大家上真实代码)

var bindGetCodeAction = function () {
    $('#getMobileCode').click(function (ev) {
        countdown()
    })
}

if ($('input[name=mobileCode]').length) {
    bindGetCodeAction()
}

$('.form-submit').click(function (ev) {
    $.ajax({
        type: 'post',
        url: '/foo/bar',
        success: function (data) {
            if (data.status) {
                $('form').submit()
            } else {
                switch (data.code) {
                    case '10404':
                        //...
                        break
                    case '10403':
                        //...
                        break
                }
            }
        }
    })
})

var countdown = function () {
    //...
}
  • 无名可用
  • 冲突导致 bug
  • 长名难读
  • 难调试

全局变量泄漏

var bindGetCodeAction = function () {
    $('#getMobileCode').click(function (ev) {
        countdown()
    })
}

if ($('input[name=mobileCode]').length) {
    bindGetCodeAction()
}

$('.form-submit').click(function (ev) {
    $.ajax({
        type: 'post',
        url: '/foo/bar',
        success: function (data) {
            if (data.status) {
                $('form').submit()
            } else {
                switch (data.code) {
                    case '10404':
                        //...
                        break
                    case '10403':
                        //...
                        break
                }
            }
        }
    })
})

var countdown = function () {
    //...
}
  • 难读
  • 难调试、难测试

嵌套过深

(可能是为了避免太多全局变量)

var bindGetCodeAction = function () {
    $('#getMobileCode').click(function (ev) {
        countdown()
    })
}

if ($('input[name=mobileCode]').length) {
    bindGetCodeAction()
}

$('.form-submit').click(function (ev) {
    $.ajax({
        type: 'post',
        url: '/xxx/yyy',
        success: function (data) {
            if (data.status) {
                $('form').submit()
            } else {
                switch (data.code) {
                    case '10404':
                        //...
                        break
                    case '10403':
                        //...
                        break
                }
            }
        }
    })
})

var countdown = function () {
    //...
}
  • 静态:定义,备用
  • 动态:调用定义好的,产生变化

“静态” 和 “动态” 夹杂

  • 难读
  • 真 TM 难读!

命名空间

JavaScript 中的命名空间

  • 以树形结构组织代码
  • 使用对象的嵌套来模拟树形结构
  • 只暴露一个全局变量(树干)

命名空间带来的好处

  • 避免全局变量泄漏
  • 命名可读性好
  • 易调试、易测试
var fabu = {
    //...
}
  1. 确定一个全局变量
var fabu = {
    init: function () {
        //...
    },

    //...
}

fabu.init()
  1. 确定一个全局变量
  2. 动静分离
var fabu = {
    init: function () {
        this._bind()
    },

    _bind: function () {
        var _this = this
        if ($('input[name=mobileCode]').length) {
            $('#getMobileCode').click(function (ev) {
                _this.countdown()
            })
        }
        $('.form-submit').click(function (ev) {
            $.ajax({
                //...
                //...
                //...
            })
        })
    },

    countdown: function () {
        //...
    }
}

fabu.init()
  1. 确定一个全局变量
  2. 动静分离
  3. 给大树增加枝叶
var fabu = {
    init: function () {
        this._bind()
    },

    _bind: function () {
        var _this = this
        if ($('input[name=mobileCode]').length) {
            $('#getMobileCode').click(function (ev) {
                _this.countdown()
            })
        }
        $('.form-submit').click(function (ev) {
            _this._ajax()
        })
    },
    _ajax: function () {
        $.ajax({
            //...
            //...
            //...
        })
    },
    
    countdown: function () {
        //...
    }
}

fabu.init()
  1. 确定一个全局变量
  2. 动静分离
  3. 给大树增加枝叶
  4. 把嵌套打平
  1. 确定一个全局变量
  2. 动静分离
  3. 给大树增加枝叶
  4. 把嵌套打平
  5. 继续打平
var fabu = {
    init: function () {
        this._bind()
    },

    _bind: function () {
        //...
    },
    _ajax: function () {
        var _this = this
        $.ajax({
            type: 'post',
            url: '/xxx/yyy',
            success: function (data) {
                _this._success(data)
            }
        })
    },
    _success: function (data) {
        //...
        //...
        //...
    },
    
    countdown: function () {...}
}

fabu.init()
  1. 确定一个全局变量
  2. 动静分离
  3. 给大树增加枝叶
  4. 把嵌套打平
  5. 继续打平
  6. 再打平
var fabu = {
    init: function () {
        this._bind()
    },

    _bind: function () {
        //...
    },
    _ajax: function () {
        //...
    },
    _success: function (data) {
        if (data.status) {
            $('form').submit()
        } else {
            switch (data.code) {
                case '10404':
                    this._caseWrongInput()
                    break
                case '10403':
                    this._caseNeedRecheck()
                    break
            }
        }
    },
    _caseWrongInput: function () {...},
    _caseNeedRecheck: function () {...},

    countdown: function () {...}
}

fabu.init()
  1. 确定一个全局变量
  2. 动静分离
  3. 给大树增加枝叶
  4. 把嵌套打平
  5. 继续打平
  6. 再打平
  7. 继续整理
var fabu = {
    init: function () {
        this._getElem()
        this._bind()
    },

    _getElem: function () {
        this.$btnSubmit = $('.form-submit')
        this.$btnGetMobileCode = $('#getMobileCode')
        this.$inputMobileCode = $('input[name=mobileCode]')
    },
    _bind: function () {
        var _this = this
        if (this.$inputMobileCode.length) {
            this.$btnGetMobileCode.click(function (ev) {
                _this.countdown()
            })
        }
        this.$btnSubmit.click(function (ev) {
            _this._ajax()
        })
    },

    _ajax: function () {
        //...
    },
    _success: function (data) {...},
    _caseWrongInput: function () {...},
    _caseNeedRecheck: function () {...},

    countdown: function () {...}
}

fabu.init()
  1. 确定一个全局变量
  2. 动静分离
  3. 给大树增加枝叶
  4. 把嵌套打平
  5. 继续打平
  6. 再打平
  7. 继续整理
  8. 幻数 → 常量
var app = {
    CODE_WRONG_INPUT: '10404',
    CODE_NEED_RECHECK: '10403',
    init: function () {
        this._getElem()
        this._bind()
    },

    _getElem: function () {...},
    _bind: function () {...},

    _ajax: function () {...},
    _success: function (data) {
        if (data.status) {
            $('form').submit()
        } else {
            switch (data.code) {
                case this.CODE_WRONG_INPUT:
                    this._caseWrongInput()
                    break
                case this.CODE_NEED_RECHECK:
                    this._caseNeedRecheck()
                    break
            }
        }
    },
    _caseWrongInput: function () {...},
    _caseNeedRecheck: function () {...},

    countDown: function () {...}
}

app.init()
var fabu = {
    CODE_WRONG_INPUT: '10404',
    CODE_NEED_RECHECK: '10403',

    init: function () {
        this._getElem()
        this._bind()
    },

    _getElem: function () {...},
    _bind: function () {...},

    _ajax: function () {...},
    _success: function (data) {...},
    _caseWrongInput: function () {...},
    _caseNeedRecheck: function () {...},

    countdown: function () {...}
}

fabu.init()
var bindGetCodeAction = function () {
    $('#getMobileCode').click(function (ev) {
        countdown()
    })
}

if ($('input[name=mobileCode]').length) {
    bindGetCodeAction()
}

$('.form-submit').click(function (ev) {
    $.ajax({
        type: 'post',
        url: '/foo/bar',
        success: function (data) {
            if (data.status) {
                $('form').submit()
            } else {
                switch (data.code) {
                    case '10404':
                        //...
                        break
                    case '10403':
                        //...
                        break
                }
            }
        }
    })
})

var countdown = function () {
    //...
}
  • 避免全局变量泄漏
  • 命名可读性好
  • 易调试、易测试

回顾一下我们的期望

避免全局变量泄漏

var app = {}

// 注册功能
app.reg = {
    //...
}

// 登录功能
app.login = {
    //...
}

// 发布功能
app.fabu = {
    //...
}

// 初始化
app.reg.init()
app.login.init()
app.fabu.init()

每个功能都要占用一个全局变量?

树干只需要一个。

命名可读性好

// 发布的初始化方法
fabu.init()

// 发布的倒计时接口
fabu.countdown()

接近自然语言:

// 初始化
app.reg.init()
app.login.init()
app.fabu.init()
  • 不仅要可读。
  • 而且要 “可读”。

易调试、易测试

// 涉及的 DOM 元素
fabu.$btnSubmit


// 公开接口
fabu.countdown()

// 内部方法
fabu._success(data)

// 更细粒度的内部方法
fabu._caseWrongInput()
fabu._caseNeedRecheck()

所有变量、方法均可在全局访问。

新技能 Get

“模块”

模块模式

  • 要点:IIFE(立即调用的函数表达式)
  • 改进:只向外暴露需要暴露的

“模块模式” 的各种变种。

有前面命名空间的基础很容易理解和掌握。

void function () {
    'use strict'

    //...
	
}()
  1. 新建一个 IIFE    
void function () {
    'use strict'

    var CODE_WRONG_INPUT = '10404'
    var CODE_NEED_RECHECK = '10403'

    var $btnSubmit, $btnGetMobileCode, $inputMobileCode

    function init() {...}

    function _getElem() {...}
    function _bind() {...}

    function _ajax() {...}
    function _success(data) {...}
    function _caseWrongInput() {...}
    function _caseNeedRecheck() {...}

    function countdown() {...}

}()
  1. 新建一个 IIFE    
  2. 把变量和函数塞进去
void function () {
    'use strict'

    var CODE_WRONG_INPUT = '10404'
    var CODE_NEED_RECHECK = '10403'

    var $btnSubmit, $btnGetMobileCode, $inputMobileCode

    function init() {...}

    function _getElem() {...}
    function _bind() {...}

    function _ajax() {...}
    function _success(data) {...}
    function _caseWrongInput() {...}
    function _caseNeedRecheck() {...}

    function countdown() {...}

    var exports = {
        init: init,
        countdown: countdown
    }

}()
  1. 新建一个 IIFE    
  2. 把变量和函数塞进去
  3. 想好要暴露什么出来
var app = {}

void function () {
    'use strict'

    var CODE_WRONG_INPUT = '10404'
    var CODE_NEED_RECHECK = '10403'

    var $btnSubmit, $btnGetMobileCode, $inputMobileCode

    function init() {...}

    function _getElem() {...}
    function _bind() {...}

    function _ajax() {...}
    function _success(data) {...}
    function _caseWrongInput() {...}
    function _caseNeedRecheck() {...}

    function countdown() {...}

    var exports = {
        init: init,
        countdown: countdown
    }

    // exports
    app.fabu = exports

}()

app.fabu.init()
  1. 新建一个 IIFE    
  2. 把变量和函数塞进去
  3. 想好要暴露什么出来
  4. 挂载到命名空间
  • 避免全局变量泄漏
  • 命名可读性好
  • 易调试、易测试

再回顾一下我们的期望

易调试、易测试

按需暴露。

var app = {}

void function () {
    'use strict'

    var CODE_WRONG_INPUT = '10404'
    var CODE_NEED_RECHECK = '10403'

    var $btnSubmit, $btnGetMobileCode, $inputMobileCode

    function init() {...}

    function _getElem() {...}
    function _bind() {...}

    function _ajax() {...}
    function _success(data) {...}
    function _caseWrongInput() {...}
    function _caseNeedRecheck() {...}

    function countdown() {...}

    var fabu = {
        init: init,
        countdown: countdown
    }

    // debug
    fabu._caseWrongInput = _caseWrongInput
    fabu._caseNeedRecheck = _caseNeedRecheck

    // exports
    app.fabu = fabu
}()

模块

真正的模块系统

  • 每个模块一个文件(生产环境需对脚本资源打包)
  • 不需要全局变量做命名空间
  • 模块对外部一无所知,只决定向外暴露什么
  • 依赖关系可声明

模块系统的规范及实现

  • CommonJS (Node / Browserify ...)
  • AMD (RequireJS ...)
  • CMD (Sea.js ...)
  • ES6 (...)

CommonJS

定义模块

// fabu.js

'use strict'

var CODE_WRONG_INPUT = '10404'
var CODE_NEED_RECHECK = '10403'

var $btnSubmit, $btnGetMobileCode, $inputMobileCode

function init() {...}

function _getElem() {...}
function _bind() {...}

function _ajax() {...}
function _success(data) {...}
function _caseWrongInput() {...}
function _caseNeedRecheck() {...}

function countdown() {...}

module.exports = {
	init: init,
	countdown: countdown
}
// page.js

var fabu = require('fabu')

fabu.init()

使用模块

对日常开发的建议

  • 灵活对待各种场景
  • 扬长避短
  • 忌死板

Q & A

Thank You!

JavaScript 代码的组织方式

By CSS魔法

JavaScript 代码的组织方式

前端进阶之路(二)

  • 1,225