《JavaScript设计模式与开发实践》前言

119 阅读11分钟

序章

了解到本书结构,分为三大部分

  1. JavaScript面向对象的函数式编程方面的知识(封装,继承,多态,原型,原型继承等)
  2. 核心部分,由浅到深的16个设计模式
  3. 面向对象的设计原则及其在设计模式中的体现

本次主要阅读前两个部分的内容

前言

阅读下来后,发现其实设计模式就是在开发过程中将某些场合中比较好的设计方法,给总结下来并且赋予一个好听又好记的名字

第一部分

面向对象的JavaScript

1.1动态语言类型和鸭子模型

  • JavaScript是一门典型的动态类型语言,当我们对一个变量赋值时,不需要考虑他的类型,可以尝试调用任何对象的任意方法,无需考虑是否被设计为拥有该方法,灵活性,(鸭子类型,面向接口编程)

1.2 多态

  • 含义:同一操作作用于不同的对象上面,可以产生不同的解释和不同的执行结果
  • 类型检查和多态:在静态语言类型中,如果要实现多态需要使用向上转型,设置一个抽象类来当一个超类,其他的类来继承这个超类
  • 继承:使用继承来得到多态效果,是让对象表现出多态性的最常用手段
  • JavaScript的多态:多态的思想时把”做什么“和”谁去做“分离开来,要实现这一点归根结底先要消除类型之间的耦合性,,而JavaScript的变量类型在运行期是可变的,这就意味着JavaScript对象的多态性是与生俱来的。

多态在面向对象程序设计中的作用

把过程化的条件分支语句转化为对象的多态性,从而消除这些条件分支语句

即你不必再向对象询问”你是什么类型“而后根据得到的答案调用对象的某个行为,你只管调用该行为就是了,其他的一切多态机制都会为你安排妥当

体现多态的例子 业务场景: 我们有几种渲染地图的方案,需要一个renderMap方法去渲染不同地图

  • 未使用多态的思想
const googleMap = {
	show: () => {
		console.log('开始渲染谷歌地图')
	}
}

const baiduMap = {
	show: () => {
		console.log('开始渲染百度地图')
	}
}
const renderMap = (type) => {
	if (type === 'google') {
		googleMap.show()
	} else if (type === 'baidu') {
		baiduMap.show()
	}
}

实现方式:在renderMap函数中去通过条件分支语句来判断使用哪一种方案来渲染,这是可以的,但是不够弹性,如果我们需要添加第三种,第四种地图,就必须改动renderMap函数,继续往里面堆砌更多的条件分支语句,那么这个渲染地图的方法就变得很脆弱了。

  • 使用了多态的思想
const renderMap = (map) => {
	if (map.show() instanceof Function) {
		map.show()
	}
}

renderMap(gooleMap) // 渲染谷歌地图
renderMap(baiduMap) // 渲染百度地图

// 如果要增加一个腾讯地图
const tencentMap = {
	show: () => {
		console.log('开始渲染腾讯地图')
	}
}

renderMap(tencentMap)

这样子去实现这个功能,renderMap函数就不会再改变了,就变得不再脆弱,对象的多态性提示我们:‘做什么’和‘怎么去做’是可以分开的,即便以后增加不同的地图,也不用去修改renderMap函数,这个例子体现了多态性。

设计模式与多态

绝大部分的设计模式都离不开多态的思想,例如:命令模式,组合模式,策略模式等。

1.3封装

封装的目的是将信息隐藏,封装使得对象之间的耦合变松散,对象之间只通过暴露的API 接口来通信。

通过封装变化的方式,把系统中稳定不变的部分和容易变化的部分隔离开来,在系统的演变过程中,我们只需要替换那些容易变化的部分,如果这些部分是已经封装好的,替换起来也相对容易。这可以最大程度地保证程序的稳定性和可扩展性

1.4 原型模式和原型继承

原型模式是一种设计模式,也是一种编程泛型,它构成了JavaScript 这门语言的根本。

function Person(name) {
  this.name = name;
}

Person.prototype.getName = function () {
  return this.name;
};

// var a = new Person("zelma");
// console.log(a.name);
// console.log(a.getName());
// console.log(a.__proto__ === Person.prototype);

// new操作符所做的事情

var objectFactory = function () {
  var obj = new Object(); // 从Object.prototype克隆一个空的对象
  Constructor = [].shift.call(arguments);
  console.log(Constructor);
  obj.__proto__ = Constructor.prototype;
  var ret = Constructor.apply(obj, arguments);
  return typeof ret === "object" ? ret : obj;
};
var a = objectFactory(Person, "zelma");
console.log(a.name);
console.log(a.getName());
console.log(a.__proto__ === Person.prototype); true
console.log(Person.prototype.__proto__ === Object.prototype); true

this、call 和apply

2.1 this

JavaScript 的this 总是指向一个对象,而具体指向哪个对象是在运行时基于函数的执行环境动态绑定的,而非函数被声明时的环境

2.1.1 this的指向

  • 作为对象的方法调用
  • 作为普通函数调用
var obj = {
  a: "zelma",
  getName: function () {
    return this.a;
  },
};
var a = "wanqian";
var getName = obj.getName;
console.log(obj.getName()); // zelma
//相当于window调用 var 定义的变量会挂载在window上,const let不会把name挂载在window上,所以会打印‘wanqian’
console.log(getName()); // wanqian  
  • 构造器调用
// new 操作符 new的那个构造函数 就执行new 出来的那个对象
// 如果返回的是一个对象,就指向那个对象
var myClass = function () {
  this.name = "seven";
  return {
    name: "anne",
  };
};
var obj = new myClass();
console.log(obj.name); // anne

// 如果return的不是一个对象或不return 就执行构造函数new出来的对象

var myClass1 = function () {
  this.name = "seven";
  return "anne";
};

var obj1 = new myClass1();
console.log(obj1.name); // seven
  • Function.prototype.call 或Function.prototype.apply 调用
var obj1 = { 
    name: 'sven', 
    getName: function(){ 
        return this.name; 
    } 
}; 
 
var obj2 = { 
    name: 'anne' 
}; 
 
console.log( obj1.getName() );     // 输出: sven 
console.log( obj1.getName.call( obj2 ) );    // 输出:anne

总结:最后是谁调用this就指向谁,apply``call调用的函数this指向第一个参数

2.2 call和apply

ES3给Function的原型定义了两个方法,call和apply,主要作用是改变函数this的指向

2.2.1 call和apply的区别

区别:传入参数形式的不同

  • apply 接受两个参数,第一个参数指定了函数体内 this 对象的指向,第二个参数为一个带下标的集合,这个集合可以为数组,也可以为类数组,apply 方法把这个集合中的元素作为参数传递给被调用的函数
  • call 传入的参数数量不固定,跟apply 相同的是,第一个参数也是代表函数体内的this 指向,从第二个参数开始往后,每个参数被依次传入函数
var fun = function (a,b,c) {
	console.log(this.name1, a, b, c)
}
var name1 = 'wanqian'
var obj = {
	name1: 'zelma'
}
fun.apply(obj, [1,2,3]) // zelma,1,2,3
fun.call(null, 4, 5, 6) // wanqian,4,5,6
  • 当第一个参数传入的为null是会默认指向window,如果是严格模式下会指向null

call手动实现

Function.prototype.callFun = function (context, ...args) {
  context = context || window;
  // console.log(this); // fun函数
  context.fn = this; // 把this赋值给传进来的context的fn
  // console.log(context); // 一个拥有fn函数的对象
  // console.log(args); // 扩展运算把arguments变成了一个参数数组
  const res = context.fn(...args);
  delete context.fn;
  return res;
};

apply手动实现

Function.prototype.applyFun = function (context, args) {
  context = context || window;
  context.fn = this;
  const res = context.fn(...args);
  delete context.fn;
  return res;
};

2.2.2 bind的实现

bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用

var obj = {
 name1: 'zelma'
}
var fun = function (a, b, c) {
	console.log(this.name1, a, b, c)
}
fun = fun.bind(obj, 1, 2)
fun('bind') // zelma,1,2,bind

手动实现bind

Function.prototype.bindFun = function () {
  var self = this;
  var context = [].shift.apply(arguments); // 获取第一个参数
  var args = [].slice.apply(arguments); // 剩余参数转换为数组
  return function () {
    // 传入的数据的arguments
    const newArgs = Array.from(arguments);
    // 返回一个函数,并把初始化的值和之后的值组合在一起
    return self.apply(context, [...args, ...newArgs]);
  };
};

2.2.3 借用其他对象的方法

在操作arguments 的时候,我们经常非常频繁地找Array.prototype 对象借用方法 想把 arguments 转成真正的数组的时候,可以借用 Array.prototype.slice 方法;想截去 arguments 列表中的头一个元素时,又可以借用 Array.prototype.shift 方法

[].shift.apply(arguments)
[].slice.apply(arguments)

3 闭包和高阶函数

3.1闭包

闭包和变量的作用域和生存周期有关

  • 变量分为全局变量和局部变量,作用域分为全局作用域和函数作用域
  • 对于全局变量生存周期是永久的,除非我们主动销毁这个全局变量
  • 对于局部变量,当退出函数时,这些局部变量失去了他们的价值,它们都会随着函数调用的结束而被销毁

当内部函数访问外部函数的作用域,导致外部函数销毁时,这个内部函数还在使用外部函数的局部变量,局部变量就不会被销毁,这样子就形成了一个闭包

var fun = function () {
  var a = 1;
  return function () {
    a++;
    console.log(a);
  };
};
var f = fun();
f() // 2
f() // 3
f() // 4
f() // 5

当执行var f = fun();时,f 返回了一个匿名函数 (匿名函数也叫lambda函数,是指一类无需定义标识符(函数名)的函数或子程序。通俗地说就是没有名字的函数)的引用,它可以访问到fun()被调用时产生的环境,而局部变量a 一直处在这个环境里。既然局部变量所在的环境还能被外界访问,这个局部变量就有了不被销毁的理由。在这里产生了一个闭包结构 for循环闭包问题

for (var i = 0; i < 5; i++) {
    setTimeout(() => {
	    // 打印5个5,使用var是全局变量会覆盖,1s后i变成了5,之前的i也会是5;
      console.log(i) 
    }, 1000);
}
for (var i = 0; i < 5; i++) {
  (function (j) {
    setTimeout(() => {
	  // 打印0-5,将i值传进来,使用闭包给每一个定时器都创建了自身的作用域中的i值
      console.log(j);
    }, 1000);
  })(i);
}

3.1.1 闭包的作用

1.封装变量 闭包可以帮助把一些不需要暴露在全局的变量封装成“私有变量” 2.延续局部变量的寿命

3.1.2 闭包与内存管理

局部变量本来应该在函数退出的时候被解除引用,但如果局部变量被封闭在闭包形成的环境 中,那么这个局部变量就能一直生存下去。跟闭包和内存泄露有关系的地方是,使用闭包的同时比较容易形成循环引用,如果闭包的作用域链中保存着一些DOM 节点,这时候就有可能造成内存泄露。

要解决循环引用带来的内存泄露问题,我们只需要把循环引用中的变量设为null 即可。将变量设置为 null 意味着切断变量与它此前引用的值之间的连接。当垃圾收集器下次运行时,就会删除这些值并回收它们占用的内存

3.2 高阶函数

高阶函数是指至少满足下列条件之一的函数。  函数可以作为参数被传递;  函数可以作为返回值输出

3.2.1 函数作为参数传递

  1. ajax请求中的回调函数
  2. Array.prototype.sort方法

3.2.2 函数作为返回值输出

判断数据类型函数

var Type = {};

for (var i = 0, type; (type = ["String", "Array", "Number"][i++]); ) {
  (function (type) {
    Type["is" + type] = function (obj) {
      return Object.prototype.toString.call(obj) === "[object " + type + "]";
    };
  })(type);
}

3.2.3 高阶函数实现AOP

AOP(面向切面编程)的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些跟业务逻辑无关的功能通常包括日志统计、安全控制、异常处理。等把这些功能抽离出来之后,再通过“动态织入”的方式掺入业务逻辑模块中。这样做的好处首先是可以保持业务逻辑模块的纯净和高内聚性,其次是可以很方便地复用日志统计等功能模块。

Function.prototype.before = function( beforefn ){ 
    var __self = this;    // 保存原函数的引用 
    return function(){    // 返回包含了原函数和新函数的"代理"函数 
        beforefn.apply( this, arguments );     // 执行新函数,修正this 
        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 和打印数字3 的两个函数通过AOP 的方式动态植入func 函数。控制台打印1,2,3

这种使用AOP 的方式来给函数添加职责,也是JavaScript 语言中一种非常特别和巧妙的装饰者模式实现

3.2.4 高阶函数的其他应用

  1. currying 函数柯里化 currying 又称部分求值。一个currying 的函数首先会接受一些参数,接受了这些参数之后,该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值。 效果:遍历本月每天的开销并求出它们的总和
var currying = function (fn) {
  var args = [];
  return function () {
    if (arguments.length === 0) {
      console.log(this);
      return fn.apply(this, args); // 通过apply方法解析args参数
    } else {
      [].push.apply(args, arguments);
      return arguments.callee;
    }
  };
};

var cost = (function () {
  console.log(this);
  var money = 0;
  return function () {
    for (var i = 0; i < arguments.length; i++) {
      money += arguments[i];
    }
    return money;
  };
})();

var cost = currying(cost);

// console.log(cost);

cost(100);
cost(200);
cost(300);

console.log(const()) // 600
  1. uncurrying 在 JavaScript 中,当我们调用对象的某个方法时,其实不用去关心该对象原本是否被设计为拥有这个方法 arguments使用数组方法
(function(){ 
    Array.prototype.push.call( arguments, 4 );    // arguments 借用Array.prototype.push 方法 
    console.log( arguments );      // 输出:[1, 2, 3, 4] 
})( 1, 2, 3 );

把泛化 this 的过程提取出来

Function.prototype.uncurrying = function () {
  var self = this; // self就是Array.prototype.push
  return function () {
    var obj = Array.prototype.shift.call(arguments);
    return self.apply(obj, arguments);
  };
};

直接使用uncurrying通用泛化this

var push = Array.prototype.push.uncurrying(); 
(function(){ 
    push( arguments, 4 ); 
    console.log( arguments );     // 输出:[1, 2, 3, 4] 
})( 1, 2, 3 );