2013年9月18日星期三

我是怎样理解闭包的 - 小方。

本邮件内容由第三方提供,如果您不想继续收到该邮件,可 点此退订
我是怎样理解闭包的 - 小方。  阅读原文»

渐入闭包

假设有这个需求,写个函数,动态生成HTML,每次生成的 HTML 有一部分的是固定不变的。

于是
function buildHtml() {
var template = ['<table><tr><td>',
'',
'</td><td>',
'',
'<td></td></tr></table>'];
template[
1] = args[0];
template[
3] = args[1];
return template.join('');
}

分析下,每次执行函数时,都会重复定义个template,执行完函数后,这个变量就被销毁;这样重复定义销毁,肯定对性能不好,考虑把template这个变量内容提取到外面,

比如改造后成了这样
这时候,你又有个需求,要把这个函数移到其它页面,移动的时候,你必须保证两件事,一个是template和函数名不能和已有命名空间的变量重复,并且这样的结构显然不方面移动,于是乎你考虑这样实现
$(function () {
//...
var template = ['<table><tr><td>',
'',
'</td><td>',
'',
'<td></td></tr></table>'];
function buildHtml(args) {
template[
1] = args[0];
template[
3] = args[1];
return template.join('');

}

//...这里可以调用上面的代码
})

这样移动代码时,貌似就不要考虑两个变量名重复,貌似也解决了一部分问题。可是还有一种更好的方案,比如

var buildHtml = (function () {
var template = ['<table><tr><td>',
'',
'</td><td>',
'',
'<td></td></tr></table>'];
return function (args) {
template[
1] = args[0];
template[
3] = args[1];
return template.join('');

}

})();

这样结构上更紧凑,移动这段代码更方便,template也没暴露出来,这里就用到了闭包。如果对(function(){...})()用法不熟悉,最好先google下。实在一时理解不了,我这里还将上面例子变通下。
function xx() {
var template = ['<table><tr><td>',
'',
'</td><td>',
'',
'<td></td></tr></table>'];
return function (args) {
template[
1] = args[0];
template[
3] = args[1];
return template.join('');

}
};
var buildHtml = xx();

然后就可以这样调用了buildHtml(/*参数*/);

闭包就像提供了接口或者公用方法,来访问和修改私有属性。这样的结构,还常见于定义一个对象的私有属性和私有方法。
比如
var win = (function () {
var fn1 = function () {}, //私有方法
pro = 1; //私有属性
return {
outFns1 :
function () {
//调用fn1或pro
},
outFns2 :
function () {
//调用fn1或pro

}
}
})();

上面的例子,貌似都用到return,不用return呢?实际上面第二个例子,稍微改造下,也是闭包

$(function () {
//...
var template = ['<table><tr><td>',
'',
'</td><td>',
'',
'<td></td></tr></table>'];
buildHtml
= function (args) {
template[
1] = args[0];
template[
3] = args[1];
return template.join('');

}

//...这里可以调用上面的代码
})

buildHtml因为没有var声明,所以是全局变量,这样在$的回调函数外面,也可以通过buildHtml访问template。

其它形式,诸如将内部函数赋给对象的属性也可以,此变量可以是全局的。
闭包概念
至此,来总结下什么是闭包?
在一个函数(这里称为父函数)内部定义一个函数(内部函数通过作用域链访问父函数的局部变量),结束时父函数返回内部函数的引用,或者在定义的时候将内部函数引用暴露出来。执行父函数时,我们取得了内部函数的引用,执行完父函数后,父函数相关变量所占据的内存仍保留着,仍可以通过闭包访问和修改,也就是说父函数的当前活动对象被维持了(这里涉及作用域链知识,下面有介绍),直到引用消失。
闭包作用看起来,就像提供了活的键值对,然后你在内部函数里面可以访问。闭包是定义时孕育的,在执行时形成。
举这个例子,只是加深对闭包的理解。为什么是“活”的。
function wrapFns() {
var arr = [];
for (var i = 10; i--; ) {
arr
= function () {
return i;
}
}
return arr;
}
var fns = wrapFns();
console.log(fns[
10]()); // 值是多少?

值为0并不是10。当然这里你要它为10,也有方法,这里就不说了。
闭包的应用
1.正如开始所举的例子,闭包用来封装插件
你在网络上download一个js插件,基本上所有插件都用到下面类似结构来实现封装?div style="border-bottom:1px solid #aaa;margin-bottom:25px">翻译:使用 ASP.NET MVC 4, EF, Knockoutjs and Bootstrap 设计和开发站点 - 6 - 业务逻辑 - 冠军  阅读原文»

Part 3: 设计逻辑层:核心开发

如前所述,我们的解决方案如下所示:
34.png

下面我们讨论整个应用的结构,根据应用中不同组件的逻辑相关性,分离到不同的层中,层与层之间的通讯通过或者不通过限制。分层属于架构风格,在应用的长时间生命周期中,解决维护和扩展问题。
所以,让我们在解决方案中添加一个类库项目,命名为 Application.Common.

Application.Common :

这是一个类库项目, 提供公共功能,可以被不同的业务逻辑层使用。例如:安全,日志,跟踪,验证等等. 定义在这个层中的组件,不仅可以被不同的层使用,还可以在不同的应用中使用。为了未来容易使用,我们使用依赖注入和抽象,在应用中实现最小化的修改。

例如,在我们马上用到的,验证组件用来验证数据,定制的日志器来记录错误或者警告。

在添加了 Common 类库之后,解决方案的文件夹如下所示:
35.png


Application.Core:

这个层实现系统的核心处理逻辑,封装相关的所有业务逻辑。从基本上说,这个层通常实现领域处理的逻辑。这个层还经常通过核心层的工作单元,以便完成 PI 特性,主要的目标是明确区分和分离核心领域的逻辑与基础架构的具体细节,例如,数据访问和数据仓储的具体技术,像 ORM ,或者简单的数据访问库,或者面向方面的架构等等。通过分离系统的核心功能,我们就可以进一步增强系统的可维护性,甚至可以替换底层的组件,而很少影响到整个应用。

36.png
下一步,我们将在解决方案中添加名为 Application.DAL 的类库。

Application.DAL:

DAL 的职责是提供数据访问和数据的持久化处理;维护多个会话,连接到不同的数据库等等。这里的主要目标是通过接口和约定包装 EF 数据访问上下文对象,使得核心层不会直接依赖 EF。数据持久化组件提供驻留在系统内的数据访问,也提供系统外的数据访问。比如对外部系统提供服务的 Web 服务。因此,这里既包含类似 仓储模式的机制来支持对系统内的数据访问,还提供服务代理来使用其它外部系统通过 Web 服务提供的数据,另外,层中还提供可以对所有仓储使用的基类和组件。


37.png
下一步,我们将会在解决方案中添加一个名为 Application.Repository 的类库。

Application.Repository:

这个类库仅仅可以通过 Application.Manager 访问,对于领域中的每一个根实体对象,我们需要创建一个仓储,基本上,所谓的仓储就是封装了访问应用数据的处理逻辑的类和组件。而且,它们处理数据处理的核心功能,使得整个应用有更好的可维护性,并且可以在 Manager 和 Core 之间进行解耦。


38.png
下一步,我们需要创建名为 Application.DTO 类库。

Application.DTO:

还是一个类库,包含了与实体不同的数据类,其中仅有表示数据的属性,但是没有处理数据的方法,用来在表示层 Applicaiton.Web 和服务层 Application.Manager 之间进行通讯。数据传输对象是用来封装数据的对象,用来从系统的一个子系统将数据传递到另外一个子系统。这里我们使用 DTO 对象在 Manager 层和 UI 层之间传递数据。使用 DTO 的主要优点是可以在分布式的系统中减少数据的流量,在 MVC 模式中,也是重要的一个部分。我们也可以为方法的调用来封装数据,在方法包含4,5个以上的参数的时候,经常使用 DTO 来传递参数。


39.png
下一步,我们创建 Application.Manager 类库。

Application.Manager :

这个类库仅仅被 Applicaiton.Web 访问,对于我们在 Manager 中定义的每个模块,Manager 的主要职责是接受来自 UI 的请求,将数据传递到仓储中的领域对象中进行处理,将处理结果返回到界面层,这个层是 UI 层和仓储层之间的中间层。

40.png

Application.Web:

在前面的文章中,我们已经使用 javascript 中的模拟数据实现过该层。这里并不仅仅依赖于 ASP.NET MVC,界面可以包含用户的界面组件,像 HTML,.aspx, cshtml,MVC 等等,它也可以是是任何的 Windows 应用。这里通过方法与 Manager 层通讯,封装返回的结果,选择将错误信息显示在页面1 或者页面2 中。这个层使用 javascript 来家在表示层的模型,但实际的数据处理通过 ajax 请求发送到服务器处理,所以,服务器负责处理业务立即。而表示层处理表示逻辑问题。

41.png
要理解各层之间通讯的最好方式,就是让我们重温一下初始的需求。

Screen 1: 联系人列表 - 显示所有联系人

1.1 界面需要显示数据库中所有的联系人信息.
1.2 用户可以删除联系人.
1.3 用户可以编辑联系人信息.
1.4 用户可以创建新的联系人.
42.png
为了填充表格中的数据,在页面加载的时候,我们调用 ContactController 的 GetAllProfiles() 方法,这个方法返回数据库中所有的联系人信息,然后以 JSON 的形式返回到页面,我们将数据以 self.Profiles 的形式绑定到一个 javascript 对象,下面就是 contact.js 代码中 GetAllProfiles() 方法的定义。

var ProfilesViewModel = function () {
var self = this;
var url = "/contact/GetAllProfiles";
var refresh = function () {
$.getJSON(url, {}, function (data) {
self.Profiles(data);
});
};

在点击删除按钮的时候,我们调用 ContactController 的 GetAllProfiles() 方法,这个方法从数据库中删除联系人信息。下面是 contact.js 中 DeleteProfile() 方法的定义。

self.removeProfile = function (profile) {
if (confirm("Are you sure you want to delete this profile?")) {
var id = profile.ProfileId;
waitingDialog({});
$.ajax({
type: 'DELETE', url: 'Contact/DeleteProfile/' + id,
success: function () { self.Profiles.remove(profile); },
error: function (err) {
var error = JSON.parse(err.responseText);
$("<div></div>").html(error.Message).dialog({ modal: true,
title: "Error", buttons: { "Ok":
function () { $(this).dialog("close"); } } }).show();
},
complete: function () { closeWaitingDialog(); }
});
}
};

对于创建和编辑按钮来说,我们仅仅重定向到 CreateEdit 页面,如果 id 参数是 0 表示创建新联系人,对于编辑来说,id 就是编辑的联系人编号了。下面的代码就是 contact.js 中 的 createProfile 和 editProfile 方法

self.createProfile = function () {
window.location.href = '/Contact/CreateEdit/0';
};

self.editProfile = function (profile) {
window.location.href = '/Contact/CreateEdit/' + profile.ProfileId;
};

下面的图展示了主要的三个层之间的关系。

43.png

Screen 2: 创建新联系人

界面提供一个空白的联系人界面,并且提供一下功能.

2.1 用户可以输入用户的姓,名,邮件地址
2.2 通过点击添加号码按钮,允许添加任何个电话号码
2.3 用户可以删除任意电话号码.
2.4 通过点击添加地址按钮,允许添加任意多个地址.
2.5 用户可以删除任意的地址.
2.6 点击保存按钮,可以保存用户输入的所有信息到数据库中,然后回到联系人列表页面.
2.7 点击返回按钮,可以回到联系人列表.
44.png

Screen 3: 更新联系人信息

这个界面显示联系人的详细信息.

3.1 用户可以编辑联系人的姓,名,邮件地址.
3.2 用户可以通过点击添加,删除号码按钮来添加,删除用户的电话号码.
3.3 用户可以通过点击添加,删除地址按钮来添加,删除用户的地址。
3.4 点击保存可以将用户的详细信息更新到数据库中,然后返回联系人列表
3.5 点击返回按钮可以回到联系人列表
45.png
如前面的实现所见,创建和编辑的需求使用的是同一个页面 CreateEdit.cshtml,通过 profileId 来进行区分,如果 profileId 是 0,表示新建,否则,就是编辑存在的信息,下面是实现的细节。

在任何情况下,在页面加载和初始化 PhoneType 和 AddressType 的时候,在 ContactController 控制器的 InitializePageData() 方法中,在 CreateEdit.js 中的下列代码初始化数组。

var AddressTypeData;
var PhoneTypeData;

$.ajax({
url: urlContact + '/InitializePageData',
async: false,
dataType: 'json',
success: function (json) {
AddressTypeData = json.lstAddressTypeDTO;
PhoneTypeData = json.lstPhoneTypeDTO;
}
});

然后,对于编辑联系人信息来说,我们需要获取详细信息,通过 ContactController 中的GetProfileById() 方法实现,我们修改 CreateEdit.js 。

$.ajax({
url: urlContact + '/GetProfileById/' + profileId,
async: false,
dataType: 'json',
success: function (json) {
self.profile = ko.observable(new Profile(json));
self.phoneNumbers = ko.observableArray(ko.utils.arrayMap(json.PhoneDTO, function (phone) {
return phone;
}));
self.addresses = ko.observableArray(ko.utils.arrayMap(json.AddressDTO, function (address) {
return address;
}));
}
});

最后,我们使用两个方法保存数据,如果我们是创建新的联系人,调用 ContactController 的 SaveProfileInformtion() 方法,否则调用 UpdateProfileInformation() 方法,我们将 CreateEd

阅读更多内容

没有评论:

发表评论