作用域链与闭包

初学JavaScript的时候,我在学习闭包上,走了很多弯路。而这次重新回过头来对基础知识进行梳理,要讲清楚闭包,也是一个非常大的挑战。 闭包有多重要?如果你是初入前端的朋友,我没有办法直观的告诉你闭包在实际开发中的无处不在,但是我可以告诉你,前端面试,必问闭包。面试官们常常用对闭包的了解程度来判定面试者的基础水平,保守估计,10个前端面试者,至少5个都死在闭包上。 可是为什么,闭包如此重要,还是有那么多人没有搞清楚呢?是因为大家不愿意学习吗?还真不是,而是我们通过搜索找到的大部分讲解闭包的中文文章,都没有清晰明了的把闭包讲解清楚。要么浅尝辄止,要么高深莫测,要么干脆就直接乱说一通。包括我自己曾经也写过一篇关于闭包的总结,回头一看,不忍直视[捂脸]。 因此本文的目的就在于,能够清晰明了得把闭包说清楚,让读者朋友们看了之后,就把闭包给彻底学会了,而不是似懂非懂。 一、作用域与作用域链 在详细讲解作用域链之前,我默认你已经大概明白了JavaScript中的下面这些重要概念。这些概念将会非常有帮助。 基础数据类型与引用数据类型 内存空间 垃圾回收机制 执行上下文 变量对象与活动对象 如果你暂时还没有明白,可以去看本系列的前三篇文章,本文文末有目录链接。为了讲解闭包,已经为大家做好了基础知识的铺垫哦。 作用域 在JavaScript中,我们可以将作用域定义为一套规则,这套规则用来管理引擎如何在当前作用域以及嵌套的子作用域中根据标识符名称进行变量查找。 这里的标识符,指的是变量名或者函数名 JavaScript中只有全局作用域与函数作用域(因为eval我们平时开发中几乎不会用到它,这里不讨论)。 作用域与执行上下文是完全不同的两个概念。我知道很多人会混淆他们,但是一定要仔细区分。 JavaScript代码的整个执行过程,分为两个阶段,代码编译阶段与代码执行阶段。编译阶段由编译器完成,将代码翻译成可执行代码,这个阶段作用域规则会确定。执行阶段由引擎完成,主要任务是执行可执行代码,执行上下文在这个阶段创建。 作用域链 回顾一下,我们分析过的执行上下文的生命周期,如下图: 我们知道函数在调用激活时,会开始创建对应的执行上下文,在执行上下文生成的过程中,变量对象,作用域链,以及this的值会分别被确定。之前一篇文章我们详细说明了变量对象,而这里,我们将详细说明作用域链。 作用域链,是由当前环境与上层环境的一系列变量对象组成,它保证了当前执行环境对符合访问权限的变量和函数的有序访问。 为了帮助大家理解作用域链,我我们先结合一个例子,以及相应的图示来说明。 var a = 20; function test() { var b = a + 10; function innerTest() { var c = 10; return b + c; } return innerTest(); } test(); 在上面的例子中,全局,函数test,函数innerTest的执行上下文先后创建。我们设定他们的变量对象分别为VO(global),VO(test), VO(innerTest)。而innerTest的作用域链,则同时包含了这三个变量对象,所以innerTest的执行上下文可如下表示。 innerTestEC = { VO: {...}, // 变量对象 scopeChain: [VO(innerTest), VO(test), VO(global)], // 作用域链 } 我们可以直接用一个数组来表示作用域链,数组的第一项scopeChain[0]为作用域链的最前端,而数组的最后一项,为作用域链的最末端,所有的最末端都为全局变量对象。 很多人会误解为当前作用域与上层作用域为包含关系,但其实并不是。以最前端为起点,最末端为终点的单方向通道我认为是更加贴切的形容。如图。 注意,因为变量对象在执行上下文进入执行阶段时,就变成了活动对象,这一点在上一篇文章中已经讲过,因此图中使用了AO来表示。Active Object 是的,作用域链是由一系列变量对象组成,我们可以在这个单向通道中,查询变量对象中的标识符,这样就可以访问到上一层作用域中的变量了。 二、闭包 对于那些有一点 JavaScript 使用经验但从未真正理解闭包概念的人来说,理解闭包可以看作是某种意义上的重生,突破闭包的瓶颈可以使你功力大增。 闭包是一种特殊的对象。 它由两部分组成。执行上下文(代号A),以及在该执行上下文中创建的函数(代号B)。 当B执行时,如果访问了A中变量对象中的值,那么闭包就会产生。 在大多数理解中,包括许多著名的书籍,文章里都以函数B的名字代指这里生成的闭包。而在chrome中,则以执行上下文A的函数名代指闭包。 因此我们只需要知道,一个闭包对象,由A、B共同组成,在以后的篇幅中,我将以chrome的标准来称呼。 // demo01 function foo() { var a = 20; var b = 30; function bar() { return a + b; } return bar; } var bar = foo(); bar(); 上面的例子,首先有执行上下文foo,在foo中定义了函数bar,而通过对外返回bar的方式让bar得以执行。当bar执行时,访问了foo内部的变量a,b。因此这个时候闭包产生。 ...

October 1, 2024

iOS中import详解

源码版本说明:本文涉及的源码基于 LLVM/Clang 23.0.0 开发版本(llvm-project main分支,commit: 301c0d91b558,2026-01-16)。不同版本的实现细节可能略有差异,但核心机制保持一致。 在Objective-C开发中,头文件引用是日常开发中最基础的操作之一。本文将从Clang编译器的角度,深入讲解iOS中各种import方式的区别、底层查找原理以及最佳实践。 引用方式 语法示例 适用场景 #include #include "Header.h" C/C++传统方式 #import #import "Header.h" Objective-C项目内部引用 #import #import <Framework/Header.h> 系统/第三方Framework @import @import Foundation; Clang Modules方式 PCH Prefix Header文件 预编译公共头文件 #include vs #import #include的工作原理 #include是C/C++中的头文件引用方式,它的工作原理非常简单:预处理器会将目标头文件的内容原封不动地复制粘贴到当前文件中。 这种方式存在一个问题:重复引用。如果多个文件都include了同一个头文件,或者存在循环引用,会导致编译错误。传统的解决方案是使用Include Guard: // Header.h #ifndef HEADER_H #define HEADER_H // 头文件内容 #endif 现代编译器还支持#pragma once指令,功能相同但更简洁: // Header.h #pragma once // 头文件内容 #import的增强 #import是Objective-C对#include的封装和增强。它在#include的基础上增加了一层判重逻辑,自动防止同一个头文件被重复引用。 // BClass.m #import "AClass.h" #import "AClass.h" // 第二次import会被自动忽略 双引号 vs 尖括号 在日常开发中,我们经常会看到两种不同的引用方式: #import "MyClass.h" // 双引号形式 #import <UIKit/UIKit.h> // 尖括号形式 它们的核心区别在于搜索路径的范围不同: ...

June 8, 2026

Prompt Engineering

大语言模型(LLM)的能力边界很大程度上取决于你怎么跟它说话。同样的模型,一个精心设计的Prompt可以得到专业级的回答,一个随意的Prompt可能只得到平庸甚至错误的结果。Prompt Engineering就是研究如何有效地与LLM沟通的技术。 一、Prompt的基本构成 1.1 一个Prompt的解剖 一个高质量的Prompt通常包含以下要素: graph TD P["完整的Prompt"] --> R["角色Role"] P --> C["上下文Context"] P --> I["指令Instruction"] P --> E["示例Examples"] P --> F["格式要求Format"] P --> CO["约束Constraints"] 要素 作用 示例 角色 设定模型的身份和专业背景 “你是一位有10年经验的iOS架构师” 上下文 提供背景信息 “我们的项目使用MVVM架构,Swift语言” 指令 明确说明任务 “请review以下代码并指出问题” 示例 展示期望的输入输出格式 给出一个输入-输出样例 格式要求 指定输出的结构 “以Markdown表格形式输出” 约束 限制条件 “不超过500字"“只使用Swift标准库” 1.2 System Prompt vs User Prompt 在API调用中,消息分为不同的角色: { "messages": [ { "role": "system", "content": "你是一位资深iOS开发工程师,擅长性能优化。回答时给出具体的代码示例。" }, { "role": "user", "content": "如何优化UITableView的滚动性能?" } ] } System Prompt:设定模型的整体行为、角色和约束,贯穿整个对话 User Prompt:具体的问题或指令 Assistant:模型的历史回复,提供对话上下文 二、核心Prompt技术 2.1 Zero-Shot Prompting 直接给指令,不提供任何示例: ...

May 2, 2026

策略模式

定义 策略模式(Strategy Pattern)是一种行为型设计模式,它定义了一系列算法,将每个算法封装起来,并使它们可以相互替换。策略模式让算法独立于使用它的调用者而变化。 策略模式的核心思想是:将算法的定义与使用分离,通过组合而非继承来实现算法的切换。 为什么需要策略模式 在实际开发中,我们经常会遇到这样的场景:同一个功能有多种实现方式,而且这些实现方式需要根据不同条件进行切换。 问题场景:假设我们正在开发一个电商App的支付功能,需要支持信用卡、Apple Pay、支付宝等多种支付方式。 最直接的实现方式可能是这样: func processPayment(type: String, amount: Double) { if type == "creditCard" { // 信用卡支付逻辑(可能有几十行代码) print("Processing credit card payment...") } else if type == "applePay" { // Apple Pay支付逻辑 print("Processing Apple Pay...") } else if type == "alipay" { // 支付宝支付逻辑 print("Processing Alipay...") } // 后续可能还要添加更多支付方式... } 这种实现方式存在几个明显的问题: 违反开闭原则:每次添加新的支付方式,都需要修改这个函数,增加新的分支 代码臃肿:随着支付方式增多,函数会变得越来越长,难以维护 测试困难:所有支付逻辑耦合在一起,难以单独测试某种支付方式 复用性差:如果其他地方也需要使用某种支付逻辑,只能复制代码 策略模式的解决思路: 策略模式将每种支付方式抽取为独立的类(策略),它们都实现相同的接口。调用方只需要持有策略接口的引用,不需要知道具体是哪种实现。 这样做的好处是: 新增支付方式:只需要新建一个策略类,无需修改现有代码 代码清晰:每种支付逻辑独立封装,职责单一 易于测试:可以对每种策略单独进行单元测试 运行时切换:用户可以随时切换支付方式,系统只需要替换策略对象 简单来说,策略模式就是把「做什么」和「怎么做」分离开来——调用方只关心「做什么」,具体「怎么做」由不同的策略类来决定。 模式结构 classDiagram class Context { -strategy: Strategy +setStrategy(strategy: Strategy) +executeStrategy() } class Strategy { <<interface>> +execute() } class ConcreteStrategyA { +execute() } class ConcreteStrategyB { +execute() } class ConcreteStrategyC { +execute() } Context o-- Strategy Strategy <|.. ConcreteStrategyA Strategy <|.. ConcreteStrategyB Strategy <|.. ConcreteStrategyC 角色说明 Strategy(策略接口):定义所有支持的算法的公共接口 ConcreteStrategy(具体策略):实现Strategy接口的具体算法 Context(上下文):持有Strategy的引用,负责调用策略方法 iOS中的实现 基础实现 // 策略协议 protocol PaymentStrategy { func pay(amount: Double) -> Bool var name: String { get } } // 具体策略 - 信用卡支付 class CreditCardPayment: PaymentStrategy { private let cardNumber: String private let cvv: String var name: String { "Credit Card" } init(cardNumber: String, cvv: String) { self.cardNumber = cardNumber self.cvv = cvv } func pay(amount: Double) -> Bool { print("Paying \(amount) using Credit Card ending with \(cardNumber.suffix(4))") // 实际支付逻辑 return true } } // 具体策略 - Apple Pay class ApplePayPayment: PaymentStrategy { var name: String { "Apple Pay" } func pay(amount: Double) -> Bool { print("Paying \(amount) using Apple Pay") // 调用Apple Pay SDK return true } } // 上下文 class PaymentContext { private var strategy: PaymentStrategy init(strategy: PaymentStrategy) { self.strategy = strategy } func setStrategy(_ strategy: PaymentStrategy) { self.strategy = strategy } func checkout(amount: Double) -> Bool { print("Processing payment with \(strategy.name)...") return strategy.pay(amount: amount) } } // 使用 let creditCard = CreditCardPayment(cardNumber: "1234567890123456", cvv: "123") let context = PaymentContext(strategy: creditCard) context.checkout(amount: 99.99) // 切换支付方式 let applePay = ApplePayPayment() context.setStrategy(applePay) context.checkout(amount: 99.99) 使用闭包简化策略 对于简单的策略,可以使用闭包代替完整的类: ...

May 2, 2026

包管理

现如今,前端开发的同学已经离不开 npm 这个包管理工具,其优秀的包版本管理机制承载了整个繁荣发展的NodeJS社区,理解其内部机制非常有利于加深我们对模块开发的理解、各项前端工程化的配置以加快我们排查问题(相信不少同学收到过各种依赖问题的困扰)的速度。 本文从三个角度:package.json、版本管理、依赖安装结合具体实例对 npm 的包管理机制进行了详细分析。 一、剖析 package.json 在 Node.js 中,模块是一个库或框架,也是一个 Node.js 项目。Node.js 项目遵循模块化的架构,当我们创建了一个 Node.js 项目,意味着创建了一个模块,这个模块必须有一个描述文件,即 package.json。它是我们最常见的配置文件,但是它里面的配置你真的有详细了解过吗?配置一个合理的 package.json 文件直接决定着我们项目的质量,所以首先带大家分析下 package.json 的各项详细配置。 1.1 必备属性 package.json 中有非常多的属性,其中必须填写的只有两个:name 和 version ,这两个属性组成一个 npm 模块的唯一标识。 npm包命名规则 name 即模块名称,其命名时需要遵循官方的一些规范和建议: 包名会成为模块url、命令行中的一个参数或者一个文件夹名称,任何非url安全的字符在包名中都不能使用,可以使用 validate-npm-package-name 包来检测包名是否合法。 语义化包名,可以帮助开发者更快的找到需要的包,并且避免意外获取错误的包。 若包名称中存在一些符号,将符号去除后不得与现有包名重复 例如:由于react-native已经存在,react.native、reactnative都不可以再创建。 如果你的包名与现有的包名太相近导致你不能发布这个包,那么推荐将这个包发布到你的作用域下。 例如:用户名 conard,那么作用域为 @conard,发布的包可以是@conard/react。 查看包是否被占用 name 是一个包的唯一标识,不得和其他包名重复,我们可以执行 npm view packageName 查看包是否被占用,并可以查看它的一些基本信息: 若包名称从未被使用过,则会抛出 404 错误: 另外,你还可以去 https://www.npmjs.com/ 查询更多更详细的包信息。 1.2描述信息 基本描述 { "description": "An enterprise-class UI design language and React components implementation", "keywords": [ "ant", "component", "components", "design", "framework", "frontend", "react", "react-component", "ui" ] } description用于添加模块的的描述信息,方便别人了解你的模块。 ...

August 7, 2025

DNS解析

TCP/IP提供了通过 IP地址 来连接到设备的功能,但对用户来讲,记住某台设备的IP地址是相当困难的,因此专门设计了一种字符串形式的主机命名机制,这些主机名与IP地址相对应。在IP地址与主机名之间需要有一种转换和查询机制,提供这种机制的系统就是域名系统DNS(Domain Name System)。 为什么要有DNS? 互联网中,一台计算机与其他计算机通信时,通过 IP地址 唯一的标志自己。此时的IP地址就类似于我们日常生活中的电话号码。但是,这种纯数字的标识是比较难记忆的,而且数量也比较庞大。例如,每个IPv4地址是一个32位长的二进制数字,或者采用点分十进制展示成192.168.1.1这种格式,有接近43亿个的IPv4地址。DNS的作用就是将人类可读的名称转换为机器识别的IP地址,供计算机相互连接。DNS的工作原理和电话簿相似,都是管理名称和数字之间的映射关系。就像我们日常打电话,一般使用人名查找,很少直接输入电话号码一样。当我们上网打开某个网页、视频时,也很少直接使用IP地址,而是在浏览器里输入的URL地址,例如:https://www.huawei.com,这其实使用的就是计算机的名字,一般称为域名。 域名的构成 最初设备的域名由字符序列组成、所有设备的域名组成一个未分级的域名结构。未分级的域名结构存在命名冲突、管理维护复杂的缺点。因此,TCP/IP把DNS的域名设计成了分级的树状结构。每个申请加入Internet的国家都要向NIC注册一个顶级域名,顶级域采用组织模式和地理模式的划分模式,如cn代表中国、us代表美国等。 常见的顶级域名如下表所示。NIC将顶级域的管理权分派给由其指定的管理机构,由这些管理机构再对被授权管理的域继续进行划分,从而形成了二级域。负责划分二级域的管理机构可以授权其下属的管理结构,由它们继续划分域。由此下去,便形成了层次型的Internet域名体系结构。 顶级Internet域名 含义 com 商业组织 edu 教育机构 gov 政府机构 mil 军事部门 net 主要网络支持中心 int 国际组织 org 其他组织 国家代码 国家(按照地理模式划分) 从语法上讲,每一个域名都是有标号序列组成,而各标号之间用点(小数点)隔开。以www.huawei.com域名为例,从右到左依次是: com:顶级域名。代表商业组织。 huawei:二级域名,归属于某个公司自己的域名。 www:三级域名,表明某个公司提供的是什么服务,www代表普通网页。 DNS服务器、DNS客户端和DNS中继 网络中与DNS相关的设备角色包括DNS服务器、DNS客户端和DNS中继。 DNS服务器 DNS服务器是将域名指向对应IP地址的服务器。DNS服务器中保存了一张域名和与之相对应的IP地址的表,以解析消息的域名。 由于互联网连通的是全球资源,单一的域名服务器不足以支撑全部的地址转换操作,因此全球有多套域名服务器相互配合使用。 域名是分层结构,域名DNS服务器也是对应的层级结构。通过根域名服务器,依次请求顶级域名服务器和权威域名服务器,最终获取对应IP地址,并将该结果保存在本地域名服务器,以待下次DNS请求使用。当用户再次对同一域名发起访问时,可以直接从本地域名服务器获得结果,无需再次发起全球递归查询。 表1-2 DNS服务器的分类 分类 作用 根DNS服务器 根DNS服务器是最高层次的域名服务器,它知道所有顶级服务器的域名和IP地址,当本地域名服务器无法对域名进行解析时,首先对根域名服务器发起请求。 顶级域名服务器 顶级域名服务器负责管理该服务器下的所有二级域名,当收到DNS查询请求时,就会给权威域名服务器相应的回答。 权威域名服务器 负责某一个区域的域名服务器。当一个顶级域名服务器还不能给出最后查询回答时,就会告知下一步应当请求的权威域名服务器。 本地域名服务器 当一个主机发出DNS查询请求时,这个查询请求报文就发送给本地域名服务器。每一个互联网服务提供者ISP都可以拥有一个本地域名服务器。当本地域名服务器无法给出应答时,就会请求最高级的根域名服务器。 DNS客户端 DNS客户端的作用是接收用户程序(User Program)的DNS请求,并对其作出回应。作为DNS客户端的设备上一般具备以下能力: 启动DNS解析 要使用DNS客户端功能,需要在设备上打开DNS解析的开关。 指定服务器的IP地址 要进行DNS域名解析,需要在设备上指定DNS服务器的IP地址。这样才能把查询请求发动到正确的DNS服务器上进行解析。 指定DNS域后缀搜索列表 DNS客户端所访问的一些服务器或主机的域名后缀往往都是相同的。用户可以预先设置一些域名后缀,在域名解析的时候,用户只需要输入域名的部分字段,系统会自动将输入域名加上不同的后缀进行解析。例如,用户想查询域名“huawei.com”,那么可以在后缀列表中配置com,然后输入“huawei”,系统会自动将输入域名与后缀连接成“huawei.com”进行查询。 DNS中继 当DNS服务器的IP地址发生变化时,用户网络中每个DNS客户端上的配置都需要改变,这样工作量极大并且容易出错。此时,可以通过部署DNS中继解决该问题。DNS客户端上配置DNS中继的IP地址,DNS服务器的IP地址在DNS中继上配置。之后,DNS客户端会将DNS请求报文直接发送给DNS中继,由DNS中继将收到的DNS请求报文转发至DNS服务器。由此,当DNS服务器的IP地址发生变化时,仅需改变DNS中继上的配置即可,简化了网络管理。 DNS中继的工作原理 DNS客户端将DNS请求报文发送给DNS中继,即请求报文的目的地址为DNS中继的IP地址。 DNS中继收到请求报文后,将报文转发给DNS服务器,通过DNS服务器进行域名解析。 DNS域名解析过程 通过域名获取对应IP地址的过程称为域名解析。DNS域名解析分为以下两种方式: 静态域名解析 静态域名解析是通过静态域名解析表进行的,即手动建立域名和IP地址之间的对应关系表,该表的作用可以将一些常用的域名放入表中。当DNS客户端需要域名所对应的IP地址时,即到静态域名解析表中去查找指定的域名,从而获得所对应的IP地址,提高域名解析的效率 动态域名解析 动态域名解析需要专用的域名解析服务器(DNS服务器)运行域名解析服务器程序,提供从域名到IP地址的映射关系,负责接收客户提出的域名解析请求。 为提高查询速度,在解析域名时,首先采用静态域名解析的方法,如果静态解析不成功,再采用动态域名解析的方法。 ...

July 28, 2025

列表和key

当我们在 React 要渲染一个列表时,如果没有在每一个被渲染的元件加上 key 这个 prop,就会跳出 Warning: Each child in a list should have a unique “key” prop. 这个错误讯息。直到开发者把 key 加上后,这个警告讯息才会消失。为什么要加上 key? 以及 key 有什么原则需要遵守? 这是在开发 React 时需要有的重要概念,也是面试经常会问的基础题。 为什么需要 key? key 就像一个独特身份,让 React 可以去分辨哪些子元件被新增、 移除,或是修改。 (编按:若要进一步说明此概念,推荐在面试时画下 Dan Abramov 的这个系列推文例子) 从上面的例子可以看到,当今天红黄蓝三个圈,变成红蓝黄;这会有两个可能性。可能性一第二个圈跟第三个圈的位置互换;可能性二是位置没互换,但第二个圈变蓝色、第三个圈变黄色。如果没有一个独特辨识的机制,我们会没办法知道,究竟是哪一个可能性。 如果没办法有效辨识,将可能出现 bug。举例来说,假如今天的圈圈是有状态的,例如里面有勾选方块,然后第个二圈有被勾选。假如今天换颜色是因为第二个圈被交换到第三个位置,这时表示新一次的渲染时,第三个圈要是被勾选的。不过假如我们没有一个辨识机制,要是 React 误以为换颜色不是因为换位置,而是单纯的第二个圈换颜色,那么将会渲染出仍是第二个圈是被勾选的;那么这就会是一个 bug。 然而,有了 key 这个辨识机制,React 就会知道在新的一次渲染时,原本的状态应该被保留在列表中的哪一个元件中。因此,React 之所以需要 key,正是因为 key 可以让 React 知道,哪些子元件被新增、 移除,或是修改。 除此之外,看到下面这段 React 官方文件的例子 (编按:面试时也推荐直接举这例子)。原本有个列表,我们在最上方新增一个 <li>Connecticut</li> ,如果没有 key,对 React 来说将会是,Duke 变成 Connecticut、Villanova 变成 Duke,而最后新增一个 Villanova。换句话说,整个列表都改变了,所以 React 会打掉旧的,重建一个新的列表。当列表变大时,就会很消耗效能。 ...

December 17, 2024

position: sticky

如果问,CSS 中 position 属性的取值有几个?大部分人的回答大概是 static、relative、absolute、fixed,实际上MDN上还有一个 sticky。 今天我们就来给大家介绍这个容易被忽视的position属性值 sticky。 先来看看MDN上对于 sticky 的介绍: 粘性定位元素(stickily positioned element)是计算后位置属性为 sticky 的元素。 粘性定位可以被认为是相对定位和固定定位的混合。 什么是结合两种定位功能于一体呢? 元素在跨越特定阈值前为相对定位,之后为固定定位。 也就是元素先按照普通文档流定位,然后相对于该元素在流中的 flow root(BFC)和 containing block(最近的块级祖先元素)定位。 元素定位表现为在跨越特定阈值前为相对定位,之后为固定定位。 这个特定阈值指的是 top、right、bottom 或 left 之一,换言之,指定 top、right、bottom 或 left 四个阈值其中之一,才可使粘性定位生效。否则其行为与相对定位相同。 示例 上面的文字描述估计还是很难理解,看看下面这张 GIF 图,想想要实现的话,使用 JS + CSS 的方式该如何做: 按照常规做法,大概是监听页面 scroll 事件,判断每一区块距离视口顶部距离,超过了则设定该区块 position:fixed,反之去掉。 而使用 position:sticky ,则可以非常方便的实现: <div class="container"> <div class="sticky-box">内容1</div> <div class="sticky-box">内容2</div> <div class="sticky-box">内容3</div> <div class="sticky-box">内容4</div> </div> .container { background: #eee; width: 600px; height: 1000px; margin: 0 auto; } .sticky-box { position: sticky; height: 60px; margin-bottom: 30px; background: #ff7300; top: 0px; } div { font-size: 30px; text-align: center; color: #fff; line-height: 60px; } 看看上面的 CSS 代码,只是给每个内容区块加上 ...

November 9, 2024

全方位解读this

我们在学习JavaScript的过程中,由于对一些概念理解得不是很清楚,但是又想要通过一些方式把它记下来,于是就很容易草率的给这些概念定下一些方便自己记忆的有偏差的结论。 危害比较大的是,有的不准确的结论在网上还广为流传。 比如对于this指向的理解中,有这样一种说法:谁调用它,this就指向谁。在我刚开始学习this的时候,我是非常相信这句话的。因为在一些情况下,这样理解也还算说得通。可是我常常会在开发中遇到一些不一样的情况,一个由于this的错误调用,可以让我懵逼一整天。那个时候我也查资料,在群里问大神,可是我仍然搞不清楚“我特么到底错哪里了”。其实只是因为我心中有一个不太准确的结论。 所以,我认为需要有这样一篇文章,来帮助大家全方位的解读this。让大家对this,有一个正确的,全面的认知。 在这之前,我们需要来回顾一下执行上下文。 在前面的几篇文章中,我有好几个地方都提到执行上下文的生命周期,为了防止大家没有记住,再次来回顾一下,如下图。 在执行上下文的创建阶段,会分别生成变量对象,建立作用域链,确定this指向。其中变量对象与作用域链我们都已经仔细总结过了,而这里的关键,就是确定this指向。 首先我们需要得出一个非常重要一定要牢记于心的结论,**this的指向,是在函数被调用的时候确定的。**也就是执行上下文被创建时确定的。因此,一个函数中的this指向,可以是非常灵活的。比如下面的例子中,同一个函数由于调用方式的不同,this指向了不一样的对象。 var a = 10; var obj = { a: 20 } function fn () { console.log(this.a); } fn(); // 10 fn.call(obj); // 20 除此之外,在函数执行过程中,this一旦被确定,就不可更改了。 var a = 10; var obj = { a: 20 } function fn () { this = obj; // 这句话试图修改this,运行后会报错 console.log(this.a); } fn(); 一、全局对象中的this 关于全局对象的this,我之前在总结变量对象的时候提到过,它是一个比较特殊的存在。全局环境中的this,指向它本身。因此,这也相对简单,没有那么多复杂的情况需要考虑。 // 通过this绑定到全局对象 this.a2 = 20; // 通过声明绑定到变量对象,但在全局环境中,变量对象就是它自身 var a1 = 10; // 仅仅只有赋值操作,标识符会隐式绑定到全局对象 a3 = 30; // 输出结果会全部符合预期 console.log(a1); console.log(a2); console.log(a3); 二、函数中的this 在总结函数中this指向之前,我想我们有必要通过一些奇怪的例子,来感受一下函数中this的捉摸不定。 // demo01 var a = 20; function fn() { console.log(this.a); } fn(); // demo02 var a = 20; function fn() { function foo() { console.log(this.a); } foo(); } fn(); // demo03 var a = 20; var obj = { a: 10, c: this.a + 20, fn: function () { return this.a; } } console.log(obj.c); console.log(obj.fn()); 这几个例子需要花点时间仔细感受一下,如果你暂时没想明白怎么回事,也不用着急,我们一点一点来分析。 ...

November 6, 2024

组件通信

Vue3 组件通信方式 props $emit expose / ref $attrs v-model provide / inject Vuex mitt Vue3 通信使用写法 1. props 用 props 传数据给子组件有两种方法,如下 方法一,setup() 方法写法 // Parent.vue 传送 <child :msg1="msg1" :msg2="msg2"></child> <script> import child from "./child.vue" import { ref, reactive } from "vue" export default { data(){ return { msg1:"这是传级子组件的信息1" } }, setup(){ // 创建一个响应式数据 // 写法一 适用于基础类型 ref 还有其他用处,下面章节有介绍 const msg2 = ref("这是传级子组件的信息2") // 写法二 适用于复杂类型,如数组、对象 const msg2 = reactive(["这是传级子组件的信息2"]) return { msg2 } } } </script> // Child.vue 接收 <script> export default { props: ["msg1", "msg2"],// 如果这行不写,下面就接收不到 setup(props) { console.log(props) // { msg1:"这是传给子组件的信息1", msg2:"这是传给子组件的信息2" } }, } </script> 方法二,setup 语法糖 ...

October 4, 2024