深入理解JavaScript中的this关键字

177 阅读7分钟

前言

JavaScript中的this关键字是语言中最令人困惑但又最强大的特性之一。对于初学者甚至是有经验的开发者来说,this的行为常常让人感到困惑。本文将全面解析this的工作原理,帮助你在各种场景下准确理解和正确使用它。


一、this的基本概念

1.1 什么是this

在JavaScript中,this是一个特殊的关键字,它指向当前执行上下文中的"所有者"对象。与大多数其他语言不同,JavaScript中的this不是固定不变的,它的值取决于函数的调用位置的调用方式所以分析this的值就是分析调用位置和调用方式。

1.2 调用位置与调用栈

调用位置就是函数在代码中被调用的位置(而不是声明的位置),想要得到调用位置,我们得分析调用栈,调用位置 由 调用栈 决定,是当前执行函数的前一个调用。

下面我们来看看什么是调用栈和调用位置。

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

1.3 调用方式:this的绑定规则

在JavaScript中调用方式的this绑定遵循四条基本规则:

  1. 默认绑定
  2. 隐式绑定
  3. 显式绑定
  4. new绑定

理解这些规则是掌握this的关键。我们将在接下来的章节中详细探讨每种绑定方式。


二、默认绑定

2.1 默认绑定

首先要介绍的是最常用的函数调用类型:独立函数调用。可以把这条规则看作是无法应用其它规则时的默认规则,优先级最低,保底使用这种方式进行绑定this

function showThis() {
  console.log(this.a); 
}

var a = 2;

showThis(); // 输出: 2

可以看到外我们在没有任何修饰的情况下进行独立函数调用,因此使用默认绑定

2.2 严格模式下的默认绑定

而在严格模式('use strict')下,默认绑定的行为会有所不同:this会被设置为undefined而不是全局对象。

'use strict';

function strictThis() {
  console.log(this.a);
}

var a = 2;
strictThis(); // 报错:TypeError: this is undefined

三、隐式绑定

3.1 方法调用中的this

当函数作为对象的方法被调用时,this会隐式绑定到该对象。

// 函数greet作为person的方法被调用
const person = {
  name: 'Alice',
  greet: function() {
    console.log(`Hello, my name is ${this.name}`);
  }
};

person.greet(); // 输出: Hello, my name is Alice

3.2 隐式丢失问题

隐式绑定最常见的陷阱是"隐式丢失"——当方法被赋值给变量或作为回调传递时,会丢失原来的this绑定。

下面代码中的const greetFunc = person.greetgreetFunc()调用的是greet函数本身,所以此时的greet()是一个没有任何修饰的调用,使用默认绑定

const person = {
  name: 'Bob',
  greet: function() {
    console.log(`Hello, my name is ${this.name}`);
  }
};

const greetFunc = person.greet;
greetFunc(); // 输出: Hello, my name is undefined (this指向全局对象)

3.3 多层对象中的this

当方法调用涉及多层对象时,this绑定到最近的直接对象。

const school = {
  name: 'ABC School',
  student: {
    name: 'Charlie',
    greet: function() {
      console.log(`I'm ${this.name} from ${this.school}`);
    }
  }
};

school.student.greet(); // 输出: I'm Charlie from undefined

四、显式绑定

4.1 call和apply方法

JavaScript提供了callapply方法,允许我们显式指定函数调用时的this值。

function introduce(lang1, lang2) {
  console.log(`My name is ${this.name} and I know ${lang1} and ${lang2}`);
}

const person = { name: 'David' };

introduce.call(person, 'JavaScript', 'Python');
// 输出: My name is David and I know JavaScript and Python

introduce.apply(person, ['Java', 'C++']);
// 输出: My name is David and I know Java and C++

4.2 硬绑定:bind方法

bind方法创建一个新函数,永久绑定指定的this值。这种方法被称为硬绑定。

const person = { name: 'Eve' };

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

const boundGreet = greet.bind(person);
boundGreet(); // 输出: Hello, Eve!

setTimeout(boundGreet, 100); // 100ms后输出: Hello, Eve!

4.3 硬绑定的应用场景

硬绑定常用于事件处理程序和异步回调中,确保回调函数中的this指向预期的对象。

function Button() {
  this.clickCount = 0;
  this.handleClick = function() {
    this.clickCount++;
    console.log(`Button clicked ${this.clickCount} times`);
  }.bind(this); // 硬绑定确保this始终指向Button实例
}

const btn = new Button();
document.querySelector('button').addEventListener('click', btn.handleClick);

五、new绑定

5.1 构造函数中的this

当使用new关键字调用函数时,会发生以下步骤:

  1. 创建一个新对象
  2. 将新对象的[[Prototype]]链接到构造函数的prototype属性
  3. this绑定到新创建的对象
  4. 如果函数没有返回其他对象,则自动返回这个新对象
function Person(name) {
  this.name = name;
  this.greet = function() {
    console.log(`Hello, I'm ${this.name}`);
  };
}

const alice = new Person('Alice');
alice.greet(); // 输出: Hello, I'm Alice

5.2 new绑定的注意事项

如果构造函数返回一个对象,则new表达式的结果是该返回对象,而不是新创建的对象。

function Person(name) {
  this.name = name;
  return { name: 'Overridden' };
}

const bob = new Person('Bob');
console.log(bob.name); // 输出: Overridden

六、this的优先级规则

6.1 绑定规则的优先级顺序

当多种绑定规则同时适用时,JavaScript按照以下优先级确定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', foo: foo };

obj1.foo(); // 隐式绑定: obj1
obj1.foo.call(obj2); // 显式绑定优先: obj2
new obj1.foo(); // new绑定优先: 新创建的对象

6.2 例外情况

某些情况下,显式绑定的this值会被忽略,例如当传入nullundefined时,会应用默认绑定规则。

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

bar.call(null); // 在非严格模式下输出: Window {...}
bar.call(undefined); // 同上

七、箭头函数中的this

7.1 箭头函数的this行为

箭头函数不绑定自己的this,而是继承外层函数作用域的this值,箭头函数在涉及this绑定时的行为不会遵守上面的四条规则。

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

obj.regularFunc(); // 输出: Frank
obj.arrowFunc(); // 输出: undefined (this继承自外层作用域)

7.2 箭头函数的适用场景

箭头函数特别适合用作回调函数,因为它们不会改变this的绑定。

function Timer() {
  this.seconds = 0;
  
  setInterval(() => {
    this.seconds++;
    console.log(this.seconds);
  }, 1000);
}

const timer = new Timer(); // 每秒输出递增的数字

7.3 箭头函数的限制

由于箭头函数没有自己的this,因此不能用作构造函数,也不能通过callapplybind来改变this绑定。

const Person = (name) => {
  this.name = name; // TypeError: 箭头函数不能用作构造函数
};

const p = new Person('Grace');

八、this的特殊情况

8.1 DOM事件处理程序中的this

在DOM事件处理程序中,this通常指向触发事件的元素。

document.querySelector('button').addEventListener('click', function() {
  console.log(this); // 输出: <button>元素
});

8.2 定时器回调中的this

setTimeoutsetInterval的回调函数中,this默认指向全局对象(非严格模式)或undefined(严格模式),除非使用箭头函数或显式绑定。

const obj = {
  name: 'Henry',
  delayedGreet: function() {
    setTimeout(function() {
      console.log(`Hello, ${this.name}`); // 输出: Hello, undefined
    }, 100);
  },
  arrowDelayedGreet: function() {
    setTimeout(() => {
      console.log(`Hello, ${this.name}`); // 输出: Hello, Henry
    }, 100);
  }
};

obj.delayedGreet();
obj.arrowDelayedGreet();

九、this的最佳实践

9.1 避免this混淆的策略

为了避免this带来的困惑,可以采取以下策略:

  1. 在构造函数和方法中使用this,在其他地方避免使用
  2. 使用箭头函数来保持this的一致性
  3. 在需要明确this指向时使用bind
  4. 使用const self = this模式(虽然箭头函数现在更受欢迎)
function OldSchool() {
  const self = this;
  this.value = 42;
  
  setTimeout(function() {
    console.log(self.value); // 使用self而不是this
  }, 100);
}

9.2 现代JavaScript中的this模式

在现代JavaScript开发中,推荐的做法包括:

  1. 在React组件中使用箭头函数作为类属性
  2. 在Vue中合理使用箭头函数和普通函数
  3. 在模块模式中避免不必要的this使用
// React组件示例
class MyComponent extends React.Component {
  state = { count: 0 };
  
  // 使用箭头函数确保this始终指向组件实例
  handleClick = () => {
    this.setState(prevState => ({ count: prevState.count + 1 }));
  };
  
  render() {
    return <button onClick={this.handleClick}>Click me</button>;
  }
}

十、总结

JavaScript中的this机制虽然复杂,但一旦掌握了它的绑定规则,就能写出更灵活、更强大的代码。关键点总结如下:

  1. this的值取决于函数的调用方式,而不是定义位置
  2. 四种绑定规则:默认、隐式、显式和new绑定
  3. 箭头函数不绑定自己的this,而是继承外层作用域的this
  4. 在不确定this指向时,可以使用console.log(this)进行调试
  5. 在现代开发中,合理使用箭头函数和显式绑定可以减少this相关的问题

理解this是成为JavaScript高手的重要一步。通过实践和经验的积累,你将能够自信地处理各种this相关的场景,写出更清晰、更健壮的代码。


🌇结尾

本文部分内容参考KYLE SIMPSON的《你不知道的JavaScript(上卷)

感谢你看到最后,最后再说两点~
①如果你持有不同的看法,欢迎你在文章下方进行留言、评论。
②如果对你有帮助,或者你认可的话,欢迎给个小点赞,支持一下~
我是3Katrina,一个热爱编程的大三学生

(文章内容仅供学习参考,如有侵权,非常抱歉,请立即联系作者删除。)

作者:3Katrina
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。