JS this指向问题

this问题,是很多前端同学初学JS的拦路虎,甚至一些两三年工作经验的同学对this的理解还是模棱两可或是有一些误解,一个常见的误区就是:this指向函数本身。本文尝试总结了一下this的使用,并在最后总结了this指向的确定规则。

其实this很简单,你早该熟悉它了!

一、普通函数:谁调用指向谁

这是确定this指向最普遍的规则,普通函数是指由构造函数Function创建或者使用关键字function声明定义的函数。通常情况下,普通函数的this并不天生指向其定义所在的对象,而是指向它的调用者。
而如果没有指定调用者,严格模式下this会是undefined,非严格模式下则是指向全局对象(Node环境中是global,浏览器环境中则是window)。
且看示例,obj2和obj共享了func函数:

let obj = {
  a: 'obj',
  func () {
    console.log(this.a)
  }
}

let obj2 = {
  a: 'obj2',
  func: obj.func
}

obj.func() // this指向调用者obj,输出:obj1
obj2.func() // this指向调用者obj2,输出:obj2

let func = obj.func
window.a = 'window'
func() // 未指定调用者,this指向全局对象,输出:window

示例中,this指向了给定的调用者或者全局对象window。如果是严格模式,最后的代码会报错,因为this指向了undefined:

"use strict"
// 省略函数定义
let func = obj.func
window.a = 'window'
func() // 报错:Uncaught TypeError: Cannot read property 'a' of undefined

二、箭头函数中的this

箭头函数的this指向,和箭头函数定义所在上下文的this相同。对于普通函数,this在函数调用时才确定;而对于箭头函数,this在箭头函数定义时就已经确定了,并且不能再被修改。

且看示例:

let obj = {
  A () {
    return () => {
      return this
    }
  },
  B () {
    return function () {
      return this
    }
  }
}

let func = obj.A()
console.log(func() === obj) // true

func = obj.B()
console.log(func() === obj) // false
console.log(func() === window) // true

例子中,obj的A/B函数都是返回一个内部定义的函数,A、B内部定义的函数函数返回值都是this,只是在A中,内部函数是箭头函数,而B中则是普通函数。将A、B函数创建的内部函数分别赋值给变量func进行调用,未指定调用者,可以发现箭头函数的返回值this能够指向obj,而普通函数的this则因为未指定调用者而指向了全局对象。

总而言之一句话,箭头函数this在定义时就已确定,指向此时外层函数的this,如果没有外层函数则指向全局对象。严格模式下箭头函数没有外层函数的情况this的指向会不会不同呢?并不会,依然指向全局对象而不是undefined,这点需要稍微注意:

"use strict"
let func = () => {
  return this
}
let func2 = function () {
  return this
}
console.log(func() === window) // true
console.log(func2() === window) // false
console.log(func2() === undefined) // true

三、构造函数中的this

JS里的普通函数可以使用new操作符来创建一个对象,此时该函数就是一个构造函数,箭头函数不能作为构造函数。执行new操作符,其实JS内部完成了以下事情:

  1. 创建一个空的简单JavaScript对象(即{});
  2. 将构造函数的prototype绑定为新对象的原型对象 ;
  3. 将步骤1新创建的对象作为this的上下文并执行函数 ;
  4. 如果该函数没有返回对象,则返回this。
"use strict"
function A () {
  this.a = 1
  this.func = () => {
    return this
  }
}

let obj = new A()
console.log(obj.a) // 1
console.log(obj.func() === obj) // true

四、使用apply和call动态改变函数this的指向

使用apply可以在调用函数时改变this的指向,当然,箭头的this已经不可改变,所以不能通过该方法修改this指向。apply是函数原型上的方法,所以通过function创建的函数都有这个属性,函数的apply属性也是一个函数,它接收两个参数,第一个参数为指定的this对象,第二个参数为函数的参数列表:

func.apply(thisArg, [argsArray])
function dir (x, y) {
  this.x = x
  this.y = y
}

let obj = {
  a: 1,
  b: 2
}
dir.apply(obj, [3, 4])

console.log(JSON.stringify(obj)) // {"a":1,"b":2,"x":3,"y":4}

可以看到,通过apply函数指定了函数dir调用的this对象为obj,对this对象的修改就是对obj的修改。call函数和apply的作用都是更改this的指向,只是使用上稍微不同,apply方法中,第二个参数为一个数组,原函数所有的参数都放入该数组中,而call函数接收多个参数,第二个及之后的所有参数都被作为参数来调用原函数,对于上例,改为call方法调用如下:

dir.call(obj, 3, 4)
console.log(JSON.stringify(obj)) // {"a":1,"b":2,"x":3,"y":4}

call和apply的第一个参数用于指定this对象。注意非严格模式下,如果该参数为null或undefined,则会指向全局对象,严格模式下则this依然指向给定的null或undefined:

let obj = {
  test () {
    return this === window
	}
}
console.log(obj.test()) // false
console.log(obj.test.call(undefined)) // 非严格模式输出:true,严格模式输出:false

对箭头函数使用call或者apply将不能指定this,因为箭头函数的this在定义时已经确定:

let dir = (x, y) => {
  this.x = x
  this.y = y
}

let obj = {
  a: 1,
  b: 2
}
dir.apply(obj, [3, 4])

console.log(JSON.stringify(obj)) // {"a":1,"b":2,"x":3,"y":4}
console.log(window.x, window.y) // 3, 4

dir该为箭头函数后,定义时this指向window,通过apply调用时,对this的操作还是在window上生效。

五、使用bind创建新函数并绑定this

bind() 方法会创建一个新函数,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。bind方法有以下特点:

  • 新生成一个函数,包装了原函数,这意味着原函数的功能不被影响,但是调用新函数会调用原函数
  • 对箭头函数无效
  • 新生成的函数绑定了固定的this,再次bind生成的函数不会重新绑定this,很类似箭头函数,不过二者存在很大不同。
  • 非严格模式指定this为undefined,结果会指向全局对象
  • bind函数的第二个以及之后的参数,会作为原函数的参数并占用参数位置,绑定函数接收的参数会拼接在后面一并传给原函数。
let test = function (b, c) {
  console.log(this.a, b, c)
}
let obj = { a: 'obj1-a' }
let obj2 = { a: 'obj2-a' }

let bindFunc = test.bind(obj, 'b')
bindFunc('c') // obj1-a, b, c

bindFunc.call(obj2, 'c') // 指定this无效,输出:obj1-a, b, c

test.call(obj2, 'b', 'c') // 原函数不被影响,输出:obj2-a, b, c

let bindFunc2 = bindFunc.bind(obj2, 'c-rebind') 
bindFunc2() // 不能再次重新绑定,输出:obj1-a, b, c-rebind

let bindFunc3 = test.bind(undefined, 'b')
window.a = 'window-a'
bindFunc3('c') // 非严格模式输出window-a, b, c

以上是bind函数的常规用法,bind函数生成的函数与箭头函数不同,它仍然可以用于new操作符,不过这种情况下,bind的this对象会被忽略,this还是指向新生成的对象:

function A (b) {
  this.a = 'a'
  this.b = 'b'
}

let obj = {
  a: 'obj-a'
}

let B = A.bind(obj)

let newObj = new B()
console.log(newObj.a, obj.a) // a, obj-a

六、总结

以上就是this指向的大部分内容,在这里对this指向的规则做一个简单总结:

  1. 绝大多数情况下,非箭头函数内部的this由调用者确定,即谁调用指向谁
  2. 箭头函数的this取决于函数定义所在的上下文中this,即函数定义外部this是什么,箭头函数内部的this就是什么,相当于固化了当前执行环境中的this,注意不是函数定义所在的对象!
  3. 构造函数中的this指向新创建的对象
  4. apply/call方式调用的函数,其this可以在调用时指定
  5. bind函数可以将根据一个函数生成一个新的函数,并且该函数的this绑定到指定的对象上

其实最基本的规则就是第1条,其它的规则则是补充一些特殊情况下this的指向和使用方法。

¥赞赏