忍者級別的JavaScript函式操作

Neal_yang發表於2017-10-31

從名字即可看書,此篇部落格總結與《JavaScript忍者祕籍》。對於JavaScript來說,函式為第一型別物件。所以這裡,我們主要是介紹JavaScript中函式的運用。

系列部落格地址:https://github.com/Nealyang/YOU-SHOULD-KNOW-JS

react技術棧+express+mongoose實戰全棧部落格教程Demo:https://github.com/Nealyang/React-Express-Blog-Demo

匿名函式

對於什麼是匿名函式,這裡就不做過多介紹了。我們需要知道的是,對於JavaScript而言,匿名函式是一個很重要且具有邏輯性的特性。通常,匿名函式的使用情況是:建立一個供以後使用的函式。

簡單的舉個例子如下:

window.onload = function() {
  alert('hello');
}
var templateObj = {
    shout:function() {
      alert('作為方法的匿名函式')
    }
}
templateObj.shout();

setTimeout(function() {
  alert('這也是一個匿名函式');
},1000)複製程式碼

上面的一個程式碼片段我就不做過多無用解釋了,比較常規。

遞迴

遞迴,說白了,就是自己呼叫自己,或者呼叫另外一個函式,但是這個函式的呼叫樹的某一個地方又呼叫了自己。所以遞迴,就產生了。

普通命名函式的遞迴

拿普通命名函式的遞迴最好的舉例就是用最簡單的遞迴需求:檢測迴文。

迴文的定義如下:一個短語,不管從哪一個方向讀,都是一樣的。檢測的工作當然方法多樣,我們可以建立一個函式,用待檢測的迴文字元逆序生成出一個字元,然後檢測二者是否相同,如果相同,則為迴文字元。

但是這種方法並不是很有逼格,確切的說,代價比較大,因為我們需要分配並建立新的字元。

所以,我們可以整理出如下簡潔的辦法:

  • 單個和零個字元都是迴文
  • 如果字串的第一個字元和最後一個字元相同,並且除了兩個字元以外,別的字元也滿足該要求,那麼我們就可以檢測出來了這個是迴文了
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,然後你懂得~這就是引用丟失的問題。


截圖自《JavaScript忍者祕籍》

通過完善之前對匿名函式的粗略定義,我們可以修復解決這個問題。在匿名函式中,我們不在使用顯示的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記憶

通過元素標籤名來獲取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沒有函式的過載,但是引數列表的靈活性是獲取其他語言類似過載功能的關鍵所在

使用apply()支援可變引數

需求:查詢陣列中的最大值、最小值

一開始,我認為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 (新建)


相關文章