鲁斯前端布鲁斯前端

文章中英模式

布鲁斯前端JS面试题目 - JavaScript this 关键字深入解析

深入讲解 JavaScript 中 this 关键字的运作原理、绑定规则、箭头函数特性,以及 call、apply、bind 方法的使用场景与最佳实践。

影片縮圖

懒得看文章?那就来看视频吧

this 关键字基本原则

JavaScript 中的 this 是一个特殊关键字,指向函数执行时的「上下文对象」。理解 this 的核心原则是:谁调用函数,this 就指向谁。

this 的五种绑定规则

  • 预设绑定:直接调用函数时,this 指向全局对象(非严格模式)或 undefined(严格模式)
  • 隐含绑定:作为对象方法调用时,this 指向调用该方法的对象
  • 显式绑定:使用 call()、apply()、bind() 明确指定 this 的值
  • new 绑定:使用 new 关键字调用函数时,this 指向新建立的对象实例
  • 箭头函数绑定:箭头函数没有自己的 this,使用外层作用域的 this

this 的不同绑定方式详解

1. 预设绑定(Default Binding)

当函数被直接调用时,this 预设指向全局对象(浏览器中为 window,Node.js 中为 global)。在严格模式 ('use strict') 下,预设绑定的 this 为 undefined。

function printThis() {
  console.log(this);
}

printThis(); // window 或 global

// 严格模式
function strictFunction() {
  'use strict';
  console.log(this);
}

strictFunction(); // undefined

2. 隐含绑定(Implicit Binding)

当函数作为对象的方法被调用时,this 指向该对象。这是最常见的 this 绑定方式。

const user = {
  name: 'Bruce',
  age: 30,
  greet() {
    console.log(`Hello, I'm ${this.name} and I'm ${this.age} years old`);
  },
  profile: {
    title: 'Developer',
    showTitle() {
      console.log(`My title is ${this.title}`);
    }
  }
};

user.greet(); // "Hello, I'm Bruce and I'm 30 years old" (this 指向 user)
user.profile.showTitle(); // "My title is Developer" (this 指向 user.profile)

// 注意:隐含绑定可能丢失
const greetFn = user.greet;
greetFn(); // "Hello, I'm undefined and I'm undefined years old" (this 指向全局对象)

3. 显式绑定(Explicit Binding)

使用 call()、apply()、bind() 明确指定函数执行时的 this 值,不论函数原本如何定义。

function introduce(hobby1, hobby2) {
  console.log(`I'm ${this.name}, I like ${hobby1} and ${hobby2}`);
}

const person = { name: 'Bruce' };

// call: 参数逐一传入
introduce.call(person, 'coding', 'reading');
// "I'm Bruce, I like coding and reading"

// apply: 参数以数组形式传入
introduce.apply(person, ['coding', 'reading']);
// "I'm Bruce, I like coding and reading"

// bind: 返回一个新函数,不立即调用
const boundFn = introduce.bind(person, 'coding');
boundFn('reading'); // 可以稍后传入剩余参数
// "I'm Bruce, I like coding and reading"

4. new 绑定(Constructor Binding)

使用 new 关键字调用函数时,会自动执行以下步骤,其中包含将 this 绑定到新建立的对象:

  • 创建一个新的空对象
  • 将该对象的原型设为构造函数的 prototype 属性
  • 将函数内的 this 绑定到新建立的对象
  • 执行构造函数中的代码
  • 如果函数没有返回其他对象,则返回这个新建立的对象
function Person(name) {
  this.name = name;
  // this 指向新建立的对象
}

const bruce = new Person('Bruce');
console.log(bruce.name); // "Bruce"

// this 在构造函数中指向新建立的对象实例
// 即使在全局作用域或严格模式下,new 绑定也有效

5. 箭头函数绑定(Arrow Function)

箭头函数没有自己的 this,它会捕获定义时所在的词法环境中的 this 值,而且这个值不会因为调用方式的改变而改变。

const obj = {
  name: 'Bruce',
  // 传统函数 - this 依调用方式决定
  regularMethod() {
    console.log(this.name);
  },
  // 箭头函数 - this 锁定在定义时的环境
  arrowMethod: () => {
    console.log(this.name);
  },
  // 常见用法: 在传统方法内使用箭头函数
  delayedGreeting() {
    setTimeout(() => {
      console.log(`Hello, ${this.name}`); // this 仍指向 obj
    }, 1000);
  }
};

obj.regularMethod(); // "Bruce" (this 指向 obj)
obj.arrowMethod();   // undefined (this 指向定义时的全局环境)
obj.delayedGreeting(); // 1秒后输出 "Hello, Bruce"

// 注意:箭头函数的 this 不受 call/apply/bind 影响
obj.arrowMethod.call({name: 'Not Bruce'}); // 仍然是 undefined

解决 this 问题的常见方式

1. 使用 bind 方法固定 this

在构造函数或类的方法中,可以使用 bind 将方法的 this 显式绑定到实例上。

class Counter {
  constructor() {
    this.count = 0;
    // 将方法的 this 绑定到实例
    this.increment = this.increment.bind(this);
  }
  
  increment() {
    this.count++;
  }
  
  getCount() {
    return this.count;
  }
}

const counter = new Counter();
const incrementFn = counter.increment;
incrementFn(); // 即使脱离对象调用也能正常工作
console.log(counter.getCount()); // 1

2. 使用箭头函数自动绑定

箭头函数会自动捕获定义时的 this 值,是处理回调函数中 this 问题的好方法。

class Counter {
  constructor() {
    this.count = 0;
  }
  
  // 使用箭头函数自动捕获外层 this
  increment = () => {
    this.count++;
  }
  
  getCount() {
    return this.count;
  }
}

const counter = new Counter();
const incrementFn = counter.increment;
incrementFn(); // 箭头函数保持 this 指向 counter 实例
console.log(counter.getCount()); // 1

3. 使用变量保存 this

在 ES6 之前,常用的方法是在外层函数中将 this 值保存到变量中,然后在内部函数中使用该变量。

function UserProfile() {
  this.name = 'Bruce';
  this.friends = ['Alice', 'Bob'];
  
  // 使用变量保存 this
  const self = this;
  
  this.showFriends = function() {
    this.friends.forEach(function(friend) {
      console.log(self.name + ' is friends with ' + friend);
      // 使用 self 而非 this,因为这里的 this 在非严格模式下指向全局
    });
  };
}

const profile = new UserProfile();
profile.showFriends();
// "Bruce is friends with Alice"
// "Bruce is friends with Bob"

🔥 常见面试题目

(一)this 关键字的值是如何决定的?请说明其判断顺序。

判断 this 的值有固定优先顺序,从高到低依次为:

  • 如果是箭头函数,this 保持定义时外部环境的 this 值,不受其他规则影响
  • 如果使用 new 调用函数,this 指向新创建的对象实例
  • 如果使用 call、apply、bind 等方法,this 指向指定的对象
  • 如果函数作为对象的方法调用,this 指向该对象
  • 如果是普通函数调用,在非严格模式下 this 指向全局对象,严格模式下为 undefined
// 示例分析 this 绑定顺序
// 1. 箭头函数优先级最高
const arrowFn = () => { console.log(this) };
arrowFn.call({ name: 'test' }); // 全局对象,call 无法改变箭头函数的 this

// 2. new 绑定高于显式绑定
function Person(name) {
  this.name = name;
  console.log(this);
}
const boundPerson = Person.bind({ type: 'bound object' });
const person = new boundPerson('Bruce'); // { name: 'Bruce' },忽略了 bind 的对象

// 3. 显式绑定高于隐含绑定
const obj = {
  name: 'object',
  display() { console.log(this.name); }
};
obj.display.call({ name: 'call object' }); // "call object", call overrides method's this

// 4. 隐含绑定高于预设绑定
obj.display(); // "object",this 指向 obj

// 5. 预设绑定优先级最低
function standalone() { console.log(this); }
standalone(); // 全局对象或 undefined (严格模式)

(二)call、apply 和 bind 的差异与使用场景?

这三个方法都能显式指定函数执行时的 this 值,但在使用方式和行为上有明显差异:

方法执行方式参数传递常见使用场景
call立即执行函数逐一传入参数借用其他对象方法、继承时调用父类构造函数
apply立即执行函数参数以数组形式传入处理有大量参数的场景、数学计算(如求数组最大值)
bind返回新函数,不立即执行可预设部分参数事件处理函数、回调函数中保持 this 上下文
// call 实例:借用数组方法处理类数组对象
function formatArgs() {
  // arguments 是类数组对象,没有 join 方法
  return Array.prototype.join.call(arguments, ' - ');
}
console.log(formatArgs('a', 'b', 'c')); // "a - b - c"

// apply 实例:求数组最大值
const numbers = [5, 3, 8, 1, 9, 4];
const max = Math.max.apply(null, numbers); // 等同于 Math.max(5, 3, 8, 1, 9, 4)
console.log(max); // 9

// bind 实例:保存 this 上下文于事件处理函数中
class Gallery {
  constructor() {
    this.images = ['img1.jpg', 'img2.jpg', 'img3.jpg'];
    this.currentIndex = 0;
    
    // 使用 bind 固定 this
    document.getElementById('next').addEventListener(
      'click', 
      this.showNext.bind(this)
    );
  }
  
  showNext() {
    this.currentIndex = (this.currentIndex + 1) % this.images.length;
    console.log('Now showing: ' + this.images[this.currentIndex]);
  }
}

(三)箭头函数与一般函数在 this 绑定上有什么不同?

箭头函数与一般函数在 this 绑定上有以下主要差异:

  • 箭头函数没有自己的 this,它会继承定义时外部环境的 this 值
  • 箭头函数的 this 绑定是固定的,无法通过 call、apply、bind 等方法改变
  • 一般函数的 this 值取决于调用方式,可以通过各种方法改变
// 箭头函数与一般函数的 this 绑定差异
const obj = {
  name: 'object',
  regularMethod() {
    console.log('Regular:', this.name);
    setTimeout(function() {
      console.log('Regular in timeout:', this.name); // undefined
    }, 100);
  },
  arrowMethod() {
    console.log('Arrow:', this.name);
    setTimeout(() => {
      console.log('Arrow in timeout:', this.name); // 'object'
    }, 100);
  }
};

obj.regularMethod();
obj.arrowMethod();

// 无法改变箭头函数的 this
const arrowFn = () => console.log(this);
arrowFn.call({ name: 'test' }); // 仍然指向全局对象

// 一般函数可以改变 this
function regularFn() { console.log(this.name); }
regularFn.call({ name: 'test' }); // 'test'

(五)判断下列代码中 this 的指向:

// 题目 1:基本绑定
const obj = {
  name: 'object',
  method() {
    console.log(this.name);
  }
};
obj.method(); // 'object' (隐含绑定)

// 题目 2:方法赋值
const method = obj.method;
method(); // undefined (预设绑定)

// 题目 3:回调函数
setTimeout(obj.method, 100); // undefined (预设绑定)

// 题目 4:箭头函数
const arrowObj = {
  name: 'arrow',
  method() {
    setTimeout(() => {
      console.log(this.name);
    }, 100);
  }
};
arrowObj.method(); // 'arrow' (箭头函数继承外部 this)

// 题目 5:构造函数
function Person(name) {
  this.name = name;
  this.sayHi = function() {
    console.log('Hi, ' + this.name);
  };
}
const person = new Person('Bruce');
person.sayHi(); // 'Hi, Bruce' (new 绑定)
// 解答:
// 1. obj.method() 中的 this 指向 obj,因为是通过对象调用方法(隐含绑定)
// 2. method() 中的 this 指向全局对象(非严格模式)或 undefined(严格模式),因为是直接调用(预设绑定)
// 3. setTimeout 中的 this 指向全局对象,因为回调函数是直接调用(预设绑定)
// 4. 箭头函数中的 this 指向 arrowObj,因为箭头函数继承了外部环境的 this(词法绑定)
// 5. person.sayHi() 中的 this 指向 person 实例,因为是通过 new 创建的对象调用方法(new 绑定)