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)
})()

参考文献

相关推荐
黄智勇30 分钟前
xlsx-handlebars 一个用于处理 XLSX 文件 Handlebars 模板的 Rust 库,支持多平台使
前端
brzhang2 小时前
为什么 OpenAI 不让 LLM 生成 UI?深度解析 OpenAI Apps SDK 背后的新一代交互范式
前端·后端·架构
brzhang2 小时前
OpenAI Apps SDK ,一个好的 App,不是让用户知道它该怎么用,而是让用户自然地知道自己在做什么。
前端·后端·架构
爱看书的小沐2 小时前
【小沐学WebGIS】基于Three.JS绘制飞行轨迹Flight Tracker(Three.JS/ vue / react / WebGL)
javascript·vue·webgl·three.js·航班·航迹·飞行轨迹
井柏然3 小时前
前端工程化—实战npm包深入理解 external 及实例唯一性
前端·javascript·前端工程化
IT_陈寒4 小时前
Redis 高性能缓存设计:7个核心优化策略让你的QPS提升300%
前端·人工智能·后端
aklry4 小时前
elpis之动态组件机制
javascript·vue.js·架构
井柏然4 小时前
从 npm 包实战深入理解 external 及实例唯一性
前端·javascript·前端工程化
羊锦磊4 小时前
[ vue 前端框架 ] 基本用法和vue.cli脚手架搭建
前端·vue.js·前端框架
brzhang4 小时前
高通把Arduino买了,你的“小破板”要变“AI核弹”了?
前端·后端·架构