從名字便可看書,此篇博客總結與《JavaScript忍者祕籍》。對於JavaScript來講,函數爲第一類型對象。因此這裏,咱們主要是介紹JavaScript中函數的運用。javascript
系列博客地址:https://github.com/Nealyang/YOU-SHOULD-KNOW-JShtml
react技術棧+express+mongoose實戰全棧博客教程Demo:https://github.com/Nealyang/React-Express-Blog-Demo前端
對於什麼是匿名函數,這裏就不作過多介紹了。咱們須要知道的是,對於JavaScript而言,匿名函數是一個很重要且具備邏輯性的特性。一般,匿名函數的使用狀況是:建立一個供之後使用的函數。java
簡單的舉個例子以下:node
window.onload = function() {
alert('hello');
}
var templateObj = {
shout:function() {
alert('做爲方法的匿名函數')
}
}
templateObj.shout();
setTimeout(function() {
alert('這也是一個匿名函數');
},1000)複製代碼
上面的一個代碼片斷我就不作過多無用解釋了,比較常規。react
遞歸,說白了,就是本身調用本身,或者調用另一個函數,可是這個函數的調用樹的某一個地方又調用了本身。因此遞歸,就產生了。git
拿普通命名函數的遞歸最好的舉例就是用最簡單的遞歸需求:檢測迴文。github
迴文的定義以下:一個短語,無論從哪個方向讀,都是同樣的。檢測的工做固然方法多樣,咱們能夠建立一個函數,用待檢測的迴文字符逆序生成出一個字符,而後檢測兩者是否相同,若是相同,則爲迴文字符。算法
可是這種方法並非頗有逼格,確切的說,代價比較大,由於咱們須要分配並建立新的字符。express
因此,咱們能夠整理出以下簡潔的辦法:
function isPalindrome(txt) {
if(txt.length<=1){
return true;
}
if(txt.charAt(0)!= txt.charAt(txt.length-1)) return false;
return isPalindrome(txt.substr(1,txt.length-2));
}複製代碼
上面的代碼咱們並無作txt的一些類型檢測,undefined、null等。
所謂的方法,天然離不開對象,直接看例子:
var ninja = {
chirp:function(n) {
return n>1?ninja.chirp(n-1)+'-chirp':'chirp';
}
}
console.log(ninja.chirp(3))//chirp-chirp-chirp複製代碼
在上述代碼中,咱們經過對象ninja.chirp方法的遞歸調用了本身。可是,由於咱們在函數上s會用了非直接引用,也就是ninja對象的chirp屬性,因此纔可以實現遞歸,這也就引出來一個問題:引用丟失
上面的示例代碼,依賴於一個進行遞歸調用的對象屬性引用。與函數的實際名稱不一樣,由於這種引用多是暫時的。
var ninja = {
chirp:function(n) {
return n>1?ninja.chirp(n-1)+'-chirp':'chirp';
}
}
var samurai = {chirp:ninja.chirp};
ninja = {};
try{
console.log(samurai.chirp(3) === 'chirp-chirp-chirp')
}catch (err){
if(err) alert(false);
}複製代碼
如上,咱們把ninja屬性上的方法賦值給了samurai,而後置空ninja,而後你懂得~這就是引用丟失的問題。
經過完善以前對匿名函數的粗略定義,咱們能夠修復解決這個問題。在匿名函數中,咱們不在使用顯示的ninja引用。這裏咱們使用this(關於this的使用詳解,請關注個人我的微信公衆號:前端的全棧之路)。
var ninja = {
chirp:function(n) {
return n>1?this.chirp(n-1)+'-chirp':'chirp';
}
}複製代碼
當函數做爲方法被調用的時候,函數的上下文指的是該方法的對象。
使用this調用,可讓咱們的匿名函數更加的強大且靈活。可是。。。
上面咱們解決了做爲函數方法做爲遞歸時候的一個完美操做。但實際上,無論是否進行方法遞歸,巧妙使用this都是咱們應該所掌握的(關注微信公衆號,遲早都給你說到)。
話說回來,其實這樣寫也仍是有問題的,問題在於給對象定義方法的時候,方法名稱是寫死的,若是屬性名稱不同,豈不是同樣會丟失引用?
因此,這裏咱們採用另外一種解決方案,給匿名函數起個名字吧!對的,確定又人會說,我擦!那仍是匿名函數麼?嗯。。。好吧,那就不叫匿名函數了吧,叫內聯函數~
var ninja = {
chirp:function signal(n) {
return n>1?signal(n-1)+'-chirp':'chirp';
}
}
var samurai = {chirps:ninja.chirp};
ninja = {};
try{
console.log(samurai.chirps(3) === 'chirp-chirp-chirp')
}catch (err){
if(err) alert(false);
}複製代碼
因此如上的解決辦法,就完美解決了咱們以前說到全部問題。內聯函數還有一個很重要的一點,就是儘管能夠給內聯函數進行命名,可是這些名稱只能在自身函數內部纔可見。
JavaScript中的函數和其餘語言中的函數有所不一樣,JavaScript賦予了函數不少的特性,其中最重要的特性之一就是函數做爲第一類型對象。是的,對象!
因此,咱們能夠給函數添加屬性,甚至能夠添加方法。
有時候,咱們可能須要存儲一組相關但又獨立的函數,事件回調管理是最爲明顯的例子。向這個集合添加函數時候,咱們得知道哪些函數在集合中存在,不然不添加。
var store = {
nextId:1,
cache:{},
add:function(fn) {
if(!fn.id){
fn.id = store.nextId++;
return !!(store.cache[fn.id] = fn);
}
}
}
function ninja() {}
console.log(store.add(ninja));
console.log(store.add(ninja));複製代碼
上述代碼比較簡單常規,也就不作過多解釋。
緩存記憶是構造函數的過程,這種函數可以記住先前計算的結果。經過避免重複的計算,極大地提升性能。
做爲一個簡單的例子,這裏我來判斷一個數字是否爲素數。
function isPrime(value) {
if(!isPrime.answers) isPrime.answers = {};
if(isPrime.answers[value]!=null){
return isPrime.answers[value]
}
var prime = value != 1;//1 不是素數
for(var i = 2;i<value;i++){
if(value%2===0){
prime = false;
break;
}
}
return isPrime.answers[value] = prime
}
console.log(isPrime(5));
console.log(isPrime.answers[5]);複製代碼
如上代碼也都是常規操做,不作過多解釋。咱們能夠經過下面的console.log判斷出緩存是否成功。
緩存記憶有兩個主要的優勢:
固然,總歸會有缺點的
經過元素標籤名來獲取DOM元素是一個很是常見的操做。可是性能可能不是特別好。因此從上面的緩存記憶咱們能夠進行以下的騷操做:
function getElements(name) {
if(!getElements.cache) getElements.cache = {};
return getElements.cache[name] = getElements.cache[name]||document.getElementsByTagName(name);
}複製代碼
上面的代碼很簡單,可是有麼有眼前一亮的感受呢??我有!並且咱們還發現,這個簡單的緩存的代碼產生了5倍以上的性能提高。
咱們能夠將狀態和緩存信息存儲在一個封裝的獨立位置上,不只在代碼組織上有好處,並且外部存儲或緩存對象無需污染做用域,就能夠獲取性能的提高。
別激動,下面還有更多的奇淫技巧~
有時候咱們想建立一個包含一組數據的對象。若是隻是集合,則只須要建立一個數組便可。可是在某些狀況下,除了集合自己,可能會有更多的狀體須要保存。
一種選擇是,每次建立對象新版本的時候都建立一個新數組,而後將元數據做爲屬性或者方法添加到這個新數組上。可是這個操做太常規了。
欣賞以下騷操做:
<html>
<head></head>
<body>
<input id="first">
<input id="second">
<script> var elems = { length:0, add:function(elem) { Array.prototype.push.call(this,elem); }, gather:function(id) { this.add(document.getElementById(id)); } } elems.gather('first'); console.log(elems.length,elems[0].nodeType); elems.gather('second'); console.log(elems.length,elems[1].nodeType); </script>
</body>
</html>複製代碼
一般,Array.prototype.push()是經過其函數上下文操做其自身數組的。這裏咱們經過call方法來說咱們本身的對象扮演了一次他的上下文。push的方法會增長length的值(會認爲他就是數組的length屬性),而後給對象添加一個數字屬性,並將其引用到傳入的元素上。
關於函數的執行上下文,以及prototype的一些說明,將在後續文章寫到。
JavaScript靈活且強大的特性之一是函數能夠接受任意數量的參數。雖然JavaScript沒有函數的重載,可是參數列表的靈活性是獲取其餘語言相似重載功能的關鍵所在
需求:查找數組中的最大值、最小值
一開始,我認爲Math中提供的min(),max()能夠知足,可是貌似他並不可以找到數組中的最大值最小值,難道要我這樣:Math.min(arr[0],arr[1],arr[3]...)??
來吧,咱們繼續咱們的奇淫技巧。
function smallest(arr) {
return Math.min.apply(Math,arr);
}
function largest(arr) {
return Math.max.apply(Math,arr);
}
console.log(smallest([0,1,2,3,4]));
console.log(largest([0,1,2,3,4]));複製代碼
不作過多解釋,操做常規,是否是又是一個眼前一亮呢?
以前咱們有介紹過函數的隱士傳遞,arguments,也正是由於這個arguments的存在,才讓函數有能力處理不一樣數量的參數。即便咱們只定義固定數量的形參,經過arguments參數咱們仍是能夠訪問到實際傳給函數的全部的參數。
方法的重載一般是經過在同名的方法裏聲明不一樣的實例來達到目的。可是在javascript中並不是如此,在javaScript中,咱們重載函數的時候只有一個實現。只不過這個實現內部是經過函數實際傳入的參數的特性和個數來達到相應目的的。
function merge(root){
for(var i = 1;i<arguments.length;i++){
for(var key in arguments[i]){
root[key] = arguments[i][key]
}
}
return root;
}
var merged = merge(
{name:'Neal'},
{age:24},
{city:'Beijing'}
);
console.log(merged);複製代碼
經過如上代碼,咱們將傳遞給函數的對象都合併到一個對象中。在javascript中,沒有強制函數聲明多少個參數就得穿入多少個參數。函數是否能夠成功處理這些參數,徹底取決於函數自己的定義。
注意,咱們要作的事情是想讓第二個或者第n個參數上的屬性合併到第一個對象中,因此這個遍歷是從1開始的。
基於函數的參數,有不少種辦法進行函數的重載。一種通用的方法是,根據傳入參數的類型執行不一樣的操做。另外一種辦法是,能夠經過某些特定參數是否存在來進行判斷。還有一種是經過傳入參數個數來進行判斷。
假如對象上有一個方法,根據傳入參數的個數來執行不一樣的操做,冗長且呆呆的函數應該張這樣:
var ninja = {
whatever:function(){
switch(arguments.length){
case 0:
//do something
break;
case 1:
//do something
break;
case 2:
//do something
break;
case 3:
//do something
break;
}
}
}複製代碼
這種方式,看起來很是的呆呆的。因此咱們換一種方式來講下。
若是按照以下思路,添加劇載的方法會怎樣呢。
var ninja = {};
addMethod(ninja,'whatever',function(){/*do something*/});
addMethod(ninja,'whatever',function(a){/*do something*/});
addMethod(ninja,'whatever',function(a,b){/*do something*/});複製代碼
這裏咱們使用一樣的名稱(whatever)將方法添加到該對象上,只不過每一個重載的函數是單獨的。注意每個重載的函數參數是不一樣的。經過這種方式,咱們真正爲每個重載都建立了一個獨立的匿名函數。漂亮且簡潔。
下面就讓我操刀來實現這個addMethod函數吧
function addMethod(object,name,fn){
var old = object[name];
object[name] = function(){
if(fn.length === arguments.length){
return fn.apply(this,arguments);
}else if(typeof old == 'function'){
return old.apply(this,arguments);
}
}
}複製代碼
這個操做咱們這裏解釋一下,第一步,咱們保存原有的函數,由於調用的時候可能不匹配傳入的參數個數。第二部建立一個新的匿名函數,若是該匿名函數的形參個數和實際個數匹配,就調用這個函數,不然調用原來的函數。
這裏的fn.length是返回函數定義時候定義的形參個數。
下面解釋下這個函數的執行吧。adMethod第一次調用將建立一個新的匿名函數傳入零個參數進行調用的時候將會調用這個fn函數。因爲此時這個ninja是一個新的對象,因此沒必要擔憂以前建立過的方法。
第二次調用addMethod的時候,首先將以前的同名函數保存到一個變量old中,而後將新建立的匿名函數做爲方法。新方法首先檢查傳入的個數是否爲1,若是是則調用新傳入的fn,若是不是,則調用舊的。從新調用該函數的時候將在此檢查參數個數是否爲0
這種調用方式相似於剝洋蔥,每一層都檢查參數個數是否匹配。這裏的一個技巧是關於內部匿名函數是否合訪問到old和fn的。這個關於函數閉包的知識就在下一篇博客講解(關注微信公衆號吧)
function addMethod(object,name,fn){
var old = object[name];
object[name] = function(){
if(fn.length === arguments.length){
return fn.apply(this,arguments);
}else if(typeof old == 'function'){
return old.apply(this,arguments);
}
}
}
var ninjas = {
values:['Neal','yang','Nealyang','Neal yang']
}
addMethod(ninjas,'find',function(){
return this.values;
});
addMethod(ninjas,'find',function(name){
var ret = [];
for(var i = 0;i<this.values.length;i++){
if(this.values[i].indexOf(name)===0){
ret.push(this.values[i]);
}
}
return ret;
});
addMethod(ninjas,'find',function(first,last){
var ret = [];
for(var i = 0;i<this.values.length;i++){
if(this.values[i]==(first+' '+last))
ret.push(this.values[i]);
}
return ret;
});
console.log(ninjas.find().length);
console.log(ninjas.find('Neal'));
console.log(ninjas.find('Neal','yang'));複製代碼
關於上面使用的閉包想關注的知識,將在下一篇博客中,爲你們總結。
而後使用如上的技巧的時候須要注意下面幾點:
掃碼關注個人我的微信公衆號,分享更多原創文章。點擊交流學習加我微信、qq羣。一塊兒學習,一塊兒進步
歡迎兄弟們加入:
Node.js技術交流羣:209530601
React技術棧:398240621
前端技術雜談:604953717 (新建)