
Javascript-进阶

作用域
作用域是变量能被访问的有效范围,本质上决定了代码中变量的可见性。理解作用域能帮助我们避免变量污染,写出更健壮的代码。
作用域分为两大类型:局部作用域和全局作用域,它们通过作用域链形成层级查找关系。
局部作用域
局部作用域内的变量只能在特定范围内访问,分为两种常见形式:
- 函数作用域
在函数内部声明的变量(包括函数参数)形成一个封闭的独立空间。例如:
1 | function calculate(x) { // x 是函数作用域的局部变量 |
函数执行完毕后,其内部变量会被销毁。不同函数之间的变量就像隔墙对话——彼此无法直接访问。
- 块作用域
用 {}
包裹的代码块(如 if
/for
语句)中使用 let
或 const
声明的变量会形成块作用域:
1 | if (true) { |
通过对比可以看到,let
将变量限制在代码块内,而 var
会泄漏到外层作用域。现代开发中推荐使用 let/const
来避免意外污染。
全局作用域
在 <script>
标签或 JS 文件的最外层声明的变量属于全局作用域,可以在任何位置访问:
1 | const globalVar = "我是全局的"; |
需要特别注意:
- 避免直接给
window
对象添加属性(如window.myVar = 1
) - 函数内部未使用
let/const/var
声明的变量会变成全局变量 - 过度使用全局变量容易引发命名冲突
作用域链
是一种查找机制,在当前作用域下去寻找该变量,如果没找到,再到它的上层作用域寻找,以此类推直到找到该变量或是已经到了全局作用域。
当前作用域 → 逐级父作用域 → 全局作用域
1 | let global = "顶层"; |
这种链式查找机制解释了为什么内层函数能访问外部变量,而外层无法访问内部变量。理解这个机制对调试变量未定义错误至关重要。
垃圾回收机制
内存管理是 JavaScript 运行时的核心机制,理解垃圾回收能帮助我们写出更高效、更安全的代码。
内存生命周期分为三个阶段:分配(声明变量)→ 使用(读写操作)→ 回收(自动清理),其中回收阶段通过垃圾回收器自动完成,开发者更需要关注的是如何避免内存泄漏。
内存分配与回收规则
当我们在函数内部声明局部变量时,这些变量就像临时便签纸——函数执行时被贴在内存墙上,执行完毕就会被自动撕下(回收)。而全局变量则像永久告示牌,会一直存在直到页面关闭:
1 | function createTemp() { |
需要警惕的是意外创建的全局变量:
1 | function leakMemory() { |
回收算法
浏览器通过特定算法判断内存是否需要回收,经历了两个重要阶段:
- 引用计数法(已淘汰)
早期 IE 浏览器通过”记账本”机制跟踪每个对象的被引用次数:
1 | let objA = { id: 1 }; // 引用次数+1 |
但当对象相互引用时会导致永久驻留内存:
1 | function createLoop() { |
- 标记清除法(现代方案)
现代浏览器采用”大扫除”机制:从全局对象出发标记所有可达对象,未被标记的视为垃圾:
1 | function createData() { |
当需要清理时:
1 | delete window.exposedData; // 断开引用链 |
闭包
闭包就像函数的专属记忆背包——当函数诞生时,它会随身携带出生地的环境变量,这种”内外函数嵌套 + 变量持续锁定”的特性,让内部函数能长期访问外部函数的变量,即使外部函数已经执行完毕。
闭包=内层函数+外层函数的变量
闭包的形成条件
- 嵌套结构:函数内部定义另一个函数
- 变量捕获:内层函数使用外层函数的变量
- 跨作用域调用:内层函数在外层函数外部被使用
1 | function createCounter() { |
此时外层函数的 count
变量不会被回收,因为内部函数始终保持对其的引用。
闭包的常见写法
- 封装私有变量(模块模式)
场景:需要保护数据不被外部直接修改时使用
写法:用立即执行函数包裹,返回操作接口
1 | // 用户积分模块 |
- 动态生成功能函数(工厂模式)
场景:需要批量创建功能相似但配置不同的函数时使用
写法:外层函数接收参数,返回携带配置的新函数
1 | // 创建不同倍数的计算器 |
闭包的应用与风险
闭包常用于实现模块化开发,例如封装私有变量:
1 | const student = (function() { |
但不当使用会导致内存泄漏,例如在 DOM 事件中滥用闭包:
1 | function bindEvents() { |
变量提升
变量提升是 JavaScript 编译阶段的特殊处理机制,表现为 var
声明的变量会提升到作用域顶部,但仅提升声明不提升赋值。这种机制容易引发代码理解错位,是许多隐蔽 Bug 的根源:
1 | console.log(name); // 输出 undefined(而非报错) |
等效于编译后的代码:
1 | var name; // 声明提升到顶部 |
let/const
强制先声明后使用,不会出现变量提升- 同一作用域下重复的
var
声明会被合并,而let/const
会报错
现代开发中,使用 let/const
配合严格模式('use strict'
) 可完全规避变量提升问题:
1 | ; |
变量提升是早期 JS 设计妥协的产物,ES6 通过块级作用域和 let/const
弥补了这一缺陷。
函数进阶
函数提升
函数声明整体提升
函数声明会带着函数体提升到作用域顶端,可在声明前调用:
1 | // ✅ 正常执行 |
函数表达式不提升
通过变量赋值的函数遵循变量提升规则,仅提升变量声明:
1 | // ❌ 报错:not a function |
等效于:
1 | var getPrice; // 变量声明提升 |
函数参数
动态参数 arguments
处理不确定数量的参数时,JavaScript 提供了两种方案。传统方式通过 arguments
伪数组获取所有实参,这种方式在箭头函数中不可用且操作受限:
1 | // 传统求和函数(使用 arguments) |
剩余参数 ...arg
现代开发更推荐使用 剩余参数 语法,直接将多余参数捕获为真数组:
1 | // 现代求和函数(使用剩余参数) |
展开运算符
与剩余参数语法相同但作用相反,展开运算符 可拆解数组用于特定场景。经典应用包括:
- 展开数组
- 合并数组
1 | // 计算最高温度(参数需要独立数值) |
假设需要实现一个配置函数,同时接收基础 URL 和多个配置项:
1 | // 传统写法(使用 arguments) |
箭头函数
基本语法
箭头函数通过简化的语法结构提升代码可读性,其设计哲学在于提供更紧凑的函数表达式方案。
与传统函数的关键差异体现在三个方面:
语法演进
1 | // 传统函数表达式 |
简写规则
箭头函数通过精简语法让代码更直观,其简写规则针对常见场景做了针对性优化:
- 单参数省略括号
当函数只有一个参数时,可省略参数外的圆括号,让代码更紧凑:
1 | // 传统写法(带括号) |
这种写法常见于简单的数学运算或数据过滤:
1 | // 筛选正数 |
- 单行返回值隐式返回:
当函数体只有一行代码时,可省略花括号和return
关键字,结果自动返回:
1 | // 传统写法(需写return) |
适用于简单的计算或转换:
1 | // 温度转换(华氏度→摄氏度) |
- 返回对象字面量特殊处理:
直接返回对象时必须包裹圆括号,避免引擎误认为代码块:
1 | // 错误写法(缺少括号) |
这种写法常见于快速构建数据对象:
1 | // 生成颜色配置 |
运行时特性
箭头函数作为表达式函数,不存在函数提升现象。这意味着:
1 | printHello(); // 正常执行 |
箭头函数参数
参数处理机制体现了对传统函数缺陷的改进,剩余参数( 方案解决了 arguments
的伪数组问题:
1 | // 传统 arguments 方案(了解即可) |
箭头函数 this
this 绑定机制是箭头函数最核心的特性变革,其设计目标为解决传统函数上下文绑定的混乱问题。
执行规则为:继承定义时的词法作用域的 this 值。
经典场景对比:
1 | // 传统函数 - this 由调用方式决定 |
DOM 事件处理警示:
1 | const btn = document.querySelector('#actionBtn'); |
对象方法陷阱:
1 | const user = { |
嵌套作用域穿透:
1 | const parent = { |
通过理解箭头函数的静态 this 绑定特性,我们可以更安全地处理异步回调和嵌套函数场景,但需特别注意在需要动态上下文的场景(如对象方法、DOM 事件)中的使用限制。
对象解构
对象解构是一种快速提取对象属性并批量赋值的现代语法,其核心价值在于简化数据访问逻辑。
与传统逐个赋值的写法相比,这种语法糖(Syntactic Sugar)显著提升了代码的可读性和维护效率。
语法糖:编程语言提供的简洁写法,用更易读的形式实现原有功能。例如
const {name} = user
本质仍是属性访问,但代码更清晰直观。
基本语法
我们可以通过等号左侧的花括号声明需要提取的属性名,实现属性到变量的直接映射:
1 | // 用户数据对象 |
关键规则:
- 属性名匹配:变量名必须与对象属性名一致
- 作用域隔离:解构变量与外部作用域同名变量冲突时会报错
- 安全防护:未匹配属性返回
undefined
1 | // 危险操作(变量名冲突) |
变量重命名
当需要避免命名冲突或提升语义时,可通过冒号语法指定新变量名:
1 | const product = { |
这种写法常见于处理多个数据源的场景,如同时解析用户配置和系统配置:
1 | // 多配置合并场景 |
数组对象解构
该语法同样支持从数组元素中解构对象,特别适合处理接口返回的规范化数据结构:
1 | // 接口返回的带元数据数组 |
掌握基于构造函数创建对象,理解实例化过程 2.掌握对象数组字符数字等类型的常见属性和方法,便捷完成功能
新的对象
对象
创建对象
在 JavaScript 中,我们可通过三种典型方式创建对象,其演进路线体现了从简单场景到复杂系统需求的适应过程:
对象字面量
作为基础创建方式,适合定义单例对象。其语法直观但复用性有限:
1 | const Peppa = { |
new Object()
是早期提供的工厂模式,本质是对象字面量的语法变体。其局限性在于无法实现定制化对象创建逻辑:
1 | const George = new Object({ |
构造函数(重点)
的引入解决了批量对象创建问题。通过将对象模板抽象为可复用的函数结构,我们实现了面向对象编程的核心特征——实例化能力:
1 | function Pig(name, age, gender) { // 构造函数要求**首字母大写** |
构造函数要求首字母大写的命名规范(如 Pig)
构造函数核心机制
当使用 new
运算符调用函数时,会触发以下隐式操作:
- 创建空对象并绑定到函数上下文(this 指向空对象)
- 执行构造函数逻辑进行属性赋值
- 自动返回新创建的对象实例
这一机制解释了为何构造函数中不需要显式 return 语句。
通过家庭成员对象的创建案例,我们可以清晰看到不同方案的适用边界:
1 | // 字面量方案:4 次重复代码 |
当需要创建同构对象超过 2 个时,构造函数模式能显著提升代码可维护性。这种模式转变带来的收益随着系统复杂度的提升呈指数级增长。
实例成员/静态成员
实例成员
实例成员是通过构造函数创建的对象专属属性与方法,每个实例对象都拥有独立的成员副本。
这种机制保证了对象间的数据隔离性,是面向对象编程的基础特性:
1 | function Person() { |
核心特性:
实例成员通过构造函数内的 this
动态绑定到每个新对象,每个实例都独立存储自己的属性和方法。
例如创建两个 Person
对象时,它们的 sayHi
方法互不影响,修改一个不会改变另一个。
1 | // 参数化构造函数 |
通过构造函数参数,我们可以像流水线一样动态设置不同初始值,快速生成结构相同但数据不同的对象。
静态成员
静态成员是构造函数自身的属性与方法,用于实现与实例无关的公共功能。
其设计目标是提供类级别的工具方法:
1 | function Person() { |
关键约束:
静态成员直接挂在构造函数上,和具体实例无关。
比如 Person.walk()
方法无法读取实例的 name
属性,就像数学工具 Math.random()
不需要知道具体数字一样。这类方法常用于定义通用工具(如日期格式化)或全局配置(如默认超时时间)。
内置构造函数
基本类型处理简单数据,引用类型承载复杂结构。
JavaScript 的类型系统存在一个精妙设计:基本数据类型通过包装类型临时对象化。
当我们操作字符串、数字等基本类型时,引擎会自动创建临时包装对象,这使得如下操作成为可能:
1 | const str = 'andy'; |
这种机制让基本类型在需要也能调用方法(如 'text'.toUpperCase()
),操作完成后立即销毁临时对象,回归基本类型状态。
引用类型
Object
键值提取 .keys()/.values()
这对方法分别提取对象的键和值,返回数组形式。在处理数据格式转换时,它们常与数组方法配合使用:
1 | const user = { name: '佩奇', age: 6 }; |
通过将对象结构转化为数组,可无缝接 接下来提到的数组方法进行深度处理。
对象合并 .assign()
Object.assign()
该方法 从左到右覆盖式 合并对象。
- 第一个参数为目标对象:所有源对象的属性都会拷贝到此对象
- 后续参数为源对象:按顺序覆盖同名属性
- 浅拷贝特性:嵌套对象仍为引用关系(修改会影响原对象)
典型场景是为已有对象追加属性或创建浅拷贝:
1 | // 基础用法:动态添加属性 |
Array 重点方法
基础迭代:.forEach()
1 | 数组.forEach(function(当前元素, 当前索引) { |
forEach()
是替代传统 for
循环的现代方案,其核心能力在于 自动化迭代控制。通过对比可见其如何简化代码逻辑:
1 | // 传统索引循环 |
与手动维护索引的传统循环不同,forEach
自动处理数组遍历过程。回调函数始终按 元素 → 索引 → 原数组 的顺序接收参数,这种标准化接口降低了认知成本。
数据转换:.map()
1 | const 新数组 = 数组.map(function(当前元素, 当前索引) { |
map()
专注于 元素映射转换,其设计理念是保持原数组不变并生成新数组。
1 | // 传统转换方式 |
map
方法通过返回新数组的特性,天然支持链式调用。在处理数据流水线时,可以流畅衔接其他数组方法形成处理链路,这种特性在组合 filter
等操作时尤为实用。
条件筛选:.filter()
1 | const 筛选数组 = 数组.filter(function(当前元素) { |
filter()
实现了 真值测试驱动 的数据过滤机制,其核心逻辑是将条件判断抽象为独立函数:
1 | // 传统筛选方式 |
该方法通过返回新数组保持数据不可变性,特别适合与 map
组合使用。当需要处理复合条件时,可通过逻辑运算符组合多个判断条件。
字符串拼接:.join()
1 | const 拼接结果 = 数组.join('分隔符') // 默认分隔符为逗号 |
join()
方法将数组转换为格式可控的字符串,其分隔符参数提供了灵活的格式定制能力:
1 | // 路径拼接场景 |
该方法在处理数据导出、URL 生成等场景时,能够显著简化字符串拼接逻辑。通过不同分隔符的配置,可快速适配 JSON、CSV 等多种数据格式要求。
累计运算:.reduce()
1 | const 最终值 = 数组.reduce(function(累计值, 当前元素) { |
reduce()
的核心在于 通过遍历逐步收敛为单一值,
- 初始值决定起点:若提供初始值,首轮累计值为初始值;否则直接取数组首元素
- 链式传递逻辑:每次迭代的返回值自动成为下一轮累计值
其执行逻辑可通过两种典型场景理解:
1 | // 场景1:数组求和(无初始值) |
- 初始值存在时,首次循环的
acc
为初始值,否则直接取数组第一个元素 - 每次循环的返回值会成为下次循环的
acc
- 最终收敛值可以是任意类型(数字、对象、数组等)
与传统循环相比,reduce
将迭代控制权交给引擎,开发者只需关注 如何更新累计值。这种模式在数据聚合、多维数组扁平化等场景中,能显著提升代码可读性。
元素搜索:.find()
1 | const 目标元素 = 数组.find(function(当前元素) { |
find()
的核心价值在于 短路搜索机制,其执行逻辑与传统搜索方式的对比:
1 | // 传统搜索方式 |
该方法在找到首个匹配元素后立即终止遍历,这种特性在处理大型数组时能显著提升性能。与 filter
返回数组不同,find
直接返回目标元素本身,在需要获取对象引用时更为实用。
全员检测:every()
1 | const 全员通过 = 数组.every(function(当前元素) { |
every()
通过 全员验证机制 实现整体性判断,其短路特性体现在:
1 | // 传统验证方式 |
当检测到首个不满足条件的元素时,遍历立即终止。该方法常与 some()
形成逻辑互补(全员满足 vs 至少一个满足),在表单验证、权限检查等场景中极为实用。
Array 其他方法
存在性检测:some()
与 every()
形成逻辑互补,当数组中至少有一个元素满足条件时即返回 true
:
1 | const hasNegative = [1, -5, 3].some(n => n < 0); // true |
典型应用场景:权限检查(至少拥有一个权限)、表单验证(存在非法输入)
数组合并:concat()
安全合并多个数组(不修改原数组):
1 | const arr1 = [1, 2]; |
特别说明:现代开发更常用扩展运算符 [...arr1, ...arr2]
排序控制:sort()
原地排序(修改原数组),默认按字符串 Unicode 排序:
1 | const nums = [10, 5, 40]; |
关键注意:比较函数返回负数/0/正数决定排序顺序
动态修改:splice()
最灵活的数组修改方法(可删除/替换/添加元素):
1 | const arr = ['a', 'b', 'c']; |
参数模式:startIndex
→ 起始位置deleteCount
→ 删除数量...items
→ 插入的新元素
结构反转:reverse()
原地反转数组元素顺序:
1 | const letters = ['a', 'b', 'c']; |
典型用途:处理栈结构、展示倒序列表
索引定位:findIndex()
与 find()
逻辑一致,但返回元素的索引值(未找到返回-1):
1 | const users = [{id:1}, {id:2}]; |
特别适配场景:需要索引进行后续操作(如结合 splice
删除元素)
结构转换:Array.from()
将类数组结构(含 length 属性)转换为标准数组:
1 | const 新数组 = Array.from(伪数组) |
典型场景:处理 DOM 元素集合或 arguments 对象
1 | // DOM 操作场景 |
Array 方法总结
基础必备
(处理数据必用,每天写代码都离不开)
1. forEach
(重点)
最简单的遍历方法,替代for
循环的最佳选择。就像自动售货机——投币(数组)后自动吐出每个商品(元素),你只需要处理每个商品:
1 | ['牛奶', '面包'].forEach((商品, 序号) => { |
⚠️ 注意:不能中途break
停止,需要中断时用for
循环
2. map
(重点)
数据变形金刚,把数组中的每个元素变成新模样,且不修改原数组:
1 | const 价格 = [100, 200] |
🛠️ 经典场景:接口数据转换(API 返回的数据 → 前端需要的格式)
3. filter
(重点)
数据过滤器,只留下符合条件的元素:
1 | const 用户列表 = [{年龄:17}, {年龄:20}] |
⚠️ 陷阱:filter(item => item.prop)
会过滤掉prop
为0
或false
的合法值
进阶必备
(提升代码效率,处理复杂场景)
4. reduce
(重点)
数据聚合器,把数组浓缩成一个值(数字/对象/新数组):
1 | // 计算总价 |
🔥 高阶用法:统计词频、扁平化嵌套数组
5. find
(重点)
精准搜索,找到第一个符合条件的元素(比filter
更快):
1 | const 订单列表 = [{id:1, 状态:'未支付'}, {id:2, 状态:'已发货'}] |
⚠️ 注意:找不到时返回undefined
,要用if
判断结果是否存在
实用工具
方法 | 一句话功能 | 使用示例 | 注意点 |
---|---|---|---|
some |
至少一个满足条件 | [1, -2].some(n => n<0) → true |
和every 相反 |
concat |
合并数组 | [1].concat([2]) → [1,2] |
用[...arr1,...arr2] 更现代 |
sort |
排序 | [10,2].sort((a,b)=>a-b) → [2,10] |
必须传比较函数! |
splice |
增删改元素 | arr.splice(1,0,'新增') 插入元素 |
直接修改原数组 |
findIndex |
找元素位置 | ['a','b'].findIndex(v=>v==='b') → 1 |
找不到返回-1 |
- **
forEach
**:每个都做点什么 - **
map
**:每个都变个样子 - **
filter
**:好的留下,坏的不要 - **
reduce
**:多个变一个 - **
find
**:抓住第一个符合条件的
包装类型
String,Number,Boolean 等
String 重点方法
结构拆分:.split()
1 | const 数组 = 字符串.split(分隔符) |
split()
实现字符串与数组的高效双向转换,与数组的 join()
方法形成互逆操作,构建字符串 ↔ 数组转换闭环。
其核心价值在于结构化数据处理:
1 | // 传统方案:手动拆分 |
分隔符支持正则表达式(如 /\s+/
匹配连续空格),连续分隔符会产生空元素,第二参数控制最大分段数量
精准截取:.substring()
1 | 字符串.substring(起始索引[, 结束索引]) // 截取内容不包含结束索引号的内容 |
与 slice()
方法类似,但不支持负索引,提供更安全的截取控制:
1 | // 传统字符操作 |
当起始索引大于结束索引时,substring
会自动调换参数,而 slice
返回空字符串。在表单输入截断等场景下更安全。自动处理起始>结束的情况(交换参数)超范围索引自动截断到有效范围
前缀检测:.startsWith()
1 | 字符串.startsWith(检测字符串[, 起始位置]) |
实现精准的头部匹配检测,替代正则检测的轻量方案:
1 | // 传统方案 |
典型应用:
1 | // 路由检测 |
存在性判断:.includes()
1 | 字符串.includes(搜索字符串[, 起始位置]) |
提供更直观的包含性检测,替代 indexOf
的现代方案:
1 | // 传统检测 |
与数组的 includes
方法形成语法统一,降低记忆成本。注意对大小写敏感,必要时先统一大小写。
String 其他方法
大小写转换:.toUpper/LowerCase()
1 | 字符串.toUpperCase() // 全大写转换 |
位置定位:.indexOf()
1 | 字符串.indexOf(搜索值[, 起始位置]) |
返回首次出现位置的索引(未找到返回 -1),适合精确查找:
1 | // 文件类型检测 |
后缀检测:.endsWith()
1 | 字符串.endsWith(检测字符串[, 检测长度]) |
常用于文件格式验证,支持限定检测范围:
1 | // 图片格式校验 |
模式替换:.replace()
1 | 字符串.replace(匹配模式, 替换内容) |
支持正则表达式替换,实现灵活字符串处理:
1 | // 日期格式转换 |
模式匹配:.match()
1 | 字符串.match(正则表达式) |
返回匹配结果的数组,捕获组信息完整保留:
1 | // 提取颜色代码 |
Number
数值格式化:.toFixed()
1 | 数值.toFixed(保留小数位数) |
toFixed()
实现数值的精确舍入与格式化输出,如同会计记账时的金额规范处理。通过对比可见其如何简化数值控制逻辑:
1 | // 传统手工计算方案 |
与手动计算不同,toFixed
自动处理四舍五入和末尾补零,返回标准化字符串。此方法与字符串的 parseFloat
形成数据处理闭环:
1 | // 金额输入校验场景 |
小数位不足时自动补零(如 5.toFixed(2)→"5.00"
)采用国际标准四舍六入五成双规则始终返回字符串以避免精度丢失,需配合 Number()
转换使用
深入对象
编程思想
面向过程编程
如同制作蛋炒饭的过程:
先热锅、倒油、炒蛋、加饭、翻炒调味,每个步骤都需要严格按照顺序执行。
这种编程方式将问题分解为线性步骤,通过函数调用逐步实现:
1 | // 制作蛋炒饭的代码模拟 |
其优势在于执行效率高(适合硬件操作、单片机开发),但维护困难——若要调整加饭和炒蛋的顺序,需要重写整个流程。
如同修改食谱步骤,可能影响最终成品。
面向对象编程(OOP)
更像盖浇饭的制作:将食材处理、酱料调配、摆盘装饰等功能拆分给不同厨师(对象),通过分工合作完成菜品。每个对象承担独立职责,彼此通过接口协作:
1 | // 盖浇饭对象协作模拟 |
这种模式更易维护和扩展——若要新增「辣味浇头」,只需扩展 toppingChef
对象,无需修改其他部分。其代价是性能略低于面向过程,但为大型项目提供了更好的灵活性和协作性。
构造函数封装
JavaScript 通过构造函数实现面向对象的封装性,将数据与操作数据的方法组合成独立对象:
1 | function Wolf(name, age) { |
此时 greyWolf.howl === redWolf.howl
返回 false
,说明每只狼的 howl
方法都是独立创建的。如同为每匹狼单独配备嚎叫设备,虽能正常工作,但造成了资源浪费。
当创建大量狼对象时,重复定义方法会导致显著的内存浪费。
这种设计下,若有 1000 匹狼,就会产生 1000 个功能相同的 howl
方法副本,严重消耗内存资源。
构造函数封装
JavaScript 通过构造函数实现面向对象的封装性,将生物特征与行为模式组合成独立对象:
1 | function Wolf(species, age) { |
此时每个狼实例都携带独立的 howl
方法副本,如同为每匹狼配备专用声带系统。当建立狼群时(如 1000 匹规模的群体),会造成显著的内存冗余。
原型
原型对象 prototype
每个 构造函数 在创建时自动获得一个名为 prototype
的 原型对象 ,该对象专门用于存储同类实例共享的方法。
通过将方法定义在原型对象上,所有通过该构造函数生成的实例均可访问这些方法,实现内存的高效利用。
1 | function Wolf(species, age) { |
原型方法中的 this
原型方法中的 this
始终指向调用该方法的实例对象,这是实现状态独立的核心。
1 | Wolf.prototype.establishTerritory = function() { |
若错误使用箭头函数定义原型方法,会导致 this
绑定失效。
原型链维护规范
constructor
修复机制
在 JavaScript 的原型继承体系中,每个构造函数的原型对象(prototype)默认包含一个 constructor 属性指向构造函数本身。
开发中常遇到需批量添加原型方法的场景。若采用直接赋值方式覆盖原型对象(而非属性追加方式),会破坏原型链的完整性:
1 | function Wolf() {} |
覆盖原型时必须手动重建构造函数关联:
1 | // 修复方案 ✅ |
后续通过 new
创建的实例才能正确继承构造函数类型特征。
对象原型 __proto__
实例对象的 __proto__
属性(现代规范中对应 [[Prototype]]
内部插槽)构成原型链的核心链路。
该属性指向其构造函数的原型对象(prototype),实现继承关系:
1 | function ArcticWolf() { |
三要素关系体系
graph TD A[构造函数 Wolf] -->|"prototype 属性"| B[原型对象 Wolf.prototype] B -->|"constructor 属性"| A A -->|"new 操作符"| C[实例 alphaWolf] C -->|"__proto__ 属性"| B C -.->|"constructor 查找路径
(通过原型链访问)"| A B -.->|"共享方法调用
(如howl/hunt)"| C
构造函数的 prototype
存储当前构造函数所有实例的共享方法,该属性是一个独立对象,通过 Wolf.prototype.howl
定义的方法可被所有狼实例调用:
1 | Wolf.prototype.groupHunt = function() { |
实例的 __proto__
实例对象的隐式链接属性,指向其构造函数的原型对象。通过此属性实现原型链查找机制。
1 | const juvenileWolf = new Wolf('幼狼', 6); |
原型的 constructor
原型对象的反向指针,指向其关联的构造函数。此属性是维护类型系统的关键:
1 | console.log(Wolf.prototype.constructor === Wolf); // true |
继承机制核心原理
通过原型链(Prototype Chain)实现属性和方法的层级共享。子类构造函数通过原型对象继承父类特性,形成「实例-子类原型-父类原型」的链式结构。
1 | // 生物基类(父类) |
当多个子类直接继承同一个原型对象时,会导致意外共享修改:
1 | function Person() { |
Man
和 Woman
的原型指向同一内存地址,修改会相互影响。
通过中间构造函数创建独立的原型副本:
1 | function Person() { |
原型链
原型链是基于原型对象的继承查找机制,通过将不同构造函数的原型对象(prototype)按层级关联形成的链状结构。
当访问对象属性/方法时,解释器按以下顺序查找:
- 对象自身属性:优先在对象实例中查找是否存在目标属性
- 原型对象查找:未找到时,通过
__proto__
属性(注:对象的内部原型指针)查找其构造函数对应的原型对象(prototype) - 原型链追溯:若仍未找到,则继续向上一级原型对象(Object.prototype)追溯
- 终止条件:最终到达 Object.prototype 的
__proto__
(值为 null)时停止查找
instanceof
运算符通过原型链检测对象类型:
1 | // 业务场景示例:设备权限校验 |
深浅拷贝
深浅拷贝仅针对数组、对象等引用类型数据(堆内存存储结构)
直接赋值会导致新旧对象完全关联,而拷贝操作能在堆内存中创建独立的新空间,实现不同程度的独立性。
1 | const obj = { a: 1, b: { c: 2 } }; |
- 本质:复制栈内存中的指针地址
- 特点:新旧变量共享同一内存空间,修改任意一方都会影响另一方
浅拷贝
浅拷贝仅复制第一层简单类型值,而嵌套的引用类型仍指向原地址。
实现浅拷贝的主要方式包括 - 使用 Object.assign()
方法和展开运算符 ...
对于数组类型,可通过 concat()
、slice()
或展开运算符实现。
1 | // 对象浅拷贝 |
这些方法共同特点是:创建新对象并复制原始对象的第一层属性,当属性值为引用类型时,新旧对象会共享该属性的内存地址。
1 | shallowCopy1.name = 'red'; // 修改表层属性 |
- ✅ 隔离第一层数据
- ❌ 共享嵌套引用
- ⚠️ 适用于单层结构对象
深拷贝
深拷贝递归复制所有层级属性(创建完全独立的数据副本)。
递归实现
递归是深拷贝的核心实现原理,通过函数自调用遍历对象所有层级。基础实现需注意两点:终止条件(处理非对象类型)和特殊对象类型的处理(如数组)。
1 | function deepClone(target) { |
优势:可处理循环引用
注意:需处理特殊对象类型(如 Date、RegExp)
JSON 序列化
利用 JSON.stringify()
和 JSON.parse()
的组合可实现快速深拷贝,但需要注意其局限性:
1 | const obj = { a: 1, b: { c: 2 } }; |
- 会丢失 undefined / function
- 无法处理循环引用
- 破坏特殊对象(如 Date 转为字符串)
使用 Lodash 库
Lodash 库的 _.cloneDeep()
方法提供生产级深拷贝方案,能正确处理各种边界情况:
1 | <!-- 引入 Lodash 库 --> |
专业选择:处理各种边界情况的最佳实践
异常处理
了解 JavaScript 中程序异常处理的方法,提升代码运行的健壮性。
throw
function counter(x,y){
if(!x|l!y){
//throw‘参数不能为空!‘;
thrownewError(参数不能为空!)
return x+y
counter()
1.throw 抛出异常信息,程序也会终止执行
2.throw 后面跟的是错误提示信息
3.Error 对象配合 throw 使用,能够设置更详细的错误信息
1.抛出异常我们用那个关键字?它会终止程序吗?
》throw 关键字
》会中止程序 2.抛出异常经常和谁配合使用?
Error 对象配合 throw 使用
try catch
function fn(){
try {
/1 可能发送错误的代码要写到 try
const p = document.querySelector(‘.p’)
p.style.color =’red’
catch (err){
11 拦截错误,提示浏览器提供的错误信息,但是不中断程序的执行
console.log(err.message)
thrownewError(‘你看看,选择器错误了吧’)
//需要加 return 中断程序
//return
7
finally{
11 不管你程序对不对,一定会执行的代码
alert(弹出对话框 T)
console.log(11)
fn()
1.捕获异常我们用那 3 个关键字?
try catch finally
try 2.怎么调用错误信息?
》利用 catch 的参数
debugger
我们可以通过 try/catch 捕获错误信息(浏览器提供的错误信息)
异常处理
异常处理是构建健壮应用的基石,能有效防止程序意外崩溃。JavaScript 提供了一套完整的错误控制机制,开发者通过合理运用可显著提升代码质量。
异常抛出
throw
语句用于主动触发异常流程,通常与内置 Error
对象配合使用。当执行到 throw 语句时,当前函数执行上下文立即终止,控制权移交最近的异常捕获块。基础使用范式:
1 | function calculate(x, y) { |
错误类型细分:除基础 Error 外,JavaScript 提供多种派生错误类型 - SyntaxError
:语法解析错误 - TypeError
:类型操作错误 - RangeError
:数值越界错误 - ReferenceError
:引用错误
自定义错误扩展
1 | class NetworkError extends Error { |
异常捕获
try/catch/finally 结构
完整的异常处理单元包含三个逻辑块,执行流程如下图所示:
1 | async function loadUserProfile() { |
应用场景:
1 | try { |
- 错误隔离性:catch 仅捕获所在 try 块的异常
- 作用域限制:try 内部声明的变量在外部不可访问
- 资源保障:finally 区块始终执行,适合执行清理操作
浏览器调试
debugger
语句:代码中插入调试断点
1 | function complexCalculation() { |
使用场景:
- 动态调试复杂逻辑
- 配合浏览器开发者工具进行单步调试
- 临时插入断点替代 console.log
this 指向
JavaScript 中的 this
关键字在不同执行环境中具有动态绑定特性,其指向规则是语言的重要特性之一。
普通函数
普通函数的 this
值由调用方式决定,遵循”谁调用指向谁”的基本原则。当函数作为独立函数调用时,非严格模式下 this
默认指向全局对象(浏览器环境中为 window
),严格模式下则指向 undefined
。
1 | // 基础函数定义 |
严格模式通过 "use strict"
指令激活,该模式下未指定调用者时 this
值为 undefined
。开发时应特别注意模式差异对代码行为的影响。
箭头函数
箭头函数采用词法作用域规则,其 this
值继承自外层最近的非箭头函数的 this
绑定。箭头函数自身不创建 this
绑定,这种特性使其特别适合需要保持上下文一致的场景。
当访问箭头函数中的 this
时,解释器将执行以下操作:
- 检查当前函数作用域是否存在
this
定义 - 沿作用域链逐层向上查找
- 使用首个找到的有效
this
绑定
1 | // 原型方法误用示例 |
改变 this
在 JavaScript 中,可通过call()
、apply()
和bind()
三种方法动态控制普通函数内部this
的指向。这些方法虽功能相似,但在使用场景和执行逻辑上存在明显差异。
call()
call()
方法通过立即执行函数实现this
指向的修改。其语法为:
1 | fun.call(thisArg, arg1, arg2, ...) |
其中thisArg
为函数运行时指定的this
值,后续参数以逗号分隔逐个传递。例如:
1 | const obj = { uname: 'pink' }; |
特性:直接调用函数,适合需要明确参数数量和立即执行的场景。
apply()
apply()
方法与call()
的核心区别在于参数传递方式,它通过数组接收参数:
1 | fun.apply(thisArg, [argsArray]) |
典型应用场景是处理数组数据,例如结合数学计算:
1 | const obj = { age: 18 }; |
特性:参数需封装为数组,适用于参数数量不确定或已有数组结构的场景。
bind()
bind()
方法通过生成新函数实现this
指向的绑定,但不会立即执行原函数:
1 | const newFun = fun.bind(thisArg, arg1, arg2, ...) |
例如实现延时操作中的this
控制:
1 | const obj = { age: 18 }; |
特性:返回修改了this
指向的函数副本,适用于需要延迟执行或事件回调的场景。
核心差异与选择
特性 | call | apply | bind |
---|---|---|---|
执行方式 | 立即调用 | 立即调用 | 返回新函数 |
参数形式 | 逗号分隔参数 | 单数组参数 | 逗号分隔参数 |
使用频率 | 中等 | 较低 | 高频 |
关键结论:
- **
call
**:明确参数数量时,替代普通函数调用 - **
apply
**:处理数组参数或不确定参数数量 - **
bind
**:需要保持this
指向的场景(如定时器回调、事件处理)
通过理解参数传递方式和执行时机的差异,可根据具体需求选择最合适的方法控制this
指向。
防抖(debounce)
防抖:单位时间内,频繁触发事件,只执行最后一次
举个栗子:王者荣耀回城,只要被打断就需要重新来
使用场景:
搜索框搜索输入。只需用户最后一次输入完,再发送请求
手机号、邮箱验证输入检测
1.防抖是什么?
》单位时间内,频繁触发事件,只执行最后一次
2.有什么使用场景呢?
N 搜索框搜索输入。只需用户最后一次输入完,再发送请求
》手机号、邮箱验证输入检测
利用防抖来处理-鼠标滑过盒子显示文字
要求:鼠标在盒子上移动,鼠标停止 500ms 之后,里面的数字才会变化+1
const box = document.querySelector(‘.box’)
let i=1
function mouseMove(){
box.innerHTML=i++
//如果存在开销较大操作,大量数据处理,大量 dom 操作,可能会卡
box.addEventListener(‘mousemove’,mouseMove)
实现方式:
lodash 提供的防抖
//利用 Lodash 库实现防抖
//语法:_.debounce(fun,时间)
box.addEventListener(’mousemove’,_.debounce(mouseMove,5oo))
手写防抖函数
11.声明定时器变量
112。每次鼠标移动(事件触发)的时候都要先判断是否有定时名
时器
1/3。如果没有定时器,则开启定时器,存入到定时器变量里面 114.定时器里面写函数调用
function debounce(fn,t){
let timer
//return 返回一个匿名函数
return function (){
1/2.3.4
if (timer) clearTimeout(timer)
timer= setTimeout(function ()
fn()//加小括号调用 fn 函数
},t)
节流-throttle
节流:单位时间内,频繁触发事件,只执行一次
要求:鼠标在盒子上移动,不管移动多少次,每隔 500ms 才+1
const box = document.querySelector(‘.box’)
leti=1
function mouseMove(){
box.innerHTML=i++
11 如果存在开销较大操作,大量数据处理,大量 dom 操作,可能会卡
box.addEventListener(‘mousemove’,mouseMove)
6266260
实现方式:
lodash 提供的节流函数来处理
box.addEventListener(‘mousemove’,_.throttle(mouseMove,30o0))
手写一个节流函数来处理
function throttle(fn,t){
let timer = null
return function (){
if(!timer){
timer= setTimeout(function (){
fn()I
11 清空定时器
timer = nul1
},t)
性能优化说明使用场景
防抖单位时间内,频繁触发事件,只执行最后一次搜索框搜索输入、手机号、邮箱验证输入检测
节流单位时间内,频繁触发事件,只执行一次高频事件:鼠标移动 mousemove、页面尺寸缩放 resize、
滚动条滚动 scroll 等等
防抖与节流
防抖(debounce)
在单位时间内,若事件被频繁触发,仅执行最后一次操作。如同电梯关门机制——当持续有人进入时,关门动作会被不断延迟,直到无人进入后才真正执行关门。
典型应用场景
- 搜索框输入联想(用户停止输入 300ms 后发起请求)
- 表单验证(如手机号/邮箱格式校验)
- 鼠标移动停止后触发操作(如示例中鼠标停留 500ms 后更新数值)
实现方案
原生 JavaScript 实现
通过定时器延迟执行,若重复触发则重置计时:
1 | const box = document.querySelector('.box'); |
Lodash 库实现
通过现成方法快速应用防抖:
1 | box.addEventListener('mousemove', _.debounce(mouseMove, 500)); |
节流(throttle)
核心概念:在单位时间内,无论事件触发多少次,最多执行一次操作。如同水龙头限流——无论快速开关多少次,水流始终以固定频率流出。
典型应用场景
- 页面滚动事件监听(如每 100ms 计算滚动位置)
- 窗口 resize 时元素重排
- 高频鼠标移动轨迹采样(如示例中每 500ms 记录一次坐标)
实现方案
原生 JavaScript 实现
通过定时器控制执行间隔:
1 | function throttle(fn, t) { |
Lodash 库实现
直接调用现成节流方法:
1 | box.addEventListener('mousemove', _.throttle(mouseMove, 500)); |
防抖适用场景:关注最终状态,高频触发中只需最后一次有效(如输入停止后的搜索请求)。
节流适用场景:需要维持固定执行频率,避免高频操作导致性能问题(如滚动事件的位置计算)。
- 标题: Javascript-进阶
- 作者: Wreckloud_雲之残骸
- 创建于 : 2025-04-02 12:09:57
- 更新于 : 2025-04-19 20:00:31
- 链接: https://www.wreckloud.com/2025/04/02/猎识印记-领域/软件工程/前端/Javascript-进阶/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。