首页 > 代码库 > web前端之MVC的JavaScript Web富应用开发一:MVC和类
web前端之MVC的JavaScript Web富应用开发一:MVC和类
web前端之MVC的JavaScript Web富应用开发一:MVC和类
开篇:
本书以 assert() 和 assertEqual() 函数来展示变量的值或者函数调用的结果。
assert() 是一种快捷表述方式, 用来表示一个特定的变量( revolves to true)。 这在自动化测试中是一种非常常见的模式。 assert() 可以接收两个参数 : 一个值和一个可选的消息。 如果运行结果不是真值, 这个函数将抛出一个异常 :
var assert = function(value, msg) {
if ( !value )
throw(msg || (value + " does not equal true"));
};
assertEqual() 是表示一个值等于另外一个值的另一种表述。 它和 assert() 类似, 但接收两个值。 如果这两个值不相等, 则这个断言失败 :
var assertEqual = function(val1, val2, msg) {
if (val1 !== val2)
throw(msg || (val1 + " does not equal " + val2));
};
本书不会教给你 JavaScript 是一门什么样的语言, 你可以阅读本人其它的博客进行学习, 但是本书将会向你展示如何搭建复杂的 JavaScript 应用, 教你创造不可思议的网络用户体验。
增加结构:
构建大型的 JavaScript 应用的秘诀是不要构建大型 JavaScript 应用。 相反, 你应当把你的应用解耦成一系列相互平等且独立的部分。 开发者常犯的错误是创建应用时使用了很多互相依赖的部分, 用了很多 JavaScript 文件, 并在 HTML 页面中用大量的 script 标签引入这些文件。 这类应用非常难于维护和扩展, 因此无论如何都应当避免这种情况的发生。
开始构建你的应用的时候, 花点精力来做应用的架构, 会为最终结果带来意想不到的改观。 不管你之前怎么看待 JavaScript, 从现在开始将它当做一门面向对象的编程语言来对待。
本书提倡使用 MVC 模式, 这是一种久经考验的搭建应用的方式, 可以确保应用的可维护性和可扩展性。 MVC 模式完全适用于 JavaScript 应用。
什么是 MVC:
MVC 是一种设计模式, 它将应用划分为 3 个部分 : 数据( 模型)、 展现层( 视图) 和用户交互层( 控制器)。 换句话说, 一个事件的发生是这样的过程 :
1. 用户和应用产生交互。
2. 控制器的事件处理器被触发。
3. 控制器从模型中请求数据, 并将其交给视图。
4. 视图将数据呈现给用户。
我们可以不用类库或框架就实现这种 MVC 架构模式。 关键是要将 MVC 的每部分按照职责进行划分, 将代码清晰地分割为若干部分, 并保持良好的解耦。 这样可以对每个部分进行独立开发、 测试和维护。
模型:
模型用来存放应用的所有数据对象。 比如, 可能有一个 User 模型, 用以存放用户列表、他们的属性及所有与模型有关的逻辑。
模型不必知晓视图和控制器的细节, 模型只需包含数据及直接和这些数据相关的逻辑。任何事件处理代码、 视图模板, 以及那些和模型无关的逻辑都应当隔离在模型之外。 将模型和视图的代码混在一起, 是违反 MVC 架构原则的。 模型是最应该从你的应用中解耦出来的部分。
当控制器从服务器抓取数据或创建新的记录时, 它就将数据包装成模型实例。 也就是说,我们的数据是面向对象的( object oriented), 任何定义在这个数据模型上的函数或逻辑都可以直接被调用。
因此, 不要这样做 :
var user = users[“foo”];
destroyUser(user);
而要这样做 :
var user = User.find(“foo”);
user.destroy();
视图:
视图层是呈现给用户的, 用户与之产生交互。 在JavaScript应用中, 视图大都是由HTML、CSS和JavaScript模板组成的。 除了模板中简单的条件语句之外, 视图不应当包含任何其他逻辑。
实际上, 和模型类似, 视图也应当从应用的其他部分中解耦出来。 视图不必知晓模型和控制器中的细节, 它们是相互独立的。 将逻辑混入视图之中是编程的大忌。
这并不是说 MVC 不允许包含视觉呈现相关的逻辑, 只要这部分逻辑没有定义在视图之内即可。 我们将视觉呈现逻辑归类为“ 视图助手”( helper) : 和视图有关的独立的小型工具函数。
来看下面的例子, 视图中包含了逻辑, 这是一个反例, 平时不应当这样做 :
// template.html
<div>
<script>
function formatDate(date) {
/* ... */
};
</script>
${ formatDate(this.date) }
</div>
可以将视觉呈现逻辑剥离出来放入视图助手中, 正如下面的代码就避免了这个问题, 可以让这个应用的结构满足 MVC。
// helper.js
var helper = {};
helper.formatDate = function(){ /* ... */ };
// template.html
<div>
${ helper.formatDate(this.date) }
</div>
所有视觉呈现逻辑都包含在 helper 变量中, 这是一个命名空间, 可以防止冲突并保持代码清晰、 可扩展。
控制器:
控制器是模型和视图之间的纽带。 控制器从视图获得事件和输入, 对它们进行处理( 很可能包含模型), 并相应地更新视图。 当页面加载时, 控制器会给视图添加事件监听, 比如监听表单提交或按钮点击。 然后, 当用户和你的应用产生交互时, 控制器中的事件触发器就开始工作了。
不用使用类库和框架也能实现控制器, 下面这个例子就是使用简单的 jQuery 代码来实现的 :
var Controller={};
//使用匿名函数封装一个作用域
(Controller.users=function($){
var nameClick=function(){
/*....*/
};
//在页面加载是绑定事件监听
$(function(){
$("#view .name").click(nameClick);
});
})(jQuery);
我们创建了 users 控制器, 这个控制器是放在 Controller 变量下的命名空间。 然后, 我们使用了一个匿名函数封装了一个作用域, 以避免对全局作用域造成污染。 当页面加载时, 程序给视图元素绑定了点击事件的监听。
向模块化进军, 创建类:
JavaScript 是基于原型的编程语言, 并没有包含内置类的实现。 但通过 JavaScript 可以轻易地模拟出经典的类。
JavaScript 中并没有真正的类, 但 JavaScript 中有构造函数和 new 运算符。 构造函数用来给实例对象初始化属性和值。 任何 JavaScript 函数都可以用做构造函数, 构造函数必须使用 new 运算符作为前缀来创建新的实例。
new 运算符改变了函数的执行上下文, 同时改变了 return 语句的行为。 实际上, 使用 new和构造函数和传统的实现了类的语言中的使用方法是很类似的 :
var assert = function(value, msg) {
if ( !value )
throw(msg || (value + " does not equal true"));
};
var assertEqual = function(val1, val2, msg) {
if (val1 !== val2)
throw(msg || (val1 + " does not equal " + val2));
};
var Person=function(name){
this.name=name;
};
//实例化一个Person
var alice=new Person("alice");
//检查这个实例
assert(alice instanceof Person);
构造函数的命名通常使用驼峰命名法, 首字母大写, 以此和普通的函数区分开来, 这是一种习惯用法。
当使用 new 关键字来调用构造函数时, 执行上下文从全局对象( window) 变成一个空的上下文, 这个上下文代表了新生成的实例。 因此, this 关键字指向当前创建的实例。
默认情况下, 如果你的构造函数中没有返回任何内容, 就会返回 this——当前的上下文。要不然就返回任意非原始类型的值。 比如, 我们可以返回一个用以新建一个新类的函数,第一步要做的是创建自己的类模拟库 :
var Class=function(){
var klass=function(){
this.init.apply(this,arguments);
};
klass.prototype.init=function(){};
return klass;
};
var Person =new Class;
Person.prototype.init=function(){
//基于Person的实例化
};
//用法:
var person =new Person;
给类添加函数:
在 JavaScript 中, 在构造函数中给类添加函数和给对象添加属性是一模一样的 :
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<title></title>
<script type="text/javascript">
var Class=function(){
var klass=function(){
this.init.apply(this,arguments);
};
klass.prototype.init=function(){};
return klass;
};
var Person =new Class;
Person.prototype.init=function(){
//基于Person的实例化
};
Person.find=function(id){
alert(id);
};
//用法:
var person =new Person.find(1);
</script>
</head>
<body>
</body>
</html>
要想给构造函数添加实例函数, 则需要用到构造函数的 prototype :
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<title></title>
<script type="text/javascript">
var Class=function(){
var klass=function(){
this.init.apply(this,arguments);
};
klass.prototype.init=function(){};
return klass;
};
var Person =new Class;
Person.prototype.init=function(){
//基于Person的实例化
};
Person.find=function(id){
alert(id);
};
Person.prototype.breath = function(){
alert("1");
};
//用法:
var person =new Person;
person.breath();
</script>
</head>
<body>
</body>
</html>
给“ 类” 库添加方法:
现在,我们的“ 类” 库包含了生成一个实例并初始化这个实例的功能,给类添加属性和给构造函数添加属性是一样的。
上面给类添加函数, 很难一眼就分辨出类的静态属性和实例的属性。 因此我们采用另外一种不同的方法来给类添加属性, 这里用到了两个函数extend() 和 include() :
var Class=function(){
var klass=function(){
this.init.apply(this,arguments);
};
klass.prototype.init=function(){};
//定义prototype的别名
klass.fn=klass.prototype;
//定义类的别名
klass.fn.parent=klass;
//给类添加属性
klass.extend=function(obj){
var extended=obj.extended;
for(var i in obj){
klass[i]=obj[i];
}
if(extended){
extended(klass);
}
};
//给实例添加属性
klass.include=function(obj){
var included=obj.included;
for(var i in obj){
klass.fn[i]=obj[i];
}
if(included){
included(klass);
}
};
return klass;
};
这段代码是“ 类” 库的增强版, 我们使用 extend() 函数来生成一个类, 这个函数的参数是一个对象。 通过迭代将对象的属性直接复制到类上 :
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<title>extend() 和 include()</title>
<script type="text/javascript">
var Class=function(){
var klass=function(){
this.init.apply(this,arguments);
};
klass.prototype.init=function(){};
//定义prototype的别名
klass.fn=klass.prototype;
//定义类的别名
klass.fn.parent=klass;
//给类添加属性
klass.extend=function(obj){
var extended=obj.extended;
for(var i in obj){
klass[i]=obj[i];
}
if(extended){
extended(klass);
}
};
//给实例添加属性
klass.include=function(obj){
var included=obj.included;
for(var i in obj){
klass.fn[i]=obj[i];
}
if(included){
included(klass);
}
};
return klass;
};
var Person =new Class;
Person.extend({
find:function(id){
alert(id);
}
,exists:function(){
alert("FZW");
}
});
var person=Person.find(1);
</script>
</head>
<body>
</body>
</html>
include() 函数的工作原理也是一样的, 只不过不是将属性复制至类中, 而是复制至类的原型中。 换句话说, 这里的属性是类实例的属性, 而不是类的静态属性。
***
var Person =new Class;
Person.include({
find:function(id){
alert(id);
}
,exists:function(){
alert("FZW");
}
});
var person=new Person;
person.find(1);
***
同样地, 这里的实现支持 extended 和 included 回调。 将属性传入对象后就会触发这两个回调 :
Person.extend({
extended: function(klass) {
console.log(klass, " was extended!");
}
});
基于原型的类继承:
prototype 属性:
JavaScript 是基于原型的编程语言, 原型用来区别类和实例, 这里提到一个概念 : 原型对象。 原型是一个“ 模板” 对象, 它上面的属性被用做初始化一个新对象。 任何对象都可以作为另一个对象的原型对象, 以此来共享属性。 实际上, 可以将其理解为某种形式的继承。
当你读取一个对象的属性时, JavaScript 首先会在本地对象中查找这个属性, 如果没有找到, JavaScript 开始在对象的原型中查找, 若还未找到还会继续查找原型的原型, 直到查找到 Object.prototype。 如果找到这个属性, 则返回这个值, 否则返回 undefined。换句话说, 如果你给 Array.prototype 添加了属性, 那么所有的 JavaScript 数组都具有了这些属性。
为了让子类继承父类的属性, 首先需要定义一个构造函数。 然后, 你需要将父类的新实例赋值给构造函数的原型:
var Animal=function(){};
Animal.prototype.breath=function(){
console.log("breath");
};
var Dog=function(){};
//Dog继承Animal
Dog.prototype=new Animal;
Dog.prototype.wag=function(){
console.log("wag tail");
};
var dog=new Dog;
dog.wag();
dog.breath();
给“ 类” 库添加继承:
var Class=function(parent){
var klass=function(){
this.init.apply(this,arguments);
};
//改变klass的原型
if(parent){
var subclass=function(){};
subclass.prototype=parent.prototype;
klass.prototype=new subclass;
};
klass.prototype.init=function(){};
//定义别名
klass.fn=klass.prototype;
klass.fn.parent=klass;
klass._super=klass.__proto__;
/*include/extend相关代码*/
return klass;
};
如果将 parent 传入 Class 构造函数, 那么所有的子类则必然共享同一个原型。 这种创建临时匿名函数的小技巧避免了在继承类的时候创建实例, 这里暗示了只有实例的属性才会被继承, 而非类的属性。 设置对象的 proto ; 属性并不是所有浏览器都支持。
现在, 我们可以通过给 Class 传入父类来实现简单的继承 :
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<title>给“ 类” 库添加继承</title>
<script type="text/javascript">
var Class=function(parent){
var klass=function(){
this.init.apply(this,arguments);
};
//改变klass的原型
if(parent){
var subclass=function(){};
subclass.prototype=parent.prototype;
klass.prototype=new subclass;
};
klass.prototype.init=function(){};
//定义别名
klass.fn=klass.prototype;
klass.fn.parent=klass;
klass._super=klass.__proto__;
klass.extend=function(obj){
var extended=obj.extended;
for(var i in obj){
klass[i]=obj[i];
}
if(extended){
extended(klass);
}
};
//给实例添加属性
klass.include=function(obj){
var included=obj.included;
for(var i in obj){
klass.fn[i]=obj[i];
}
if(included){
included(klass);
}
};
return klass;
};
var Animal=new Class;
Animal.include({
breath:function(){
console.log("breath");
}
});
var Cat=new Class(Animal);
//用法
var tommy=new Cat;
tommy.breath();
</script>
</head>
<body>
</body>
</html>
函数调用:
在 JavaScript 中, 函数和其他东西一样都是对象。 然而, 和其他对象不同的是, 函数是可调用的。 函数内上下文, 如 this 的取值, 取决调用它的位置和方法。除了使用方括号可以调用函数之外, 还有其他两种方法可以调用函数 : apply() 和call()。 两者的区别在于传入函数的参数的形式。
apply() 函数有两个参数 : 第 1 个参数是上下文, 第 2 个参数是参数组成的数组。 如果上下文是 null, 则使用全局对象代替。 例如 :
function.apply(this, [1, 2, 3])
call() 函数的行为和 apply() 函数的并无不同, 只是使用方法不一样。 call() 的第 1 个参数是上下文, 后续是实际传入的参数序列。 换句话说, 这里使用多参数——而不是类似 apply()的数组——来将参数传入函数。
function.call(this, 1, 2, 3);
jQuery 在其 API 的实现中就利用了 apply() 和 call() 来更改上下文, 比如在事件处理程序中或者使用 each() 来做迭代时。 起初这很让人费解, 一旦你理解了就会发现它非常有用 :
$(‘.clicky‘).click(function(){
//this指向当前节点
$(this).hide();
});
$(‘p‘).each(function(){
//this指向本次迭代
$(this).remove();
});
为了访问原始上下文,可以将 this的值存入一个局部变量中,这是一种常见的模式,比如:
var clicky={
wasClicked:function(){
/*...*/
},
addListeners:function(){
var self=this;
$(‘.clicky‘).click(function(){
self.wasClicked();
});
}
};
clicky.addListeners();
然而, 我们可以使用 apply 来将这段代码变得更干净一些, 通过将回调包装在另外一个匿名函数中, 来保持原始的上下文 :
var proxy=function(func,thisObject){
return(function(){
return func.apply(thisObject,arguments);
});
};
var clicky={
wasClicked:function(){
/*...*/
},
addListeners:function(){
var self=this;
$(‘.clicky‘).click(proxy(this.wasClicked,this));
}
};
因此在上面的例子中, 我们在点击事件的回调中指定了要使用的上下文 ; jQuery 中调用这个函数所用的上下文就可以忽略了。 实际上, jQuery 也包含了实现了这个功能的 API,你或许已经猜到了 jQuery.proxy() :
$(‘.clicky’).click($.proxy(function(){ /* … */ }, this));
控制“ 类” 库的作用域:
var Class=function(parent){
var klass=function(){
this.init.apply(this,arguments);
};
klass.prototype.init=function(){};
//定义别名
klass.fn=klass.prototype;
// 添加一个 proxy 函数
klass.proxy = function(func){
var self = this;
return(function(){
return func.apply(self, arguments);
});
}
klass.fn.proxy = klass.proxy;
klass.extend=function(obj){
var extended=obj.extended;
for(var i in obj){
klass[i]=obj[i];
}
if(extended){
extended(klass);
}
};
//给实例添加属性
klass.include=function(obj){
var included=obj.included;
for(var i in obj){
klass.fn[i]=obj[i];
}
if(included){
included(klass);
}
};
return klass;
};
var Button=new Class;
Button.include({
init:function(element){
this.element=jQuery(element);
//代理了这个click函数
this.element.click(this.proxy(this.click));
},
click:function(){
/***/
}
});
如果我们没有使用 proxy 将 click() 的回调包装起来, 它就会基于上下文 this.element来调用, 而不是 Button, 这会造成各种问题。 在新版本的 JavaScript——ECMAScript 5( ES5) ——中同样加入了 bind() 函数用以控制调用的作用域。 bind() 是基于函数进行调用的, 用来确保函数是在指定的 this 值所在的上下文中调用的。 例如:
Button.include({
init: function(element){
this.element = jQuery(element);
// 绑定这个 click 函数
this.element.click(this.click.bind(this));
},
click: function(){ /* ... */ }
});
这个例子和我们的 proxy() 函数是等价的, 它能确保 click() 函数基于正确的上下文进行调用。 但老版本的浏览器不支持 bind()
if (!Function.prototype.bind) {
Function.prototype.bind = function (obj) {
var slice = [].slice,
args = slice.call(arguments, 1),
self = this,
nop = function () {},
bound = function () {
return self.apply( this instanceof nop ? this : (obj || {}),
args.concat(slice.call(arguments)));
};
nop.prototype = self.prototype;
bound.prototype = new nop();
return bound;
};
}
如果浏览器原生不支持 bind(), 我们仅仅重写了 Function 的原型。 现代浏览器则可以继续使用内置的实现。 对于数组来说这种“ 打补丁” 式的做法非常有用,因为在新版本的 JavaScript 中, 数组增加了很多新的特性。
添加私有函数:
迄今为止, 我们为“ 类” 库添加的属性都是“ 公开的”, 可以被随时修改。 现在我们来探究一下如何给“ 类” 添加私有属性。
var Person=function(){};
(function(){
var findById=function(){/*...*/};
Person.find=function(id){
if(typeof id=="integer"){
return findById(id);
}
};
})();
我们将类的属性都包装进一个匿名函数中, 然后创建了局部变量( findById), 这些局部变量只能在当前作用域中被访问到。 Person 变量是在全局作用域中定义的, 因此可以在任何地方都能访问到。
定义变量的时候不要丢掉 var 运算符, 因为如果丢掉 var 就会创建全局变量。 如果你需要定义全局变量, 在全局作用域中定义它或者定义为 window 的属性:
(function(){
***
})(window);
web前端之MVC的JavaScript Web富应用开发一:MVC和类