Skip to main content

Command Palette

Search for a command to run...

JavaScript 作用域和閉包

Updated
•3 min read•View as Markdown
JavaScript 作用域和閉包
E

ESTJ 前端工程師|曾經任職乘車服務、電商和區塊鏈公司,走過新加坡,現座標台北|有一隻很二哈的賓士貓|樂於探索知識擁抱新工具|同時也是 Hip Hop 人偶爾跳跳 jazz

關注問題

  • 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 的高階函式,它的參數是一個函式 fn。cached 的作用是建立一個緩存(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 陣列裡面只有兩個元素,分別是 2 和 3,並使用 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

使用擴展運算符 ...args 將 args 陣列展開,傳入 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 陣列中的兩個元素 2 和 3,並使用方括號包圍,元素之間使用逗號分隔。這樣做的好處是能夠保證 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

More from this blog

TypeScript 學習系列 | 什麼是 TypeScript? 類型有哪些?

為什麼要使用 TypeScript 呢?好處在於掌握輸入和輸出的數值以及類型,編寫的時候即時顯示提供錯誤,輸入的參數也可以先定義好,避免踩到不清楚資料型別時的坑,比如資料等於 0 是數字還是布林值這種。 程式語言類型有以下兩種: 動態類型語言:執行期間才會檢查數據,初始化時不用給定型態或變量,ex: JavaScript, Ruby, Python 靜態類型語言:編譯階段即執行形態檢查,須在執行前聲明變量或是數據類型, TypeScript 即屬於這類型。 TypeScript 類型 基礎類...

May 17, 20231 min read

JavaScript 原型和原型鏈

💡 為什麼原型和 原型鏈很重要?因為JS 本身是基於原型繼承的語言, ES6 之後可以使用 Class 實作繼承 Overview 範圍 如何準確判斷一個變量是不是陣列 class 的原型本質 知識點 涵蓋以下三大點 class 實現繼承 instanceOf 使用 原型和原型鏈 class class 本身像是模板的作用,可以在 constructor 內對屬性定義賦值,透過 class 可以聲明 new 實例 constructor 屬性 方法 // 類 // ...

Apr 18, 20232 min read
JavaScript 原型和原型鏈
E

Emma's 613 小行星

8 posts

ESTJ 前端工程師 | 紀錄學習的輸出、閱讀分享或是其他所見所聞。看過一句話說「寫作就是和內在潛意識進行對話」, 想試著做這樣的練習而產生的 613 小行星