• [JavaScript]面向对象编程浅析之XJB讲


    面向对象编程浅析

    关于面向对象编程(Object Oriented Programming),每个人都有关于自己的理解,我们来参考一下维基的定义:

    维基百科

    是不是觉得很拗口?反正我是这么觉得的...

    因为他足够抽象,抽象到只有那种资深、拥有一定编程经验的程序员才能总结、归纳出来。

    作为(或者说是着手)一篇技术文章,理应做到客观中立、条理清晰,但是以本人的功力,自认尚未做得到抽丝剥茧层层递进,所以我把文章名叫面向对象编程浅析之XJB讲(当然只是调侃哈哈,还是要认真写的),说说自己对于OOP的粗浅认知,以及通过方老师讲课的例子记录一下怎么从MVP过渡到OOP,让代码更加便于分析和理解...反正我一度对着一连串的this笑了,学习可真有乐趣啊不是吗?


    从MVP说起

    在上一篇文章中,我们把代码根据不同的功能划分为三个部分:

    View代表视图

    Model代表对数据的操作(存储和获取等)

    Controller代表了View和Model两者之间的交互逻辑以及其他

    //message.js
    
    !function(){
        var view = $('section.messages')
    
        var model = {
            init : function(){
                var APP_ID = 'xxxxxxxxxxxxxxxxxxxxxxx'
                var APP_KEY = 'xxxxxxxxxxxxxxxxxxxxxxxxxx'
                AV.init({appId: APP_ID,appKey: APP_KEY})
            },
            //获取数据
            fetch:function(){
                var query = new AV.Query('Message');
                return query.find() //Promise对象
            },
    
            // 保存数据
            save:function(name,content){
                var Message = AV.Object.extend('Message')
                var messages = new Message();
                return messages.save({
                    "name":name,
                    "content":content ,
                })
            },
        }
    
        var controller = {
            view : null,
            model: null,
            init : function(view,model){
                this.model = model
                this.view = view
                this.myForm = document.querySelector('#postMessage')
                this.model.init()
                this.loadMessages()
                this.bindEvents()
            },
            loadMessages : function () {
                this.model.fetch().then((messages) => {
                    messages.forEach((item) => {
                        let span = $('<span></span>').text('发布于:'+ this.parseTime(item.createdAt))[0]
                        let li = $('<li></li>').text(item.attributes.name + ':' + item.attributes.content)
                        li.append(span)
                        $('#comments').append(li)
                    });
                }).then(function (messages) {
                    // 更新成功
                }, function (error) {
                    // 异常处理
                });
            },
            bindEvents : function(){
                this.myForm.addEventListener('submit',(e) => {
                    // 阻止传播,防止点击刷新
                    e.preventDefault()
    
                    this.saveMessages()
                })
            },
            saveMessages : function(){
                //获取输入框内的值
                let content = $('input[name="content"]').val()
                let name = $('input[name="name"]').val()
                this.model.save(name,content).then((object) => {
                    let span = $('<span></span>').text('发布于:'+ this.parseTime(object.createdAt))[0]
                    let li = $('<li></li>').text(object.attributes.name +':'+ object.attributes.content)
                    li.append(span)
                    $('#comments').append(li)
                    document.querySelector('#postMessage input[name="content').value = ''
                },function(error){
                    console.log(error)
                })
            },
            parseTime:function(d){
                const newDate = d.getFullYear() + '-' + (d.getMonth() + 1) + '-' + d.getDate() + ' '
                                + d.getHours() + ':' + d.getMinutes() + ':' + d.getSeconds();
                return newDate;
            }
        }
        controller.init(view,model)
    }.call()
    

    通过MVC,代码各司其职,逻辑自洽,而且便于维护:

    用户对View视图做出了操作,监听着View视图的controller接收到了View的变化通知,然后按需求去调用Model,Model向server发出请求;server返回响应给Model,Model返回数据给Controller,Controller根据接收到的数据更新View视图。

    现在这个js文件的结构是这样的:

    messages.js

    他通过提交表单让数据库保存用户留言、获取用户留言并展示在页面上,负责了留言功能的实现。

    但是,一旦我们把视角放大,我们就会发现,类似messages.js这样的文件在项目中有很多,比如a.js负责用户登录,b.js负责topbar样式切换,c.js负责轮播:

    all.js

    虽然,他们负责的功能都不同,但是!

    我们经过观察发现,每一个js文件中都存在同样的结构,即MVC!既然结构相同,我们就有了优化的可能。


    再说OOP

    OOP的重要思想之一就是:

    它将对象作为程序的基本单元,将程序和数据封装其中,以提高软件的重用性、灵活性和扩展性,对象里的程序可以访问及经常修改对象相关连的数据。

    还是很拗口啊...

    在参考了此问题的众多答案后,我总结了一个简单,易于理解的:

    OOP也是一种代码组织形式,他把相同的代码逻辑和行为封装成一个对象,简称‘父’;每当有类似的代码和行为需要得到应用,则可以通过传入参数new一个’父‘对象返回一个新对象,简称’子‘,‘子’在各方面都有着父的影子,他可以做‘父’能做的事情,但是‘子’和‘父’又有一点不同,‘子’可以被赋予更多的能力。

    that's it.

    概念就是这么简单,但是要执行起来的话就有点难度了,为什么?因为他很抽象。

    那么,就以message.js为例,从浅到略浅,练习一下怎么逐渐OOP。


    View

    既然每个js文件中都有view视图,我们就从view开始,分析view的结构和作用:

    var view = $('section.messages')
    

    view视图的作用是接受一个选择器,回文档中与指定选择器或选择器组匹配的第一个html元素。

    我们完全可以指定一个函数,把选择器传入函数内,函数就会返回对应的元素;这样每当有同样的需要返回选择器元素的功能时,我们便可以调用这个函数,而不用另外写view了。

    // 新建View.js,通过script标签中引入
    
    window.View = function(selector){
        return this.document.querySelector(selector)
    }
    

    massage.js中的view就可以写成:

    var view = View('section.message')
    

    我们基本已经摸到点头脑了,我们把相同的功能(接受选择器,返回一个元素对象)封装成了一个函数,一旦我们需要在另一个文件中创建另一个view,则调用这个函数即可。


    Model

    接下来我们尝试把所有的model也给封装一个函数。

    在此之前,先来明确一下model的职责:

    1.初始化数据库

    2.保存数据到数据库

    3.从数据库获取数据

    所以我们可以通过调用函数,传入参数,返回数据对象。

    //新建一个Model.js文件,通过script标签引入
    // Model.js
    window.Model = function(options){
    
        var resourceName = options.resourceName
    
        return {
            //初始化
            init:function(){
                var APP_ID = 'xxxxxxxxxxxxxxxxxxxxxxx'
                var APP_KEY = 'xxxxxxxxxxxxxxxxxxxxxxxxxx'
                AV.init({appId: APP_ID,appKey: APP_KEY})
            },
    
            // 获取数据
            fetch:function(){
                var query = new AV.Query(resourceName);
                return query.find() //Promise对象
            },
    
            // 通过传入需要保存的对象 保存数据
            save:function(object){
                var Message = AV.Object.extend('Message')
                var messages = new Message();
                return messages.save(object)
            }
        }
    }
    

    构建好Model函数之后,以后每次需要新的model,我们就调用Model,传入参数,done。

    //message.js
    var model = Model({resourceName:"Message"})
    

    之前差不多20多行的有效代码,现在1行就搞定了,而且别的js文件也需要model的话,直接一句话调用,都不带含糊的。


    Controller

    controller我觉得是MVC里面最难搞的部分,因为他设计到的交互逻辑最多,需要处理的事情也最多最杂:

    1.负责监听、更新view

    2.负责调用、处理model返回的数据

    3.anything else...

    先和其他文件对比一下,init()一定是要有的,因为controller需要处理的事情很多,所以他加入的参数必然会变多且都不一样,所以我们让Controller返回的是一个容易操作的对象:

    // 新建一个Controller.js,并通过script标签引入
    // Controller.js
    
    window.Controller = function(options){
        var object = {
            init:function(view,model){},
        }
        return object
    }
    

    由于不是每一个controller都需要(绑定事件的)的bindEvents()、(获取留言信息的)loadMessages()和(保存留言信息的)saveMessages()以及其他乱七八糟的函数,为了更方便地加载通过参数传进来的函数,所以我们可以使用遍历的方法,把参数遍历,然后加入到返回的object中:

    window.Controller = function(options){
        var object = {
            init:function(view,model){},
        }
        for(let key in options){
            if(options[key] !== "init"){
                object[key] = options[key]
            }
        }
        return object
    }
    

    我们的view和model是由传进来的参数决定的,一开始view和model是null;然后初始化model:

    // Controller.js
    
    window.Controller = function(options){
        var object = {
            view = null
            model = null
            init:function(view,model){
                this.view = view
                this.model = model
    
                // 初始化model
                this.model.init()
            },
        }
        for(let key in options){
            if(options[key] !== "init"){
                object[key] = options[key]
            }
        }
        return object
    }
    

    两个init():

    // Controller.js
    
    window.Controller = function(options){
    
        var init = options.init // 1
    
        var object = {
            view = null
            model = null
    
            // 2
            init:function(view,model){
                this.view = view
                this.model = model
    
                // 初始化model
                this.model.init()
                init.call(this,view,moel)   // 1
            },
        }
        for(let key in options){
            if(options[key] !== "init"){
                object[key] = options[key]
            }
        }
        return object
    }
    

    本来我也有点懵逼,为什么会有两个 init,这不重复了吗?

    非也,此 init 非彼 init,为了区分不同已在上面注释标记:

    init1 指的是我们通过调用函数 Controller 的 options 参数传进去的 init,指的是每一个 controller 所不同的部分。即前面所说的‘子’与‘子’之间不同的地方。

    init2 指的是调用函数 Controller 返回的对象 object 中的 init,是必须执行的 init。是每个‘子’一定会相同的地方。

    他们的关系就是,每当执行 controller.init(view,model) 这句代码的时候,init2 必须执行;如果参数中没有传入 init1,init2 执行;如果参数中传入了 init1,则在执行完 init2 后,执行 init1。

    接着执行 bindEvents():

    // Controller.js
    
    window.Controller = function(options){
    
        var init = options.init
    
        var object = {
            view:null,
            model:null,
            init:function(view,model){
                //this.view就是view:null,通过传入参数view后this.view就等于传入的view
                this.view = view
                this.model = model
                this.model.init()
                init.call(this,view,model)
                this.bindEvents.call(this)
                //this.bindEvents.call(this)
            },
        }
        for(let key in options){
            if(key !== "init"){
                object[key]=options[key]
            }
        }
        return object
    }
    

    我们的 Controller OOP 就完成了!

    我们通过传参的方式将需要的函数都打包成一个对象,传给 Controller,而 Controller 会返回一个新的对象,这个对象里面包括了 init 和其他定制的函数,这个 init 会帮我们做任何一个 controller 都会做的事,再通过我们传进去的 init 做任何一个 controller 想单独做的事,done。

    接着我们就改一下 messages.js 中的 controller :

    var controller = Controller({
            init:function(view,controller){
                this.myForm = document.querySelector('#postMessage')
                this.loadMessages()
            },
            loadMessages : function () {
                this.model.fetch().then((messages) => {
                    messages.forEach((item) => {
                        let span = $('<span></span>').text('发布于:'+ this.parseTime(item.createdAt))[0]
                        let li = $('<li></li>').text(item.attributes.name + ':' + item.attributes.content)
                        li.append(span)
                        $('#comments').append(li)
                    });
                }).then(function (messages) {
                }, function (error) {
                });
            },
            bindEvents : function(){
                this.myForm.addEventListener('submit',(e) => {
                    e.preventDefault()
                    this.saveMessages()
                })
            },
            saveMessages : function(){
                let content = $('input[name="content"]').val()
                let name = $('input[name="name"]').val()
                this.model.save({"name":name,"content":content}).then((object) => {
                    let span = $('<span></span>').text('发布于:'+ this.parseTime(object.createdAt))[0]
                    let li = $('<li></li>').text(object.attributes.name +':'+ object.attributes.content)
                    li.append(span)
                    $('#comments').append(li)
                    document.querySelector('#postMessage input[name="content').value = ''
                },function(error){
                    console.log(error)
                })
            },
            parseTime:function(d){
                const newDate = d.getFullYear() + '-' + (d.getMonth() + 1) + '-' + d.getDate() + ' '
                                + d.getHours() + ':' + d.getMinutes() + ':' + d.getSeconds();
                return newDate;
            }
        })
    controller.init(view,model)
    

    注意事项

    this的问题:

    1.message.js 中 controller.init(view,model)中的 this :controller 是调用函数 Controller() 返回的对象 object,所以 controller 的 this 是他本身,也就是 object 。

    这句代码也就等同于 controller.init.call(controller,view,model)

    2.Controller.js 中 init:function(view,model){...} 中的 init 的 this 就是 object 。

    这句代码也就等同于 object.init:function(view,model){...}

    3.Controller.js 中 init.call(this,view,model) 中的 this 是我们通过 controller.init.call(controller,view,model)传进去的 this ,也就是 object 。

    done。

    真是写得又臭又长...但是没办法,学习还是要总结归纳的,关于this的问题会另外总结一下。

  • 相关阅读:
    复杂业务
    重析业务逻辑架构模式
    Katana介绍以及使用
    使用ServiceStack构建Web服务
    ASP.NET vNext 在 Mac OS
    用户端的防腐层作用及设计
    Mvc 模块化开发
    编程语言
    页面生命周期
    If you pay peanuts,you get monkeys
  • 原文地址:https://www.cnblogs.com/No-harm/p/9682384.html
Copyright © 2020-2023  润新知