前端進階-執行時函式

moduzhang發表於2018-11-15

一級函式

函式是一級函式

在 JavaScript 中,函式是一級函式。這意味著,就像物件一樣,你可以像處理其他元素(如數字、字串、陣列等)一樣來處理函式。JavaScript 函式可以:

  • 儲存在變數中
  • 從一個函式返回
  • 作為引數傳遞給另一個函式

注意,雖然我們可以將函式當作物件來處理,但是函式和物件之間的一個主要區別是,函式可以被呼叫(即使用 () 執行),而常規物件則不能。

在很多方面,JavaScript 中的函式都可以被當作一個值。你完全可以從一個函式中返回它,將其儲存在一個變數中,甚至作為引數傳遞給另一個函式

函式可以返回函式

函式必須始終返回一個值。無論是在 return 語句中顯式指定一個值(例如,返回一個字串布林值陣列等),還是函式隱式地返回 undefined(例如,一個簡單地將某些東西記錄到控制檯的函式),函式始終只會返回一個值

既然我們知道函式是一級函式,我們可以將函式作為一個值,十分簡便地從函式返回函式!返回另一個函式的函式被稱為高階函式。請考慮以下示例:

function alertThenReturn() {
  alert('Message 1!');

  return function () {
    alert('Message 2!');
  };
}

如果在瀏覽器中呼叫 alertThenReturn(),我們會先看到一條提示訊息,寫著 Message 1!,接著是 alertThenReturn() 函式,它會返回一個匿名函式。但是,我們並不會看到一個 Message 2! 提示,因為並未執行內部函式中的任何程式碼。那麼,我們如何執行所返回的函式呢?

由於 alertThenReturn() 會返回一個函式,因此我們可以給這個返回值分配一個變數

const innerFunction = alertThenReturn();

然後,我們可以像使用其他函式一樣使用 innerFunction 變數!

innerFunction();
// 顯示 'Message 2!'

同樣,這個函式可以被立即呼叫,而無需儲存在一個變數中。如果我們簡單地向 alertThenReturn() 新增另一組圓括號,我們仍然會得到相同的結果:

alertThenReturn()();
// 顯示 'Message 1!' 然後顯示 'Message 2!'

請注意這個函式呼叫中的兩對圓括號(即 ()())!第一對圓括號將執行 alertThenReturn 函式。這個呼叫的返回值將返回一個函式,然後再被第二對圓括號呼叫!

在這裡插入圖片描述

回撥

回撥函式

JavaScript 函式是一級函式。我們可以像處理其他值一樣來處理函式——包括將它們傳遞給其他函式!接受其他函式作為引數(和/或返回函式,如上一部分所述)的函式被稱為高階函式。作為引數傳入其它函式中的函式被稱為回撥函式接收函式可以在執行自己的程式碼後執行回撥函式

關於回撥:

  • 作為引數傳遞給另一個函式的函式被稱為回撥函式
  • 接受另一個函式作為引數的函式是一個高階函式
  • 我們可以利用回撥函式,因為 JavaScript 函式是一級函式

陣列方法

forEach()

陣列的 forEach() 方法接受一個回撥函式,併為陣列中的每個元素呼叫該函式。換句話說,forEach() 讓你可以迭代(即遍歷)一個陣列,類似於使用 for 迴圈。

array.forEach(function callback(currentValue, index, array) {
	// 回撥函式本身會接收引數:當前陣列元素、其索引和整個陣列本身。
    // 函式程式碼寫在這裡
});

將一個匿名函式作為引數傳遞給 forEach() 是很常見的:

[1, 5, 2, 4, 6, 3].forEach(function logIfOdd(n) {
  if (n % 2 !== 0) {
    console.log(n);
  }
});

[1, 5, 2, 4, 6, 3].forEach(function (n) {
  if (n % 2 !== 0) {
    console.log(n);
  }
});

// 1
// 5
// 3

另外,也可以簡單地傳入函式的名稱(當然,假設函式已經被定義了)。

[1, 5, 2, 4, 6, 3].forEach(logIfOdd);

// 1
// 5
// 3

map()

陣列的 map() 方法類似於 forEach(),也會為陣列中的每個元素呼叫一個回撥函式。但是,.map() 會根據回撥函式所返回的內容返回一個新的陣列

const names = ['David', 'Richard', 'Veronika'];

const nameLengths = names.map(function(name) {
  return name.length;
});

請記住,.forEach().map() 之間的主要區別在於,.forEach() 不會返回任何東西,而 .map() 則會返回一個新的陣列,其中包含從該函式返回的值。

map() 方法會返回一個新的陣列,而不會修改原始陣列。

map()

const musicData = [
    { artist: 'Adele', name: '25', sales: 1731000 },
    { artist: 'Drake', name: 'Views', sales: 1608000 },
    { artist: 'Beyonce', name: 'Lemonade', sales: 1554000 },
    { artist: 'Chris Stapleton', name: 'Traveller', sales: 1085000 },
    { artist: 'Pentatonix', name: 'A Pentatonix Christmas', sales: 904000 },
    { artist: 'Original Broadway Cast Recording', 
      name: 'Hamilton: An American Musical', sales: 820000 },
    { artist: 'Twenty One Pilots', name: 'Blurryface', sales: 738000 },
    { artist: 'Prince', name: 'The Very Best of Prince', sales: 668000 },
    { artist: 'Rihanna', name: 'Anti', sales: 603000 },
    { artist: 'Justin Bieber', name: 'Purpose', sales: 554000 }
];

const albumSalesStrings = musicData.map(function(music) {
    return `${music.name} by ${music.artist} sold ${music.sales} copies`;
});

console.log(albumSalesStrings);

/**
[ '25 by Adele sold 1731000 copies',
  'Views by Drake sold 1608000 copies',
  'Lemonade by Beyonce sold 1554000 copies',
  'Traveller by Chris Stapleton sold 1085000 copies',
  'A Pentatonix Christmas by Pentatonix sold 904000 copies',
  'Hamilton: An American Musical by Original Broadway Cast Recording sold 820000 copies',
  'Blurryface by Twenty One Pilots sold 738000 copies',
  'The Very Best of Prince by Prince sold 668000 copies',
  'Anti by Rihanna sold 603000 copies',
  'Purpose by Justin Bieber sold 554000 copies' ]
*/

filter()

陣列的 filter() 方法與 map() 方法類似:

  • 它在一個陣列上被呼叫
  • 它將一個函式作為引數
  • 它會返回一個新的陣列

區別在於,傳遞給 filter() 的函式會被用作一個測試,只有陣列中通過測試的專案會被包含在新的陣列中。

const names = ['David', 'Richard', 'Veronika'];

const shortNames = names.filter(function(name) {
  return name.length < 6;
});
// ['David']

map() 一樣,傳遞給 filter() 的函式會為 names 陣列中的每個專案被呼叫。第一個專案(即 ‘David’)會被儲存在 name 變數中。然後執行測試——這一步會進行過濾。首先,它會檢查該名稱的長度。如果它是 6 或更大,則會被跳過(而不會被包含在新陣列中!)。相反,如果該名稱的長度小於 6,那麼 name.length < 6 則會返回 true,並且該名稱會被包含在新陣列中

因此,shortNames 將是新的陣列 ['David']。請注意,它現在只包含一個名稱,因為 'Richard''Veronika' 都有 6 個或更長的字元,因此都被過濾掉了。

const musicData = [
    { artist: 'Adele', name: '25', sales: 1731000 },
    { artist: 'Drake', name: 'Views', sales: 1608000 },
    { artist: 'Beyonce', name: 'Lemonade', sales: 1554000 },
    { artist: 'Chris Stapleton', name: 'Traveller', sales: 1085000 },
    { artist: 'Pentatonix', name: 'A Pentatonix Christmas', sales: 904000 },
    { artist: 'Original Broadway Cast Recording', 
      name: 'Hamilton: An American Musical', sales: 820000 },
    { artist: 'Twenty One Pilots', name: 'Blurryface', sales: 738000 },
    { artist: 'Prince', name: 'The Very Best of Prince', sales: 668000 },
    { artist: 'Rihanna', name: 'Anti', sales: 603000 },
    { artist: 'Justin Bieber', name: 'Purpose', sales: 554000 }
];

const results = musicData.filter(function(music) {
    const nameLength = music.name.length;
    return (nameLength >= 10) && (nameLength <= 25);
});

console.log(results);
/**
[ { artist: 'Pentatonix',
    name: 'A Pentatonix Christmas',
    sales: 904000 },
  { artist: 'Twenty One Pilots',
    name: 'Blurryface',
    sales: 738000 },
  { artist: 'Prince',
    name: 'The Very Best of Prince',
    sales: 668000 } ]
*/

陣列方法

作用域

函式的作用域描述了給定函式內的可用變數。函式內的程式碼究竟能夠訪問什麼呢?

  • 該函式的引數
  • 該函式內宣告的區域性變數
  • 來自其父函式作用域的變數
  • 全域性變數

在這裡插入圖片描述

巢狀的 child() 函式可以訪問所有 ab、和 c 變數,也就是說,這些變數都在 child() 函式的作用域內。

const myName = 'Andrew';
// 全域性變數

function introduceMyself() {

  const you = 'student';
  // 已宣告的變數,其中定義了 introduce()
  // (換句話說,在 introduce() 的父函式 introduceMyself() 中宣告的變數)

  function introduce() {
    console.log(`Hello, ${you}, I'm ${myName}!`);
  }

  return introduce();
}

introduceMyself();
// Hello, student, I'm Andrew!

JavaScript 使用函式作用域

JavaScript 中的變數傳統上是在函式作用域內定義的,而不是在塊作用域內。由於輸入一個函式會改變作用域,因此在函式內部定義的變數在該函式外部是不可用的。相反,如果在塊中定義了任何變數(例如,在 if 語句中),則這些變數在該塊外部是可用的

ES6 語法允許額外的作用域,並使用 letconst 關鍵字來宣告變數。這些關鍵字在 JavaScript 中用於宣告塊作用域變數,並在很大程度上取代了使用 var 的需求。

作用域鏈

在這裡插入圖片描述

當解析變數時,JavaScript 引擎惠先檢視巢狀子函式的區域性定義變數。如果能夠找到,則檢索該值;否則,JavaScript 引擎會繼續向外查詢,直到變數被解析。如果 JavaScript 引擎已到達全域性作用域,但仍然無法解析變數,則該變數為未定義。

變數陰影

當你所建立的變數與作用域鏈中的另一個變數具有相同名稱時,會發生什麼?

區域性作用域的變數只會暫時“遮蔽”外部作用域中的變數。這被稱為變數陰影。

const symbol = '¥';

function displayPrice(price) {
  const symbol = '$';
  console.log(symbol + price);
}

displayPrice('80');
// $80

總而言之,如果在不同上下文中的變數之間有任何命名重疊,則會通過從內部作用域到外部作用域(即從區域性一直到全域性)遍歷作用域鏈來解決。因此,區域性變數總是優先於更寬作用域內與其同名的變數

JavaScript 引擎會先檢視最內層,然後向外查詢——從直接在函式中定義的區域性變數,直到全域性作用域內的變數(如有必要)。

函式和函式作用域

閉包(Closure)

示例一

function outerFunction() {
  let num1 = 5;

  return function(num2) {
    console.log(num1 + num2);
  };
}

let result = outerFunction();

result(10);
// ???

outerFunction() 被返回後,看起來好像它的所有區域性變數都會被分配回可用的記憶體。但是事實證明,巢狀的 innerFunction() 仍然可以訪問 num1 變數!

outerFunction() 會返回一個對內部巢狀函式的引用。這個呼叫的返回值將儲存在 result 中。當這個函式被呼叫時,它會保持對其作用域的訪問;也就是它最初被定義的時候能夠訪問的所有變數。這包括在其父作用域中的 num1 變數。這個巢狀函式會遮蔽這些變數,只要對該函式本身的引用仍然存在,這些變數就會一直存在。

這樣,當 result(10); 被執行時,該函式仍然可以訪問 num1 的值 5。因此,15 會被記錄到控制檯。

示例二

function myCounter() {
	let count = 0;
	return function() {
		count += 1;
		return count;
	}
}
let counter = myCounter();
counter(); // 1
counter(); // 2
counter(); // 3
counter.count; // undefined
counte; // Uncaught ReferenceError: count is not defined...

使用閉包建立私有狀態的真正好處是,閉包本身之外的任何函式,根本無法訪問 count 狀態或 count 資料。私有狀態很實用,因為現在使用者無法意外地重置該 count 了,外部函式根本無法訪問該資料。

閉包的應用

閉包的兩個常見和強大的應用:

  • 隱含地傳遞引數
  • 在函式宣告中,儲存作用域的快照

垃圾回收

JavaScript 通過自動垃圾回收來管理記憶體。這意味著,當資料不再可引用時(即沒有可用於可執行程式碼的對該資料的剩餘引用),它將被“垃圾回收”,並在稍後的某個時間點被銷燬。這可以釋放該資料曾經消耗的資源(即計算機記憶體),從而使這些資源可供重新使用。

父函式的變數可以被巢狀的內層函式訪問。如果巢狀函式捕獲並使用其父函式的變數(或其作用域鏈上的變數,如其父函式的父函式的變數),那麼只要使用這些變數的函式仍可被引用,這些變數就會一直保留在記憶體中。

閉包是指函式和該函式宣告位置的詞法環境的組合。每次定義函式時,都會為該函式建立閉包。對於在一個函式中定義另一個函式的情況,閉包尤其強大,它讓巢狀函式可以訪問其外部的變數。即使父函式已返回,函式也會保留一個到其父作用域的連結。這可以防止父函式內的資料被垃圾回收

記憶體管理
閉包
詞法環境(英)

立即呼叫函式表示式(IIFE)

函式宣告與函式表示式

函式宣告會定義一個函式,而不需要將變數賦給函式。它只是宣告一個函式,而不會返回一個值

function returnHello() {
  return 'Hello!';
}

函式表示式會返回一個值。函式表示式可以是匿名或命名的,並且是另一個表示式語法的一部分。它們通常也會賦給變數

// 匿名
const myFunction = function () {
  return 'Hello!';
};

// 命名
const otherFunction = function returnHello() {
  return 'Hello!';
};

立即呼叫函式表示式:結構和語法

立即呼叫函式表示式或 IIFE 是在定義之後立即被呼叫的函式

(function sayHi(){
    alert('Hi there!');
  }
)();
// 展示 'Hi there!'

向 IIFE 傳遞引數

(function (name){
    alert('Hi, ' + name);
  }
)('Andrew');

// 展示 'Hi, Andrew'

(function (x, y){
    console.log(x * y);
  }
)(2, 3);

// 6

IIFE 和私有作用域

IIFE 的主要用途之一就是建立私有作用域。JavaScript 中的變數傳統上遵循函式作用域。我們可以利用閉包的行為方式來保護變數或方法不被訪問

const myFunction = (
  function () {
    const hi = 'Hi!';
    return function () {
      console.log(hi);
    }
  }
)();

在這裡插入圖片描述

myFunction 指向一個帶有區域性定義變數 hi 和一個返回函式的 IIFE,該函式會遮蔽 hi,並將其值輸出到控制檯。

  • IIFE 可以用於建立私有作用域
  • IIFE 與作用域和閉包密切相關
  • 有一種替代語法可以用於編寫 IIFE(把第一個右括號移到最後)

IIFE、私有作用域和事件處理

假設我們想在頁面上建立一個按鈕,每隔一次點選就提醒使用者。這樣做的第一步思路可以是跟蹤按鈕被點選的次數。但是,我們應該如何保持這個資料呢?

我們可以使用在全域性作用域內宣告的一個變數來跟蹤計數(如果應用程式的其他部分需要訪問計數資料,這樣做就很合理)。但是,更好的方式是將這些資料放在事件處理器中!首先,它可以防止我們使用額外的變數來汙染全域性(還可能會發生名稱衝突)。更重要的是:如果我們使用 IIFE,我們就可以利用閉包來保護 count 變數不被外部訪問!這可以防止意外的改變或未預期的連帶結果無意中改變計數。

<!-- button.html -->
<html>
  <body>
     <button id='button'>Click me!</button>
     <script src='button.js'></script>
  </body>
</html>
// button.js
const button = document.getElementById('button');
button.addEventListener('click', (function() {
  let count = 0;

  return function() {
    count += 1;

    if (count === 2) {
      alert('This alert appears every other press!');
      count = 0;
    }
  };
})());

首先,我們宣告瞭一個區域性變數 count,它最初被設定為 0。然後,我們從_該_函式返回一個函式。所返回的函式會遞增 count,但當計數達到 2 時,則會提醒使用者,並將計數重置為 0。

需要特別指出的是,所返回的函式會遮蔽 count 變數。也就是說,由於函式會維持對其父函式作用域的引用,count 可供所返回的函式使用!因此,我們立即呼叫返回該函式的函式。而且,由於所返回的函式可以訪問內部變數 count,所以建立了一個私有作用域,以便有效地保護該資料!我們將 count 放在一個閉包中,從而讓我們可以保留每次點選的資料。

(function(n){
  delete n;
  return n;
})(2);// 返回值為2

delete 運算子實際上只會影響物件的屬性;它不會用於直接釋放資源(即釋放記憶體),也不會影響變數或函式名稱。因此,傳入這個立即呼叫函式表示式的數字 2 將被返回。

立即呼叫函式表示式的好處

我們已經知道,使用立即呼叫函式表示式可以建立一個私有作用域來保護變數或方法不被訪問。IIFE 最終會使用所返回的函式來訪問閉包內的私有資料。這樣做很有好處:雖然這些所返回的函式可以公開訪問,但它們仍可保持內部定義變數的私有性

總而言之,如果你只想完成某個一次性任務(例如初始化應用程式),那麼 IIFE 將是完成任務,同時避免額外變數汙染全域性環境的好辦法。畢竟,清理全域性名稱空間可以減少重複變數名稱衝突的機率。

JavaScript 設計模式(豆瓣)

相關文章