> ## Content Index
> Fetch the complete content index at: https://blog.hacking.pub/llms.txt
> Use this file to discover other available public pages before exploring further.

# ajax-hook.js 剖析
- URL: https://blog.hacking.pub/2017/02/09/ajax-hook-js-pou-xi/
- Published: 2017-02-09T09:45:46.000Z
- Updated: 2017-02-13T08:48:22.000Z
- Author: chainhelen
- Tags: js, javascript, 源码剖析

#### 问题

今天某群里有人提了这样一个问题  
![2.jpg](https://blog.hacking.pub/qiniu/ajax-hook.js%20%E5%89%96%E6%9E%90/2.jpg)  
![1.jpg](https://blog.hacking.pub/qiniu/ajax-hook.js%20%E5%89%96%E6%9E%90/1.jpg)

就是在chrome调试器明明打印出来了xhr，能看到response的值，而在代码里面直接打印response却看不见这个值

#### 结论

直接上结论，console.log打印的是xhr的引用，也就是说，open在打印的时候，xhr其实是对的，xhr.response也确实是空字符串的。但是后来请求完毕过后，xhr.response这个值被修改了，接着才是点开chrome dev-tool里面的XMLHttpRequest，所以这时候才能看到response，产生了一种错觉。

#### ajax-hook.js

发现这个库[代码](https://github.com/wendux/Ajax-hook/blob/master/src/ajaxhook.js?ref=blog.hacking.pub)写得十分简洁  
这里是[demo代码](https://github.com/wendux/Ajax-hook/blob/master/demon/demo.html?ref=blog.hacking.pub)

```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那个地方的代码，首先执行我们的重载代码funs[attr](that)，再执行上层f.apply(xhr, arguments)

3.下滑线的主要目的是为了解决类似于responseText不可写，所以当修改responseText值的时候，实际修改的是responseText\_

---

**未经授权，禁止转载**  
**by chainhelen**

#### 参考

1. [Ajax-hook 原理解析](http://www.jianshu.com/p/7337ac624b8e?ref=blog.hacking.pub)
2. [Js 拦截全局ajax请求](http://www.jianshu.com/p/9b634f1c9615?ref=blog.hacking.pub)
3. [JS中的双向数据绑定及Object.defineProperty方法](http://www.tuicool.com/articles/nA36neR?ref=blog.hacking.pub)
4. [Object.defineproperty语法](http://www.cnblogs.com/oceanxing/p/3938443.html?ref=blog.hacking.pub)
5. [探讨跨域请求资源的几种方式](http://www.cnblogs.com/dojo-lzz/p/4265637.html?ref=blog.hacking.pub)