javascript中的this绑定规则

389 阅读11分钟

首先恭喜你发现了宝藏🚀🚀🚀,读完本文章大约耗费十分钟,相信你看完后会得到你想要的,如果感觉写的好的话,请给一个宝贵的👍

关于this绑定的问题是我们在面试中常常被问到的问题,这一篇文章带你彻底弄懂!👍

this到底指向什么

首先我们先来看一个令人困惑的问题🤔

定义一个函数,我们采用三种不同的方式对它进行调用,它产生了三种不同的结果

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

  var obj = {
    bar: foo
  }
  obj.bar(); //obj

  obj.bar.apply(123); //Number

我们可以看到同一个函数,用三种不同方式的调用方式,打印出的this的值不同

从这里我们可以得到什么指示呢?🤔

  • 函数在调用的时候,javascript会给它绑定一个特定的值
  • this的绑定和定义的位置(编写的位置)无关
  • this的绑定和调用方式以及调用位置有关
  • this是在运行的时候绑定的

this的绑定规则

this的绑定规则有三种

  • 默认绑定
  • 隐式绑定
  • 显示绑定
  • new绑定

默认绑定

什么是默认绑定?🤨

默认绑定是JavaScript 中的一种函数 this 绑定行为,它适用于函数在全局作用域中调用时(即不通过对象调用),或者是没有明确通过 callapplybind 指定 this 时。

什么情况在使用默认绑定?默认绑定发生在什么时候?😄 我们来看下面的代码

  // 1.1独立函数调用
  function foo() {
    // 这里的this是window
    console.log(this);
  }
  foo();

独立函数调用 中(即 foo()),this 通常会指向 全局对象(在浏览器中是 window,在 Node.js 中是 global)。

  // 1.2函数定义在对象中,但是是独立调用
  var obj = {
    name: 'obj',
    foo: function () {
      // 这里的this是window
      console.log(this);
    }
  }
  var foo = obj.foo;
  foo();
  • 虽然 foo 最初是 obj 的一个方法,但将方法提取出来并独立调用时,this 的指向仍然是全局对象 window(而不是 obj)。

  • 只是定义在对象中的方法,如果被独立调用(即从对象中提取出来并直接调用),会丢失对象上下文,this 不再指向对象,丢失了对 obj 的引用。

这里我们可以再次加强理解this 是在调用时绑定的,而不是在定义时绑定⚠️!!。

  // 1.3高阶函数
  function foo1(fn) {
    // 这里的this是window
    fn();
  }
  foo1(obj.foo);

  • 高阶函数 中(foo1 调用 fn),当 obj.foo 作为参数传递给 foo1 并在 foo1 中执行时,fn() 仍然是一个独立调用,导致 this 指向 window(而不是 obj)。

  • 高阶函数在传递方法作为参数时,也会导致 this 指向全局对象,而不是原来定义该方法的对象。

  • 即使 obj.foo 原本是 obj 的方法,传递到 foo1 函数并作为回调执行时,this 不会指向 obj,而是全局对象(除非明确绑定 this也就是显示绑定

从这里我们可以得到什么启示呢?🤨

默认绑定 是 JavaScript 中关于 this 的一种绑定规则,它发生在独立函数调用时。具体来说,当函数没有明确指定 this 的值(例如,没有使用 call()apply()bind() 方法),并且该函数是作为一个普通函数被调用时,this 将根据 调用环境 来决定。

总结

何时发生默认绑定?

默认绑定发生在以下情况下:

  • 函数被直接调用(即函数名后加 ())。
  • 独立函数调用的时候,this 会丢失上下文对象,发生默认绑定。
  • 函数没有明确的上下文(比如 call()apply()bind())或不是作为对象的方法调用。

注意,这是在非严格模式下,如果在严格模式下,this指向的是undefined!!⚠️

严格模式是什么?就是在你的js代码上加"use strict"这一句话😄

隐式绑定

隐式绑定是什么?

隐式绑定指的是当一个函数作为对象的方法被调用时,this 会绑定到该对象。换句话说,this 的值取决于函数调用的上下文,尤其是函数是如何被调用的。如果一个方法是作为对象的属性被调用,this 就会指向那个对象。

我们看下面一段代码

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

调用obj.bar()会执行foo()函数。在 JavaScript 中,函数内部的 this 是根据调用该函数的上下文来决定的。当我们调用 obj.bar() 时,bar 方法是 foo 的引用,因此 foo 被调用时,this 会指向 obj,因为 foo 是作为 obj 的方法被调用的。

 function foo() {
    console.log(this);
  }
  var obj1 = {
    a: 1,
    foo: foo
  }
  var obj2 = {
    a: 2,
    obj1: obj1
  }
  obj2.obj1.foo(); //{ a: 1, foo: [Function: foo] }

这里,obj2.obj1obj1 对象,fooobj1 中的一个方法。所以,foo() 函数的调用实际上是通过 obj1 来触发的,尽管 obj2 包含 obj1,但调用 obj2.obj1.foo() 时,this 并不会指向 obj2,而是指向 obj1

这里给我们什么启示呢?🤨

  • 方法调用时,this 会指向调用该方法的对象。
  • 嵌套对象的影响:嵌套的对象并不影响 this 的绑定,因为 this 总是会绑定到方法的实际调用对象。
  • 隐式绑定只会在函数被对象方法调用时生效。
  • this 的绑定是由调用方式决定的。

总结

什么时候发生隐式绑定?

  • 当函数作为对象的方法被调用时:
  • 函数调用的上下文决定了 this 的绑定
  • 当对象方法内部调用另一个对象方法时

何时不发生隐式绑定

  • 方法丢失上下文(丢失 this),比如将一个对象的方法赋值给一个变量或者传递给其他函数
  • 函数调用时不在对象上下文中,比如独立调用

注意隐式绑定有一个前提条件⚠️!!

  • 必须在调用的对象内部有一个对函数的引用(比如一个属性);

  • 如果没有这样的引用,在进行调用时,会报找不到该函数的错误;

  • 正是通过这个引用,间接的将this绑定到了这个对象上;

显示绑定

在介绍显示绑定的时候,我们先思考一个问题🤔

   var obj = {
     name: '张三',
   }
   function fn() {
     console.log(this);
   }

我们如何让这串代码中的this绑定到obj上呢?🤨 根据我们之前学过的,这里可以隐式绑定!😄

var obj = {
    name: '张三',
  }
  function fn() {
    console.log(this);
  }
  // 执行函数,并且将函数中this指向obj
  // 1,.通过隐式调用函数,将函数内部的this指向obj
  obj.fn = fn
  obj.fn()

如果我们不希望在对象内部包含这个函数的引用,同时又希望在这个对象上进行强制调用,该怎么做呢?🤨

这里就要隆重请出我们的显示绑定了👏👏

什么是显示绑定?🤨

显示绑定是指在 JavaScript 中,使用 callapplybind 方法显式地指定函数调用时的 this 值。通过这些方法,我们可以强制 this 指向某个特定的对象,而不依赖默认绑定或隐式绑定的规则。

这里就要使用显示绑定 显示绑定有三种方法

  • call
  • apply
  • bind

call和apply

function.apply(thisArg,[argsArray]
function.call(thisArg,argl,arg2,...)

第一个参数是相同的,要求传入一个对象

  • 这个对象的作用是什么呢?就是给this准备的。
  • 在调用这个函数时,会将this绑定到这个传入的对象上。

后面的参数,apply为数组,call为参数列表;

我们来看例子

function fn(a, b) {
    console.log(this, a, b);
  }
fn.call('abc', 1, 2); //1.String 1 2
function fn(a, b) {
    console.log(this, a, b);
  }
fn.apply('abc', [1, 2]);//1.String 1 2

'abc' 作为 this 的值(即 this = 'abc'),12 作为函数参数 ab

注意,打印出来的是string,这是为什么?🤔

由于 this'abc',它会在打印时被转化为原始的字符串值'abc' 是一个原始字符串类型,它不是一个对象,所以可以直接当作原始值显示

bind

  function greet(name) {
    console.log(`Hello, ${name}! I am ${this.name}.`);
  }

  const person = { name: 'Alice' };

  // 使用 bind 来绑定 this 为 person 对象
  const greetPerson = greet.bind(person);

  // 调用新函数 greetPerson,this 会始终指向 person 对象
  greetPerson('Bob'); // 输出: "Hello, Bob! I am Alice."

greet.bind(person) 创建了一个新函数 greetPerson,它的 this 被永久绑定到 person 对象。这样,即使我们在 greetPerson 中传入不同的参数(如 'Bob'),this 仍然会指向 person

new绑定

JavaScript中的函数可以当做一个类的构造函数来使用,也就是使用new关键字。 使用new关键字来调用函数时,会执行如下的操作:

  • 1.创建一个全新的对象;

  • 2.这个新对象会被执行prototype连接,将this指向这个空对象

  • 3.执行函数体中的代码,这个新对象会绑定到函数调用的this上(this的绑定在这个步骤完成);

  • 4.如果函数没有返回其他对象,表达式会返回这个新对象;

  function Person(name) {
    console.log(this); // Person {}
    this.name = name; // Person {name: "张三"}
  }
  var p1 = new Person('张三');
  console.log(p1);

使用 new Person('张三') 创建了一个新的 Person 对象 p1,并自动将 this 绑定到这个新对象上。

总结

  • call,bind,apply,new绑定都能用于显示绑定this,将this设置为你指定的值.

this绑定规则优先级

上面提到了this的四种绑定规则,如果在函数调用时,使用到了多种绑定规则,最终函数的this指向什么呢?那么这里就涉及到this绑定的优先级,优先级高的规则决定this的最终绑定。

this四种绑定规则优先级如下:

  • 默认绑定优先级最低:函数调用存在其它规则时,就会遵循其它规则来绑定其this;

  • 显示绑定优先级高于隐式绑定

    const obj1 = {
      name: 'obj1',
      foo: function() {
        console.log(this)
      }
    }
    const obj2 = {
      name: 'obj2'
    }
    
    obj1.foo.call(obj2) // obj2对象
    obj1.foo.apply(obj2) // obj2对象
    
    const newFoo = obj1.foo.bind(obj2)
    newFoo() // obj2对象
    
  • 显示绑定中bind高于call和apply

    function foo() {
      console.log(this)
    }
    
    const obj1 = {
      name: 'curry',
      age: 30
    }
    const obj2 = {
      name: 'kobe',
      age: 24
    }
    
    const newFoo = foo.bind(obj1)
    newFoo() // obj1对象
    newFoo.call(obj2) // obj1对象
    newFoo.apply(obj2) // obj1对象
    
  • new绑定优先级高于隐式绑定和显示绑定

    // 1.new绑定高于隐式绑定
    const obj1 = {
      foo: function() {
        console.log(this)
      }
    }
    
    const f = new obj1.foo() // foo函数对象
    
    // 2.new绑定高于显示绑定
    function foo(name) {
      console.log(this)
    }
    
    const obj2 = {
      name: 'obj2'
    }
    
    const newFoo = foo.bind(obj2)
    const nf = new newFoo(123) // foo函数对象
    

总结

  • new绑定 > 显示绑定(apply/call/bind) > 隐式绑定 > 默认绑定
  • 注意new关键字不能和apply、call一起使用,所以不太好进行比较,默认为new绑定是优先级最高的;

4.特殊情况下的this绑定

在特殊情况下,this的绑定不一定满足上面的绑定规则,主要有以下特殊情况:

  • 显示绑定的忽略:在显示绑定中,如果给call、apply和bind第一个参数传入null或者undefined,那么这样的显示绑定会被忽略,最终使用默认绑定,也就是全局的window;

    function foo() {
      console.log(this)
    }
    
    foo.call(null) // window
    foo.call(undefined) // window
    
    foo.apply(null) // window
    foo.apply(undefined) // window
    
    const newFoo1 = foo.bind(null)
    const newFoo2 = foo.bind(undefined)
    newFoo1() // window
    newFoo2() // window
    
  • 间接函数的引用:创建一个函数的间接引用,该情况也使用默认绑定。如下代码中给obj2创建一个bar属性并赋值为obj1中foo函数时直接进行调用;

    const obj1 = {
      name: 'obj1',
      foo: function() {
        console.log(this)
      }
    }
    const obj2 = {
      name: 'obj2'
    }
    
    // 被认为是独立函数调用
    ;(obj2.bar = obj1.foo)() // window
    
  • ES6中的箭头函数:箭头函数不会使用上面的四种绑定规则,也就是说不绑定this,箭头函数的this是根据它外层作用域中的this绑定来决定的;

    • 数组内置方法中回调使用箭头函数:

      const names = ['curry', 'kobe', 'klay']
      const obj = {
        name: 'obj'
      }
      
      names.map(() => {
        console.log(this) // window
      }, obj)
      
    • 多层对象中的方法属性使用箭头函数:

      const obj1 = {
        name: 'obj1',
        obj2: {
          name: 'obj2',
          foo: () => {
            console.log(this)
          }
        }
      }
      
      obj1.obj2.foo() // window
      
    • 函数的返回值为箭头函数:

      function foo() {
        return () => {
          console.log(this)
        }
      }
      
      const obj = {
        name: 'curry',
        age: 30
      }
      
      const bar = foo.call()
      bar() // obj对象
      

5.node环境下全局this的指向

以上提到的内容都是在浏览器环境中进行测试的,在浏览器环境下的this是指向全局window的,那么在node环境中全局this指向什么呢?

  • 在node环境下打印一下全局this:

  • 为什么全局this打印为一个空对象?

    • 当我们js文件被node执行时,该js文件会被视为一个模块;

    • node加载编译这个模块,将模块中的所有代码放入到一个函数中;

    • 然后会执行这个函数,在执行这个函数时会通过apply绑定一个this,而绑定的this就为{}

  • 那为什么node中还是可以使用想window中的全局方法呢?像setTimeout、setInterval等。

    • 因为node环境中也是存在全局对象的,通过打印globalThis就可以进行查看;