JavaScript 中的 this:一篇文章终结你的困惑

this 是 JavaScript 最让人困惑的概念之一。本文用最通俗的方式,彻底讲清楚 this 的指向规则。

12 分钟阅读
小明

灵魂拷问

const obj = {
  name: '小明',
  sayHi: function() {
    console.log(this.name);
  }
};

obj.sayHi();  // 输出什么?

const fn = obj.sayHi;
fn();  // 输出什么?

如果你能立刻说出答案,这篇文章可以跳过。

如果你犹豫了……那我们今天就把 this 彻底搞清楚。

this 到底是什么?

一句话解释:this 是函数执行时的上下文对象

注意关键词:执行时。不是定义时,是执行时。

这意味着,同一个函数,在不同的情况下调用,this 可能指向不同的对象。

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

const person1 = { name: '小明', sayHi };
const person2 = { name: '小红', sayHi };

person1.sayHi();  // 小明
person2.sayHi();  // 小红

同一个函数,谁调用,this 就指向谁。

四条规则判断 this

判断 this 指向,只需要记住这四条规则,优先级从高到低:

规则 1:new 绑定

如果函数是通过 new 调用的,this 指向新创建的对象。

function Person(name) {
  this.name = name;
  // this 指向新创建的对象
}

const p = new Person('小明');
console.log(p.name);  // 小明

原理new 做了什么?

  1. 创建一个新对象
  2. 把这个新对象的原型指向构造函数的 prototype
  3. this 绑定到这个新对象
  4. 执行构造函数
  5. 如果构造函数没有返回对象,就返回这个新对象

规则 2:显式绑定

使用 callapplybind 可以明确指定 this

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

const person = { name: '小明' };

sayHi.call(person);    // 小明
sayHi.apply(person);   // 小明

const boundFn = sayHi.bind(person);
boundFn();  // 小明

三者的区别

方法执行时机参数格式
call立即执行逐个传参 fn.call(obj, arg1, arg2)
apply立即执行数组传参 fn.apply(obj, [arg1, arg2])
bind返回新函数逐个传参 fn.bind(obj, arg1, arg2)
function greet(greeting, punctuation) {
  console.log(greeting + ', ' + this.name + punctuation);
}

const person = { name: '小明' };

greet.call(person, 'Hello', '!');    // Hello, 小明!
greet.apply(person, ['Hello', '!']); // Hello, 小明!

const boundGreet = greet.bind(person, 'Hello');
boundGreet('!');  // Hello, 小明!

规则 3:隐式绑定

如果函数作为对象的方法调用,this 指向那个对象。

const obj = {
  name: '小明',
  sayHi: function() {
    console.log(this.name);
  }
};

obj.sayHi();  // 小明,this 指向 obj

注意隐式丢失

const obj = {
  name: '小明',
  sayHi: function() {
    console.log(this.name);
  }
};

const fn = obj.sayHi;  // 把方法赋值给变量
fn();  // undefined,this 指向全局对象(严格模式下是 undefined)

为什么?因为 fn() 调用时,没有"调用者",不符合"谁调用就指向谁"的规则。

常见的隐式丢失场景

// 场景 1:赋值给变量
const fn = obj.sayHi;
fn();

// 场景 2:作为回调函数
setTimeout(obj.sayHi, 100);

// 场景 3:作为参数传递
function execute(callback) {
  callback();
}
execute(obj.sayHi);

规则 4:默认绑定

如果以上规则都不适用,this 使用默认绑定。

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

sayHi();  // 非严格模式:window(浏览器)/ global(Node.js)
          // 严格模式:undefined

严格模式的影响

'use strict';

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

sayHi();  // undefined

优先级排序

当多个规则同时出现时,按这个优先级判断:

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

例子:

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

const obj = { name: '小红' };
const BoundPerson = Person.bind(obj);

const p = new BoundPerson('小明');
console.log(p.name);  // 小明,不是小红!

// 虽然 bind 了 obj,但 new 的优先级更高

箭头函数:没有自己的 this

箭头函数不遵守上述规则,它没有自己的 this,而是继承外层的 this

const obj = {
  name: '小明',
  sayHi: function() {
    // 这里的 this 是 obj
    const inner = () => {
      console.log(this.name);  // 继承外层的 this,还是 obj
    };
    inner();
  }
};

obj.sayHi();  // 小明

对比普通函数:

const obj = {
  name: '小明',
  sayHi: function() {
    const inner = function() {
      console.log(this.name);  // 默认绑定,this 是 window/undefined
    };
    inner();
  }
};

obj.sayHi();  // undefined(严格模式)或 ''(非严格模式)

箭头函数的 this 在定义时就确定了,不会改变

const obj = {
  name: '小明',
  sayHi: () => {
    console.log(this.name);
  }
};

obj.sayHi();  // undefined,不是"小明"!
// 因为箭头函数在定义时,外层没有函数,this 是全局对象

不能对箭头函数使用 call/apply/bind 改变 this

const fn = () => {
  console.log(this.name);
};

const person = { name: '小明' };
fn.call(person);  // undefined,不是"小明"

常见面试题

题目 1:基础绑定

var name = '全局';

const obj = {
  name: '小明',
  sayHi: function() {
    console.log(this.name);
  },
  sayHi2: () => {
    console.log(this.name);
  }
};

obj.sayHi();   // ?
obj.sayHi2(); // ?

答案

  • obj.sayHi()小明(隐式绑定)
  • obj.sayHi2()全局(箭头函数继承外层 this,即全局对象)

题目 2:回调函数中的 this

const obj = {
  name: '小明',
  friends: ['小红', '小刚'],
  showFriends: function() {
    this.friends.forEach(function(friend) {
      console.log(this.name + '的朋友:' + friend);
    });
  }
};

obj.showFriends();  // 输出什么?

答案

undefined的朋友:小红
undefined的朋友:小刚

因为 forEach 的回调函数是普通函数,this 默认绑定到全局对象。

修复方法 1:使用箭头函数

showFriends: function() {
  this.friends.forEach((friend) => {
    console.log(this.name + '的朋友:' + friend);
  });
}

修复方法 2:使用 forEach 的第二个参数

showFriends: function() {
  this.friends.forEach(function(friend) {
    console.log(this.name + '的朋友:' + friend);
  }, this);  // 传入 this 作为回调的 this
}

修复方法 3:提前保存 this

showFriends: function() {
  const self = this;
  this.friends.forEach(function(friend) {
    console.log(self.name + '的朋友:' + friend);
  });
}

题目 3:嵌套对象

const obj = {
  name: '小明',
  inner: {
    name: '小红',
    sayHi: function() {
      console.log(this.name);
    }
  }
};

obj.inner.sayHi();  // ?

const fn = obj.inner.sayHi;
fn();  // ?

答案

  • obj.inner.sayHi()小红(隐式绑定,this 指向最近的调用对象 inner)
  • fn()undefined(隐式丢失,默认绑定)

题目 4:类中的 this

class Person {
  constructor(name) {
    this.name = name;
  }
  
  sayHi() {
    console.log(this.name);
  }
}

const p = new Person('小明');
p.sayHi();  // ?

const fn = p.sayHi;
fn();  // ?

答案

  • p.sayHi()小明
  • fn() → 报错!Cannot read property 'name' of undefined

类中的方法默认在严格模式下运行,隐式丢失后 this 是 undefined。

修复方法:在构造函数中绑定 this

class Person {
  constructor(name) {
    this.name = name;
    this.sayHi = this.sayHi.bind(this);  // 绑定 this
  }
  
  sayHi() {
    console.log(this.name);
  }
}

或者使用类属性 + 箭头函数:

class Person {
  constructor(name) {
    this.name = name;
  }
  
  sayHi = () => {
    console.log(this.name);
  }
}

总结

判断 this 指向的口诀:

  1. new 调用:this 指向新创建的对象
  2. call/apply/bind:this 指向指定的对象
  3. 对象方法调用:this 指向调用的对象
  4. 独立函数调用:this 指向全局对象(严格模式是 undefined)
  5. 箭头函数:继承外层的 this,不能被改变
// 一个综合例子
const obj = {
  name: '小明',
  methods: {
    sayHi: function() { console.log(this.name); },
    sayHi2: () => { console.log(this.name); },
    sayHi3() { console.log(this.name); }
  }
};

obj.methods.sayHi();   // undefined (this 是 methods 对象,没有 name)
obj.methods.sayHi2();  // undefined (箭头函数,this 是全局对象)
obj.methods.sayHi3();  // undefined (同 sayHi)

理解 this,关键是理解函数是怎么被调用的

下一篇,我们来聊聊数据结构中的"排队专家"——队列。