阿川私房教材:學程式,拿 offer!

63 個專案實戰,直接上手!
無需補習,按步驟打造你的面試作品。

立即解鎖你的轉職秘笈

在本文中,我們透過從頭開始建立幾個關鍵元件來探索 JavaScript 的基本建構塊。當我們深入研究這些概念時,我們將應用一系列從基礎到複雜的技術,使這種探索對於 JavaScript 世界的新手和專業人士都很有價值。

大綱

<a name="memoize"></a>

memoize()

任務說明

重新建立memoize函數(來自“lodash”),該函數透過快取函數呼叫的結果來優化效能。透過傳回快取的結果而不是重新計算,可以確保使用相同參數的重複函數呼叫更快。

執行

function customSerializer(entity, cache = new WeakSet()) {
  if (typeof entity !== 'object' || entity === null) {
    return `${typeof entity}:${entity}`;
  }
  if (cache.has(entity)) {
    return 'CircularReference';
  }
  cache.add(entity);

  let objKeys = Object.keys(entity).sort();
  let keyRepresentations = objKeys.map(key =>
    `${customSerializer(key, cache)}:${
      customSerializer(entity[key], cache)
    }`
  );

  if (Array.isArray(entity)) {
    return `Array:[${keyRepresentations.join(',')}]`;
  }

  return `Object:{${keyRepresentations.join(',')}}`;
}

function myMemoize(fn) {
  const cache = new Map();

  return function memoized(...args) {
    const keyRep = args.map(arg => 
      customSerializer(arg)
    ).join('-');
    const key = `${typeof this}:${this}-${keyRep}`;

    if (cache.has(key)) {
      return cache.get(key);
    } else {
      const result = fn.apply(this, args);
      cache.set(key, result);
      return result;
    }
  };
}

實施的關鍵面

  1. 快取機制:它使用Map物件cache來儲存函數呼叫的結果。選擇Map物件是因為其高效率的鍵值配對和檢索功能。

  2. Custom SerializercustomSerializer函數將函數參數轉換為用作快取鍵的字串表示形式。此序列化考慮了基本類型、物件(包括巢狀物件)、陣列和循環參考。對於物件和陣列,它們的鍵經過排序以確保一致的字串表示形式,無論屬性聲明順序如何。

  3. 序列化thisthis的值指的是函數所屬的物件。在 JavaScript 中,方法可以根據呼叫它們的物件(即呼叫它們的上下文)而有不同的行為。這是因為this提供了對上下文物件的屬性和方法的存取,並且其值可能會根據函數的呼叫方式而變化。

  4. 循環引用:當物件直接或透過其屬性間接引用自身時,就會發生循環引用。這可能發生在更複雜的資料結構中,例如,物件A包含對物件B的引用,而物件B則直接或間接引用物件A 。處理循環引用以避免無限循環至關重要。

  5. 使用WeakSet進行自動垃圾收集WeakSet保留對其物件的「弱」引用,這表示如果沒有其他引用, WeakSet中物件的存在不會阻止該物件被垃圾收集。此行為在需要臨時追蹤物件存在而又不會不必要地延長其生命週期的情況下特別有用。由於customSerializer函數可能只需要在序列化過程中標記物件的存取,而不儲存額外的資料,因此使用WeakSet可以確保物件不會僅僅因為它們在集合中的存在而保持活動狀態,從而防止潛在的內存洩漏。

<a name="arraymap"></a>

Array.map()

任務說明

重新建立Array.map() ,它將轉換函數作為參數。此轉換函數將在陣列的每個元素上執行,並採用三個參數:當前元素、目前元素的索引和陣列本身。

實施的關鍵面

  1. 記憶體預先分配new Array(this.length)用於建立預先確定大小的陣列,以優化記憶體分配並透過避免加入元素時動態調整大小來提高效能。

執行

Array.prototype.myMap = function(fn) {
  const result = new Array(this.length);
  for (let i = 0; i < this.length; i++) {
    result[i] = fn(this[i], i, this);
  }
  return result;
}

<a name="arrayfilter"></a>

Array.filter()

任務說明

重新建立Array.filter() ,它將謂詞函數作為輸入,迭代呼叫它的陣列的元素,將謂詞應用於每個元素。它傳回一個新陣列,僅包含謂詞函數傳回true元素。

實施的關鍵面

  1. 動態記憶體分配:它動態地將符合條件的元素加入到filteredArray中,從而在很少有元素通過謂詞函數的情況下使該方法更有效地使用記憶體。

執行

Array.prototype.myFilter = function(pred) {
  const filteredArray = [];
  for (let i = 0; i < this.length; i++) {
    if (pred(this[i], i, this)) {
      filteredArray.push(this[i]);
    }
  }
  return filteredArray;
}

<a name="arrayreduce"></a>

Array.reduce()

任務說明

重新建立Array.reduce() ,它對陣列的每個元素執行reducer函數,從而產生單一輸出值。 reducer函數有四個參數:累加器、currentValue、currentIndex 和整個陣列。

實施的關鍵面

  1. initialValue valueaccumulatorstartIndex會根據是否將initialValue作為參數傳遞來初始化。如果提供了initialValue (意味著arguments.length至少為2 ),則accumulator設定為此initialValue ,並且迭代從第0個元素開始。否則,如果未提供initialValue ,則將陣列本身的第 0 個元素用作initialValue

執行

Array.prototype.myReduce = function(callback, initialValue) {
  let accumulator = arguments.length >= 2 
    ? initialValue 
    : this[0];
  let startIndex = arguments.length >= 2 ? 0 : 1;

  for (let i = startIndex; i < this.length; i++) {
    accumulator = callback(accumulator, this[i], i, this);
  }

  return accumulator;
}

<a name="bind"></a>

bind()

任務說明

重新建立bind()函數,該函數允許將物件以及預先指定的初始參數(如果有)作為呼叫原始函數的上下文傳遞。它還應該支援new運算符的使用,從而能夠建立新實例,同時維護正確的原型鏈。

執行


Function.prototype.mybind = function(context, ...bindArgs) {
  const self = this;
  const boundFunction = function(...callArgs) {
    const isNewOperatorUsed = new.target !== undefined;
    const thisContext = isNewOperatorUsed ? this : context;
    return self.apply(thisContext, bindArgs.concat(callArgs));
  };

  if (self.prototype) {
    boundFunction.prototype = Object.create(self.prototype);
  }

  return boundFunction;
};

實施的關鍵面

  1. 處理new Operator :語句const isNewOperatorUsed = new.target !== undefined;檢查是否透過new運算子將boundFunction作為建構函數呼叫。如果使用new運算符,則thisContext將設定為新建立的物件 ( this ) 而不是提供的context ,確認實例化應使用新的上下文而不是綁定期間提供的上下文。

  2. 原型保留:為了維護原始函數的原型鏈, mybind有條件地將boundFunction的原型設定為繼承自self.prototype的新物件。此步驟確保從boundFunction (用作建構函數時)建立的實例正確地繼承原始函數原型的屬性。此機制保留了預期的繼承層次結構並維護instanceof 檢查。

bind()new一起使用的範例

讓我們考慮一個簡單的建構函數,它建立代表汽車的物件:

function Car(make, model, year) {
  this.make = make;
  this.model = model;
  this.year = year;
}

想像一下,我們常常創造「豐田」品牌的Car物件。為了讓這個過程更有效率,我們可以使用bind為Toyotas建立一個專門的建構函數,預先填入make參數:

// Creating a specialized Toyota constructor with 'Toyota'
// as the pre-set 'make'
const ToyotaConstructor = Car.bind(null, 'Toyota');

// Now, we can create Toyota car instances
// without specifying 'make'
const myCar = new ToyotaConstructor('Camry', 2020);

// Output: Car { make: 'Toyota', model: 'Camry', year: 2020 }
console.log(myCar);

<a name="callapply"></a>

call()apply()

任務說明

重新建立call()apply()函數,它們允許使用給定的 this 值和單獨提供的參數來呼叫函數。

執行

Function.prototype.myCall = function(context, ...args) {
  const fnSymbol = Symbol('fnSymbol');
  context[fnSymbol] = this;

  const result = context[fnSymbol](...args);

  delete context[fnSymbol];

  return result;
};

Function.prototype.myApply = function(context, args) {
  const fnSymbol = Symbol('fnSymbol');
  context[fnSymbol] = this;

  const result = context[fnSymbol](...args);

  delete context[fnSymbol];

  return result;
};

實施的關鍵面

  1. 屬性命名的符號用法:為了防止覆蓋上下文物件上潛在的現有屬性或由於名稱衝突而導致意外行為,使用唯一的Symbol作為屬性名稱。這確保了我們的臨時屬性不會幹擾上下文物件的原始屬性。

  2. 執行後清理:函數呼叫執行後,新增到上下文物件中的臨時屬性將被刪除。此清理步驟對於避免在上下文物件上留下修改後的狀態至關重要。

<a name="setinterval"></a>

setInterval()

任務說明

使用setTimeout重新建立setInterval 。此函數應以指定的時間間隔重複呼叫提供的回呼函數。它會傳回一個函數,當呼叫該函數時,該函數會停止間隔。

執行

function mySetInterval(callback, interval) {
  let timerId;

  const repeater = () => {
    callback();
    timerId = setTimeout(repeater, interval);
  };

  repeater();

  return () => {
    clearTimeout(timerId);
  };
}

實施的關鍵面

  1. 取消功能mySetInterval傳回的函數提供了一種簡單直接的方法來取消正在進行的間隔,而無需在函數範圍之外公開或管理計時器 ID。

<a name="clonedeep"></a>

cloneDeep()

任務說明

重新建立執行給定輸入的深度複製的cloneDeep函數(來自“lodash”)。該函數應該能夠複製複雜的資料結構,包括物件、陣列、映射、集合、日期和正規表示式,並保持每個元素的結構和類型完整性。

執行

function myCloneDeep(entity, map = new WeakMap()) {
  if (entity === null || typeof entity !== 'object') {
    return entity;
  }

  if (map.has(entity)) {
    return map.get(entity);
  }

  let cloned;
  switch (true) {
    case Array.isArray(entity):
      cloned = [];
      map.set(entity, cloned);
      cloned = entity.map(item => myCloneDeep(item, map));
      break;
    case entity instanceof Date:
      cloned = new Date(entity.getTime());
      break;
    case entity instanceof Map:
      cloned = new Map(Array.from(entity.entries(),
        ([key, val]) => 
        [myCloneDeep(key, map), myCloneDeep(val, map)]));
      break;
    case entity instanceof Set:
      cloned = new Set(Array.from(entity.values(),
        val => myCloneDeep(val, map)));
      break;
    case entity instanceof RegExp:
      cloned = new RegExp(entity.source,
                          entity.flags);
      break;
    default:
      cloned = Object.create(
        Object.getPrototypeOf(entity));
      map.set(entity, cloned);
      for (let key in entity) {
        if (entity.hasOwnProperty(key)) {
          cloned[key] = myCloneDeep(entity[key], map);
        }
      }
  }

  return cloned;
}

實施的關鍵面

  1. 循環引用處理:利用WeakMap來追蹤已存取的物件。如果遇到已經克隆的物件,則返回先前克隆的物件,有效處理循環參考並防止堆疊溢位錯誤。

  2. 特殊物件的處理:區分幾種物件類型( ArrayDateMapSetsRegExp ),以確保每種類型都被適當地克隆,並保留其特定特徵。

- **`Array`**: Recursively clones each element, ensuring deep cloning.
- **`Date`**: Copies the date using its numeric value (timestamp).
- **Maps and Sets**: Constructs a new instance, recursively cloning each entry (for `Map`) or value (for `Set`).
- **`RegExp`**: Clones by creating a new instance with the source and flags of the original.
  1. 物件屬性的複製:當輸入是普通物件時,它會建立一個與原始物件具有相同原型的物件,然後遞歸地複製每個自己的屬性,在保持原型鏈的同時確保深度克隆。

  2. 效率和性能:利用WeakMap進行記憶,有效處理具有重複引用和循環的複雜大型結構,透過避免冗餘克隆來確保最佳性能。

<a name="debounce"></a>

debounce()

任務說明

重新建立debounce函數(來自“lodash”),它允許限制給定回調函數觸發的頻率。當在短時間內重複呼叫時,在指定的延遲後僅執行最後一次呼叫。

function myDebounce(func, delay) {
  let timerId;
  const debounced = function(...args) {
    clearTimeout(timerId);
    timerId = setTimeout(() => {
      func.apply(this, args);
    }, delay);
  };

  debounced.cancel = function() {
    clearTimeout(timerId);
    timerId = null;
  };

  debounced.flush = function() {
    clearTimeout(timerId);
    func.apply(this, arguments);
    timerId = null;
  };

  return debounced;
}

實施的關鍵面

  1. 取消功能:引入.cancel方法使外部控制能夠取消去抖函數的任何暫停執行。這增加了靈活性,允許響應特定事件或條件而取消去抖功能。

  2. 透過 Flush 立即執行.flush方法允許立即執行去抖函數,而不考慮延遲。這在需要確保立即應用去抖函數的效果的情況下非常有用,例如,在卸載元件或完成互動之前。

<a name="throttle"></a>

throttle()

任務說明

重新建立throttle函數(來自“lodash”),它確保給定的回調函數在每個指定的時間間隔內最多只呼叫一次(在我們的例子中是在開始時)。與去抖動不同,限制保證函數會定期執行,確保進行更新,儘管更新速度是受控的。

執行

function myThrottle(func, timeout) {
  let timerId = null;

  const throttled = function(...args) {
    if (timerId === null) {
      func.apply(this, args)
      timerId = setTimeout(() => {
        timerId = null;
      }, timeout)
    }
  }

  throttled.cancel = function() {
    clearTimeout(timerId);
    timerId = null;
  };

  return throttled;
}

實施的關鍵面

  1. 取消功能:引入.cancel方法可以清除節流計時器的任何計劃重置。這在清理階段非常有用,例如 UI 庫/框架中的元件卸載,以防止過時的執行並有效管理資源。

<a name="promise"></a>

Promise

任務說明

重新建立Promise類別。它是為非同步程式設計的構造,允許暫停程式碼的執行,直到非同步進程完成。從本質上講,承諾代表了在其建立時不一定已知的值的代理。它允許您將處理程序與非同步操作的最終成功值或失敗原因相關聯。這使得非同步方法可以像同步方法一樣傳回值:非同步方法不是立即傳回最終值,而是傳回一個在未來某個時刻提供該值的承諾。 Promise包含處理已完成和拒絕狀態的方法( thencatch ),以及無論結果如何都執行程式碼的方法( finally )。

class MyPromise {
  constructor(executor) {
    ...
  }

  then(onFulfilled, onRejected) {
    ...
  }

  catch(onRejected) {
    ...
  }

  finally(callback) {
    ...
  }
}

constructor實現

constructor(executor) {
  this.state = 'pending';
  this.value = undefined;
  this.reason = undefined;
  this.onFulfilledCallbacks = [];
  this.onRejectedCallbacks = [];

  const resolve = (value) => {
    if (this.state === 'pending') {
      this.state = 'fulfilled';
      this.value = value;
      this.onFulfilledCallbacks.forEach(fn => fn());
    }
  };

  const reject = (reason) => {
    if (this.state === 'pending') {
      this.state = 'rejected';
      this.reason = reason;
      this.onRejectedCallbacks.forEach(fn => fn());
    }
  };

  try {
    executor(resolve, reject);
  } catch (error) {
    reject(error);
  }
}

constructor實現的關鍵方面

  1. 狀態管理:以「待處理」狀態初始化。解決時切換為“已完成”,被拒絕時切換為“拒絕”。

  2. 值和原因:保存承諾的最終結果( value )或拒絕的原因( reason )。

  • 處理非同步:接受包含非同步操作的executor函數。 executor採用兩個函數, resolvereject ,當呼叫它們時,將promise轉換到對應的狀態。
  1. 回呼陣列:維護回呼佇列( onFulfilledCallbacksonRejectedCallbacks ),以用於等待解決或拒絕承諾的延遲操作。

.then實施

resolvePromise(promise2, x, resolve, reject) {
  if (promise2 === x) {
    return reject(new TypeError(
      'Chaining cycle detected for promise'));
  }
  if (x instanceof MyPromise) {
    x.then(resolve, reject);
  } else {
    resolve(x);
  }
}

then(onFulfilled, onRejected) {
  onFulfilled = typeof onFulfilled === 'function' ? 
    onFulfilled : value => value;

  onRejected = typeof onRejected === 'function' ? 
    onRejected : reason => { throw reason; };

  let promise2 = new MyPromise((resolve, reject) => {
    if (this.state === 'fulfilled') {
      setTimeout(() => {
        try {
          let x = onFulfilled(this.value);
          this.resolvePromise(promise2, x, resolve, reject);
        } catch (error) {
          reject(error);
        }
      });
    } else if (this.state === 'rejected') {
      setTimeout(() => {
        try {
          let x = onRejected(this.reason);
          this.resolvePromise(promise2, x, resolve, reject);
        } catch (error) {
          reject(error);
        }
      });
    } else if (this.state === 'pending') {
      this.onFulfilledCallbacks.push(() => {
        setTimeout(() => {
          try {
            let x = onFulfilled(this.value);
            this.resolvePromise(promise2, x, resolve,
              reject);
          } catch (error) {
            reject(error);
          }
        });
      });
      this.onRejectedCallbacks.push(() => {
        setTimeout(() => {
          try {
            let x = onRejected(this.reason);
            this.resolvePromise(promise2, x, resolve,
              reject);
          } catch (error) {
            reject(error);
          }
        });
      });
    }
  });

  return promise2;
}

.then實施的關鍵方面

  1. 預設處理程序:將非函數處理程序轉換為標識函數(用於實現)或拋出程序(用於拒絕),以確保承諾鏈中的正確轉發和錯誤處理。

  2. Promise 連結then方法允許連結 Promise,從而實現順序非同步操作。它會建立一個新的 Promise ( promise2 ),該 Promise 取決於傳遞給它的回呼函數 ( onFulfilledonRejected ) 的結果。

  3. 處理解決方案和拒絕:僅在當前承諾解決(履行或拒絕)後才會呼叫所提供的回調。每個回呼的結果 ( x ) 可能是一個值或另一個 Promise,決定了promise2的解析。

  4. 防止連結循環resolvePromise函數檢查promise2是否與結果 ( x ) 相同,避免 Promise 等待自身的循環,從而導致TypeError

  5. 支援 MyPromise 和 Non-Promise 值:如果結果 ( x ) 是MyPromise的實例, then使用其解析或拒絕來解決promise2 。此功能支援基於 Promise 的操作的無縫集成,無論是來自MyPromise實例還是本機 JavaScript Promise,假設它們具有相似的行為。對於非 Promise 值,或當onFulfilledonRejected只是傳回一個值時, promise2將使用該值進行解析,從而在 Promise 鏈中實現簡單的轉換或分支邏輯。

  6. 非同步執行保證:透過使用setTimeout延遲onFulfilledonRejected的執行, then確保非同步為。此延遲保持一致的執行順序,確保onFulfilledonRejected在執行堆疊清除後呼叫。

  7. 錯誤處理:如果onFulfilledonRejected內發生異常, promise2會因錯誤而被拒絕,從而允許錯誤處理通過 Promise 鏈傳播。

catchfinally實現

static resolve(value) {
  if (value instanceof MyPromise) {
    return value;
  }
  return new MyPromise((resolve, reject) => resolve(value));
}

catch(onRejected) {
  return this.then(null, onRejected);
}

finally(callback) {
  return this.then(
    value => MyPromise.resolve(callback())
              .then(() => value),
    reason => MyPromise.resolve(callback())
              .then(() => { throw reason; })
  );
}

.catch實施的關鍵面向:

  1. 簡化的錯誤處理: .catch方法是.then(null, onRejected)的簡寫,專門專注於處理拒絕場景。當只需要拒絕處理程序時,它允許更清晰的語法,從而提高程式碼的可讀性和可維護性。

  2. Promise Chaining 支援:由於它在內部委託給.then ,所以.catch返回一個新的 Promise,從而保持 Promise 鏈功能。這允許在錯誤恢復或透過重新拋出或返回新的被拒絕的承諾傳播錯誤後繼續進行鏈操作。

  3. 錯誤傳播:如果提供了onRejected並且執行時沒有錯誤,則傳回的 Promise 將使用onRejected的傳回值進行解析,從而有效地允許 Promise 鏈中的錯誤復原。如果onRejected拋出錯誤或傳回被拒絕的 Promise,則錯誤會沿著鏈傳播。

.finally實現的關鍵面向:

  1. 始終執行: .finally方法確保執行提供的callback ,無論 Promise 是履行還是拒絕。這對於需要在非同步操作之後發生的清理操作特別有用,與其結果無關。

  2. 傳回值保留:雖然.finally中的callback不接收任何參數(與.then.catch不同),但 Promise 的原始履行值或拒絕原因將被保留並透過鏈傳遞。從.finally傳回的 Promise 會以相同的值或原因被解析或拒絕,除非callback本身導致被拒絕的 Promise。

  3. 錯誤處理與傳播:如果callback執行成功, .finally傳回的 Promise 將按照與原始 Promise 相同的方式進行結算。但是,如果callback拋出錯誤或返回被拒絕的 Promise,則從.finally返回的 Promise 會因這個新錯誤而被拒絕,從而允許錯誤攔截並更改 Promise 鏈中的拒絕原因。

<a name="eventemitter"></a>

EventEmitter

任務說明

重新建立EventEmitter類,該類別允許實現觀察者模式,使物件(稱為「發射器」)能夠發出命名事件,從而導致呼叫先前註冊的偵聽器(或「處理程序」)。這是 Node.js 中用於處理非同步事件的關鍵元件,廣泛用於發出訊號以及管理應用程式狀態和行為。實作自訂EventEmitter涉及建立用於註冊事件偵聽器、觸發事件和刪除偵聽器的方法。

class MyEventEmitter {
  constructor() {
    this.events = {};
  }

  on(eventName, listener) {
    if (!this.events[eventName]) {
      this.events[eventName] = [];
    }
    this.events[eventName].push(listener);
  }

  once(eventName, listener) {
    const onceWrapper = (...args) => {
      listener.apply(this, args);
      this.off(eventName, onceWrapper);
    };
    this.on(eventName, onceWrapper);
  }

  emit(eventName, ...args) {
    const listeners = this.events[eventName];
    if (listeners && listeners.length) {
      listeners.forEach((listener) => {
        listener.apply(this, args);
      });
    }
  }

  off(eventName, listenerToRemove) {
    if (!this.events[eventName]) {
      return;
    }
    const filterListeners = 
      (listener) => listener !== listenerToRemove;
    this.events[eventName] = 
      this.events[eventName].filter(filterListeners);
  }
}

EventEmitter實現的關鍵面

  1. EventListener Registration .on將偵聽器函數新增至指定事件的偵聽器陣列中,如果該事件名稱尚不存在則建立一個新陣列。

  2. 一次性事件偵聽器.once註冊一個偵聽器,該偵聽器在呼叫一次後會自行刪除。它將原始偵聽器包裝在一個函數 ( onceWrapper ) 中,該函數也會在執行後刪除包裝器,確保偵聽器僅觸發一次。

  3. 發出事件.emit觸發事件,使用提供的參數呼叫所有已註冊的偵聽器。它將參數應用於每個偵聽器函數,從而允許將資料傳遞給偵聽器。

  4. 刪除事件偵聽器.off從事件偵聽器陣列中刪除特定偵聽器。如果事件在刪除後沒有偵聽器,則可以將其保留為空陣列或可選地進一步清理(此實作中未顯示)。


原文出處:https://dev.to/antonzo/implementing-javascript-concepts-from-scratch-4623


共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。

阿川私房教材:學程式,拿 offer!

63 個專案實戰,直接上手!
無需補習,按步驟打造你的面試作品。

立即解鎖你的轉職秘笈