JavaScript设计模式(一)

概述

动态语言类型与鸭子类型

javascript是动态类型语言,只有在运行时才会知道变量类型,缺点是运行时可能出现与类型有关的错误,
好处是,针对接口编程,不关心它isA,只关心它hasA,比如,有length,有slice等等的就可以当数组用。(相当于,可能一只会鸭子叫的鸡也会被认为是鸭子…)

多态

多态的思想其实是把“做什么”跟“谁去做”分离开来,需要找出各个对象中共有的“做什么”的部分。多态性的根本作用在于把过程化的条件分支语句转化为对象的多态性
javascript由于不用类型检查,与生俱来有多态性。很多设计模式就有多态性的内涵在里面,而javascript的高阶函数也可以用来实现这些设计模式。

1
2
3
4
5
6
7
8
9
var makeSound = function(animal){
animal.sound();
}
//这样就可以调用
// makeSound(new Duck())
var Duck = function(){}
Duck.prototype.sound = function(){
console.log("嘎嘎嘎");
}

封装

包括封装数据、封装方法、封装类型、封装变化。其中

  • 封装数据(访问权限)

    1. let限定作用域
    2. 闭包
    3. symbol 伪私有属性
  • 封装变化
    《设计模式》一书中共归纳总结了23种设计模式。从意图上区分,这23种设计模式分别被划分为创建型模式、结构型模式和行为型模式。
    拿创建型模式来说,要创建一个对象,是一种抽象行为,而具体创建什么对象则是可以变化的,创建型模式的目的就是封装创建对象的变化。而结构型模式封装的是对象之间的组合关系。行为型模式封装的是对象的行为变化。

原型系统

javascript遵守原型编程的基本原则:

  • 所有的数据都是对象
    除了undefined以外,所有数据都是对象(当然基本类型的那几种也可以通过包装类的方式得到对象),根对象都是Object.prototype。
  • 要得到一个对象,不是通过实例化类,而是找到一个对象作为原型并克隆它(Object.create(proto))
    可以通过Object.getPrototypeOf来获取原型。
    是通过new运算符进行的操作,可以用ObjectFactory函数来模拟这一操作。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function Person(name){
    this.name = name
    }
    Person.prototype.getName = function(){
    return this.name;
    }
    var objectFactory = function(){
    var obj = new Object(); //1
    var Constructor = [].shift.call(arguments);
    obj._proto_ = Constructor.prototype //!!指向原型 //2
    var ret = Constructor.apply(obj,arguments); //3
    return typeof ret==='object'? ret: obj; //4
    }
    var person = objectFactory(Person,'luchen')

    分别对应于以下四个步骤:

用new调用一个函数发生了这些事:
(1)新建一个对象
instance=new Object();
(2)设置原型链
instance.__proto__=F.prototype;
(3)让F中的this指向instance,执行F的函数体。
(4)判断F的返回值类型:
如果是值类型,就丢弃它,还是返回instance。
如果是引用类型,就返回这个引用类型的对象,替换掉instance。
作者:何幻
链接:https://www.zhihu.com/question/36440948/answer/71234418
来源:知乎
著作权归作者所有,转载请联系作者获得授权。

new操作的等价为: 克隆一个object对象,并把它指向正确的原型,并设置它的内部属性等其他操作。

  • 对象会记住它的原型
    通过对象的_proto_来记住原型。在创建这个对象(new)的时候就建立了这么一条链。
    但是也不是说原型就固定了,它可以动态的指向别处

    1
    A.prototype = obj
  • 如果对象无法响应某个请求,会把这个请求委托给他的[构造器的]原型
    查找属性时,先通过对象的_proto_来委托到该对象的构造器的原型(A.prototype)上,而A.prototype在上面被指向为obj,那么就从obj来查找属性。

    obj -> obj._proto_= A.prototype ->objB

    虽然es6中有类的概念了,但本质还是用的原型链,看es6 feature里面的对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//es6
class Shape {
constructor (id, x, y) {
this.id = id
this.move(x, y)
}
move (x, y) {
this.x = x
this.y = y
}
}
//继承
class Rectangle extends Shape {
constructor (id, x, y, width, height) {
super(id, x, y)
this.width = width
this.height = height
}
}
//es5
var Shape = function (id, x, y) {
this.id = id;
this.move(x, y);
};
Shape.prototype.move = function (x, y) {
this.x = x;
this.y = y;
};
//继承
//继承属性
var Rectangle = function (id, x, y, width, height) {
Shape.call(this, id, x, y);
this.width = width;
this.height = height;
};
//这边是继承方法
Rectangle.prototype = Object.create(Shape.prototype);//因为重写原型链了,所以constructor会丢失,要再补上
Rectangle.prototype.constructor = Rectangle;

new运算符的精髓在与_proto_的挂载,继承的精髓在于属性和方法的复制,方法的复制可以用Object.create,然后重写原型链,但重写后会导致constructor丢失,因此要找回来。具体的可以回头再看下高程…

this

this的若干种场景,在楼主的某篇blog里面已经说过了。
比较好的代码片段

1
2
3
4
5
6
7
8
9
10
11
12
document.getElementById = (
return function(func){
return func.apply(document,arguments)
}
)(document.getElementById);
//实现bind
Function.prototype.bind = function(context){
var self = this; //保存当前函数
return function(){
self.apply(context,arguments);
}
}

闭包和高阶函数

用处:

  1. 封装变量
  2. 延续局部变量的寿命
1
2
3
4
5
6
7
8
var mult = (function(){
var privateParam = {};
return function(){
privateParam[key] = someValue;
return privateParam
}
})
mult()

命令模式是把请求封装成对象,从而分离请求的发起者和请求的接受者(执行者)之间的耦合关系·用闭包可以事先往命令对象中植入命令的接收者。
虽然闭包使得局部变量可以长期被引用,一直生存下去,但一般来说它不会导致内存泄露,要回收的话只需要设置null即可。与内存有关的地方是比较容易形成循环引用,如果闭包的作用域链中保存着一些DOM节点,那就可能造成内存泄露,但这是由于IE浏览器中BOM和DOM是使用C++用COM对象的凡是实现的,它的垃圾回收机制是引用计数,循环引用会导致无法被回收,所以内存泄漏。

经典闭包

1
2
3
4
5
6
7
for(var i=0;i<nodes.length;i++){
(function(i){
node[i].onclick = function(){
console.log(i);
}
})(i)
}

高阶函数

高阶函数是至少满足以下条件的函数:
参数为函数
返回值为函数

  • 参数为函数: ajax回调、Array.prototype.sort函数
  • 返回值为函数:常见的为单栗模式,具体的可以看之前的博文第八条

还有一段很不错的代码片段:

1
2
3
4
5
6
7
function isType(type){
return function(obj){
return Object.prototype.toString.call(obj)===`[object ${type}]`;
}
}
var isArray = isType('Array')
isArray([1,2,4])

高阶函数的其他场景还有:

  • currying 之后我们看函数式编程会涉及到,具体再看
  • 函数节流(之前我们讲过)
  • 惰性加载(首次嗅探后函数覆盖)
1
2
3
4
5
6
7
8
9
10
11
12
function createXHR(){
iftypeof XMLHttpRequest != 'undefined'){
createXHR = function(){
new XMLHttpRequest();
}
}else if(typeof ActiveObject != 'undefined'){
createXHR = function(){
...
}
}else ...
return createXHR();
}
  • 分时函数(一次往页面上大量添加dom节点会让浏览器卡顿,可以用settimeout分多次进行操作)

设计模式

1. 单例模式

js中单例模式的核心是:确保只有一个实例,供全局访问。

1
2
3
4
5
6
7
//单例
var Singleton = (function(){
var param;
return function(initialValue){
return param || (param = initialValue);
}
})();

全局变量不是单例,但常被用作单例,但会造成命名空间的污染,有以下几种方式可以降低污染:

  • 使用命名空间

    1
    2
    3
    4
    var a = {
    someSpace1: {},
    someSpace2: {}
    }
  • 使用闭包封装私有变量

    1
    2
    3
    4
    5
    6
    var func1 = function(){
    var privateParam;
    return function(){
    //...
    }
    }

1.1 惰性单例

使用时才创建,而不是一运行/加载页面就创建。比如登录窗口,通常的做法是一开始就隐藏,使用时display:block。但很可能一直用不到。改进的方法是onclick的时候创建,但是维持的需要保证是同一个变量,就可以用之前的闭包来进行判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//惰性单例
var generateLoginBox = (function(){
var div;
function createDiv(){
var div = document.createElement('div');
div.innerHTML = "登录窗口";
document.body.appendChild(div);
return div;
}
return function(){
return div || (div = createDiv());
}
})();
document.getElementById("loginBtn").addEventListener("click",function(){
var div = generateLoginBox();
div.style.display = 'block';
}

但实际上上述代码中管理单例的跟创建对象的进行了耦合(即这个单例可以用来创建很多类型的单例,不光是div,还可能是其他的image等等)。因此需要进行拆分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function createDiv(){
var div = document.createElement('div');
div.innerHTML = "登录窗口";
document.body.appendChild(div);
return div;
}
//朴素的改进,一步生成结果
var generateSingle = (function(){
var result;
return function(fn){
return result || (result = fn.apply(this,arguments));
}
})();
document.getElementById("loginBtn").addEventListener("click",function(){
var div = generateSingle(createDiv);
div.style.display = 'block';
}

method2:生成函数的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
//管理单例的函数!
var generateSingle = function(fn){
var result;
return function(){
return result || (result = fn.apply(this,arguments));
}
}
document.getElementById("loginBtn").addEventListener("click",function(){
var createSingleDiv = generateSingle(createDiv);
var div = createSingleDiv();
div.style.display = 'block';
}

类似的场景还包括对dom元素绑定事件,实际上只要在最开始创建的时候绑一次就好。

【summary】
全局+命名空间-》单例-》惰性单例(管理单例的代码与创建对象的会耦合)-》带有代理的惰性单例


策略模式

js中策略模式的核心是:定义一系列可以互相替代的算法,把他们分别封装起来。在js中表现为一个strategy对象

程序需要包括两部分,一个是接受用户请求并进行分配的总调度部分,一个是各策略计算部分。如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//调度部分Context
var calculateBonus = function(salary,level){
return strategies[level](salary);
}
//策略计算部分
var strategies = {
'A': function(salary){
return salary*3;
},
'B': function(salary){
return salary*2;
},
'C': function(salary){
return salary*1;
}
}

还有其他栗子如,缓动动画和表单校验:

  • 对于缓动动画来说,策略就是集结了众多” 根据time,起终点等信息最终计算位置的函数”策略的对象。
  • 对于表单校验,后面我们会提到,表单校验也涉及到装饰器模式(在不改动原有代码的基础上,先进行validate 的before校验,返回值正确后再提交)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
var strategies = {
noEmptyName:function(name,errMsg){
if(!name) return errMsg;
},
minLength: function(value,length,errMsg){
if(value.length<length) return errMsg;
},
isMobile: function(phoneNum,reg,errMsg){
if(!reg.test(phoneNum)) return errMsg;
}
}
function Validator(){
this.validateQueue = [];
}
/**
* [validator.add()]
* @param {[type]} item [description]
* @param {[type]} rules [description]
*/
Validator.prototype.add = function(item,rules){
var ruleFuncs = rules.map(function(rule){
var strategyArr = rule.strategy.split(':');
var strategy = strategyArr.shift();
strategyArr.unshift(item);
strategyArr.push(rule.errorMsg);
return function(){
return strategies[strategy].apply(null,strategyArr);
};
})
this.validateQueue = this.validateQueue.concat(ruleFuncs);
}
Validator.prototype.start = function(){
return this.validateQueue.every(function(fn){
var errMsg = fn();
if(errMsg) {
console.log(errMsg);
return false;
}
return true;
})
}
var validate = function(){
var validator = new Validator();
var form = {
username: 'luchen'
}
validator.add(form.username,
[{
strategy: 'noEmptyName',
errorMsg: '用户名不能为空'
},
{
strategy: 'minLength:7',
errorMsg: '用户名长度不能小于7位'
}]);
var result = validator.start();
}
validate();

============更新
增加了装饰器模式的策略模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
var validate = function(){
var validator = new Validator();
var form = {
username: 'luchen'
}
validator.add(form.username,
[{
strategy: 'noEmptyName',
errorMsg: '用户名不能为空'
},
{
strategy: 'minLength:7',
errorMsg: '用户名长度不能小于6位'
}]);
var result = validator.start();
return result;
}
// validate()
/**
* 装饰器模式
*/
Function.prototype.before = function(beforeFn){
var fn = this;
return function () {
//装饰器添加validate的before判断
if(!beforeFn.apply(this,arguments)) return;
else
return fn.apply(this,arguments)
}
}
var submit = function(){
//ajax
console.log('----ajax----');
};
submit = submit.before(validate);
//调用
submit()

【summary】策略模式考虑用对象简便分配策略,来代替switch及耦合,case还有 计算奖金、缓动动画和表单校验三个例子。