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

一、预解析是什么?

在前端开发中,我们常常会遇到一些看似不符合常规逻辑的代码执行现象,比如为什么在变量声明之前访问它,得到的结果是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 预解析等技术的应用,也在前端性能优化方面发挥着关键作用,显著提升了页面的加载速度和用户体验。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

stm32教程:EXTI外部中断应用

早上好啊大佬们&#xff0c;上一期我们讲了EXTI外部中断的原理以及基础代码的书写&#xff0c;这一期就来尝试一下用它来写一些有实际效能的工程吧。 这一期里&#xff0c;我用两个案例代码来让大家感受一下外部中断的作用和使用价值。 旋转编码器计数 整体思路讲解 这里&…

数据分析系列--⑦RapidMiner模型评价(基于泰坦尼克号案例含数据集)

一、前提 二、模型评估 1.改造⑥ 2.Cross Validation算子说明 2.1Cross Validation 的作用 2.1.1 模型评估 2.1.2 减少过拟合 2.1.3 数据利用 2.2 Cross Validation 的工作原理 2.2.1 数据分割 2.2.2 迭代训练与测试 ​​​​​​​ 2.2.3 结果汇总 ​​​​​​​ …

WPS mathtype间距太大、显示不全、公式一键改格式/大小

1、间距太大 用mathtype后行距变大的原因 mathtype行距变大到底怎么解决-MathType中文网 段落设置固定值 2、显示不全 设置格式&#xff1a; 打开MathType编辑器点击菜单栏中的"格式(Format)"选择"间距(Spacing)"在弹出的对话框中调整"分数间距(F…

【Postman接口测试】Postman的安装和使用

在软件测试领域&#xff0c;接口测试是保障软件质量的关键环节之一&#xff0c;而Postman作为一款功能强大且广受欢迎的接口测试工具&#xff0c;能够帮助测试人员高效地进行接口测试工作。本文将详细介绍Postman的安装和使用方法&#xff0c;让你快速上手这款工具。 一、Pos…

因果推断与机器学习—用机器学习解决因果推断问题

Judea Pearl 将当前备受瞩目的机器学习研究戏谑地称为“仅限于曲线拟合”,然而,曲线拟合的实现绝非易事。机器学习模型在图像识别、语音识别、自然语言处理、蛋白质分子结构预测以及搜索推荐等多个领域均展现出显著的应用效果。 在因果推断任务中,在完成因果效应识别之后,需…

python算法和数据结构刷题[2]:链表、队列、栈

链表 链表的节点定义&#xff1a; class Node():def __init__(self,item,nextNone):self.itemitemself.nextNone 删除节点&#xff1a; 删除节点前的节点的next指针指向删除节点的后一个节点 添加节点&#xff1a; 单链表 class Node():"""单链表的结点&quo…

AJAX案例——图片上传个人信息操作

黑马程序员视频地址&#xff1a; AJAX-Day02-11.图片上传https://www.bilibili.com/video/BV1MN411y7pw?vd_source0a2d366696f87e241adc64419bf12cab&spm_id_from333.788.videopod.episodes&p26 图片上传 <!-- 文件选择元素 --><input type"file"…

deepseek大模型本机部署

2024年1月20日晚&#xff0c;中国DeepSeek发布了最新推理模型DeepSeek-R1&#xff0c;引发广泛关注。这款模型不仅在性能上与OpenAI的GPT-4相媲美&#xff0c;更以开源和创新训练方法&#xff0c;为AI发展带来了新的可能性。 本文讲解如何在本地部署deepseek r1模型。deepseek官…

使用 Ollama 和 Kibana 在本地为 RAG 测试 DeepSeek R1

作者&#xff1a;来自 Elastic Dave Erickson 及 Jakob Reiter 每个人都在谈论 DeepSeek R1&#xff0c;这是中国对冲基金 High-Flyer 的新大型语言模型。现在他们推出了一款功能强大、具有开放权重的思想链推理 LLM&#xff0c;这则新闻充满了对行业意味着什么的猜测。对于那些…

Greenplum临时表未清除导致库龄过高处理

1.问题 Greenplum集群segment后台日志报错 2.回收库龄 master上执行 vacuumdb -F -d cxy vacuumdb -F -d template1 vacuumdb -F -d rptdb 3.回收完成后检查 仍然发现segment还是有库龄报警警告信息发出 4.检查 4.1 在master上检查库年龄 SELECT datname, datfrozen…

栈和队列特别篇:栈和队列的经典算法问题

图均为手绘,代码基于vs2022实现 系列文章目录 数据结构初探: 顺序表 数据结构初探:链表之单链表篇 数据结构初探:链表之双向链表篇 链表特别篇:链表经典算法问题 数据结构:栈篇 数据结构:队列篇 文章目录 系列文章目录前言一.有效的括号(leetcode 20)二.用队列实现栈(leetcode…

记录一次,PyQT的报错,多线程Udp失效,使用工具如netstat来检查端口使用情况。

1.问题 报错Exception in thread Thread-1: Traceback (most recent call last): File "threading.py", line 932, in _bootstrap_inner File "threading.py", line 870, in run File "main.py", line 456, in udp_recv IndexError: list…

论文阅读(十):用可分解图模型模拟连锁不平衡

1.论文链接&#xff1a;Modeling Linkage Disequilibrium with Decomposable Graphical Models 摘要&#xff1a; 本章介绍了使用可分解的图形模型&#xff08;DGMs&#xff09;表示遗传数据&#xff0c;或连锁不平衡&#xff08;LD&#xff09;&#xff0c;各种下游应用程序之…

本地部署DeepSeek开源多模态大模型Janus-Pro-7B实操

本地部署DeepSeek开源多模态大模型Janus-Pro-7B实操 Janus-Pro-7B介绍 Janus-Pro-7B 是由 DeepSeek 开发的多模态 AI 模型&#xff0c;它在理解和生成方面取得了显著的进步。这意味着它不仅可以处理文本&#xff0c;还可以处理图像等其他模态的信息。 模型主要特点:Permalink…

从 UTC 日期时间字符串获取 Unix 时间戳:C 和 C++ 中的挑战与解决方案

在编程世界里&#xff0c;从 UTC 日期时间字符串获取 Unix 时间戳&#xff0c;看似简单&#xff0c;实则暗藏玄机。你以为输入一个像 “Fri, 17 Jan 2025 06:07:07” 这样的 UTC 时间&#xff0c;然后轻松得到 1737094027&#xff08;从 1970 年 1 月 1 日 00:00:00 UTC 开始经…

Linux——网络(tcp)

文章目录 目录 文章目录 前言 一、TCP逻辑 1. 面向连接 三次握手&#xff08;建立连接&#xff09; 四次挥手&#xff08;关闭连接&#xff09; 2. 可靠性 3. 流量控制 4. 拥塞控制 5. 基于字节流 6. 全双工通信 7. 状态机 8. TCP头部结构 9. TCP的应用场景 二、编写tcp代码函数…

51单片机(STC89C52)开发:点亮一个小灯

软件安装&#xff1a; 安装开发板CH340驱动。 安装KEILC51开发软件&#xff1a;C51V901.exe。 下载软件&#xff1a;PZ-ISP.exe 创建项目&#xff1a; 新建main.c 将main.c加入至项目中&#xff1a; main.c:点亮一个小灯 #include "reg52.h"sbit LED1P2^0; //P2的…

力扣116. 填充每个节点的下一个右侧节点指针

Problem: 116. 填充每个节点的下一个右侧节点指针 文章目录 题目描述思路复杂度Code 题目描述 思路 遍历思想(利用二叉树的先序遍历) 本题目的难点在于对于不同父节点的邻接问题因此我们可以抽象将两两节点为一组&#xff08;不同父节点的两个孩子节点也抽象为一组&#xff09…

k8s简介,k8s环境搭建

目录 K8s简介环境搭建和准备工作修改主机名&#xff08;所有节点&#xff09;配置静态IP&#xff08;所有节点&#xff09;关闭防火墙和seLinux&#xff0c;清除iptables规则&#xff08;所有节点&#xff09;关闭交换分区&#xff08;所有节点&#xff09;修改/etc/hosts文件&…

苯乙醇苷类化合物的从头生物合成-文献精读108

Complete pathway elucidation of echinacoside in Cistanche tubulosa and de novo biosynthesis of phenylethanoid glycosides 管花肉苁蓉中松果菊苷全生物合成途径解析及苯乙醇苷类化合物的从头生物合成 摘要 松果菊苷&#xff08;ECH&#xff09;是最具代表性的苯乙醇苷…