最全JS 的this机制,讨论this指向问题

关于this

关于this关键字是javaScript最复杂的机制之一。是一个特别的关键字,被自动定义在所有函数的作用域中。很多人其实都不知道具体指向什么。
本篇文章介绍this的目标:

  1. 了解为什么要用this机制
  2. 常见对this指向的误解
  3. this到底是什么
  4. this的绑定规则
  5. this绑定例外
  6. this的词法(涉及ES6新加的胖箭头函数)
    先来一些必要的概念:
    this是函数特有的关键字,当然你在函数外面(全局内)调用 也是生效的。

为什么要用this机制

遇到问题,寻找官方MDN解决问题:
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/this

官方解释:
与其他语言相比,**函数的 this 关键字(this是函数特有的关键字)**在 JavaScript 中的表现略有不同,此外,在严格模式和非严格模式之间也会有一些差别。
这里的浏览器在解释执行JS程序时用 严格模式 和非严格模式两种。 如果直接执行代码,现在的浏览器默认都是在strict模式下。 可以在函数里面加 “use strict” 和" use no strict"指定浏览器的执行环境

在绝大多数情况下,函数的调用方式决定了 this 的指向(运行时绑定)。this 不能在执行期间被赋值,并且在每次函数被调用时 this 的值也可能会不同。ES5 引入了 bind 方法来设置函数的 this 值,而不用考虑函数如何被调用的。ES2015 引入了箭头函数,箭头函数不提供自身的 this 绑定(this 的值将保持为闭合词法上下文的值)。

总的来说,this的指向与他最有直接关系的就是执行上下文,调用位置与调用位置最后也是对上下文产生影响。(当然也是个人理解) 所以下文会针对执行上下文进行分类讲解。大概有 全局上下文 函数上下文 类上下文 派生类等。函数上下文和类上下文其实差不多,类其实也是函数,只是与普通的类有点区别。

所以为什么要用this呢?

this能够很好的节约代码! 在函数运行过程中需要对上下文的一些属性,方法等进行引用,在引用时会重复写对象的名称 函数名称等很多比较繁琐的操作,引用this时就可以解决这种问题(仅限个人的见解,该死的求生欲哈哈哈)。但是随着代码越写越多,结构越来越复杂的时候,this很灵活,到底是指向哪里是一个很值得深究的问题。

常见对this指向的误解(也仅限个人的理解)

1. 指向自身
常见的误解是指向函数自身,从“this”这个英语单词的角度来说,也许是成立的。JS中所有的函数都是对象,就可以在调用函数的过程中存储状态(也就是属性的值)。大多数情况下,这个说法其实是成立的,但是举个反例来说,
看如下代码:

<script>
    // 常见this指向问题  误解
    //1. 指向函数自身
    function foo(num) {
        console.log("foo" + " " + num);
        // 记录foo被调用的次数
        this.count++;
    }
    foo.count = 0; //为foo函数对象追加一个count属性 赋值为0
    for (var i = 0; i < 10; i++) {
        if (i > 5) {
            foo(i);
        }


    }
    //foo到底被调用了多少次?
    console.log("foo到底被调用了多少次  " +
        foo.count);
</script>

输出结果:
在这里插入图片描述
分析一下代码:
函数foo(num){ }接受参数 并正常调用输出没问题。 foo.count=0 用来给函数对象追加一个count属性也没问题,那照this指向函数自身的说法,this.count 就是访问自身的属性count, 并且每一次调用foo(num) 都会实现+1操作, 最后foo被调用的次数就应该为4 但是输出的是0!!! 证明,其实this指向自身这种说法是错误的。 (其实在隐式的创建了一个全局对象,当变量没有声明 直接使用时会创建一个全局对象,而且值时NaN, 那为什么是NaN,而不是其他的类型? 比如undefind? 这里先不深究)
研究结果如下:接上上面的代码

 // 因为this.count中的count并不是我们追加的foo里面的count,这里是的变量直接没有声明就
    // 直接使用   这会间接导致全局对象的创建 而且值为NaN,  为什么?  这里先不深究
    console.log("我是全局对象 count  " +
        count);

输出:
在这里插入图片描述

2. this指向函数的作用域?
这个说法以及第一个说法,我曾经也非常的认同。但是现在就迷糊了。。。。。。

这个说法在很多情况下也是成立的,但是“作用域” 是一个抽象出来的概念,不能用具体的代码去访问, 虽然作用域和对象非常相似,可见的标识符都是它的属性,但是不能因为相似就混为一谈。 作用域是无法用具体的代码去访问的,它存在与js引擎内部。而且需要明确的是。this在任何情况下,都不会指向函数的词法作用域。

到底什么是this

排除了一些常见的误解之后,我们正式来给this的指向 来做一个准确的定义。
this 的指向取决于 函数的调用位置和调用方式嘻嘻相关的。 它的上下文实在函数运行时动态决定的。
官网有话要说:绝大多数情况下,函数的调用方式决定了 this 的值(运行时绑定)。this 不能在执行期间被赋值,并且在每次函数被调用时 this 的值也可能会不同。
执行上下文: 当函数运行时,会创建一个记录,该记录包含了函数的调用位置(调用栈)和调用方式,以及传递的参数等。 This就是记录其中的一个属性,供函数在执行时调用。 所以这里的执行上下文其实就是This指向的一个重要的点。

说到this的指向与 函数的调用位置和调用方式有关,
那我们怎么确定函数的实际的调用位置(注意,是调用 的位置 而不是声明的位置)。

这里就涉及到调用栈的概念了。
调用栈: 为了执行到当前的这个函数 所调用的所有函数,我们关心的调用位置就在里面。 在实际开发中可以在浏览器提供的开发者工具来确定调用栈与函数的执行位置。 如goole 中javaScript的调试工具确定函数的调用栈和 函数在调用栈中的位置(函数被调用的位置) 通过分析调用位置,其实是为了更好的确定执行上下文,确定好执行上下文之后 确定This会使用哪条绑定规则(下文会讲), 最终就确定了this的指向
代码:

<script>
 //确定 调用栈  调用位置
    function baz() {
        console.log("baz");
        bar();
    }

    function bar() {
        console.log("bar");
        foo();
    }

    function foo() {
        console.log("foo");
    }
    baz();
    //baz实际的调用位置</script>

请添加图片描述

this的绑定规则

this的绑定规则有:下面会一一介绍

  1. 默认绑定
  2. 隐式绑定
  3. 显式绑定
  4. new绑定
  5. 还有就是有绑定例外情况

官网: (官网还是保持 This的指向只跟函数的调用位置有关系,其实调用方式也是隐式的影响实际的调用位置,调用位置决定了该函数的执行上下文)

官网: 官网是根据this在执行上下文的位置来分的。 分了This的调用位子位于 全局环境下(没有在函数内使用时)的全局上下文,this在函数内部调用时就是处于 函数上下文(也是类上下文)
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/this

1. 默认绑定

默认绑定要分清除 this是处在哪种执行上下文时,当处于全局上下文和函数上下文(类上下文)时有所不同

- 全局上下文

this的执行上下文是全局上下文(也就是函数体外)时,也就是this在全局环境下调用时,this的默认绑定规则会将其绑定在全局对象中(window) 而且不管是不是运行在严格模式下 还是非严格模式下
结合代码分析:

<script>
 // "use strict"
    "use no strict"
    //当This 处于“全局上下文时”
    var a = 2;
    console.log(this == window); //true
    console.log(this.a); //2
    console.log(window.a) //2
</script>

在这里插入图片描述
分析代码:当this处于全局上下文时 This会指向全局对象window,不管是否是严格模式下。

- 函数上下文(类上下文)
声明:当然这里只举最简单的函数调用方式 foo() ,函数执行上下文没有对象的情况哦!!!!!! 当函数的执行上下文有对象时 会有另外的绑定规则。下面会介绍。
不管是函数上下文还是类上下文,This的绑定(也就是指向)只取决与实际的调用位置。
这里比较特殊的是,当this处于函数上下文(类上下文)时,要分是否在严格模式下
严格模式:指向undefined
非严格模式:指向全局对象

代码分析:

<script>
    // 当this处于“函数上下文”时
    function foo() {
        "use strict";
        console.log(this.a); //TypeError: Cannot read properties of undefined (reading 'a')

    }
    var a = 6;
    foo();
</script>

在这里插入图片描述

<script>
    // 当this处于“函数上下文”时
    function foo() {
        // "use strict";
        "use no strict"; 
        console.log(this.a);//6

    }
    var a = 6;
    foo();
</script>

在这里插入图片描述
代码分析:

2.隐式绑定

这种绑定规则就是考虑到函数上下文有没有涉及对象,this会绑定在这个函数上下文的对象上。(换种说法就是 This会绑定在调用这个函数的对象上)。
代码:

<script>
    //隐式绑定
    function foo() {
        console.log(this.a)
    }
    var a = 3; //全局对象,做对比
    var obj = {
        a: 2,
        foo: foo
    }
    obj.foo(); //2
</script>

在这里插入图片描述
代码分析:
foo函数的调用位置obj.foo() 涉及对象,所以this将会绑定在调用该函数的对象obj上,所以通过this.a访问到对象里面的属性a 值为2, 而不是全局属性a 值为3!

隐式丢失:
但是隐式绑定会有this绑定的对象丢失的情况(可以用硬绑定解决,后面会涉及),会使用默认绑定规则, 取决于是否是严格模式下。
严格:undefined
不严格:全局对象window
(本来根据隐式绑定的规则,this会绑定在调用函数的对象上,但是也会有失效的情况,称为隐式丢失)
隐式绑定的丢失是因为 函数在调用时会因为 直接赋值间接赋值(传参) 进而影响函数的调用位置 从而改变this的绑定对象
(再说一遍,this在函数上下文时,函数的调用位置特别重要)

代码:

<script>
// 隐式丢失
    function foo1() {
        console.log(this.b);

    }
    var obj1 = {
        b: 2,
        foo1: foo1
    };
    var bar = obj1.foo1; //函数别名
    var b = "I am global variety";
    bar(); //函数的调用位置
</script>

在这里插入图片描述
代码分析: 这里其实也不算是真正意义上的丢失,因为我们说了当This处于函数上下文时,函数的调用位置特别重要。 分析这个例子函数的调用位置发现,直接使用bar()调用,用隐式绑定的规则 this会绑定在调用这个函数的对象中。对,这里调用bar()函数的这个对象是全局对象window。 因为在处于全局环境下,bar() 和window.bar()是等价的,只是说我们在写的时候会将window省略。当this绑定在window上时,通过this.a访问到全局对象 最后输出“I am global variety”。 但是有没有可能这里会发生一个歧义,那obj1.foo的obj1对象呢?? 再声明一遍,函数的真正调用位置!!! 这里真正的调用位置是 bar(), 只是这个函数调用时会通过访问obj1的属性foo1 直接使用foo1的声明块。根据隐式绑定的规则,this是会绑定在调用函数的对象上,而不是说函数上下文涉及的对象上(因为这里有访问对象属性的操作,所以也会有上下文对象)。

当函数调用位置改成下面的形式,this就绑定在了obj1对象身上了。

obj1.foo1()—此时调用函数的对象就是obj1, 通过this.b 访问到obj1属性b 值为2 在这里插入图片描述
还有一种函数传参的情况,也是通过赋值操作影响实际函数的调用位置,这里就不详细举例了。
只要记住 函数的调用位置 真的很重要。

3. 显式绑定

在上面分析隐式绑定时,是通过访问对象的属性 从而(隐式)的绑定在对象上。
现在来介绍可以直接 指定this指向的方法—显示绑定 利用函数内置的方法 call() 和apply()
方法。这样会使得函数在运行 的过程中将this绑定在指定的对象上使得this的绑定不会像隐式绑定那样取决于函数的调用位置

介绍一下call 和 apply方法:
call()和apply() 方法是所有函数内置的函数方法,通过Function.call() Function.apply()
函数也是对象,将两个方法理解成属性就行了。
call() 和apply()通过接收一个对象,将this强制绑定在指定的对象上 并且调用原来的函数。 call和apply底层的实现是通过ES5出现的bind()函数,这里放着官网的介绍。
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/this
ES5 引入了 bind 方法来设置函数的 this 值,而不用考虑函数如何被调用的。bind(…)会返回一个硬编码的新函数,它会把参数设置为this的上下文并调用原始函数。

(参数:当call和apply的第一个参数不是传入的对象,而是具体的值 (字符串类型 布尔类型 数字类型)call(1) apply( “s”)等时,函数的内部就会将其转换为对应的对象形式(new String(…) new Boolean(…)) --这个过程叫做“装箱”)

         代码:
<script>
    // 显式绑定
    function foo() {
        console.log(this.a);
    };

    function foo1() {
        console.log(this.a)
    }
    var a = "Global variety"
    var obj = {
        a: 2,
        foo: foo,
        foo1: foo1
    }
    var obj1 = {
        a: 3
    }
    foo.call(obj); //显式绑定对象obj-----输出2
   
</script>

分析代码:

foo.call(obj) 使得foo函数在运行过程中this绑定在obj上, this.a访问到obj的属性a上。

但是显然我们隐式丢失的问题还是存在的!(下面的硬绑定会解决这个问题)
注意这里不能通过
var bar=foo.call(obj);
bar();
来试图解决隐式绑定丢失的问题哦。 因为这里的foo.call(obj) 除了将this绑定在指定对象obj上,同时也会调用函数 foo()。 所以这里的bar不是在给foo函数取别名 并且试图通过bar()来调用, 而是变成了一个普通的变量,用来接收foo()函数运行返回的值!

硬绑定

上面的隐式绑定的 绑定丢失的问题就可以用硬绑定来解决问题
//硬绑定--直接在函数内部就绑定,而不是在运行时才绑定,这样,无论是以那种形式的调用 this都会绑定到指定的对象----

代码案例:

<script>
    //硬绑定--直接在函数内部就绑定,而不是在运行时才绑定,这样,无论是以那种形式的调用
    //this都会绑定到指定的对象
    function foo() {
        console.log(this.a);
    }
    var obj = {
        a: 2
    }
    var a = "Global Variable"
    //bar函数内部直接用call指定对象
    var bar = function() {
        foo.call(obj);
    }
    bar(); //2
    // 试图通过call(window)来使得在调用过程中,将this绑定在全局对象上
    bar.call(window); //2
</script>

在这里插入图片描述
当使用硬绑定之后,后面就不能手动的更改This绑定的对象

API 的调用上下文

第三方库的许多函数,以及javaScript和宿主环境中的许多内置函数,都提供了可选的参数,通常被称为“上下文”(context),他的作用也和bind(…)一样,确保回调函数使用指定的this

4. new绑定

这是this绑定规则中最后一条。 我们在使用new关键字调用构造函数 用来创建对象时,比如说下面这段代码
function Myclass(){

}
var obj=new Myclass()—创建一个对象

上面的代码实际会发生如下行为:

  1. 创建(构造)一个全新的对象
  2. 这个新的对象会被[[原型]]连接
  3. 这个新对象会被绑定在函数调用时的this
  4. 如果函数没有返回其他对象,那么new表达式中的函数会自动返回这个对象。

以上,this绑定的几条规则已经介绍完了。那么,他们在同时设置的时候,优先级如何呢?

优先级

那么有了这几种规则,在确定了函数的调用位置之后 这个位置的this绑定规则应该需要哪一种呢? 显然,默认绑定是优先级最低的,这里就不考虑了先

1. 显式绑定 优先于 隐式绑定

代码分析:

 <script>
        // 优先级  隐式<显示
        function foo() {
            console.log(this.a);
        }
        var obj1 = {
            a: 2,
            foo: foo
        }
        var obj2 = {
            a: 3,
            foo: foo
        };
        obj1.foo(); //2
        obj2.foo(); //3
        obj1.foo.call(obj2); //3
        obj2.foo.call(obj1); //2
    </script>

代码分析:
这里主要分析 下面两行代码
obj1.foo.call(obj2); //3
obj2.foo.call(obj1); //2
如果隐式绑定优先于显示绑定的话 输出的应该是 2和3,而不是 3和2
显然,在函数的调用位置确定后 显式绑定是优先于隐式绑定的。

2. new绑定 优先与 隐式绑定

代码:

  <script>
      function foo(argument) {
            this.a = argument;
        }
        var obj1 = {
            foo: foo
        }

        obj1.foo(2);
        console.log(obj1.a);
        var bar = new obj1.foo(4);
        console.log(obj1.a); //2
        console.log(bar.a); //4
    </script>

代码分析:
主要看 var bar = new obj1.foo(4);
这里的函数调用位置确定之后,显然new绑定的规则要优先与隐式绑定。
当然 这是因为new直接是创建了一个新的对象,将创建的这个对象绑定在函数调用时的this上。 所以当new关键字出现时。当然是使用new绑定规则啦~

3. new绑定 优先于 显式绑定

当new绑定和显式绑定需要做比较时,new 和apply call不能同时用。 但是可以用硬绑定来比较一下。(硬绑定也是显示绑定的一种)。
代码:

   <script>
 // new与显式绑定
        function foo(something) {
            this.a = something;
        }
        var obj1 = {};
        var bar = foo.bind(obj1);
        bar(2);
        console.log(obj1.a) //2-----this指向obj1
        var baz = new bar(3);
        console.log(obj1.a); //2-----this指向Obj1
        console.log(baz.a); //3-----this指向baz
    </script>

分析代码:
前面分析到硬绑定之后 后面就不能修改this的指向,但是这里失效了

严格来说是因为ES5内置的函数bind() 函数内部的实现机制会有点复杂,这里就不为难自己去深究。 总的来说, bind()函数 内部会判断bind()是否被new调用,如果调用了的话, 就用新的this替换掉bind原先的this。

绑定例外

有规则,就有意外。
下面介绍一下this绑定规则的例外

1. 空绑定

主要的应用场景是利用call()和apply() 用来展开数组的时候,因为第一个参数(需要绑定的目标对象)是必须的,但是我们又不需要绑定对象,只是用来展开数组。所以常常传进去一个空对象 NULL或undefined。但是这里的空对象会绑定失效,会使用默认绑定了。虽然绑定null是一个很好的想法,但是遇到第三方库的时候 可能会导致许多问题。
首先介绍一下 用call apply bind函数如何展开数组。
这里介绍一下bind函数会预先设置一些参数,剩下的参数就会传到下一级的函数中
叫做参数“柯里化”
代码:

<script>
    //绑定例外  展开数组
    function foo(a, b, ) {
        console.log(
            a, b);
    }
    var arr = [1, 2, 3];
    foo.apply(null, arr); // 1 2
    //用bind将参数“柯里化”
    var bar = foo.bind(null, 4);
    bar(5); //4 5
</script>

更安全的绑定:
上面介绍到使用null绑定会发生问题,但是我们有更安全的方法。 就是将null换成一个真正的空对象。

  1. {}
  2. var obj=Object.create(null)
    推荐使用第2种, 因为它比1更“空”。 不会发生prototype这个委托

2. 间接引用造成的绑定丢失

间接引用,就是通过直接赋值 或者间接赋值(也就是传参)影响实际函数的调用位置
进而影响this的绑定。 也就是上面隐式绑定规则的 隐式丢失问题。

3. 软绑定

用法 Function.softBind()。 用法和bind一样,也支持柯里化。但是软绑定可以先捕获函数的this,如果this是绑定在全局对象或undefined上 就把this修改成指定的对象,否则不修改。 这也很好的平衡了硬绑定带来的不灵活性,以及隐式和默认绑定可以修改this的能力。

this的词法作用域

上面说明的this都是通过用4条规则里面的任何一条去将this绑定到对应规则的对象上。 但是有一种例外就是this根本不是绑定在对象上,而是绑定在了函数运行时的词法作用域上。 涉及到ES6 提出的箭头函数

官网解释:
ES2015 引入了箭头函数,箭头函数不提供自身的 this 绑定(this 的值将保持为闭合词法上下文的值)。

代码:

<script>
    // 胖箭头函数 与this 
    function foo() {
        // 返回一个箭头函数
        return (a) => {
            // 当前this处于 foo的词法作用域  而 foo被绑定在了obj1上 被箭头函数的this继承
            console.log(this.a);
        }
    }
    var obj1 = {
        a: 2
    }
    var obj2 = {
        a: 3
    }
    var bar = foo.call(obj1);
    bar.call(obj2); // 2 而不是3
</script>

代码分析, 箭头函数的this完全无视了前面讲到的this的4条规则,bar.call(ob2)也不会影响this。 所以胖箭头函数的this是只关系到箭头函数的此法作用域的上下文,例子中箭头函数的this就是继承自此法作用域上层foo的this。

总结

当以后函数上下文(类上下文也一样)中涉及 this的绑定时, 先去判断函数的调用位置(重要),再去判断使用哪条规则。

  1. 如果有new关键字的调用,则用new绑定规则。 this绑定在新创建的对象上
  2. 如果有显式绑定, function.call(obj) 或apply(obj) bind(obj)时,this会被绑定在指定的对象obj上。
  3. 隐式绑定,注意隐式绑定丢失问题(用硬绑定解决)
  4. 默认绑定,当以上规则都没有用上的时候,就得用默认绑定了。
    严格模式下:undefined
    严格模式下:全局对象window

常见函数上下文this的绑定问题

这里直接引用官网,它的总结还有实例。
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/this

this指向调用函数的对象
this指向构造函数创建的对象
this指向正在使用监听事件的DOM对象
this指向触发事件的DOM对象
等详细内容都有涉及。

最后

文章就分享到这里啦~ 纯属分享笔记和收获,制作不易
欢迎交流~~~~~~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值