Javascript中this指向,你真的熟悉了吗

前言

Javascript中,this指向一直是非常困扰我们前端小伙伴的一个问题,今天就通过几个角度分享一下我对this指向在不同场景下的理解

全局作用域

在全局作用域中浏览器打印this,this指向的是window全局对象,并且不管是否为严格模式

console.log(this === window) // true

'use strict'
console.log(this === window) // true
复制代码

默认绑定

当一个普通函数被独立调用时,非严格模式下该函数中的this指向的是window全局对象,严格模式下this的值为undefined

function fn() {
  console.log(this === window) // true
}
fn()
复制代码
'use strict'
function fn() {
  console.log(this) // undefined
}
fn()
复制代码

在这里值得注意的是,在非严格模式下,虽然函数独立调用中this指向window,却不能理解为函数的独立调用等同于window调用

函数表达式为例,分别以varletconst命令声明一个普通函数,并分别进行直接调用和使用window调用

var命令声明的函数,直接调用和使用window调用都可以正常调用,this指向window

var fn = function() {
  console.log(this) // window
}
fn()
window.fn() 
复制代码

let命令声明的函数,直接调用,this指向window,使用window调用报错

let fn = function() {
  console.log(this)
}
fn() // 指向window
window.fn() // 报错
复制代码

const命令声明的函数,直接调用,this指向window,使用window调用报错

const fn = function() {
  console.log(this)
}
fn() // 指向window
window.fn() // 报错
复制代码

var命令和function命令声明的全局变量,是顶层对象的属性,所以通过window可以调用,而在ES6中,let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。也就说明了,通过let、const命令声明的函数并不能通过window调用。这也更加说明了一点,在非严格模式下普通函数独立调用时,其this指向window,但不等同于是window调用

对象成员

对象成员中的方法,由对象调用,this指向该对象,若将某个函数,或另外一个对象的方法,赋值于该对象下成员,该对象进行调用,this指向亦是该对象,默认情况下,对象方法的调用,this指向就是该方法的上下文对象

方法

const james = {
  name: 'James',
  sayHi: function() {
    console.log(this)
  }
}
const curry = {
  name: 'Curry',
  sayHi: function() {
    console.log(this)
  }
}
james.sayHi() // this指向james对象
curry.sayHi() // this指向curry对象
复制代码

隐式绑定

即函数(方法)调用位置,存在上下文对象,那么this指向便默认绑定到该上下文对象中

  // 全局作用域下声明一个sayHi函数
const sayHi = function() {
  console.log(this)
}
const james = {
  name: 'James',
    // 赋值
  sayHi: sayHi,
  friend: {
    name: 'Curry',
      // 赋值
    sayHi: sayHi
  }
}
sayHi() // this指向window
james.sayHi() // this指向james对象
james.friend.sayHi() // this指向james.firend对象
复制代码

无论事先定义对象的sayHi方法,还是引用sayHi函数作为对象中的方法,默认情况下,只要该函数(方法)调用中存在上下文对象,this则指向该上下文对象

显式绑定

显式绑定指的是通过callapply以及bind方法改变this的指向,相比于隐式绑定,显式绑定则是主动指明this的绑定关系

const james = {
  name: 'James',
  sayHi: function() {
    console.log(this)
  }
}
const curry = {
  name: 'Curry'
}

james.sayHi.call(curry) // this指向curry对象
james.sayHi.apply(curry) // this指向curry对象
james.sayHi.bind(curry)() // this指向curry对象
复制代码

上面的例子中,我们分别通过callapplybind的方式调用了james.sayHi方法,并传入curry对象,使this主动指向了curry对象

传入基本类型之非严格模式

ECMAScript中五种基本类型分别是:undefinednullbooleannumberstring

非严格模式下我们分别将这五种基本类型传入,以call为例,apply、bind同理,看看this指向发生了哪些变化

const james = {
  name: 'James',
  sayHi: function() {
    console.log(this)
  }
}

james.sayHi.call(undefined) // this指向window
james.sayHi.call(null) // this指向window
james.sayHi.call(true) // this指向Boolean包装类型
james.sayHi.call(0) // this指向Number包装类型
james.sayHi.call('0') // this指向String包装类型
复制代码

非严格模式下,通过传入五种基本类型进行显式绑定,其中除了传入undefinednull,this指向了window,剩余的booleannumberstring都指向其对应的基本包装类型

传入基本类型之严格模式

严格模式下我们分别将这五种基本类型传入,以call为例,apply、bind同理,看看this指向发生了哪些变化

'use strict'

const james = {
  name: 'James',
  sayHi: function() {
    console.log(this)
  }
}
james.sayHi.bind(undefined)() // this的值为undefined
james.sayHi.bind(null)() // this的值为null
james.sayHi.bind(true)() // this的值为true
james.sayHi.bind(0)() // this的值为0
james.sayHi.bind('0')() // this的值为'0'
复制代码

严格模式下进行显式绑定,this的值便是对应传入的值

通过new调用

我们先了解使用new操作符调用函数,内部会有以下几个动作

  1. 创建一个空对象
  2. 该对象会执行[[Prototype]]链接
  3. 生成的空对象会绑定到函数调用的this
  4. 通过new创建的每个对象将最终被[[Prototype]]链接到这个函数的prototype对象上
  5. 如果函数没有返回对象类型Object(包含Array、Functoin、Date、RegExg...),那么new表达式中的函数调用会自动返回(隐式返回)这个新的对象

通过以上步骤,我们通过代码写一个方法简单模拟一个new内部执行的过程

function myNew() {
  // 创建一个空对象
  let target = {}
  // constructor则是函数(构造函数),args则是获得其余参数
  const [constructor, ...args] = [...arguments]
  // 执行[[Prototype]]链接
  target.__proto__ = constructor.prototype
  let result = constructor.apply(target, args)
  // 这里简单判断是否为 object 或 function
  if (result && (typeof result == "object" || typeof result == "function")) {
    // 如果函数返回对象类型为Object类型,则返回该对象
    return result
  }
  // 如果构造函数返回的不是Object类型,返回我们创建的对象
  return target
}
复制代码

new操作符调用时,内部会返回一个新对象, 则this指向new内部生成的这个新对象,所以通常我们会说,通过构造函数创建一个实例(new),构造函数中的this,指向该实例。(这个实例便是new内部隐式返回的新对象)

function Persion(name, age) {
  this.name = name
  this.age = age
  console.log(this) // {name: 'James', age: 36}
}
const p = new Persion('James', 36)
复制代码

特别特别特别注意!,“通过构造函数创建一个实例(new),构造函数中的this,指向该实例”,前提是函数(构造函数)中,不能显式返回Object类型的值,否则创建出来的实例并不是new内部隐式返回的新对象了,而是构造函数中显式返回的Object类型的值,虽然此时构造函数中的this依然指向new内部生成的新对象,因为没能隐式返回,所以实例已经不是new内部生成的新对象了,而是构造函数中显式返回的Object,此时就不能说this指向该实例

function Persion(name, age) {
  this.name = name
  this.age = age
  // this 指向new内部生成的新对象 {name: 'James', age: 14}
  console.log(this) // {name: 'James', age: 14}
  // 由于这里显式返回了一个数组,无法隐式返回new内部生成的新对象
  return [1, 2, 3, 4]
}
// 通过构造函数创建实例
const p = new Persion('James', 14)
// 创建出来的实例是一个数组
console.log(p) // [1, 2, 3, 4]
复制代码

原型链中的this

很显然,方法的调用,默认情况下,调用位置存在上下文对象,则this指向该上下文对象,我们知道new操作符最后会返回一个新对象。原型中的this便指向由该构造函数创建的实例(new内部生成的新对象),即便你继承自其他对象,也会通过原型链方式进行查找

function Persion(name, age) {
  this.name = name
  this.age = age
}
Persion.prototype.sayHi = function() {
  console.log(this) // {name: 'James', age: 14}
}
const p = new Persion('James', 14)
p.sayHi()
复制代码

DOM事件处理

DOM事件的处理,this默认指向的是事件绑定的DOM元素

const btn = document.getElementById('btn')

btn.addEventListener('click', function(e) {
  console.log(e.currentTarget === this) // true
})
复制代码

e.currentTarget事件绑定的DOM元素,e.target是当前事件促发的DOM元素

创建 ul > li,点击li标签

<ul id="uu">
  <li style="height: 30px; background-color: pink;"></li>
</ul>
复制代码
const el = document.getElementById('uu')

el.addEventListener('click', function(e) {
  console.log(e.currentTarget) // 当前事件绑定对象 ul元素
  console.log(e.currentTarget === this) // true
  console.log(e.target) // 当前事件促发对象 li元素
  console.log(e.target === this) // false
})
复制代码

箭头函数

阮一峰老师的ECMAScript 6入门详细介绍到了使用箭头函数的以下几点注意事项

(1)箭头函数没有自己的this对象。

(2)不可以当作构造函数,也就是说,不可以对箭头函数使用new命令,否则会抛出一个错误。

(3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。

(4)不可以使用yield命令,因此箭头函数不能用作 Generator 函数。

// 非严格模式下

console.log(this) // window
const sayHi = () => {
  console.log(this) // window
}
sayHi()
复制代码
// 严格模式下
'use strict'
console.log(this) // window
const sayHi = () => {
  console.log(this) // window
}
sayHi()
复制代码
window.name = 'Curry'
const james = {
  name: 'James',
  sayHi: function() {
    console.log(this.name) // James
  },
  sayHello: () => {
    console.log(this.name) // Curry
  }
}
// james对象中的普通函数调用
james.sayHi() // this指向james对象

// james对象中的箭头函数调用
james.sayHello() // this指向window
复制代码

箭头函数没有自己的this,而是引用外层的this

// ES6
function foo() {
    // 使用箭头函数
  setTimeout(() => {
    console.log('id:', this.id)
  }, 100)
}

// Babel 编译后的ES5
function foo() {
  var _this = this;

  setTimeout(function () {
    console.log('id:', _this.id);
  }, 100);
}
复制代码

总结

  1. 全局作用域下,严格模式和非严格模式下,this 指向全局对象
  2. 普通函数独立调用,非严格模式中 this 指向 window,严格模式中 this 的值为 undefined,注意非严格模式下普通函数的独立调用 this 指向 window 但不等同于 window 调用
  3. 函数调用位置存在上下文对象,this 指向该上下文对象(对象调用方法,this指向该对象)
  4. 通过call、apply、bind方法调用进行显式绑定,严格模式下,this指向的值是传入的第一个参数。非严格模式下,第一个参数传入基本类型,除undefined或null指向window,传入的其余基本类型都指向其对应的基本包装类型
  5. 通过构造函数创建一个实例(new),构造函数中的this,指向该实例
  6. DOM事件,this指向事件绑定的DOM元素
  7. 箭头函数没有自己的this,而是引用外层的this

Guess you like

Origin juejin.im/post/7052606336818741284