前言
《JavaScript设计模式与开发实践》是由腾讯的前端团队Alloy Team出品的一本关于js设计模式的书籍。全书主要分为3大部分:基础知识、设计模式和设计原则和编程技巧。基础知识主要是介绍了js语言的面向对象思想、多态、封装、原型、闭包、高阶函数等;设计模式以没中模式一个章节介绍了16种经常在js开发中用的设计模式;设计原则和编程技巧,从新的高度讲解常用到的开发和设计技巧。
本文主要基于全书第一部分: js基础知识的阅读经验,进行总结。
动态语言和鸭子类型
静态语法: 一种需要进行编译的语言,其在编译时决定了数据的类型,同时在编译时能够发现程序中的错误。它能够让程序员按照相应的约束进行开发,但是会在一定程度上降低开发效率。
动态语言:运行时才能决定数据的类型。编码简洁,程序员可以把更多的精力放到逻辑实现上去。
鸭子模型: 走起路来想鸭子,叫起来像鸭子,那么它就是鸭子。 鸭子模型知道我们只关注对象的行为,而不是关注对象本身,也就是关注has-a,而不是is-a。
鸭子模型可以指导我们在动态语言中实现一个原则:面向接口编程而不是面向实现编程。如:一个对象如有push和pop方法,并且这些方法提供了正确的实现,它就可以当做栈来使用。在静态语言中,面向接口编程是一件不容易的事情,需要通过抽象类型奖对象向上转型(java里的interface?)。
多态
多态的主旨思想是: 把“做什么”和“谁去做以及怎么做”分离开,也就是将“不变的事情”和“可能变的事情”分离开。如下面的例子:
var makeSound = function(animal){
if(animal instanceof Duck){
console.log("嘎嘎嘎");
}else if(animal instanceof Chikcen){
console.log("咯咯咯");
}
};
var Duck = function(){};
var Chicken = function(){};
makeSound(new Duck());
makeSound(new Chicken());
上述代码中没有把不变的事情(动物会叫)和可能变得事情(不同动物以及不不同动物的叫声)区分开来,如果加以区分,就会成为下面的样子:
var makeSound = function(animal){
animal.sound();
}
var Duck = function(){};
Duck.prototype.sound = function(){
console.log("嘎嘎嘎");
}
var Chicken = function(){};
Chicken.prototype.sound = function(){
console.log("咯咯咯");
}
makeSound(new Duck());
makeSound(new Chicken());
这样以后才增加新的动物时就可以只关注变化的东西,而不要在去处理不要变化的东西了,如增加狗叫:
var Dog = function(){};
Dog.prototype.sound = funciton(){
console.log("汪汪汪");
}
makeSound(new Dog());
静态语言(如java)实现多态的基本思想是基类定义此方法,然后子类给以不同的实现,在进行调用时通过对象的多态实现方法的调用。
总结起来: 多态最根本的作用就是通过把过程化的条件分支语句转成对象的多态性,从而消除这些条件分支语句;它时刻在提醒我们做什么和怎么去做是可一个分开的。
在设计模式中,命令模式、组合模式以及策略模式都是多态的典型代表,这些可以在后续的文章中讲述。
封装
封装可以分为:封装数据、封装实现、封装数据和封装变化。
封装数据主要体现在局部作用域,对外隐藏,如下面的代码段:
js:
var myObj = function(){
var name = "ahu";
return {
getName: function(){
return name;
}
}
}
console.log(myObj.name) // undefined
console.log(myObj.getName()) // ahu
java:
public Class myObj{
private name;
public myObj(name){
this.name = name;
}
public getName(){
return name;
}
}
myObj nameInfo = new myObj("ahu");
nameInfo.getName() // ahu
封装实现:对象对它自己的行为负责,其他对象或者用户不关心它的内部实现。这样使得对象之间的耦合松散,对象之间通过暴露的API接口进行通信。例如迭代器方法(js中的each方法),它负责遍历对象,其内部实现改变了,但只要功能不变,对用户来说都是不变的。
封装类型: 封装类型是通过抽象类和接口来实现的,如:java的多态
封装变化: 找到变化并封装之是《设计模式》的出发点,23种的设计模式被划分为:创建型模式、结构性模式和行为型模式。创建型模式的目的是封装创建对象的变化;结构型模式封装的是对象之间的组合关系;行为型模式封装的是对象的行为变化。
原型模式(继承)
原型模式在设计模式上来看,是一种创建对象的模式。原型模式很大程序上是基于clone来实现的,clone是创建对象的一种手段。 如ECMAScript5中提供了Object.create实现对象的clone,不支持此方法的浏览器可以用下面的方法进行兼容:
Objcet.create = Object.create || function(_obj){
var F = function(){};
F.prototype = _obj;
return new F();
}
js中的原型继承,其遵循以下基本原则:
- 所有的数据都是对象(其实js此处实现并不好,基础类型number、string、boolean、undefined需要进行封装才能得到对象)
- 要得到一个对象,不是通过实例化类,而是找到一个对象作为原型并clone它
- 对象会记住它的原型(proto属性指向了构造函数的constructor)
- 如果对象无法响应某个请求,它会把这个请求委托给它自己的原型(原形链搜索)
如下程序:
function person(name){ // person不是类 而是一个构造器 js中的函数,既可以作为普通函数调用,也可以作为构造器调用
this.name = name;
}
Person.prototype.getName = function(){
return this.name;
}
var aa = new Person("ahu") // new时 此时函数是一个构造器
// new操作和一下过程是等同的
var ObjectFactory = function(){
var obj = new Object(), // 从Object.prototype上clone一个空的对象
Constructor = [].shift(call).agruments; // 获得构造器,如上面的Person
obj.__proto__ = ConStructor.prototype; // 指向正确的原型, 这也是在一些框架中经常出现:AA.prototype.constructor = AA 的修正
var ret = Constructor.apply(obj, arguments); // 确保构造器总是返回一个对象
return typeof ret === 'object' ?ret :obj;
}
var bb = ObjectFactory(person, "ahu");
console.log(Object.getPrototypeOf(a) === Person.prototype); // true
套用Peter Norving的一句话:设计模式是对语言不足的补充, 如果要是有设计模式, 不如去找一门更好的语言。