2015年10月26日星期一

理解JavaScript的作用域链 - 田小计划

本邮件内容由第三方提供,如果您不想继续收到该邮件,可 点此退订
理解JavaScript的作用域链 - 田小计划  阅读原文»

上一篇文章中介绍了Execution Context中的三个重要部分:VO/AO,scope chain和this,并详细的介绍了VO/AO在JavaScript代码执行中的表现。

本文就看看Execution Context中的scope chain。

作用域

开始介绍作用域链之前,先看看JavaScript中的作用域(scope)。在很多语言中(C++,C#,Java),作用域都是通过代码块(由{}包起来的代码)来决定的,但是,在JavaScript作用域是跟函数相关的,也可以说成是function-based。

例如,当for循环这个代码块结束后,依然可以访问变量"i"。

for(var i = 0; i < 3; i++){
console.log(i);
}

console.log(i);
//3

对于作用域,又可以分为全局作用域(Global scope)和局部作用域(Local scpoe)。

全局作用域中的对象可以在代码的任何地方访问,一般来说,下面情况的对象会在全局作用域中:

  • 最外层函数和在最外层函数外面定义的变量
  • 没有通过关键字"var"声明的变量
  • 浏览器中,window对象的属性

局部作用域又被称为函数作用域(Function scope),所有的变量和函数只能在作用域内部使用。

var foo = 1;
window.bar
= 2;

function baz(){
a
= 3;
var b = 4;
}
// Global scope: foo, bar, baz, a
//
Local scope: b

作用域链

通过前面一篇文章了解到,每一个Execution Context中都有一个VO,用来存放变量,函数和参数等信息。

在JavaScript代码运行中,所有用到的变量都需要去当前AO/VO中查找,当找不到的时候,就会继续查找上层Execution Context中的AO/VO。这样一级级向上查找的过程,就是所有Execution Context中的AO/VO组成了一个作用域链。

所以说,作用域链与一个执行上下文相关,是内部上下文所有变量对象(包括父变量对象)的列表,用于变量查询。

Scope = VO/AO + All Parent VO/AOs

看一个例子:

var x = 10;

function foo() {
var y = 20;

function bar() {
var z = 30;

console.log(x
+ y + z);
};

bar()
};

foo();

上面代码的输出结果为"60",函数bar可以直接访问"z",然后通过作用域链访问上层的"x"和"y"。

  • 绿色箭头指向VO/AO
  • 蓝色箭头指向scope chain(VO/AO + All Parent VO/AOs)

再看一个比较典型的例子:

var data = [];
for(var i = 0 ; i < 3; i++){
data
=function() {
console.log(i);
}
}

data[
0]();// 3
data[1]();// 3
data[2]();// 3

第一感觉(错觉)这段代码会输出"0,1,2"。但是根据前面的介绍,变量"i"是存放在"Global VO"中的变量,循环结束后"i"的值就被设置为3,所以代码最后的三次函数调用访问的是相同的"Global VO"中已经被更新的"i"。

结合作用域链看闭包

在JavaScript中,闭包跟作用域链有紧密的关系。相信大家对下面的闭包例子一定非常熟悉,代码中通过闭包实现了一个简单的计数器。

function counter() {
var x = 0;

return {
increase:
function increase() { return ++x; },
decrease:
function decrease() { return --x; }
};
}

var ctor = counter();

console.log(ctor.increase());
console.log(ctor.decrease());

下面我们就通过Execution Context和scope chain来看看在上面闭包代码执行中到底做了哪些事情。

1. 当代码进入Global Context后,会创建Global VO

  • 绿色箭头指向VO/AO
  • 蓝色箭头指向scope chain(VO/AO + All Parent VO/AOs)

2. 当代码执行到"var cter = counter();"语句的时候,进入counter Execution Context;根据上一篇文章的介绍,这里会创建counter AO,并设置counter Execution Context的scope chain

3. 当counter函数执行的最后,并退出的时候,Global VO中的ctor就会被设置;这里需要注意的是,虽然counter Execution Context退出了执行上下文栈,但是因为ctor中的成员仍然引用counter AO(因为counter AO是increase和decrease函数的parent scope),所以counter AO依然在Scope中。

4. 当执行"ctor.increase()"代码的时候,代码将进入ctor.increase Execution Context,并为该执行上下文创建VO/AO,scope chain和设置this;这时,ctor.increase AO将指向counter AO。

  • 绿色箭头指向VO/AO
  • 蓝色箭头指向scope chain(VO/AO + All Parent VO/AOs)
  • 红色箭头指向this
  • 黑色箭头指向parent VO/AO

相信看到这些,一定会对JavaScript闭包有了比较清晰的认识,也了解为什么counter Execution Context退出了执行上下文栈,但是counter AO没有销毁,可以继续访问。

二维作用域链查找

通过上面了解到,作用域链(scope chain)的主要作用就是用来进行变量查找。但是,在JavaScript中还有原型链(prototype chain)的概念。

由于作用域链和原型链的相互作用,这样就形成了一个二维的查找。

对于这个二维查找可以总结为:当代码需要查找一个属性(property)或者描述符(identifier)的时候,首先会通过作用域链(scope chain)来查找相关的对象;一旦对象被找到,就会根据对象的原型链(prototype chain)来查找属性(property)

下面通过一个例子来看看这个二维查找:

var foo = {}

function baz() {

Object.prototype.a
= 'Set foo.a from prototype';

return function inner() {
console.log(foo.a);
}

}

baz()();
// Set bar.a from prototype

对于这个例子,可以通过下图进行解释,代码首先通过作用域链(scope chain)查找"foo",最终在Global context中找到;然后因为"foo"中没有找到属性"a",将继续沿着原型链(prototype chain)查找属性"a"。

  • 蓝色箭头表示作用域链查找
  • 橘色箭头表示原型链查找

总结

本文介绍了JavaScript中的作用域以及作用域链,通过作用域链分析了闭包的执行过程,进一步认识了JavaScript的闭包。

同时,结合原型链,演示了JavaScript中的描述符和属性的查找。

下一篇我们就看看Execution Context中的this属性。


本文链接:理解JavaScript的作用域链,转载请注明。

优美的包裹――面向包和组件设计的架构模式原则 - 辰希小筑  阅读原文»

之前写了一篇关于面向类(对象)的几个设计模式原则,但随着应用程序规模和复杂度的增加,我们需要更高层次的包和组件来对其之间的依赖关系进行组织管理。

首先明确一下本文中谈到的包或组件的概念,因为通常软件开发中关于包这个术语都有各自的说法,所以在本文中需要明确说明,在此定义为一种能够被独立部署的二进制单元。而在.NET中,包通常是一个被称为程序集assembly的动态链接库DLL,也可以是子系统、库或组件。

大型系统的设计好坏依赖于是否有好的包或组件设计。那么我们应该使用什么设计原则来管理包之间的关系呢?

很幸运,Bob大叔已经为我们做好了规划:

1、重用-发布等价原则(Reuse-Release Equivalence Principle, REP)
2、共同重用原则(Common-Reuse Principle, CRP)
3、共同封闭原则(Common-Closure Principle, CCP)
4、无环依赖原则(Acyclic-Dependencies Principle, ADP)
5、稳定依赖原则(Stable-Dependencies Principle, SDP)
6、稳定抽象原则(Stable-Abstractions Principle, SAP)

上述6个设计原则,描述了包的内容和相互管理的关系

前三个原则(REP、CRP、CCP)用来指导如何把类划分到包中,属于包的内聚性设计要求(package cohesion),考虑的是粒度;
后三个原则(ADP、SDP、SAP)用于处理包之间的关系,属于包的耦合性设计要求(package coupling),考虑的是稳定性。

内聚性:粒度

包的内聚性原则是开发者决定如何把类划分到不同包中的指导,它是一种“自底向上”的思想。

内聚性不单单是指一个模块执行一项且仅单独一项功能,它还需要考虑到可重用性(reusability)和可开发性(developability),及其之间的相互作用力和需求之间的平衡关系。

REP

The granule of reuse is the granule of release.
重用的粒度就是发布的粒度。

当开发人员重用一个类库时,都希望有清楚的文档说明,稳定的代码功能,清晰的接口格式等。但优秀的开发人员有更高的期望:首先类库的作者能够保证在相对长的时间内持续维护这些代码,与其将来要自己要花时间去维护这些代码,我相信你更愿意自己去花时间设计更好的组件;其次是保证该组件的兼容性,谁都不愿意使用甚至忍受反复无常的变化,至少要保证一段时间内(一个月?一个季度?半年?)的支持,当然这方面可以通过行政上的手段获得的支持。

所以REP指出:一个组件的重用粒度(granule of reuse)可以和发布粒度(granule of release)一样大。我们所重用的任何包、组件、类库都必须同时被发布和跟踪。建立一个跟踪系统,为潜在的使用者提供所需要的变更通知、安全性和支持,让重用真正的成为可能。

如果一个包中的软件是用来重用的,那么就不能再包含任何不是为了重用目的而设计的软件。也就是说,一个包中的类要么都是可重用的,要么都不是可重用的。另外,一个包中所有类对于同一类用户来说都应该是可重用的。不要将为不同类型用户设计的类放入同一个包中。

CRP

The classes in a package are reused together. If you reuse one of the classes in a package, you reuse them all.
一个包中的所有类应该是共同重用的。如果重用了包中的一个类,那么就要重用包中所有类。

这个原则规定了:(1)、趋向于共同重用的类应该属于同一个包;(2)、相互之间没有紧密联系的类不应该在同一个包中

它不仅帮助我们决定哪些类需要且应该放进同一个包中,还告诉开发人员什么类不可以放在一起。如果类之间的关系是紧密耦合的,可重用的类需要与作为该可重用抽象的一部分的其他类协作,那么很明显这些类应该在同一个包中。如果一个包仅仅是使用了另外一个包中的一个类,然而事实上并无法削弱这两个包之间的依赖关系,所以每当依赖于一个包时,应该依赖于其中的每一个类。

确保包中的所有类是不可分开的,避免不必要的重新验证和重新部署。

CCP

The classes in a package should be closed together against the same kinds of changes. a change that affects a package affects all the classes in that package.
包中所有类对于同一种性质的变化应该是共同封闭的。一个变化若对一个封闭的包产生影响,则将对该包中的所有类产生影响,而对于其他包则不造成任何影响。

该原则规定:一个包不应该包含多个引起变化的原因PS:似曾相识啊

在实际的应用程序开发过程中,大部分软件对可维护性的要求往往要高于可重用性。可维护性的重要性更大。所以软件中涉及到必须更改代码的情况,开发人员肯定更希望是尽量在同一个包中进行修改。对于一些确定变化类型的开放类或可能由于同样原因而产生变更的所有类共同组织在同一个包中,进行策略性的封闭,将变化限制在最小数量的包中,有助于减少软件的重新发布。

耦合性:稳定

包的耦合性反应了不同包之间的依赖关系。其受影响的因素有很多,如可开发性、逻辑设计、技术路线和行政力量等。我们可以通过依赖性管理度量去测试和判断一个设计的依赖性与抽象结构模式间的匹配程度。

ADP

The dependency structure between packages must be a directed acyclic graph (DAG). That is, there must be no cycles in the dependency structure.
在包的依赖关系图中不允许存在环。

次晨综合症(morning-after syndrome):忙碌了一天,终于开发、测试、提交完成了某项功能,第二天回到公司却发现昨天那项完成的功能却不能用了,或是无法正常运行了,o(�幡洇�)o, 多么熟悉的场景啊~想必有过这样经历的人还不在少数,而且他们明确的知道,肯定是有人更改了该项功能所依赖的组件中的某些代码。

针对这个问题,目前主要形成了两个解决方案:每周构建和ADP。

顾名思义,每周构建的工作方式为一周的前4天所有开发人员互不干扰的各自独立开发,并在周五进行集成。它的好处在于前四天的高效无干扰工作,不利之处是每周五将要付出巨大的集成代价。更糟糕的是,随着项目的增长,集成的工作量会不断增加,造成团队的效率随之下降。所以,每周构建适合于中小型规模的项目开发。

ADP旨在消除依赖环和解除依赖环。

我们可以通过把开发环境划分成可发布的组件来解决消除依赖环的问题。需要注意的是,首先,组件的依赖关系中不能有环;其次,无论从哪个组件开始,都无法沿着依赖关系绕回到这个组件。

保证组件的依赖关系结构是一个有向无环图(DAG)

那么,我们如何解除组件之间的依赖环并把依赖关系图恢复为一个DAG呢?主要有两个方法:
(1)、使用依赖倒置原则(DIP)解除依赖环:开发者应该从客户、使用者的角度出发来命名接口。
(2)、使用新组件解除依赖环:容易导致依赖关系结构增长。

SDP

The dependencies between packages in a design should be in the direction of the stability of the packages. A package should only depend upon packages that are more stable that it is.
朝着稳定的方向进行依赖。

要使设计可维护,某种程度的可变性和易变性是必要的,而组件的稳定性更是至关重要的。

什么是稳定性?稳定性和更改所需要的工作量有关。且所需工作量越大,其稳定性越高。影响一个组件更改难易程度的因素有很多,比如软件规模、复杂性、清晰度等等。这里我们抛开上述这些因素不谈,关注以下简单、可行的方法:让其他许多软件组件依赖于它。

聪明的开发者早已洞穿一切:具有多输入依赖关系的组件是非常稳定的。

通过计算进、出该组件的依赖关系的数目,可以计算该组件的位置稳定性(position stability)。
□ (Ca)输入耦合度(afferent coupling):处于该组件的外部,并依赖于该组件内的类的数目。
□ (Ce)输出耦合度(efferent coupling):处于该组件的内部,并依赖于该组件外的类的数目。
□ (不稳定性II = Ce /(Ca+Ce)
这个度量I 的取值范围是[0,1]。
I=0表示该组件具有最大的稳定性(负有责任且无依赖性);
I=1表示该组件具有最大的不稳定性(不承担责任且有依赖性)。

该原则规定:一个组件的I 度量值应该大于它所依赖的组件的I 度量值,I 度量值应该顺着依赖的方向减少

事实上,组件稳定性是多样的,如果一个系统中所有的组件都是最大稳定的,那么意味着该系统不能改变,显然不符合实际生产环境。系统中组件的理想配置应该是可改变的组件位于顶部并依赖于底部稳定的组件。此外,应该把封装系统高层设计的软件放进稳定的组件中,并通过抽象保证其灵活性。

SAP

Packages that are maximally stable should be maximally abstract. Instable packages should be concrete. The abstraction of a package should be in proportion to its stability.
包的抽象程度应该与其稳定程度一致。

没有评论:

发表评论