JavaScript 中 this 的设计:一场“谁在调用我?”的哲学追问

30 阅读7分钟

引子:一个看似简单的问题

var bar = { 
  myName: "time.geekbang.com",
  printName: function() {
    console.log(this.myName);
  }
}

当你写下:

bar.printName(); // 输出 "time.geekbang.com"

一切如你所愿。但如果你这样写:

let _printName = bar.printName;
_printName(); // 输出什么?

答案是:undefined(或全局变量的值)!

为什么?明明是同一个函数,只是换了个方式调用,结果却天差地别?

这正是 JavaScript 中 this 最令人费解的地方——它不是由函数定义的位置决定的,而是由函数被谁调用、以何种方式调用决定的

这种设计,既体现了 JavaScript 的灵活性,也埋下了无数初学者的“坑”。


一、自由变量 vs this:两个世界的规则

要真正理解 this,必须先厘清 JavaScript 中两种完全不同的变量查找机制。

自由变量:词法作用域说了算

JavaScript 是一门词法作用域(Lexical Scope)语言。这意味着:

  • 函数内部使用的变量,如果既不是参数也不是局部变量,就会沿着声明时的作用域链向上查找。
  • 这个过程发生在编译阶段(更准确地说,是在函数创建时确定),是静态的、确定的。

比如:

let myName = '极客邦';

function foo() {
  let myName = '极客时间';
  return function inner() {
    console.log(myName); // 输出 '极客时间'
  };
}

foo()(); // '极客时间'

这里 myName自由变量(free variable)——它在 inner 函数中使用,但既不是参数也不是局部变量。它的值由函数在哪里写(即词法位置)决定,而不是在哪里执行。

这就是所谓的“闭包”:函数记住了它被创建时的环境。

this:动态绑定,运行时才揭晓

this 完全不同!

  • this 不属于词法作用域
  • 它的值完全取决于函数如何被调用,是在执行阶段动态确定的。
  • 换句话说:谁调用了这个函数,this 就指向谁。

这正是 JavaScript 设计中最“反直觉”但也最灵活的地方。

📌 关键区别

  • 自由变量 → 看“出生地”(词法位置)
  • this → 看“工作单位”(调用方式)

二、this 的五种常见命运

JavaScript 中 this 的指向并非随机,而是有明确规则。以下是五种最常见的场景。

1. 作为对象方法调用 → this 指向该对象

bar.printName(); // this === bar

这是最符合“面向对象直觉”的情况。当你通过对象访问一个函数并调用它,JavaScript 会自动将 this 绑定到该对象。

设计初衷:让函数能访问所属对象的属性和方法。


2. 作为普通函数调用 → 非严格模式下 this 指向 window

let _printName = bar.printName;
_printName(); // this === window(浏览器中)

⚠️ 问题来了:window 上并没有 myName 属性(尤其是用 let 声明时),所以 this.myNameundefined

历史包袱
JavaScript 诞生于1995年,当时没有类(class),也没有模块系统。Brendan Eich 在设计时认为:“既然函数可以作为方法使用,那它总得有个 this 吧?”于是他“偷懒”地让所有未绑定的 this 默认指向全局对象(浏览器中是 window,Node.js 中是 global)。
这一设计导致了严重的全局污染问题——var 声明的变量会自动挂载到 window 上,使得 this 在普通函数调用时可能意外访问到全局变量。


3. 严格模式下 → thisundefined

'use strict';
_printName(); // TypeError: Cannot read property 'myName' of undefined

ES5 引入了严格模式(strict mode),其中一项重要改进就是:在普通函数调用中,this 不再默认指向全局对象,而是 undefined

✅ 这是更安全的设计:不让错误静默发生,直接报错,迫使开发者显式处理 this 的绑定问题。


4. 使用 call / apply / bind → 手动指定 this

_printName.call(bar);      // 输出 "time.geekbang.com"
_printName.apply(bar);
const bound = _printName.bind(bar);
bound();

这是 JavaScript 提供的“救赎机制”——你可以强行告诉函数:“你属于谁”。

  • callapply 立即执行函数,并传入 this
  • bind 返回一个新函数,永久绑定 this

这在事件处理、回调函数、高阶函数中非常常用。


5. 构造函数中 → this 指向新创建的实例

function Person(name) {
  this.name = name;
}
const p = new Person('Alice'); // this === p

当使用 new 调用函数时,JavaScript 会:

  1. 创建一个新对象;
  2. this 绑定到该对象;
  3. 执行函数体;
  4. 返回该对象(除非显式返回其他对象)。

这也是早期实现“类”的方式之一。

💡 注意:如果构造函数中忘记写 newthis 会指向全局对象(非严格模式下),造成灾难性后果!


三、var vs let:为何影响 this.myName

你可能注意到:

var myName = '全局';   // window.myName = '全局'
let myName = '全局';   // window.myName === undefined
  • var 声明的变量会挂载到全局对象(如 window)上;
  • let/const 声明的变量存在于词法环境中,不会污染全局对象

所以当 this 指向 window 时:

  • varthis.myName 能拿到值;
  • letthis.myNameundefined

这进一步说明:this 和变量作用域是两套独立系统

技术细节:

  • var 变量在全局作用域中会被提升并绑定到全局对象;
  • let/const 存在于“全局词法环境”中,但不成为全局对象的属性
    因此,window.myNamelet 声明的变量始终是 undefined

四、执行上下文视角:this 从何而来?

每次函数调用,JavaScript 引擎都会创建一个执行上下文(Execution Context),包含以下核心组件:

组件作用
词法环境(Lexical Environment)存放 let/const/函数参数等,支持块级作用域
变量环境(Variable Environment)存放 var 声明(用于变量提升)
This 绑定(This Binding)关键! 根据调用方式决定 this 的值
Outer 引用构建作用域链,用于自由变量查找

重点this 绑定与词法环境完全无关!它不参与自由变量查找,也不受函数定义位置影响。它只关心:此刻是谁在调用我?

这种分离设计,使得 JavaScript 既能支持函数式编程(依赖词法作用域),又能支持面向对象编程(依赖 this 动态绑定)。


五、箭头函数:this 的“叛逆者”

ES6 引入了箭头函数(Arrow Function),它有一个颠覆性的特性:

箭头函数没有自己的 this,它继承自外层词法作用域。

var bar = {
  myName: 'time.geekbang.com',
  printName: () => {
    console.log(this.myName); // this 来自外层(通常是 window 或 undefined)
  }
};

在这个例子中,printName 是箭头函数,它的 this 不是指向 bar,而是指向定义时的外层作用域(比如全局)。

适用场景

  • 回调函数(如 setTimeout、数组方法);
  • 避免 this 丢失。

不适用场景

  • 对象方法(因为无法访问对象自身);
  • 构造函数(箭头函数不能用 new 调用)。

💡 记住:箭头函数的 this 是“静态”的,由词法作用域决定,而非调用方式。


六、如何避免 this 的陷阱?实战建议

1. 显式绑定 this

const print = bar.printName.bind(bar);
print(); // 安全!

在传递方法引用时,务必使用 bind 固定 this

2. 使用严格模式

'use strict';

让未绑定的 this 直接报错,而不是静默失败。

3. 优先使用 class(现代语法)

class Bar {
  constructor() {
    this.myName = 'time.geekbang.com';
  }
  printName() {
    console.log(this.myName);
  }
}

虽然底层仍是函数,但语义更清晰,减少误用。

4. 谨慎使用箭头函数作为方法

不要为了“避免 this 问题”而滥用箭头函数做对象方法,否则会失去访问对象自身的能力。

5. 理解调用栈

调试时,问自己:“这个函数是怎么被调用的?”

  • obj.method()this = obj
  • method()this = window(或 undefined
  • method.call(ctx)this = ctx

七、结语:this 是 JavaScript 的“双刃剑”

this 的设计,反映了 JavaScript 作为一门“脚本语言”的实用主义哲学:

  • 灵活性:同一段代码可以在不同上下文中复用;
  • 动态性:无需提前声明类型或绑定关系;
  • 代价:不确定性,容易出错。

理解 this 的关键在于记住一句话:

this 不是你写的,而是别人怎么叫你的。”

就像一个人,在公司是“张经理”,在家是“爸爸”,在朋友面前是“老张”——身份随场景变化。this 也是如此。

掌握它,你就能驾驭 JavaScript 的动态之美;忽视它,你将陷入无尽的 undefined 之中。


附:代码行为验证

// 全局
let myName = '极客邦';

var bar = { 
  myName: "time.geekbang.com",
  printName: function() {
    console.log(myName);        // 自由变量 → '极客邦'(因为函数在全局定义)
    console.log(bar.myName);    // 显式访问 → "time.geekbang.com"
    console.log(this);          // 调用方式决定
    console.log(this.myName);   // 若 this=window,且 myName 用 let 声明 → undefined
  }
}

function foo() {
  let myName = '极客时间';
  return bar.printName; // 返回函数引用,不改变其 this 绑定
}

var _printName = foo();

_printName();           // this → window → this.myName = undefined(let 不挂载到 window)
bar.printName();        // this → bar → this.myName = "time.geekbang.com"

完美印证了 this 的动态绑定本质,以及 var/let 对全局对象的影响。


愿你在 JavaScript 的世界里,不再被 this 迷失方向。