this:从JavaScript执行上下文视角讲this|浏览器篇
# this:从JavaScript执行上下文视角讲this
30 秒速记
- 函数中的普通变量名通过词法作用域解析,查找路径由函数定义时的代码结构决定。
printName直接读取myName时,不会自动读取bar.myName;示例两次调用最终都解析到全局绑定。- 从
foo返回函数不会改变该函数的词法作用域,因此局部的myName也不会成为它的查找目标。 - 对象方法若要访问接收者的属性,需要显式写成
this.myName,而不是依赖同名裸变量。 - 作用域链负责解析标识符,
this负责提供当前调用关联的对象;两套机制不能混为一谈。
示例中的两次调用都会打印全局变量 myName 的值“极客邦”,不会读取 bar.myName。 printName 里的裸变量由词法作用域解析,查找路径在函数定义时就由代码结构确定,从 foo 返回这个函数也不会改变它。对象方法要读取接收者属性,需要明确写成 this.myName。简单来说,作用域链负责寻找变量,this 负责关联当前调用对象,这两套机制不能混用。
在上篇文章中,我们讲了词法作用域、作用域链以及闭包,并在最后思考题中留了下面这样一段代码
var bar = {
myName:"time.geekbang.com",
printName: function () {
console.log(myName)
}
}
function foo() {
let myName = " 极客时间 "
return bar.printName
}
let myName = " 极客邦 "
let _printName = foo()
_printName()
bar.printName()
相信你已经知道了,在 printName 函数里面使用的变量 myName 是属于全局作用域下面的,所以最终打印出来的值都是“极客邦”。这是因为 JavaScript 语言的作用域链是由词法作用域决定的,而词法作用域是由代码结构来确定的。
不过按照常理来说,调用bar.printName方法时,该方法内部的变量 myName 应该使用 bar 对象中的,因为它们是一个整体,大多数面向对象语言都是这样设计的,比如我用 C++ 改写了上面那段代码,如下所示:
#include <iostream>
using namespace std;
class Bar{
public:
char* myName;
Bar(){
myName = "time.geekbang.com";
}
void printName(){
cout<< myName <<endl;
}
} bar;
char* myName = " 极客邦 ";
int main() {
bar.printName();
return 0;
}
在这段 C++ 代码中,我同样调用了 bar 对象中的 printName 方法,最后打印出来的值就是 bar 对象的内部变量 myName 值——“time.geekbang.com”,而并不是最外面定义变量 myName 的值——“极客邦”,所以在对象内部的方法中使用对象内部的属性是一个非常普遍的需求。但是 JavaScript 的作用域机制并不支持这一点,基于这个需求,JavaScript 又搞出来另外一套this 机制。
所以,在 JavaScript 中可以使用 this 实现在 printName 函数中访问到 bar 对象的 myName 属性了。具体该怎么操作呢?你可以调整 printName 的代码,如下所示:
printName: function () {
console.log(this.myName)
}
接下来咱们就展开来介绍 this,不过在讲解之前,希望你能区分清楚作用域链和this是两套不同的系统,它们之间基本没太多联系。在前期明确这点,可以避免你在学习 this 的过程中,和作用域产生一些不必要的关联。
面试官追问
追问 1用户卡片对象有 myName: 'time.geekbang.com',方法内部却写 console.log(myName);分别执行 bar.printName() 和取出方法后执行 _printName(),为什么两次都可能打印全局的“极客邦”?
裸标识符 myName 按函数定义处形成的词法作用域链解析,不会因为成员调用而自动读取对象属性。两种调用形式都没有改变函数的词法结构,所以在给定代码中都会命中全局绑定;若全局不存在该名称,则会走未解析标识符的错误路径。
追问 2产品要求 bar.printName() 必须展示当前卡片对象的名称,评审中应把方法改成什么形式,为什么仅调整调用位置不够?
方法内部应读取 this.myName,让属性访问通过当前调用对应的 this 完成。仅把函数写在对象附近或从另一个函数返回它,都不会把对象属性加入词法作用域;若方法被脱离对象调用,仍可能丢失预期接收者。
追问 3业务函数 foo() 内有 let myName = '极客时间',它返回的是预先定义好的 bar.printName;同事认为返回动作会让该方法捕获 foo 的局部变量,这个判断哪里不成立?
返回现有函数不会重建它的词法作用域,也不会让它自动捕获调用者 foo 的局部环境。只有函数定义位置决定其词法作用域关系;这里的 printName 并非在 foo 内创建,因此不能据此读取该局部绑定。
追问 4线上将 bar.printName 交给事件系统后,页面名称显示异常;方法内部既有 myName 又有 this.myName,排查时为何必须把两条解析路径分开?
myName 通过词法作用域链查找,而 this.myName 先依赖本次调用的 this,再读取对应对象属性。事件系统可能改变调用入口,却不会改写函数定义处的词法链;混在一起分析会把全局变量命中误判成对象绑定丢失。
追问 5组件库正在争论“方法里统一使用裸属性名”还是“通过 this 访问对象状态”,在这段代码结构下应如何取舍?
需要访问对象自身属性时应明确写成 this.myName,裸写 myName 表达的是词法变量而不是对象成员。采用 this 会让结果依赖调用方式,因此方法被传递为回调时还需保留接收者;若不想承担这种依赖,可显式传参或使用闭包保存数据。
# JavaScript 中的 this 是什么
30 秒速记
this是执行上下文的一部分,与变量环境、词法环境和外部环境并列存在。- 每次进入相应执行过程时,都要结合该执行上下文理解其
this,不能把它当作普通词法变量。 - 原文将执行上下文分为全局、函数和
eval三类,因此分别讨论这三类上下文中的this。 - 实际分析通常重点落在全局执行上下文和函数执行上下文,二者的
this需要分别判断。
this 是执行上下文携带的绑定,不是通过词法作用域链查找出来的普通变量。 原文把执行上下文分为全局、函数和 eval 三类,因此每类上下文都有对应的 this,实际分析通常重点看全局和函数上下文。普通函数的 this 主要取决于调用方式,例如成员调用、call、apply、bind 或构造调用可能得到不同绑定。需要注意,箭头函数不会创建自己的 this,而浏览器经典脚本、ES 模块和 Node.js 模块的顶层绑定也不能一概而论。
关于 this,我们还是得先从执行上下文说起。在前面几篇文章中,我们提到执行上下文中包含了变量环境、词法环境、外部环境,但其实还有一个 this 没有提及,具体你可以参考下图:

从图中可以看出,this 是和执行上下文绑定的,也就是说每个执行上下文中都有一个 this。前面《08 | 调用栈:为什么 JavaScript 代码会出现栈溢出?》中我们提到过,执行上下文主要分为三种——全局执行上下文、函数执行上下文和 eval 执行上下文,所以对应的 this 也只有这三种——全局执行上下文中的 this、函数中的 this 和 eval 中的 this。
那么接下来我们就重点讲解下全局执行上下文中的 this和函数执行上下文中的 this。
原理拆解: this 是一次执行所携带的绑定,不通过词法作用域链按声明位置查找。普通函数的 this 主要由调用形式决定:对象以成员调用函数时绑定到点号左侧对象;通过 call、apply 或 bind 可以显式指定;构造调用会把它绑定到新创建的实例。因而同一个函数体仅改变调用入口,就可能观察到不同的 this。
调用示例: 假设有 const user = { name: 'Ada', read() { return this.name; } },执行 user.read() 时,函数执行上下文中的 this 是 user。若取出 const read = user.read 再执行 read(),成员调用关系已经丢失;其结果取决于严格模式和宿主环境,不能再认定为 user。使用 read.call({ name: 'Lin' }) 则会返回 Lin。
边界与反例: 箭头函数不创建自己的 this,而是沿用定义位置外层执行上下文中的绑定,所以不能通过 call 改写它。浏览器经典脚本顶层的全局 this 通常指向 window,但 ES 模块顶层为 undefined;Node.js 模块又有自己的封装语义,因此不应把“顶层 this 等于全局对象”当作跨环境规则。eval 的行为还受直接或间接调用及严格模式影响,工程代码不宜依赖它推断绑定。
工程验证: 调试时应同时检查函数类型和调用点,而不只查看函数定义。可在目标函数入口设置断点,观察调用栈及当前 this;遇到回调丢失接收者时,使用包装函数、箭头函数捕获外层绑定,或以 bind 固定接收者,并通过严格模式测试暴露对隐式全局绑定的依赖。
