zoukankan      html  css  js  c++  java
  • 读书笔记-你不知道的JS上-this

    关于this

     

      与静态词法作用域不用,this的指向动态绑定,在函数执行期间才能确定。感觉有点像C++的多态?

        var a = 1;
        var obj = {
            a: 2,
            fn: function() {
                console.log(this.a);
            }
        };
        obj2 = {
            a: 3,
            fn: obj.fn
        };
        //通过对象调用 this指向obj
        obj.fn(); //2
        //通过函数调用 this指向window
        setTimeout(obj.fn, 0); //1
        obj2.fn(); //3

      这个例子很好理解,谁调用的函数,this就指向谁。

      当一个函数被调用时,会创建一个活动记录(上下文)。这个记录会包含函数在哪里被调用,函数的调用方法,传入的参数等信息。this就是记录的其中一个信息。

     

      开始详解this了!

      理解this绑定,首先要理解调用位置:调用位置就是函数在代码中被调用的位置。

      分析位置最重要的是分析调用栈,我们关心的调用位置就在当前正在执行的函数的前一个调用中。

        //调用栈是f1 即全局作用域
        function f1() {
            console.log('f1');
            f2(); //f2调用位置
        }
        //调用栈是f1 => f2
        function f2() {
            console.log('f2');
            f3(); //f3调用位置
        }
        //调用栈是f1 => f2 => f3
        function f3() {
            console.log('f3');
        }
        f1(); //f1调用位置

       

      通过找到调用位置,判断需要应用哪一条规则。this绑定的四条规则:

     

    默认绑定

      最常用的函数调用类型:独立函数调用。

        var a = 1;
        // 全局函数
        function fn() {
            // 'use strict' error
            console.log(this.a); //1
        }
        //默认this绑定到window
        fn();

      在这里,fn是直接进行调用,没有任何修饰,相当于window.fn(),只能适用默认绑定,指向了window。

     

    隐式绑定

      这种绑定需要考虑调用位置是否有上下文对象。

        function fn() {
            console.log(this.a);
        }
        var obj = {
            a: 2,
            fn: fn
        };
        obj.fn(); //2

      当fn被调用时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。因为调用fn时this被绑定到obj,因此this.a和obj.a是一样的。

      另外,链式调用时,只有最近的调用对象会影响this。

        var a = 1;
        var obj = {
            a: 2,
            fn: function() {
                console.log(this.a);
            }
        };
        obj2 = {
            a: 3,
            fn: obj
        };
        //this.a相当于obj.a
        obj2.fn.fn(); //2

    隐式丢失

      一个最常见的this绑定问题是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把this绑定到全局对象或undefined上。

        function fn() {
            console.log(this.a);
        }
        var a = 1;
        var obj = {
            a: 2,
            fn: fn
        }
        var f = obj.fn;
        obj.fn(); //2
        f(); //1

      被作为函数调用时,会被默认绑定到window对象。

      另外一种情况是传入回调函数时:

        function fn() {
            console.log(this.a);
        }
    
        function fn2(fn) {
            fn();
        }
        var a = 1;
        var obj = {
            a: 2,
            fn: fn
        }
        //还是作为函数调用
        fn2(obj.fn);

     

    显式绑定

      就是用apply和call方法强制绑定this。一个例子说明一切:

        var a = 1;
        var obj = {
            a: 2,
            fn: function() {
                console.log(this.a);
            }
        };
        obj2 = {
            a: 3,
            fn: obj.fn
        };
        obj.fn.call(this); //1
        obj.fn.call(obj); //2
        obj.fn.call(obj2); //3

     

    硬绑定

      直接用vue源码来举例子吧。

        function bind(fn, ctx) {
            return function(a) {
                var len = arguments.length;
                return len ? len > 1 ? fn.apply(ctx, arguments) : fn.call(ctx, a) : fn.call(ctx);
            }
        }

      简单暴力,都会背了。

     

    new绑定

      JS中new的机制实际上和面向类的语言完全不同。

      在JS中,构造函数只是一些使用new操作符时被调用的函数,它们不属于某个类,也不会实例化一个类。实际上,它们甚至都不能说是一种特殊的函数类型,它们只是被new操作符调用的普通函数而已。

      Number作为构造函数时,ES5.1中这样描述:当Number在new表达式中被调用时,它是一个构造函数,会初始化新创建的对象。

      因为,包括内置对象函数在内的所有函数都可以用new来调用,这种函数调用被称为构造函数调用。实际上不存在所谓的构造函数,只有对于函数的构造调用。

      使用new来调用函数,会自动执行下面的操作:

      1、创建一个全新的对象。

      2、这个新对象会被执行【原型】连接。

      3、这个新对象会绑定到函数调用的this。

      4、如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。

        function fn(a, b) {
            this.a = a;
            this.b = b;
        }
        var f = new fn();
        console.log(f); //{a:undefined,b:undefined}

     

     

    优先级

     

      如果某个调用位置应用多条规则,需要为这些规则设定优先级。(默认绑定、隐式绑定、显示绑定、new绑定)

      首先默认绑定优先级最低。

      隐式 VS 显示?

        function fn() {
            console.log(this.a);
        }
        var obj1 = {
            a: 2,
            fn: fn
        };
        var obj2 = {
            a: 3,
            fn: fn
        }
        obj1.fn(); //2
        obj2.fn(); //3
        obj1.fn.call(obj2); //3
        obj2.fn.call(obj1); //2

      显式比较厉害。

     

      再来比较一下new和显示绑定优先级:

        function fn(a) {
            this.a = a;
        }
        var obj = {};
        //this强制绑定到obj对象上
        var f1 = fn.bind(obj);
        f1(2);
        //现在obj有了一个a属性
        console.log(obj.a); //2
        //new一个对象将this.a改成3
        var f2 = new f1(3);
        //这里依然没有变
        console.log(obj.a); //2
        //将this指向了new出来的a
        console.log(f2.a); //3

      可以看出,通过new的绑定,覆盖了bind强制绑定的a,所以new绑定是优先于显示绑定的。

     

      顺便贴一下MDN提供的bind()实现:

        if (!Function.prototype.bind) {
            Function.prototype.bind = function(oThis) {
                if (typeof this !== 'function') {
                    throw new TypeError('Function.prototype.bind - what is trying ' +
                        'to be bound is not callable');
                }
                var aArgs = Array.prototype.slice.call(arguments, 1),
                    fToBind = this,
                    fNOP = function() {},
                    fBound = function() {
                        return fToBind.apply(
                            this instanceof fNoP && oThis ? this : oThis,
                            aArgs.concat(Array.prototype.slice.call(arguments))
                        );
                    };
                fNOP.prototype = this.prototype;
                fBound.prototype = new fNOP();
                return fBound;
            };
        }

      

    总结

    优先级为:

      1、如果使用了new,this绑定新创建的对象。

      2、通过call、apply显式绑定。

      3、是否使用隐式绑定?即对象调用

      4、其余情况默认绑定在window上

     

    例外情况

      1、如果把null或者undefined作为this的绑定对象传入call、apply或者bind,这些值在调用时会被忽略,实际应用默认绑定规则(window)。

      如果用apply展开数组可能用得上,比如fn.apply(null,arr),其中arr为数组,里面的值作为参数传给fn。

      但是这个参数十分不安全,可能会不经意的修改全局变量,可以通过纯空对象来创建一个安全的this。

        var a = 1,
            b = 1;
        console.log(a); //1
    
        function fn(a) {
            this.a = a;
        }
    
        function fn2(b) {
            this.b = b;
        }
        //原本可能只想利用apply
        fn.apply(null, [2]);
        //不小心改了全局变量!
        console.log(a);
        //创建一个纯空对象
        var obj = Object.create(null);
        console.log(b); //1
        //安全的this
        fn2.call(obj, 3);
        // 现在安全了!
        console.log(b); //1

       

      

      但是引入了箭头函数,问题就有点奇怪了。

        var a = 1;
        var obj = {
            a: 2,
            fn: () => {
                console.log(this.a);
            }
        };
        obj2 = {
            a: 3,
            fn: obj.fn
        };
        obj.fn(); //1
        setTimeout(obj.fn, 0); //1
        obj2.fn(); //1

      全是1,这是怎么回事?换arguments的例子来看:

        function fn() {
            setTimeout(function() {
                console.log(arguments);
            }, 0)
        }
        fn(1, 2); //[] 确实未传入参数
        function fn2() {
            setTimeout(() => {
                console.log(arguments);
            }, 0)
        }
        fn2(1, 2); //[1,2] 把外部的参数吸进来了!

      简单来讲,箭头函数使得this查询回到了词法作用域规则,它本身不具有this、arguments等属性,根据外层作用域来决定this。

        function fn() {
            return (a) => console.log(this.a);
        }
        var obj = {
            a: 1
        };
        var obj2 = {
            a: 2
        }
        var f = fn.call(obj);
        f(); //1
        f.call(obj2); //1

      当箭头函数外部的fn被绑定到obj时,无论怎么调用和绑定,this指向永远是上一层作用域(window),所以会输出1。

     

    真·总结

      如果要判断一个运行中函数的this绑定,就需要找到这个函数的直接调用位置。找到之后就可以顺序应用下面4条规则来判断this的绑定对象。

      1、由new调用?绑定到新创建的对象。

      2、由call、apply或bind调用?绑定到指定对象。

      3、由上下文调用?绑定到那个上下文调用。

      4、默认:严格模式绑定到undefined,否则绑定到全局对象。

     

  • 相关阅读:
    [Swift]关键字:class与staitc的区别
    [Swift]LeetCode1171. 从链表中删去总和值为零的连续节点 | Remove Zero Sum Consecutive Nodes from Linked List
    [Swift]LeetCode1172. 餐盘栈 | Dinner Plate Stacks
    [Swift]LeetCode1170. 比较字符串最小字母出现频次 | Compare Strings by Frequency of the Smallest Character
    [Swift]LeetCode1169. 查询无效交易 | Invalid Transactions
    [Swift]LeetCode1167. 连接棒材的最低费用 | Minimum Cost to Connect Sticks
    [Swift]LeetCode1166.设计文件系统 | Design File System
    [Swift]LeetCode1165. 单行键盘 | Single-Row Keyboard
    [Swift]LeetCode1168. 水资源分配优化 | Optimize Water Distribution in a Village
    METRO风格
  • 原文地址:https://www.cnblogs.com/QH-Jimmy/p/6471337.html
Copyright © 2011-2022 走看看