JavaScript 设计模式学习笔记(一)

六月 7, 20267读书笔记JavaScript

很早之前看《JavaScript 设计模式与开发实践》整理的笔记,把这些笔记搬到这里,仅供个人学习记录使用,如有需要请支持正版图书,侵删。

面向对象的JavaScript

JavaScript 是一门典型的动态类型的语言,我们可以尝试调用任何对象的任意方法,而无需去考虑它原本是否被设计为拥有该方法,这一切都是建立在鸭子类型的概念上的。
鸭子类型(duck typing):如果它走起路来像鸭子,叫起来也是鸭子,那么它就是鸭子。鸭子类型指导我们:只关注对象的行为,而不关注对象本身。

多态

一段多态的JavaScript代码:
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()); // 咯咯咯

// 如果有一天动物世界里又增加了一只狗,这时候只要简单地追加一些代码就可以了
// 而不用改动以前的makeSound函数
var Dog = function() {};
Dog.prototype.sound = function() {
  console.log('汪汪汪');
};

makeSound(new Dog()); // 汪汪汪
多态背后的思想是将“做什么”和“谁去做以及怎样去做”分离开来,也就是将“不变的事物”与“可能改变的事物”分离开来。在这个故事中,动物都会叫,这是不变的,但是不同类型的动物具体怎么叫是可变的。把不变的部分隔离出来,把可变的部分封装起来。

JavaScript的多态

多态的思想实际上是把“做什么”和“谁去做”分离开来,要实现这一点,归根结底先要消除类型之间的耦合关系。如果类型之间的耦合关系没有被消除,那么我们在 makeSound 方法中指定了发出叫声的对象是某个类型,它就不可能再被替换为另外一个类型。在 Java 中,可以通过向上转型来实现多态。
JavaScript 的变量类型在运行期是可变的。一个 JavaScript 对象,既可以表示 Duck 类型的对象,又可以表示 Chicken 类型的对象,这意味着 JavaScript 对象的多态性是与生俱来的。
某一种动物能否发出叫声,只取决于它有没有makeSound方法,而不取决于它是否是某种类型的对象,这里不存在任何程度上的“类型耦合”。在 JavaScript 中,并不需要诸如向上转型之类的技术来取得多态的效果。
多态最根本的作用就是通过把过程化的条件分支语句转化为对象的多态性,从而消除这些条件分支语句。
// 假设我们要编写一个地图应用,现在有两家可选的地图API提供商供我们接入自己的应用
var googleMap = {
  show: function() {
    console.log('开始渲染谷歌地图');
  }
};

var renderMap = function() {
  googleMap.show();
}

renderMap();

// 后来因为某些原因,要把谷歌地图换成百度地图,为了让renderMap函数保持一定的弹性,
// 我们用一些条件分支来让renderMap函数同时支持谷歌地图和百度地图
var googleMap = {
  show: function() {
    console.log('开始渲染谷歌地图');
  }
};

var baiduMap = {
  show: function() {
    console.log('开始渲染百度地图');
  }
};

var renderMap = function(type) {
  if (type === 'google') {
    googleMap.show();
  } else if (type === 'baidu') {
    baiduMap.show();
  }
}

renderMap('google'); // 开始渲染谷歌地图
renderMap('baidu'); // 开始渲染百度地图
//虽然renderMap函数目前保持了一定的弹性,但这种弹性是很脆弱的,一旦需要替换成搜搜地图,
// 那无疑必须得改动renderMap函数,继续往里面堆砌条件分支语句
先把程序中相同部分抽象出来,那就是显示某个地图:
var renderMap = function(map) {
  if (map.show instanceof Function) {
    map.show();
  }
};

renderMap(googleMap);
renderMap(baiduMap);


// 多态: 当我们向谷歌地图对象和百度地图对象分别发出“展示地图”的消息时,会分别调用它们的show方法,
// 就会产生各自不同的执行结果。对象的多态性提示我们,“做什么”和“怎么去做”是可以分开的
var sosoMap = {
  show: function() {
    console.log('开始渲染搜搜地图');
  }
};

renderMap(sosoMap);
// 对象的多态性提示我们,“做什么”和“怎么去做”是可以分开的,增加了搜狗地图,renderMap函数仍然不需要做任何改变

封装

封装的目的是将信息隐藏。

1. 封装数据

在许多语言的对象系统中,封装数据是由语法解析来实现的,这些语言也许提供了privatepublicprotected等关键字来提供不同的访问权限。
但 JavaScript 并没有提供对这些关键字的支持,我们只能依赖变量的作用域来实现封装特性,而且只能模拟出 publicprivate 这两种封装性。
var myObject = (function() {
  var __name = 'sevn'; // 私有 private 变量
  retunr {
    getName: function() { // 公开 public 方法
      return __name;
    }
  };
})();


console.log(myObject.getName()); // sevn
console.log(myObject.__name); // undefined

2. 封装实现

封装的目的是将信息隐藏,封装应该被视为“任何形式的封装”,也就是说,封装不仅仅是隐藏数据,还包括隐藏实现细节、设计细节以及隐藏对象的类型等。
从封装实现细节来讲,封装使得对象内部的变化对其他对象而言是透明的,也就是不可见的。对象对它自己的行为负责。其他对象或者用户都不关心它的内部实现。
封装使得对象之间的耦合变松散,对象之间只通过暴露的API接口来通信。
当我们修改一个对象时,可以随意地修改它的内部实现,只要对外的接口没有变化,就不会影响到程序的其他功能。

3. 封装类型

封装类型是静态类型语言中一种重要的封装方式。一般而言,封装类型是通过抽象类和接口来进行的。
当然在 JavaScript 中,并没有对抽象类和接口的支持。JavaScript 本身也是一门类型模糊的语言。在封装类型方面,JavaScript 没有能力,也没有必要做得更多。

4. 封装变化

从设计模式的角度出发,封装在更重要的层面体现为封装变化。
通过封装变化的方式,把系统中稳定不变的部分和容易变化的部分隔离开来,在系统的演变过程中,我们只需要替换那些容易变化的部分,如果这些部分是已经封装好的,替换起来也相对容易。这可以最大程度地保证程序的稳定性和可扩展性。
当我们想办法把程序中变化的部分封装好之后,剩下的即是稳定而可复用的部分了。

高阶函数实现AOP

AOP(面向切面编程)的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些跟业务逻辑无关的功能通常包括日志统计、安全控制、异常处理等。
把这些功能抽离出来之后,再通过“动态织入”的方式掺入业务逻辑模块中。这样做的好处首先是可以保持业务逻辑模块的纯净和高内聚性,其次是可以很方便地复用日志统计等功能模块。
在 JavaScript 中实现AOP,都是指把一个函数“动态织入”到另外一个函数之中:
Function.prototype.before = function(beforefn) {
  var __self = this;
  return function() {
    beforefn.apply(this, arguments);
    return __self.apply(this, arguments);
  };
};

Function.prototype.after = function(afterfn) {
  var __self = this;
  return function() {
    var ret = __self.apply(this, arguments);
    afterfn.apply(this, arguments);
    return ret;
  };
};


var func = function() {
  console.log(2);
}

func = func.before(function() {
  console.log(1);
}).after(function() {
  console.log(3);
});

func(); // 1 2 3

uncurrying

把泛化this的过程提取出来
Function.prototype.uncurrying = function() {
  var self = this;
  return function() {
    var obj = Array.prototype.shift.call(arguments);
    return self.apply(obj, arguments);
  }
};
常见用法
var push = Array.prototype.push.uncurrying();

(function() {
  push(arguments,4);
  console.log(arguments); // [1, 2, 3, 4]
})(1, 2, 3);

// Function.prototype.call和Function.prototype.apply本身也可以被uncurrying
var call = Function.prototype.call.uncurrying();
var fn = function(name) {
  console.log(name);
};
call(fn, window, 'sevn'); // sevn
另一种实现方式
Function.prototype.uncurrying = function() {
  var self = this;
  return function() {
    return Function.prototype.call.apply(self, arguments);
  }
};
END

评论

欢迎分享你的看法

发表评论
欢迎留言,请友好互动
点击添加表情:

全部评论

0

还没有评论

来说两句吧!