- 在JavaScript中,定义变量时没必要声明其类型。但这并不意味着变量没有类型。一个变量能够属于几种类型之一,这取决于其包含的数据。JavaScript中有三种原始类型:布尔型、数值型和字符串类型(不区分整数和浮点数是JavaScript与大多数其余主流语言的一个不一样之处)。此外,还有对象类型和包含可执行代码的函数类型,前者是一种复合数据类型(数组是一种特殊的对象,它包含着一批值的有序集合)。最后,还有空类型(null)和未定义类型(undefined)这两种数据类型。原始数据类型按值传送,而其余数据类型则按引用传送。
- 与其余弱类型语言同样,JavaScript中的变量能够根据所赋的值改变类型。原始类型之间也能够进行类型转换。toString能够把数值或布尔值转为字符串。parseFloat和parseInt函数能够把字符串转变为数值。双重“非”能够把字符串或数值转变为布尔值:
var bool = !!num;
匿名函数最有趣的用途是用来建立闭包。闭包是一个受到保护的变量空间,由内嵌函数生成。JavaScript具备函数级的做用域。这意味着定义在函数内部的变量在函数外部不能被访问。JavaScript的做用域又是词法性质的。这意味着函数运行在定义它的做用域中,而不是在调用它的做用域中。把这两个因素结合起来,就能经过把变量包裹在匿名函数中而对其加以保护。
下面列出的设计模式,尤为依赖接口:程序员
- 工厂模式。对象工厂所建立的具体对象会因具体状况而异。使用接口能够确保所建立出来的这些对象能够互换使用。也就是说,对象工厂能够保证其生产出来的对象都实现了必需的方法。
- 组合模式。若是不用接口你就不可能用这个模式。组合模式的中心思想在于能够将对象群体与其组成对象同等对待。这是经过让它们实现一样的接口来作到的。若是不进行某种形式的鸭式辨型或类型检查,组合模式就会失去大部分做用。
- 装饰者模式。装饰者经过透明地为另外一对象提供包装而发挥做用。这是经过实现与另外那个对象彻底相同的接口而作到的。对于外界而言,一个装饰者和它所包装的对象看不出有什么区别。
- 命令模式。代码中全部的命令对象都要实现同一批方法。经过使用接口,你为执行这些命令对象而建立的类能够没必要知道这些对象具体是什么,只要知道它们都实现了正确的接口便可。
在一些方法和属性的名称前面加下划线以示其私用性。下划线的这种用法是一个众所周知的命名规范,它代表一个属性(或方法)仅供对象内部使用,直接访问它或设置它可能会致使意想不到的后果。这有助于防止程序员对它的无心使用,却不能防止对它的有意使用。后一个目标的实现须要有真正私用性的方法。
下面这个示例说明了JavaScript中做用域的特色:编程
function foo() { var a = 10; function bar() { a *= 2; } bar(); return a; }
在这个示例中,a定义在函数foo中,但函数bar能够访问它,由于bar也定义在foo中。bar在执行过程当中将a设置为a乘以2。当bar在foo中被调用时它可以访问a,这能够理解。可是若是bar是在foo外部被调用呢?设计模式
function foo() { var a = 10; function bar() { a *= 2; return a; } return bar; } var baz = foo(); console.log(baz());//20 console.log(baz());//40 console.log(baz());//80 var blat = foo(); console.log(blat());//20
在上述代码中,所返回的对bar函数的引用被赋给变量baz。这个函数如今是在foo外部被调用,但它依然可以访问a。这是由于JavaScript的做用域是词法性的。函数是运行在定义它们的做用域中(本例中是foo内部的做用域),而不是运行在调用它们的做用域中。只要bar被定义在foo中,它就能访问在foo中定义的全部变量,即便foo的执行已经结束。
这就是闭包的一个例子。在foo返回后,它的做用域被保存下来,但只有它返回的那个函数可以访问这个做用域。在前面的示例中,baz和blat各有这个做用域及a的一个副本,并且只有它们本身能对其进行修改。返回一个内嵌函数是建立闭包最经常使用的手段。数组
在门户打开型对象建立模式中,全部方法都建立在原型对象中,所以无论派生多少对象实例,这些方法在内存中只存在一份。而包含特权方法、私用成员的建立模式中,每生成一个新的对象示例都将为每个私用方法和特权方法生成一个新的副本。这会比其余作法耗费更多内存,因此只宜用在须要真正的私用成员的场合。这种对象建立模式也不利于派生子类,由于所派生出的子类不能访问超类的任何私用属性或方法。相比之下,在大多数语言中,子类都能访问超类的全部私有属性和方法。故在JavaScript中用闭包实现私用成员致使的派生问题称为“继承破坏封装”。
前面所讲的做用域和闭包的概念可用于建立静态成员,包括公用和私用的。大多数方法和属性所关联的是类的实例,而静态成员所关联的则是类自己。换句话说,静态成员是在累的层次上操做,而不是在实例的层次上操做。每一个静态成员都只有一份。稍后将会看到,静态成员是直接经过类对象访问的。
下面是添加了静态属性和方法的Book类:
var Book = (function () { //私有静态变量 var numOfBooks = 0; //私有静态方法 function checkIsbn(isbn) { } //返回一个构造器 return function (newIsbn, newTitle, newAuthor) { //私有属性 var isbn, title, author; //特权方法 this.getIsbn = function () { return isbn; }; this.setIsbn = function (newIsbn) { if (!checkIsbn(newIsbn)) { throw new Error('Book: Invalid ISBN.'); } isbn = newIsbn; }; this.getTitle = function () { return title; }; this.setTitle = function (newTitle) { title = newTitle || "No title specified"; }; this.getAuthor = function () { return author; }; this.setAuthor = function (newAuthor) { author = newAuthor || "No author specified"; }; //Constructed code. numOfBooks++; if (numOfBooks > 50) { throw new Error("."); } this.setIsbn(newIsbn); this.setTitle(newTitle); this.setAuthor(newAuthor); } })(); //公共静态方法 Book.convertToTitleCase = function (inputString) { }; //公共非特权方法 Book.prototype = { display: function () { } };
这里的私用成员和特权成员仍然被声明在构造器中(分别使用var和this关键字)。但哪一个构造器却从原来的普通函数变成了一个内嵌函数,而且被做为包含它的函数的返回值赋给变量Book。这就建立了一个闭包,你能够把静态的私用成员声明在里面。位于外层函数声明以后的一对空括号很重要,其做用是一段代码载入就当即执行这个函数(而不是在调用Book构造函数时)。这个函数的返回值是另外一个函数,它被赋给Book变量,Book所以成了一个构造函数。在实例化Book时,所调用的是这个内层函数。外层那个函数只是用于建立一个能够用来存放静态私用成员的闭包。浏览器
- 在本例中,checkIsbn被设计为静态方法 ,缘由是为Book的每一个实例都生成这个方法的一个新副本毫无道理。此外还有一个静态属性numOfBooks,其做用在于跟踪Book构造器的总调用次数。本例利用这个属性将Book实例的个数限制为不超过50个。
- 这些私用的静态成员能够从构造器内部访问,这意味着全部私用函数和特权函数都能访问它们。与其余方法相比,它们有一个明显的优势,那就是内存中只会存放一份。由于其中那些静态方法被声明在构造器以外,因此它们不是特权方法,不能访问任何定义在构造器中的私用属性。定义在构造器中的私用方法可以调用那些私用静态方法,反之则否则。要判断一个私用方法是否应该被设计为静态方法,一条经验法则是看它是否须要访问任何实例数据。若是它不须要,那么将其设计为静态方法会更有效率(从内存占用的意义上来说),由于它只会被建立一份。
- 建立公用的静态成员则容易得多,只需直接将其做为构造函数这个对象的属性建立便可,前述代码中的方法converToTitleCase就是一例。这实际上至关于把构造器做为命名空间来使用。
- 全部公用静态方法若是做为独立的函数来声明其实也一样简单,但最好仍是像这样把相关行为集中在一块儿。这些方法用于与类这个总体相关的任务,而不是与类的任一特定实例相关的任务。它们并不直接依赖于对象实例中包含的任何数据。
经过建立只有取值器而没有赋值器的私用变量能够模仿常量。
var Class = (function () { var UPPER_BOUND = 100; //构造器 var ctor = function (constructorArgument) { }; //静态特权方法 ctor.getUPPER_BOUND = function () { return UPPER_BOUND; }; return ctor; })();
- 私用方法很难进行单元测试。由于它们及其内部变量都是私用的,因此在对象外部没法访问到它们。这个问题没有什么很好的应对之策。你要么经过使用公用方法来提供访问途径(这样一来就葬送了使用私有方法所带来的大多数好处),要么设法在对象内部定义并执行全部单元测试。最好的解决办法是只对公用方法进行单元测试。这应该能覆盖到全部私用方法,尽管对它们的测试只是间接的。这种问题不是JavaScript所独有的,只对公用方法进行单元测试是一种广为接收的处理方式。
- 使用封装意味着不得不与复杂的做用域链打交道。
- 封装可能会损害类的灵活性,导致其没法被用于某些你不曾想到过的目的。
单体模式是JavaScript中最基本但又最有用的模式之一,它可能比其余任何模式都更经常使用。这种模式提供了一种将代码组织为一个逻辑单元的手段,这个逻辑单元中的代码能够经过单一的变量进行访问。经过确保单体对象只存在一份实例,你就能够确信本身的全部代码使用的都是一样的全局资源。
单体类在JavaScript中有许多用处。它们能够用来划分命名空间,以减小网页中全局变量的数目。更重要的是,借助于单体模式,你能够把代码组织得更为一致,从而使其更容易阅读和维护。
var Singleton = { attribute1: true, attribute2: 10, method1: function () { }, method2: function (args) { } };
为了不无心中改写变量,最好的解决办法之一是用单体对象将代码组织在命名空间之中。下面是前面的例子用单体模式改良后的结果:数据结构
var MyNamespace = { findProduct: function (id) { } };
如今findProduct函数是MyNamespace中的一个方法,它不会被全局命名空间中声明的任何新变量改写。要注意,该方法仍然能够从各个地方访问。不一样之处在于如今其调用方式不是findProduct(id),而是MyNamespace.findProduct(id)。还有一个好处就是,这可让其余程序员大致知道这个方法的声明地点及其做用。用命名空间把相似的方法组织到一块儿,也有助于加强代码的文档性。闭包
有一种单体模式被称为模块模式,由于它能够把一批相关方法和属性组织为模块并起到划分命名空间的做用。例如:
MyNamespace.Singleton = (function () { //私有成员 var privateAttribute1 = false; var privateAttribute2 = [1, 2, 3]; function privateMethod1() { } function privateMethod2() { } return { //public members publicAttribute1: true, publicAttribute2: 10, publicMethod1: function () { }, publicMethod2: function (args) { } } })();
最好用一个例子来讲明简单工厂模式的概念。假设你想开几个自行车商店,每一个店都有几种型号的自行车出售。这能够用一个类来表示:
/*BicycleShop class.*/ var BicycleShop = function () { }; BicycleShop.prototype = { sellBicycle: function (model) { var bicycle; switch (model) { case "The Speedster": bicycle = new SpeedSter(); break; case "The Lowrider": bicycle = new Lowrider(); break; case "The Comfort Cruiser": default: bicycle = new ComfortCruiser(); } Interface.ensureImplements(bicycle, Bicycle); bicycle.assemble(); bicycle.wash(); return bicycle; } };
sellBicycle方法根据所要求的自行车型号用switch语句建立一个自行车的实例。各类型号的自行车实例能够互换使用,由于它们都实现了Bicycle接口:
/* The Bicycle interface. */ var Bicycle = new Interface('Bicycle', ['assemble', 'wash', 'ride', 'repair']); /* Speedster class. */ var Speedster = function () { }; Speedster.prototype = { assemble: function () { }, wash: function () { }, ride: function () { }, repair: function () { } };
要出售某种型号的自行车,只要调用sellBicycle方法便可:
var californiaCruisers = new BicycleShop(); var yourNewBike = californiaCruisers.sellBicycle("The Speedster");
在状况发生变化以前,这倒也挺管用。但要是你想在供货目录中加入一款新车型又会怎么样呢?你得为此修改BicycleShop的代码,哪怕这个类的实际功能实际上并无发生改变——依旧是建立一个自行车的新实例,组装它,清洗它,而后把它交给顾客。更好的解决办法是把sellBicycle方法中“建立新实例”这部分工做转交给一个简单工厂对象:
/* BicycleFactory namespace. */ var BicycleFactory = { createBicycle:function(model){ var bicycle; switch (model) { case "The Speedster": bicycle = new SpeedSter(); break; case "The Lowrider": bicycle = new Lowrider(); break; case "The Comfort Cruiser": default: bicycle = new ComfortCruiser(); } Interface.ensureImplements(bicycle, Bicycle); return bicycle; } };
BicycleFactory是一个单体,用来把createBicycle方法封装在一个命名空间中。这个方法返回一个实现了Bicycle接口的对象,而后你能够照常对其进行组装和清洗:async
/* BicycleShop class, improved. */ var BicycleShop = function () { }; BicycleShop.prototype = { sellBicycle: function (model) { var bicycle = BicycleFactory.createBicycle(model); bicycle.assemble(); bicycle.wash(); return bicycle; } };
这个BicycleFactory对象能够供各类类用来建立新的自行车实例。有关可供车型的全部信息集中在一个地方管理 ,因此添加更多车型很容易:ide
/* BicycleFactory namespace,with more models. */ var BicycleFactory = { createBicycle: function (model) { var bicycle; switch (model) { case "The Speedster": bicycle = new SpeedSter(); break; case "The Lowrider": bicycle = new Lowrider(); break; case "The Flatlander": bicycle = new Flatlander(); break; case "The Comfort Cruiser": default: bicycle = new ComfortCruiser(); } Interface.ensureImplements(bicycle, Bicycle); return bicycle; } };
真正的工厂模式与简单工厂模式的区别在于,它不是另外使用一个类或对象来建立自行车,而是使用一个子类。按照正式定义,工厂是一个将其成员对象的实例化推迟到子类中进行的类。
- 动态实现:若是须要建立一些用不一样方式实现同一接口的对象,那么可使用一个工厂方法或简单工厂对象来简化选择实现的过程。
- 节省设置开销:若是对象须要进行复杂而且彼此相关的设置,那么使用工厂模式能够减小每种对象所需的代码量。若是这种设置只须要为特定类型的全部实例执行一次便可,这种做用尤其突出。把这种设置代码放到类的构造函数中并非一种高效的作法,这是由于即使设置工做已经完成,每次建立新实例的时候这些代码仍是会执行,并且这样作会把设置代码分散到不一样的类中。工厂方法很是适合于这种场合。它能够在实例化全部须要的对象以前先一次性地进行设置。不管有多少类会被实例化,这种办法均可以让设置代码集中在一个地方。
- 用许多小型对象组成一个大对象
- 工厂模式的主要好处在于消除对象间的耦合。经过使用工厂方法而不是new关键字及具体类,你能够把全部实例化的代码集中在一个位置。这能够大大简化更换所用的类或在运行期间动态选择所用的类的工做。在派生子类时它也提供了更强大的灵活性。
- 全部这些好处都与面向对象设计的这两条原则有关:弱化对象间的耦合;防止代码的重复。在一个方法中进行类的实例化,能够消除重复性的代码。这是在用一个对接口的调用取代一个具体的实现。这些都有助于建立模块化的代码。
桥接模式最多见和实际的应用场合之一就是事件监听器回调函数。假设有一个名为getBeerById的API函数,它根据一个标识符返回有关某种啤酒的信息。你但愿用户在点击的时候获取这种信息。那个被点击的元素极可能有啤酒的标识符信息,它多是做为元素自身的ID保存,也多是做为别的自定义属性保存。下面是一种作法:模块化
addEvent(element, 'click', getBeerById); function getBeerById(e) { var id = this.id; asyncRequest('GET', 'beer.uri?id=' + id, function (resp) { console.log(resp.responseText); }); }
这个API只能工做在浏览器中,若是要对这个API函数作单元测试,或者在命令行中执行,可能会报错。一个优良的API设计,不该该把它与任何特定的实现搅在一块儿。
function getBeerById(id, callback) { asyncRequest('GET', 'beer.uri?id=' + id, function (resp) { callback(resp.responseText); }) }
如今咱们将针对接口而不是实现进行编程,用桥接模式把抽象隔离开来:
addEvent(element, 'click', getBeerByIdBridge); function getBeerBIdBridge(e) { getBeerById(this.id, function (beer) { console.log(beer); }); }
这下getBeerById并无和事件对象捆绑在一块儿了。
var Class1 = function (a, b, c) { this.a = a; this.b = b; this.c = c; }; var Class2 = function (d) { this.d = d; }; var BridgeClass = function (a, b, c, d) { this.one = new Class1(a, b, c); this.two = new Class2(d); };
适配器模式能够用来在现有接口和不兼容的类之间进行适配。使用这种模式的对象又叫包装器,由于它们是在用一个新的接口包装另外一个对象。
var clientObject = { string1: "foo", string2: "bar", string3: "baz" }; function interfaceMethod(str1, str2, str3) { }
为了把clientObject做为参数传递给interfaceMethod,须要用到适配器。咱们能够这样建立一个:
function clientToInterfaceAdapter(o) { interfaceMethod(o.string1, o.string2, o.string3); } //如今就能够把整个对象传给这个函数 clientToInterfaceAdapter(clientObject);
clientToInterfaceAdapter函数的做用就在于对interfaceMethod函数进行包装,并把传递给它的参数转换给后者须要的形式。
装饰者模式可用来透明地把对象包装在具备一样接口的另外一对象中。这样一来,你能够给一个方法添加一些行为,而后将方法调用传递给原始对象。相对于建立子类来讲,使用装饰者对象是一种更灵活的选择。
享元模式最适合于解决因建立大量相似对象而累及的性能问题。这种模式在JavaScript中尤为有用,由于复杂的JavaScript代码可能很快就会用光浏览器的全部可用内存。经过把大量独立对象转化为少许共享对象,能够下降运行Web应用程序所需的资源数量。
享元模式用于减小应用程序所需对象的数量。这是经过将对象的内部状态划分为内在数据和外在数据两类而实现的。内在数据是指类的内部方法所需的信息,没有这种数据的话类不能正常运转。外在数据则是能够从类身上剥离并存储在其外部的信息。咱们能够将内在状态相同的全部对象替换为同一个共享对象,这种方法能够把对象数量减小到不一样内在状态的数量。