前端进阶:深度剖析预解析机制

news2025/2/5 19:13:01

一、预解析是什么?

在前端开发中,我们常常会遇到一些看似不符合常规逻辑的代码执行现象,比如为什么在变量声明之前访问它,得到的结果是undefined,而不是报错?为什么函数在声明之前就可以被调用?这些问题的答案,都与前端的预解析机制有关。那预解析究竟是什么呢?

简单来说,预解析是 JavaScript 解析器在运行代码前的一个重要处理步骤。当 JavaScript 代码被加载到浏览器中时,解析器并不会立即逐行执行代码,而是会先进行预解析。在这个阶段,解析器会将所有带有var声明的变量和function声明的函数,在内存中进行提前声明或者定义。 这样做的目的是为了让 JavaScript 引擎在正式执行代码时,能够更高效地处理变量和函数的调用,避免因为变量或函数未定义而导致的错误。

二、预解析的类型

(一)全局预解析

当我们在浏览器中打开一个包含 JavaScript 代码的页面时,全局预解析就开始了。它会对全局代码进行通读,找出所有使用var声明的变量和function声明的函数。在这个过程中,变量只是被声明,并不会被赋值,而函数则会被完整地定义。需要注意的是,函数体内的代码在全局预解析阶段不会被处理 。

例如以下代码:

console.log(num);

var num = 10;

function fn() {

console.log('这是一个函数');

}

fn();

在全局预解析阶段,浏览器会先找到var num,将num声明为一个变量,但此时num的值为undefined。接着找到function fn,将fn定义为一个函数。当真正开始执行代码时,console.log(num)会输出undefined,因为此时num虽然已经声明,但还未被赋值。然后执行num = 10,给num赋值为 10。最后调用fn函数,输出 “这是一个函数”。

(二)局部预解析

局部预解析发生在函数被调用的时候。当一个函数被调用时,会创建一个私有作用域,在这个私有作用域内,会对函数内部的代码进行预解析。同样,它会查找函数内使用var声明的变量和function声明的函数,变量声明会被提前,函数也会被提前定义,解析完成后才会执行函数体里的代码。

以下面这段代码为例:

function test() {

console.log(a);

var a = 5;

console.log(a);

function inner() {

console.log('这是内部函数');

}

inner();

}

test();

在调用test函数时,进入局部预解析阶段。首先,在这个私有作用域内找到var a,将a声明为局部变量,值为undefined,同时找到function inner,将inner定义为一个函数。然后开始执行函数体代码,console.log(a)会输出undefined,接着执行a = 5,给a赋值为 5,再次执行console.log(a),输出 5。最后调用inner函数,输出 “这是内部函数”。

三、预解析解析的内容

(一)var 声明

在 JavaScript 中,使用var声明变量时,会发生变量提升现象,即变量的声明会被提升到其所在作用域的顶部,但变量的赋值操作并不会被提升,仍然保留在原来的位置。这意味着,我们可以在变量声明之前访问它,只不过此时它的值是undefined。

例如:

console.log(num);

var num = 10;

console.log(num);

在上述代码中,第一行console.log(num)输出的结果是undefined。这是因为在预解析阶段,var num被提升到了当前作用域的顶部,相当于代码变成了:

var num;

console.log(num);

num = 10;

console.log(num);

所以,在第一个console.log(num)执行时,num已经被声明,但还没有被赋值,其值为undefined。而在执行num = 10后,第二个console.log(num)输出的结果就是 10。

(二)函数声明

函数声明在预解析阶段也会被提升,与变量提升不同的是,函数声明是整个函数定义被提升,而不仅仅是函数名。这就使得我们可以在函数声明之前调用它。

例如声明式函数:

fn();

function fn() {

console.log('这是一个声明式函数');

}

在这个例子中,fn()函数调用在函数声明之前,但代码依然能够正常执行,输出 “这是一个声明式函数”。这是因为在预解析阶段,函数fn的定义被提升到了作用域的顶部,所以在调用时,JavaScript 引擎已经知道了fn是一个函数。

再看赋值式函数:

fn2();

var fn2 = function() {

console.log('这是一个赋值式函数');

};

上述代码中,fn2()函数调用会报错,提示fn2 is not a function。这是因为在预解析阶段,只有var fn2被提升,此时fn2只是一个普通变量,值为undefined,还没有被赋值为函数。当执行到fn2()时,fn2还不是一个函数,所以会报错 。只有在执行到var fn2 = function() {... }时,fn2才被赋值为一个函数。

四、预解析原理揭秘

了解了预解析的类型和内容后,我们来深入探究一下预解析的原理。在浏览器中,预解析是与 HTML 解析并行进行的一个重要过程 。当浏览器接收到 HTML 文档后,会开启一个主线程来解析 HTML,同时启动一个预解析线程。预解析线程的主要任务是扫描 HTML 文档,寻找其中的外部资源引用,如<link>标签引用的 CSS 文件、<script>标签引用的 JavaScript 文件以及<img>标签引用的图片等,并提前下载这些资源。

在解析 HTML 时,假设遇到了如下代码:

<!DOCTYPE html>

<html lang="en">

<head>

<meta charset="UTF-8">

<link rel="stylesheet" href="styles.css">

<script src="script.js"></script>

</head>

<body>

<img src="image.jpg" alt="示例图片">

</body>

</html>

主线程在解析到<link rel="stylesheet" href="styles.css">时,会继续解析后续的 HTML 内容,而预解析线程则会立即开始下载styles.css文件。同样,当解析到<script src="script.js"></script>时,预解析线程也会下载script.js,以及解析到<img src="image.jpg" alt="示例图片">时,下载image.jpg。这样,当主线程后续需要这些资源时,它们可能已经被下载完成,从而减少了等待时间,提高了页面的加载速度和渲染效率。

需要注意的是,对于<script>标签,如果没有使用async或defer属性,浏览器会按照默认行为暂停 HTML 解析,并等待脚本下载和执行完毕。因为 JavaScript 代码的执行可能会修改 DOM 树结构,所以为了保证 DOM 树的一致性,在执行 JavaScript 时,HTML 解析会被暂停。但在预解析阶段,预解析线程可以提前开始下载这些脚本文件,即使尚未到达需要执行它们的部分。

五、预解析常见问题及解决

(一)变量提升与作用域

在 JavaScript 中,变量提升和作用域的概念紧密相关,这也导致了一些容易让人困惑的问题。

  • 局部变量遮蔽全局变量:当局部作用域中声明了与全局变量同名的变量时,就会发生局部变量遮蔽全局变量的情况。在局部作用域内,对该变量的访问和操作都将针对局部变量,而不会影响到全局变量。

例如:

var num = 10;

function test() {

var num = 20;

console.log(num);

}

test();

console.log(num);

在上述代码中,全局作用域中声明了变量num并赋值为 10。在test函数内部,又声明了一个同名的局部变量num并赋值为 20。在test函数内,console.log(num)输出的是局部变量num的值 20,因为局部变量遮蔽了全局变量。而在函数外部,console.log(num)输出的依然是全局变量num的值 10 。

  • 函数参数与变量提升:当函数参数与函数内部使用var声明的变量同名时,会出现一些特殊的情况。函数参数会被优先提升,并且函数内部的var声明会被忽略,但赋值操作仍然会执行。

例如:

function fun(param) {

console.log(param);

var param = function () {

console.log(1);

};

console.log(param);

}

fun(5);

在这个例子中,调用fun(5)时,参数param被赋值为 5。在函数内部,虽然有var param的声明,但由于参数param已经存在,这个声明会被忽略。所以,第一个console.log(param)输出的是参数param的值 5。接着,param被赋值为一个函数,此时第二个console.log(param)输出的就是这个函数 。

为了避免这些问题,我们在编写代码时,应该遵循一些最佳实践:

  • 尽量避免在不同作用域中使用相同的变量名,以减少变量遮蔽带来的困惑。
  • 养成良好的变量命名习惯,使变量名具有描述性,能够清晰地表达其用途。
  • 在函数内部,明确区分函数参数和局部变量,避免同名冲突。

(二)函数声明与变量声明的优先级

在 JavaScript 中,函数声明和变量声明都存在提升现象,但函数声明的优先级高于变量声明。这意味着在预解析阶段,函数声明会先被提升到作用域的顶部,然后才是变量声明。

当函数声明和变量声明同名时,函数声明会覆盖变量声明,但变量的赋值操作会在执行阶段覆盖函数的定义。例如:

console.log(foo);

function foo() {

console.log('这是一个函数');

}

var foo = 10;

console.log(foo);

在上述代码中,第一个console.log(foo)输出的是函数foo的定义。这是因为在预解析阶段,函数声明function foo()被提升到了作用域的顶部,此时foo是一个函数。接着,var foo的声明也被提升,但它不会覆盖已经存在的函数声明。在执行阶段,foo = 10将foo赋值为 10,所以第二个console.log(foo)输出的是 10 。

再看下面这个例子:

function bar() {

console.log(a);

var a = 5;

function a() {

console.log('这是内部函数');

}

console.log(a);

}

bar();

在bar函数中,预解析时,函数声明function a()先被提升,然后是var a的声明(这个声明会被忽略,因为已经有同名的函数声明)。所以第一个console.log(a)输出的是函数a。接着,执行a = 5,将a赋值为 5,此时第二个console.log(a)输出的就是 5。如果此时再调用a(),就会报错,因为a已经被赋值为 5,不再是一个函数 。

理解函数声明和变量声明的优先级,有助于我们写出更准确、可维护的代码。在实际开发中,要避免函数声明和变量声明同名,以免造成不必要的错误和困惑。

六、预解析对前端性能的影响

预解析在前端性能优化方面发挥着重要作用,尤其是在网络资源加载和页面渲染速度上。

(一)缩短 DNS 解析时间

DNS 解析是将域名转换为 IP 地址的过程,这个过程通常会消耗一定的时间,而 DNS 预解析(dns-prefetch)技术可以提前解析之后可能会用到的域名,使解析结果缓存到系统缓存中,从而缩短 DNS 解析时间。当浏览器解析 HTML 文档时,会遇到各种资源的引用,如<script>标签引用的 JavaScript 文件、<link>标签引用的 CSS 文件以及<img>标签引用的图片等,这些资源可能来自不同的域名,每次访问不同域名都需要进行 DNS 解析。例如,在一个电商网站的页面中,不仅有来自主域名的商品信息展示,还引用了第三方 CDN 上的图片资源和字体文件,以及其他合作平台的广告脚本,这些不同来源的资源都需要进行 DNS 解析。通过 DNS 预解析,浏览器可以在后台提前完成这些域名的解析工作,当真正需要加载这些资源时,就可以直接使用已经解析好的 IP 地址,减少了等待 DNS 解析的时间。

(二)提高页面加载速度

通过提前解析域名,浏览器能够更快地建立与服务器的连接,从而加快资源的下载速度。在一个复杂的前端应用中,可能会有大量的 JavaScript、CSS 和图片等资源需要加载。以一个在线新闻网站为例,页面上除了文章内容,还包含各种配图、广告、推荐文章链接等,这些资源分布在不同的域名下。如果没有预解析,浏览器在加载这些资源时,需要逐个进行 DNS 解析,这会导致页面加载时间延长。而使用预解析后,在页面解析的同时,DNS 解析已经在后台完成,资源可以更快地被下载和加载,大大提高了页面的加载速度,用户能够更快地看到完整的页面内容,提升了用户体验。

(三)减少资源加载阻塞

在浏览器解析 HTML 文档时,如果遇到<script>标签,会暂停 HTML 解析,先去加载和执行 JavaScript 代码,这是因为 JavaScript 代码可能会修改 DOM 树结构,为了保证 DOM 树的一致性,需要先执行 JavaScript。而在加载 JavaScript 文件时,又会涉及到 DNS 解析、建立连接、下载文件等过程,如果 DNS 解析时间过长,就会阻塞页面的渲染。预解析可以提前完成 DNS 解析,减少了这一过程对页面渲染的阻塞。例如,在一个视频播放网站中,视频播放器的初始化脚本可能需要从不同的域名获取配置信息和资源,通过预解析,这些域名的解析工作可以提前完成,当解析到<script>标签时,能够更快地加载和执行脚本,减少了视频播放前的等待时间,让用户能够更流畅地观看视频,避免了因资源加载阻塞而导致的页面卡顿或白屏现象。

七、最后小结

前端预解析作为 JavaScript 解析过程中的重要环节,对代码的执行顺序和结果有着深远的影响。它通过提前声明变量和函数,为代码的顺利执行奠定了基础。同时,DNS 预解析等技术的应用,也在前端性能优化方面发挥着关键作用,显著提升了页面的加载速度和用户体验。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2293440.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【力扣】53.最大子数组和

AC截图 题目 思路 这道题主要考虑的就是要排除负数带来的负面影响。如果遍历数组&#xff0c;那么应该有如下关系式&#xff1a; currentAns max(prenums[i],nums[i]) pre是之前记录的最大和&#xff0c;如果prenums[i]小于nums[i]&#xff0c;就要考虑舍弃pre&#xff0c;从…

基于Spring Security 6的OAuth2 系列之七 - 授权服务器--自定义数据库客户端信息

之所以想写这一系列&#xff0c;是因为之前工作过程中使用Spring Security OAuth2搭建了网关和授权服务器&#xff0c;但当时基于spring-boot 2.3.x&#xff0c;其默认的Spring Security是5.3.x。之后新项目升级到了spring-boot 3.3.0&#xff0c;结果一看Spring Security也升级…

vim-plug的自动安装与基本使用介绍

vim-plug介绍 Vim-plug 是一个轻量级的 Vim 插件管理器&#xff0c;它允许你轻松地管理 Vim 插件的安装、更新和卸载。相较于其他插件管理器&#xff0c;vim-plug 的优点是简单易用&#xff0c;速度较快&#xff0c;而且支持懒加载插件&#xff08;即按需加载&#xff09; 自动…

Deep Crossing:深度交叉网络在推荐系统中的应用

实验和完整代码 完整代码实现和jupyter运行&#xff1a;https://github.com/Myolive-Lin/RecSys--deep-learning-recommendation-system/tree/main 引言 在机器学习和深度学习领域&#xff0c;特征工程一直是一个关键步骤&#xff0c;尤其是对于大规模的推荐系统和广告点击率预…

想品客老师的第十天:类

类是一个优化js面向对象的工具 类的声明 //1、class User{}console.log(typeof User)//function//2、let Hdclass{}//其实跟1差不多class Stu{show(){}//注意这里不用加逗号&#xff0c;对象才加逗号get(){console.log(后盾人)}}let hdnew Stu()hd.get()//后盾人 类的原理 类…

MyBatis-Plus速成指南:条件构造器和常用接口

Wrapper 介绍 Wrapper&#xff1a;条件构造抽象类&#xff0c;最顶端父类 AbstractWrapper&#xff1a;用于查询条件封装&#xff0c;生成 SQL 的 where 条件QueryWrapper&#xff1a;查询条件封装UpdateWrapper&#xff1a;Update 条件封装AbstractLambdaWrapper&#xff1a;使…

(脚本学习)BUU18 [CISCN2019 华北赛区 Day2 Web1]Hack World1

自用 题目 考虑是不是布尔盲注&#xff0c;如何测试&#xff1a;用"1^1^11 1^0^10&#xff0c;就像是真真真等于真&#xff0c;真假真等于假"这个测试 SQL布尔盲注脚本1 import requestsurl "http://8e4a9bf2-c055-4680-91fd-5b969ebc209e.node5.buuoj.cn…

【Numpy核心编程攻略:Python数据处理、分析详解与科学计算】2.25 多线程并行:GIL绕过与真正并发

2.25 多线程并行&#xff1a;GIL绕过与真正并发 目录 #mermaid-svg-JO4lsTIyjOweVkos {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-JO4lsTIyjOweVkos .error-icon{fill:#552222;}#mermaid-svg-JO4lsTIyjOweVkos …

Java 大视界 -- Java 大数据在智能医疗影像诊断中的应用(72)

💖亲爱的朋友们,热烈欢迎来到 青云交的博客!能与诸位在此相逢,我倍感荣幸。在这飞速更迭的时代,我们都渴望一方心灵净土,而 我的博客 正是这样温暖的所在。这里为你呈上趣味与实用兼具的知识,也期待你毫无保留地分享独特见解,愿我们于此携手成长,共赴新程!💖 一、…

【Leetcode刷题记录】1456. 定长子串中元音的最大数目---定长滑动窗口即解题思路总结

1456. 定长子串中元音的最大数目 给你字符串 s 和整数 k 。请返回字符串 s 中长度为 k 的单个子字符串中可能包含的最大元音字母数。 英文中的 元音字母 为&#xff08;a, e, i, o, u&#xff09;。 这道题的暴力求解的思路是通过遍历字符串 s 的每一个长度为 k 的子串&#xf…

upload-labs安装与配置

前言 作者进行upload-labs靶场练习时&#xff0c;在环境上出了很多问题&#xff0c;吃了很多苦头&#xff0c;甚至改了很多配置也没有成功。 upload-labs很多操作都是旧时代的产物了&#xff0c;配置普遍都比较老&#xff0c;比如PHP版本用5.2.17&#xff08;还有中间件等&am…

从Transformer到世界模型:AGI核心架构演进

文章目录 引言:架构革命推动AGI进化一、Transformer:重新定义序列建模1.1 注意力机制的革命性突破1.2 从NLP到跨模态演进1.3 规模扩展的黄金定律二、通向世界模型的关键跃迁2.1 从语言模型到认知架构2.2 世界模型的核心特征2.3 混合架构的突破三、构建世界模型的技术路径3.1 …

每日一博 - 三高系统架构设计:高性能、高并发、高可用性解析

文章目录 引言一、高性能篇1.1 高性能的核心意义1.2 影响系统性能的因素1.3 高性能优化方法论1.3.1 读优化&#xff1a;缓存与数据库的结合1.3.2 写优化&#xff1a;异步化处理 1.4 高性能优化实践1.4.1 本地缓存 vs 分布式缓存1.4.2 数据库优化 二、高并发篇2.1 高并发的核心意…

【工欲善其事】利用 DeepSeek 实现复杂 Git 操作:从原项目剥离出子版本树并同步到新的代码库中

文章目录 利用 DeepSeek 实现复杂 Git 操作1 背景介绍2 需求描述3 思路分析4 实现过程4.1 第一次需求确认4.2 第二次需求确认4.3 第三次需求确认4.4 V3 模型&#xff1a;中间结果的处理4.5 方案验证&#xff0c;首战告捷 5 总结复盘 利用 DeepSeek 实现复杂 Git 操作 1 背景介绍…

【C++】线程池实现

目录 一、线程池简介线程池的核心组件实现步骤 二、C11实现线程池源码 三、线程池源码解析1. 成员变量2. 构造函数2.1 线程初始化2.2 工作线程逻辑 3. 任务提交(enqueue方法)3.1 方法签名3.2 任务封装3.3 任务入队 4. 析构函数4.1 停机控制 5. 关键技术点解析5.1 完美转发实现5…

数据结构实战之线性表(三)

目录 1.顺序表释放 2.顺序表增加空间 3.合并顺序表 4.线性表之链表实现 1.项目结构以及初始代码 2.初始化链表(不带头结点) 3.链表尾部插入数据并显示 4.链表头部插入数据 5.初始化链表&#xff08;带头结点&#xff09; 6.带头结点的链表头部插入数据并显示 7.带头结…

【python】python基于机器学习与数据分析的手机特性关联与分类预测(源码+数据集)【独一无二】

&#x1f449;博__主&#x1f448;&#xff1a;米码收割机 &#x1f449;技__能&#x1f448;&#xff1a;C/Python语言 &#x1f449;专__注&#x1f448;&#xff1a;专注主流机器人、人工智能等相关领域的开发、测试技术。 python基于机器学习与数据分析的手机特性关联与分类…

ZOJ 1007 Numerical Summation of a Series

原题目链接 生成该系列值的表格 对于x 的 2001 个值&#xff0c;x 0.000、0.001、0.002、…、2.000。表中的所有条目的绝对误差必须小于 0.5e-12&#xff08;精度为 12 位&#xff09;。此问题基于 Hamming (1962) 的一个问题&#xff0c;当时的大型机按今天的微型计算机标准来…

全面解析文件上传下载删除漏洞:风险与应对

在数字化转型的时代&#xff0c;文件上传、下载与删除功能已经成为各类应用程序的标准配置&#xff0c;从日常办公使用的协同平台&#xff0c;到云端存储服务&#xff0c;再到社交网络应用&#xff0c;这些功能在给用户带来便捷体验、显著提升工作效率的同时&#xff0c;也隐藏…

【C语言深入探索】结构体详解(二):使用场景

目录 一、复杂数据的表示 二、数据的封装 三、多态的模拟 四、回调函数的实现 五、多线程编程 六、通信协议的实现和文件操作 6.1. 使用结构体实现简单通信协议 6.2. 使用结构体进行文件操作 七、图形界面编程 结构体在C语言中具有广泛的应用场景&#xff0c;以下是一…