本文系《JavaScript设计模式和开发实战》读书笔记
第一部分:基础知识
第一章 面向对象的JavaScript
1. 动态类型语言和鸭子类型
静态类型语言在编译时便已确定变量的类型,而动态类型语言的变量类型要到程序运行的时候,待变量被赋予某个值之后,才会具有某种类型。
int a = 1 //静态类型语言
var a = 1 //动态类型语言
鸭子类型: 只关注对象的行为,而不关注对象本身。
2.多态
多态的含义: 同一操作作用于不同的对象上面,可以产生不同的解释和不同的执行结果。
var makeSound = function( animal ) {
if( animal instanceof Duck ) {
console.log("嘎嘎嘎")
}else if( animal instanceof Chicken ) {
console.log("咯咯咯")
}
};
var Duck = function() {};
var Chicken = function() {};
makeSound( new Duck() );
makeSound( new Chicken() );
多态背后的思想是将“做什么”和“谁去做以及怎样去做”分离开来,也就是将“不变的事物”与“可能改变的事务”分离开来。
“一只麻雀在飞”或“一直喜鹊在飞” ---> “一只鸟在飞”
3.封装
JavaScript只能依赖变量的作用域来实现封装特性,而且只能模拟出public和private这两种封装性。
“透明” == “不可见”
4.原型模式和基于原型继承的JavaScript对象系统
在原型编程的思想中,类并不是必须的,对象未必需要从类中创建而来,一个对象是通过克隆另外一个对象所得到的。
原型编程规范至少包括以下基本规则:
- 所有的数据都是对象。
- 要得到一个对象,不是通过实例化类,而是找到一个对象作为原型并克隆它。
- 对象会记住它的原型。
- 如果对象无法响应某个请求,它会把这个请求委托给它自己的原型。
JavaScript中的根对象是Object.prototype对象。
function Person( name ){
this.name = name;
};
Person.prototype.getName = function() {
return this.name;
};
var a = new Person( 'sven' );
console.log( a.name ); //输出sven
console.log( a.getName() ); //输出:sven
console.log( Object.getPrototypeOf(a) === Person.prototype ); //输出:true
在这里Person并不是类,而是函数构造器,JavaScript的函数既可以作为普通函数被调用,也可以作为构造器被调用。
就JavaScript的真正实现来说,其实并不能说对象有原型,而只能说对象的构造器有原型。对于“对象把请求委托给它自己的原型”这句话,更好的说法是对象把请求委托给他的构造器的原型。
JavaScript给对象提供了一个名为 —proto— 的隐藏属性,某个对象的 —proto— 属性默认会指向它的构造器的原型对象,即{Constructor}.prototype。
可以通过Object.create( null )可以创造出没有原型的对象。
第二章 this, call 和 apply
1. this的指向
JavaScript的this总是指向一个对象,而具体指向哪个对象是在运行时基于函数的执行环境动态绑定的,而非函数被声明时的环境。
this的指向大致可以分为以下四种:
- 作为对象的方法调用
- 作为普通函数的调用
- 构造器调用
Function.prototype.call或Function.prototype.apply调用
//修正this
document.getElementById = (function(func) {
return function() {
return func.apply( document, arguments );
}
})( document.getElementById );
2. call和apply
2.1 call与apply的区别
Math.max.apply(null, [1, 2, 3, 4, 5])
2.2 call和apply的用途
- 改变
this的指向 Function.prototype.bind模拟实现
Function.prototype.bind = function() {
var self = this; // 保存原函数
context = [].shift.call(arguments); //需要绑定的this上下文
args = [].slice.call(arguments); // 剩余的参数转成数组
return function() { //返回一个新的函数
return self.apply( context, [].concat.call(args, [].slice.call(arguments)) )
// 执行新的函数的时候,会把之前传入的context当作新韩淑体内的this
// 并且组合两次分别传入的参数,作为新函数的参数
}
};
3. 借用其他对象的方法
3.1 借用构造函数 --- 实现类似继承的效果
var A = function( name ) {
this.name = name
}
var B = function() {
A.apply( this, arguments )
}
B.prototype.getName = function() {
return this.name
}
var b = new B('sven');
console.log( b.getName() ); //'sven'
3.2 借用Array.prototype对象上的方法
(function(){
Array.prototype.push.call( arguments, 3 );
console.log( arguments ); // [1,2,3]
})(1, 2)
借用Array.prototype.push方法使用需要满足两个条件:
- 对象本身要可以存取属性(number类型不行)
- 对象的
length属性要可读写(函数不行)
第三章 闭包与高阶函数
3.1 闭包
闭包的形成与 变量的作用域 以及 变量的生存周期 密切相关。
变量的生存周期:
var func = function() {
var a = 1; //退出函数化局部变量a将被销毁
alert ( a );
}
func();
var func = function() {
var a = 1;
return function() {
a++; // a会一直被保存下来
alert ( a );
}
}
var f = func();
f(); //输出:2
f(); //输出:3
f(); //输出:4
判断变量类型方法
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 )
};
Type.isArray( [] ); //输出:true
Type.isString( 'str' ); //输出:true
闭包的更多作用
- 封装变量(把一些不需要暴露在全局的变量封装成“私有变量”)
var mult = function() {
var a = 1;
for ( var i=0, l=arguments.length; i<l; i++){
a = a * arguments[i];
}
return a;
}
第一步 加入缓存机制之后:
var cache = {};
var mult = function() {
var args = Array.prototype.join.call( arguments, ',' );
if( cache[args] ){
return cache[ args ];
}
var a = 1;
for( var i=0; l=arguments.length; i<l; i++){
a = a * arguments[i];
}
return cache[ args ] = a;
};
alert ( mult(1,2,3) ); //输出:6
alert ( mult(1,2,3) ); //输出:6, 第二次不需要重新计算
第二步 将cache变量封闭在mult函数内:
var mult = (function(){
var cache = {};
return function(){
var args = Array.prototype.join.call( arguments, ',' );
if( args in cache ){
return cache[ args ];
}
var a = 1;
for( var i=0; l=arguments.length; i<l; i++){
a = a * arguments[i];
}
return cache[ args ] = a;
}
})()
第三步 提炼函数
var mult = (function(){
var cache = {};
var calculate = function(){ //封闭calculate函数
var a = 1;
for( var i=0; l=arguments.length; i<l; i++){
a = a * arguments[i];
}
return a;
}
return function(){
var args = Array.prototype.join.call( arguments, ',' );
if( args in cache ){
return cache[ args ];
}
return cache[ args ] = calculate.apply( null, arguments ); //传入null来代表某个对象
}
})()
- 延续局部变量的寿命
//img 对象经常用于进行数据上报
var report = function( src ){
var img = new Image();
img.src = src;
};
report( "http://xxx.com" );
以上数据上报存在数据丢失的现象,丢失数据的原因是img是report函数中的局部变量,当report函数的调用结束后,img局部变量随即被销毁,此时还来不及发送http请求。使用闭包解决请求丢失问题的方法:
var report = (function(){
var imgs = [];
return function( src ){
var img = new Image();
imgs.push( img );
img.src = src;
}
})();
闭包和面向对象设计
对象以方法的形式包含了过程,而闭包则是在过程中以环境的形式包含了数据。
3.2 高阶函数
高阶函数需要满足的条件:
- 函数可以作为参数传递
- 函数可以作为返回值输出
- 函数作为参数传递;
- 函数作为返回值输出;
- 高阶函数实现AOP(面向切面编程);
AOP(面向切面编程)的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些跟业务逻辑无关的功能通常包括日志统计,安全控制,异常处理等。把这些功能抽离出来之后,再通过“动态织入”的方式参入业务逻辑块中。好处:1.可以保持业务逻辑模块的纯净和高内聚性;2可以很方便地复用日志统计等模块。
动态织入,Function.prototype扩展
Function.prototype.before = function( beforefn ){
var _self = this; //保存原函数地引用
return function() { //返回包含了原函数和新函数的“代理”函数
before.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.currying(部分求值)
currying 描述: 一个currying的函数首先会接受一些函数,接受了这些寒素之后,该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值。
遍历本月每天的开销并求出他们的总和:
var currying = function( fn ){
var args = [];
return function(){
if( arguments.length === 0){
return fn.apply( this, args )
}else{
[].push.apply( args, arguments );
return arguments.callee;
//callee是对象的一个属性,可以用于引用该函数体内当前正在执行的函数
//此属性在ES5严格模式已删除
}
}
}
var cost = (function(){
var money = 0;
return function() {
for(var i = 0, l = arguments.length; i < l; i++){
money += arguments[ i ];
}
return money;
}
})
var cost = currying( cost ); //转化成currying函数
cost( 100 );
cost( 200 );
cost( 300 );
cost(); //600
2.uncurring(提取泛化this的过程)
鸭子类型思想:只关注对象行为,不关注对象本身。
Array.prototype上的方法可以操作任何对象。
Function.prototype.uncurrying = function () {
var self = this; // self此时是需要提取方法的对象
return function() {
var obj = Array.prototype.shift.call( arguments );
// arguments 对象的第一个元素被截去
return self.apply( obj, arguments );
};
};
- 函数节流
函数被频繁调用的场景:
- window.onresize事件
- mousemove事件
- 上传进度
代码实现:
var throttle = function( fn, interval ){
var _self = fn, //保存需要被延迟执行的函数的引用
timer, //定时器
firstTime = true; //是否第一次调用
return function () {
var args = arguments,
_me = this;
if( firstTime ){ //如果是第一次调用,不需要延迟执行
_self.apply(_me, args);
return firstTime = false;
}
if( timer ){ //如果定时器还在,说明前一次延迟执行还没有完成
return false
}
timer = setTimeout(function(){
clearTimeout(timer);
timer = null;
_self.apply(_me, args);
}, interval || 500) //延迟一段时间执行
};
};
window.onresize = throttle(function(){
console.log( 1 );
}, 500);
- 分时函数
5.惰性加载函数
第二部分:设计模式
第四章 单例模式
单例模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
4.1 JavaScript中的单例模式
全局变量不是单例模式,但在
JavaScript开发中,我们经常会把全局变量当成单例来使用。
尽量减少全局变量的使用:
- 使用命名空间
适当的使用命名空间,并不会杜绝全局变量,但可以减少全局变量的数量。
//动态创建命名空间
var MyApp = {};
MyApp.namespace = function( name ){
var parts = name.split( '.' );
var current = MyApp;
for( var i in parts ){
if( !current [ parts[i] ] ){
currents[ parts[ i ] ] = {};
}
current = currents[ parts[ i ] ];
}
}
MyApp.namespace( 'event' );
MyApp.namespace( 'dom.style' );
console.dir( MyApp );
<=>
var MyApp = {
event: {},
dom: {
style: {}
}
}
- 使用闭包封装私有变量
var user = (function(){
var _name = 'sven';
_age = 29;
return {
getUserInfo: function() {
return _name + '-' _age;
}
}
})
4.2 惰性单例(eg: QQ登录浮窗)
惰性单例是指在需要时才创建的对象实例。
4.3 通用的惰性单例
创建对象和管理单例的职责被分布在两个不同的方法中。
// fn为创建对象方法
var getSingle = function( fn ) {
var result;
return function() {
return result || ( result = fn.apply(this, arguments ) );
}
}
使用iframe实现跨域怎么做?
第五章 策略模式
将不变的部分和变化的部分隔开是每个设计模式的主题。
策略模式的思想: 定义一系列的算法,把它们各自封装成策略类,算法被封装在策略类内部的方法里。在客户对Context发起请求的时候,Context总是把请求委托给这些策略对象中间的某一个进行计算。
使用策略模式可以消除大量得条件分支语句
5.1 策略模式版本1 (模仿传统面向对象语音):
//计算规则
var performanceS = function(){};
performanceS.prototype.calculate = function( salary ){
return salary * 4;
}
var performanceA = function(){};
performanceS.prototype.calculate = function( salary ){
return salary * 3;
}
var performanceB = function(){};
performanceS.prototype.calculate = function( salary ){
return salary * 2;
}
//定义奖金类
var Bonus = function(){
this.salary = null; //工资
this.strategy = null; //策略对象
}
Bonus.prototype.setSalary = function( salary ){
this.salary = salary; //设置工资
}
Bonus.prototype.setStrategy = function( strategy ){
this.strategy = strategy; //设置策略对象
}
Bonus.prototype.getBonus = function() {
if (!this.strategy){
throw new Error('未设置strategy属性')
}
return this.strategy.calculate( this.salary ); //把计算操作委托给策略对象
}
//使用
var bonus = new Bonus();
bonus.setSalary( 1000 );
bonus.setStrategy( new performanceS() ); //设置策略对象
console.log( bonus.getBonus() ); //输出:4000
5.2 JavaScript版本的策略模式:
var strategies = {
"S": function( salary ){
return salary * 4;
},
"A": function( salary ){
return salary * 3;
},
"B": function( salary ){
return salary * 2;
}
}
var calculateBonus = function( level, salary ){
return strategies[ level ]( salary );
};
console.log( calculateBonus('S', 2000) ); //80000
console.log( calculateBonus('A', 1000) ); //30000
5.3 使用策略模式实现缓动动画(略):
5.4 使用策略模式实现表单验证:
i 不使用策略模式
var registerForm = document.getElementById( 'registerForm' );
registerForm.onsubmit = function() {
if (registerForm.userName.value === ''){
alert('用户名不能为空')
return false
}
if (registerForm.password.value.length < 6){
alert('密码长度不能少于6位')
return false
}
if ( !/(^1[3|5|8|][0-9]{9}$)/.test( registerForm.phoneNumber.value ){
alert('手机号码格式不正确')
return false
}
}
ii 使用策略模式
var strategies = {
isNonEmpty: function( value, errorMsg ){ //不为空
if( value === '' ){
return errorMsg;
}
},
minLength: function( value, length, errorMsg ){ //限制最小长度
if( value.length < length ){
return errorMsg;
}
},
isMobile: function( value, errorMsg ){
if ( !/(^1[3|5|8|][0-9]{9}$)/.test( value )){
return errorMsg;
}
}
}
var validataFunc = function() {
var validator = new Validator(); // 创建一个validator对象
/*****添加一些校验规则*****/
validator.add( registerForm.userName, 'isNonEmpty', '用户名不能为空');
validator.add( registerForm.password, 'minLength:6', '密码长度不能少于6位');
validator.add( registerForm.userName, 'isMobile', '手机号码格式不正确');
var errorMsg = validator.start(); //获得校验结果
return errorMsg; // 返回校验结果
}
var Validator = function() {
this.cache = [];
}
Validator.prototype.add = function( dom, rule, errorMsg ) {
var ary = rule.split( ':' ); // 把strategy和参数分开
this.cache.push(function(){ // 把校验的步骤用空函数包装起来,并且放入cache
var strategy = ary.shift(); // 用户挑选的Strategy
ary.unshift( dom.value ); // 把input的value添加进参数列表
ary.push( errorMsg ); // 把errorMsg添加进参数列表
return strategies[ strategy ].apply( dom, ary );
})
}
Validator.prototype.start = function(){
for( var i = 0, validatorFunc; validatorFunc = this.cache[ i++ ];){
var msg = validatorFunc(); // 开始校验,并取得校验后的返回信息
if(msg){
return msg;
}
}
}
拓展: 给文本框输入添加多种验证规则。
疑问:目前这样只报一个错,如何实现点击提交时,错误全部显示出来?(类似饿了么表单验证组件)
第六章 代理模式
6.1 保护代理和虚拟代理
客户 ---> 代理 ---> 本体
代理可以帮助本体过滤掉一些请求,这被称为保护代理。
把一些开销很大的对象,延迟到真正需要它的时候才由代理去创建,这被称为虚拟代理。
6.2 虚拟代理实现图片预加载
图片过大时或者网络不佳,图片的位置会出现一片空白。那么常见的做法就是先用一张loading图片占位,然后用异步的方式加载图片,等图片好了再填充到img节点里,这种场景非常适用虚拟代理。
var myImage = (function(){
var imgNode = document.createElement( 'img' );
document.body.appendChild( imgNode );
return {
setSrc: function( src ){
imgNode.src = src
}
}
})();
var proxyImage = (function(){
var img = new Image;
img.onload = function(){ //监听异步加载完成
myImage.setSrc( this.src );
}
return {
setSrc: function( src ){
myImage.setSrc( 'file:// /C:/Users/XX/XX/loading.gif' ); //先用该图片代替
img.src = src; //开始加载图片
}
}
})()
proxyImage.setSrc('file:// /C:/Users/XX/XX/need.jpg'')
6.3 代理的意义
单一职责原则
6.4 代理和本体接口的一致性(见94)
6.5 虚拟代理合并HTTP请求
解决方案:可以通过一个代理函数proxySynchronousFile来收集一段hijack之内的请求,最后一次性发给服务器。
var synchronousFile = function( id ){
console.log( '开始同步文件, id为:' + id );
};
var proxySynchronousFile = (function(){
var cache = [], //保存一段时间内需要同步的ID
timer; //定时器
return function( id ){
cache.push(id);
if( timer ){ //保证不会覆盖异价启动的定时器
return;
}
}
timer = setTimeOut(function(){
synchronousFile( cache.join(',') ); //2秒后向本地发送需要同步的集合
clearTimeout( timer ); //清空定时器
timer = null;
cache.length = 0; //清空ID集合
}, 2000)
})()
var checkbox = document.getElementByTagName( 'input' );
for( var i=0; c; c=checkbox[i++] ){
c.onclick = function(){
if( this.checked === true ){
proxySynchronousFile(this.id)
}
}
}
6.6 虚拟代理在惰性加载中的应用(见p99)
6.7 缓存代理
缓存代理可以为一些开销大的运算结果提供暂时的存储,在下次运算时。如果传递进来的参数跟之前一致,则可以直接返回前面存储的运算结果。
var mult = function() {
var a = 1;
for(var i=0; l=arguments.length; i++){
a = a * arguments[i]
}
return a
}
//加入代理缓存
var proxyMult = (function(){
var cache = {};
return function(){
var args = Array.prototype.join.call( arguments, ',' );
if( args in cache ){
return cache[ args ];
}
return cache[ args ] = mult.apply( this, arguments );
}
})()
proxyMult( 1, 2, 3, 4 ); //24
proxyMult( 1, 2, 3, 4 ); //24
6.8 用高阶函数动态创建代理
var mult = function() {
var a = 1;
for(var i=0; l=arguments.length; i++){
a = a * arguments[i]
}
return a
}
var plus = function() {
var a = 0;
for(var i=0; l=arguments.length; i++){
a = a + arguments[i]
}
return a
}
var createProxyFactory = function( fn ){
var cache = {};
return function(){
var args = Array.prototype.join.call( arguments, ',' );
if( args in cache ){
return cache[ args ];
}
return cache[ args ] = fn.apply( this, arguments );
}
}
var proxyMult = createProxyFactory( mult );
var proxyPlus = createProxyFactory( plus );
proxyMult(1,2,3,4); //24
proxyMult(1,2,3,4); //24
proxyPlus(1,2,3,4); //10
proxyPlus(1,2,3,4); //10
第七章 迭代器模式
迭代器模式是指提供一种方法顺序访问一个聚合对象中的各个元素,二又不需要暴露该对象的内部表示。
7.1 内部迭代器
内部迭代器在调用的时候非常方便,外界不用担心迭代器的内部实现,跟迭代器的交互也仅仅是一次初始调用,这也是内部迭代器的缺点。
var each = ...
7.2 外部迭代器
外部迭代器必须显式的请求下一个元素。
var Iterator = function( obj ){
var current = 0;
}
...
7.3 迭代类数组对象和字面量对象
jquery中的$.each实现方法
js中的forEach,for...in for...of实现方法
7.4 应用实例(上传组件的遍历)
第八章 发布-订阅模式(观察者模式)
定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。
8.1 真实例子---网站登录
需要登录之后设置各模块的头像以及刷新有关模块
使用发布-订阅模式之前伪代码:
login.succ(function(data){
header.setAvatar( data.avatar ); //设置header模块的头像
nav.setAvatar( data.avatar ); //设置导航模块的头像
message.refresh(); //刷新消息列表
cart.refresh(); //刷新购物车列表
address.refresh(); //增加刷新收货地址列表(这是一种令人抓狂的添加方法)
})
使用发布-订阅模式之后伪代码:
$.ajax( 'http://xxx.com?login', function(data){
login.trigger('loginSucc', data); //登录成功发布登录成功的消息
})
//header模块监听发布对象设置头像
var header = (function(){
login.listen( 'loginSucc', function( data ){
header.setAvatar( data.avatar )
});
return {
setAvatar: function( data ){
console.log('设置header模块的头像')
}
}
})
//navr模块监听发布对象设置头像
(...)
//监听发布对象刷新收货地址列表
var address = (function(){
login.listen( 'loginSucc', function( data ){
header.setAvatar( data.avatar )
});
return {
refresh: function( data ) {
console.log('刷新收货地址列表')
}
}
})