关于this

关于this

this 实际上是在函数被调用时发生的绑定, 它指向什么完全取决于函数在哪里被调用(调用位置)

this 是在运行时进行绑定的, 并不是在编写时绑定, 它的上下文取决于函数调用时的各种条件。 this 的绑定和函数声明的位置没有任何关系, 只取决于函数的调用方式(调用位置)。

当一个函数被调用时, 会创建一个活动记录(有时候也称为执行上下文)。 这个记录会包含函数在哪里被调用(调用栈)、 函数的调用方法、 传入的参数等信息。 this 就是记录的其中一个属性, 会在函数执行的过程中用到

误区

有两种常见的对于 this 的解释, 但是它们都是错误的。

指向自身

人们很容易把 this 理解成指向函数自身

function foo(num) {
  console.log('foo: ' + num)
  // 记录 foo 被调用的次数
  this.count++
}

foo.count = 0
var i
for (i = 0; i < 10; i++) {
  if (i > 5) {
    foo(i)
  }
}

// foo: 6
// foo: 7
// foo: 8
// foo: 9
// foo 被调用了多少次?
console.log(foo.count) // 0 -- WTF?

它的作用域

第二种常见的误解是, this 指向函数的作用域。 这个问题有点复杂, 因为在某种情况下它是正确的, 但是在其他情况下它却是错误的。

需要明确的是, this 在任何情况下都不指向函数的词法作用域。 在 JavaScript 内部, 作用域确实和对象类似可见的标识符都是它的属性。 但是作用域“对象” 无法通过 JavaScript代码访问, 它存在于 JavaScript 引擎内部

function foo() {
  var a = 2
  this.bar()
}
function bar() {
  console.log(this.a)
}
foo() // ReferenceError: a is not defined

调用位置

在理解 this 的绑定过程之前, 首先要理解调用位置: 调用位置就是函数在代码中被调用的位置(而不是声明的位置)。 只有仔细分析调用位置才能回答这个问题: 这个 this 到底引用的是什么?

通常来说, 寻找调用位置就是寻找“函数被调用的位置”。

最重要的是要分析调用栈(就是为了到达当前执行位置所调用的所有函数)。

我们关心的调用位置就在当前正在执行的函数前一个调用中

function baz() {
  // 当前调用栈是: baz
  // 因此, 当前调用位置是全局作用域
  console.log('baz')
  bar() // <-- bar 的调用位置
}
function bar() {
  // 当前调用栈是 baz -> bar
  // 因此, 当前调用位置在 baz 中
  console.log('bar')
  foo() // <-- foo 的调用位置
}
function foo() {
  // 当前调用栈是 baz -> bar -> foo
  // 因此, 当前调用位置在 bar 中
  console.log('foo')
}
baz() // <-- baz 的调用位置

注意我们是如何(从调用栈中) 分析出真正的调用位置的, 因为它决定了 this 的绑定

绑定规则

在函数的执行过程中调用位置如何决定 this 的绑定对象?

你必须找到调用位置, 然后判断需要应用下面四条规则中的哪一条。

默认绑定

首先要介绍的是最常用的函数调用类型: 独立函数调用

可以把这条规则看作是无法应用其他规则时的默认规则

function foo() {
  'use strict'
  console.log(this.a)
}
var a = 2
foo() // TypeError: this is undefined

函数调用时应用了 this 的默认绑定, 因此 this 指向全局对象。

通过分析调用位置来看看 foo() 是如何调用的。 在代码中, foo() 是直接使用不带任何修饰的函数引用进行调用的, 因此只能使用默认绑定, 无法应用其他规则。

如果使用严格模式(strict mode), 那么全局对象将无法使用默认绑定, 因此 this 会绑定到 undefined:

隐式绑定

另一条需要考虑的规则是调用位置是否有上下文对象, 或者说是否被某个对象拥有或者包含

function foo() {
  console.log(this.a)
}
var obj = {
  a: 2,
  foo: foo,
}
obj.foo() // 2

首先需要注意的是 foo() 的声明方式, 及其之后是如何被当作引用属性添加到 obj 中的。
但是无论是直接在 obj 中定义还是先定义再添加为引用属性, 这个函数严格来说都不属于obj 对象。

然而, 调用位置会使用 obj 上下文来引用函数, 因此你可以说函数被调用时 obj 对象“拥有” 或者“包含” 它。

无论你如何称呼这个模式, 当 foo() 被调用时, 它的落脚点确实指向 obj 对象

函数引用有上下文对象时, 隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。 因为调用 foo() 时 this 被绑定到 obj, 因此 this.a 和 obj.a 是一样的。

对象属性引用链中只有最顶层或者说最后一层影响调用位置

function foo() {
  console.log(this.a)
}
var obj2 = {
  a: 42,
  foo: foo,
}
var obj1 = {
  a: 2,
  obj2: obj2,
}
obj1.obj2.foo() // 42

隐式丢失

一个最常见的 this 绑定问题就是被隐式绑定的函数会丢失绑定对象, 也就是说它会应用默认绑定, 从而把 this 绑定到全局对象或者 undefined 上, 取决于是否是严格模式。

function foo() {
  console.log(this.a)
}
var obj = {
  a: 2,
  foo: foo,
}
var bar = obj.foo // 函数别名!
var a = 'oops, global' // a 是全局对象的属性
bar() // "oops, global"

虽然 bar 是 obj.foo 的一个引用, 但是实际上, 它引用的是 foo 函数本身, 因此此时的bar() 其实是一个不带任何修饰的函数调用, 因此应用了默认绑定。

一种更微妙、 更常见并且更出乎意料的情况发生在传入回调函数时:

function foo() {
  console.log(this.a)
}
function doFoo(fn) {
  // fn 其实引用的是 foo
  fn() // <-- 调用位置!
}
var obj = {
  a: 2,
  foo: foo,
}
var a = 'oops, global' // a 是全局对象的属性
doFoo(obj.foo) // "oops, global"

就像我们看到的那样, 回调函数丢失 this 绑定是非常常见的

显示绑定

在分析隐式绑定时, 我们必须在一个对象内部包含一个指向函数的属性, 并通过这个属性间接引用函数, 从而把 this 间接(隐式) 绑定到这个对象上。

那么如果我们不想在对象内部包含函数引用, 而想在某个对象上强制调用函数?

使用函数的 call(..) 和apply(..) 方法。

它们的第一个参数是一个对象, 它们会把这个对象绑定到this, 接着在调用函数时指定这个 this。

因为你可以直接指定 this 的绑定对象, 因此我们称之为显式绑定

new绑定

首先我们重新定义一下 JavaScript 中的“构造函数”。 在 JavaScript 中, 构造函数只是一些使用 new 操作符时被调用的函数。 它们并不会属于某个类, 也不会实例化一个类。 实际上,它们甚至都不能说是一种特殊的函数类型, 它们只是被 new 操作符调用的普通函数而已

这里有一个重要但是非常细微的区别: 实际上并不存在所谓的“构造函数”, 只有对于函数的“构造调用”

使用 new 来调用函数, 或者说发生构造函数调用时, 会自动执行下面的操作。

  1. 创建(或者说构造) 一个全新的对象。
  2. 这个新对象会被执行 [[ 原型 ]] 连接。
  3. 这个新对象会绑定到函数调用的 this。
  4. 如果函数没有返回其他对象, 那么 new 表达式中的函数调用会自动返回这个新对象。

function foo(a) {
  this.a = a
}
var bar = new foo(2)
console.log(bar.a) // 2

使用 new 来调用 foo(..) 时, 我们会构造一个新对象并把它绑定到 foo(..) 调用中的 this上。 new 是最后一种可以影响函数调用时 this 绑定行为的方法, 我们称之为 new 绑定

优先级

你需要做的就是找到函数的调用位置并判断应当应用哪条规则。 但是, 如果某个调用位置可以应用多条规则该怎么办?

判断this

可以按照下面的顺序来进行判断:

  1. 函数是否在 new 中调用(new 绑定) ? 如果是的话 this 绑定的是新创建的对象。
    var bar = new foo()

  2. 函数是否通过 call、 apply(显式绑定) 或者硬绑定调用? 如果是的话, this 绑定的是指定的对象。
    var bar = foo.call(obj2)

  3. 函数是否在某个上下文对象中调用(隐式绑定) ? 如果是的话, this 绑定的是那个上下文对象。
    var bar = obj1.foo()

  4. 如果都不是的话, 使用默认绑定。 如果在严格模式下, 就绑定到 undefined, 否则绑定到全局对象。

var bar = foo()

对于正常的函数调用来说, 理解了这些知识你就可以明白 this 的绑定原理了。不过……凡事总有例外

绑定例外

在某些场景下 this 的绑定行为会出乎意料, 你认为应当应用其他绑定规则时, 实际上应用的可能是默认绑定规则。

被忽略的this

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

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

间接引用

另一个需要注意的是, 你有可能(有意或者无意地) 创建一个函数的“间接引用”, 在这种情况下, 调用这个函数会应用默认绑定规则。

function foo() {
  console.log(this.a)
}
var a = 2
var o = { a: 3, foo: foo }
var p = { a: 4 }
o.foo(); // 3
(p.foo = o.foo)(); // 2

赋值表达式 p.foo = o.foo 的返回值是目标函数的引用, 因此调用位置是 foo() 而不是p.foo() 或者 o.foo()。 根据我们之前说过的, 这里会应用默认绑定。

注意: 对于默认绑定来说, 决定 this 绑定对象并不是调用位置是否处于严格模式, 而是函数体是否处于严格模式如果函数体处于严格模式, this 会被绑定到 undefined, 否则this 会被绑定到全局对象。

this词法

ES6 中介绍了一种无法使用这些规则的特殊函数类型: 箭头函数

箭头函数不使用 this 的四种标准规则, 而是根据外层(函数或者全局) 作用域来决定 this

我们来看看箭头函数的词法作用域:

function foo() {
  // 返回一个箭头函数
  return (a) => {
    //this 继承自 foo()
    console.log(this.a)
  }
}
var obj1 = {
  a: 2,
}
var obj2 = {
  a: 3,
}
var bar = foo.call(obj1)
bar.call(obj2) // 2, 不是 3 !

foo() 内部创建的箭头函数会捕获调用时 foo() 的 this。 由于 foo() 的 this 绑定到 obj1,bar( 引用箭头函数) 的 this 也会绑定到 obj1, 箭头函数的绑定无法被修改。(new 也不行! )

箭头函数最常用于回调函数中, 例如事件处理器或者定时器:

function foo() {
  setTimeout(() => {
    // 这里的 this 在此法上继承自 foo()
    console.log(this.a)
  }, 100)
}
var obj = {
  a: 2,
}
foo.call(obj) // 2

箭头函数可以像 bind(..) 一样确保函数的 this 被绑定到指定对象, 此外, 其重要性还体现在它用更常见的词法作用域取代了传统的 this 机制。

总结

如果要判断一个运行中函数的 this 绑定, 就需要找到这个函数的直接调用位置。 找到之后就可以顺序应用下面这四条规则来判断 this 的绑定对象。

  1. 由 new 调用? 绑定到新创建的对象。
  2. 由 call 或者 apply(或者 bind) 调用? 绑定到指定的对象。
  3. 由上下文对象调用? 绑定到那个上下文对象。
  4. 默认: 在严格模式下绑定到 undefined, 否则绑定到全局对象。

ES6 中的箭头函数并不会使用四条标准的绑定规则, 而是根据当前的词法作用域来决定this, 具体来说, 箭头函数会继承外层函数调用的 this 绑定(无论 this 绑定到什么)。 这其实和 ES6 之前代码中的 self = this 机制一样。

posted @ 2025-07-11 03:19  CD、小月  阅读(14)  评论(0)    收藏  举报