Skip to content

this 指向问题

与其他语言相比,函数的 this 关键字在 JavaScript 中的表现略有不同。实际上,this 就是一个指针,指向调用函数的对象。 然而,要搞懂 this 指向问题,远远没这么简单。

this 绑定规则

为了弄清楚真正的指向,我们需要理解 this 的 4 个绑定规则:默认绑定、隐式绑定、显式绑定、new 绑定。

默认绑定

默认绑定,在不能应用其它绑定规则时使用的默认规则,通常是独立函数调用,如 doSomething()

javascript
function sayHi() {
  console.log(this.name)
}
var name = 'Global'
sayHi() // Global

sayHi() 应用了默认绑定,this 指向全局对象(非严格模式),全局对象的 name 属性为 Global

WARNING

在严格模式或 node 环境中,this 指向 undefined,此时会报错。

隐式绑定

隐式绑定,调用位置上存在上下文对象,或者被某个对象拥有或者包含,表现形式为 person.doSomething()

javascript
function sayHi() {
  console.log(this.name)
}
var person = {
  name: 'Achilles',
  sayHi: sayHi,
}
var name = 'Global'
person.sayHi() // Achilles

此时使用 person 上下文来引用函数,因此可以说函数被调用时 person 对象“拥有”或者“包含”它。隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象,在这个例子里,this 指向了person

有一点需要注意,对象属性链中只有最后一层会影响到调用位置:

javascript
function sayHi() {
  console.log(this.name)
}
var person2 = {
  name: 'Patroclus',
  sayHi: sayHi,
}
var person1 = {
  name: 'Achilles',
  friend: person2,
}
person1.friend.sayHi() // Patroclus

即使像 XX...XX.sayHi() 有很多层引用,影响调用位置的只有最后一层。

另一个需要注意的点是 隐式丢失。隐式绑定的函数会丢失绑定对象,也就是说会应用默认绑定,从而把 this 绑定到全局对象或者 undefined 上,取决于是否是严格模式。如:

javascript
function sayHi() {
  console.log(this.name)
}
var person = {
  name: 'Achilles',
  sayHi: sayHi,
}
var name = 'Global'
var Hi = person.sayHi
Hi() // Global

虽然 Hiperson.sayHi 的一个引用,但是实际上,它引用的是 sayHi 函数本身,与 person已经没有任何关系了,此时的 Hi() 其实是一个独立函数调用,因此应用了默认绑定。

除了上面这种丢失之外,隐式绑定的丢失也可以发生在回调函数中(事件回调也是其中一种):

javascript
function sayHi() {
  console.log(this.name)
}
function Hi(fn) {
  // fn 其实引用的是 sayHi
  fn() // <-- 调用位置!
}
var person = {
  name: 'Achilles',
  sayHi: sayHi,
}
var name = 'Global'
Hi(person.sayHi) // Global

参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值,fn 实际指向的是 sayHi ,和 person 没有任何关系了。

显式绑定

显式绑定,即通过 call、apply、bind的方式,显式的指定 this 所指向的对象。

三个方法使用方式各有不同:

函数名称语法参数返回值
callcall(thisArg)
call(thisArg, arg1, arg2, …argN);
thisArg:将要要绑定的 this 值。

arg1, …, argN(可选):函数的参数
使用指定的 this 值和参数调用函数后的结果
applyapply(thisArg);
apply(thisArg, argsArray)
thisArg:将要要绑定的 this 值。

argsArray(可选):一个类数组对象,用于指定调用 func 时的参数
使用指定的 this 值和参数调用函数的结果
Bindbind(thisArg)
bind(thisArg, arg1, arg2, …argN);
thisArg:在调用绑定函数时,作为 this 参数传入目标函数 func 的值

arg1, …, argN(可选):在调用 func 时,插入到传入绑定函数的参数前的参数
使用指定的 this 值和初始参数(如果提供)创建的给定函数的副本。

可以看出,callapply 的作用一样,只是传参方式不同。callapply 都会执行对应的函数,而 bind 方法不会(只返回一个绑定后的新函数)。

javascript
function sayHi() {
  console.log(this.name)
}
var person = {
  name: 'Achilles',
  sayHi: sayHi,
}
var name = 'Global'
sayHi.call(person) // Achilles

通过这种方式,明确地将 this 绑定在了 person 上。 另外,显式绑定同隐式绑定一样,也会发生 绑定丢失 的现象,这里就不再赘述了。

被忽略的 this

如果你把 null 或者 undefined 作为 this 的绑定对象传入 callapply或者 bind,这些值在调用时会被忽略,实际应用的是默认绑定规则:

javascript
function foo() {
  console.log(this.a)
}
var a = 2
foo.call(null) // 2

new 绑定

在前文 new 操作符 中,我们已经知道,当使用 new 来调用函数的时候,就会新对象绑定到这个函数的 this 上。

javascript
function Person(name) {
  this.name = name
}
var p = new Person('conyu')
console.log(p.name) // conyu

绑定优先级

我们知道了 this 有四种绑定规则,但是如果同时应用了多种规则,怎么办?

此时由绑定优先级来决定,这四种绑定的优先级为:

new 绑定 > 显式绑定 > 隐式绑定 > 默认绑定

如果有兴趣,可以写个 demo 测试一下。

箭头函数中的 this

ES6 中有一种特殊的函数类型:箭头函数。它和普通函数有一个很大的区别就在于:箭头函数没有自己的 this,而是根据外层(函数或者全局)作用域来决 定 this。

javascript
var name = 'Global'
var person = {
  name: 'Achilles',
  sayHi: function () {
    console.log(this.name)
  },
  hi: () => {
    console.log(this.name)
  },
}

person.sayHi() // Achilles
person.hi() // Global,因为箭头函数不绑定this,只能往上找,即全局的this。

思考题

  1. 以下代码的输出结果是?
javascript
function foo() {
  console.log(this.a)
}

function doFoo() {
  foo()
}

var obj = {
  a: 1,
  doFoo: doFoo,
}

var a = 2
obj.doFoo()
查看答案 👀

输出结果:

javascript
2

解析: 在执行 foo 的时候,执行环境就是 doFoo 函数,执行环境为全局。

  1. 以下代码的输出结果是?
javascript
var a = 10
var obj = {
  a: 20,
  say: () => {
    console.log(this.a)
  },
}
obj.say()

var anotherObj = { a: 30 }
obj.say.apply(anotherObj)
查看答案 👀

输出结果:

javascript
10
10

解析: 箭头函数不绑定 this

  1. 以下代码的输出结果是?
javascript
function a() {
  console.log(this)
}
a.call(null)
查看答案 👀

输出结果: Window 对象

解析: 根据 ECMAScript262 规范规定:如果第一个参数传入的对象调用者是 null 或者 undefinedcall 方法将把全局对象(浏览器上是 window 对象)作为 this 的值。所以,不管传入 null 还是 undefined,其 this 都是全局对象 window。所以,在浏览器上答案是输出 window 对象。

要注意的是,在严格模式中,nullundefined 不会被替换:

javascript
'use strict'

function a() {
  console.log(this)
}
a.call(null) // null
a.call(undefined) // undefined
  1. 以下代码的输出结果是?
javascript
var obj = {
  say: function () {
    var f1 = () => {
      console.log('1111', this)
    }
    f1()
  },
  pro: {
    getPro: () => {
      console.log(this)
    },
  },
}
var o = obj.say
o()
obj.say()
obj.pro.getPro()
查看答案 👀

输出结果:

javascript
1111 window对象
1111 obj对象
window对象

解析:

  1. o()o 是在全局执行的,而 f1 是箭头函数,它是没有绑定 this 的,它的 this 指向其父级的 this,其父级 say 方法的 this 指向的是全局作用域,所以会打印出 window
  2. obj.say(),谁调用 saysay 的 this 就指向谁,所以此时 this 指向的是 obj 对象;
  3. obj.pro.getPro(),我们知道,箭头函数时不绑定 this 的,getPro 处于 pro 中,而对象不构成单独的作用域,所以箭头的函数的 this 就指向了全局作用域 window
  1. 以下代码的输出结果是?
javascript
var myObject = {
  foo: 'bar',
  func: function () {
    var self = this
    console.log(this.foo)
    console.log(self.foo)
    ;(function () {
      console.log(this.foo)
      console.log(self.foo)
    })()
  },
}
myObject.func()
查看答案 👀

输出结果:

javascript
bar
bar
undefined
bar

解析:

  1. 首先 func 是由 myObject 调用的,this 指向 myObject。又因为 var self = this;所以 self 指向 myObject
  2. 这个立即执行匿名函数表达式是由 window 调用的,this 指向 window 。立即执行匿名函数的作用域处于 myObject.func 的作用域中,在这个作用域找不到 self 变量,沿着作用域链向上查找 self 变量,找到了指向 myObject 对象的 self
  1. 以下代码的输出结果是?
javascript
window.number = 2
var obj = {
  number: 3,
  db1: (function () {
    console.log(this)
    this.number *= 4
    return function () {
      console.log(this)
      this.number *= 5
    }
  })(),
}
var db1 = obj.db1
db1()
obj.db1()
console.log(obj.number)
console.log(window.number)
查看答案 👀

输出结果:

javascript
15
40

解析:

  1. 执行 db1() 时,this 指向全局作用域,所以 window.number * 4 = 8,然后执行匿名函数, 所以 window.number * 5 = 40
  2. 执行 obj.db1();时,this 指向 obj 对象,执行匿名函数,所以 obj.numer * 5 = 15
  1. 以下代码的输出结果是?
javascript
var length = 10
function fn() {
  console.log(this.length)
}

var obj = {
  length: 5,
  method: function (fn) {
    fn()
    arguments[0]()
  },
}

obj.method(fn, 1)
查看答案 👀

输出结果:

javascript
10
2

解析:

  1. 第一次执行 fn(),this 指向 window 对象,输出 10
  2. 第二次执行 arguments[0](),相当于 arguments 调用方法,this 指向 arguments,而这里传了两个参数,故输出 arguments 长度为 2
  1. 以下代码的输出结果是?
javascript
var a = 1
function printA() {
  console.log(this.a)
}
var obj = {
  a: 2,
  foo: printA,
  bar: function () {
    printA()
  },
}

obj.foo() // 2
obj.bar() // 1
var foo = obj.foo
foo() // 1
查看答案 👀

输出结果:

javascript
2
1
1

解析:

  1. obj.foo()foo 的 this 指向 obj 对象,所以 a 会输出 2
  2. obj.bar()printAbar 方法中执行,所以此时 printA 的 this 指向的是 window,所以会输出 1
  3. foo()foo 是在全局对象中执行的,所以其 this 指向的是 window,所以会输出 1
  1. 以下代码的输出结果是?
javascript
var x = 3
var y = 4
var obj = {
  x: 1,
  y: 6,
  getX: function () {
    var x = 5
    return (function () {
      return this.x
    })()
  },
  getY: function () {
    var y = 7
    return this.y
  },
}
console.log(obj.getX())
console.log(obj.getY())
查看答案 👀

输出结果:

javascript
3
6

解析:

  1. 我们知道,匿名函数的 this 是指向全局对象的,所以 this 指向 window,obj.getX() 会打印出 3
  2. getY 是由 obj 调用的,所以其 this 指向的是 obj 对象,会打印出 6
  1. 以下代码的输出结果是?
javascript
var a = 10
var obt = {
  a: 20,
  fn: function () {
    var a = 30
    console.log(this.a)
  },
}
obt.fn()
obt.fn.call()
obt.fn()
查看答案 👀

输出结果:

javascript
20
10
20

解析:

  1. obt.fn()fn 是由 obt 调用的,所以其 this 指向 obt 对象,会打印出 20

  2. obt.fn.call(),这里 call 没有参数,就表示 null,我们知道如果 call 的参数为 undefinednull,那么 this 就会指向全局对象 this,所以会打印出 10

  3. (obt.fn)(), 这里给表达式加了括号,而括号的作用是改变表达式的运算顺序,而在这里加与不加括号并无影响;相当于 obt.fn(),所以会打印出 20; :::

  4. 以下代码的输出结果是?

javascript
function a(xx) {
  this.x = xx
  return this
}
var x = a(5)
var y = a(6)

console.log(x.x)
console.log(y.x)
查看答案 👀

输出结果:

javascript
undefined
6

解析:

  1. 最关键的就是 var x = a(5),函数 a 是在全局作用域调用,所以函数内部的 this 指向 window 对象。所以 this.x = 5 就相当于:window.x = 5。之后 return this,也就是说 var x = a(5) 中的 x 变量的值是 window,这里的 x 将函数内部的 x 的值覆盖了。然后执行console.log(x.x), 也就是 console.log(window.x),而 window 对象中没有 x 属性,所以会输出 undefined
  2. 当指向 y.x 时,会给全局变量中的 x 赋值为 6,所以会打印出 6
  1. 以下代码的输出结果是?
javascript
function foo(something) {
  this.a = something
}

var obj1 = {
  foo: foo,
}

var obj2 = {}

obj1.foo(2)
console.log(obj1.a)

obj1.foo.call(obj2, 3)
console.log(obj2.a)

var bar = new obj1.foo(4)
console.log(obj1.a)
console.log(bar.a)
查看答案 👀

输出结果:

javascript
2
3
2
4

解析:

  1. 首先执行 obj1.foo(2); 会在 obj 中添加 a 属性,其值为 2。之后执行 obj1.aa 是右 obj1 调用的,所以 this 指向 obj,打印出 2
  2. 执行 obj1.foo.call(obj2, 3) 时,会将 foo 的 this 指向 obj2,后面就和上面一样了,所以会打印出 3
  3. obj1.a 会打印出 2
  4. 最后就是考察 this 绑定的优先级了,new 绑定是比隐式绑定优先级高,所以会输出 4