当前位置:首页 > 技术 > 正文内容

JavaScript 核心机制深入解析

访客 技术 2026年5月31日 1

词法作用域:静态绑定的变量查找规则

在 JavaScript 中,作用域决定了变量的可访问范围。其核心机制是词法作用域(也称静态作用域),即变量的查找路径在函数定义时就已经确定,而非运行时动态决定。

与之相对的是动态作用域,它依据调用栈动态解析变量。而 JavaScript 始终采用静态方式,这意味着函数内部引用的变量将沿着其定义位置的作用域链向上查找。

示例说明:

const outerValue = 'global';
function createFunction() {
  const outerValue = 'local';
  function inner() {
    return outerValue;
  }
  return inner;
}
const fn = createFunction();
console.log(fn()); // 输出 'local',而非 'global'

尽管 fn 在全局环境中被调用,但由于 inner 定义在 createFunction 内部,因此它捕获的是该函数作用域中的 outerValue

执行上下文栈:代码执行的管理结构

JavaScript 引擎通过一个栈结构来管理代码执行流程,这个栈称为执行上下文栈(Execution Context Stack, ECS)。每当有新代码块需要执行,就会创建一个新的执行上下文并压入栈顶;执行完毕后则弹出。

可执行代码主要分为三类:

  • 全局代码 —— 程序启动时最先压入的上下文
  • 函数代码 —— 每次函数调用都会创建新的上下文
  • eval 代码 —— 在 eval 内部执行的脚本片段

由于栈遵循"后进先出"原则,当前正在运行的代码总是位于栈顶上下文中。

执行上下文的组成要素

每个执行上下文可以抽象为一个对象,包含以下关键组件:

  • 变量对象(VO/AO):存储当前作用域中声明的变量、函数和参数
  • 作用域链:用于变量查找的链式结构
  • this 值:指向当前执行环境的上下文对象

上下文的生命周期分为两个阶段:

  1. 创建阶段(进入上下文):初始化变量对象,建立作用域链,确定 this 指向
  2. 执行阶段:执行代码,更新变量值

变量对象与活动对象的区别

变量对象(Variable Object, VO)是执行上下文中用于存储标识符的抽象概念。根据上下文类型不同,其实现形式有所差异:

  • 全局上下文中,变量对象就是全局对象(浏览器中为 window)。所有全局变量和函数声明都会作为属性挂载到它上面。
  • 函数上下文中,初始的变量对象被称为活动对象(Activation Object, AO),最初只包含 arguments 对象。随后,在创建阶段会加入形参、函数声明和变量声明。

注意:变量提升(hoisting)现象正是发生在变量对象的初始化过程中 —— 所有 var 声明和函数声明会被提前至作用域顶部。

作用域链的构建过程

当进行变量查找时,JavaScript 引擎首先检查当前执行上下文的变量对象。若未找到,则沿上级作用域继续搜索,直到全局对象为止。这一系列链接的变量对象构成了作用域链

每个函数在创建时都会附带一个内部属性 [[Scope]],它保存了函数定义时所能访问的所有外部变量对象的引用列表。例如:

function outer() {
  const x = 1;
  function inner() {
    console.log(x); // 可访问 outer 的变量
  }
  return inner;
}

此时,inner 函数的 [[Scope]] 包含了 outer 函数的变量对象和全局对象。

当函数被调用时,系统会创建新的执行上下文,并将当前的活动对象插入作用域链前端,形成完整的查找路径:
[AO].concat([[Scope]])

特别地,使用 new Function() 创建的函数,其 [[Scope]] 仅包含全局对象,无法访问外部私有变量。

this 的绑定机制

this 的值完全由函数的调用方式决定,与定义位置无关。其绑定规则如下:

  • 方法调用:如 obj.method(),则 this 指向 obj
  • 独立函数调用:直接调用函数名,如 func(),严格模式下 thisundefined,非严格模式下为全局对象
  • 构造函数调用:使用 new 调用时,this 指向新创建的实例
  • 显式绑定:通过 callapplybind 显式指定 this

理解 this 的关键是识别调用表达式的"引用类型"。只有当调用左侧是一个属性访问表达式(Reference 类型)时,this 才会被正确设置为其 base 对象。

示例分析:

let value = 1;

const exampleObj = {
  value: 2,
  getValue() {
    return this.value;
  }
};

console.log(exampleObj.getValue());        // 2 → 正常方法调用
console.log((exampleObj.getValue)());      // 2 → 分组运算符不改变引用
console.log((exampleObj.getValue = exampleObj.getValue)()); // 1 → 赋值操作返回函数本身,丢失上下文
console.log((false || exampleObj.getValue)());             // 1 → 逻辑或返回函数值,非引用类型
console.log((exampleObj.getValue, exampleObj.getValue)()); // 1 → 逗号运算符返回右侧值,非引用

最后三个例子中,调用左侧不再是 Reference 类型,因此 this 指向全局或 undefined。

闭包的本质:函数与词法环境的结合体

从理论上看,闭包是指函数与其定义时所处的词法环境的组合。JavaScript 中每个函数天然具备这种能力。

实践中,当我们把一个内部函数从外部函数中返回或传递出去,且该内部函数仍能访问外部函数的变量时,就形成了典型的闭包场景。

闭包的关键在于:即使外部函数已经执行结束,其变量对象依然保留在内存中,因为内部函数的 [[Scope]] 仍然引用着它。

function counter() {
  let count = 0;
  return function() {
    return ++count;
  };
}
const increment = counter();
console.log(increment()); // 1
console.log(increment()); // 2

上述代码中,count 变量被封闭在闭包中,不会被垃圾回收。

原型与原型链:对象继承的基础机制

JavaScript 使用基于原型的继承模型。每个对象都有一个内部指针 [[Prototype]],可通过 __proto__ 访问(现代标准推荐使用 Object.getPrototypeOf())。

对于函数而言,还有一个 prototype 属性,它是用来生成实例对象原型的模板。

关系总结:

  • 所有函数的 prototype 默认指向一个对象,该对象的 constructor 指回函数自身
  • 所有对象的 __proto__ 最终都指向其构造函数的 prototype
  • 普通函数对象的 __proto__ 指向 Function.prototype
  • Function.prototype.__proto__ 指向 Object.prototype
  • Object.prototype.__proto__null,表示原型链终点
  • Function.__proto__ 指向自身 Function.prototype,体现"自举"特性

当访问对象属性时,如果本地不存在,引擎会沿着 __proto__ 链逐级向上查找,直至 null,这就是原型链的工作原理。

参数传递机制:按共享传递

JavaScript 中的参数传递既不是纯粹的值传递也不是引用传递,而是按共享传递(call by sharing),也叫"对象引用传递"。

具体表现为:

  • 原始类型(string、number、boolean 等):传递的是值的副本,修改形参不影响实参
  • 对象类型(包括数组和函数):传递的是对象引用的副本,形参和实参指向同一对象,因此可通过引用修改对象内容

示例说明:

function modify(primitive, object) {
  primitive = 100;
  object.prop = 'changed';
}

let a = 1;
let b = { prop: 'initial' };

modify(a, b);
console.log(a); // 1 → 原始值未变
console.log(b); // { prop: 'changed' } → 对象内容被修改

需要注意的是,虽然可以修改对象的内容,但如果尝试重新赋值整个形参引用,则不会影响外部变量:

function reassign(obj) {
  obj = { newProp: 'new' };
}
reassign(b);
console.log(b); // 仍然是 { prop: 'changed' }

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。