<body>
<div id="box"class="clearfix"></div>
<script src="https://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script>
<script src="./index.js"></script>
</body>
// index.js
// 回調函數
// 異步請求
let getInfo = function(keywords, callback) {
$.ajax({
url: 'http://musicapi.leanapp.cn/search', // 以網易雲音樂為例
data: {
keywords
},
success: function(res) {
callback && callback(res.result.songs);
}
})
};
$('#btn').on('click', function() {
let keywords = $(this).prev().val();
$('#loading').show();
getInfo(keywords, getData);
});
// 加入回車
$("#search_inp").on('keyup', function(e){
if(e.keyCode === 13) {
$('#loading').show();
getInfo(this.value, getData);
}
});
function getData(data) {
if(data && data.length) {
let html = render(data);
// 初始化Dom結構
initDom(html, function(wrap) {
play(wrap);
});
}
}
// 格式化時間戳
function formatDuration(duration) {
duration = parseInt(duration / 1000); // 轉換成秒
let hour = Math.floor(duration / 60/ 60),
min = Math.floor((duration % 3600) / 60),
sec = duration % 60,
result = '';
result += `${fillIn(min)}:${fillIn(sec)}`;
return result;
}
function fillIn(n) {
return n < 10? '0'+ n : ''+ n;
}
let initDom = function(tmp, callback) {
$('.item').remove();
$('#loading').hide();
$('#box').append(tmp);
// 這裡因為不知道dom合適才會被完全插入到頁面中
// 所以用callback當參數,等dom插入後再執行callback
callback && callback(box);
};
let render = function(data) {
let template = '';
let set= newSet(data);
data = [...set]; // 可以利用Set去做下簡單的去重,可忽略這步
for(let i = 0; i < 8; i++) {
let item = data[i];
let name = item.name;
let singer = item.artists[0].name;
let pic = item.album.picUrl;
let time = formatDuration(item.duration);
template += `
<div class="item">
<div class="pic" data-time="${time}">
<span></span>
<img src="${pic}"/>
</div>
<h4>${name}</h4>
<p>${singer}</p>
<audio src="http://music.163.com/song/media/outer/url?id=${item.id}.mp3"></audio>
</div>`;
}
return template;
};
let play = function(wrap) {
wrap = $(wrap);
wrap.on('click', '.item', function() {
let self = $(this),
$audio = self.find('audio'),
$allAudio = wrap.find('audio');
for(let i = 0; i < $allAudio.length; i++) {
$allAudio[i].pause();
}
$audio[0].play();
self.addClass('play').siblings('.item').removeClass('play');
});
};
按照上面的代碼啪啪啪,就會得到下面這樣的效果,一起來看下吧不過依然感謝網易雲音樂提供的API接口,讓我們聆聽美妙好音樂函數作為返回值輸出親們,函數作為返回值輸出的應用場景那就太多了,這也體現了函數式編程的思想。其實從閉包的例子中我們就已經看到了關於高階函數的相關內容了,哈哈還記得在我們去判斷數據類型的時候,我們都是通過Object.prototype.toString來計算的。每個數據類型之間只是'[object XXX]'不一樣罷了。所以在我們寫類型判斷的時候,一般都是將參數傳入函數中,這裡我簡單寫一下實現,咱們先來看看。function isType(type) {
returnfunction(obj) {
returnObject.prototype.toString.call(obj) === `[object ${type}]
}
}
const isArray = isType('Array');
const isString = isType('String');
console.log(isArray([1, 2, [3,4]]); // true
console.log(isString({}); // false
其實上面實現的isType函數,也屬於偏函數的範疇,偏函數實際上是返回了一個包含預處理參數的新函數,以便之後可以調用。另外還有一種叫做預置函數,它的實現原理也很簡單,當達到條件時再執行回調函數。function after(time, cb) {
returnfunction() {
if(--time === 0) {
cb();
}
}
}
// 舉個慄子吧,吃飯的時候,我很能吃,吃了三碗才能吃飽
let eat = after(3, function() {
console.log('吃飽了');
});
eat();
eat();
eat();
上面的eat函數只有執行3次的時候才會輸出'吃飽了',還是比較形象的。這種預置函數也是js中巧妙的裝飾者模式的實現,裝飾者模式在實際開發中也非常有用,再以後的歲月裡我也會好好研究之後分享給大家的。// 這裡我們創建了一個單例模式
let single = function(fn) {
let ret;
returnfunction() {
console.log(ret); // render一次undefined,render二次true,render三次true
// 所以之後每次都執行ret,就不會再次綁定了
return ret || (ret = fn.apply(this, arguments));
}
};
let bindEvent = single(function() {
// 雖然下面的renders函數執行3次,bindEvent也執行了3次
// 但是根據單例模式的特點,函數在被第一次調用後,之後就不再調用了
document.getElementById('box').onclick = function() {
alert('click');
}
returntrue;
});
let renders = function() {
console.log('渲染');
bindEvent();
}
renders();
renders();
renders();
這個高階函數的慄子,可以說一石二鳥啊,既把函數當做參數傳遞了,又把函數當返回值輸出了。單例模式也是一種非常實用的設計模式,在以後的文章中也會針對這些設計模式去分析的,敬請期待,哈哈,下面再看看高階函數還有哪些用途。其他應用函數柯裡化柯裡化又稱部分求值,柯裡化函數會接收一些參數,然後不會立即求值,而是繼續返回一個新函數,將傳入的參數通過閉包的形式保存,等到被真正求值的時候,再一次性把所有傳入的參數進行求值。還能闡述的更簡單嗎?在一個函數中填充幾個參數,然後再返回一個新函數,最後進行求值,沒了,是不是說的簡單了。// 普通函數
function add(x,y){
return x + y;
}
add(3,4); // 7
// 實現了柯裡化的函數
// 接收參數,返回新函數,把參數傳給新函數使用,最後求值
let add = function(x){
returnfunction(y){
return x + y;
}
};
add(3)(4); // 7
以上代碼非常簡單,只是起個引導的作用。下面我們來寫一個通用的柯裡化函數function curry(fn) {
let slice = Array.prototype.slice, // 將slice緩存起來
args = slice.call(arguments, 1); // 這裡將arguments轉成數組並保存
returnfunction() {
// 將新舊的參數拼接起來
let newArgs = args.concat(slice.call(arguments));
return fn.apply(null, newArgs); // 返回執行的fn並傳遞最新的參數
}
}
實現了通用的柯裡化函數,了不起啊,各位很了不起啊。不過這還不夠,我們還可以利用ES6再來實現一下,請看如下代碼:// ES6版的柯裡化函數
function curry(fn) {
const g = (...allArgs) => allArgs.length >= fn.length ?
fn(...allArgs) :
(...args) => g(...allArgs, ...args)
return g;
}
// 測試用例
const foo = curry((a, b, c, d) => {
console.log(a, b, c, d);
});
foo(1)(2)(3)(4); // 1 2 3 4
const f = foo(1)(2)(3);
f(5); // 1 2 3 5
不過大家有沒有發現我們在ES5中使用的bind方法,其實也利用了柯裡化的思想,那麼再來看一下下。let obj = {
songs: '以父之名'
};
function fn() {
console.log(this.songs);
}
let songs = fn.bind(obj);
songs(); // '以父之名'
為什麼這麼說?這也看不出什麼頭緒啊,別捉急,再來看一下bind的實現原理。Function.prototype.bind = function(context) {
let self = this,
slice = Array.prototype.slice,
args = slice.call(arguments);
returnfunction() {
return self.apply(context, args.slice(1));
}
};
是不是似曾相識,是不是,是不是,有種師出同門的趕腳了啊。反柯裡化啥?反柯裡化,剛剛被柯裡化弄的手舞足蹈的,現在又出現了個反柯裡化,有木有搞錯啊!那麼反柯裡化是什麼呢?簡而言之就是函數的借用,天下函數(方法)大家用。比如,一個對象未必只能使用它自身的方法,也可以去借用原本不屬於它的方法,要實現這點似乎就很簡單了,因為call和apply就可以完成這個任務。(function() {
// arguments就借用了數組的push方法
let result = Array.prototype.slice.call(arguments);
console.log(result); // [1, 2, 3, 'hi']
})(1, 2, 3, 'hi');
Math.max.apply(null, [1,5,10]); // 數組借用了Math.max方法
從以上代碼中看出來了,大家都是相親相愛的一家人。利用call和apply改變了this指向,方法中用到的this再也不局限在原來指定的對象上了,加以泛化後得到更廣的適用性。反柯裡化的話題是由我們親愛的js之父發表的,我們來從實際例子中去看一下它的作用。let slice = Array.prototype.slice.uncurrying();
(function() {
let result = slice(arguments); // 這裡只需要調用slice函數即可
console.log(result); // [1, 2, 3]
})(1,2,3);
以上代碼通過反柯裡化的方式,把Array.prototype.slice變成了一個通用的slice函數,這樣就不會局限於僅對數組進行操作了,也從而將函數調用顯得更為簡潔清晰了。Function.prototype.uncurrying = function() {
let self = this; // self 此時就是下面的Array.prototype.push方法
returnfunction() {
let obj = Array.prototype.shift.call(arguments);
/*
obj其實是這種樣子的
obj = {
'length': 1,
'0': 1
}
*/
return self.apply(obj, arguments); // 相當於Array.prototype.push(obj, 110)
}
};
let slice = Array.prototype.push.uncurrying();
let obj = {
'length': 1,
'0': 1
};
push(obj, 110);
console.log(obj); // { '0': 1, '1': 110, length: 2 }
其實實現反柯裡化的方式不只一種,下面再給大家分享一種,直接看代碼Function.prototype.uncurrying = function() {
let self = this;
returnfunction() {
returnFunction.prototype.call.apply(self, arguments);
}
};
實現方式大致相同,大家也可以寫一下試試,動動手,活動一下筋骨函數節流下面再說一下函數節流,我們都知道在onresize、onscroll和mousemove,上傳文件這樣的場景下,函數會被頻繁的觸發,這樣很消耗性能,瀏覽器也會吃不消的於是大家開始研究一種高級的方法,那就是控制函數被觸發的頻率,也就是函數節流了。簡單說一下原理,利用setTimeout在一定的時間內,函數隻觸發一次,這樣大大降低了頻率問題。函數節流的實現也多種多樣,這裡我們實現大家常用的吧。function throttle (fn, wait) {
let _fn = fn, // 保存需要被延遲的函數引用
timer,
flags = true; // 是否首次調用
returnfunction() {
let args = arguments,
self = this;
if(flags) { // 如果是第一次調用不用延遲,直接執行即可
_fn.apply(self, args);
flags = false;
return flags;
}
// 如果定時器還在,說明上一次還沒執行完,不往下執行
if(timer) returnfalse;
timer = setTimeout(function() { // 延遲執行
clearTimeout(timer); // 清空上次的定時器
timer = null; // 銷毀變量
_fn.apply(self, args);
}, wait);
}
}
window.onscroll = throttle(function() {
console.log('滾動');
}, 500);
給頁面上body設置一個高度出現滾動條後試試看,比每滾動一下就觸發來說,大大降低了性能的損耗,這就是函數節流的作用,起到了事半功倍的效果,開發中也比較常用的。分時函數我們知道有一個典故叫做:羅馬不是一天建成的;更為通俗的來說,胖子也不是一天吃成的。體現在程序裡也是一樣,我們如果一次獲得了很多數據(比如有10W數據),然後在前端渲染的時候會卡到爆,瀏覽器那麼溫柔的物種都會起來罵娘了。所以在處理這麼多數據的時候,我們可以選擇分批進行,不用一次塞辣麼多,嘴就辣麼大。function timeChunk(data, fn, count = 1, wait) {
let obj, timer;
function start() {
let len = Math.min(count, data.length);
for(let i = 0; i < len; i++) {
val = data.shift(); // 每次取出一個數據,傳給fn當做值來用
fn(val);
}
}
returnfunction() {
timer = setInterval(function() {
if(data.length === 0) { // 如果數據為空了,就清空定時器
return clearInterval(timer);
}
start();
}, wait); // 分批執行的時間間隔
}
}
// 測試用例
let arr = [];
for(let i = 0; i < 100000; i++) { // 這裡跑了10萬數據
arr.push(i);
}
let render = timeChunk(arr, function(n) { // n為data.shift()取到的數據
let div = document.createElement('div');
div.innerHTML = n;
document.body.appendChild(div);
}, 8, 20);
render();
惰性加載兼容現代瀏覽器以及IE瀏覽器的事件添加方法就是一個很好的慄子。// 常規的是這樣寫的
let addEvent = function(ele, type, fn) {
if(window.addEventListener) {
return ele.addEventListener(type, fn, false);
} elseif(window.attachEvent) {
return ele.attachEvent('on'+ type, function() {
fn.call(ele);
});
}
};
這樣實現有一個缺點,就是在調用addEvent的時候都會執行分支條件裡,其實只需要判斷一次就行了,非要每次執行都來一波。下面我們再來優化一下addEvent,以規避上面的缺點,就是我們要實現的惰性加載函數了。let addEvent = function(ele, type, fn) {
if(window.addEventListener) {
addEvent = function(ele, type, fn) {
ele.addEventListener(type, fn, false);
}
} elseif(window.attachEvent) {
addEvent = function(ele, type, fn) {
ele.attachEvent('on'+ type, function() {
fn.call(ele)
});
}
}
addEvent(ele, type, fn);
};
上面的addEvent函數還是個普通函數,還是有分支判斷。不過當第一次進入分支條件後,在內部就會重寫了addEvent函數。下次再進入addEvent函數的時候,函數裡就不存在條件判斷了。終點節目不早,時間剛好,又到了該要說再見的時候了,來一個結束語吧。可以把函數當做參數傳遞和返回值輸出
函數柯裡化
參數復用 (add函數慄子)
提前返回 (惰性加載)
延遲計算 (bind)
反柯裡化
函數節流
分時函數
惰性加載