【读书笔记】Javascript的设计模式(Part 1 面向对象的JavaScript)

191 阅读15分钟

第1章 富有表现力的JavaScript

1.1 JavaScript的灵活性

即可采用函数式编程风格,也可采用面向对象变成风格;

1.2 弱类型语言

不必声明数据类型,根据所赋的值改变类型,且原始类型之间也可进行类型转换。

1.3 函数是一等对象

函数可以存储在变量中,可以作为参数传递,可以作为返回值从其他函数传出,还可在运行时进行构造,因此可创建闭包。

1.4 对象的易变性

所有的对象和类都是易变的,可以对先前定义的类和实例化的对象进行修改;

反射: 在运行时检查对象所具有属性和方法,还可使用这种信息动态实例化类和执行其他方法;

PS:这里模仿传统的面向对象的特性都依赖于对象的易变性和反射,而在其他静态语言如C++,java,则不能对已经实例化的对象进行扩展,也不能对已经定义好的类进行修改;

1.5 继承

js可供使用的继承有两种,一种是通过对象的原型继承的;一种是类式继承,各有优缺点。

1.6 JavaScript中的设计模式

js使用设计模式的优点:

  1. 可维护性
  2. 沟通
  3. 性能(如享元模式、代理模式)

缺点:

  1. 复杂性
  2. 性能(多数模式对性能有所拖累)

第2章 接口

2.1 什么是接口

接口提供了一种用以说明一个对象应该具有哪些方法的手段。

2.1.1 接口之利

  1. 既定的一批接口具有自我描述性,并能促进代码的重用。
  2. 有助于稳定不同类之间的通信方式。
  3. 测试和调试变得更轻松。

2.1.2 接口之弊

  1. 降低灵活性。
  2. js并没有提供接口的内置支持,若试图模仿总会有一些风险。
  3. 影响性能。
  4. 无法强迫其他程序员遵守定义的接口。

2.2 其他面向对象语言处理接口的方式

接口结构包含的信息说明了需要实现什么方法以及这些方法具备的参数,类定义明确地说明他们实现了这个接口(implements),一个类可实现多个接口,若接口中的方法没有被实现,则会产生一个错误,有的语言在编译时报错,有的在运行时报错,错误信息包含类名,接口名和未被实现的方法名。

2.3 在Javascript中模仿接口

2.3.1 用注释描述接口

优点: 易于实现,提高代码可重用性,不影响文件尺寸或执行速度。

缺点:不会进行检查、抛错,对接口的约定完全靠自觉。对调试,测试没有帮助。

2.3.2 用属性检查模仿接口

for循环检查是否实现这个方法。

优点:会检查,抛错,对调试测试有帮助,规范其他程序员声明。 缺点:声明了,但是不一定实现了。

2.3.3 用鸭式辨型模仿接口

如果对象具有与接口定义的方法同名的所有方法,那么就可以认为它实现了这个接口。

优点:三种方法中最有用的。 缺点:降低了代码的可重用性,缺乏自我描述性;需要使用辅助类Interface和辅助函数ensureImplements只关心方法名,不检查其参数的名称、数目、类型。

2.4 本书采用的接口实现方法

第一种和第三种的结合;

2.5 Interface类

var Interface = function(name, methods) {
    if(arguments.length != 2) {
        throw new Error("Interface constructor called with " + arguments.length
          + "arguments, but expected exactly 2.");
    }
    
    this.name = name;
    this.methods = [];
    for(var i = 0, len = methods.length; i < len; i++) {
        if(typeof methods[i] !== 'string') {
            throw new Error("Interface constructor expects method names to be " 
              + "passed in as a string.");
        }
        this.methods.push(methods[i]);        
    }    
};    
// Static class method.
Interface.ensureImplements = function(object) {
    if(arguments.length < 2) {
        throw new Error("Function Interface.ensureImplements called with " + 
          arguments.length  + "arguments, but expected at least 2.");
    }
    for(var i = 1, len = arguments.length; i < len; i++) {
        var interface = arguments[i];
        if(interface.constructor !== Interface) {
            throw new Error("Function Interface.ensureImplements expects arguments "   
              + "two and above to be instances of Interface.");
        }
        
        for(var j = 0, methodsLen = interface.methods.length; j < methodsLen; j++) {
            var method = interface.methods[j];
            if(!object[method] || typeof object[method] !== 'function') {
                throw new Error("Function Interface.ensureImplements: object " 
                  + "does not implement the " + interface.name 
                  + " interface. Method " + method + " was not found.");
            }
        }
    } 
};

2.5.1 Interface类的使用场合

接口既能向函数传递任何类型的参数,还能保证只会使用那些具有必要方法的对象。在大型项目中,若还没编写出api或需要提供一些占位代码以免延误开发进度。

2.5.2 Interface类的用法

  1. 将Interface类纳入HTML文件中。
  2. 逐一检查代码中所有以对象为参数的方法。
  3. 为你需要的每个不同方法集创建一个Interface对象。
  4. 剔除所有针对构造器的显示检查。
  5. 以Interf。ensureImplements取代原来的构造器检查。

2.5.3 实例:使用Interface类

只要使用了实现所需方法集的类的实例即可,而不需要确定是哪个类。

2.6 依赖于接口的设计模式

  1. 工厂模式
  2. 组合模式
  3. 装饰者模式
  4. 命令模式

2.7 小结

强制进行不必要的严格类型检查会损害js的灵活性,谨慎使用有助于创建更健壮的类和更稳定的代码;

第3章 封装和信息隐藏

私有变量可降低对象之间的耦合,保持数据的完整性并对其修改方式进行约束。

3.1 信息隐藏原则

两个参与者必须通过明确的通道传送信息,这样即使有一方换了,由于没有依赖,另一方还可正常执行。

3.1.1 封装与信息隐藏

封装:对对象的内部数据表现形式和实现细节进行隐藏,若想要访问封装后的对象中的数据,只有使用已定义的操作这一种方法。信息隐藏是目的,封装是技术。JavaScript是使用闭包达到该效果。

3.1.2 接口扮演的角色

接口提供了一份记载着可供公众访问的方法的契约,定义了两个对象可以具有的关系,只要接口不变,这个关系的双方都是可替换的。

3.2 创建对象的基本模式

JavaScript中创建对象的基本模式有3种:门户大开型、命名规范区别、闭包创建私有变量。

下面分三小节分别使用三种方法完成下面的代码执行:

// Book(isbn, title, author)
var theHobbit = new Book('0-395-07122-4', 'The Hobbit', 'J. R. R. Tolkein');
theHobbit.display(); // Outputs the data by creating and populating an HTML element.

3.2.1 门户大开型对象

设置性提供对数据具有保护作用的取值器和赋值器。

优点:易学会、可读性好。

缺点:无法保护内部数据,取值器和赋值器引入额外的代码。

var Book = function(isbn, title, author) { // implements Publication
  this.setIsbn(isbn);
  this.setTitle(title);
  this.setAuthor(author);
}
Book.prototype = {
  checkIsbn: function(isbn) {
    ...
  },
  getIsbn: function() {
    return this.isbn;
  },
  setIsbn: function(isbn) {
    if(!this.checkIsbn(isbn)) throw new Error('Book: Invalid ISBN.');
    this.isbn = isbn;
  },
  getTitle: function() {
    return this.title;
  },
  setTitle: function(title) {
    this.title = title || 'No title specified';
  },
  getAuthor: function() {
    return this.author;
  },
  setAuthor: function(author) {
    this.author = author || 'No author specified';
  },
  display: function() {
    ...
  }
};

3.2.2 用命名规范区别私有成员

在门户大开的基础上,将变量添加_前缀,以表示变量私有,不能私自更改;

var Book = function(isbn, title, author) { // implements Publication
  this.setIsbn(isbn);
  this.setTitle(title);
  this.setAuthor(author);
}
Book.prototype = {
  _checkIsbn: function(isbn) {
    ...
  },
  getIsbn: function() {
    return this._isbn;
  },
  setIsbn: function(isbn) {
    if(!this._checkIsbn(isbn)) throw new Error('Book: Invalid ISBN.');
    this._isbn = isbn;
  },
  getTitle: function() {
    return this._title;
  },
  setTitle: function(title) {
    this._title = title || 'No title specified';
  },
  getAuthor: function() {
    return this._author;
  },
  setAuthor: function(author) {
    this._author = author || 'No author specified';
  },
  
  display: function() {
    ...
  }
};

3.3.3 用闭包实现私有成员

优点:真正实现私有;

缺点:占用更多内存。

var Book = function(newIsbn, newTitle, newAuthor) { // implements Publication
  // Private attributes.
  var isbn, title, author;
  // Private method.
  function checkIsbn(isbn) {
    ... 
  }  
  // Privileged methods.
  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';
  };
  // Constructor code.
  this.setIsbn(newIsbn);
  this.setTitle(newTitle);
  this.setAuthor(newAuthor);
};
// Public, non-privileged methods.
Book.prototype = {
  display: function() {
    ...
  }
};

3.3 更多高级对象创建模式

3.3.1 静态方法和属性

静态私有方法checkIsbn:每个Book实例逗生成这个方法的一个新副本占用内存,所以可保存在闭包中,每个实例用同一个。

静态共有方法:convertToTitleCase;

var Book = (function() {
  
  // Private static attributes.
  var numOfBooks = 0;
  // Private static method.
  function checkIsbn(isbn) {
    ... 
  }    
  // Return the constructor.
  return function(newIsbn, newTitle, newAuthor) { // implements Publication
    // Private attributes.
    var isbn, title, author;
    // Privileged methods.
    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';
    };
    // Constructor code.
    numOfBooks++; // Keep track of how many Books have been instantiated
                  // with the private static attribute.
    if(numOfBooks > 50) throw new Error('Book: Only 50 instances of Book can be '
        + 'created.');
    this.setIsbn(newIsbn);
    this.setTitle(newTitle);
    this.setAuthor(newAuthor);
  }
})();
// Public static method.
Book.convertToTitleCase = function(inputString) {
  ...
};
// Public, non-privileged methods.
Book.prototype = {
  display: function() {
    ...
  }
};

3.3.2 常量

通过只创建取值器而没有赋值器的私有变量来模仿常量。

var Class = (function() {
  
  // Constants (created as private static attributes).
  var UPPER_BOUND = 100;
  
  // Constructor
  var ctor = function(constructorArgument) {
  ...
  };
  
  // Privileged static method.
  ctor.getUPPER_BOUND = function() {
    return UPPER_BOUND;
  };
  ...
  // Return the constructor.
  return ctor;
})();
/* Grouping constants together. */
var Class = (function() {
  
  // Private static attributes.
  var constants = {
    UPPER_BOUND: 100,
    LOWER_BOUND: -100
  };
  
  var ctor = function(constructorArgument) {
  ...
  };
  
  // Privileged static method.
  ctor.getConstant = function(name) {
    return constants[name];
  };
  ...
  // Return the constructor.
  return 
})();
/* Usage. */
Class.getConstant('UPPER_BOUND');

3.4 封装之利

  1. 保护内部数据的完整性;
  2. 利于重构;
  3. 提高对象的可重用性;
  4. 避免命名空间冲突;
  5. 代码修改更轻松;
  6. 减少其他函数所需的错误检查代码的数量,并确保数据不会处于无效状态;

3.5 封装之弊

  1. 难以进行单元测试;
  2. 调试更加困难;
  3. 可能存在过度封装;
  4. 可能损害类的灵活性;
  5. 可能难以理解既有代码;

第4章 继承

4.1 为什么需要继承

优点:

  1. 减少重复性代码,弱化对象间的耦合;
  2. 对设计进行修改更轻松; 缺点:
  3. 父类和子类产生强耦合;

4.2 类式继承

父类;

/* Class Person. */
function Person(name) {
  this.name = name;
}
Person.prototype.getName = function() {
  return this.name;
}
var reader = new Person('John Smith');
reader.getName();

4.2.1 原型链方式

让子类的prototype指向父类的实例即可;

/* Class Author. */
function Author(name, books) {
  Person.call(this, name); // Call the superclass' constructor in the scope of this.
  this.books = books; // Add an attribute to Author.
}
Author.prototype = new Person(); // Set up the prototype chain.
Author.prototype.constructor = Author; // Set the constructor attribute to Author.
Author.prototype.getBooks = function() { // Add a method to Author.
  return this.books;
};
var author = [];
author[0] = new Author('Dustin Diaz', ['JavaScript Design Patterns']);
author[1] = new Author('Ross Harmes', ['JavaScript Design Patterns']);
author[1].getName();
author[1].getBooks();

4.2.2 extend 函数

不需要手工设置prototype和constructor属性;通过superclass去获取父类的属性和方法;

/* Extend function, improved. */
function extend(subClass, superClass) {
  var F = function() {};
  F.prototype = superClass.prototype;
  subClass.prototype = new F();
  subClass.prototype.constructor = subClass;
  subClass.superclass = superClass.prototype;
  if(superClass.prototype.constructor == Object.prototype.constructor) {
    superClass.prototype.constructor = superClass;
  }
}
/* Class Author. */
function Author(name, books) {
  Author.superclass.constructor.call(this, name);
  this.books = books;
}
extend(Author, Person);
Author.prototype.getBooks = function() {
  return this.books;
};
Author.prototype.getName = function() {
  var name = Author.superclass.getName.call(this);
  return name + ', Author of ' + this.getBooks().join(', ');
};

4.3 原型式继承

使用原型式继承时,并不需要用类来定义对象的结构,只需要创建一个对象即可。这得益于原型链查找的工作机制。

/* Person Prototype Object. */
var Person = {
  name: 'default name',
  getName: function() {
    return this.name;
  }
};
var reader = clone(Person);
alert(reader.getName()); // This will output 'default name'.
reader.name = 'John Smith';
alert(reader.getName()); // This will now output 'John Smith'.
/* Author Prototype Object. */
var Author = clone(Person);
Author.books = []; // Default value.
Author.getBooks = function() {
  return this.books;
}
var author = [];
author[0] = clone(Author);
author[0].name = 'Dustin Diaz';
author[0].books = ['JavaScript Design Patterns'];
author[1] = clone(Author);
author[1].name = 'Ross Harmes';
author[1].books = ['JavaScript Design Patterns'];
author[1].getName();
author[1].getBooks();

4.3.1 对继承而来的成员的读和写的不对等性

由于原型链查找的关系,修改成员不仅会影响当前对象,还会影响到所有继承了当前对象的对象。所以父类的任何复杂的子对象都应该用方法创建。如下:

// Best approach. Uses a method to create a new object, with the same structure and
// defaults as the original.
var CompoundObject = {};
CompoundObject.string1 = 'default value',
CompoundObject.createChildObject = function() {
  return {
    bool: true,
    num: 10
  }
};
CompoundObject.childObject = CompoundObject.createChildObject();
var compoundObjectClone = clone(CompoundObject);
compoundObjectClone.childObject = CompoundObject.createChildObject();
compoundObjectClone.childObject.num = 5;

4.3.2 clone函数

该函数返回的是一个以给定对象为原型对象的空对象。

/* Clone function. */
function clone(object) {
    function F() {}
    F.prototype = object;
    return new F;
}

4.4 类式继承与原型式继承的对比

  1. 原型式继承更能节约内存,更为简练。
  2. 类式继承和其他语言一样,更为熟悉。

4.5 继承与封装

门户大开类型最适合于派生子类,如果某个成员需要稍加隐藏,可以下划线符号规范。在派生具有真正的私用变量时,可在子类中,间接访问父类的私有属性,但子类自身的实例方法都不能直接访问这些私用属性;父类的私有成员只能通过这些既有的特权方法进行访问,不能在子类中添加能够直接访问他们的新的特权方法。

4.6 掺元类

由于一个对象只能有一个原型对象,所以JavaScript中是不允许对继承,但是可以通过掺元类进行扩充,以实现多继承的效果。就是将掺元类的所有属性赋值給子元素。

掺元类: 包含通用方法的类。掺元类非常适合于组织那些彼此迥然不同的类所共享的方法。

/* Mixin class. */
var Mixin = function() {};
Mixin.prototype = {
  serialize: function() {
    var output = [];
    for(key in this) {
      output.push(key + ': ' + this[key]);
    }
    return output.join(', ');
  }
};

实现多继承的效果,如果只有父类和子类,则将父类的属性全部給子类;如果只需要固定的属性,则只給固定的属性;

function augment(receivingClass, givingClass) {
  if(arguments[2]) { // Only give certain methods.
    for(var i = 2, len = arguments.length; i < len; i++) {
      receivingClass.prototype[arguments[i]] = givingClass.prototype[arguments[i]];
    }
  } 
  else { // Give all methods.
    for(methodName in givingClass.prototype) { 
      if(!receivingClass.prototype[methodName]) {
        receivingClass.prototype[methodName] = givingClass.prototype[methodName];
      }
    }
  }
}
augment(Author, Minxin, 'serilalize');

4.7 继承的适用场所

优点:

  1. 代码重用度高;
  2. 修改、排查方便,省时省力;

在内存效率比较重要的场合原型式继承时最佳选择;如果对面向对象语言的继承机制熟悉的话,用类式继承,这两种都适用于类间差异较小的类层次体系,如果差异较大,可用掺元类更为合理。

第5章 单体模式

在网页上使用全局变量有很大的风险,而用单体对象创建的命名空间则是清楚这些全局变量的最佳手段之一。单体模式提供了一种将代码组织为一个逻辑单元的手段,这个逻辑单元中的代码可以通过单一的变量进行访问。还可以在一种名为分支的技术中用来封装浏览器之间的差异(借助分支技术,你在使用各种常用的工具函数时就不必再操心浏览器嗅探的事);借助于单体模式,你可以把代码组织的更为一致,更易于阅读和维护。

5.1 单体的基本结构

传统意义的单体:只能被实例化一次并且可以通过一个众所周知的方法点访问的类; 本书的单体概念:单体是一个用来划分命名空间并将一批相关方法和属性组织在一起的对象,如果它可以被实例化,只能实例化一次。对象字面量只是用以创建单体的方法之一。

/* Basic Singleton. */
var Singleton = {
  attribute1: true,
  attribute2: 10,
  method1: function() {
  },
  method2: function(arg) {
  }
};
Singleton.attribute1 = false;
var total = Singleton.attribute2 + 5;
var result = Singleton.method1();

5.2 划分命名空间

单体对象由两个部分组成:包含着犯法和属性成员的对象自身,以及用于访问它的变量。这个变量通常是全局性的,以便在网页上任何地方都能直接访问到它所指向的单体对象。有助于让用户了解代码的组织结构及其各部分的用途;

/* Declared globally. */	
function findProduct(id) {
  ...
}
...
// Later in your page, another programmer adds...
var resetProduct = $('reset-product-button');
var findProduct = $('find-product-button'); // The findProduct function just got
                                            // overwritten.
/* Using a namespace. */
var MyNamespace = {
  findProduct: function(id) {
	  ...
  },
  // Other methods can go here as well.
}
...
// Later in your page, another programmer adds...
var resetProduct = $('reset-product-button');
var findProduct = $('find-product-button'); // Nothing was overwritten.
/* GiantCorp namespace. */
var GiantCorp = {};
GiantCorp.Common = {
  // A singleton with common methods used by all objects and modules.
};
GiantCorp.ErrorCodes = {
  // An object literal used to store data.
};
GiantCorp.PageHandler = {
  // A singleton with page specific methods and attributes.
};

5.3 用作特定网页专用代码的包装器的单体

用于封装一些数据(如常量),为各网页特有的行为定义一些方法以及定义初始化方法;以及涉及DOM中特有元素的大多数代码。

/* Generic Page Object. */
Namespace.PageName = {
  // Page constants.
  CONSTANT_1: true,
  CONSTANT_2: 10,
  // Page methods.
  method1: function() {
  },
  method2: function() {
  },
  // Initialization method.
  init: function() {
  }
}
// Invoke the initialization method after the page loads.
addLoadEvent(Namespace.PageName.init);
var GiantCorp = window.GiantCorp || {};
/* RegPage singleton, page handler object. */
GiantCorp.RegPage = {
  // Constants.
  FORM_ID: 'reg-form',
  OUTPUT_ID: 'reg-results',
  // Form handling methods.
  handleSubmit: function(e) {
    e.preventDefault(); // Stop the normal form submission.
    var data = {};
    var inputs = GiantCorp.RegPage.formEl.getElementsByTagName('input');
    // Collect the values of the input fields in the form.
    for(var i = 0, len = inputs.length; i < len; i++) {
      data[inputs[i].name] = inputs[i].value;
    }
    // Send the form values back to the server.
    GiantCorp.RegPage.sendRegistration(data);
  },
  sendRegistration: function(data) {
    // Make an XHR request and call displayResult() when the response is
    // received.
    ...
  },
  displayResult: function(response) {
    // Output the response directly into the output element. We are
    // assuming the server will send back formatted HTML.
    GiantCorp.RegPage.outputEl.innerHTML = response;
  },
  // Initialization method.
  init: function() {
    // Get the form and output elements.
    GiantCorp.RegPage.formEl = $(GiantCorp.RegPage.FORM_ID);
    GiantCorp.RegPage.outputEl = $(GiantCorp.RegPage.OUTPUT_ID);
    // Hijack the form submission.
    addEvent(GiantCorp.RegPage.formEl, 'submit', GiantCorp.RegPage.handleSubmit);
  }
};
// Invoke the initialization method after the page loads.
addLoadEvent(GiantCorp.RegPage.init);

5.4 拥有私用成员单体

使用真正的司用方法的一个缺点在于比较耗费内存,因为每个实例都具有方法的一个新副本。而单体对象只会被实例化一次。

5.4.1 使用下划线表示法

/* DataParser singleton, converts character delimited strings into arrays. */ 
GiantCorp.DataParser = {
  // Private methods.
  _stripWhitespace: function(str) {
    return str.replace(/\s+/, '');
  },
  _stringSplit: function(str, delimiter) {
    return str.split(delimiter);
  },
  
  // Public method.
  stringToArray: function(str, delimiter, stripWS) {
    if(stripWS) {
      str = this._stripWhitespace(str);
    }
    var outputArray = this._stringSplit(str, delimiter);
    return outputArray;
  }
};

5.4.2 使用闭包

这种单体又称为模块模式,它可把一批相关方法和属性组织为模块并起到划分命名空间的作用。

/* Singleton with Private Members, step 3. */
MyNamespace.Singleton = (function() {
  // Private members.
  var privateAttribute1 = false;
  var privateAttribute2 = [1, 2, 3];
  
  function privateMethod1() {
    ...
  }
  function privateMethod2(args) {
    ...
  }
  return { // Public members.
    publicAttribute1: true,
    publicAttribute2: 10,
    
    publicMethod1: function() {
      ...
    },
    publicMethod2: function(args) {
      ...
    }
  };
})();

5.4.3 两种技术的比较

下划线的方式简单易用,但是并没有做到真正的私有,而后者可以享受到真正私有成员带来的所有好处,而且也不需要付出什么代价。

5.5 惰性实例化

前面所讲的单体模式都是在脚本加载时被创建出来,对于资源密集型或配置开销较大的单体,可将其实例化推迟到需要使用的时候,这种技术称为惰性加载。而被用作命名空间,特定网页专用代码包装器或组织相关实用方法的工具单体最好还是立即实话。

实现方式是调用方法之前先调用特定方法(如果被实例化了则返回实例,如果没有则实例化并返回),

MyNamespace.Singleton = (function() {
  
  var uniqueInstance; // Private attribute that holds the single instance.
  
  function constructor() { // All of the normal singleton code goes here.
    ...
  }
  
  return {
    getInstance: function() {
      if(!uniqueInstance) { // Instantiate only if the instance doesn't exist.
        uniqueInstance = constructor();
      }
      return uniqueInstance;
    }
  }
})();

惰性加载单体的缺点之一是增加复杂性,容易被别人当作普通单体,所以需要注释解释清楚

5.6 分支

分支是一种用来把浏览器之间的差异封装到运行期间进行设置的动态方法中的技术。如我们需要返回一个xhr对象,使用分支技术的做法是在脚本加载时一次性确定针对特定浏览器的代码。

/* Branching Singleton (skeleton). */
MyNamespace.Singleton = (function() {
  var objectA = {
    method1: function() {
      ...
    },
    method2: function() {
      ...
    }
  };
  var objectB = {
    method1: function() {
      ...
    },
    method2: function() {
      ...
    }
  };
  return (someCondition) ? objectA : objectB;
})();

考虑是否使用该技术,必须在缩短计算事件和占用更多内存这一利弊之间权衡。

5.7 示例:用分支技术创建XHR对象

var SimpleXhrFactory = (function() {
  
  // The three branches.
  var standard = {
    createXhrObject: function() {
      return new XMLHttpRequest();
    }
  };
  var activeXNew = {
    createXhrObject: function() {
      return new ActiveXObject('Msxml2.XMLHTTP');
    }
  };
  var activeXOld = {
    createXhrObject: function() {
      return new ActiveXObject('Microsoft.XMLHTTP');
    }
  };
  
  // To assign the branch, try each method; return whatever doesn't fail.
  var testObject;
  try {
    testObject = standard.createXhrObject();
    return standard; // Return this if no error was thrown.
  }
  catch(e) {
    try {
      testObject = activeXNew.createXhrObject();
      return activeXNew; // Return this if no error was thrown.
    }
    catch(e) {
      try {
        testObject = activeXOld.createXhrObject();
        return activeXOld; // Return this if no error was thrown.
      }
      catch(e) {
        throw new Error('No XHR object found in this environment.');
      }
    }
  }
})();

5.8 单体模式的使用场合

  1. 需要为代码提供命名空间和增加其模块性;
  2. 想控制实例数目,节省系统资源的时候。

5.9 单体模式之利

  1. 对代码的组织作用;
  2. 易于调试和维护;
  3. 有利于阅读和理解;
  4. 减少全局变量的数目;
  5. 提升性能,减少不需要的内存消耗和带宽消耗;

5.10 单体模式之弊

  1. 导致模块间的强耦合,与单一职责原则冲突;
  2. 不利于单元测试;

第6章 方法的链式调用

通过返回实例对象的引用来对该实例进行一个或多个操作的过程。

6.1 调用链的结构

(function() {
  function _$(els) {
    this.elements = [];
    for (var i = 0, len = els.length; i < len; ++i) {
      var element = els[i];
      if (typeof element == 'string') {
        element = document.getElementById(element);
      }
      this.elements.push(element);
    }
  }
  _$.prototype = {
    each: function(fn) {
      for ( var i = 0, len = this.elements.length; i < len; ++i ) {
        fn.call(this, this.elements[i]);
      }
      return this;
    },
    setStyle: function(prop, val) {
      this.each(function(el) {
        el.style[prop] = val;
      });
      return this;
    },
    show: function() {
      var that = this;
      this.each(function(el) {
        that.setStyle('display', 'block');
      });
      return this;
    },
    addEvent: function(type, fn) {
      var add = function(el) {
        if (window.addEventListener) {
          el.addEventListener(type, fn, false);
        } 
        else if (window.attachEvent) {
          el.attachEvent('on'+type, fn);
        }
      };
      this.each(function(el) {
        add(el);
      });
      return this;
    }
  };
  window.$ = function() {
    return new _$(arguments);
  };
})();
/* Usage. */
$(window).addEvent('load', function() {
  $('test-1', 'test-2').show().
    setStyle('color', 'red').
    addEvent('click', function(e) {
      $(this).setStyle('color', 'green');
    });
});

6.2 设计一个支持方法链式调用的JavaScript库

// Include syntactic sugar to help the development of our interface.
Function.prototype.method = function(name, fn) {
  this.prototype[name] = fn;
  return this;
};
(function() {
  function _$(els) {
    // ...
  }
  /*
    Events
      * addEvent
      * getEvent
  */
  _$.method('addEvent', function(type, fn) {
    // ...
  }).method('getEvent', function(e) {
    // ...
  }).
  /*
    DOM
      * addClass
      * removeClass
      * replaceClass
      * hasClass
      * getStyle
      * setStyle
  */
  method('addClass', function(className) {
    // ...
  }).method('removeClass', function(className) {
    // ...
  }).method('replaceClass', function(oldClass, newClass) {
    // ...
  }).method('hasClass', function(className) {
    // ...
  }).method('getStyle', function(prop) {
    // ...
  }).method('setStyle', function(prop, val) {
    // ...
  }).
  /*
    AJAX
      * load. Fetches an HTML fragment from a URL and inserts it into an element.
  */
  method('load', function(uri, method) {
    // ...
  });
  window.$ = function() {
    return new _$(arguments);
  });
})();
Function.prototype.method = function(name, fn) {
  // ...
};
(function() {
  function _$(els) {
    // ...
  }
  _$.method('addEvent', function(type, fn) {
    // ...
  })
  // ...
    
  window.installHelper = function(scope, interface) {
    scope[interface] = function() {
      return new _$(arguments);
    }
  };
})();
/* Usage. */
installHelper(window, '$');
$('example').show();
/* Another usage example. */
// Define a namespace without overwriting it if it already exists.
window.com = window.com || {};
com.example = com.example || {}; 
com.example.util = com.example.util || {};
installHelper(com.example.util, 'get');
(function() {
  var get = com.example.util.get;
  get('example').addEvent('click', function(e) {
    get(this).addClass('hello');
  });
})();

6.3 使用回调从支持链式调用的方法获取数据

// Accessor without function callbacks: returning requested data in accessors.
window.API = window.API || function() {
  var name = 'Hello world';
  // Privileged mutator method.
  this.setName = function(newName) {
    name = newName;
    return this;
  };
  // Privileged accessor method.
  this.getName = function() {
    return name;
  }
}();
// Implementation code.
var o = new API;
console.log(o.getName()); // Displays 'Hello world'.
console.log(o.setName('Meow').getName()); // Displays 'Meow'.
// Accessor with function callbacks.
window.API2 = window.API2 || {};
API2.prototype = function() {
  var name = 'Hello world';
  // Privileged mutator method.
  this.setName = function(newName) {
    name = newName;
    return this;
  };
  // Privileged accessor method.
  this.getName = function(callback) {
    callback.call(this, name);
    return this;
  }
}();
// Implementation code.
var o2 = new API2;
o2.getName(console.log).setName('Meow').getName(console.log);
// Displays 'Hello world' and then 'Meow'.

6.4 小结

这种编程风格有助于简化代码;