JavaScript 作用域和閉包

Photo by rocknwool on Unsplash

JavaScript 作用域和閉包

·

3 min read

關注問題

  • this 的不同應用場景,如何取值?

  • 手寫 bind 函數

知識點

  • 作用域和自由變量

    • 全局作用域

    • 函數作用域

    • 塊級作用域

  • 閉包:兩種常見方式 & 自由變量查找規則

  • this

閉包

  • 作用域應用特殊狀態,有兩種狀況
function create() {
  let a = 100;
  return function () {
    console.log(a);
  };
}

let fn = create();
let a = 200;
fn(); // 100
function print(fn) {
  let a = 200;
  fn();
}

let a = 100;
function fn() {
  console.log(a);
}

print(fn); // 100

閉包:所有的自由變量(上面範例為 a)的查找,是在函数定義的地方,向上级作用域查找 不是在執行的地方!!


  • 函數做為參數被傳遞

  • 函數作為返回值被返回

閉包的應用

緩存機制:手寫 cache 緩存函數

function cached(fn) {
  const cache = {};

  return (...args) => {
    const key = JSON.stringify(args);

    if (key in cache) {
      return cache[key];
    } else {
      const val = fn(...args);
      cache[key] = val;
      return val;
    }
  };
}

這段程式碼定義了一個名為 cached 的高階函式,它的參數是一個函式 fncached 的作用是建立一個緩存(cache),用於存儲 fn 函式運算後的結果。由於緩存是使用閉包(closure)的方式實現的,因此 cache 變數將被儲存在函式的作用域中,不會因為函式的調用而被清除。

cached 函式內部定義了一個箭頭函式,它的作用是對於每一次函式調用,檢查傳入的引數(args)是否已經有過相同的呼叫,如果是,則從緩存(cache)中取回之前儲存的結果並直接返回,否則就將函式運算得到的結果放入緩存中,以便後續調用時可以直接返回。這樣就可以減少計算時間,提高效率。

具體地,cached 先聲明了一個空物件 cache,用來存儲緩存內容。接著返回一個箭頭函式,該函式通過擴展運算符將所有的引數都收集到一個名為 args 的陣列中。函式將引數轉換為一個 JSON 字串,並使用該字串作為緩存的鍵(key)。

接下來,函式檢查是否有該鍵對應的緩存。如果是,直接返回緩存內容;否則,調用 fn 函式,得到結果值並將其存入緩存中,然後返回該值。

最終,當使用 cached 函式時,將需要緩存的函式作為引數傳遞給 cached 函式,返回的值是一個使用了緩存的新函式,可以在需要時重複使用該函式,而不必重新運算。

const key = JSON.stringify(args) 的作用?

const key = JSON.stringify(args); 這行程式碼的作用是把傳入的 args 陣列轉換成一個字串,用作緩存的鍵(key)。

以一個簡單的例子為例,假設我們有以下函式:

const add = (a, b) => a + b;

現在,我們使用 cached 函式來創建一個可以緩存結果的新函式:

const cachedAdd = cached(add);

當我們第一次使用 cachedAdd(2, 3) 時,由於緩存中沒有相應的結果,所以它會調用 add(2, 3),並將結果儲存在緩存中。此時 key 的值會是 "[2,3]",因為 args 陣列裡面只有兩個元素,分別是 23,並使用 JSON.stringify 方法將其轉換成一個字串。

接下來,當我們再次使用 cachedAdd(2, 3) 時,緩存中已經有相應的結果,所以它會直接返回緩存中的值,而不需要再次調用 add 函式。這樣就可以節省計算時間,提高效率

...args 是什麼?

...args 是 JavaScript 中的擴展運算符,它可以將一個可迭代物件(例如陣列、字串、Map 或 Set 等)展開成多個獨立的值。在這段程式碼中,...args 操作符被用來將傳入 cached 函式的引數列表轉換為一個包含這些引數的陣列。

例如,在下面的程式碼中:

function sum(a, b, c) {
  return a + b + c;
}

const args = [1, 2, 3];
console.log(sum(...args)); // 6

使用擴展運算符 ...argsargs 陣列展開,傳入 sum 函式,相當於呼叫 sum(1, 2, 3),得到結果 6

cached 函式中,...args 用來將傳入 cached 函式的引數列表展開成一個陣列,這樣就可以使用 JSON.stringify 方法將其轉換為字串,作為 cache 物件的鍵。

回到 cache 程式碼中,使用 JSON.stringify 方法將 args 陣列轉換為字串,JSON.stringify 方法的作用是將一個 JavaScript 值轉換為一個 JSON 字串,而在 JSON 字串中,陣列的表示方式是使用方括號 [] 包圍其內部的元素,元素之間使用逗號 , 分隔。

因此,當 args 陣列的值為 [2, 3] 時,JSON.stringify(args) 的結果會是 "[2,3]",即一個字串,裡面包含了 args 陣列中的兩個元素 23,並使用方括號包圍,元素之間使用逗號分隔。這樣做的好處是能夠保證 key 的唯一性,因為不同的引數會導致不同的 key 值,從而保證每個 key 只對應一個結果。

this 的不同應用場景,如何取值?

  • 當作普通函數被調用時,是由調用的對象來決定,不是聲明的對象。所以影響 this 值不是宣告的時機,要看在什麼時候被調用。

    1. 如果 this 不被當作函式的方法調用時,默認指向全局物件,也就是 Window。特別注意的是,在嚴格模式下,this 會為 undefined

    2. 下列程式碼要注意的是,使用 var 宣告才會將 player 變數綁定在全域物件 Window 上。使用 let 宣告的話並不會綁定在 Window 物件。

       var player = 'Faker'
      
       function selectPlayerPool(){
           console.log(this) // window
           console.log(this.player) // Faker
       }
      
       selectPlayerPool()
      
  • 作為物件方法調用時,this 會指向物件本身

      const faker = {
          name:'Lee Sang-Hyeok',
          callFaker() {
              console.log(`3 champions record ${Lee Sang-Hyeok}`)
          }
      }
    
      faker.callFaker() // 3 champions record Lee Sang-Hyeok
    
  • 構造函式調用,也就是使用 new 字符創建出新的物件,this 會指向這個物件本身

  • bind, call, apply 方法調用

  • 箭頭函式:箭頭函式沒有自己的 this 值,箭頭函式的 this 會繼承外在函式的 this,假如外在函式也是箭頭函式,就繼續用上級尋找,直到最後找到全域環境的 this

手寫 bind

bind也可以改變 this 指定的對象,這裡展示手寫 bind 幫助我們更加理解這個語法的作用

function fn1(a, b, c) {
  console.log(this, "this");
  console.log(a, b, c);
  return "this is fn1";
}

Function.prototype.bind1 = function () {
  // 將參數拆為數組
  const args = Array.prototype.slice.call(arguments);
  console.log(args, "args");

  // 獲取 this (數組第一項)
  const t = args.shift();

  // this 指的就是 fn1
  // this 在 class 之中,原型的函數,代表的就是對象的本身
  // fn1.bind(...) 中的 fin1
  const self = this;

  // 返回一個函數
  return function () {
    return self.apply(t, args);
  };
};

const fn2 = fn1.bind1({ x: 100 }, 10, 20, 30); // bind 返回函數
const res = fn2();
console.log("res", res); // this is fn1