最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 来一份this相关知识点详细总结!

    正文概述 掘金(夜剑剑)   2020-12-03   423

    前言

    thisJavaScript中是非常重要的概念,因为我们用到它的频率非常之高,在享受到它的便利性的同时,与之对应的是它的绑定规则比较难理解。今天我们就来好好总结一下this的相关知识点,这样在使用它的过程中就能更有自信和把握啦!

    了解一下this

    老规矩,在总结之前我们需要得先了解一下,this是什么东西?

    this是什么

    我们先来看一个例子:

        function foo() {
          console.log(this);	//Window
        }
        foo();
    

    我们定义了一个函数foo并执行,在函数体中我们直接打印输出this,发现结果并不是undefined,而是Window对象。这是为什么呢?这就引出了相关的概念:

    只看概念云里雾里的,但这句话讲出了两个关键点:

    1. this是一个关键字
    2. this自动定义在所有函数的作用域中

    我们分别剖析一下:

    1. this是一个关键字

    这句话说出了this的本质其实是一个关键字,只不过有些特别,具体特别在哪里我们下面再介绍。既然this是一个关键词,我们就得注意它应该具备的一个特点了:this无法被重写。我们修改一下刚刚的例子试试:

        function foo() {
          this = "null"; //Uncaught SyntaxError
          console.log(this);
        }
        foo();
    

    可以看到,this的值无法被修改,否则会报错。

    2. this自动定义在所有函数的作用域中

    这句话里面包含很多信息点,首先就是自动定义这个说法,指出了this这个关键词在函数作用域中会被自动定义,这也就是为什么我们刚刚在foo函数中可以直接输出this得到Window对象。

    其次是所有函数的作用域中,这句话说明了,在所有的的函数作用域中都存在this关键字。但这不是说只有在函数中才有this,假如我们试着在全局直接输出this会是什么结果呢?

    console.log(this);	//Window
    

    可以看到,同样输出了Window对象,这说明在全局作用域下同样存在this关键词。

    看到这里你可能有点疑问了,为什么举得例子中,this的值都是Window对象,是不是this的值就是等于Window对象呢?我们再看个例子:

        var obj = {
          say: function () {
            console.log(this);
          },
        };
        obj.say();	//{say: ƒ}
    

    我们将例子修改了一下,发现此时输出的this值变成了obj对象,这说明this的值并不是之前猜测的固定等于Window对象,那么this值到底是怎么来的呢?

    this的指向值

    事实上,我们之前在介绍执行上下文的时候有介绍到(感兴趣的话可以点这里看一下),在动态创建执行上下文时,会确认This Binding也就是this的值,它和函数定义的位置无关,而是由函数调用时的绑定规则决定的

    这也就是为什么,在全局的情况下this值也是存在的,正是因为存在全局上下文,而且this的值就存储在这个上下文中。

    那么this值具体指向谁,函数调用时的绑定规则是什么,这就是我们接下来要讲的重头戏了。

    this的绑定规则

    我们刚刚讲了,this的指向值由函数调用时的绑定规则决定,我们现在就来讲讲这些绑定规则分别有哪些。

    默认绑定

    默认绑定是最基本的绑定规则,它被应用在其他规则均不适用的情况下,因此也是最常见的绑定规则。默认绑定比较典型的一种判断就是:当使用不带任何修饰的函数引用进行调用时,只能使用默认绑定,而不能使用其他绑定规则。 举个例子:

    function foo(){
    	console.log(this);	//Window
    }
    foo();
    

    可以看到,这里的foo()就是不带任何修饰的函数调用,foo前面光秃秃的啥也没有。另外你会发现,这里输出的this值为Window对象,那大家就已经知道了这可能就是默认规则下this的指向值,不过不准确,事实上默认规则下this的指向值可以分几种情况:

    • 严格模式

    在严格模式下,使用默认规则得到的this值会指向undefined,可以看代码:

    'use strict'
    function foo(){
    	console.log(this);	//undefined
    }
    foo();
    
    • 非严格模式

    非严格模式下,如果应用了默认规则,那么this的值会指向Window对象,这也就是为什么我们刚刚举得好几个例子this值都是Window,看下代码:

    function foo(){
    	console.log(this);	//Window
    }
    foo();
    
    • 严格模式和非严格模式混用

    这种模式比较特殊,如果我们在非严格模式下定义了函数,又在严格模式下调用了函数,最终this的值还是会指向Window对象,看下代码:

        function foo() {
          console.log(this);	//Window
        }
        {
          ("use strict");
          foo();
        }
    

    隐式绑定

    讲完了默认的绑定规则,那么肯定要看看一些特殊的绑定规则了。当函数作为某个对象的方法调用时,此时这个对象就是函数的上下文对象,这时候this会指向这个对象,我们来看个例子:

        var obj = {
          foo: function () {
            console.log(this);	//{foo: ƒ}
          },
        };
        obj.foo();
    

    可以看到,我们给对象obj定义了一个方法foo,并通过obj.foo()的方式进行调用,此时的obj对象就是函数foo调用时的上下文对象,因此this会指向obj对象,我们把这种情况称为隐式绑定。隐式绑定有几个重要的点需要着重说明一下:

    隐式丢失

    隐式绑定的函数,在有些情况下会丢失绑定的上下文对象,这时候就会应用我们的默认绑定规则,把this指向Window对象(非严格模式)或者undefined(严格模式)。我们先来看一个例子:

        var name = "我是全局的name";
        var obj = {
          name: "我是obj的name",
          foo: function () {
            console.log(this.name); //我是全局的name
          },
        };
        let fn = obj.foo; //这里用变量fn保存foo函数
        fn();
    

    这里我们用一个变量fn保存了obj下的foo函数的引用,此时调用以后输出的是全局对象下定义的name变量值,可见此时的this对象指向了Window

    当我们执行let fn = obj.foo时,实际上是将fn指向了函数foo的地址,因此当我们执行fn()的时候实际上就是在执行foo(),上面的代码也就是这样:

        var name = "我是全局的name";
        function foo() {
          console.log(this.name); //我是全局的name
        }
        foo();
    

    这时候实际上就是不带任何修饰的函数调用,因此会应用默认绑定规则。

    隐式丢失还有一种情况,就是对象的方法作为参数传递到另外的函数中去,来看个例子:

        var name = "我是全局的name";
        var obj = {
          name: "我是obj的name",
          foo: function () {
            console.log(this.name); //我是全局的name
          },
        };
    
        function otherFn(fn) {
          fn();
        }
        otherFn(obj.foo);
    

    这个例子和上个例子唯一的不同就是在后面,我们不是通过变量保存foo函数后再调用,而是把foo函数作为参数传递给了另一个函数,然后在另一个函数中调用,这里为什么也发生了隐式丢失呢?

    答案是,参数在传递给函数的时候,会发生一个赋值操作,也就是将实参赋值给形参,函数部分的代码可以看成这样:

        function otherFn() {
          var fn = obj.foo;	//这里实际上就是将实参赋值给形参
          fn();
        }
        otherFn(obj.foo);
    

    这下我们就发现了,这种情况和上面那种隐式丢失的情况一样,也是不带任何修饰的函数调用,因此也应用了默认规则,发生了隐式丢失

    对象属性引用链

    我们刚刚说到,当函数作为对象的方法调用时,会以这个对象作为上下文对象,所以this会指向这个对象,那么当多个对象嵌套在一起后调用方法以后this会指向哪个对象呢?我们来看个例子:

        var obj1 = {
          name: "obj1",
          obj2: {
            name: "obj2",
            obj3: {
              name: "obj3",
              foo: function () {
                console.log(this.name); //obj3
              },
            },
          },
        };
        obj1.obj2.obj3.foo();
    

    是不是看蒙了?不要怕,我们看到结果输出了obj3,事实上当我们通过对象属性调用链来调用方法时,最终起作用的只有函数前面的那个调用对象。比如刚刚的obj1.obj2.obj3.foo(),实际上可以看成obj3.foo()。同理,哪怕是...a.b.c.d.e.foo(),我们只要看最后的e.foo()即可。

    显式绑定

    我们刚刚介绍了隐式绑定this的指向值完全由调用对象决定,十分难把控,有没有一个办法,不管我是通过哪个对象调用的,我都可以指定我想要的this指向值呢?有的,在函数的原型上有两个方法callapply,可以传入指定的对象作为this的指向值,不过两者有一些区别,我们来分别看下用法。

    call的用法

    关于call的定义和基本用法可以参照MDN:传送门

    我们看看call如何显式的改变this的指向值:

        var obj = {
          name: "obj",
        };
        function foo() {
          console.log(this.name);	//obj
        }
        foo.call(obj);
    

    foo函数调用call方法,并传入obj作为第一个参数,obj会作为foo函数的this指向值,并执行函数。那么call方法是如何实现这样的功能的呢?我们下面再讲,我们先来看下apply

    apply的用法

    关于apply的定义和基本用法可以参照MDN:传送门

    apply的用法和效果和call几乎一样,看例子就可以看出来:

        var obj = {
          name: "obj",
        };
        function foo() {
          console.log(this.name);	//obj
        }
        foo.apply(obj);
    

    从这两个例子来看的话,两者几乎完全相同,但它们还是有区别的,区别就在于它们接收参数的方式不同,我们看个例子:

        var obj = {
          name: "obj",
        };
        function foo(a,b,c) {
          console.log(this.name,a,b,c);	//obj 1 2 3
        }
        foo.call(obj,1,2,3);
        foo.apply(obj,[1,2,3]);
    

    可以看到,call函数接收多个参数并传入foo函数中,而apply函数接收的参数放在了一个数组里,然后拆分开来传入了foo函数中,这就是两者的区别。

    我们通过调用call或者apply的方式显式的绑定了this的指向值,但是你会发现通过call或者apply的方式会直接调用函数,就没办法把函数连带着this一起传到别的函数中存储或者执行。能不能给函数先绑定好了this值,再套个壳子包装好,这样就可以传来传去的还保留着绑定好的this值了呢?于是我们的bind函数就应运而生了。

    bind的用法

    关于bind的定义和基本用法可以参照MDN:传送门

    bind函数可以传入指定对象作为this的指向对象,除此之外,bind函数还可以接受参数并存储起来,下次调用的时候可以只传入剩余的参数,我们来看个例子:

        //还是刚刚隐式丢失的例子,我们使用bind函数绑定上obj对象看看
        var name = "我是全局的name";
        var obj = {
          name: "我是obj的name",
          foo: function () {
            console.log(this.name);
          },
        };
    
        function otherFn(fn) {
          //会发现函数被传入进来以后,this依然指向obj而没有发生隐式丢失
          fn(); //我是obj的name
        }
        otherFn(obj.foo.bind(obj)); //我们这里不直接传入,而是把obj.foo包装一下再传入
    

    从这个例子我们可以看到,通过bind绑定了this指向值的函数,即使传入了其他函数中执行也不会丢失this对象。我们再举个例子看看,如何在绑定this的同时存储参数:

        function foo(a, b, c) {
          console.log(a, b, c);
        }
    
        //第一个参数为null时,非严格模式下会以Window对象作为this指向值,后面会介绍
        //给foo函数套了层壳子,并存储了两个参数1,2
        var bindFoo = foo.bind(null, 1, 2);
        //当调用这个包装函数的时候,传入的参数会连同之前存储的参数一起传给foo函数
        bindFoo(3); //1 2 3
    

    这样我们就实现了预传参数和this值,可以方便传值的函数啦~

    还有一种情况,我觉得这种显示绑定的方式太僵硬了,其实我想要一种更灵活的绑定方式,我想预设一个this指向对象,当我不小心应用了默认绑定规则,this指向了Window或者undefined时候把this重新指向我预设的对象,否则的话就指向他本来的this对象,这样可以吗?好家伙,要求还不少,但是我满足你了,那就是我们的softBind啦~

    softBind的用法

    softBind函数在函数原型上并不存在,是后来创造的,顾名思义就是软绑定,为了实现我们刚刚说的需求而出现的。 既然原型上没有,自然要介绍一下怎么定义实现的啦:

        Function.prototype.softBind = function (obj) {
          //先拿到调用softBind的函数本身
          var fn = this;
    
          //这里是为了拿到传入的其他参数,并存储起来
          var curried = [].slice.call(arguments, 1); //arguments是类数组所以没有slice方法
    
          //这里是返回的包装好的函数
          var bound = function () {
            
            //判断this的情况,这里的this是返回的封装函数执行时的this,和调用softBind函数时的this不同
            var that = (!this || this === (window || global)) ? obj : this; //判断this是否空,同时考虑node环境
    
            //这里的目的是为了把包装时传入的参数,和执行包装函数时传入的参数进行合并,arguments和之前的arguments不同
            var newArguments = [].concat.apply(curried, arguments);
    
            //这里其实就是调用函数
            return fn.apply(that, newArguments);
          };
    
          //有一个细节是调整包装好的函数的原型链,使得instanceof能够用于包装好的函数的判断
          bound.prototype = Object.create(fn.prototype);
          return bound;
        };   
    

    看不懂的话慢慢琢磨一下,然后我们来举个例子看看它的用法:

        var name = "我是全局的name";
        var obj1 = {
          name: "我是obj1的name"
        };
        var obj2 = {
          name: "我是obj2的name"
        };
    
        function foo(){
        	console.log(this.name)
        }
        
        //包装一个默认this为obj1的函数
        var fn=foo.softBind(obj);
        
        //当通过obj2调用时,会使用obj2作为this值
        fn.call(obj2);	//我是obj2的name
        
        //当不加修饰符调用时,会应用绑定的this值
        fn();	//我是obj1的name
    

    这样我们就实现了一个灵活的显示绑定函数啦~

    new绑定

    new操作符也可以实现改变this指向,关于new操作符的知识点我在之前的文章有介绍过:传送门,这里我们简单介绍下new操作符做了什么:

    • 创建一个新对象,将this绑定到新创建的对象
    • 使用传入的参数调用构造函数
    • 将创建的对象的_proto__指向构造函数的prototype
    • 如果构造函数没有显式返回一个对象,则返回创建的新对象,否则返回显式返回的对象(即手动返回的对象)

    然后我们来看个例子:

    function Foo(name){
    	this.name=name;
    }
    
    let person=new Foo('xiaowang');
    console.log(person);	//xiaowang
    

    可以看到,当函数作为构造函数执行new的过程中,this指向了最终创建的实例person,说明new操作符确实能够改变this的指向。

    箭头函数绑定

    除了之前介绍的那么多种,还存在着一种ES6中的特殊函数类型:箭头函数。箭头函数中的this比较特殊,它的指向值不是动态决定的,而是由函数定义时作用域中包含的this值确定的,我们举个例子:

        //定义一个箭头函数
        var foo = () => {
          console.log(this.name);
        };
        var name = "我是全局的name";
        var obj1 = {
          name: "我是obj1的name",
        };
    
        foo.call(obj1); // "我是全局的name"
    

    可以看到,虽然我们调用了call传入了obj1,但最终输出的值还是全局的name,这是因为函数foo定义在全局中,因此this会指向window对象。

    绑定规则的优先级

    讲了这么多,头都大了,不要着急,休息会儿我们再来看个关键的知识点,关于this绑定规则的优先级。我们刚刚讲了很多绑定规则,但没有讲这些绑定规则组合起来的结果会是如何,当多个绑定规则同时运用的时候,会使用优先级更高的绑定规则。那这些规则的优先级怎么排列呢?我们按照由低到高进行排序的话就是:

    1. 默认绑定
    2. 隐式绑定
    3. 显示绑定
    4. new操作符绑定、箭头函数

    接下来我们分别看几个例子来验证它们的优先级:

    • 默认绑定和隐式绑定

    直接看开始的例子就好了:

    var obj = {
          foo: function () {
            console.log(this);	//{foo: ƒ}
          },
        };
        obj.foo();
    

    毫无疑问,结果应用了隐式绑定的this值,因此隐式绑定的优先级是大于默认绑定的

    • 隐式绑定和显示绑定

    我们也举个简单的例子:

        var obj1 = {
          name: "obj1",
          foo: function () {
            console.log(this.name);
          },
        };
        var obj2 = {
          name: "obj2",
        };
    
        obj1.foo.apply(obj2); //obj2
        obj1.foo.call(obj2); //obj2
        obj1.foo.bind(obj2)(); //obj2
    

    最终也可以看出来,隐式绑定的结果被显示绑定覆盖了,因此显式绑定的优先级是大于隐式绑定的

    • 显示绑定和new操作符绑定

    由于callapply只能执行函数,没法和new操作符一起使用,因此我们只对比bind函数和new操作符的优先级。

        var obj = {};
        function foo() {
          this.name = "obj";
        }
        //将foo绑定到obj上
        var fn = foo.bind(obj);
    
        //执行new操作
        var f1 = new foo();
        console.log(obj.name); //undefined
        console.log(f1.name); //obj
    

    可以看到,foo已经显式绑定obj对象了,最终name值还是赋值到了实例f1上,因此new操作符绑定的优先级是大于显式绑定(bind)的

    你可能会疑惑,bind明明为foo函数套了层壳,按照new操作符的逻辑怎么都不能把里面的this指向改了才对,事实上bind函数内部做了判断,如果和new操作符一起使用的话,要把this让给new操作符的对象,这也坐实了它们之间优先级的关系了。

    • 箭头函数绑定和new操作符绑定

    由于箭头函数是不可构造的,所以无法和new操作符组合,因此我把他们放在了同级。

    加强理解:手写call,apply,bind函数

    call实现

    我们以foo.call(obj)举例说明:

      Function.prototype.myCall = function (context) {
        //判断传入的this指向值是否为空,context其实就是obj
        context = context || window;
    
        //我们命名一个独一无二的属性名,避免重名导致覆盖
        let fn = new Symbol('fn');
    
        //这里的this其实就是函数foo,我们把它作为传入的context对象的属性进行调用,这样就可以利用隐式绑定的规则设置this
        context[fn] = this
    
        //去除掉传入的context值,剩下的就是参数了
        const args = [...arguments].slice(1);
    
        //执行函数并返回结果
        const result = context.fn(...args);
    
        //删除这个属性值,销毁犯罪现场
        delete context.fn;
    
        return result;
      }
    

    apply

    我们同样以foo.apply(obj)举例说明,大部分代码相同,但传入的参数由于是数组,所以参数数量是已知的:

      Function.prototype.myApply = function (context, args) {
        //判断传入的this指向值是否为空,context其实就是obj
        context = context || window;
    
        //我们命名一个独一无二的属性名,避免重名导致覆盖
        let fn = new Symbol('fn')
    
        //这里的this其实就是函数foo,我们把它作为传入的context对象的属性进行调用,这样就可以利用隐式绑定的规则设置this
        context[fn] = this
    
        //执行函数并返回结果,...可以把数组中的参数展开
        const result = context[fn](...args);
    
        //删除这个属性值,销毁犯罪现场
        delete context.fn;
    
        return result;
      }
    

    bind

    bind的话其实就是对上面的显示绑定做了包装,同样以foo.bind(obj)为例:

    Function.prototype.myBind = function (context, ...args) {
        //同样是为了拿到调用bind的函数本身,如foo
        const fn = this
    
        //对参数做个空处理
        args = args ? args : []
    
        //bind会返回一个闭包函数,保存传入的context和参数
        return function newFn(...newFnArgs) {
          //对new操作和bind组合的情况,做处理,把this让给new
          if (this instanceof newFn) {
            return new fn(...args, ...newFnArgs)
          }
    
          //否则直接通过apply显示绑定context目标
          return fn.apply(context, [...args, ...newFnArgs])
        }
      }
    

    大功告成!

    总结

    关于this的知识点总结写了很多,我不想只是简单的罗列知识点,而是想要有条理有层次的介绍,同时写出我自己的理解,这样的总结才更有意义。码了这么多字真不容易,希望能帮到你们~最后毕竟内容比较多,有错误的地方希望大家指出,谢谢!

    写在最后

    1. 很感谢你能看到这里,不妨点个赞支持一下,万分感激~!

    2. 以后会陆续更新更多文章和知识点,感兴趣的话可以关注一波~


    起源地下载网 » 来一份this相关知识点详细总结!

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元