JavaScript || 函數

函數

  • JavaScript中,函數指只定義一次,但能夠屢次被屢次執行或調用的一段JavaScript代碼。與數組相似,JavaScript中函數是特殊的對象,擁有自身屬性和方法算法

  • 每一個函數對象都有prototypelength屬性,bindapply()call()方法。函數的特殊性在於:能夠經過函數調用執行函數體中的語句。編程

  • 函數是對象,因此能夠賦值給變量、做爲參數傳遞進其餘函數、掛載到對象上做爲方法數組

1 函數定義

函數定義總共有三種方法:函數定義表達式、函數聲明語句和new Function()瀏覽器

  • 可是new Function()使用不多,由於經過它建立的函數不使用詞法做用域,建立的函數都在全局做用域被調用。緩存

  • 函數定義表達式和函數聲明語句都利用關鍵字function來定義函數閉包

    // 函數聲明語句
    function funcName([arg1 [, arg2] [..., argn]]) {
      statements
    }
    //函數定義表達式
    var funcName = function([arg1 [, arg2] [..., argn]]) {
      statements
    }
  • 函數名標識符funcName:引用新定義的函數對象app

  • 參數列表:函數中的參數與函數體中的局部變量相同,function(x)至關於在函數體中var x;編程語言

  • { statments }:構成函數體的語句,調用函數後執行的語句ide

1.1 變量提高

JavaScript中由var關鍵字聲明的變量存在變量提高:將變量聲明提高到做用域的頂部,但賦值仍保留在原處。因此函數聲明語句和函數定義表達式有本質的區別函數式編程

  • 函數聲明語句:將函數聲明和函數的賦值都提高到做用域的頂部,在同一個做用域中能夠出現調用在函數定義以前;

ECMAScript容許函數聲明語句做爲頂級語句,能夠出如今全局做用域中、也能夠出如今嵌套函數中,但不能出如今循環、判斷、try-catch-finallywith語句中。函數定義表達式沒有限制

  • 函數定義表達式:與var聲明的普通變量相同,只是將變量聲明提高到做用域頂部,但賦值仍然保留在原處,不能在定義前使用

    //沒有顯式指明返回值的函數,默認返回undefined
    //輸出對象o的每一個屬性的名稱
    function printPrps(o) {
      for(var prop in o) {
        console.log(prop + ": " + o[prop] + "\n");
      }
    }
    
    //計算笛卡爾座標系中兩點間的距離
    function distance(x1, y1, x2, y2) {
      var dx = x2 - x1;
      var dy = y2 - y1;
      return Math.sqrt(dx * dx + dy * dy);
    }
    
    // 計算階乘的遞歸函數,x!是x到1間(步長1)的累乘
    function factorial(x) {
      //遞歸結束標誌
      if(x <= 1) {
        return 1;
      }
      return x * factorial(x - 1);
    }
    
    //將函數表達式賦值給變量
    var square = function (x) {return x * x;};
    // 函數表達式能夠包含函數名,在遞歸時頗有用
    //
    var f = function fact(x) {
      if(x <= 1) {return 1;}
      return x * fact(x -1);
    };
    
    // 函數表達式能夠做爲參數傳遞給其餘函數
    data.sort(function(a, b) {return a  - b;});
    
    //定義後當即調用函數表達式
    var tensquare = (function(x) {return x * x;})(10);

1.2 嵌套函數

JavaScript中,函數能夠嵌套在其餘函數中。內部函數能夠訪問外部函數的局部變量和參數。

//  內部函數square能夠訪問到外部函數的參數a、b和局部變量c
function hypotenuse(a, b) {
  var c = 10;
  function square(x) {return x * x;}
  return Math.sqrt(square(a) + square(b) + square(c));
}

2 函數的調用

在定義函數時,函數體中的代碼不會執行,只有在調用函數時,才執行函數體中的語句。有四種方式能夠調用函數:

  1. 做爲普通函數

  2. 做爲對象的方法

  3. 做爲構造器函數

  4. 使用函數的call()apply()方法間接調用

2.1 調用函數

使用調用表達式來調用普通函數,每一個調用表達式由多個函數表達式組成。每一個函數表達式包括函數對象、括號和傳入的實參組成。

  • 每次調用會擁有本次調用的上下文this;在ES5非嚴格模式下,普通函數的this值是全局對象;在嚴格模式下是undefined

  • 以函數形式調用的函數一般不使用this關鍵字

  • 若是函數沒有顯式return語句返回一個值,默認返回undefined

  • 傳入的實參是由逗號分隔的0個或多個函數表達式

    // 調用printProps()函數,傳入對象做爲實參便可
    printPrps({x: 1});  
    // 調用distance()函數
    var total = distance(0, 0, 2, 1) + distance(2, 1, 3, 5);
    // 調用factorial()函數
    var probability = factorial(5) / factorial(13);

2.2 方法調用

方法是保存在JavaScript對象屬性中的函數。

  • 對方法調用的參數和返回值處理與函數調用相同

  • 方法調用由兩個部分組成:對象.屬性名(),其中屬性名是值爲函數的屬性

  • 方法調用中:調用上下文指調用方法的對象,使用this關鍵字引用

    printProps({x: 1});
    var total = distance(0, 0, 2, 1) + distance(2, 1, 3, 5);
    var probability = factorial(5) / factorial(13);
    
    
    var calculator = {  //對象字面量
      operand1: 1,
      operand2: 2,
      add: function() {
        //用this關鍵字指代當前對象calculator
        this.result = this.operand1 + this.operand2;
      }
    };
    calculator.add();      //調用其add方法,使calculator對象得到result屬性
    calculator.result;   //   ==> 3

方法和this關鍵字是面向對象的核心,任何函數做爲方法調用時會傳入一個隱式實參(指代調用方法的對象this),基於this的方法能夠執行多種操做。

  • this是一個關鍵字,不是變量名、屬性名,JavaScript不容許爲this賦值,可是能夠將其賦值給其餘變量

  • this沒有做用域限制,可是嵌套的函數不會從調用它的函數中繼承this

    • 嵌套函數若是做爲方法調用,this的值指向調用它的對象;

    • 嵌套函數若是做爲函數調用,this不是全局變量(ES5非嚴格模式),就是undefined(ES5嚴格模式)

    • 嵌套函數的this並不指向調用它的外層函數的上下文

  • 在外層函數中使用變量將外層函數的this對象及arguments屬性保存下來,在嵌套函數中即可以訪問

    var o = {
      m: function() {
        var self = this;             // 保存this(指向o對象)在變量self中
        console.log(this === o);    // ==> true,this指向o對象
        f();                        //將f()做爲函數調用
    
        function f() {
          console.log(this);  //  ==> window嚴格模式下,嵌套函數做爲函數來調用,其this是undefined;非嚴格模式下是全局對象
          console.log(this === o);   //false,此處的this指向全局對象或undefined
          console.log(self === o);   //true,self指向外部函數的this值
        }
      }
    };
    o.m();       //調用對象o的方法m()

2.3 構造函數調用

若是函數或方法調用前有關鍵字new,函數或者方法便做爲構造函數來調用。構造函數會建立一個新對象,新對象繼承構造函數的prototype屬性。

  • 做爲構造器函數的調用,會將新建立的對象做爲其調用上下文(this指向新建立的對象),在構造器函數中使用this引用新建立的對象。

2.4 間接調用call()apply()

函數是對象,每一個函數都有call()apply()兩個方法,做用是改變函數運行時的上下文context--改變函數體內部this的指向
,由於JavaScript中有函數定義時上下文函數運行時上下文函數中上下文能夠改變的概念。

  • call()apply()做用都是動態改變函數體內this指向,只是接受參數形式不太同樣。

  • call()須要將參數按順序傳遞進函數,而且知道參數的數量(參數數量肯定時使用)

  • apply()將參數放在數組中傳進函數(參數數量不肯定時使用)

call()apply()存在的意義

在JavaScriptOOP中,使用原型實現繼承,call()apply()是用於不一樣對象間的方法複用。當一個object沒有某個方法,可是另外一個objAnother對象有,能夠藉助call()apply()使object能夠操做objAnother對象的方法。

function Cat() {}
function Dog() {}

Cat.prototype = {
  food: "fish",
  say: function () {
    console.log("I love " + this.food);
  }
};

Dog.prototype = {food: "bone"};

var bCat = new Cat();
var bDog = new Dog();
bCat.say();                 //  ==> "I love fish"
bCat.say.call(bDog);          //==>"I love bone",bDog對象使用bCat對象的say方法,輸出自身的`this.food`屬性

3 函數的實參和形參

實參和形參是相對的概念,在函數定義時指定的參數叫作形參;在函數調用時傳入的參數叫作實參。對於須要省略的實參,可使用null或undefined`做爲佔位符。

3.1 參數默認值

若是調用函數時,傳入的實參個數arguments.length小於定義時形參的個數arguments.callee.length,剩餘的形參都被設置爲undefined。對能夠省略的值應該賦一個合理的默認值。

//  將對象obj中可枚舉的自身屬性追加到數組a中,並返回數組a
//  若是省略a,則建立一個新數組,並返回這個新數組
function getPropertyNames(obj, /*optional*/ a) {
  if(!a) { a = []; }       //若是未傳入a,則使用新數組。
  // a = a || [];代替寫法更有語義
  for(var prop in obj) {
    if(!obj.hasOwnProperty(prop)) {continue;}
    a.push(prop);
  }
  return a;
}
// 調用,出入一個參數或兩個參數
var a = getPropertyNames(obj);   //將obj的屬性存儲到一個新數組中
getPropertyNames(obj, arr);   //將obj的屬性追加到arr數組中

函數中的參數等同於函數體內的局部變量,具備函數的做用域。

3.2 參數對象

函數體內,標識符arguments指向實參對象的引用,實參對象是一個類數組對象,能夠經過下標訪問每一個傳入的參數。

  • arguments僅是一個標識符,嚴格模式下不能賦值;

  • 應用場景:函數包含固定個數的必須參數,隨後包含不定數量的可選參數

    // 能夠接收任意個數的實參,
    // 接收任意數量的實參,返回傳入實參的最大值,內置的Math.max()方法功能相似
    function max(/*...optional*/) {  //實參個數不能爲0
      var maxNum = Number.NEGATIVE_INFINITY;   //將保存最大值的變量初始化
      for(var i in arguments) {
        maxNum = (arguments[i] > maxNum) ? arguments[i] : maxNum;
      }
      return maxNum;
    }

3.3 calleecaller屬性

  • callee是ECMAScript規範中arguments對象的屬性:表明當前正在執行的函數。

  • caller是非標準的,只是瀏覽器基本都實現了這個屬性:帶表調用當前函數的函數。

  • 在嚴格模式中,對這兩個屬性讀寫都會產生錯誤

    // arguments的callee屬性用在匿名函數的遞歸實現
    var factorial = function(x) {
      if(x <= 1) {return 1;}
      return x * arguments.callee(x - 1);
    }

3.4 將對象屬性做爲參數

在定義一個函數時,若是傳入的參數多於3個,在調用時按順序傳入會變得很麻煩。一種解決方式是傳入key/value形式的參數,無需關注參數的順序。

  • 在定義函數時,形參指定爲一個對象;

  • 調用函數時,將整個對象傳入函數,無需關心每一個屬性的順序。(性能會差,參數須要在對象中去查找值)

    // 將原始數組的length複製到目標數組
    // 開始複製原始數組的from_start元素
    // 而且將其複製至目標數組的to_start中
    // 參數複雜,調用時順序難以控制
    function arrayCopy(array, from_start, target_arr, to_start, length) {
      // (原始數組, index, 目標數組, index, length)
      {
        // 實現邏輯
      }
    }
    
    // 無需關心參數順序的版本,效率略低
    // from_start和to_start默認爲0
    function easyCopy(args) {
      arrayCopy(args.array,
                args.from_start || 0,
                args.target_arr,
                args.to_start || 0,
                args.length);
    }
    // easyCopy()的調用
    var a = [1, 2, 3, 4];
    var b = [];
    easyCopy({array: a, target_arr: b, length: 4});

3.5 實參類型

JavaScript在定義函數時並未聲明形參類型,形參總體傳入函數體前不會作類型檢查,若是對傳入的實參有某種限制,最好在函數體內增長類型檢查的代碼。

// 返回數組或類數組a中元素的累加和
// 數組a中的元素必須是數字,null和undefined被忽略
// 類型檢查嚴格,可是靈活性不好
function sum(a) {
  if(isArrayLike(a)) {  // a是數組或類數組
    var result = 0;
    for(var i in a) {
      var element = a[i];
      if(element == null) {continue;}   //跳過null和undefined
      if(isFinite(element)) {
        result += element;
      } else {
        throw new Error("sum(): elements must be finite number");
      }
    }
    return result;
  } else {
    throw new Error("sum(): arguments must be array-like");
  }
}

4 函數做爲值

函數定義及調用是JavaScript中的詞法特性;同時JavaScript中函數是一個對象:

  • 能夠賦值給變量

  • 存儲在對象的屬性中或者數組的元素中

  • 做爲參數傳入另外一個函數:例如Array.sort()方法,用來對數組元素進行排序。可是排序的規則有不少中,將具體規則封裝在函數中,傳入sort()。函數實現對任意兩個值都返回一個值,指定它們在排序好數組中的前後順序

    // 簡單函數
    function add(x, y) {return x + y;}
    function subtract(x, y) {return x - y;}
    function mutiply(x, y) {return x * y;}
    function divide(x, y) {return x / y;}
    
    // 這個函數以上面一個函數做爲參數,並傳入兩個操做數,使用傳入的函數來調用
    // 過程抽象:兩個數能夠執行加、減、乘、除四個操做,將四個運算抽象爲操做符,根據操做符不一樣,執行不一樣的函數
    function operate(operator, operand1, operand2) {
      return operator(operand1, operand2);
    }
    // 執行(2 + 3) + (4 * 5)
    var i = operate(add, 2, 3) + operate(mutiply, 4, 5);   // ==>25
    
    // 另一種實現
    var  operators = {
      add: function(x, y) {return x + y;},
      subtrack: function(x, y) {return x + y;},
      mutiply: function(x, y) {return x + y;},
      divide: function(x, y) {return x + y;},
      pow: Math.pow
    };
    function operate2(operator, operand1, operand2) {
      if(typeof operators[operator] === 'function') {
        return operators[operator](operand1, operand2);
      } else {
        throw "unknown operator";
      }
    }
    // 計算("hello" + " " + "world")的值
    operate2("add", "hello", operate2("add", " ", "world"));   //  ==> "hello world"
    operate2("pow", 10, 2);    // ==> 100

自定義屬性

函數是對象,能夠擁有屬性。對於函數中的靜態變量,能夠直接存入函數的屬性中。

// 初始化函數對象的計數器屬性,函數聲明會被提早,因此能夠先給他的屬性賦值
uniqueInteger.counter = 0;
// 每次調用這個函數,都會返回一個不一樣的整數,使用counter屬性保存下次要返回的值
function uniqueInteger() {
  return uniqueInteger.counter++;   // 先返回計數器的值,再自增1
}

5 函數做爲命名空間

JavaScript中只存在函數做用域和全局做用域,沒有塊級做用域。可使用自執行函數用做臨時命名空間,這樣不會污染全局變量。

(function() {/* 模塊代碼 */})();  //注意調用括號的位置,兩種寫法都可
(function() {/* 模塊代碼 */} ());

6 閉包

編程界崇尚優雅簡潔惟美,不少時候若是你以爲一個概念很複雜,那麼多是你理錯了

閉包在JavaScript中,指內部函數老是能夠訪問其所在的外部函數中聲明的變量和參數,即便外部函數被返回(調用結束)。

  • Closure使JavaScript使當前做用域可以訪問到外部做用域中的變量;

  • 函數是JavaScript中惟一擁有自身做用域的結構,因此Closure的建立依賴於函數

6.1 如何理解

var scope = "global scope";
function checkScope() {
  var scope = "local scope";
  function f() {return scope;}
  return f;   //將函數對象返回
}
checkScope()();    //  ==>  "local scope"

圖片描述

  1. 在JavaScript中,每一個函數在定義時會建立一個與之相關的做用域鏈,而且在程序執行期間一直存在

    • 外部函數checkScope有自身的做用域鏈,內部函數f有自身單獨的的做用域鏈)

  2. 每次調用函數會建立一個新對象來保存參數和局部變量,並將其添加到做用域鏈。

    • 當函數返回時,將綁定的新對象從做用域鏈上刪除。若是沒有其餘變量引用該對象、或該對象沒有保存在某個對象的屬性中,它會被當作垃圾回收。

    • 若是沒有外部變量引用checkScope調用函數時建立的臨時對象,函數return後便被垃圾回收

  3. 若是checkScope定義有嵌套函數f,並將f做爲返回值或保存在某個對象的屬性中。至關於有一個外部引用指向嵌套函數。

    • f有自身的做用域鏈和保存參數與局部變量的對象

    • fcheckScope函數體內,能夠訪問外部函數中全部的變量和參數

綜上所述:JavaScript中的函數,經過做用域鏈和詞法做用域二者的特性,將該函數定義時的所處的做用域中的相關函數進行捕獲和保存,從而能夠在徹底不一樣的上下文中進行引用

6.2 注意點

  1. 每一個函數調用都有一個this值和arguments對象,須要在外部函數中用變量保存this值和arguments對象,Closure才能夠訪問到外部函數的這兩個值。that = thisouterArguments = arguments

  2. Closure是經過調用外部函數返回內部嵌套函數建立的,每次調用外部函數都會建立一個Closure可是每一個Closure共享外部函數聲明的變量,不會爲每一個Closure單首創建一份外部做用域的副本

    // 函數返回一個返回v的函數
    function constFunc(v) {
      return function() {return v;};
    }
    //建立一個數組用來保存常數
    var funcs = [];
    for(var i=0; i<10; i++) {
      funcs[i] = constFunc(i);  // 建立了10個Closure,每一個Closure的值不一樣,由於每次傳入外層函數constFunc的值不一樣
    }
    console.log(funcs[6]());   //  ==> 6
    
    function  constFuncs() {
      var funcs = [];
      for(var i=0; i<10; i++) {
        funcs[i] = function() {return i;};   // 建立10個Closure,但10個Closure在同一個外層函數constFuncs內,共享它的局部變量。
      }                                     // 10個Closure建立完畢後,i的值變爲0,因此每一個Closure返回的值都是0
      return funcs;
    }
    var foo = constFuncs();
    console.log(foo[4]());    //  ==> 10
  3. CLosure中部分資源不能自動釋放,容易形成內存泄漏

內存泄漏指因爲疏忽或錯誤形成程序未能釋放已經再也不使用的內存(即再也不利用的值或對象依然佔據內存空間)


7 函數的屬性、方法和構造函數

JavaScript中函數是對象,每一個函數都有lenghtprototype屬性;每一個函數都有call()apply()bind()方法,而且能夠利用函數的構造函數Function()來建立函對象。

7.1 length屬性

函數對象的length屬性是只讀的,用於獲取定義函數時指定的形參個數。能夠用來檢驗定義的參數與傳入的參數是否相同。

// arguments.callee不能在嚴格模式下工做
function check(args) {
  var actual = args.length;
  var expected = args.callee.length;      // arguments.callee指代函數自己
  if(expected !== actual) {
    throw Error("Expected:" + expected + " args; got " + actual + "args;");
  }
}
// 測試函數,只有傳入三個函數纔不會報錯
function f(x, y, z) {
  check(arguments);
  return x + y + z;
}

7.2 prototype屬性

每一個函數都有一個prototype屬性,指向一個原型對象的引用,每一個函數的原型對象都不一樣

  • 將函數用做建立對象的構造器函數使用時,新建立的對象會從函數的原型對象上繼承屬性

7.3 call()apply()

call()apply()用於動態改變this的指向,使對象的方法能夠借用給別的對象使用。

7.4 bind()

bind()方法的做用是將函數綁定至某個對象,bind()方法的返回值是一個新的函數對象

  • f()函數調用bind()方法綁定至對象o,用變量g來接收bind()返回的函數,(以函數調用形式)調用g時,會將原始函數f當作對象o的方法來使用。

    var f = function(y) {return this.x + y;};
    var o = {x: 2};
    var g = f.bind(o);   // 將f()綁定到o對象上
    console.log(g(6));    //  ==>  以函數調用的方式調用g(x),至關於調用o.f(x)
    
    // 實現bind()綁定
    function bind(f, o) {
      if(f.bind) {   //若是bind()方法存在,使用bind()方法
        return f.bind(o);
      } else {
        return function() {  //利用apply()使o對象來調用f()方法,而且傳入類數組對象參數arguments
          return f.apply(o, arguments);  //arguments是調用綁定函數時傳入的參數
        };
      }
    }
  • bind()第一個實參是要綁定方法的對象(本質是將函數的this指向改成傳入的對象),同時後面的實參也會綁定至this,函數式編程中的currying 柯里化。

    var sum = function(x, y) {return x + y;};
    // 建立一個相似sum的新函數,可是this綁定到null
    // 而且第一個參數綁定爲1,新的函數只指望傳入一個參數
    var g = sum.bind(null, 1);  //  將sum的第一個參數x綁定爲1
    console.log(g(3));   // ==> 4,由於x綁定爲1,將3做爲參數傳入y
    
    function f(y, z) {return this.x + y + z;}
    var g = f.bind({x: 2}, 3); // 將f函數綁定到對象{x: 2},將3綁定到函數的第一個參數y,新建立的函數傳入一個參數
    console.log(g(1));   // ==>6
  • 模擬實現bind()方法:bind()方法返回的是一個Closure

    if(!Function.prototype.bind) {  //不支持bind方法
      Function.prototype.bind = function (o) {
        var self = this;     // 保存bind()中的this與arguments,便於在嵌套函數中使用
        var boundArgs = arguments;
        // bind()方法返回一個函數對象
        return function() {
          // 建立一個實參列表,將傳入bind()的第二個及之後的實參都傳入這個函數
          var args = [];
          // 傳入bind()函數的參數處理,從第二位開始
          for(var i=1; i<boundArgs.length; i++) {args.push(boundArgs[i]);}
          // 將調用新函數時傳入的參數繼續添加到args中
          for (var j=0; j < arguments.length; j++) {args.push(arguments[j]);}
    
          // 將self做爲o的方法來調用
          return self.apply(o, args);
        };
      };
    }

注意點

bind()方法的某些特性是上述模擬方法不能替代的。

  1. bind()方法返回一個真正的函數對象,函數對象的length屬性是綁定函數的形參個數減去綁定的實參個數(length的值不能小於0

    function f(y, z) {return this.x + y + z;}  // 綁定函數f的形參個數時2
    var g = f.bind({x: 2}, 3); // 綁定的實參個數是1(從第二位開始是傳入綁定函數的實參),即將3傳遞給f的第一個參數y
    g(1);   // ==> 6,繼續將1傳遞給函數f的形參z
  2. ES5的bind()方法能夠順帶作構造函數,此時將會忽略傳入bind()方法的this,原始函數以構造函數的形式調用,其實參已經綁定。

  3. bind()方法返回的函數並不包含prototype屬性(普通函數的固有prototype屬性是不能刪除的);而且將綁定的函數用做構造器函數時所建立的對象,從原始爲綁定的構造器函數中繼承prototype

    • 若是將g()做爲構造函數,其建立的對象與直接利用f當作構造函數建立的對象原型是同一個prototype

7.5 toString()方法

根據ECMAScript規範,函數的toString()方法返回一個字符串,字符串與函數聲明語句的語法有關。

  • 大多數函數的toString()方法都返回函數的完整源碼

  • 內置函數的toString()方法返回一個相似"[native code]"的字符串做爲函數體

7.6 Function()構造函數

  • Function()構造函數運行JavaScript在運行時動態建立並編譯函數

  • 每次調用Function()構造函數都會解析函數體,並建立新的函數對象。若是在循環中執行Function(),會影響效率;

  • Function()建立的函數不使用詞法做用域,函數體的代碼編譯總在全局做用域執行

Function()在實際編程中使用不多。

8 函數式編程

JavaScript並不是函數式編程語言,但JavaScript中函數是對象,能夠像對象同樣操控,因此能夠應用函數式編程技術

8.1 使用函數處理數組

假設有一個數組,元素都是數字,要計算全部元素的平均值與標準差。

  • 非函數式編程風格

    var data = [1, 1, 3, 5, 5];
    var total = 0;  //平均數是全部元素的和除以元素的個數
    data.forEach(function(value) {
      total += value;
    });
    var mean = total / data.length;
    //標準差:先計算每一個元素與平均值的差的平方的和
    var sum = 0;
    data.forEach(function(value) {
      var tmp = value - mean;
      sum += tmp * tmp;
    });
    //標準差stddev
    var stddev = Math.sqrt(sum / data.length-1);
  • 函數式編程風格,利用map()reduce()來實現,抽象出兩個過程:

    • 求平均值和標準差會用到求一個數組中全部元素的和:使用reduce()

    • 求數組中每一個元素的平方:使用map()

      // 定義求和、求積兩個過程
      var add = function(x, y) {return x + y;};
      var square = function(x) {return x * x;};
      
      var data = [1, 1, 3, 5, 5];
      // reduc()實現數組求和
      var avg = data.reduce(add) / data.length;
      // map()實現差的平方,返回操做後的數組,再調用reduce()
      var sum = data.map(function(value) {return value - avg;});
      var stddev = Math.sqrt(sum.map(square).reduce(add) / (data.length - 1));

8.2 高階函數

高階函數higher-order function指操做函數的函數,接收一個或多個函數做爲參數,並返回一個新函數。

// 高階函數not()返回一個新函數,新函數將它的實參傳入f()
function not(f) {
  return function() {    // 返回一個新函數
    var result = f.apply(this, arguments);    // 調用f()
    return !result;   // 對結果求反
  };
}
var even = function (x) {   //判斷一個數是不是偶數
  return x % 2 === 0;
};
var odd = not(even);     // 一個新函數,所作的事情與even()相反
[1, 1, 3, 5, 5].every(odd);    // ==> true每一個元素都是奇數

// mapper()返回的函數的參數是數組,對每一個元素執行函數f()
// 返回全部計算結果組成的數組
function mapper(f) {
  return function(a) {
    return map(a, f);
  };
}
var increment = function(x) {return x + 1;};
var incrementer = mapper(increment);
incrementer([1, 2, 3]);

8.3 不徹底函數

將一次完整的函數調用拆分爲屢次函數調用,每次傳入的實參都是完整實參的一部分,每一個拆分開的函數叫作不徹底函數partial function,每次函數調用叫作不徹底函數調用partial application特色是每次調用都返回一個函數,知道獲得最終運行結果爲止

if(!Function.prototype.bind) {  //不支持bind方法
  Function.prototype.bind = function (o) {
    var self = this;     // 保存bind()中的this與arguments,便於在嵌套函數中使用
    var boundArgs = arguments;
    // bind()方法返回一個函數對象
    return function() {
      // 建立一個實參列表,將傳入bind()的第二個及之後的實參都傳入這個函數
      var args = [];
      // 傳入bind()函數的參數處理,從第二位開始
      for(var i=1; i<boundArgs.length; i++) {args.push(boundArgs[i]);}
      // 將調用新函數時傳入的參數繼續添加到args中
      for (var j=0; j < arguments.length; j++) {args.push(arguments[j]);}

      // 將self做爲o的方法來調用
      return self.apply(o, args);
    };
  };
}
  • 函數f()bind()方法返回一個新函數,給新函數傳入特定的上下文和一組指定的參數,而後調用函數f()。傳入bind()的實參都是放在傳入原始參數的實參列表開始的位置。
    但有時但願將傳入bind()的實參放在完整實參列表的右側:

// 實現一個工具函數,將類數組對或對象轉化爲真正的數組
// 將arguments對象轉化爲真正的數組
function array(a, n) {return Array.prototype.slice.call(a, n || 0);}

// 這個函數的實參傳遞至左側
function partialLeft(f) {
  var args = arguments;   // 保存外部的實參數組
  return function() {    // 返回一個函數
    var a = array(args, 1);  // 開始處理外部的第一個args
    a = a.concat(array(arguments));  //而後增長全部的內部實參
    return f.apply(this, a);   // 基於這個實參列表調用f()
  };
}

// 這個函數的實參傳遞至右側
function partialRight(f) {
  var args = arguments;   // 保存外部的實參數組
  return function() {    // 返回一個函數
    var a = array(arguments);  // 從內部參數開始
    a = a.concat(array(args, 1));  //而後從外部第一個args開始添加
    return f.apply(this, a);   // 基於這個實參列表調用f()
  };
}

// 這個函數的實參被用做模板,實參列表中的undefined值都被填充
function partial(f) {
  var args = arguments;
  return function() {
    var a = array(args, 1);
    var i = 0, j = 0;
    // 遍歷args,從內部實參填充undefined值
    for(; i<a.length; i++) {
      if(a[i] === undefined) {a[i] = arguments[j++];}
    }
    a = a.concat(array(arguments, j));
    return f.apply(this, a);
  };
}

// 函數帶有三個實參
var f = function(x, y, z) {
  return x * (y - z);
};
// 注意三個不徹底調用間的區別
partialLeft(f, 2)(3, 4);   //  ==> -2:綁定第一個實參 2*(3-4)
partialRight(f, 2)(3, 4);   //  ==> 6:綁定最後一個實參 3*(4-2)
partial(f, undefined, 2)(3, 4);   //  ==> -6:綁定中間的實參 3*(2-4)
  • 利用不徹底函數的編程技巧,能夠利用已有的函數來定義新的函數

8.4 記憶

在函數式編程中,把將上次計算記過緩存的技術叫作記憶memerization

本質上是犧牲算法的空間複雜度以換取更優的時間複雜度。由於在客戶端中JavaScript代碼的執行速度每每成爲瓶頸。

// 返回f()的帶有記憶功能的版本(緩存上次計算結果)
// 只有在f()的實參字符串表示都不相同時才工做
function memorize(f) {
  var cache = {};   //將值保存在閉包內
  return function() {
    // 將實參轉爲字符串形式,並將其用做緩存的鍵
    var key = arguments.length + Array.prototype.join.call(arguments, ",");
    if(key in cache) {
      return cache[key];
    } else {
      return cache[key] = f.apply(this, arguments);
    }
  };
}
// memorize()建立新對象cache並將其保存在局部變量中,對於返回的函數來講它是私有的(在閉包中)。
// 返回的函數將它的實參數組轉化爲字符串,並將字符串用做緩存對象的屬性名。若是在緩存中有這個值,則直接返回
// 若是沒有,調用既定函數對實參進行計算,將計算結果緩存並返回

// 返回兩個整數的最大公約數
function gcd(a, b) {
  var t;
  if(a < b) { t= b; b = a; a = t; }
  while(b !== 0) {
    t = b;
    b = a %  b;
    a = t;
  }
  return a;
}
var gcdmemo = memorize(gcd);
gcdmemo(85, 187);   //  ==> 17
//注意寫一個遞歸函數時,每每須要記憶功能
// 調用實現了記憶功能的遞歸函數
var factorial = memorize(function(n) {
  return (n <= 1)? 1 : n * factorial(n - 1);
});
factorial(5);   // ==> 120,同時緩存了1~4的值。
相關文章
相關標籤/搜索