一文搞懂 this指向

450 阅读8分钟

this关键字是JS中非常重要的一部分,也是JS编程基础中的基础。很多人去学习它的时候,往往会独立的去探寻this到底指向什么?但我认为这样的理解是孤立和断层的,我们不会是因为用this而用this。this是因为可以方便的解决问题而出现。同时,它的出现又能在其他方面延伸出一些“特殊的应用”。

我对于this的学习也是一个探索的过程,有些地方说的未必准确,如果大佬们发现有不对的地方,欢迎在评论区积极提出来~

this的出现有什么意义?

看下面的代码不难发现,this帮助我们引用了合适的上下文对象。

    let person = {
      name: "meng",
    };

    function read(...params) {
      console.log(`${this.name} is reading`, params); // meng is reading (2) ["参数1", "参数2"]
    }

    read.call(person, "参数1", "参数2");

如果没有this,那这坨代码的风格就变了。看下面的代码:

    let person = {
      name: "meng",
    };

    function read(person, ...params) {
      console.log(`${person.name} is reading`, params); // meng is reading (2) ["参数1", "参数2"]
    }

    read(person, "参数1", "参数2");

1. 上下文对象和参数混在了一起

我们无法第一眼看出来person的作用,也许他是函数的上下文对象,或许他只是一个普通参数,代码可读性不高

2. 可维护性差

如果我们的对象名变更了,不叫person而是叫student,一下子就要改很多地方。这只是短短几行代码,如果现在有500多行代码,肯定蒙圈了。

所以this在我们开发过程中是十分重要的,但是在使用this的过程中,我相信每个人在小白时期都会有不解的时候。

  • 明明在对象中写了变量,为什么会this.xx = undefined
  • 看到同事经常用 let that = this; that.xxx();,为什么要这样写?

下面来看下,this到底指向什么?

this的四种绑定规则

学习this首先要明白,this是函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。所以它与this所在的作用域无关。

简单点说,就是和调用的地方有关系,和定义的地方没关系。

4种绑定规则

  1. 默认绑定
  2. 隐式绑定
  3. 显式绑定
  4. new绑定

优先级是:new绑定 > 显式绑定 > 隐式绑定 > 默认绑定

注:箭头函数比较特殊,不受限于这4种规则,取决于this的作用域

默认绑定

不带任何修饰的函数直接调用,就是默认绑定。默认绑定的this绑定到window

  var a = 2;
  function foo() {
    console.log(this.a);
  }
  // 直接调用(默认绑定)
  foo(); // 2

严格模式下this是undefined

this的绑定规则取决于调用位置,这句话的前提是foo()运行在非严格模式下,也就是说非严格模式下默认绑定能绑定到全局对象。严格模式下不可以绑定this,this就是undefined

  "use strict";
  var a = 2;
  function foo() {
    console.log(this.a); // 报错
  }
  foo(); 
  
  
  var a = 2;
  function foo() {
    "use strict";
    console.log(this.a); // 报错
  }
  foo();

但需要注意的是,严格模式指的是函数体是否处于严格模式,而不是调用位置是否处于严格模式。如果函数体处于严格模式,this会被绑定为undefined,否则this会被绑定到全局对象。

  var a = 2;
  function foo() {
    console.log(this.a); // 2
  }

  (function () {
    "use strict";
    foo();
  })();

隐式绑定

隐式绑定是通过对象属性调用函数,并且绑定的是最后一层调用,看下面这个例子。

obj1.obj2.foo();  // foo中的this绑定的就是obj2

隐式丢失

this绑定时,被隐式绑定的函数有时会丢失绑定对象,这时候会应用默认绑定,把this绑定在全局对象或者undefined上(取决于是否为严格模式)。

以下这几种情况会引起隐式丢失:

  • 函数别名
  • 传入回调函数

函数别名

函数别名就是用一个变量名去接收一个引用,如下。

  function foo() {
    console.log(this.a); 
  }

  var obj = {
    a: 2,
    foo,
  };
  var bar = obj.foo; 
  bar(); // undefined

在上面代码中,bar是obj.foo的一个引用,但它引用的是foo本身,因此bar是默认绑定。

传入回调函数(自定义函数)

用回调函数作为参数,回调函数本身会隐式丢失,如下

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

传入回调函数(内置函数)

以上情况是把参数传到自己定义的函数中,如果把函数传到内置函数中,同样会隐式丢失,这是我们日常开发中十分常见的一种形式。

  function foo() {
    console.log(this.a);
  }

  var obj = {
    a: 2,
    foo,
  };
  var a = "global";

  setTimeout(obj.foo); // global

  setTimeout(function () {
    console.log(this); // window
  });

  // setTimeout 相当于一个普通函数
  function setTimeout(fn) {
    fn();
  }

显式绑定(硬绑定)

apply、call、bind

显式绑定采用的是call,apply,强行定义函数中的this。call和apply二者的区别仅在于传参的形式不同。

    function foo(...params) {
      console.log(this, params); // obj  ["参数1", "参数2"]
    }
    let obj = {
      a: 2,
    };
    foo.call(obj, "参数1", "参数2");
    foo.apply(obj, ["参数1", "参数2"]);
    
    let a = foo.bind(obj);
    a("参数1", "参数2");

this为null

apply,call绑定还有一种特殊情况,就是this是一个null,那么会应用默认绑定。

      function foo() {
        console.log(this.a);
      }
      var a = 2;
      foo.apply(null); // 2

在实现函数柯理化的时候,会用到xx.apply(null)的形式来接收参数,具体实现函数柯理化的过程在文末有写。

注意事项:

  • 这个操作会在window上挂载一个a属性,但这种绑定形式会修改全局对象,从而造成一些难以分析的bug。
  • 可使用 Object.create 来创建一个空对象,这样既不影响应用,又不会污染全局变量
      let _null = Object.create(null);

      function foo() {
        console.log(this.a);
      }
      var a = 2;
      foo.apply(_null); // undefined

可选参数作为上下文

很多内置函数也给我们提供了一个可选参数作为上下文,比如forEach,这也是显式绑定的一种形式。

    let obj = {
      a: 2,
    };
    ["参数1", "参数2"].forEach(function (item) {
      console.log(item, this.a); // 参数1 2
    }, obj);
    
    注意:当我们使用箭头函数时,这种显示绑定就不生效了
    
    ["参数1", "参数2"].forEach((item) => {
      console.log(item, this.a); // 参数1 undefined
    }, obj);

硬绑定

显式绑定也称为硬绑定,因为一旦显式绑定后,无法第二次再通过其它绑定来修改this的指向(new绑定可修改,但这属于特殊情况)。后面的要说的软绑定解决了这个问题。

      var a = 3;
      function foo() {
        console.log(this.a);
        return this.a;
      }
      let obj = {
        a: 2,
      };
      let bar = function () {
        foo.call(obj);
      };

      bar(); // 2
      bar.call(window); //2,this没有被修改为window

new绑定

new绑定是构造出一个新对象,把新对象绑定在this上。任何函数都可以通过new来调用,new的这种调用形式被称为构造函数调用。

    function foo(a) {
      this.a = a;
    }
    // 把新对象绑定在this上
    let bar = new foo(2);
    console.log(bar.a); //2

再来看一个复杂点的例子

     function foo(something) {
        this.a = something;
      }
      let obj1 = {
        foo,
      };

      let obj2 = {};

      obj1.foo(2);

      let bar = new obj1.foo(4);
      console.log(obj1.a); // 2
      console.log(bar.a);  //4

总结一下:使用new来调用函数时,会执行下面的操作

  1. 创建一个全新的对象
  2. 新对象执行prototype连接
  3. 这个新对象会绑定到函数调用的this
  4. 如果函数没有返回其他对象,那么new表达式中的函数会自动返回新对象

对于第四个流程中的,函数返回新对象,我们可以测试下

    // 函数没有返回其他对象,new表达式返回新对象
    function foo(a) {
      this.a = a;
    }

    let bar = new foo(2);
    console.log(bar); // foo {a: 2}
    
    // 函数返回了其他对象,new表达式返回函数返回的对象
    function foo(a) {
      this.a = a;
      return {
        b: this.a,
        c: 1,
      };
    }

    let bar = new foo(2);
    console.log(bar); // {b: 2, c: 1}
    
    // 函数返回的不是一个对象的时候,new表达式会忽略这个返回,返回的还是默认的对象
    function foo(a) {
      this.a = a;
      return "测试";
    }

    let bar = new foo(2);
    console.log(bar); // foo {a: 2}

new绑定修改了硬绑定的this

在下面的代码中,通过显式绑定,将this指向obj1,obj1.a = 3。 new绑定通过创建一个新对象的方式,修改了this的指向,使baz.a = 3,所以new是唯一可以修改硬绑定的方式。

      function foo(something) {
        this.a = something;
      }
      var obj1 = {};

      let bar = foo.bind(obj1);
      bar(2);
      console.log(obj1.a); // 2

      var baz = new bar(3);
      console.log(obj1.a); // 2
      console.log(baz.a);  // 3

在new绑定中使用硬绑定,方便了参数的传递,也是柯理化的一种形式。

      function foo(p1, p2) {
        this.val = p1 + p2;
      }

      let bar = foo.bind(null, "p1");
      var baz = new bar("p2");

      console.log(baz.val); // p1p2

特殊的箭头函数

四条绑定规则可以适用于所有函数,但箭头函数除外。箭头并不适用于上面的规则,箭头函数中this的指向,根据它所处的作用域来决定。

      function foo() {
        return (a) => {
          console.log(this.a);
        };
      }

      let obj1 = { a: 2 };
      let obj2 = { a: 3 };

      let bar = foo.call(obj1);
      bar.call(obj2); // 2

显式绑定bar.call不能改变箭头函数的指向,箭头函数中的this继承自它的作用域,foo中的this指代的是obj1。

箭头函数常用于回调函数中,比如定时器中。因为这种情况会出现隐式丢失的情况,使用箭头函数可以绑定外层的this。

      function foo() {
        setTimeout(() => {
          // 这里的this,指带的是foo
          console.log(this.a);
        });
      }

      let obj = { a: 2 };
      foo.call(obj); // 2

练习题

前面我们学习完了各种形式,现在可以做几道题检验一下。

题1

  function foo() {
    var a = 2;
    this.bar();
  }
  function bar() {
    console.log(this.a);
  }
  foo();

答案是undefined,foo是一个默认绑定。

题2

  function foo(num) {
    this.count++;
  }
  foo.count = 0;
  for (let i = 0; i < 5; ++i) {
    foo(i);
  }
  console.log(foo.count);

答案是0,调用foo的时候是默认绑定,默认绑定this绑定到了window上,相当于在全局创建了一个count。但window.count是undefined,undefined + 数字 = NaN

window.count; // NaN

题3

  let count = 0;
  window.count =0;
  function foo(num) {
    this.count++;
  }
  foo.count = 0;
  for (let i = 0; i < 5; ++i) {
    foo(i);
  }
  console.log(count);
  console.log(window.count); 

答案是0和5。

其他

非严格模式下,var声明的全局变量都会成为window的属性,而let,const 声明的变量不会绑定给window对象。

我自己平时也会刷一些题目来巩固自己的理解,这里推荐一篇文章,他总结的题还是挺全面的。

juejin.cn/post/684490…

手撕代码

本文涉及到了函数柯理化,以及apply,call,bind,顺道我们就看下他们具体的内部实现是什么样的

函数柯理化

函数柯理化就是将接收多个参数的函数,变为可以接收单个参数的函数。

add(1, 2, 3, 4);
柯理化后
add(1)(2)(3)(4)

实现的思路:

  • 比较传入参数的个数,是否等于函数的参数个数
  • 如果相等则返回计算结果,如果不等则继续返回接受参数后的新函数
      let _null = Object.create(null);

      const curry = (fn, ...args) =>
        fn.length > args.length
          ? (...params) => curry(fn, ...args, ...params)
          : fn.apply(_null, args);

      let addSum = (a, b, c, d) => a + b + c + d;
      let add = curry(addSum);
      console.log(add(1)(2)(3)(4));
      console.log(add(1, 2, 3, 4));
      console.log(add(1, 2, 3, 4, 5));

结尾

以上就是所有东西啦,本文是读了《你不知道的javascript》写出的文章,理解后做了自己的总结,推荐大家可以自己去看下这本书。有些东西理解的还比较浅显,或者不够全面。欢迎大家在评论区提出宝贵意见。

下期再见。