JS中THIS的五種情況

一、事件綁定
THIS1: 給元素的某個事件行爲綁定方法,事件觸發,方法執行,此時方法中的this一般都是當前元素本身
在代碼中,有一個button

    <button id="btn">點我一下~</button>
<script>
        //DOM0事件綁定
        // btn.onclick = function anonymous() {
        //     console.log(this)
        // }

        // DOM2事件綁定
        // btn.addEventListener('click', function anonymous() {
        //     console.log(this); //元素
        // }, false) //在冒泡階段執行,不兼容IE678

        // btn.attachEvent('onclick',function anonymous(){
        //     // <= IE8瀏覽器中的DOM2事件綁定
        //     console.log(this); //window
        // })

        // function fn() {
        //     console.log(this);
        // }
        // btn.onclick = fn.bind(window); //=>fn.bind(window):首先會返回一個匿名函數(AM),把AM綁定給事件;點擊觸發執行AM,AM中的THIS是元素,但是會在AM中執行FN,FN中THIS是預先指定的WINDOW
        /*
        bind原理:
            (function(){
                return function(){ //執行的時候是執行這個函數
                    fn.call(window);
                }
            })()
        */
    </script>

二、普通函數執行
THIS2:普通函數執行,它裏面的THIS是誰,取決於方法執行前面是否有“.”點,有的話,“點”前面是誰THIS就是誰,沒有THIS指向WINDOW(嚴格模式下是UNDEFINED)

<script>        
            function fn() {
                console.log(this);
            }
            let obj = {
                name: 'OBJ',
                fn: fn
            };
        
        fn(); 
        obj.fn();
        console.log(obj.hasOwnProperty('name'));//=>hasOwnProperty方法中的this:obj 輸出爲TRUE
        console.log(obj.__proto__.hasOwnProperty('name')); //=>hasOwnProperty方法中this:obj.__proto__(即Object.prototype原型) 輸出爲FALSE
        console.log(Object.prototype.hasOwnProperty.call(obj, 'name')); //=>讓object原型的hasOwnproperty方法的this強制變成obj <==> 等價於obj.hasOwnProperty('name'),

        /*
        hasOwnProperty用來檢測某個屬性名是否屬於當前對象的私有屬性。
        in用來檢測是否爲其屬性(不論私有還是共有)
        */
        console.log(obj.hasOwnProperty('name')); //=>true
        console.log(obj.hasOwnProperty('toString'));//=>false
        console.log('toString' in obj); //=>true
    </script>

三、構造函數執行
THIS3:構造函數執行(new xxx),函數中的this是當前類的實例

<script>
        function Fn() {
            console.log(this); //Fn {}
            // => this.xxx = xxx 是給當前實例設置私有屬性 
        }
        let f = new Fn;
</script>

四、箭頭函數
THIS4:箭頭函數中的沒有自身的THIS,所用到的THIS就是其上下文中的THIS

<script>
        /*
          箭頭函數沒有的東西很多:
                1.它沒有prototype(也就是沒有構造器),所以不能被new執行
                2.它沒有arguments實參集合(可以基於...args剩餘運算符獲取)
                箭頭函數如下所示:
                let obj = {
                    fn: () => {
                    // this:Window
                        console.log(this);
                    }
                };
                obj.fn();
        */
        //    舉個小栗子
        // let obj = {
        //     name: 'OBJ',
        //     fn: function () {
        //         console.log(this); //this:obj,輸出爲{name: "OBJ", fn: ƒ}
        //         return function () {
        //             console.log(this);
        //         }
        //     }
        // };
        // let ff = obj.fn(); //將obj.fn()執行,後將值賦值給ff
        // ff(); //就是上述的ff函數,方法執行,沒有".",所以this是WINDOW 輸出window

        /*新的小需求:
            若想在以上小例子中,obj.fn()中返回的函數,也就是ff()方法中,修改obj.name的值,就要指定this是obj,現在小函數的this指向window,若直接這麼寫:
            let obj = {
            name: 'OBJ',
            fn: function () {
                console.log(this); //this:obj,輸出爲{name: "OBJ", fn: ƒ}
                return function () {
                    console.log(this);
                    this.name = '你好世界'; //修改的是window.name,不符合需求
                }
            }
        };//修改的是window.name,不符合需求
        */
        // 故有下面兩種方法修改:
        // // 寫法一:
        // let obj = {
        //     name: 'OBJ',
        //     fn: function () {
        //         console.log(this); //this:obj,輸出爲{name: "OBJ", fn: ƒ}
        //         let _this = this; //將外面和返回函數中的this都指向obj
        //         return function () {
        //             _this.name = '你好世界'//這裏就修改成功了,返回的這個函數在此刻已經將obj.name='OBJ',改爲你好世界,可以直接在控制檯輸入obj,就可以看到結果
        //             //console.log(this);
        //         }
        //     }
        // };
        // let ff = obj.fn(); 
        // ff();

        // // 寫法二:
        // let obj = {
        //     name: 'OBJ',
        //     fn: function () {
        //         // console.log(this); //this:obj,輸出爲{name: "OBJ", fn: ƒ}
        //         return () => { //箭頭函數,使用的是上下文中的this,因爲上面的this是obj,所以直接這麼寫就修改成功了
        //             console.log(this); //=>{name: "OBJ", fn: ƒ} this:obj
        //             this.name = '你好世界';
        //             console.log(this); //this:obj => 控制檯輸出{name: "你好世界", fn: ƒ},已經修改成功
        //         }
        //     }
        // };
        // let ff = obj.fn();
        // ff();

        // 若想要使用setTimeout,1秒後修改obj.name,可以直接使用箭頭函數
        let obj = {
            name: 'OBJ',
            fn: function () {
                /*
                若直接這麼寫還是會修改window.name
                setTimeout(function(){
                    this.name = '你好世界';
                    console.log(this);
                },1000);
                */
                setTimeout(_ => {
                    this.name = '你好世界';
                    console.log(this); //{name: "你好世界", fn: ƒ}
                }, 1000);
            }
        };
        let ff = obj.fn();
    </script>

五、call/apply/bind

<script>
        /*
            THIS5:基於call/apply/bind可以改變函數中this的指向(強行改變)
                CALL/APPLY:
                    第一個參數就是改變的this指向,寫誰就是誰(特殊:非嚴格模式下,傳遞null/undefined指向的也是window):func.call([context])與func.apply([context])
                    兩者唯一區別:執行函數,傳遞的參數方式有區別,call是一個個傳遞,apply是把需要傳遞的參數放到數組中整體傳遞                   
                    若想傳兩個值10和20:call傳遞方法:func.call([context],10,20);
                                      apply傳遞方法:func.apply([context],[10,20]);數組形式傳遞

                BIND:
                    call和apply都是改變this的同時直接把函數執行了,而bind不是立即執行函數,屬於預先改變this和傳遞一些內容 => "柯理化函數編程思想"                      
        */
        //Function.prototype = {call/apply/bind...}function的原型上的方法

        // 若自己寫一個bind/call/apply方法源碼
        ~ function anonymous(proto) {
            // 重置內置的bind--ES5的寫法,具體思路如下:
            /*
            function bind(context) {
                // context 可能是null || undefined
                if (context == undefined) {
                    context = window;
                }
                // 獲取傳遞的數組集合
                // =>傳遞的值可能是類數組,arguments:{0:context,1:10,2:20,length:3}
                // =>想要獲取除了第一個的值(因爲第一個參數是要綁定的對象context),可以使用slice方法,但是這個arguments不是數組,它的原型鏈指向Object.prototype,
                // => console.log(arguments); ...__proto__:... constructor: ƒ Object()指向Object
                // 想使用slice,若是數組的話,直接slice(1),從索引1開始,直接取到最後,是它的值。但此時的arguments不是數組,就不能直接調用這個方法,slice不是Object原型上的方法,是屬於數組的原型上的方法,可以借用數組的這個方法
                // =>像上文中提及的方法一樣,obj.hasOwnproperty('name')與直接調用Object的原型上的方法,將this變成obj:Object.prototype.hasOwnProperty.call(obj,'name'),是相同的
                // => 所以[].slice.call(arguments,1);就是將this變成arguments,然後去執行數組的slice方法,然後返回結果一定是一個數組。
                // args是執行bind時傳遞的參數
                var args = [].slice.call(arguments, 1);
                // 需要最終執行的函數
                var _this = this;

                //返回的函數根據具體情況,會有不同:
                // 1、 沒有事件行爲時會返回一個新的函數,這麼寫:
                // return function anonymous() {
                // 改變this爲傳遞的context,且傳遞的參數是數組args
                //     _this.apply(context, args);
                // }
                // 2、若綁定的事件方法,會有一個ev的傳參,返回的函數就是:
                //  return function anonymous(ev) {
                //      args.push(ev);
                //      _this.apply(context,args)}

                //3、(最終版)若不是事件點擊,可能參數不是ev(不僅是ev,可能還有其他),具體是什麼也不知道,所以就不能像上面註釋中的那麼寫,應該寫成如下形式: =>完整版
                return function anonymous() {
                    //amArgs是將bind給事件或者別的時,執行anonymous時的傳值,將傳進來的參數,借用數組的slice方法變成數組,從索引0開始到最後,變成一個數組。
                    var amArgs = [].slice.call(arguments, 0);
                    // 然後將args和傳的參數數組合並
                    // args = args.concat(amArgs);
                    // 最後將參數統一給要執行的obj.fn(在此文中的使用場景),在別處是別的要執行的函數
                    // _this.apply(context, args);
                    // 也可以將上兩步合起來,直接這麼寫
                    _this.apply(context, args.concat(amArgs));
                }
            }
            */

            // 重置內置的bind--ES6寫法 重置bind方法
            // 經過測試,apply的性能不如call
            function bind(context = window, ...args) {
                return (amArgs) => this.call(context, ...args.concat(amArgs));
            }

            /*
            // call 重寫改變this方法,在此保證context是引用類型值
            function call(context = window, ...args) {
                // => 必須保證context是引用類型
                // =>this:需要執行的函數
                // 因爲call是立即執行的方法,還要傳值,所以是直接執行this(...args);=>但要修改this,可以使用一個方法,有個方法是fn,有個this是obj,想要fn執行時,this是obj,可以直接這麼寫:obj.fn,在此也可以使用這個方法,context是想要改變this指向的對象(就相當於obj),$fn是想要加的一個屬性,等於要執行的那個函數this
                context.$fn = this;
                // 執行這個函數。並傳參數context.$fn(...args),接收返回結果
                let result = context.$fn(...args); //此時要執行的這個函數中,this是context
                // 加上這個參數,用過後要刪除
                delete context.$fn;
                // 最後將執行結果返回
                return result;
            }
            */
            // 重寫call方法,將context類型值全部考慮在內的全面寫法
            function call(context = window, ...args) { //這裏識別undefined和具體的context值,識別不了null
                // 如果context,要改變的this指向對象===null,讓它===window,否則什麼都不做
                context === null ? context = window : null;
                // 判斷context,三個條件都不成立,就不是引用類型的值,是基本數據類型
                let type = typeof context;
                if (type !== 'object' && type !== 'function' && type !== 'symbol') {
                    // console.log('基本類型');
                    // => 基本類型值
                    switch (type) {
                        case 'number':
                            context = new Number(context); //可以把context變成數字的引用類型值,構造函數創建的結果都是引用類型的
                            break;
                        case 'string':
                            context = new String(context); //構造函數創建的結果都是引用類型的
                            break;
                        case 'boolean':
                            context = new Boolean(context);//構造函數創建的結果都是引用類型的
                            break;
                    }
                }
                // 在此的context的類型就是引用類型值
                context.$fn = this;
                // 執行這個函數。並傳參數context.$fn(...args),接收返回結果
                let result = context.$fn(...args); //此時要執行的這個函數中,this是context
                // 加上這個參數,用過後要刪除
                delete context.$fn;
                // 最後將執行結果返回
                return result;
            }

            // 重寫apply方法(簡單):context是引用類型值,若想判斷其他類型,同上call中的類型判斷
            //與call的區別在於參數,只有兩個,第二個參數是數組
            function apply(context = window, args) {
                context.$fn = this;
                // 執行這個函數。並傳參數context.$fn(...args),接收返回結果,把參數展開
                let result = context.$fn(...args); //此時要執行的這個函數中,this是context
                // 加上這個參數,用過後要刪除,但是在此,不生效,刪除不了,可不寫
                delete context.$fn;
                // 最後將執行結果返回
                return result;
            }
            //原型上掛載
            proto.bind = bind;
            proto.call = call;
            proto.apply = apply;
        }(Function.prototype)

        let obj = {
            fn(x, y) {
                console.log(this, x, y);
            }
            // 在anonymous中有ev,在此用的時候也會有ev,若有其他操作,也會顯示
            // fn(x, y, ev) {
            //     console.log(this, x, y, ev);
            // }
        }
        obj.fn.call('1', 10, 20);//這個obj.fn函數執行,要把返回結果接收
        obj.fn.apply(window, [10, 20]); //window 10 20
        // 若想1秒鐘後執行obj.fn,傳值,寫法爲:
        //直接這麼寫setTimeout(obj.fn.call(window,10, 20), 1000);會立即執行,換成apply也是一樣,所以可以換成bind. 

        // 將bind在定時器中使用
        // setTimeout(obj.fn.bind(window, 10, 20), 1000);
        //=>setTimeout(anonymous, 1000); 1秒後先執行bind的返回結果anonymous函數,在anonymous中再把需要執行的obj.fn執行,把之前存儲的context/args傳遞給函數

        // 若把這個bind給頁面的點擊事件:
        // document.body.onclick = obj.fn.bind(window, 10, 20);
        //=> document.body.onclick = anonymous;不僅會執行anonymous,還會默認給anonymous傳一個ev的參數
        // call的源碼:
        // function call(context = window, ...args) {
        //     context.$fn = this;
        //     let result = context.$fn(...args); //此時要執行的這個函數中,this是context
        //     return result;
        // } => AAAFFF000可視爲堆內存
        function fn1() { console.log(1) };
        function fn2() { console.log(2) };
        fn1.call(fn2);// => this是fn1, context:fn2,先執行call函數,在call中this是fn2,沒有用到,故不管  => 執行的是FN1 => 1
        fn1.call.call(fn2); //=>執行的是FN2 =>2 不管幾個call,都是原型上的call方法執行
        /*
            多個CALL執行,執行過程:
            先讓最後一個CALL執行
                this=> fn1.call => 去原型上找call這個方法,就是對AAAFFF000
                context=> fn2
                args=>[]
            fn2.$fn = AAAFFF000  fn2.$fn(...[]),沒有傳參,直接執行
            讓call方法再執行:
                this=> fn2
                context=> undefined
            undefined.$fn=fn2; undefined.$fn() 
            =>執行fn2 => 輸出2 
        */
        Function.prototype.call(fn1);
        /*
            先讓CALL執行
                this=>Function.prototype(FN的prototype是anonymous匿名函數)
                context=> fn1
                args=>[]
            fn1.$fn=Function.prototype fn1.$fn()
            => 讓fn1.$fn()執行,相當於Function.prototype執行,這是個匿名空函數,執行沒有返回值,所以沒有輸出
        */
        Function.prototype.call.call(fn1); //=>執行FN1 =>1
        /*
            執行最後一個CALL
                this=>Function.prototype.call(AAAFFF000)
                context=>fn1
                args=>[]
            fn1.$fn=Function.prototype.call =>AAAFFF000  fn1.$fn()執行
            再次執行CALL函數
                this=>fn1
                context=>undefined
                args=[]
            undefined.$fn=fn1    讓undefined.$fn()執行
            => 相當於執行fn1 => 1
        */
        /*
        在這總結下call的使用規律:
            1個call,call左邊是誰,就執行誰
            多個call,兩個及兩個以上call,第一個參數傳誰就讓誰執行
        */
    </script>

這就是JS 中的五種THIS情況,記錄下,以便以後用到(很多執行過程寫在了代碼註釋中,便於理解)。
加油~
學過的每樣東西,都會派上用場

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章