JavaScript 中 this 的设计与指向规则

49 阅读5分钟

JavaScript 中 this 的设计与指向规则

JavaScript 中的 this 是语言中最具争议且易引发困惑的核心机制之一。它不同于其他编程语言的静态绑定,而是由函数调用方式动态决定。这种设计源于 JavaScript 的历史演进:早期版本旨在支持面向对象编程(OOP),却缺乏内置类语法,因此引入 this 来模拟对象方法的上下文。然而,这种灵活性也带来了复杂性和潜在陷阱。本文将结合作用域、执行上下文及实际代码示例,系统剖析 this 的工作原理,帮助开发者避开常见误区,并在现代 JavaScript 中高效应用。

基础概念:自由变量查找与作用域链

在探讨 this 之前,先厘清变量查找机制,这有助于突出 this 的独特性。

编译阶段与作用域链

JavaScript 虽为解释型语言,却具备编译阶段。在此阶段,引擎进行词法分析并确立作用域。作用域定义变量的可见性和访问边界,主要包括全局作用域、函数作用域及 ES6 引入的块级作用域(let/const)。

  • Lexical Scope(词法作用域):变量查找基于代码书写位置(静态确定),而非运行时动态调整。这意味着作用域在编译时即固定。
  • 作用域链:函数内部引用非局部变量或参数时,该变量称为自由变量。引擎沿作用域链向上搜索:从当前函数作用域起始,逐层向外,直至全局作用域。

示例代码:

var bar = {
  myName: "time.geekbang.com",
  printName: function() {
    console.log(myName);  // 自由变量 myName,沿作用域链向上查找
    console.log(bar.myName);
    console.log(this.myName);
  }
};

function foo() {
  let myName = '极客时间';
  return bar.printName;
}

let myName = '极客邦';
let _printName = foo();
_printName();  // 输出全局 myName: '极客邦'
bar.printName();  // 同上

在此,printName 中的 myName 为自由变量,其查找路径依函数定义位置(全局作用域)而定,最终指向全局 myName = '极客邦'。注意,var 声明的变量会挂载至全局对象(如浏览器 window),易导致污染。ES6 的 letconst 则避免此问题,不挂载全局。

严格模式('use strict')进一步优化:在非严格模式,未声明变量隐式全局化;严格模式下,直接抛错。

this 的设计:历史遗留与权宜之计

JavaScript 作者 Brendan Eich 旨在让函数灵活充当对象方法,故引入 this 以引用“当前对象”。理想中,对象方法内 this 应指向对象,便于属性访问(如 this.myName)。

然而,this 的指向非词法作用域决定(编译时静态),而依执行阶段调用方式动态绑定。这与变量查找形成鲜明对比,是设计例外。原因在于 JS 函数为一等公民,可随意传递。若函数脱离对象独立执行,this 需动态适应。

  • 设计缺陷:早期忽略普通函数调用时的 this,默认指向全局对象(浏览器 window)。这放大全局污染,尤其结合 var
  • 严格模式补救:普通函数 thisundefined,防止意外全局修改。
  • 核心原则this 作为指针,指向函数调用者。调用者决定指向。

此设计赋予 JS 极致灵活性,却常引发 bug。以下通过执行上下文剖析其规则。

执行上下文:this 的动态绑定环境

执行上下文(Execution Context)是引擎管理代码的环境,包含变量环境、词法环境及 this 绑定。函数调用创建新上下文,this 据调用形式绑定。

  • 全局上下文this 指向全局对象。
  • 函数上下文this 依调用决定。

以下详解 this 指向的五种常见场景,并配代码示例。

1. 对象方法调用:指向对象

函数作为对象属性调用时,this 指向该对象。这是 OOP 核心用法。

示例:

let bar = {
  myName: "极客邦",
  foo: function() {
    this.myName = "极客时间";  // 修改 bar.myName
  }
};

bar.foo();
console.log(bar);  // { myName: "极客时间", foo: [Function: foo] }

调用者为 bar,故 this 指向之。

若提取函数独立调用,则变:

var myObj = {
  name: "极客时间",
  showThis: function() {
    console.log(this);
  }
};

var foo = myObj.showThis;
foo();  // this 为 window(非严格)或 undefined(严格)

2. 普通函数调用:指向全局或 undefined

独立调用函数,this 默认全局对象;严格模式下为 undefined

示例:

function foo() {
  console.log(this);  // window(非严格)
}

foo();  // 隐式 window.foo()

易致污染:修改 this 即改全局。

3. call/apply/bind 显式绑定:指向指定对象

这些方法手动设定 this,适用于方法借用或上下文切换。

  • call(obj, arg1...) / apply(obj, [args]):立即调用,this 为 obj。
  • bind(obj):返回绑定函数。

示例:

function foo() {
  this.myName = "极客时间";
}

let bar = { myName: "极客邦" };
foo.apply(bar);
console.log(bar);  // { myName: "极客时间" }

4. 构造函数调用:指向新实例

new 关键字时,this 指向新建对象。

示例:

function CreateObj() {
  this.name = "极客时间";
}

var myObj = new CreateObj();
console.log(myObj);  // { name: "极客时间" }

内部:创建空对象、绑定 this、设原型、返回。

5. 事件处理函数:指向绑定元素

DOM 事件中,this 指向触发元素。

示例:

<a href="#" id="link">点击我</a>
<script>
  document.getElementById('link').addEventListener('click', function() {
    console.log(this);  // <a id="link" ...>
  });
</script>

利于事件委托。

实际应用:如何可靠获取 time.geekbang.com

针对开例,若需在可能独立调用的函数中访问 bar.myName = "time.geekbang.com"

  • 优先 this.myName:对象方法中可靠。
  • 箭头函数(ES6):不绑定自身 this,继承外层。
  • 最佳实践:严格模式 + let/const 防污染;class 与箭头简化 OOP。

this 体现了 JS 的动态本质,却源于早期妥协。它非静态词法绑定,而是运行时调用决定,常令初学者困惑。关键:聚焦调用者,结合上下文分析。