本文源于本人关于《JavaScript设计模式与开发实践》(曾探著)的阅读总结。想详细了解具体内容建议阅读该书。
1. 闭包
首先我们需要了解变量的作用域以及变量的生存周期。
1.1 变量的作用域
- 在函数中声明的变量作用域 —— 局部变量。
- 未在任何函数中声明的变量 —— 全局变量。
- 如果在函数中未用var等声明变量,这个变量就会变成全局变量。
- 函数外不能访问函数内的变量,函数内可以访问函数外的变量。
var a = 1;
var func = function(){
var b = 2;
var func2 = function(){
var c = 3;
console.log(b); // 2
console.log(a); // 1
}
console.log(c); // undefined
}
func();
console.log(b); // undefined
1.2 变量的生存周期
对于存在函数内用var声明的局部变量而言,当函数推出时,这些局部变量就失去了它们的价值,它们都会随着函数调用的结束而被销毁。
现在看看这段代码:
var func = function() {
var a = 0;
return function(){
console.log(a++);
}
}
var f = func();
f() // 0
f() // 1
f() // 2
f() // 3
跟我们之前的推论相反,函数退出后,局部变量a并没有消失。这是因为执行f=func()时,f返回了一个匿名函数的引用,它可以访问到func被调用时产生的环境,而局部变量a一直处于这个环境里。既然局部变量所在环境还能被外界访问,这个变量就有了不被销魂的理由。在这里产生了一个闭包结构。
var Type = (function () {
var Type = {};
for (var i = 0, type; type = ['String', 'Number', 'Array'][i++];) {
(function (type) {
Type['is' + type] = function (obj) {
return Object.prototype.toString.call(obj) === '[object ' + type + ']';
}
})(type)
}
return Type;
})();
console.log(Type.isArray([]));
console.log(Type.isString(''));
利用闭包保存每次传入的type。
1.3 闭包作用
封装变量
计算阶乘:
var mult = function () {
var a = 1;
for (var i = 0, l = arguments.length; i < l; i++) {
a = a * arguments[i];
}
return a;
}
console.log(mult(1, 2, 3, 4, 5, 5, 5));
var mult_closure = (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;
}
})()
t1 = new Date();
console.log(mult_closure(1, 2, 3, 4, 5, 5, 5));
console.log(new Date() - t1); // 花费4ms
t2 = new Date();
console.log(mult_closure(1, 2, 3, 4, 5, 5, 5));
console.log(new Date() - t2); // 花费0ms
第二个用了闭包封装了缓存,当计算相同参数时,可以直接返回结果。
闭包合面向对象设计
闭包能够实现的,面向对象也可以实现,反之依然。
闭包:
var extent = function(){
var value = 0;
return {
call: function(){
console.log(value++);
}
}
}
var extent = extent();
extent.call(); // 0
extent.call(); // 1
extent.call(); // 2
面向对象:
var extent = {
value: 0,
call: function(){
console.log(this.value++);
}
}
extent.call(); // 0
extent.call(); // 1
extent.call(); // 2
闭包实现命令模式
命令模式意图把请求封装为对象, 从而分离请求的发起者和请求的接受者。在命令被执行前预先植入命令的接收者。
var Tv = {
open: function () {
console.log('open');
},
close: function () {
console.log('close');
}
}
var createCommand = function (receiver) {
var execute = function () {
receiver.open();
},
undo = function () {
receiver.close();
}
return {
execute: execute,
undo: undo
}
}
var setCommond = function (commond) {
document.getElementById('execute').onclick = commond.execute;
document.getElementById('undo').onclick = commond.undo;
}
setCommond(createCommand(Tv));
把命令封存在了createCommond中。
高阶函数
- 函数可以作为参数被传递
- 函数可以作为返回值输出
函数作为参数传递
- 回调函数
var getUserInfo = function(userId, callback){
$.ajax('http://xxx.com/getUserInfo?' + userId, function({
if(typeof callback === 'function'){
callback(data)
}
})
}
getUserInfo(123, function(data){
console.log(data.userName);
})
该例子和第一章的一致, 把变化的地方抽离了出来,以回调函数的形式传入。
- Array.prototype.sort:接受一个函数作为参数。这个函数里面封装了数组元素的排序规则。我们目的对数组进行排序,这是不变的部分,而使用什么规则去排序是可变的部分。
函数作为返回值输出
- 判断数据类型:
var Type = (function () {
var Type = {};
for (var i = 0, type; type = ['String', 'Number', 'Array'][i++];) {
(function (type) {
Type['is' + type] = function (obj) {
return Object.prototype.toString.call(obj) === '[object ' + type + ']';
}
})(type)
}
return Type;
})();
console.log(Type.isArray([]));
console.log(Type.isString(''));
这个函数把返回的函数都赋予给了Type对象,返回的都是函数,故也为高阶函数。
- getSingle:单例模式
var getSingle = function (fn) {
var ret;
return function () {
return ret || (ret = fn.apply(this, arguments));
}
}
var getObj = getSingle(function () {
return {};
})
var obj1 = getObj();
var obj2 = getObj();
console.log(obj1 === obj2); // true
既把函数作为了参数也作为了返回值,利用了闭包保存了一个单一不变的饮用,如果存在引用则直接返回,不存在才调用函数创建单例。
高阶函数实现AOP
AOP指面向切面编程,主要作用把一些跟核心业务逻辑模块无关的功能抽离出来,再通过“动态织入”的方式渗入业务模块中。通常js中实现AOP都是只把一个函数织入到另一个函数之中,实现方法之一:
Function.prototype.before = function (fn) {
var _self = this; // 保存原函数的引用
return function () { // 返回了包含原函数和新函数的代理函数
fn.apply(this, arguments);
return _self.apply(this, arguments); // 执行原函数
}
}
Function.prototype.after = function (fn) {
var _self = this; // 保存原函数的引用
return function () {
var ret = _self.apply(this, arguments);
fn.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
其他应用
- currying:柯里化,函数首先会接受一些参数,接受了参数并不会马上求值,而是继续返回另一个函数,待真正需要求值时,之前传入的所有参数都会被计算。
function curring(fn) {
var args = []; // 保存不计算的值
return function () {
if (arguments.length === 0) {
return fn.apply(this, args);
} else {
[].push.apply(args, arguments);
}
}
}
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 = curring(cost);
cost(100);
cost(200);
cost(300);
console.log(cost()); // 600
- uncurrying:我们通常可以使用call和apply去借用其他对象的方法, 但是有没有办法把泛化this的过程提取出来呢?以下代码是uncurrying的实现方式之一:
Function.prototype.uncurrying = function () {
var self = this;
return function () {
var obj = Array.prototype.shift.call(arguments);
return self.apply(obj, arguments);
}
}
for (var i = 0, fn, ary = ['push', 'shift', 'forEach']; fn = ary[i++];) {
Array[fn] = Array.prototype[fn].uncurrying();
};
var obj = {
length: 3,
0: 1,
1: 2,
2: 3
};
Array.push(obj ,4);
console.log(obj.length); // 4
通过uncurrying的方式,Array的push就变成了一个普通的push函数,这样push函数的作用也与Array的效果一样。同样不仅仅局限于操作array对象,其他对象也可以。
- 函数节流:函数被频繁调用 ,严重影响性能:
- window.onresize
- mousemove事件
- 上传进度
节流原理:比如window.onresize,我们在改变窗口大小时,打印窗口大小的工作1s进行了10次,而我们实际上只需要2次或者3次,这就需要我们按时间段来忽略掉一些事件请求。
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; // 定时器还在 说明还在上一次执行后的暂停时间中
} else {
timer = setTimeout(function () {
clearTimeout(timer);
timer = null;
_self.apply(_me, args);
}, interval || 500);
}
}
}
window.onresize = throttle(function () {
console.log(1);
}, 500);
- 分时函数: 比如qq渲染好友列表,一次性渲染1000个好友会让浏览器吃不消,故可以改为每隔200毫秒创建8个节点。
var timeChunk = function (ary, fn, count) {
var obj, t;
var len = ary.length;
var start = function () {
for (var i = 0; i < Math.min(count || 1, ary.length); i++) {
var obj = ary.shift();
fn(obj);
}
}
return function () {
t = setInterval(function () {
if (ary.length === 0) {
return clearInterval(t);
}
start();
}, 200);
};
};
var ary = [];
for (var i = 1; i <= 1000; i++) {
ary.push(i);
}
var renderFriendList = timeChunk(ary, function (n) {
var div = document.createElement('div');
div.innerHTML = n;
document.body.appendChild(div);
}, 8)
renderFriendList();
- 惰性加载函数:比如判断浏览器类型而采用不同的添加事件方法,如果每次执行添加事件时都要判断,那么效率很低,因为毕竟判断过了一次之后,代码一直都是在相同的环境中运行。
- 解决办法:提前判断浏览器类型,并选定使用的方法。
- 缺点:如果我们整个业务下来,并没有用到事件添加函数,那么这次计算就白计算了。
- 最终:调用了 事件添加函数后,才进行第一次判断,并且利用这个判断的返回值,以后不再继续做判断。
var addEvent = function (elem, type, handler) {
if (window.addEventListener) {
addEvent = function (elem, type, handler) {
elem.addEventListener(type, handler, false);
}
} else if (window.attachEvent) {
addEvent = function (elem, type, handler) {
elem.attachEvent('on' + type, handler);
}
}
addEvent(elem, type, handler);
}
第一次调用后,根据浏览器类型选定一个固定的事件添加函数赋值给addEvent。 之后就一直使用该方法调用,如果整个流程不使用到事添加函数,则不发生判断。