JavaScript | 揭秘JavaScript中this的变化规律

在之前的文章中,我们已经了解了JavaScript中的callapplybind这些方法(JavaScript | apply、bind与call),它们都与this有着密切的关系,而今天,我们将继续探讨this指向的问题。

this指的是什么?

JavaScript中,this指向的是当前执行代码的环境对象。

在MDN上给出的this描述是这样的:

this 的值取决于它出现的this:函数、类或全局。------this - JavaScript | MDN

那么针对这些不同的场景,相应的会出现不同的this指向,而对于这些不同的this指向会有些对应的规律,那么接下来我们将针对不同的场景下的this指向来好好的理解并解决一些常见的面试题。

⚡注1:本文是主要讨论的是对于浏览器环境下this的指向的变化

❕注2:如果文章有误请在评论区指出,我会核实并修改

在不同情况下this的指向

  • this的指向是当我们调用函数的时候确定的
  • 调用方式的不同决定了this的指向不同

一、通过对象打点方式调用的函数

  • 对象打点调用它的方法函数,则函数的this为这个打点的对象
javascript 复制代码
let obj = {
  a: 1,
  b: 2,
  fn: function() {
    return this
  }
}

console.log(obj.fn() === obj) // true

练习1:

下面这题的输出是?

javascript 复制代码
let getObj = function(){
  var a = 1;
  var b = 2;
  return {
    a: 22,
    b: 33,
    fn: function() {
      return this.a + this.b
    }
  }
}

console.log(getObj().fn())

答案:55

解析:

  1. 调用了getObj()这个函数,这个函数返回了一个对象
  2. 通过1中返回的对象调用该对象的fn属性,该fn属性指向一个匿名函数
  3. 此时是通过对象调用方法,满足了规则:对象打点调用它的方法函数,则函数的this为这个打点的对象,所以此时fn属性中的函数的this指向为调用者,也就是对象,所以本题的答案为22 + 33 = 55

练习2:

下面这题的输出是?

javascript 复制代码
var boss1 = {

  name: 'boss1',

  returnThis () {

    return this

  }

}

var boss2 = {

  name: 'boss2',

  returnThis () {

    return boss1.returnThis()

  }

}

console.log(boss1.returnThis())

console.log(boss2.returnThis())

答案:

两次的输出都为boss1

解析:

  1. 第一次输出:调用boss1.returnThis(),因为boss1为对象,直接调用自身的returnThis方法返回boss1自身,所以输出的thisboss1
  2. 第二次输出:调用boss2.returnThis(),在boss2对象的returnThis函数中返回了boss1returnThis()的调用,所以这里的返回结果变成了boss1调用returnThis()这个函数,根据规则:对象打点调用它的方法函数,则函数的this为这个打点的对象,所以this指向为boss1

二、通过圆括号直接调用的函数

  • 通过圆括号直接调用的函数,函数的this会指向window对象
javascript 复制代码
function showThis () {

  console.log(this)

}

showThis() // window
  • 实际上上面的代码我们可以认为是window.showThis()的简写

练习1:

下面这段代码的输出是?

javascript 复制代码
var boss1 = {

  name: 'boss1',

  returnThis () {

    return this

  }

}


var boss3 = {

  name: 'boss3',

  returnThis () {

    var returnThis = boss1.returnThis

    return returnThis()

  }

}


console.log(boss3.returnThis())

答案:window

解析:

  1. 首先在调用boss3.returnThis()这个方法的时候,函数体内会定义一个变量即returnThis变量
  2. returnThis变量的值为boss1这个对象的returnThis属性,而boss1returnThis这个属性指向一个函数,等于说这里把这个函数赋值给了定义的returnThis变量
  3. 然后返回这个函数的调用,而此时满足了规则2:通过圆括号直接调用的函数,函数的this会指向window对象,所以此时this指向window对象

练习2:

下面这段代码的输出是?

javascript 复制代码
function fun() {
  return this.a + this.b
}
var a = 1;
var b = 2;
var obj = {
  a: 3,
  b: fun(),
  fun: fun
}

var result = obj.fun()
console.log(result)

答案:
6

解析:

  1. 首先这里的通过obj对象调用其fun属性,满足了规则1:对象打点调用它的方法函数,则函数的this是这个打点的对象,所以此时的结果为this.a + this.b就相当于obj.a + obj.b
  2. 而这里obj对象的b属性为一个函数的调用,满足了规则2:通过圆括号直接调用的函数,函数的this会指向window对象,所以此时的结果相当于obj.a + window.a + window.b => 3 + 1 + 2结果为6

三、数组(类数组对象)枚举出的函数

  • 数组(类数组对象)枚举出函数进行调用,this是这个数组(类数组对象)
  • 什么是类数组对象:所有键名为自然数序列(从0开始),且有length属性的对象
  • arguments对象是最常见的类数组对象,它是函数的实参列表
javascript 复制代码
// 1. 数组
var arr = ['a', 'b', 'c', function(){
  console.log(this[0])
}]
arr[3]()

// 2. 类数组对象
function fun() {
  arguments[3]()
}

fun('a', 'b', 'c', function(){
  console.log(this[1])
})

练习:

下面代码执行后会打印什么?

javascript 复制代码
function func() {
  console.log(this)
}
debugger
var arr = [{
  func
}, 'b', 'c', function(){
  this[0].func()
}]
arr[3]()

答案:
arr[0]的对象{func}

解析:

  1. 首先这里一开始就调用了arr[3](),满足规则3:数组(类数组对象)枚举出函数进行调用,this是这个数组(类数组对象),所以此时函数的内部this指向arr数组
  2. this[0]相当于获取arr[0]也就是{func}这个对象,调用这个对象的func方法,满足规则1:对象打点调用它的方法函数,则函数的this是这个打点的对象,所以此时this就指向于{func}这个对象

四、IIFE

  • [IIFE](https://developer.mozilla.org/zh-CN/docs/Glossary/IIFE)也就是立即执行函数中this为window对象
javascript 复制代码
(function(){
  console.log(this)
})();

练习:

下面这段代码打印的结果是?

javascript 复制代码
var a = 1;
var obj = {
  a: 2,
  fun: (function() {
    var a = this.a;
    return function() {
      console.log(a + this.a)
    }
  })()
}
obj.fun()

答案:
3

解析:

  1. IIFE在定义的时候会立刻执行,所以obj对象的实际值为IIFE中返回的函数
  2. 因为规则4:IIFE中this为window对象,所以var a = this.a,这里的this指向了window,所以这里a的值为1
  3. IIFE返回的函数中的console.log(a + this.a),根据2可以知道前者的值为1(这里形成了闭包)后者的this取决于实际调用的情况
  4. 根据obj.fun(),满足规则1:对象打点调用它的方法函数,则函数的this是这个打点的对象,所以a + this.a中的this指向obj,所以结果为3

五、定时器与延时器调用函数

  • 定时器、延时器调用函数,上下文是window对象
javascript 复制代码
setTimeout(function() {
  console.log(this)
}, 200)
javascript 复制代码
let i = 0
let timer = setInterval(function() {
  console.log(this)
  i++
  if(i > 1){
    clearInterval(timer)
  }
}, 1000)

练习:

下面这道题的运行后打印的结果是?

javascript 复制代码
function fun() {
  console.log(this.a + this.b)
  return this.a + this.b
}
var obj = {
  a: 1,
  b: 2,
  fun
}
var a = 3
var b = 4
setTimeout(obj.fun, 200)

答案:
7

解析:

  1. 首先这里设置了一个定时器,规定200毫秒以后执行obj.fun中的函数体
    1. 相当于定义了这样一个定时器:
javascript 复制代码
setTimeout(obj.fun, 200)
// 相当于
setTimeout(function() {
  console.log(this.a + this.b)
  return this.a + this.b
}, 200)
  1. 满足规则5:定时器、延时器调用函数,上下文是window对象,所以此时的打印结果为7
  2. 注意点:下面这种写法和上面完全不一样
javascript 复制代码
setTimeout(function() {
  obj.fun()
}, 200)
  • 这里的意思是200毫秒后执行obj.fun()这个函数,此时满足规则1:对象打点调用它的方法函数,则函数的上下文是这个打点的对象,所以此时的thisobj这个对象,结果就为3

六、事件处理函数

  • 事件处理函数的上下文是绑定事件的DOM元素

比如给一个div元素添加了一个点击事件,则当前绑定的this指向该div元素

html 复制代码
<body>
    <div id="test_div1">TEST1</div>
    <div id="test_div2">TEST2</div>
    <script>
      let testDOM1 = document.getElementById('test_div1')
      let testDOM2 = document.getElementById('test_div2')
      function clickHandle() {
        console.log(this)
      }
      testDOM1.addEventListener('click', clickHandle)
      testDOM2.addEventListener('click', clickHandle)
    </script>
  </body>

七、通过call、apply调用的函数

关于bind

  • 多次 bind 时只认第一次 bind 的值(使用bind后相当于永久改变this的指向)

练习:

javascript 复制代码
let obj = {
  func() {
    let that = this
    const arrowFunc = function() {
      console.log(that._name)
    }

    return arrowFunc
  },

  _name: "obj",
}

obj.func()()

obj.func.bind({ _name: "bindObj" }).apply({ _name: "applyObj" })()

答案:

依次输出objbindObj

解析:

  1. obj.func()()
    1. 上面这段代码先是调用了obj.func()这个方法,该方法返回一个函数arrowFunc
    2. obj.func()这个方法中首先通过that保存了当前的this,而当前的this因为满足规则1:对象打点调用它的方法函数,则函数的this为这个打点的对象,所以此时的that就指向obj对象
    3. 因为obj.func()的返回值为arrowFunc函数,所以obj.func()()就相当于arrowFunc(),调用arrowFunc()会输出that._name,上一步中我们知道了that此时的指向为obj,所以此时会输出obj
  2. obj.func.bind({ _name: "bindObj" }).apply({ _name: "applyObj" })()
    1. obj.func.bind({ _name: "bindObj" })通过bind返回一个原函数的拷贝,并拥有指定的 this(此时为{ _name: "bindObj" }) ,注意bind方法不会直接调用函数
    2. 通过apply({ _name: "applyObj" })调用之前bind函数返回的原函数的拷贝 并指定此时的this{ _name: "applyObj" },因为使用bind后相当于永久改变this的指向,所以这个时候不会改变this的指向为{ _name: "applyObj" },此时this的指向依旧是上一步通过bind指定的{ _name: "bindObj" }对象,函数执行后会得到内部返回的arrowFunc函数
    3. 调用arrowFunc函数,因为通过bind方法指定了this{ _name: "bindObj" },所以obj.func函数保存的that此时指向了{ _name: "bindObj" }对象,所以打印结果为bindObj

八、通过new关键字调用函数(构造函数)

  • 通过new关键字调用函数,函数中的this此时指向这个"new出来的对象",且优先级高于bind
javascript 复制代码
function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function () {
        console.log(this.name);
    };
    console.log(this)
}
Person.bind(1)
let person1 = new Person("Walter White", 50, "Chemist");

特殊情况:箭头函数

  • 箭头函数没有自己的this,箭头函数中的this指向外层非箭头函数中的this
  • 不支持call/apply的函数特性,且使用bind不会改变this指向,bind指定的参数会传给函数
  • 箭头函数不可以当做构造函数
    • 构造函数是通过new关键字来生成对象实例,生成对象实例的过程也是通过构造函数给实例绑定this的过程,而箭头函数没有自己的this。
    • 创建对象过程,new 首先会创建一个空对象,并将这个空对象的__proto__指向构造函数的prototype,从而继承原型上的方法,但是箭头函数没有prototype因此不能使用箭头作为构造函数,也就不能通过new操作符来调用箭头函数------可参考

练习:

javascript 复制代码
var returnThis = () => this

returnThis() // 1

var boss1 = {

  name: 'boss1',

  returnThis () {

    var func = () => this

    return func()

  }

}

returnThis.call(boss1) // 2

var boss1returnThis = returnThis.bind(boss1)

boss1returnThis() // 3

boss1.returnThis() // 4

var boss2 = {

  name: 'boss2',

  returnThis: boss1.returnThis

}

boss2.returnThis() // 5

答案:

  • 1->window
  • 2->window
  • 3->window
  • 4->boss1
  • 5->boss2

解析:

  1. 箭头函数中的this指向外层非箭头函数中的this,此时外层为window,所以这个时候对应的thiswindow
  2. 不支持call/apply的函数特性,所以此时的this指向为外层非箭头函数中的thiswindow
  3. 箭头函数中使用bind不会改变this指向,此时的this指向为外层非箭头函数中的thiswindow
  4. boss1.returnThis()返回一个匿名函数的调用,箭头函数中的this指向外层非箭头函数中的this,通过对象打点方式调用的函数,函数的this为这个打点的对象,所以此时的this为boss1
  5. boss2.returnThis()返回一个匿名函数的调用,箭头函数中的this指向外层非箭头函数中的this,通过对象打点方式调用的函数,函数的this为这个打点的对象,所以此时的this为boss2

严格模式下的变化

  1. 非严格模式下全局作用域中的函数中的this指向window对象。
  2. 严格模式中全局作用域中函数中的this指向undefined
javascript 复制代码
function test1() {
  console.log("test1:",this)
}

function test2() {
  // 只给test2函数开启严格模式
  "use strict"
  console.log("test2:",this)
}

test1()
test2()
javascript 复制代码
'use strict'
// 全局作用域中函数中的this指向undefined
function test() {
  console.log('test->this:', this) // undefined
}
test()
  1. 非严格模式下构造函数不使用new也可以作为普通函数调用,this指向全局对象
javascript 复制代码
function Person() {
  this.sex = 'male'
  console.log(this, this.sex)
}
Person()
  1. 严格模式下,如果 构造函数不加new调用, this 指向的是 undefined 如果给他赋值则 会报错
javascript 复制代码
'use strict'
function Person() {
  this.sex = 'male'
  console.log(this, this.sex)
}
Person()
  1. new 实例化的构造函数指向创建的对象实例。
javascript 复制代码
'use strict'
function Star() {
  this.sex = 'male'
  console.log(this, this.sex)
}
new Star()
  1. 定时器中的this指向的还是window
javascript 复制代码
setTimeout(function () {
  console.log(this)
}, 100)
  1. 事件处理函数或者对象打点方式调用的函数指向的还是调用者
javascript 复制代码
'use strict'


let obj = {
  a: 1,
  b: 2,
  fn: function () {
    return this
  }
}

console.log(obj.fn() === obj)
javascript 复制代码
'use strict'
let testDOM1 = document.getElementById('test_div1')
let testDOM2 = document.getElementById('test_div2')
function clickHandle() {
  console.log(this)
}
testDOM1.addEventListener('click', clickHandle)
testDOM2.addEventListener('click', clickHandle)
  1. IIFE中的this指向为undefined
javascript 复制代码
'use strict';
(function () {
  console.log(this)
})()

参考文献

相关推荐
问道飞鱼2 分钟前
【前端知识】强大的js动画组件anime.js
开发语言·前端·javascript·anime.js
k09334 分钟前
vue中proxy代理配置(测试一)
前端·javascript·vue.js
傻小胖5 分钟前
React 脚手架使用指南
前端·react.js·前端框架
程序员海军18 分钟前
2024 Nuxt3 年度生态总结
前端·nuxt.js
m0_7482567828 分钟前
SpringBoot 依赖之Spring Web
前端·spring boot·spring
web135085886351 小时前
前端node.js
前端·node.js·vim
m0_512744641 小时前
极客大挑战2024-web-wp(详细)
android·前端
若川1 小时前
Taro 源码揭秘:10. Taro 到底是怎样转换成小程序文件的?
前端·javascript·react.js
潜意识起点1 小时前
精通 CSS 阴影效果:从基础到高级应用
前端·css
奋斗吧程序媛1 小时前
删除VSCode上 origin/分支名,但GitLab上实际上不存在的分支
前端·vscode