跳到主要内容
预计阅读 40 分钟

Javascript篇 | this指向与执行上下文

前言

「this指向」是前端面试的高频考点,也是实际开发中的常见陷阱。很多同学都有这样的体验:

  • 写代码时感觉没问题,运行起来this就指错了
  • 面试时被问到this,脑子里一团浆糊
  • 箭头函数和普通函数的this总是搞混

今天我们就用一道经典面试题,彻底搞懂JavaScript中this的所有玩法

诊断自测

在开始之前,试着回答以下问题,检测你当前对 this 的理解程度:

1. 以下代码输出什么?为什么?

var x = 10;
const obj = {
  x: 20,
  getX: function() {
    return this.x;
  }
};

const retrieveX = obj.getX;
console.log(retrieveX());

2. 下面两个 console.log 分别输出什么?

function Timer() {
  this.seconds = 0;

  setInterval(function() {
    this.seconds++;
    console.log(this.seconds); // A
  }, 1000);
}

const t = new Timer();

3. 以下代码中,this.color 输出什么?

const car = {
  color: 'red',
  getColor: () => {
    return this.color;
  }
};

console.log(car.getColor());
点击查看答案

第1题: 输出 10(浏览器环境)或报错(严格模式)。retrieveX 是一个独立调用的函数引用,发生了隐式绑定丢失,this 指向全局对象。

第2题: A 处不断输出 NaNsetInterval 的回调是普通函数且被独立调用,this 指向全局对象(window),window.secondsundefinedundefined + 1 = NaN

第3题: 输出 undefined。箭头函数没有自己的 this,它继承定义时外层的执行上下文。对象字面量 {} 不构成执行上下文,所以 this 指向全局对象,而全局对象上没有 color 属性。

如果三道题都答对了,说明你基础不错!继续阅读可以查漏补缺。如果有答错的,别担心,读完本文你就能彻底搞懂。

先来个灵魂拷问👇

var name = 'window';

function Person(name) {
  this.name = name;
  this.show1 = function() {
    console.log(this.name);
  };
  this.show2 = () => {
    console.log(this.name);
  };
  this.show3 = function() {
    setTimeout(function() {
      console.log(this.name);
    }, 0);
  };
  this.show4 = function() {
    setTimeout(() => {
      console.log(this.name);
    }, 0);
  };
}

var personA = new Person('personA');
var personB = new Person('personB');

// 请写出以下代码的输出结果
personA.show1();
personA.show1.call(personB);

personA.show2();
personA.show2.call(personB);

personA.show3();
personA.show3.call(personB);

personA.show4();
personA.show4.call(personB);

看完之后是不是有点懵?别急,读完这篇文章,你不仅能轻松解答这道题,还能彻底搞懂JavaScript中this的所有玩法。

从规范说起:this到底是什么?

要想真正理解this,我们得先从ES规范说起。

在ECMAScript规范中,this是执行上下文的一个属性。每当代码执行进入一个新的执行上下文时,this就会被重新绑定。

听起来有点抽象?我们换个说法:

this的值不是在函数定义时确定的,而是在函数被调用时动态绑定的。

这就解释了为什么同一个函数,在不同的调用场景下,this会指向不同的对象。

执行上下文是什么?

执行上下文(Execution Context)是JavaScript代码运行的环境。主要分为两种:

  • 全局执行上下文:代码首次运行时创建,只有一个
  • 函数执行上下文:每次函数调用时创建,可以有多个

每个执行上下文都有自己的this绑定,这就是我们要研究的重点。

全局上下文中的this

这是最简单的场景。

console.log(this); // 浏览器环境: window

在全局上下文中:

  • 浏览器环境:this指向window对象
  • Node.js环境:this指向global对象

没什么好说的,记住就行。

函数上下文中的this:五种绑定规则

重头戏来了!函数中的this绑定有五种情况,我们一个一个来看。

默认绑定

当函数独立调用时,就会触发默认绑定。

var name = 'window';

function sayName() {
  console.log(this.name);
}

sayName(); // 'window'

规则:

  • 非严格模式:this指向全局对象(window)
  • 严格模式:this为undefined
'use strict';

function sayName() {
  console.log(this); // undefined
}

sayName();

💡 记忆技巧:直接调用函数,没有任何”主人”,this就只能找全局对象了。

隐式绑定

当函数作为对象的方法被调用时,this指向调用该方法的对象。

const person = {
  name: 'Alice',
  sayName: function() {
    console.log(this.name);
  }
};

person.sayName(); // 'Alice'

看起来很简单?但有个陷阱:

const person = {
  name: 'Alice',
  sayName: function() {
    console.log(this.name);
  }
};

const fn = person.sayName;
fn(); // 'window' 或 undefined(严格模式),因为这个时候的代码执行上下文变成了全局!

为什么?

因为fn只是拿到了函数的引用,调用fn()时已经不是通过person.sayName()的形式了,变成了独立调用,触发的是默认绑定。

⚠️ 隐式绑定丢失是面试高频考点!记住:关键看调用时的形式,而不是定义时。

显式绑定:call / apply / bind

有时候我们希望手动指定this的值,这时就要用到callapplybind

call

function greet() {
  console.log(`Hello, ${this.name}`);
}

const person1 = { name: 'Alice' };
const person2 = { name: 'Bob' };

greet.call(person1); // 'Hello, Alice'
greet.call(person2); // 'Hello, Bob'

语法: func.call(thisArg, arg1, arg2, ...)

apply

function greet(greeting, punctuation) {
  console.log(`${greeting}, ${this.name}${punctuation}`);
}

const person = { name: 'Alice' };

greet.apply(person, ['Hello', '!']); // 'Hello, Alice!'

语法: func.apply(thisArg, [argsArray])

bind

function greet() {
  console.log(`Hello, ${this.name}`);
}

const person = { name: 'Alice' };

const boundGreet = greet.bind(person);
boundGreet(); // 'Hello, Alice'

语法: const boundFunc = func.bind(thisArg, arg1, arg2, ...)

三者对比

方法是否立即执行参数传递方式返回值
call逐个传递函数执行结果
apply数组形式函数执行结果
bind逐个传递新函数

💡 记忆口诀:call和apply立即执行,bind返回新函数;call逐个传参,apply数组传参。

new绑定

使用new调用构造函数时,this会绑定到新创建的对象上。

function Person(name) {
  this.name = name;
  this.sayName = function() {
    console.log(this.name);
  };
}

const person = new Person('Alice');
person.sayName(); // 'Alice'

💡 核心:new会创建新对象,并让this指向它。

箭头函数:特立独行的存在

箭头函数是ES6引入的新特性,它的this绑定规则完全不同

核心规则:箭头函数没有自己的this,它的this继承自定义时所在的外层执行上下文。

const obj = {
  name: 'object',
  regularFunc: function() {
    console.log('regularFunc:', this.name);
  },
  arrowFunc: () => {
    console.log('arrowFunc:', this.name);
  }
};

obj.regularFunc(); // 'regularFunc: object'
obj.arrowFunc();   // 'arrowFunc: undefined' (继承自全局执行上下文)

更重要的是:箭头函数的this一旦确定就无法改变

const obj = {
  name: 'object',
  arrowFunc: () => {
    console.log(this.name);
  }
};

const anotherObj = { name: 'another' };

obj.arrowFunc.call(anotherObj); // 依然是undefined,无法改变

让我们再看一个更清晰的例子:

function Person(name) {
  this.name = name;

  // 普通函数,this由调用时决定
  this.regularFunc = function() {
    console.log(this.name);
  };

  // 箭头函数,this继承自Person构造函数的this
  this.arrowFunc = () => {
    console.log(this.name);
  };
}

const person = new Person('Alice');
person.regularFunc(); // 'Alice'
person.arrowFunc();   // 'Alice'

const anotherObj = { name: 'Bob' };
person.regularFunc.call(anotherObj); // 'Bob' - 可以改变
person.arrowFunc.call(anotherObj);   // 'Alice' - 无法改变

⚠️ 重点:箭头函数的this在定义时就已经确定,它会捕获外层执行上下文的this,之后无论如何调用都不会改变。

绑定优先级

当多种绑定规则同时出现时,优先级如何?

优先级从高到低:

  1. new绑定
  2. 显式绑定(call/apply/bind)
  3. 隐式绑定
  4. 默认绑定
function foo() {
  console.log(this.name);
}

const obj1 = { name: 'obj1', foo: foo };
const obj2 = { name: 'obj2' };

// 隐式绑定 vs 显式绑定
obj1.foo.call(obj2); // 'obj2' - 显式绑定优先

// 显式绑定 vs new绑定
const boundFoo = foo.bind(obj1);
const instance = new boundFoo(); // undefined - new绑定优先

特殊情况: 箭头函数不参与优先级比较,因为它的this完全由定义时的外层执行上下文决定。

综合实战:解答开头的面试题

现在我们有了完整的知识体系,可以轻松解答开头的题目了。

var name = 'window';

function Person(name) {
  this.name = name;
  this.show1 = function() {
    console.log(this.name);
  };
  this.show2 = () => {
    console.log(this.name);
  };
  this.show3 = function() {
    setTimeout(function() {
      console.log(this.name);
    }, 0);
  };
  this.show4 = function() {
    setTimeout(() => {
      console.log(this.name);
    }, 0);
  };
}

var personA = new Person('personA');
var personB = new Person('personB');

show1

personA.show1();              // 输出: 'personA'
personA.show1.call(personB);  // 输出: 'personB'

分析:

  • show1是普通函数
  • 第一次调用:隐式绑定,this指向personA
  • 第二次调用:显式绑定,this被改为personB

show2

personA.show2();              // 输出: 'personA'
personA.show2.call(personB);  // 输出: 'personA'

分析:

  • show2是箭头函数,定义在Person构造函数内
  • this继承自Person构造函数的执行上下文,在new Person('personA')时this绑定为personA
  • 箭头函数的this无法通过call改变,所以两次都是'personA'

show3

personA.show3();              // 输出: 'window'
personA.show3.call(personB);  // 输出: 'window'

分析:

  • show3内部的setTimeout使用了普通函数作为回调
  • setTimeout的回调函数是独立调用的,触发默认绑定
  • 非严格模式下,this指向window
  • 即使用call改变了show3的this,也不影响内部setTimeout回调的this

show4

personA.show4();              // 输出: 'personA'
personA.show4.call(personB);  // 输出: 'personB'

分析:

  • show4内部的setTimeout使用了箭头函数作为回调
  • 箭头函数的this继承自外层(show4函数的执行上下文)
  • 第一次:show4的this是personA,箭头函数继承,输出'personA'
  • 第二次:call改变了show4的this为personB,箭头函数继承,输出'personB'

Bonus1: 类中的 this

class C {
  instanceField = this;
  static staticField = this;
}

const c = new C();
console.log(c.instanceField === c); // true
console.log(C.staticField === C); // true

前面说了这么多,为什么没有提到类中的 this 呢?你可能注意到,类中的 this 语义好像和前面讨论的都不太一样。那是不是应该也存在一种”类上下文”呢?

其实不需要。类本质上是一种特殊的”函数”,它使用的仍然是我们前面讨论的函数上下文。类只是构造函数的一层语法糖包装,所以会让你感觉类中的 this 好像不一样了。

让我们简单验证一下类确实是函数:

class C {}
console.log(typeof C); // "function"
console.log(C.prototype.constructor === C); // true

// 类的实例字段初始化,等价于在构造函数中赋值
class C1 {
  instanceField = this;
}

// 等价于:
function C2() {
  this.instanceField = this;
}

当你写 instanceField = this 时,这个初始化器会在构造函数执行时运行,此时的 this 遵循 new 绑定规则,指向新创建的实例。

static staticField = this 则是在类定义时执行,此时 this 指向类构造函数本身。

所以类中的 this 并没有引入新的绑定规则,它只是在不同位置应用了我们已经熟悉的函数上下文中的 this 绑定机制。

Bonus2:eval中的this

掌握了上面的this绑定知识,可能对于通过面试已经够用了。但如果想惊艳到面试官,那么这个冷门的this绑定也必须学!

eval是JavaScript中一个比较特殊的存在,它能够执行字符串形式的代码。而在eval中,this的绑定也有特殊的规则。

直接调用 vs 间接调用

eval有两种调用方式,它们对this的影响完全不同:

const obj = {
  name: 'object',
  test: function() {
    // 直接调用eval
    eval('console.log(this.name)'); // 'object'

    // 间接调用eval
    (0, eval)('console.log(this)'); // 'window'
  }
};

obj.test();

看到区别了吗?同样是调用eval,结果却天差地别。

什么是直接调用?

所谓直接调用,就是直接使用eval(...)的形式。这种情况下,eval中的代码会继承当前执行上下文的this

function foo() {
  eval('console.log(this)');
}

const obj = { name: 'object' };
foo.call(obj); // 输出: obj对象

你可以把直接调用的eval理解为:这段字符串代码就像是直接写在当前位置一样,它”看到”的this就是外层的this。

什么是间接调用?

间接调用是指通过其他方式调用eval,常见的有:

// 方式1:通过逗号运算符
(0, eval)('console.log(this)');

// 方式2:赋值给变量
const myEval = eval;
myEval('console.log(this)');

// 方式3:通过window调用
window.eval('console.log(this)');

间接调用时,eval中的代码会在全局执行上下文中执行,因此this指向全局对象。

const obj = {
  name: 'object',
  directEval: function() {
    eval('console.log(this.name)'); // 'object'
  },
  indirectEval: function() {
    (0, eval)('console.log(this.name)'); // undefined
  }
};

obj.directEval();
obj.indirectEval();

为什么会有这样的设计?

这其实是ECMAScript规范的一个设计细节。直接调用eval时,它需要访问当前的执行上下文(包括变量、this等);而间接调用时,为了安全性和一致性,规范规定它应该在全局上下文中执行。

再看一个更复杂的例子:

var name = 'global';

const obj = {
  name: 'object',
  test: function() {
    var name = 'local';

    // 直接调用:能访问局部变量和当前this
    eval('console.log(name, this.name)'); // 'local', 'object'

    // 间接调用:只能访问全局,this指向window
    (0, eval)('console.log(name, this.name)'); // 'global', 'global'
  }
};

obj.test();

看出来了吗?直接调用的eval就像透明的一样,它完全融入了当前的执行环境;而间接调用则是独立的,始终在全局环境中执行。

常见误区

学完了这么多规则,我们来看看大家在理解 this 时最容易踩的坑,帮你避免”以为自己懂了”的尴尬。

误区一:this 指向函数本身

很多从其他语言转来的同学,看到 this 这个词会本能地以为它指向函数本身。毕竟 “this” 嘛,“这个函数”,听起来很合理?

function counter() {
  this.count++;
}

counter.count = 0;
counter();
counter();

console.log(counter.count); // 0,没有增加!

实际上 counter() 是独立调用,this 指向的是全局对象,this.count++ 操作的是 window.count,而不是 counter.count。this 永远不会指向函数自身,除非你显式地用 call(counter) 来绑定。

误区二:对象方法中嵌套的函数,this 会指向该对象

这是隐式绑定丢失的经典场景。很多同学以为只要函数写在对象的方法内部,this 就会指向那个对象。

const team = {
  name: 'Frontend',
  members: ['Alice', 'Bob'],
  printMembers: function() {
    this.members.forEach(function(member) {
      console.log(`${member} belongs to ${this.name}`);
    });
  }
};

team.printMembers();
// Alice belongs to undefined
// Bob belongs to undefined

forEach 里的回调函数是独立调用的,触发默认绑定,this 指向全局对象。要解决这个问题,可以用箭头函数、bind,或者在外层缓存 const self = this

误区三:箭头函数的 this 指向定义时所在的对象

很多人把”定义时确定”误解为”定义在哪个对象里,this 就指向哪个对象”。实际上箭头函数继承的是外层执行上下文的 this,而对象字面量 {} 不会创建新的执行上下文。

const config = {
  env: 'production',
  getEnv: () => {
    return this.env;
  }
};

console.log(config.getEnv()); // undefined,不是 'production'

这里箭头函数定义在对象字面量内部,但对象字面量不是执行上下文,所以 this 继承的是外层(全局)的 this。只有函数调用(包括构造函数/类)才会创建新的执行上下文。

误区四:bind 之后还能再被 call/apply 改变 this

一旦通过 bind 绑定了 this,后续的 callapply 甚至再次 bind 都无法覆盖第一次的绑定。

function sayHi() {
  console.log(`Hi, ${this.name}`);
}

const boundToAlice = sayHi.bind({ name: 'Alice' });
boundToAlice.call({ name: 'Bob' }); // 'Hi, Alice',不是 'Hi, Bob'

const rebind = boundToAlice.bind({ name: 'Charlie' });
rebind(); // 'Hi, Alice',依然是 Alice

bind 产生的绑定是”硬绑定”,一旦确定就不会被后续的显式绑定所覆盖。这个特性在面试中经常被用来出题。

总结

核心要点

优先级: new绑定 > 显式绑定 > 隐式绑定 > 默认绑定

核心原则:

  1. this不是在编写时绑定,而是在运行时绑定
  2. 关键看调用位置和调用方式
  3. 箭头函数是特例,它的this在定义时就确定了,继承自外层执行上下文
  4. eval的直接调用和间接调用对this的影响不同

记忆技巧

  • 默认绑定:独立调用,找全局
  • 隐式绑定:谁调用指向谁,但容易丢失
  • 显式绑定:手动指定,强制绑定
  • new绑定:创建新对象,指向新对象
  • 箭头函数:定义时确定,继承外层,无法改变

掌握了这些,无论遇到多复杂的this问题,你都能游刃有余地解决!

练习挑战

光看不练假把式,来试试这几道题,检验一下学习成果。

挑战一:基础(⭐)

请写出以下代码的输出结果:

var age = 100;

const user = {
  age: 25,
  getAge: function() {
    return this.age;
  },
  getAgeArrow: () => {
    return this.age;
  }
};

const { getAge, getAgeArrow } = user;

console.log(user.getAge());     // A
console.log(getAge());           // B
console.log(user.getAgeArrow()); // C
console.log(getAgeArrow());      // D
答案与解析
  • A: 25 — 隐式绑定,user.getAge() 中 this 指向 user。
  • B: 100 — 解构赋值后独立调用,隐式绑定丢失,触发默认绑定,this 指向全局对象,window.age = 100
  • C: 100 — 箭头函数的 this 继承自定义时的外层执行上下文(全局),对象字面量不创建执行上下文,所以 this 是 window。
  • D: 100 — 同 C,箭头函数的 this 不受调用方式影响,始终是全局对象。

挑战二:进阶(⭐⭐)

请写出以下代码的输出结果:

const factory = {
  label: 'Factory',
  create: function() {
    return {
      label: 'Product',
      getLabel: function() {
        console.log(this.label);
      },
      getLabelArrow: () => {
        console.log(this.label);
      }
    };
  }
};

const product = factory.create();

product.getLabel();            // A
product.getLabelArrow();       // B

const product2 = factory.create.call({ label: 'Custom' });
product2.getLabelArrow();      // C
答案与解析
  • A: 'Product'product.getLabel() 是隐式绑定,this 指向 product 对象,其 label 是 'Product'
  • B: 'Factory'getLabelArrow 是箭头函数,定义在 create 函数的执行上下文中。factory.create() 调用时 this 是 factory(隐式绑定),所以箭头函数继承的 this 是 factory,输出 'Factory'
  • C: 'Custom'factory.create.call({ label: 'Custom' }) 通过 call 将 create 的 this 改为 { label: 'Custom' }。箭头函数继承的就是这个被改变的 this,输出 'Custom'

这道题的关键在于理解:箭头函数继承的是外层函数调用时的 this,而不是外层对象。如果外层函数的 this 被 call/apply 改变了,箭头函数继承的也是改变后的值。

挑战三:综合(⭐⭐⭐)

请写出以下代码的输出顺序和结果:

var id = 'Global';

function Article(id) {
  this.id = id;
}

Article.prototype.getId = function() {
  return this.id;
};

Article.prototype.getIdLater = function() {
  setTimeout(function() {
    console.log('A:', this.id);
  }, 0);

  setTimeout(() => {
    console.log('B:', this.id);
  }, 0);

  const boundFn = function() {
    console.log('C:', this.id);
  }.bind(this);
  setTimeout(boundFn, 0);
};

const article = new Article('Article-1');
const getId = article.getId;

console.log('D:', article.getId());
console.log('E:', getId());

article.getIdLater();
答案与解析

同步输出(按代码顺序):

  • D: 'Article-1'article.getId() 隐式绑定,this 是 article。
  • E: 'Global'getId 是独立调用,隐式绑定丢失,this 指向全局对象。

异步输出(setTimeout 回调,按注册顺序):

  • A: 'Global' — setTimeout 的普通函数回调独立调用,默认绑定,this 是全局对象。
  • B: 'Article-1' — 箭头函数继承外层 getIdLater 的 this,而 article.getIdLater() 是隐式绑定,this 是 article。
  • C: 'Article-1' — 通过 bind 硬绑定了 getIdLater 的 this(即 article),所以输出 article 的 id。

完整输出顺序:

D: Article-1
E: Global
A: Global
B: Article-1
C: Article-1

这道题综合考察了隐式绑定、隐式绑定丢失、默认绑定、箭头函数继承、bind 硬绑定,以及同步异步的执行顺序。如果你全部答对了,说明你对 this 的理解已经非常扎实了!

自我检测

读完本章后,确认你能回答以下问题:

  • 能说出 this 的五种绑定规则(默认绑定、隐式绑定、显式绑定、new 绑定、箭头函数)
  • 能说出绑定的优先级顺序:new > 显式 > 隐式 > 默认
  • 能解释什么是”隐式绑定丢失”,以及它在什么场景下会发生
  • 能解释为什么箭头函数的 this 无法被 call/apply/bind 改变
  • 理解对象字面量 {} 不会创建执行上下文,所以箭头函数不会把 this 绑定到对象本身
  • 能区分 call、apply、bind 的区别(是否立即执行、参数传递方式)
  • 能解释 bind 产生的硬绑定为什么无法被后续的 call/apply 覆盖
  • 了解 eval 直接调用和间接调用对 this 的不同影响
  • 能解释类中的 this 为什么本质上还是函数上下文的 this

购买课程解锁全部内容

大厂前端面试通关:71 篇构建完整知识体系

¥89.90