this ,this , 你到底指向谁 ??? 我受不了了 😭

145 阅读7分钟

前言

this 到底指向谁 , 看完本文就好受了 !

请开始你的表演.gif

注意


本文样例主要在 node.js 环境中测试 , 但由于以下特性会导致结果不同 , 倔友请先食用。

  • 在浏览器全局环境中,使用 var 声明的变量会挂载在 window 上 , 成为 window 的属性,但 letconst 声明的变量,不会挂载在 window 上 。
  • 但是 node.js 全局环境中 , var 声明的变量不会挂载在 global 上 。

所以当 this 指向全局对象 window 和 global 时 , 可能会导致代码结果不一样 , 所以啊 , 面试官给你 js 代码的时候 , 要问清楚是在什么环境下执行 , 尤其是涉及到 var 定义的变量的时候 !!!

比如以下例子 :

  1. 浏览器中
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
  </head>
  <body>
    <script>
      var a = 111
      let b = 222
      const c = 333
      console.log(window.a) // 111 
      console.log(window.b) // undefined let、const 声明变量没有挂载在 window 上
      console.log(window.c) // undefined const 声明变量没有挂载在 window 上
    </script>
  </body>
</html>

  1. node中
var a = 111
let b = 222
const c = 333
console.log(global.a) // undefined 
console.log(global.b) // undefined let、const 声明变量没有挂载在 window 上
console.log(global.c) // undefined

所以后面 , 我们分析完 this 指向 , 讨论代码的结果的时候 , 会分为两种环境来讨论 !

this 指向看什么 ?

this 的指向是动态的 , 在函数定义的时候不能确定指向谁 , 在调用的时候才能确定 this 的指向 , 而在调用的时候主要看调用的位置 , 以下对调用的位置讨论 。

调用位置是函数在代码中调用的位置 , 而不是声明的位置。

调用位置分为以下几种情况:

  • 普通函数调用:在全局环境中调用函数
  • 对象方法调用:通过对象的方法调用
  • 构造器调用:使用 new 运算符实例化调用
  • 显式调用:通过 callapplybind 调用,修正 this 指向

注意


箭头函数的 this 指向是固定的,‌在函数定义时就确定了,之后不会再改变。

箭头函数没有自己的 this 值,它会捕获上下文中的 this 作为自己的 this。无论箭头函数是如何调用的,它的 this 始终保持不变。

箭头函数在处理回调函数时特别有用,因为它避免了常见的 this 绑定问题。

function Timer() {
  this.seconds = 0;

  setInterval(() => {
    this.seconds++;
    console.log(this.seconds); // 这里的 `this` 始终指向 Timer 的实例
  }, 1000);
}

const timer = new Timer();

普通函数与箭头函数的区别可以参考这篇文章 : juejin.cn/post/743070…

普通函数调用

普通函数调用, this 指向全局对象。

  • 在浏览器 JS 引擎中this指向 window
  • Nodejs 环境 this 指向 global
function f1(){
  return this;
}

// 在浏览器中,全局对象是 window
//console(f1() === window)   // true 

//在Node中,全局对象是 global
console.log(f1() === global) // true

以上是在 nodeJs 的环境中测试

注意

  • 在严格模式下,this 指向 undefined
function f1(){
  // 严格模式下,全局对象是 undefined
  'use strict'
  return this;
}
console.log(undefined===f1())

node

浏览器

对象方法调用

当函数作为对象的方法被调用时, this 指向该对象:

var obj =  {
    name: 'John',
    getName: function () {
      console.log(this === obj)
      console.log(this.name === 'John')

    //   console.log(this)
    //   console.log(this.name)
      return this.name
    }
  }
  
obj.getName();

node

浏览器

注意

使用对象方法调用时,this 有可能会丢失,看下面这段代码

var name = '李四'
var obj = {
  name: '张三',
  getName: function () {
    function fn () {
        console.log(this === global)
        return this.name
    }
    return fn()
  }
}
console.log(obj.getName()) 

node

因为在 getName 函数内部调用 fn, 此时 fn 函数执行上下文this不是指向调用的对象 obj,而是指向 global , 由于 var 的变量不会挂载在为 global的属性 , 所以输出 undefined

浏览器

将代码中的 global改为 window , 输出结果如下 :

与 node 环境原理一致 , 不是指向调用的对象 obj,而是指向 window , 但是 var 声明的 name ,挂载为 window 的属性 , 所以 this.name 就相当于 window.name , 所以输出的是 李四

构造器调用

构造器表面和普通函数一模一样,不同的地方在于被调用的方式。

使用 new 运算符调用函数时,该函数总会返回一个对象,通常情况下,构造器里的 this 就指向这个对象

举个栗子 :

var Name = function () {
  this.name = 'ganzhibin'
}
var obj = new Name()
console.log(obj.name) // ganzhibin

使用 new 运算符创建 Name 构造器,此时this 指向 obj

node

浏览器

注意

使用 new 调用构造器时,还要注意一个问题,如果构造器显式返回一个 object 类型的对象,那么此次运行结果最终是返回这个对象,而不是我们之前期待的 this:

var Name = function () {
  this.name = 'ganzhibin'
  return { // 显示返回一个对象
    name: 'this is myName'
  }
}
var obj = new Name()
// 输出 this is myName,而不是上面的 ganzhibin
console.log(obj.name) // this is myName

如果构造器不显式返回任何数据,或者返回一个非对象类型的数据,就不会存在上面这个问题

var Name = function () {
    this.name = 'ganzhibin'
  }
  var obj = new Name()
  console.log(obj.name) // ganzhibin

node 和 浏览器 结果一致

显式调用

call、apply 修正 this 指向

callapply 调用函数和其他函数调用相比,它会显式改变传入函数的 this, 指向第一个传入的参数。callapply 两者实现功能相同, 不同的地方在于接收参数形式不一样

  • call 接收的是参数列表
  • apply 接收的是一个数组
var obj1 =  {
    name: 'name1',
    getName: function () {
      return this.name
    }
  }
  
  var obj2 = {
    name: 'name2'
  }
  
  // 对象方法调用,所以 this 指向 obj1
  console.log(obj1.getName()) 
  
  // 使用 call 显示调用,改变了原来 this 指向,指向了 obj2
  console.log(obj1.getName.call(obj2))

node

浏览器

注意

callapply 第一个参数除了可以是对象引用类型,也可以是基本类型

  • nullundefinedthis 指向 window;不过,在严格模式下,this 还是指向 undefined
  • numberstringbooleanthis 会指向其内置构造函数 NumberStringBoolean
var name = 'globalName'
var obj =  {
    name: 'obj name',
    getName: function () {
        // 'use strict'  // 严格模式下,this 指向 undefined, null 会报错
        return this.name
    },
    getThis: function () {
        return this
    }
}

obj.getName.call(null) // globalName
obj.getName.apply(undefined) // globalName

// number boolean string
obj.getThis.call(111) // Number {111}
obj.getThis.call(true) // Boolean {true}
obj.getThis.call('str') // String {"str"}

这里注意区分浏览器环境和 node 环境

node

浏览器

bind 修正 this 指向

bindcallapply 不同的地方在于

  • 改变this指向 bind()的参数
  • 同时会返回一个新的函数 , 比如下面的 g() , h()
  • bind只生效一次
function f(){
  return this.a;
}

var g = f.bind({a:"azerty"});
console.log(g()); // azerty

var h = g.bind({a:'yoo'}); // bind只生效一次!
console.log(h()); // azerty

var o = {a:37, f:f, g:g, h:h};
console.log(o.f(), o.g(), o.h()); // 37(对象方法调用,指向o), azerty, azerty

bind 绑定改变 this 只能生效一次,如果链式发生多次绑定以第一次为准 !

node

浏览器

因为这里的 this 没有指向全局对象 window 或者 global , 所以结果一般是一致的 !

事件处理器

在为 DOM 元素添加事件监听器时,通常事件处理函数中的 this 会指向触发事件的 DOM 元素,而不是定义函数的对象。

Javascript
 代码解读
复制代码
const button = document.querySelector('button');
button.addEventListener('click', function() {
    console.log(this); // 指向 <button> 元素
});

总结

  1. 当 this 指向全局对象window 或者 global ,且 this 选取的是 var 声明的变量时 , 代码输出的结果 可能不一致 。

  2. 箭头函数的 this , 固定的,‌在函数定义时就确定了,之后不会再改变 。

  3. 除了箭头函数 , this 的指向取决于函数调用的位置 , 其中调用的位置有

    • 普通函数调用:在全局环境中调用函数
    • 对象方法调用:通过对象的方法调用
    • 构造器调用:使用 new 运算符实例化调用
    • 显式调用:通过 callapplybind 调用,修正 this 指向
    • 事件处理器

你还行吗 ? 👀

补充栗子


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script>
        // var name = 'windowName'
        // var a = {
        //     name : 'lisi',
        //     fn1 : function(){
        //         console.log(this.name)
        //     },
        //     fn2 : function(){
        //         let _this = this ; // this指向
        //         setTimeout(function(){
        //             _this.fn1() //
        //         },100)
        //     }
        // }

        // var name = 'windowName'
        // var a = {
        //     name : 'lisi',
        //     fn1 : function(){
        //         console.log(this.name)
        //     },
        //     fn2 : function(){
        //         setTimeout(() =>{
        //             this.fn1() // 指向词法环境中的this
        //         },100)
        //     }
        // }



        var name = 'windowName'
        var a = {
            name : 'lisi',
            fn1 : function(){
                console.log(this.name)
            },
            fn2 : function(){
                setTimeout((() =>{
                    this.fn1() // 指向词法环境中的this
                }).call(a),100)
            }
        }
       

        a.fn2()
    </script>
</body>
</html>

image.png

上面三种方法实现的效果一样 , 都是指向a对象 .