(JS基礎)函數高級用法

尾遞歸

函數調用自身,稱爲遞歸。當遞歸調用是整個函數體中最後執行的語句且它的返回值不屬於表達式的一部分時,這個遞歸調用就是尾遞歸。javascript

關於遞歸的性能問題

函數調用會在內存造成一個「調用記錄」,又稱「調用幀」(call frame),保存調用位置和內部變量等信息。若是在函數A的內部調用函數B,那麼在A的調用幀上方,還會造成一個B的調用幀。等到B運行結束,將結果返回到A,B的調用幀纔會消失。若是函數B內部還調用函數C,那就還有一個C的調用幀,以此類推。全部的調用幀,就造成一個「調用棧」(call stack)java

因此,遞歸是很是耗內存的。但尾遞歸只存在一個調用幀,不會發生「棧溢出」錯誤。es6

非尾調用的缺點

// 非尾遞歸的 Fibonacci 數列
function Fibonacci (n) {
  if ( n <= 1 ) {return 1};
  return Fibonacci(n - 1) + Fibonacci(n - 2);
}
Fibonacci(10) // 89
Fibonacci(100) // 超時複製代碼
// 遞歸版的 Fibonacci 數列
function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
  if( n <= 1 ) {return ac2};
  return Fibonacci2 (n - 1, ac2, ac1 + ac2);
}
Fibonacci2(100);  // 573147844013817200000
Fibonacci2(1000); // 7.0330367711422765e+208複製代碼
嵌套層次稍微變多,就會致使「調用棧」過大致使溢出。

尾調用

尾調用是指一個函數裏的最後一個動做是返回一個函數的調用結果的情形,即最後一步新調用的返回值直接被當前函數的返回結果。簡單的例子:數組

function f(x){
  return g(x);
}
複製代碼

但如下三種狀況是不屬於尾調用安全

// 狀況一,實際是先在f函數體內執行完g函數後的全部計算纔會被返回
function f(x){
  let y = g(x);
  return y;
}
// 狀況二,返回函數有其餘運算
function f(x){
  return g(x) + 1;
}
// 狀況三,沒有返回值
function f(x){
  g(x);
}複製代碼

尾遞歸

尾調用自身就是尾遞歸。
function factorial(n, total = 1) {
  if (n === 1) return total;
  return factorial(n - 1, n * total);
}
factorial(5);    // 120複製代碼

非嚴格模式不能實現尾遞歸

ES6 的尾調用優化只在嚴格模式下開啓,正常模式是無效的。 這是由於在正常模式下,函數內部的arguments和caller變量,能夠跟蹤函數的調用棧。bash

下面例子演示了棧溢出的狀況:閉包

function sum(x, y) {
  if (y > 0) {
    return sum(x + 1, y - 1);
  } else {
    return x;
  }
}
sum(1, 100000);    // 報錯複製代碼

蹦牀函數(trampoline)

蹦牀函數的思想是,把遞歸函數轉化成循環執行
app

// 蹦牀函數
function trampoline(f) {
  // 不斷執行f返回的函數,直到f返回非函數的值
  while (f && f instanceof Function) {
    f = f();
  }
  return f;
}
// 本來遞歸也改爲 返回一個等待執行的函數
function sum(x, y) {
  if (y > 0) {
    return sum.bind(null, x + 1, y - 1);
  } else {
    return x;
  }
}
// 測試
trampoline(sum(1, 100000));    // 100001
複製代碼

尾遞歸的終極優化方案

function tco(f) {
  var value;
  var active = false;
  var accumulated = [];
  return function accumulator() {
    accumulated.push(arguments);
    if (!active) {
      active = true;
      while (accumulated.length) {
        value = f.apply(this, accumulated.shift());
      }
      active = false;
      return value;
    }
  };
}
var sum = tco(function(x, y) {
  if (y > 0) {
    return sum(x + 1, y - 1)
  }
  else {
    return x
  }
});
sum(1, 100000);    // 100001複製代碼

上面代碼中,tco函數是尾遞歸優化的實現,它的奧妙就在於狀態變量active。默認狀況下,這個變量是不激活的。一旦進入尾遞歸優化的過程,這個變量就激活了。而後,每一輪遞歸sum返回的都是undefined,因此就避免了遞歸執行;而accumulated數組存放每一輪sum執行的參數,老是有值的,這就保證了accumulator函數內部的while循環老是會執行。這樣就很巧妙地將「遞歸」改爲了「循環」,然後一輪的參數會取代前一輪的參數,保證了調用棧只有一層。(摘自阮一峯的ES6)函數


防抖 和 節流

節流和防抖都是利用定時器,具體描述請查看另外一篇文章,這裏簡單介紹。
post

防抖(debounce)

防抖,指的是在 n 秒後執行該函數,若在此期間調用了該函數,則會從新計算倒計時。簡單說就是,n 秒內只容許執行該函數一次,目的是限制函數的執行次數,如發送請求等。

節流(throttle)

節流,指的是在 n 秒內只能有一個該函數執行, n 秒後能夠繼續添加。因此,對於連續觸發的函數,節流能把函數執行限制在必定時間頻率執行。舉個簡單例子,頁面滾動時會不斷觸發scroll事件,但我只但願每 300 ms獲取一次事件對象,這時候就用上節流函數。

二者對比

二者都是利用定時器,不一樣的是,在時間內觸發函數,防抖函數會將函數和定時器清除,後從新添加計時器;而節流函數則只是忽略新加入的函數,除非計時器結束。


做用域安全的構造函數

構造函數內用到this關鍵字,爲建立的對象提供屬性或方法。假如直接執行構造函數,this可能會指向對象,就變成了爲全局對象添加屬性,這並非咱們但願的。

可能產生問題的狀況

// 構造函數,指望是建立新對象,併爲其添加指定屬性
function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
}
// 正確使用:
let person = new Person('lisi', 29, 'worker');
// 錯誤用法。等同爲全局對象添加屬性
let person1 = Person('lisi', 29, 'worker');複製代碼

解決辦法

在構造函數內部判斷函數被調用時this的實例對象。做爲普通函數調用,this會指向執行上下文;做爲構造函數調用時,this會指向新對象。

function Person(name, age, job) {
  // 判斷 this 的原型對象是否爲本函數
  if (this instanceof Person) {
    this.name = name;
    this.age = age;
    this.job = job;
  } else {
    // 直接調用時也返回構造函數
    return Person(name, age, job);
    // 也能夠拋出錯誤
    // throw new Error('該函數是構造函數')
  }
}複製代碼


惰性載入函數

有這樣一種函數,有多個if語句用於判斷執行環境,但執行環境不會改變。但是每次調用該函數都須要經過多個if語句的判斷,形成性能的浪費。解決這類問題的函數就是惰性載入函數。原理就是,符合某個if語句時,把整個函數替換。下面給出一個簡單例子:

function lazyLoad() {
  if (someValue === 'typeA') {
    lazyLoad = function () {
      // 第一種狀況...
    }
  } else if (someValue === 'typeB') {
    lazyLoad = function () {
      // 第二種狀況...
    }
  } else {
    lazyLoad = function () {
      // ...
    }
  }
  // 返回執行結果
  return lazyLoad()
}複製代碼






仿私有變量

JavaScript 中沒有私有成員的概念。但能夠利用閉包的形式建立"私有成員"。但不建議過多使用,由於對性能有必定影響。

簡單例子:

function Fn(prop) {
  this.getProp = function () {
    return prop
  }
  this.setProp = function (val) {
    prop = val
  }
}
// 建立實例
let obj = new Fn();
// 經過指定方法 設定私有成員
obj.setProp('private');
// 經過指定方法 訪問私有成員
console.log(obj.getProp());
// 私有成員沒法直接訪問
console.log(obj.prop);複製代碼

靜態私有變量

簡單例子中,每次使用 new 建立的實例都會建立一組相同的方法,形成冗餘。爲了解決這個問題,應該把相同的方法"公有化"。

let Fn = (function () {
  // 私有變量
  let prop = '';
  // 構造函數
  let Fn = function (val) {
    prop = val
  }
  // 公有/特權方法
  Fn.prototype.getProp = function () {
    return prop
  }
  Fn.prototype.setProp = function (val) {
    prop = val
  }
  return Fn
})()
// 建立實例
let obj = new Fn('aaa');
obj.setProp('bbb');
console.log(obj.getProp());  // 'bbb'
console.log(obj.prop);  // undefined複製代碼

模塊模式

模塊模式是爲單例建立私有變量和特權方法。單例,就是隻有一個實例的對象。

簡單來講,就是私有成員是惟一的,但必須經過特權/公有方法去訪問該私有成員。

let singleton = (function () {
  // 私有變量和方法
  let privateValue = 'value';
  function privateFunction(args) {
    console.log(args)
  }
  // 特權/公有方法和屬性
  return {
    usePrivateFunction(args, context) {
      privateFunction.call(context, args)
    },
    getPrivateValue() {
      return privateValue
    }
  }
})()
// 經過接口訪問私有方法
singleton.usePrivateFunction({ a: 1 });   // { a: 1 }
// 訪問私有屬性
console.log(singleton.getPrivateValue()); // "value"
複製代碼


函數建立對象的幾種模式

工廠模式

就像工廠流水線同樣,對原料進行屢次加工,最後輸出成品。

原理就是先建立一個空對象,而後爲其添加屬性方法,最後把對象返回

function factory(a, b) {
  let obj = new Object();
  obj.a = a;
  obj.b = b;
  return obj
}
let obj = factory(1, 2);  // { a: 1, b: 2 }
複製代碼

構造函數模式

要使用構造函數建立對象,必須使用 new 運算符。這種方式建立對象,會經歷如下 4 個階段:

  1. 建立一個新對象;
  2. 將構造函數的做用域賦給新對象(即 this 指向該對象);
  3. 執行構造函數中的代碼;
  4. 返回新對象。
function Fn(a, b) {
  this.a = a;
  this.b = b;
}
let obj = new Fn(1,2);  // { a: 1, b: 2 }複製代碼

原型模式

每一個函數都有 prototype 屬性,指向一個對象,該對象包含能夠由特定類型的全部實例共享的屬性和方法

簡單說,全部由該函數構造的實例對象,都共享同一個對象(內含屬性/方法)。

function Fn() { }
Fn.prototype.a = 1;
Fn.prototype.b = 1;
let obj = new Fn();
console.log(obj);   // {}
// 訪問 prototype 的屬性
console.log(obj.a); // 1複製代碼

寄生構造函數模式

此模式的代碼其實和工廠模式同樣,只是本模式使用"new"關鍵字建立。

所謂寄生,就是爲宿主添加本身的屬性和方法。其實就是建立一個實例對象,而後爲其添加屬性和方法,只是爲此打包成一個構造函數而已。

假設咱們須要一個特殊的數組,爲其添加指定的屬性和方法。以下:

function SpecialArray(){
  // 建立數組(宿主)
  let arr = new Array();
  // 添加值(寄生屬性)
  arr.push.apply(arr, arguments);
  // 添加方法(寄生方法)
  arr.toPipedString = function(){
    return this.join('|');
  }
  // 返回被寄生的宿主
  return arr
}
// 建立被寄生的實例
let colors = new SpecialArray('red','green','blue');複製代碼

要注意的是,使用此方法則沒法經過instance操做符肯定對象原型。對於上述例子,運行colors instanceof SpecialArray獲得 false 。


與繼承有關的幾種構造函數

借用構造函數

顧名思義,就是在子類構造函數的內部調用超類型構造函數,實例化的對象也會包含被借用的屬性和方法。(也被成爲僞造對象經典繼承

function Super(foo) {
  this.foo = foo;
  // 爲自身原型對象添加方法
  Super.prototype.baz = function () {
    return 'baz'
  }
}
// 借用構造函數
function Sub(foo, baz) {
  Super.apply(this, arguments)
  this.subBaz = baz
}
let obj = new Sub('f', 'b');
console.log(obj);         // { foo: 'f', subBaz: 'b' }
console.log(obj.baz());   // 報錯!
console.log(new Super('z').baz());  // 'baz'複製代碼

上述代碼了除了演示瞭如何建立"借用構造函數",還證實了其缺點:沒法繼承超類型構造函數的原型鏈

組合繼承 (原型鏈和借用構造函數的組合)

爲了解決借用構造函數的缺點。實現也很簡單,爲借用構造函數添加超類型構造函數的原型鏈。

一樣是"借用構造函數"示例代碼的例子,修改"借用構造函數"部份內容:

// 借用構造函數
function Sub(foo, baz) {
  Super.apply(this, arguments)
  this.subBaz = baz;
  // 添加原型鏈
  Sub.prototype = new Super(foo);
  Sub.prototype.constructor = Super;
}複製代碼

這個模式是最經常使用的繼承模式,但缺點是:調用了兩次超類型構造函數

原型式繼承

其思想就是,傳入一個對象,以該對象爲原型對象建立一個新對象。其實就是 ES5 的Object.creat()函數所實現的,具體不展開細說了。

寄生式繼承

思路與寄生構造函數和工廠模式相似,即建立一個僅用於封裝繼承過程的函數,該函數在內部以某種方式來加強對象,最後再像真的是它作了全部工做同樣返回對象。

簡單說就是,把多個加強某對象的代碼打包成一個函數。

function createAnother(original) {
  // 調用函數建立一個新對象(此函數不是必須,能夠任意形式建立對象)
  let clone = object(original);
  // 爲該對象添加屬性或方法,以加強該對象
  clone.sayHi = function () {
    console.log('Hi');
  }
  return clone
}
let person = { name: 'lisi', friends: ['a', 'b'] }
let anotherPerson = createAnother(person);
anotherPerson.sayHi();  // 'Hi'複製代碼

寄生組合式繼承

解決組合繼承模式的缺點。

所謂寄生組合式繼承,就是經過借用構造函數來繼承屬性,經過原型鏈的混成形式來繼承方法。

其背後的思路是:沒必要爲了指定子類型的原型而調用超類型的構造函數,爲其添加超類型原型的副本便可。

function inheritPrototype(sub, super) {
  // 建立對象
  let prototype = object(super.prototype);
  // 加強對象
  prototype.constructor = sub;
  // 指定對象
  sub.prototype = prototype;
}複製代碼
相關文章
相關標籤/搜索