本文源于本人關(guān)于《JavaScript設(shè)計(jì)模式與開發(fā)實(shí)踐》(曾探著)的閱讀總結(jié)。想詳細(xì)了解具體內(nèi)容建議閱讀該書。
1. 閉包
首先我們需要了解變量的作用域以及變量的生存周期。
1.1 變量的作用域
- 在函數(shù)中聲明的變量作用域 —— 局部變量。
- 未在任何函數(shù)中聲明的變量 —— 全局變量。
- 如果在函數(shù)中未用var等聲明變量,這個變量就會變成全局變量。
- 函數(shù)外不能訪問函數(shù)內(nèi)的變量,函數(shù)內(nèi)可以訪問函數(shù)外的變量。
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 變量的生存周期
對于存在函數(shù)內(nèi)用var聲明的局部變量而言,當(dāng)函數(shù)推出時,這些局部變量就失去了它們的價值,它們都會隨著函數(shù)調(diào)用的結(jié)束而被銷毀。
現(xiàn)在看看這段代碼:
var func = function() {
var a = 0;
return function(){
console.log(a++);
}
}
var f = func();
f() // 0
f() // 1
f() // 2
f() // 3
跟我們之前的推論相反,函數(shù)退出后,局部變量a并沒有消失。這是因?yàn)閳?zhí)行f=func()時,f返回了一個匿名函數(shù)的引用,它可以訪問到func被調(diào)用時產(chǎn)生的環(huán)境,而局部變量a一直處于這個環(huán)境里。既然局部變量所在環(huán)境還能被外界訪問,這個變量就有了不被銷魂的理由。在這里產(chǎn)生了一個閉包結(jié)構(gòu)。
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 閉包作用
封裝變量
計(jì)算階乘:
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); // 花費(fèi)4ms
t2 = new Date();
console.log(mult_closure(1, 2, 3, 4, 5, 5, 5));
console.log(new Date() - t2); // 花費(fèi)0ms
第二個用了閉包封裝了緩存,當(dāng)計(jì)算相同參數(shù)時,可以直接返回結(jié)果。
閉包合面向?qū)ο笤O(shè)計(jì)
閉包能夠?qū)崿F(xiàn)的,面向?qū)ο笠部梢詫?shí)現(xiàn),反之依然。
閉包:
var extent = function(){
var value = 0;
return {
call: function(){
console.log(value++);
}
}
}
var extent = extent();
extent.call(); // 0
extent.call(); // 1
extent.call(); // 2
面向?qū)ο螅?/p>
var extent = {
value: 0,
call: function(){
console.log(this.value++);
}
}
extent.call(); // 0
extent.call(); // 1
extent.call(); // 2
閉包實(shí)現(xiàn)命令模式
命令模式意圖把請求封裝為對象, 從而分離請求的發(fā)起者和請求的接受者。在命令被執(zhí)行前預(yù)先植入命令的接收者。
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中。
高階函數(shù)
- 函數(shù)可以作為參數(shù)被傳遞
- 函數(shù)可以作為返回值輸出
函數(shù)作為參數(shù)傳遞
- 回調(diào)函數(shù)
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);
})
該例子和第一章的一致, 把變化的地方抽離了出來,以回調(diào)函數(shù)的形式傳入。
- Array.prototype.sort:接受一個函數(shù)作為參數(shù)。這個函數(shù)里面封裝了數(shù)組元素的排序規(guī)則。我們目的對數(shù)組進(jìn)行排序,這是不變的部分,而使用什么規(guī)則去排序是可變的部分。
函數(shù)作為返回值輸出
- 判斷數(shù)據(jù)類型:
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(''));
這個函數(shù)把返回的函數(shù)都賦予給了Type對象,返回的都是函數(shù),故也為高階函數(shù)。
- 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
既把函數(shù)作為了參數(shù)也作為了返回值,利用了閉包保存了一個單一不變的飲用,如果存在引用則直接返回,不存在才調(diào)用函數(shù)創(chuàng)建單例。
高階函數(shù)實(shí)現(xiàn)AOP
AOP指面向切面編程,主要作用把一些跟核心業(yè)務(wù)邏輯模塊無關(guān)的功能抽離出來,再通過“動態(tài)織入”的方式滲入業(yè)務(wù)模塊中。通常js中實(shí)現(xiàn)AOP都是只把一個函數(shù)織入到另一個函數(shù)之中,實(shí)現(xiàn)方法之一:
Function.prototype.before = function (fn) {
var _self = this; // 保存原函數(shù)的引用
return function () { // 返回了包含原函數(shù)和新函數(shù)的代理函數(shù)
fn.apply(this, arguments);
return _self.apply(this, arguments); // 執(zhí)行原函數(shù)
}
}
Function.prototype.after = function (fn) {
var _self = this; // 保存原函數(shù)的引用
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
其他應(yīng)用
- currying:柯里化,函數(shù)首先會接受一些參數(shù),接受了參數(shù)并不會馬上求值,而是繼續(xù)返回另一個函數(shù),待真正需要求值時,之前傳入的所有參數(shù)都會被計(jì)算。
function curring(fn) {
var args = []; // 保存不計(jì)算的值
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:我們通??梢允褂胏all和apply去借用其他對象的方法, 但是有沒有辦法把泛化this的過程提取出來呢?以下代碼是uncurrying的實(shí)現(xiàn)方式之一:
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函數(shù),這樣push函數(shù)的作用也與Array的效果一樣。同樣不僅僅局限于操作array對象,其他對象也可以。
- 函數(shù)節(jié)流:函數(shù)被頻繁調(diào)用 ,嚴(yán)重影響性能:
- window.onresize
- mousemove事件
- 上傳進(jìn)度
節(jié)流原理:比如window.onresize,我們在改變窗口大小時,打印窗口大小的工作1s進(jìn)行了10次,而我們實(shí)際上只需要2次或者3次,這就需要我們按時間段來忽略掉一些事件請求。
var throttle = function (fn, interval) {
var _self = fn, // 保存所需被延遲執(zhí)行的函數(shù)引用
timer, // 定時器
firstTime = true; // 是否第一次被調(diào)用
return function () {
var args = arguments,
_me = this;
if (firstTime) {
_self.apply(_me, args);
return firstTime = false;
}
if (timer) {
return false; // 定時器還在 說明還在上一次執(zhí)行后的暫停時間中
} else {
timer = setTimeout(function () {
clearTimeout(timer);
timer = null;
_self.apply(_me, args);
}, interval || 500);
}
}
}
window.onresize = throttle(function () {
console.log(1);
}, 500);
- 分時函數(shù): 比如qq渲染好友列表,一次性渲染1000個好友會讓瀏覽器吃不消,故可以改為每隔200毫秒創(chuàng)建8個節(jié)點(diǎn)。
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();
- 惰性加載函數(shù):比如判斷瀏覽器類型而采用不同的添加事件方法,如果每次執(zhí)行添加事件時都要判斷,那么效率很低,因?yàn)楫吘古袛噙^了一次之后,代碼一直都是在相同的環(huán)境中運(yùn)行。
- 解決辦法:提前判斷瀏覽器類型,并選定使用的方法。
- 缺點(diǎn):如果我們整個業(yè)務(wù)下來,并沒有用到事件添加函數(shù),那么這次計(jì)算就白計(jì)算了。
- 最終:調(diào)用了 事件添加函數(shù)后,才進(jìn)行第一次判斷,并且利用這個判斷的返回值,以后不再繼續(xù)做判斷。
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);
}
第一次調(diào)用后,根據(jù)瀏覽器類型選定一個固定的事件添加函數(shù)賦值給addEvent。 之后就一直使用該方法調(diào)用,如果整個流程不使用到事添加函數(shù),則不發(fā)生判斷。