JavaScript 之「偷天換日術」
在開始填 YouDanMu 這個坑之前我就已經知道了遲早要碰上這種問題,趁著這個機會我來詳細地講一講如何偷到閉包裏面的私有變量。即 JavaScript 之「偷天換日術」。
爲了簡化問題,請考慮下面這段代碼:
(function(global){
function GUID(secret) {
this.secret = secret;
}
GUID.prototype.generate = function() {
return Math.floor((1 + this.secret) * 0x10000000000000).toString(16).toUpperCase();
};
GUID.history = [];
GUID.guid = function() {
var id;
do {
id = (new GUID(Math.random())).generate();
} while (GUID.history.indexOf(id) >= 0);
GUID.history.push(id);
return id;
};
global.guid = GUID.guid;
})(window);
這是一個簡單的 GUID 生成器(爲了簡單并沒有按照 RFC 4122 規範來寫,請不要使用)。他會向外部暴露一個 window.guid()
函數,每次調用時它會生成一個隨機數作爲種子, new
一個 GUID
對象,產生一段 GUID 碼,如果這段 GUID 碼存在于 GUID.history
中則重新生成,以保證生成 GUID 碼的唯一性(爲了簡單這個唯一性衹在這段程序的生命周期中有效)。
我們現在要寫一個瀏覽器插件,在不改動上面這段代碼的情況下,我們可以在這段代碼執行前後的全局作用域中添加我們的代碼,我們最終的目的是拿到閉包中 GUID
私有對象(Function)的讀寫權限。即我們能操縱 GUID.history
,比如能夠刪掉之前生成過的 GUID 碼記錄讓唯一性失效等等。
這個簡化版的問題正是我在對 YouTube 内部 API 進行劫持時遇到的等同問題,如果我們能解決這個簡化版的問題,那麽也就能成功劫持到 YouTube 的内部 API 對象了。
「偷天換日術」
我們先來看看這個「偷天換日術」的實現代碼:
/* 「偷天換日術」的實現部分 */
Object.defineProperty(Function.prototype, 'guid', {
__proto__: null,
enumerable: true,
configurable: false,
get: function _getter() {
return this._guid;
},
set: function _setter(value) {
value._this = this;
this._guid = value;
}
});
/* 這是原來的代碼 */
(function(global){
function GUID(secret) {
this.secret = secret;
}
GUID.prototype.generate = function() {
return Math.floor((1 + this.secret) * 0x10000000000000).toString(16).toUpperCase();
};
GUID.history = [];
GUID.guid = function() {
var id;
do {
id = (new GUID(Math.random())).generate();
} while (GUID.history.indexOf(id) >= 0);
GUID.history.push(id);
return id;
};
global.guid = GUID.guid;
})(window);
/* 結果演示 */
window.guid();
console.log(window.guid._this.history);
可以先在瀏覽器裏面跑一跑體驗一下,然後可以跟蹤調試一下并嘗試理解。在 _getter 和 _setter 的函數裏面設置斷點,在中斷的時候追溯一下 Call Stack 嘗試理解一下工作原理。
通過跟蹤我們能發現,這個「偷天換日術」最關鍵的一步發生在 GUID.guid = function() ...
這裏。我們知道,GUID
是一個 Function,不,準確來説是 Function 類的一個實例,相當於 new Function()
的結果,所以 GUID
應該繼承了 Function 這個類的 prototype
,也就是說 GUID.__proto__ === Function.prototype
。如果這裏不理解,請溫習一下 JavaScript 的面嚮對象編程以及封裝方法(by 阮一峰)。
其次我們知道,在 JavaScript 中對一個對象 x
的屬性 y
進行賦值的流程是這樣的:
- 如果在當前對象的直屬屬性中存在名爲
y
的屬性,則調用y
相應的 setter 將新的值傳進去,同時設置這個 setter 的 this 爲x
;如果該屬性存在但是沒有定義 setter 則直接賦值在它的 Property Descriptor 的value
屬性上。 - 如果在當前對象的直屬屬性中找不到名爲
y
的屬性,則訪問當前對象的__proto__
屬性,如果__proto__ != undefined
則把當前對象替換爲該__proto__
屬性,然後重複步驟 1。 - 如果
__proto__
訪問發生循環,或最後訪問到的__proto__ == undefined
,則終止__proto__
鏈訪問。這種情況下,JavaScript 才會重新回到最開始的對象x
上面,在它的直屬屬性中定義一個新的名爲y
的屬性,并賦值在它的 Property Descriptor 的value
屬性上面。
我們把這種遞歸式的屬性查詢訪問稱爲 Prototype Chain。我們可以把整個流程用 JavaScript 重現一遍:
function assignProperty(object, property, value) {
var current = object;
var protoChain = [current];
while (true) {
if (current.hasOwnProperty(property)) {
var descriptor = Object.getOwnPropertyDescriptor(current, property);
if (typeof descriptor.set === 'function') {
descriptor.set.call(object, value);
} else {
descriptor.value = value;
Object.defineProperty(current, property, descriptor);
}
break;
} else {
if (current.__proto__ != undefined
&& protoChain.indexOf(current.__proto__) < 0) {
current = current.__proto__;
protoChain.push(current);
} else {
Object.defineProperty(object, property, {
configurable: true,
enumerable: true,
value: value,
writable: true
});
break;
}
}
}
}
這段代碼將讓 x.y = 'value'
等價于 assignProperty(x, 'y', 'value')
,可以試試。
看到這裏,我想很多人都已經明白了這個「偷天換日術」的秘密了。最關鍵的一點,在於 Prototype Chain 賦值操作中,一個屬性擁有 setter 的時候,調用 setter 的同時會將 this
設置爲最上級的對象。也就是説,如果我們能在一個對象 x
的某一級 __proto__
中定義一個帶有我們自己設置的 setter 的屬性 y
,所有對 x.y
進行的賦值操作,除了會把新的值傳給我們的 setter 之外,還會把 setter 調用的 this
設成 x
,所以我們在 setter 中訪問 this
就是在訪問 x
。明白了這點,這個「偷天換日術」就不是什麽秘密了。
最後的問題就是如何在 GUID
的某層 __proto__
中插入一個我們提前制定好了 setter 的 guid
屬性。之前我們講到了,GUID
繼承了 Function.prototype
,用 JavaScript 重現一遍 function GUID() {...}
的内部實現就是:
var GUID = Function('/* function body ... */');
GUID.__proto__ = Function.prototype;
// 真正的内部實現還有很多別的内容,此處簡化省略
所以你看,我們衹要修改一下 Function.prototype
這個全局變量,就能在 GUID
的 Prototype Chain 上面隨意地添加「陷阱」了。好了我們再回頭看看我們的「偷天換日術」的實現:
/* 「偷天換日術」的實現部分 */
Object.defineProperty(Function.prototype, 'guid', {
__proto__: null,
enumerable: true,
configurable: false,
get: function _getter() {
return this._guid;
},
set: function _setter(value) {
value._this = this;
this._guid = value;
}
});
可以看到,我在 Function.prototype
上面定義了一個同時帶有 setter 和 getter 的屬性 'guid'
。於是在原來代碼執行到 GUID.guid = function() ...
這裏的時候,就會執行我們定義的 setter,把新的值(一個 Function)作爲參數傳進來,同時把 setter 的 this
設置成 GUID
。於是在這個 setter 中我們就能進行「偷天」:把 this
,也就是 GUID
放在 value
的 _this
屬性上,注意這個 value
最後會被暴露到全局作用域上,我們就能通過它把 GUID
一起帶出去。其實我這麽寫衹是爲了好看,我們完全可以在這個 setter 裏面直接把 this
賦值給一個全局變量,比如 window.GUID = this
,就直接偷到 GUID
了。爲了優雅,我還加上了「換日」:setter 會把傳進來的值存在一個別的屬性 _guid
上,然後我寫了一個 getter,會把 _guid
返回回去。所以在原來的代碼 global.guid = GUID.guid
這裏我們得到的 guid
實際上來自於 GUID._guid
,而且上面還帶著我們「偷天」回來的 _this
屬性,一起被傳出了全局作用域。
那麽一式 JavaScript 之「偷天換日術」就是這樣了。