你有用過 JavaScript 中的函數(shù)劫持么?
什么是函數(shù)劫持
最近業(yè)務(wù)上看到一段邏輯,找了好久,沒發(fā)現(xiàn)它是怎么被觸發(fā)的,后來發(fā)現(xiàn)其實使用了函數(shù)劫持,大致如下:
// 原始函數(shù)
var saveLog = function (log) {
console.log(`我保存了日志:${log}`);
}
// 1-保存原有函數(shù)
var originSaveLog = saveLog;
// 2-改寫原有函數(shù)
saveLog = function () {
const args = Array.prototype.slice.call(arguments);
// 3-在改寫后的函數(shù)中執(zhí)行原有函數(shù)的邏輯
originSaveLog.apply(null, args);
console.log('我要劫持你這個函數(shù),用來做自己的事情');
}
saveLog('test Save Log');
大致實現(xiàn)的邏輯就是在每次調(diào)用保存日志的同時執(zhí)行自己的邏輯,比如格式化、通知等。
函數(shù)劫持,在一個函數(shù)運行之前就把它劫持下來,添加我們想要的功能。當(dāng)這個函數(shù)實際運行的時候,它已經(jīng)不是原本的函數(shù)了,而是被我們添加上去的功能。這也是我們常見的鉤子函數(shù)的原理之一。
如上面的示例,一般函數(shù)劫持會分成三步 :
- 使用新的變量保存被劫持函數(shù)
- 新函數(shù)中改寫被劫持函數(shù)
- 新函數(shù)中調(diào)用原有的函數(shù)(保存在變量中的函數(shù))
為什么可以這么做?
一開始,我看上面這段代碼還有疑惑,當(dāng)重新給 saveLog 賦值的時候,不會改變 originSaveLog 的引用指向么?事實上是不會的,只會將 saveLog 指向另外一個引用地址。
可以看下面的例子就很容易理解了:
let a = {};
let b = a;
a.name = 'Gopal';
// ture {name: 'Gopal'} {name: 'Gopal'}
console.log(b === a, a, b);
基礎(chǔ):兩個對象指向同一個地址的時候,修改某個對象的屬性,另外一個對象也會隨之變化
let a = {};
let b = a;
a = {name: 'Gopal'};
// false {name: 'Gopal'} {}
console.log(b === a, a, b);
基礎(chǔ):將新的對象賦值給對象變量的時候,該對象變量就指向了新對象的引用地址,跟舊引用切斷關(guān)聯(lián)
應(yīng)用場景
增強你的函數(shù)功能
如上面的第一個例子,在原有的函數(shù)之上,實現(xiàn)特定的邏輯。
追蹤 XSS 攻擊
一般 XSS 攻擊會先利用 alert() 等方法輸出信息進行測試,這個時候,我們可以對原先的 alert() 進行劫持,向其輸入追蹤信息的代碼,最后才把原函數(shù)執(zhí)行。
如下所示:
function report(caller) {
var img=new Image();
img.src=`http://www.site.com/getReport.php?caller=${encodeURIComponent(caller)}`;
}
var _alert = window.alert;
window.alert = function (s) {
// 拿到攻擊者相關(guān)信息,并上報
report(alert.caller)
_alert(s)
}
alert('test');
劫持 ajax 請求,實現(xiàn) mock 功能
mock.js 中,就是通過對原生的 XMLHttpRequest(或 ActiveXObject)進行劫持,判斷有沒有找到匹配的數(shù)據(jù)模板,如果找到,則攔截 XHR 請求邏輯,執(zhí)行自身規(guī)則對應(yīng)的邏輯。如果未找到匹配的數(shù)據(jù)模板,則采用原生 XHR 發(fā)送請求。詳細代碼看這里[1]。
總結(jié)
JavaScript 中的函數(shù)劫持是一個增強原有函數(shù)的技巧,一般我們用來對原有的 JavaScript 全局方法做一些能力的增強。
參考資料
[1]這里: https://github.com/nuysoft/Mock/blob/refactoring/src/mock/xhr/xhr.js