this指向与箭头函数

写在前面


大家好,我是一溪风月🤠,一名前端工程师。在 JavaScript 里,this的指向问题恐怕是很多开发者绕不开的 "老大难"------ 无论是原生开发场景,还是在 React、Vue 等主流框架的实践中,this的使用总能让人栽跟头。

这篇文章我会从基础场景到复杂情境,系统拆解this的绑定规则:从函数调用方式对this的影响,到箭头函数与普通函数的本质区别,再到框架中常见的this陷阱(比如类组件中的事件处理、Vue 方法中的this指向)。

相信看完这篇内容,你不仅能快速判断this的指向,更能在实际开发中主动掌控它的行为。话不多说,我们直接进入正题!

一.this到底指向什么?


首先我们先来思考一个让人困惑的问题,定义一个函数我们使用三种不同的方式进行调用,它产生了三种不同的结果,以上的案例能够给我们什么启示哪?

  1. 函数在调用时,JavaScript会默认给this绑定一个值。
  2. this的绑定和定义的位置(编写的位置)没有关系。
  3. this的绑定和调用方式以及调用的位置有关系。
  4. this是在运行时被绑定的。

那么this到底指向的什么东西哪?以下是this常见的绑定规则。

  • 绑定一:默认绑定
  • 绑定二:隐式绑定
  • 绑定三:显示绑定
  • 绑定四:new绑定

二.默认绑定


默认绑定就是直接对函数进行调用,这种方式我们可以称之为独立函数调用,此时函数中的this指向的是window。

js 复制代码
function foo () {
  console.log("打印foo函数", this)
}

foo() // 指向的是window

函数定义在对象中但是独立进行调用,依然指向的是window和函数定义的位置没有关系。

js 复制代码
let obj = {
  name: "zzz",
  foo: function () {
    console.log(this)
  }
}

let baz = obj.foo
baz() // this指向的是window

💡严格模式下,独立函数调用的函数中的this指向的是undefined

js 复制代码
"use strict"
// 定义函数
function foo () {
  console.log(this)
}
foo() // 此时this指向的是undefined

通过高阶函数的方式进行函数的调用,被调用函数中的this依然指向的是window

js 复制代码
function fn () {
  console.log(this)
}

function foo (callback) {
  callback && callback()
}

foo(fn) // 此时打印的this指向的window

那么我们来总结一下💡:默认绑定(独立函数调用)在严格模式下指向的是undefined在非严格模式下指向的是window并且跟this编写的位置没有关系,无论编写的位置是在对象中,还是通过高阶函数的方式来进行调用其实this指向的都是window

三.隐式绑定


除了上述内容,另外一个比较常用的绑定方式就是通过对象进行调用,也就是它的调用位置中是通过某个对象对函数发起的调用,这个时候this指向的是发起调用的这个对象。

js 复制代码
function foo () {
  console.log(this)
}

let obj = {
  bar: foo
}

console.log(obj.bar()); // this指向的是foo这个对象。

四.new绑定


在JavaScript中的函数可以当一个类的构造函数来使用,也就是使用new关键字,首先我们先看下当我们进行实例化的时候究竟做了什么事情。

  1. 创建一个新的空对象。
  2. 将this指向这个空对象。
  3. 执行函数体中的代码。
  4. 没有显式返回非空对象的时候会默认返回这个对象。
js 复制代码
function foo (name) {
  this.name = name
  console.log(this, "打印this数据信息")
}
let obj = new foo("why")

五.显式绑定


隐式绑定有一个前提条件:

  • 必须在调用的对象内部有一个对函数的引用(比如一个属性)
  • 如果没有这样的引用,在进行调用的时候,会找不到该函数的错误。
  • 正是通过这个引用,间接的将this绑定到了这个对象上。

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

JavaScript中所有的函数都可以使用callapply方法

  • 第一个参数是相同的,要求传入一个对象,这个对象的作用是什么?这是给this准备的,在调用这个函数的时候,会将this绑定到这个传入的对象上。
  • 后面的参数,apply为数组,call为参数列表。

显式绑定指的是开发者自己手动去对this进行绑定,常见的显式绑定有三种方式分别是call``apply``bind三种方式进行绑定和调用。

  1. 使用call进行this的显示绑定:第一个参数绑定this,第二个参数传递额外参数以多参数的形式进行传递,会作为实参。
js 复制代码
let obj = {
  name: "zzz",
  age: 22
}

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

foo.call(obj)
  1. 使用apply进行this的显式绑定:第一个参数绑定this,传入额外的实参以数组的形式。
js 复制代码
let obj = {
  name: "zzz",
  age: 22
}

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

foo.apply(obj) // this指向的是函数绑定的对象
  1. 如果当我们在调用的时候不希望obj身上有函数,这个时候可以使用bind进行this的绑定:会返回一个绑定函数(怪异函数对象),通过返回的值进行手动调用,第一个参数绑定this,第二个参数以多参数的形式进行实参的传递。
js 复制代码
let obj = {
  name: "zzz",
  age: 22
}

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

let bindFn = foo.bind(obj)
bindFn() // this指向的是obj对象

六.内置函数的this思考


其实在很多时候我们不知道某个函数内部做了什么,因此我们不能安全确定某个函数的内部this的指向。

定时器函数:指向的是window

js 复制代码
setTimeout(function () {
  console.log("定时器函数" + this)
}, 1000)

按钮的点击监听:指向的是按钮btnEle

js 复制代码
// 点击按钮触发事件
let btnEle = document.querySelector("button")
btnEle.onclick = function () {
  console.log("按钮的点击", this)
}

forEach循环:指向的window,也可以通过第二个参数来指定this

js 复制代码
let name = ['a', 'b', 'c', 'd', 'e']
name.forEach(item => {
  console.log("forEach:", this)
})

七.this绑定的优先级比较


通过以上的规则,当我们在项目代码中遇到了上述的情况可以通过上述的规则来进行匹配,但是如果一个函数调用位置应用了多条这样的规则,优先级谁更高哪?

  1. 默认规则的优先级较低,毫无疑问默认规则的优先级是最低的,因为存在其他规则的时候,就会通过其他规则的方式来绑定this。
  2. 显式绑定的优先级高于隐式绑定。
js 复制代码
function foo () {
  console.log("foo:", this)
}

var obj = { foo: foo }
obj.foo.apply("abc")  // 指向abc
js 复制代码
function foo () {
  console.log("foo:", this)
}

let bar = foo.bind("aaa")
let obj = {
  name: "zzz",
  baz: bar
}

obj.baz()
  1. new绑定优先级高于隐式绑定。
js 复制代码
let obj = {
  name: "why",
  foo: function () {
    console.log(this)
    console.log(this === obj)
  }
}

new obj.foo()
  1. new绑定和显示绑定,new不可以和apply/call一起使用,new优先级高于bind绑定。
js 复制代码
function foo () {
  console.log("foo", this)
}

let bindFn = foo.bind("aaa")
new bindFn()

那么我们来总结一下优先级:

  1. new优先级
  2. bind
  3. apply/call
  4. 隐式绑定
  5. 默认绑定

八.this规则之外


上述的内容其实我们足以应付平时的开发了,但是其实在规则之外还有一些特殊的情况我们需要注意的。

情况一:显式绑定null/undefined,那么规则使用的是默认绑定。

js 复制代码
function foo () {
  console.log("foo", this)
}

foo.apply(null)
foo.apply(undefined)

在严格模式下的规则:

js 复制代码
"use strict"

function foo () {
  console.log("foo", typeof this)
}

foo.apply("abc")
foo.apply(null)
foo.apply(undefined)

情况二:间接函数引用,创建函数间接引用,这种情况使用默认绑定规则

js 复制代码
var obj1 = {
  name: "obj1",
  foo: function () {
    console.log("foo", this)
  }
}

var obj2 = {
  name: "obj2",
};

(obj2.foo = obj1.foo)() // this->window

需要说明间接函数引用本质上还是一种独立函数调用!

九.箭头函数arrow function


箭头函数是ES6之后增加的一种编写函数的方法,并且它比表达式要更加的简洁。

  • 箭头函数不会绑定thisarguments属性
  • 箭头函数不能作为构造函数使用(不能和new一起使用,全抛出错误)
js 复制代码
const foo = (name, age) => {
  console.log("箭头函数的函数体")
  console.log(name, age)
}

箭头函数的精简写法:

  1. 如果只有一个参数的话箭头函数的小括号可以省略
js 复制代码
const foo = name => {
  console.log("箭头函数的函数体")
  console.log(name)
}
  1. 如果函数体中只有一行执行代码,这行代码的结果会自动作为整个函数的返回值。
js 复制代码
const foo = age => age*2;
  1. 如果函数体中只有一行执行代码,那么{}可以省略,一行代码中不能带return关键字,如果省略需要带return一起省略。
js 复制代码
var nums = ['a', 'b', 'c', 'd', 'e']
nums.filter(item => {
  return item === 'a'
})

// 优化后
var nums = ['a', 'b', 'c', 'd', 'e']
nums.filter(item => item === 'a')
  1. 如果默认返回值是一个对象,那么这个对象必须添加小括号(react中使用较多)
js 复制代码
const foo = () => ({ name: "zzz", age: 12 })

小案例练习:箭头函数实现nums的所有偶数平方的和

js 复制代码
var nums = [20, 30, 11, 15, 111]
var result = nums.filter(item => item % 2 === 0)
  .map(item => item ** 2)
  .reduce((acc, item) => acc + item, 0)

console.log(result)
// 结果:1300

十.箭头函数的this


我们知道普通函数是有this的,但是其实在箭头函数中压根没有this,我们在箭头函数中打印出来的this其实是上层作用域中的this,this这个变量会通过作用域一层一层的进行查找,如下述代码就是找到了window

js 复制代码
function foo () {
  console.log("foo", this)
}

foo() // window

十一.this面试题解析


this面试题一:

js 复制代码
var name = "window"

var person = {
  name: "person",
  sayName: function () {
    console.log(this.name)
  }
}

function sayName () {
  var sss = person.sayName
  sss() //绑定:默认绑定  window
  person.sayName(); //绑定:隐式绑定 person
  (person.sayName)(); //绑定: 隐式绑定 person
  (b = person.sayName)() //绑定: 间接函数引用 window
}

sayName()

this面试题二:

js 复制代码
var name = 'window'
var person1 = {
  name: 'person1',
  foo1: function () {
    console.log(this.name)
  },
  foo2: () => console.log(this.name),
  foo3: function () {
    return function () {
      console.log(this.name)
    }
  },
  foo4: function () {
    return () => {
      console.log(this.name)
    }
  }
}

var person2 = { name: 'person2' }

person1.foo1(); // person1
person1.foo1.call(person2); // person2

person1.foo2(); // window
person1.foo2.call(person2); // window

person1.foo3()(); // window
person1.foo3.call(person2)(); // window
person1.foo3().call(person2); // person2

person1.foo4()(); // person1
person1.foo4.call(person2)(); // person2
person1.foo4().call(person2); // person1

this面试题三:

js 复制代码
var name = 'window'
function Person (name) {
  this.name = name
  this.foo1 = function () {
    console.log(this.name)
  },
    this.foo2 = () => console.log(this.name),
    this.foo3 = function () {
      return function () {
        console.log(this.name)
      }
    },
    this.foo4 = function () {
      return () => {
        console.log(this.name)
      }
    }
}
var person1 = new Person('person1')
var person2 = new Person('person2')

person1.foo1() // person1
person1.foo1.call(person2) // person2

person1.foo2() // person1
person1.foo2.call(person2) // person1

person1.foo3()() // window
person1.foo3.call(person2)() // window
person1.foo3().call(person2) // person2

person1.foo4()() // person1
person1.foo4.call(person2)() // person2
person1.foo4().call(person2) // person1

this面试题四:

js 复制代码
var name = 'window'
function Person (name) {
  this.name = name
  this.foo1 = function () {
    console.log(this.name)
  },
    this.foo2 = () => console.log(this.name),
    this.foo3 = function () {
      return function () {
        console.log(this.name)
      }
    },
    this.foo4 = function () {
      return () => {
        console.log(this.name)
      }
    }
}
var person1 = new Person('person1')
var person2 = new Person('person2')

person1.foo1() // person1
person1.foo1.call(person2) // person2

person1.foo2() // person1
person1.foo2.call(person2) // person1

person1.foo3()() // window
person1.foo3.call(person2)() // window
person1.foo3().call(person2) // person2

person1.foo4()() // person1
person1.foo4.call(person2)() // person2
person1.foo4().call(person2) // person1

十一.总结


这篇文章到这里就结束了😈,这篇文章我们学习了this和箭头函数的内容,这篇文章我们首先讲解了this的绑定的几种规则,分别包括,默认绑定,隐式绑定,new绑定,显式绑定等等内容,除了常规的绑定之外,我们了解了this绑定规则之外的一些内容,最后我们学习了ES6的箭头函数以及箭头函数中的this。

相关推荐
寅时码8 分钟前
无需安装,纯浏览器实现跨平台文件、文本传输,支持断点续传、二维码、房间码加入、粘贴传输、拖拽传输、多文件传输
前端·后端·架构
哔哩哔哩技术31 分钟前
哔哩哔哩Android视频编辑页的架构升级
前端
小小小小宇39 分钟前
重提Vue 3 性能提升
前端
eason_fan39 分钟前
React 源码执行流程
前端·源码阅读
will_we1 小时前
服务器主动推送之SSE (Server-Sent Events)探讨
前端·后端
yume_sibai1 小时前
Less Less基础
前端·css·less
小小小小宇1 小时前
重提Vue3 的 Diff 算法
前端
清岚_lxn1 小时前
前端js通过a标签直接预览pdf文件,弹出下载页面问题
前端·javascript·pdf
不爱说话郭德纲1 小时前
别再花冤枉钱!手把手教你免费生成iOS证书(.p12) + 打包IPA(超详细)
前端·ios·app
代码的余温1 小时前
Vue多请求并行处理实战指南
前端·javascript·vue.js