问题
今天某群里有人提了这样一个问题 
 
  
就是在chrome调试器明明打印出来了xhr,能看到response的值,而在代码里面直接打印response却看不见这个值
结论
直接上结论,console.log打印的是xhr的引用,也就是说,open在打印的时候,xhr其实是对的,xhr.response也确实是空字符串的。但是后来请求完毕过后,xhr.response这个值被修改了,接着才是点开chrome dev-tool里面的XMLHttpRequest,所以这时候才能看到response,产生了一种错觉。
ajax-hook.js
    hookAjax({
        onreadystatechange:function(xhr){
            console.log("onreadystatechange called: %O",xhr)
            //return true
        },
        onload:function(xhr){
            console.log("onload called: %O",xhr)
            xhr.responseText="hook"+xhr.responseText;
            //return true;
        },
        open:function(arg,xhr){
          console.log("open called: method:%s,url:%s,async:%s",arg[0],arg[1],arg[2],xhr)
          arg[1]+="?hook_tag=1";
          //统一添加请求头
        },
        send:function(arg,xhr){
          console.log("send called: %O",arg[0])
          xhr.setRequestHeader("_custom_header_","ajaxhook")
        },
        setRequestHeader:function(arg,xhr){
            console.log("setRequestHeader called!",arg)
        }
    })
对于原生的XMLHttpRequest对象的一些方法,比如open,send都是支持“重载”的(暂时用这个词) 
也就是说它能够控制ajax的报文  
原理其实重新了XMLHttpRequest对象
// 用._ahrealxhr保存原始的XMLHttpRequest对象
window._ahrealxhr = window._ahrealxhr || XMLHttpRequest
// 重写XMLHttpRequest对象
XMLHttpRequest = function () {
    // 每次new XMLHttpRequest(假设创建对象为A)的时候
    // 都偷偷地new一个原始的XMLHttpRequest,挂在A.xhr这个变量里面
    this.xhr = new window._ahrealxhr;
    // 遍历XMLHttpRequest的属性,都复制一份到对象A的属性上
    for (var attr in this.xhr) {
        var type = "";
        try {
            type = typeof this.xhr[attr]
        } catch (e) {}
        if (type === "function") {
            // "成员方法"(暂时这么叫)
            this[attr] = hookfun(attr);
        } else {
           // "成员变量"(也是暂时这么叫)
            Object.defineProperty(this, attr, {
                get: getFactory(attr),
                set: setFactory(attr)
            })
        }
    }
}
一、 接下来,看看 "成员方法" 是怎么实现"重载"
function hookfun(fun) {  
    return function () {
        // [].slice.all(arguments)就是把arguments对象转化成array
        var args = [].slice.call(arguments)
        // 比方说,open函数,假如原本open的参数是arguments
        // 那么重载后open的参数是[arguments, this.xhr]
        if (funs[fun] && funs[fun].call(this, args, this.xhr)) {
            return;
        }
        this.xhr[fun].apply(this.xhr, args);
    }
}
如果不理解[].slice.call(arguments),那就试试下面的代码
function A() {  
    console.log("%O", arguments)
    for(let i in arguments) {
        console.log(i)
    }
    let args = [].slice.call(arguments);
    console.log("%O", args)
    for(let i in args) {
        console.log(i)
    }
}
从对比输出结果(我这里没有列出来),可以看到数组对象比arguments多出很多方法
二、 接下来,看看 "成员变量" 是怎么实现"重载"
Object.defineProperty(this, attr, {  
    get: getFactory(attr),
    set: setFactory(attr)
})
function getFactory(attr) {  
    return function () {
        // 这个this.xhr[attr]的其中一个原因是为了防止死循环暴栈而亡
        // 这个this[attr + "_"] 要配合下面set方法才能明白
        return this[attr + "_"] || this.xhr[attr]
    }
}
function setFactory(attr) {  
    return function (f) {
        var xhr = this.xhr;
        var that = this;
        if (attr.indexOf("on") != 0) {
            this[attr + "_"] = f;
            return;
        }
        if (funs[attr]) {
            xhr[attr] = function () {
                funs[attr](that) || f.apply(xhr, arguments);
            }
        } else {
            xhr[attr] = f;
        }
    }
}
1.上面说暴栈,如果不理解,可以试试下面代码
var obj2 = {}  
Object.defineProperty(obj2, "val", {  
    get : function(){ return this["val"] },
    set : function(newValue){ this["val"] = newValue}
})
var b = obj2.val  
//上面执行 = 的时候,会调用get,但是 get 函数里面,会去索引"val",如此循环下去
2.创建XMLHttpRequest的时候,它的大量回调函数,比方说onload、onprogress、onreadystatechange、ontimeout还是null值,在上层被赋值成函数,就会走到xhr[attr] = function那个地方的代码,首先执行我们的重载代码funsattr,再执行上层f.apply(xhr, arguments)
3.下滑线的主要目的是为了解决类似于responseText不可写,所以当修改responseText值的时候,实际修改的是responseText_
未经授权,禁止转载 
by chainhelen