好程式設計師技術文件HTML5開發中的javascript閉包

好程式設計師IT發表於2019-04-03

   好程式設計師 技術文件 HTML5 開發中的 javascript 閉包 事實上,透過使用閉包,我們可以做很多事情。比如模擬物件導向的程式碼風格 ; 更優雅,更簡潔的表達出程式碼 ; 在某些方面提升程式碼的執行效率,同時避免對名稱空間的汙染,最重要的是可以從一個域中取出原本訪問不到的變數去使用。

 

  函式的作用域:

 

  1. 變數的宣告會提前。

 

  2. 全域性變數和區域性變數。

 

  3. 函式作用域的生命週期。

 

  變數的宣告

 

  tt='ee';

 

  function b(){

 

  function test(){

 

  alert(tt);

 

  var tt='ff';

 

  }

 

  test()

 

  }

 

  b()

 

  第一段函式返回的是一個 undefined , 因為變數的宣告會提前相當於

 

  var tt='123'

 

  function test(){

 

  var tt;

 

  alert(tt);

 

  tt='ff';

 

  }

 

  2.

 

  tt='ee';

 

  function b(){

 

  function test(){

 

  alert(tt);

 

  /*var tt='ff';*/

 

  }

 

  test()

 

  }

 

  b()

 

  變數作用域

 

  1.function outFn(){

 

  function innerFn(){

 

  var inner=0;

 

  inner++;

 

  console.log('inner='+inner)

 

  }

 

  return innerFn;

 

  }

 

  var fn1=outFn();

 

  fn1();

 

  fn1();

 

  var fn2=outFn();

 

  fn2();

 

  fn2();

 

  // 每當這個內部函式被呼叫的時候,都會重新生命一個 inner 變數,然後讓這個變數自增。

 

  2.

 

  var inner=0;

 

  function outFn(){

 

  function innerFn(){

 

  inner++;

 

  console.log('inner='+inner)

 

  }

 

  return innerFn;

 

  }

 

  var fn1=outFn();

 

  fn1();

 

  fn1();

 

  var fn2=outFn();

 

  fn2();

 

  fn2();

 

  // 每當內部函式被呼叫時,都會讓全域性變數 inner 自增。

 

  如果讓這個函式變成父元素的區域性變數,會是什麼結果 ?

 

  3.

 

  function outFn(){

 

  var inner=0;

 

  function innerFn(){

 

  inner++;

 

  console.log('inner='+inner)

 

  }

 

  return innerFn;

 

  }

 

  var fn1=outFn();

 

  fn1();

 

  fn1();

 

  var fn2=outFn();

 

  fn2();

 

  fn2();

 

  一段概念:

 

  這次的結果可能會出人預料,因為當內部函式在定義它的作用域的外部被引用的時候,就建立了改內部函式的一個閉包,這種情況下我們稱既不是區域性變數也不是其引數的變數為自由變數,稱內部函式的呼叫環境為封閉閉包的環境。從本質上將,如果內部函式引用了位於外部函式中的變數,相當於授權該變數能夠被延遲使用。因此,當外部函式呼叫完成後,這些變數的記憶體不會被釋放,因為閉包仍然需要使用它們。

 

 

 

  閉包

 

  一句話理解閉包: JavaScript 中的函式執行在它們被定義的作用域裡,而不是它們被執行的作用域裡。

 

  一個故事理解閉包

 

  我的年齡是秘密,你想知道。

 

  但是每次我都含糊其辭的對你說 undefined;

 

  為了防止我自己也忘記或搞錯自己的年齡,我辦了一張身份證,上面記錄我的年齡資訊,藏在我家裡。

 

  你知道了這件事,為了得到我的年齡,決定對我投其所好,

 

  於是你送我一隻逗比間諜貓。

 

  作為感謝我給了你一把我家的鑰匙,方便你有空來看貓。

 

  這隻貓實在太神奇了,每次都能找到我的身份證,並把資訊傳遞給你。

 

  於是你每次想知道我的年齡的時候就來看貓,

 

  然後間諜貓每次都能把我的最新的年齡資訊反饋給你。

 

  var day=0;

 

  var timer=setInterval("dayChanges()",(24*60*60*1000)); // 定時器,

 

  function dayChanges(){

 

  day++;

 

  } // 每過 24 小時 次函式執行一次,我的年齡又多了一天

 

  //------------ 下面的程式碼就是一個比較常見的閉包。

 

  function isMyHome(){ // 我家

 

  var myAge=0; // 我的身份證資訊

 

  if(day%365==0){ // 我的年齡變化

 

  myAge+=1;

 

  }

 

  function myHomeKey(){ // 我家鑰匙

 

  return myAge; //return 就是間諜貓

 

  }

 

  return myHomeKey; // 給你我家的鑰匙。

 

  }

 

  var key=isMyHome(); // 你拿到我家鑰匙

 

  var you=key(); // 得到年齡。

 

 

  匿名函式:

 

  (functoin( ){

 

  alert( 2 )

 

  })()

 

  特性: 1. 自呼叫

 

  2. 引數傳入方式 ;

 

  閉包的使用方法:

 

  1.

 

  function outFn(){

 

  console.log(' 我是外部函式 ');

 

  function innerFn(){

 

  console.log(' 我是內部函式 ');

 

  }

 

  innerFn()

 

  }

 

  outFn();

 

  // 可以在外部函式呼叫到內部函式,但是並不能在外部呼叫內部函式。這種技術適合用於小型,單用途的函式。

 

  2. 在任何地方呼叫內部函式,透過一個全域性變數可以訪問到內部函式 ;

 

  var global;

 

  function outFn(){

 

  console.log(' 我是外部函式 ')

 

  function innerFn(){

 

  console.log(' 我是內部函式 ')

 

  }

 

  global=innerFn;

 

  }

 

  outFn();

 

  global();

 

  3. 一個最簡單的閉包,可以迅速呼叫,並且不會汙染全域性變數。

 

  var data = {

 

  table : [1,2,3,4,5],

 

  tree : {}

 

  };

 

  (function(data){

 

  alert(data.table)

 

  //build dm.tree

 

  })(data);

 

  4. 可以透過對變數進行封裝實現閉包。

 

  var obj=(function(){

 

  var name=' 找不見的名字 ';

 

  return {

 

  getName:function(){

 

  return name;

 

  },

 

  setName:function(newName){

 

  return newName

 

  }

 

  }

 

  })()

 

  obj.getName()

 

  高階函式英文叫 Higher-order function 。那麼什麼是高階函式 ?

 

  JavaScript 的函式其實都指向某個變數。既然變數可以指向函式,函式的引數能接收變數,那麼一個函式就可以接收另一個函式作為引數,這種函式就稱之為高階函式。

 

  一個最簡單的高階函式:

 

  function add(x, y, f) {

 

  return f(x) + f(y);

 

  }

 

  當我們呼叫 add(-5, 6, Math.abs) 時,引數 x y f 分別接收 -5 6 和函式 Math.abs ,根據函式定義,我們可以推導計算過程為:

 

  x = -5;

 

  y = 6;

 

  f = Math.abs;

 

  f(x) + f(y) ==> Math.abs(-5) + Math.abs(6) ==> 11;

 

  return 11;

 

  用程式碼驗證一下:

 

  add(-5, 6, Math.abs); // 11

 

  編寫高階函式,就是讓函式的引數能夠接收別的函式

 

  1. 高階函式 map

 

  舉例說明,比如我們有一個函式 f(x)=x2 ,要把這個函式作用在一個陣列 [1, 2, 3, 4, 5, 6, 7, 8, 9] 上,就可以用 map 實現如下:

 

 

  由於 map() 方法定義在 JavaScript Array 中,我們呼叫 Array map() 方法,傳入我們自己的函式,就得到了一個新的 Array 作為結果:

 

  function pow(x) {

 

  return x * x;

 

  }

 

  var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];

 

  arr.map(pow); // [1, 4, 9, 16, 25, 36, 49, 64, 81]

 

  map() 傳入的引數是 pow ,即函式物件本身。

 

  你可能會想,不需要 map() ,寫一個迴圈,也可以計算出結果:

 

  var f = function (x) {

 

  return x * x;

 

  };

 

  var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];

 

  var result = [];

 

  for (var i=0; i

 

  result.push(f(arr[i]));

 

  }

 

  的確可以,但是,從上面的迴圈程式碼,我們無法一眼看明白 “把 f(x) 作用在 Array 的每一個元素並把結果生成一個新的 Array ”。

 

  所以, map() 作為高階函式,事實上它把運算規則抽象了,因此,我們不但可以計算簡單的 f(x)=x2 ,還可以計算任意複雜的函式,比如,把 Array 的所有數字轉為字串:

 

  var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];

 

  arr.map(String); // ['1', '2', '3', '4', '5', '6', '7', '8', '9']

 

  只需要一行程式碼。

 

  2. 高階函式 reduce

 

  再看 reduce 的用法。 Array reduce() 把一個函式作用在這個 Array [x1, x2, x3...] 上,這個函式必須接收兩個引數, reduce() 把結果繼續和序列的下一個元素做累積計算,其效果就是:

 

  [x1, x2, x3, x4].reduce(f) = f(f(f(x1, x2), x3), x4)

 

  比方說對一個 Array 求和,就可以用 reduce 實現:

 

  var arr = [1, 3, 5, 7, 9];

 

  arr.reduce(function (x, y) {

 

  return x + y;

 

  }); // 25

 

  練習:利用 reduce() 求積。

 

  練習:把 [1, 3, 5, 7, 9] 變換成整數 13579 ,用 reduce()

 

  練習:請用 map() 把使用者輸入的不規範的英文名字,變為首字母大寫,其他小寫的規範名字。輸入: ['adam', 'LISA', 'barT'] ,輸出: ['Adam', 'Lisa', 'Bart']

 

  3. 高階函式 filter

 

  filter 也是一個常用的操作,它用於把 Array 的某些元素過濾掉,然後返回剩下的元素。

 

  和 map() 類似, Array filter() 也接收一個函式。和 map() 不同的是, filter() 把傳入的函式依次作用於每個元素,然後根據返回值是 true 還是 false 決定保留還是丟棄該元素。

 

  例如,在一個 Array 中,刪掉偶數,只保留奇數,可以這麼寫:

 

  var arr = [1, 2, 4, 5, 6, 9, 10, 15];

 

  var r = arr.filter(function (x) {

 

  return x % 2 !== 0;

 

  });

 

  r; // [1, 5, 9, 15]

 

  把一個 Array 中的空字串刪掉,可以這麼寫:

 

  var arr = ['A', '', 'B', null, undefined, 'C', ' '];

 

  var r = arr.filter(function (s) {

 

  return s && s.trim(); // 注意: IE9 以下的版本沒有 trim() 方法

 

  });

 

  r; // ['A', 'B', 'C']

 

  可見用 filter() 這個高階函式,關鍵在於正確實現一個“篩選”函式。

 

  練習:請使用 filter() 篩選出一個陣列中所有的素數

 

  函式作為返回值

 

  高階函式除了可以接受函式作為引數外,還可以把函式作為結果值返回。

 

  我們來實現一個對 Array 的求和。通常情況下,求和的函式是這樣定義的:

 

  function sum(arr) {

 

  return arr.reduce(function (x, y) {

 

  return x + y;

 

  });

 

  }

 

  sum([1, 2, 3, 4, 5]); // 15

 

  但是,如果不需要立刻求和,而是在後面的程式碼中,根據需要再計算怎麼辦 ? 可以不返回求和的結果,而是返回求和的函式 !

 

  function lazy_sum(arr) {

 

  var sum = function () {

 

  return arr.reduce(function (x, y) {

 

  return x + y;

 

  });

 

  }

 

  return sum;

 

  }

 

  當我們呼叫 lazy_sum() 時,返回的並不是求和結果,而是求和函式:

 

  var f = lazy_sum([1, 2, 3, 4, 5]); // function sum()

 

  呼叫函式 f 時,才真正計算求和的結果:

 

  f(); // 15

 

  在這個例子中,我們在函式 lazy_sum 中又定義了函式 sum ,並且,內部函式 sum 可以引用外部函式 lazy_sum 的引數和區域性變數,當 lazy_sum 返回函式 sum 時,相關引數和變數都儲存在返回的函式中,這種稱為“閉包 (Closure) ”的程式結構擁有極大的威力。

 

  請再注意一點,當我們呼叫 lazy_sum() 時,每次呼叫都會返回一個新的函式,即使傳入相同的引數:

 

  var f1 = lazy_sum([1, 2, 3, 4, 5]);

 

  var f2 = lazy_sum([1, 2, 3, 4, 5]);

 

  f1 === f2; // false

 

  f1() f2() 的呼叫結果互不影響。

 

  閉包

 

  注意到返回的函式在其定義內部引用了區域性變數 arr ,所以,當一個函式返回了一個函式後,其內部的區域性變數還被新函式引用,所以,閉包用起來簡單,實現起來可不容易。

 

  還有一個需要注意的問題是,返回的函式並沒有立刻執行,而是直到呼叫了 f() 才執行。我們來看一個例子:

 

  function count() {

 

  var arr = [];

 

  for (var i=1; i<=3; i++) {

 

  arr.push(function () {

 

  return i * i;

 

  });

 

  }

 

  return arr;

 

  }

 

  var results = count();

 

  var f1 = results[0];

 

  var f2 = results[1];

 

  var f3 = results[2];

 

  在上面的例子中,每次迴圈,都建立了一個新的函式,然後,把建立的 3 個函式都新增到一個 Array 中返回了。

 

  你可能認為呼叫 f1() f2() f3() 結果應該是 1 4 9 ,但實際結果是:

 

  f1(); // 16

 

  f2(); // 16

 

  f3(); // 16

 

  全部都是 16! 原因就在於返回的函式引用了變數 i ,但它並非立刻執行。等到 3 個函式都返回時,它們所引用的變數 i 已經變成了 4 ,因此最終結果為 16

 

  返回閉包時牢記的一點就是:返回函式不要引用任何迴圈變數,或者後續會發生變化的變數。

 

  如果一定要引用迴圈變數怎麼辦 ? 方法是再建立一個函式,用該函式的引數繫結迴圈變數當前的值,無論該迴圈變數後續如何更改,已繫結到函式引數的值不變:

 

  function count() {

 

  var arr = [];

 

  for (var i=1; i<=3; i++) {

 

  arr.push((function (n) {

 

  return function () {

 

  return n * n;

 

  }

 

  })(i));

 

  }

 

  return arr;

 

  }

 

  var results = count();

 

  var f1 = results[0];

 

  var f2 = results[1];

 

  var f3 = results[2];

 

  f1(); // 1

 

  f2(); // 4

 

  f3(); // 9

 

  注意這裡用了一個 “建立一個匿名函式並立刻執行”的語法:

 

  (function (x) {

 

  return x * x;

 

  })(3); // 9

 

  理論上講,建立一個匿名函式並立刻執行可以這麼寫:

 

  function (x) { return x * x } (3);

 

  但是由於 JavaScript 語法解析的問題,會報 SyntaxError 錯誤,因此需要用括號把整個函式定義括起來:

 

  (function (x) { return x * x }) (3);

 

  通常,一個立即執行的匿名函式可以把函式體拆開,一般這麼寫:

 

  (function (x) {

 

  return x * x;

 

  })(3);

 

  說了這麼多,難道閉包就是為了返回一個函式然後延遲執行嗎 ?

 

  當然不是 ! 閉包有非常強大的功能。舉個例子:

 

  在物件導向的程式設計語言裡,比如 Java C++ ,要在物件內部封裝一個私有變數,可以用 private 修飾一個成員變數。

 

  在沒有 class 機制,只有函式的語言裡,藉助閉包,同樣可以封裝一個私有變數。我們用 JavaScript 建立一個計數器:

 

  'use strict';

 

  function create_counter(initial) {

 

  var x = initial || 0;

 

  return {

 

  inc: function () {

 

  x += 1;

 

  return x;

 

  }

 

  }

 

  }

 

  它用起來像這樣:

 

  var c1 = create_counter();

 

  c1.inc(); // 1

 

  c1.inc(); // 2

 

  c1.inc(); // 3

 

  var c2 = create_counter(10);

 

  c2.inc(); // 11

 

  c2.inc(); // 12

 

  c2.inc(); // 13

 

  在返回的物件中,實現了一個閉包,該閉包攜帶了區域性變數 x ,並且,從外部程式碼根本無法訪問到變數 x 。換句話說,閉包就是攜帶狀態的函式,並且它的狀態可以完全對外隱藏起來。

 

  閉包還可以把多引數的函式變成單引數的函式。例如,要計算 xy 可以用 Math.pow(x, y) 函式,不過考慮到經常計算 x2 x3 ,我們可以利用閉包建立新的函式 pow2 pow3

 

  function make_pow(n) {

 

  return function (x) {

 

  return Math.pow(x, n);

 

  }

 

  }

 

  // 建立兩個新函式 :

 

  var pow2 = make_pow(2);

 

  var pow3 = make_pow(3);

 

  pow2(5); // 25

 

  pow3(7); // 343


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69913892/viewspace-2640267/,如需轉載,請註明出處,否則將追究法律責任。

相關文章