[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这样的文件在项目中有很多,比如a.js负责用户登录,b.js负责topbar样式切换,c.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的问题会另外总结一下。