JS
认知 7 问:JS
这是什么?——了解定义和基本特征
- JavaScript 是什么?
- JavaScript 是一种广泛应用于网页开发的编程语言,主要用于前端开发,能够在浏览器中实现动态效果、用户交互、数据处理等功能。它是一种高层次的解释型语言,支持面向对象、函数式编程等多种编程范式。
- JavaScript 是什么?
它有什么用?——探讨用途、功能和价值
- JavaScript 的用途是什么?
- JavaScript 主要用于网页的前端开发,可以动态更新页面内容,响应用户的输入,并与后台服务器进行数据交换。它使网页具备动态特性,如表单验证、交互动画、异步请求等。
- 除了前端开发,Node.js 使 JavaScript 可以在服务器端运行,从而实现全栈开发。
- JavaScript 的用途是什么?
为什么需要它?——理解背景、动机和需求
- 为什么需要 JavaScript 而不只用 HTML 和 CSS?
- HTML 和 CSS 主要用于网页的结构和样式,但它们是静态的,不具备处理交互和动态内容的能力。JavaScript 通过提供动态和交互功能,让网页能够响应用户输入、更新内容、进行异步通信等,提升用户体验。
- 为什么需要 JavaScript 而不只用 HTML 和 CSS?
它的核心原理是什么?——了解工作机制和理论基础
- JavaScript 是如何工作的?
- JavaScript 代码在浏览器中由 JavaScript 引擎(如 V8 引擎)解释并执行。执行时,JavaScript 引擎会把代码解析成 AST(抽象语法树),然后执行对应的操作。JavaScript 还使用事件循环机制来处理异步操作,这允许 JavaScript 在等待某些操作时不阻塞程序执行。
- JavaScript 是如何工作的?
它有哪些优缺点?——分析优势与不足
- JavaScript 的优缺点是什么?
- 优点:
- 跨平台性:可以在不同操作系统和设备上运行,尤其是在浏览器中。
- 动态性:能够快速响应用户交互,更新页面内容,支持异步操作等。
- 全栈开发:通过 Node.js,JavaScript 不仅可以做前端开发,还能做后端开发。
- 缺点:
- 单线程:JavaScript 默认是单线程的,可能会导致处理复杂计算时出现性能瓶颈。
- 浏览器兼容性问题:不同浏览器对 JavaScript 的支持有所不同,可能会导致不同的行为表现。
- 优点:
- JavaScript 的优缺点是什么?
在哪些情况下使用最合适?——适用场景和最佳实践
- JavaScript 最适合在哪些场景下使用?
- 前端开发:JavaScript 是前端开发的核心语言,可以用来创建交互式网页、动态内容和用户界面。
- 单页应用(SPA):利用框架(如 React、Vue、Angular)和 JavaScript,可以开发复杂的单页应用,提供更流畅的用户体验。
- 服务器端开发:通过 Node.js,JavaScript 可以用于处理请求、操作数据库等后端开发任务,实现全栈开发。
- 异步操作和事件驱动:JavaScript 的事件循环和异步机制使其非常适合处理用户交互、网络请求和文件操作等场景。
- JavaScript 最适合在哪些场景下使用?
未来的发展方向是什么?——展望潜力与改进空间
- JavaScript 的未来发展方向是什么?
- 增强性能:随着技术的进步,JavaScript 引擎和 V8 引擎等正在不断优化执行效率,减少内存占用,提升速度。
- WebAssembly(Wasm):WebAssembly 使得可以在浏览器中执行其他语言(如 C、C++)编写的代码,未来 JavaScript 可能与 WebAssembly 深度结合,实现更高效的网页应用。
- 更加现代的功能:随着语言本身的演进,JavaScript 将继续加强对函数式编程、类型检查(如 TypeScript)、模块化等现代开发理念的支持。
- 多线程和并发:JavaScript 的 Web Worker API 和异步编程机制让开发者能够以更高效的方式处理多线程任务,未来可能有更多原生支持并发操作的特性。
- JavaScript 的未来发展方向是什么?
1. 基础概念
1.1 数据类型
基本数据类型(Primitive Types):
undefined
,null
,boolean
,number
,string
,symbol
(ES6)引用数据类型(Reference Types):
Object
,Array
,Function
1.1.1 JavaScript 中的常见数据类型总结
- 基本数据类型:undefined, null, boolean, number, string, symbol, bigint
- 引用数据类型:Object, Array, Function
- 特殊值:NaN, Infinity, -Infinity
1.1.2 为什么要区分不同的数据类型?
- 内存管理:基本数据类型通常存储在栈中,操作快速而简洁;引用数据类型存储在堆中,可以共享数据但操作相对复杂。
- 性能优化:基本数据类型是值传递,复制代价小,效率高;引用数据类型是引用传递,允许多个变量共享数据,内存开销较大,但更灵活。
- 程序逻辑清晰:通过区分基本数据类型和引用数据类型,开发者可以更直观地理解和操作数据,避免不必要的错误。
1.1.3 值传递和引用传递的区别?
JavaScript 中有两种传递机制:值传递和引用传递,这两种机制与数据类型的行为息息相关。
- 值传递:在基本数据类型中,当一个值被赋给另一个变量时,实际上是复制了该值。修改副本不会影响原始值。
- 引用传递:在引用数据类型中,传递的是对象的引用(内存地址)。多个变量可能指向同一内存地址,修改其中一个变量的值会影响到其他引用同一对象的变量。
- 相关问题:
- 为什么基本数据类型是值传递,而引用数据类型是引用传递?
- 基本数据类型是值传递,因为它们直接存储实际的值,而引用数据类型是引用传递,因为它们存储的是对象的内存地址,多个变量可以指向同一个对象。
- 引用传递可能导致的副作用如何避免?(如:对象共享和修改的问题)
- 可以通过创建对象的副本(深拷贝或浅拷贝)来避免修改共享引用数据带来的副作用。
- 为什么基本数据类型是值传递,而引用数据类型是引用传递?
1.1.4 值传递和引用传递的区别?
JavaScript 中的 类型强制 机制可能会导致一些隐式的类型转换,影响数据类型的行为。例如,在使用 + 操作符时,number 类型和 string 类型会被强制转换为字符串相加。
- 相关问题:
- 为什么 JavaScript 需要进行类型转换?如何处理隐式类型转换?
- JavaScript 需要进行类型转换是因为它是一种弱类型(loosely-typed)语言,不同类型的数据可以在运算中自动转换,以保证操作能够执行和满足多种场景的需求。
- 隐式类型转换通常由 JavaScript 引擎根据上下文自动进行,开发者需要理解不同类型之间的转换规则,特别是当不同数据类型参与运算时。例如,+ 运算符会将非字符串类型转换为字符串,而其他算术运算符则会将字符串转换为数字。要小心处理这些自动转换,确保操作符合预期。
- 什么是类型强制?如何避免出现隐式类型转换带来的错误?
- 类型强制(Type Coercion)是 JavaScript 中的一种机制,当运算符或函数期望一个特定的数据类型时,JavaScript 会自动将其他类型的值转换为所期望的类型,这种转换分为隐式类型强制和显式类型强制。
- 为了避免隐式类型转换带来的错误,可以通过以下方式:
- 使用严格相等(===) 代替非严格相等(==),避免自动类型转换带来的问题。
- 显式类型转换,即在进行运算或比较时,明确地将数据转换为所期望的类型,比如使用 String(), Number(), Boolean() 等进行显式转换。
- 谨慎使用类型不明确的运算符,尤其是在比较和字符串拼接时,明确了解不同类型的数据如何相互转换。
- 为什么 JavaScript 需要进行类型转换?如何处理隐式类型转换?
1.1.5 原始类型 vs. 包装类型
JavaScript 中有 原始类型 和 包装类型 的概念。包装类型是原始类型的对象包装器(如 String, Number, Boolean)。它们允许你在原始类型上调用方法和属性。
- 原始类型:例如 string, number, boolean 等。
- 包装类型:如 new String(), new Number(), new Boolean(),这些对象提供了额外的方法和属性,但它们和原始类型有所不同。
- 相关问题:
- 为什么 JavaScript 有包装类型?它们和原始类型有何不同?
- JavaScript 通过包装类型为原始类型提供对象方法和属性,弥补原始类型无法直接调用方法的限制。
- 原始类型是不可变的,直接存储值;包装类型是对象,提供原始类型的封装,允许调用方法和访问属性。
- JavaScript 中的自动类型转换(隐式转换)是如何在包装类型和原始类型之间工作的?
- 当原始类型值需要访问方法时,JavaScript 会临时将其转换为包装对象,操作完成后再返回原始值。
- 包装类型在调用方法时会临时创建一个对象,调用结束后对象会被销毁,这个过程称为“自动装箱”和“自动拆箱”。
- 为什么 JavaScript 有包装类型?它们和原始类型有何不同?
1.1.6 符号(Symbol)类型
Symbol 是 JavaScript 中 ES6 引入的 基本数据类型,它的目的是为对象的属性名提供唯一性。每一个 Symbol 都是唯一的,因此它可以用于创建“私有”属性,防止属性名冲突。
- 相关问题:
- Symbol 与其他数据类型有什么区别?为什么它是唯一的?
- Symbol 是一种原始数据类型,表示唯一的标识符,与其他数据类型的值不同,Symbol 值不可变且不可通过 new 操作符创建。
- 每个 Symbol 值都是唯一的,即使使用相同的描述符创建多个 Symbol,它们也不会相等。Symbol 的唯一性来源于其内建的机制,每次创建一个 Symbol 时,它会生成一个全局唯一的标识符,即使使用相同的描述符,也会返回不同的 Symbol 值。
- 在实际开发中如何使用 Symbol 来避免属性冲突或实现私有成员?
- 可以使用 Symbol 作为对象属性的键,以避免属性冲突,因为 Symbol 总是唯一的,且不会被外部访问到,从而能模拟私有成员。
- Symbol 与其他数据类型有什么区别?为什么它是唯一的?
1.1.7 类型推断与动态类型语言
JavaScript 是 动态类型语言,这意味着变量的类型在运行时才会确定,开发者不需要显式声明类型。这带来了灵活性,但也增加了出错的可能性。
- 相关问题:
- 为什么 JavaScript 是动态类型语言?与静态类型语言相比有哪些优缺点?
- JavaScript 是动态类型语言,因为变量的类型是在运行时确定的,而不是在编写代码时显式声明的。变量可以在不同时间持有不同类型的值,类型检查在程序执行时进行。
- 优点:
- 更高的灵活性:无需提前声明类型,代码更简洁、易于编写。
- 开发速度较快:可以在运行时动态处理不同类型的数据。
- 缺点:
- 运行时错误:由于类型在运行时决定,可能导致类型错误在程序执行时才发现。
- 可维护性差:在大型项目中,动态类型可能会增加调试和修改的难度。
- 如何通过静态类型检查工具(如 TypeScript)来提升代码的可维护性?
- 通过使用静态类型检查工具,如 TypeScript,开发者可以在编写代码时明确指定变量类型,提前捕获类型错误,增加代码的可读性、可预测性和可维护性,尤其在大型项目中能有效减少错误和提高代码的可靠性。
- 为什么 JavaScript 是动态类型语言?与静态类型语言相比有哪些优缺点?
1.1.8 类型兼容性和类型保护
在 JavaScript 中,类型兼容性和类型保护是处理不同类型之间的转换、赋值和操作时需要考虑的一个方面。
- 类型兼容性:JavaScript 在进行类型转换时,会自动检查值是否兼容。
- 类型保护:通过条件判断(如 typeof、instanceof)来确保数据的类型是预期的。
- 相关问题:
- 类型保护如何确保数据类型的安全性?如何避免类型错误?
- 类型保护通过明确检查和限制值的类型,使得在运行时仅允许特定类型的数据通过,从而避免类型不匹配或不符合预期的数据操作。例如,使用 typeof 或 instanceof 来确保变量是某种特定类型,从而提供类型安全。
- 如何避免类型错误?
- 类型检查:使用 typeof, instanceof 等运算符显式检查数据类型,确保其符合预期。
- TypeScript 类型注解:通过静态类型检查工具(如 TypeScript)在编译时捕获类型错误,避免运行时出错。
- 防御性编程:使用条件判断和默认值等方法,确保操作之前数据符合预期的类型和格式。
- 使用类型守卫(Type Guards):在代码中使用类型守卫(例如类型断言或类型保护函数),确保变量在特定范围内是正确的类型,从而避免意外的类型错误。
- 类型保护如何确保数据类型的安全性?如何避免类型错误?
1.2 变量声明与作用域
var
,let
,const
的区别- 作用域:全局作用域、函数作用域、块级作用域
- var:属于 函数作用域,在声明前访问会得到 undefined,而且会存在变量提升(Hoisting),即变量声明被提升到函数/全局作用域的顶部。
- let 和 const:属于 块级作用域,只在块级代码块内有效(如 if、for 等),不会发生变量提升。let 允许变量重新赋值,而 const 是常量,声明时必须初始化,且一旦赋值后不能修改。
- 作用域链和闭包
- 作用域链:当访问一个变量时,JavaScript 会根据作用域链的规则从当前作用域开始查找,逐级向上查找,直到找到该变量或到达全局作用域。如果找不到,抛出 ReferenceError。
- 闭包:闭包是指函数在访问其外部作用域的变量时,形成的一种持久的作用域链。即使外部函数执行完毕,内部函数依然能访问外部作用域的变量。
1.3 类型转换与判断
强制类型转换(如
Number()
,String()
,Boolean()
)- 强制类型转换通过内置函数明确地将一个值转换为特定的类型,帮助处理不同类型数据的转换需求。
类型判断:
typeof
,instanceof
,Object.prototype.toString.call()
- 类型判断使用不同的操作符和方法(typeof, instanceof, Object.prototype.toString.call())来判断数据的类型,以确保在代码中进行正确的类型处理和避免类型错误。
1.4 基本操作
- 算术、比较、逻辑运算符
- 赋值与解构赋值
- 解构赋值允许从数组或对象中提取值并将其赋给变量,提供了一种简洁的赋值方式。对于数组,按位置提取值;对于对象,按键提取值。
- 字符串操作、数组操作(包括常用方法
map()
,filter()
,reduce()
等)- 字符串常用方法:
charAt()
,concat()
,indexOf()
,slice()
,substring()
,toUpperCase()
,toLowerCase()
,trim()
,split()
,replace()
等 - 数组常用方法:
map()
,filter()
,reduce()
,forEach()
,push()
,pop()
,shift()
,unshift()
,find()
,sort()
,concat()
- 字符串常用方法:
2. 核心概念
2.1 闭包 (Closure)
- 闭包的定义和应用:如何通过闭包访问外部函数的变量
- 定义:
- 闭包是 JavaScript 中一个重要的概念,它是由函数及其函数内部引用的外部变量组成的一个结构。
- 本质:闭包允许内部函数访问外部函数的作用域,即使外部函数已经执行完毕并返回,内部函数依然可以访问外部函数的变量。
- 闭包的核心特点是 函数能够记住并访问定义时的作用域。
- 应用:
- 封装私有变量:闭包可以用来创建私有变量,这些变量只能在闭包内部访问,从而实现数据的封装和隐藏。
- 模拟私有方法:闭包可以用来模拟私有方法,使得这些方法只能在闭包内部调用,外部无法直接访问。
- 实现模块模式:闭包可以用来实现模块模式,将相关的数据和函数封装在一个闭包中,从而创建一个独立的模块。
- 实现回调函数:闭包可以用来实现回调函数,使得回调函数能够访问外部函数的变量。
- 定义:
- 闭包的性能问题(内存泄漏)
- 本质:闭包会保持对外部函数变量的引用,因此如果闭包引用的变量持续存在,会导致这些变量在不再需要时无法被垃圾回收,从而导致内存泄漏。
- 内存泄漏通常发生在以下情况下:
- 闭包引用了大量数据并且长时间存在(例如,长时间未被销毁的 DOM 元素引用)。
- 闭包被多次创建并且没有及时清除。
- 如何避免内存泄漏:
- 及时清除不再需要的闭包引用:通过将闭包置为 null 或通过垃圾回收机制释放不再使用的闭包引用。
- 避免闭包长期持有大量内存数据:减少闭包中持有大量数据的情况,尤其是引用 DOM 元素时,应谨慎避免不必要的长时间引用。
- 使用 WeakMap/WeakSet:使用弱引用数据结构(例如 WeakMap 或 WeakSet)来避免对大对象的强引用,从而减少内存泄漏风险。
2.2 原型与原型链
- 原型链的工作原理:如何通过原型链查找属性
Object.prototype
和构造函数的原型prototype
和__proto__
的关系
2.2.1 原型链和继承
原型链(Prototype Chain) 是 JavaScript 中继承机制的核心,它允许对象通过原型链访问父对象的属性和方法。所有的对象都继承自 Object 类型,它的原型就是 null。
- 原型链的形成:每个对象都有一个隐式的 [[Prototype]] 属性,指向它的构造函数的 prototype 对象。对象属性查找是通过原型链逐层向上查找的。
- 相关问题:
- 原型链是如何工作的?它与数据类型的继承关系如何结合?
- 原型链是 JavaScript 中对象属性和方法查找的一种机制,当访问一个对象的属性或方法时,若该对象没有该属性,则会查找该对象的原型对象,依此类推,直到找到该属性或到达原型链的顶端。
- 原型链实现了对象之间的继承关系,子对象可以继承父对象的属性和方法,通过原型链可以动态共享父对象的功能和数据。
- 为什么使用原型链来进行继承,而不是直接使用类的继承?
- JavaScript 最初是基于原型的继承模型而设计的,原型链继承灵活且实现简单,在 ES6 引入类(class)语法后,类的继承本质上也是基于原型链机制的。
- 原型链是如何工作的?它与数据类型的继承关系如何结合?
2.2.2 深拷贝与浅拷贝
在处理引用数据类型时,通常需要考虑 深拷贝 和 浅拷贝 的问题。浅拷贝只是复制对象的引用,而深拷贝会递归地复制对象的所有属性和子对象。
- 浅拷贝:只复制对象的引用。
- 深拷贝:递归地复制对象的所有属性,甚至包括嵌套对象。
- 相关问题:
- 深拷贝和浅拷贝的实现原理是什么?如何在 JavaScript 中手动实现它们?
- 浅拷贝:创建一个新对象,新对象的属性是对原对象属性的引用,若原属性是基本数据类型,则复制值,若原属性是引用数据类型,则复制引用。
- 深拷贝:创建一个新对象,新对象的所有属性(包括嵌套的对象)都会递归地进行拷贝,确保没有任何引用共享。
- 浅拷贝:可以使用
Object.assign()
方法或展开运算符(...
)来实现浅拷贝。 - 深拷贝:使用递归来手动拷贝所有嵌套对象,也可以使用
JSON.parse(JSON.stringify(obj))
方法来实现深拷贝,但这种方法无法处理函数、undefined
、Symbol
等特殊数据类型。
- 为什么浅拷贝可能引起问题(如引用共享问题)?
- 浅拷贝仅复制对象的引用,因此修改新对象的嵌套对象会影响原对象,反之亦然,这会导致不期望的副作用,特别是当多个对象共享同一个引用时。
- 深拷贝和浅拷贝的实现原理是什么?如何在 JavaScript 中手动实现它们?
2.3 事件循环 (Event Loop)
同步与异步的区别:
- 同步:代码按顺序逐行执行,执行完当前任务才会执行下一个任务,可能导致阻塞。
- 异步:任务会被挂起,当前任务执行完后,异步任务进入队列,等待执行,不会阻塞主线程。
JavaScript 的执行栈与事件队列:
- 执行栈 (Call Stack):JavaScript 代码的执行顺序是由调用栈管理的。同步任务按照先进后出的顺序执行。
- 事件队列 (Event Queue):当异步任务(如
setTimeout
、事件处理函数、Promise 回调等)完成时,会将回调函数推入事件队列。只有执行栈空闲时,事件队列中的回调函数才会被执行。
宏任务与微任务的执行顺序:
- 宏任务 (Macrotasks):比如
setTimeout
、setInterval
、I/O 操作等。每次事件循环开始时,执行栈中的任务按顺序执行。 - 微任务 (Microtasks):如
Promise.then
、MutationObserver
、process.nextTick
等。微任务在每轮事件循环的宏任务执行后立即执行,但优先于下一个宏任务。 - 执行顺序:执行栈 -> 微任务队列 -> 宏任务队列。
- 宏任务 (Macrotasks):比如
事件循环的异步操作:
- 异步操作(如
setTimeout
,Promise
等)通过事件循环机制,使得非阻塞性操作可以在完成后执行回调,而不会阻塞主线程的执行。 setTimeout
等宏任务会被加入宏任务队列,在执行栈空闲时执行,而Promise
等微任务会被加入微任务队列,在执行栈完成后立即执行。
- 异步操作(如
2.3.1 事件循环 (Event Loop) 的原理与本质
事件循环的本质就是将 同步任务 和 异步任务 分离,通过 事件队列 和 执行栈 的配合,确保异步任务按顺序被执行而不阻塞主线程,保证 JavaScript 能在单线程环境中高效处理异步操作。
- 原理:事件循环是 JavaScript 用来处理异步操作和并发任务的一种机制。它的核心原理是 分离同步任务与异步任务的执行,通过一个 事件循环 来确保异步任务能够在合适的时机执行,而不阻塞主线程。
- 本质:
- 主线程单线程执行:JavaScript 是单线程的,意味着只有一个执行栈可以同时执行任务。
- 任务分离:JavaScript 将同步任务和异步任务分开处理,同步任务依赖于执行栈,异步任务则通过事件队列来排队。
- 非阻塞:事件循环保证了异步操作(如网络请求、计时器、I/O 操作等)不会阻塞 JavaScript 代码的执行,通过将异步任务的回调函数推送到队列中,按时执行,最大限度地利用 CPU 资源。
- 微任务与宏任务:事件循环中存在微任务队列和宏任务队列的区分,微任务的优先级高于宏任务,因此它们会在每轮事件循环的同步任务和宏任务之间执行。
3. 面向对象编程 (OOP)
3.1 对象和类
- 对象字面量和构造函数
- 对象字面量:直接创建对象,简单易用。
- 构造函数:通过函数创建对象,可以使用 new 关键字实例化。
this
的指向与作用- 类(ES6+):
class
,constructor
,extends
,super
class
: 用class
关键字定义类,它是ES6
引入的语法糖,封装了构造函数和方法。
- 静态方法和实例方法
- 实例方法是属于实例的函数,静态方法属于类本身。
3.1.1 this
的指向与作用
this
的指向:- 在 全局上下文(非函数内)中,
this
指向 全局对象(浏览器中是 window)。 - 在 函数上下文 中,
this
指向 调用该函数的对象(在严格模式下,this
为 undefined)。 - 在 对象方法 中,
this
指向该方法所在的对象。 - 在 构造函数 中,
this
指向通过 new 关键字创建的实例对象。 - 在 箭头函数 中,
this
是 词法绑定,即取决于外部上下文的this
。
- 在 全局上下文(非函数内)中,
3.2 继承与多态
3.2.1 原型链继承与类继承
JavaScript 中的继承机制基于原型链,本质上是基于对象的动态原型继承模型。每个对象都有一个隐式的链接指向其原型对象,通过这个链条,子对象可以访问父对象的属性和方法。
原型链继承: 原型链继承是 JavaScript 通过构造函数和原型链来实现继承的方式。子类的实例通过
new
创建,会继承父类的所有属性和方法。原型链继承的核心是通过prototype
链接到父类的构造函数和方法。- 实现方式: 通过让子类的 prototype 指向父类的实例来实现继承,这样子类就能继承父类的方法和属性。
js// 原型链继承 function Animal(name) { this.name = name; } Animal.prototype.speak = function () { console.log(`${this.name} makes a sound.`); }; function Dog(name) { Animal.call(this, name); // 继承父类属性 } Dog.prototype = Object.create(Animal.prototype); // 继承父类方法 Dog.prototype.constructor = Dog; // 修正构造函数指向 const dog = new Dog("Rex"); dog.speak(); // 输出 'Rex makes a sound.'
类继承(ES6+): 类继承是 ES6 引入的继承机制。通过
extends
关键字,可以直接继承父类的属性和方法。类继承实际上是基于原型链的继承实现的,但语法上更加简洁、清晰。- 实现方式: 使用
class
和extends
关键字,子类通过super()
调用父类的构造函数,并继承父类的方法。
js// 类继承(ES6+) class Animal { constructor(name) { this.name = name; } speak() { console.log(`${this.name} makes a sound.`); } } class Dog extends Animal { constructor(name, breed) { super(name); // 调用父类的构造函数 this.breed = breed; } speak() { super.speak(); // 调用父类的方法 console.log(`${this.name} barks.`); } } const dog = new Dog("Rex", "Golden Retriever"); dog.speak(); // 输出: // 'Rex makes a sound.' // 'Rex barks.'
- 实现方式: 使用
3.2.2
Object.create()
和继承关系Object.create()
方法可以创建一个新的对象,并将指定的对象作为新对象的原型,从而实现继承。它是实现继承的一种简洁方式,避免了传统的原型链继承中常见的问题。- 优点:清晰、灵活,可以避免构造函数中重复的属性。
- 缺点:使用
Object.create()
方式创建的对象无法直接访问构造函数。
3.2.3 方法重写与多态
- 方法重写(Override): 方法重写指的是子类重新实现父类的方法。子类继承父类的方法后,可以根据需要修改该方法的实现,以便适应子类的需求。
- 实现方式: 在子类中重写父类的某个方法,使得在调用时,子类的实现会覆盖父类的实现。
- 多态(Polymorphism): 多态是指同一种方法在不同对象上有不同的表现形式。通过继承和方法重写,多个子类可以使用同名的方法,但表现不同。这允许不同类型的对象以相同的方式调用方法,但实际执行不同的行为。
- 实现方式: 通过继承和方法重写实现多态。
- 方法重写(Override): 方法重写指的是子类重新实现父类的方法。子类继承父类的方法后,可以根据需要修改该方法的实现,以便适应子类的需求。
3.3 封装与模块化
封装是面向对象编程中的一项基本原则,它指的是将对象的内部状态(数据)和行为(方法)包装在一起,对外部提供明确的接口,并隐藏实现细节。通过封装,外部只能通过公共方法(即接口)与对象进行交互,从而避免直接访问和修改对象的内部状态。
- 3.3.1 私有变量和方法
- 私有成员:通过使用闭包或 WeakMap 等手段,私有变量可以确保外部无法直接访问和修改对象的内部状态。ES6 引入了私有字段(通过 # 语法)来实现更清晰的私有属性。
- 封装与闭包:闭包能够实现私有变量,因为闭包允许函数访问其外部函数的作用域,即使外部函数已经返回。通过将私有变量存储在闭包中,可以使得外部无法直接访问这些变量。
- 3.3.2 闭包与封装设计模式
- 封装的核心原理:
- 数据隐藏:封装通过私有变量和方法,防止外部直接访问和修改对象的内部数据,保证数据的一致性和安全性。
- 提供公共接口:通过公共的方法(即 getter 和 setter)来操作私有数据,确保外部对数据的访问是受控的。
- 行为与状态绑定:封装不仅包含数据(状态),还包含数据相关的行为(方法)。它通过将数据与行为绑定在一起,使得对象成为一个功能模块。
- 封装设计模式是一种常见的设计模式,目的是通过隐藏复杂的内部实现细节,只暴露简单易用的接口,来简化外部与对象的交互。常见的封装设计模式有:
- 模块化模式:将代码分成若干个相对独立的模块,每个模块封装了相关的功能,外部通过明确的接口与模块交互。
- 工厂模式:通过工厂方法封装对象的创建过程,外部无需了解对象的创建细节,只需要调用工厂方法即可获得所需的对象。
- 封装的核心原理:
- 3.3.3 模块化
- 模块化:模块化是一种将代码组织成独立、可重用和可维护的代码单元的方法。它通过将代码分割成多个模块,每个模块负责实现特定的功能,从而提高代码的可维护性和可重用性。
- 模块化工具:在现代 JavaScript 开发中,模块化已经成为一种标准实践。常见的模块化工具包括:
- CommonJS:CommonJS 是 Node.js 使用的模块化规范,它定义了模块的加载和导出机制。在 CommonJS 中,每个文件都被视为一个模块,可以使用
require
函数导入其他模块,使用module.exports
导出模块。 - ES6 模块:ES6 模块是 JavaScript 语言规范的一部分,它提供了原生的模块化支持。在 ES6 模块中,使用
import
语句导入模块,使用export
语句导出模块。 - Webpack:Webpack 是一个流行的 JavaScript 模块打包工具,它可以将多个模块打包成一个或多个文件,从而实现模块的加载和依赖管理。Webpack 支持多种模块化规范,包括 CommonJS、ES6 模块等。
- Babel:Babel 是一个 JavaScript 编译器,它可以将 ES6 模块编译成 CommonJS 模块,以便在 Node.js 环境中使用。Babel 还支持其他 ES6+ 特性,如箭头函数、模板字符串等。
- CommonJS:CommonJS 是 Node.js 使用的模块化规范,它定义了模块的加载和导出机制。在 CommonJS 中,每个文件都被视为一个模块,可以使用
4. 现代 JavaScript 特性 (ES6+)
4.1 ES6 语法特性
4.1.1
let
,const
和块级作用域let
和const
是块级作用域的变量声明方式,区别于var
(后者是函数级作用域)。let
用于声明一个变量,它可以被重新赋值,但不能重复声明。const
用于声明一个常量,声明后必须立即赋值,并且该值不能被改变。
4.1.2 解构赋值:数组解构、对象解构
- 数组解构:按照元素的位置从数组中提取值。
- 对象解构:通过对象的属性名从对象中提取值。
js// 数组解构 let [a, b] = [1, 2]; // 对象解构 let { name, age } = { name: "Alice", age: 25 };
4.1.3 模板字符串(Template Literals)
- 模板字符串是 ES6 中引入的字符串插值方式,使用反引号(`)包裹字符串,可以在其中插入变量或表达式。通过 ${} 语法来嵌入变量或表达式,支持多行字符串和字符串插值。
4.1.4 箭头函数(Arrow Functions)
- 箭头函数是 ES6 中新增的一种简化函数声明的语法,使用
=>
语法。箭头函数有以下特性:- 没有自己的
this
:箭头函数不会创建自己的this
,它的this
来自于函数定义时的上下文(即外层函数的this
)。 - 简洁的语法:可以省略函数体内的
return
关键字和大括号(当只有一行代码时)。
- 没有自己的
- 箭头函数是 ES6 中新增的一种简化函数声明的语法,使用
4.1.5 默认参数、剩余参数(Rest Parameters)
- 默认参数:在函数定义时为参数设置默认值,若函数调用时未传递该参数,使用默认值。
- 剩余参数:使用
...
语法来接收不定数量的参数,将它们放入一个数组中。常用于处理可变参数的函数。
4.1.6 扩展运算符(Spread Operator)
扩展运算符
...
用于将数组或对象“展开”为单独的元素或属性。在数组中,...
将数组元素展开;在对象中,...
用于展开对象的属性。作用:
- 数组合并和浅拷贝:扩展运算符可以很方便地实现数组的合并或创建数组的浅拷贝。
- 对象合并:扩展运算符可以用于合并多个对象,复制对象的属性,避免了传统的对象复制方法。
jslet arr1 = [1, 2]; let arr2 = [...arr1, 3, 4]; // 合并数组 let obj1 = { name: "Alice" }; let obj2 = { age: 25 }; let obj3 = { ...obj1, ...obj2 }; // 合并对象
4.1.7 类与继承(Class & Inheritance)
- ES6 引入了类(
class
)的概念,这是对基于原型的继承的封装,使得 JavaScript 更接近传统面向对象编程语言的语法。类定义了构造函数、方法以及继承机制。- 类声明:使用
class
关键字定义类。 - 构造函数:用
constructor
定义类的构造函数,用于初始化类的实例。 - 继承:使用
extends
关键字实现继承,子类可以继承父类的属性和方法,使用super
调用父类的构造函数和方法。
- 类声明:使用
- ES6 引入了类(
4.2 模块化
4.2.1 模块的引入与导出:
import
和export
export
:用于导出模块中的变量、函数、类等,可以是命名导出或默认导出。命名导出:导出多个对象,可以按需导入。
js// 导出 export const PI = 3.14; export function calculateArea(radius) { return PI * radius * radius; } // 导入 import { PI, calculateArea } from "./math";
默认导出:每个模块只能有一个默认导出,导出的对象可以是任何值(函数、对象、类等)。
js// 导出 export default function greet(name) { return `Hello, ${name}`; } // 导入 import greet from "./greet"; console.log(greet("World")); // "Hello, World"
import
:用于引入模块,可以按需引入命名导出的成员,也可以引入默认导出的内容。- 按需导入:可以选择导入某个模块的部分功能。js
import { functionA, functionB } from "./module";
- 默认导入:可以直接导入模块的默认导出。js
import defaultExport from "./module";
- 按需导入:可以选择导入某个模块的部分功能。
4.2.2 动态导入:
import()
- 动态导入:import() 语法允许在运行时动态地导入模块,这对按需加载(懒加载)非常有用。
- 应用场景:常用于懒加载、条件加载、按需加载等,可以延迟模块的加载,直到需要时才加载它们,优化性能。js
// 例如,在用户触发某个操作后才加载模块 document.getElementById("loadButton").addEventListener("click", () => { import("./math").then((module) => { console.log(module.calculateArea(5)); // 使用动态导入的模块 }); });
4.2.3 CommonJS 和 ES Module 比较
CommonJS 和 ES Module 各自有不同的适用场景和特点,ES Module 是现代 JavaScript 的标准模块系统,支持浏览器和 Node.js,而 CommonJS 主要用于 Node.js 中的同步模块加载。
CommonJS(CJS):
- 定义:CommonJS 是 Node.js 中的模块系统,它采用同步方式加载模块。
- 导出:通过
module.exports
导出内容,通过require()
引入。
js// 导出 module.exports = function greet(name) { return `Hello, ${name}`; }; // 导入 const greet = require("./greet"); console.log(greet("World"));
ES Module(ESM):
- 定义:ES6 引入的原生模块化标准,支持在浏览器和 Node.js 中使用,提供异步模块加载机制。
- 导出:通过
export
和export default
导出,使用import
导入。
js// 导出 export function greet(name) { return `Hello, ${name}`; } // 导入 import { greet } from "./greet"; console.log(greet("World"));
比较:
- 同步 vs 异步:CommonJS 是同步加载模块,而 ES Module 是异步加载模块,适用于浏览器和 Node.js 环境。
- 导出方式:CommonJS 使用
module.exports
和require()
,ES Module 使用export
和import
。 - 适用场景:CommonJS 更适合 Node.js 环境中的同步操作,而 ES Module 更适合浏览器环境中的异步操作。
- 兼容性:ES Module 在浏览器中需要通过
<script type="module">
标签来启用,而 CommonJS 在 Node.js 中是原生支持的。 - 模块解析:CommonJS 使用文件系统路径解析模块,而 ES Module 使用 URL 解析模块。
- 循环依赖:CommonJS 支持循环依赖,而 ES Module 不支持循环依赖。
- 动态导入:ES Module 支持动态导入,而 CommonJS 不支持。
- Tree Shaking(树摇优化):ES Module 支持树摇,而 CommonJS 不支持。
- 作用域:ES Module 有自己的作用域,而 CommonJS 没有作用域。
- 模块缓存:ES Module 有自己的模块缓存,而 CommonJS 没有模块缓存。
- 模块类型:ES Module 支持多种模块类型,如 ES Module、CommonJS、JSON 等,而 CommonJS 只支持 CommonJS 模块类型。
5. 异步编程
5.1 回调函数 (Callback)
5.1.1 基本概念与应用:回调地狱
- 回调函数是一个函数,它被作为参数传递给另一个函数,并在某个操作完成后被调用。回调函数通常用于处理异步操作或延迟执行的任务。
- 应用:回调函数广泛应用于异步编程中,尤其是在处理 I/O 操作、定时任务、事件监听等场景。
- 回调地狱(Callback Hell) 是指嵌套多层回调函数的情况,使得代码的可读性和可维护性大大下降。通常出现在异步操作中,多个异步操作的回调函数被嵌套在一起,从而形成类似“金字塔”结构的代码,导致理解和调试困难。js
doSomething(function (result) { doSomethingElse(result, function (newResult) { doThirdThing(newResult, function (finalResult) { console.log(finalResult); }); }); });
5.1.2 解决回调地狱的技巧
目前常见的通用做法是:使用
Promise
和 使用async/await
。使用命名函数
使用
Promise
- 使用
Promise
来改写回调函数。Promise
可以帮助我们更清晰地处理异步操作,避免回调嵌套。它通过链式调用(.then
())来实现异步操作的顺序执行,避免了回调地狱。
jsdoSomething() .then((result) => doSomethingElse(result)) .then((newResult) => doThirdThing(newResult)) .then((finalResult) => console.log(finalResult)) .catch((error) => console.error(error));
- 使用
使用
async/await
- 使用
async/await
可以将异步操作写成同步代码的形式,消除回调地狱。async
函数会返回一个Promise
,await
可以暂停执行,直到Promise
返回结果,从而实现异步操作的顺序执行。
jsasync function doEverything() { try { let result = await doSomething(); let newResult = await doSomethingElse(result); let finalResult = await doThirdThing(newResult); console.log(finalResult); } catch (error) { console.error(error); } } doEverything();
- 使用
使用事件驱动
- 事件驱动编程模式可以在某些情况下避免回调地狱。通过在事件发生时触发特定的回调,而不是让回调函数直接嵌套,来解耦逻辑。
jsconst EventEmitter = require("events"); const emitter = new EventEmitter(); emitter.on("done", function (result) { doSomethingElse(result, function (newResult) { emitter.emit("done2", newResult); }); }); emitter.on("done2", function (newResult) { doThirdThing(newResult, function (finalResult) { console.log(finalResult); }); }); doSomething(function (result) { emitter.emit("done", result); });
使用生成器函数 (
Generators
)
5.2 Promise
Promise 是一种用于处理异步操作的对象,它代表了异步操作的最终完成(或失败)及其结果值。Promise 让异步代码更加清晰,避免了传统的回调地狱问题。
5.2.1
Promise
的基本概念和状态:pending
,fulfilled
,rejected
状态只能从 pending 转变为 fulfilled 或 rejected,一旦状态改变,就不会再改变。
- pending(待定):初始状态,表示异步操作尚未完成。
- fulfilled(已完成):表示异步操作成功完成,并带有结果值。
- rejected(已拒绝):表示异步操作失败,并带有错误原因。
jslet promise = new Promise((resolve, reject) => { let success = true; if (success) { resolve("Success!"); } else { reject("Failure!"); } }); promise .then((result) => { console.log(result); // Success! }) .catch((error) => { console.log(error); // Failure! });
5.2.2 链式调用:
then()
,catch()
then()
:用于指定Promise
成功时的回调函数,返回一个新的Promise
,从而支持链式调用。then()
返回的Promise
可以继续调用后续的then()
或catch()
方法。catch()
:用于指定Promise
失败时的回调函数,也返回一个新的Promise
。它是then(null, rejection)
的简写,专门用于处理Promise
的拒绝情况。
5.2.3 异常处理:
catch()
与错误传递catch()
:用于处理Promise
链中的异常。当then()
中的回调函数抛出错误或Promise
被拒绝时,catch()
会捕获并处理错误。- 错误会沿着链式调用传递,直到找到合适的
catch()
来处理。如果链条上没有catch()
,则会导致未处理的Promise
错误。
5.2.4
Promise.all()
,Promise.race()
Promise.all()
:接收一个Promise
数组,返回一个新的Promise
,当所有输入的Promise
都变为fulfilled
时,新的Promise
也会变为fulfilled
,并返回所有Promise
的结果。如果有任何一个Promise
被拒绝,Promise.all()
立即返回拒绝结果,并且停止等待其他Promise
。- 应用场景:
Promise.all()
用于并发执行多个异步操作,并等待所有的异步操作完成。 - 1)示例: 上传多个文件,并在全部上传成功后进行合并。
- 2)示例: 多个异步操作,但只要一个失败就需要处理错误。
- 应用场景:
Promise.race()
:接收一个Promise
数组,返回一个新的Promise
,该Promise
会以第一个完成(无论是成功还是失败)的Promise
的结果为准。如果第一个Promise
变为fulfilled
,则返回该值;如果第一个Promise
变为rejected
,则返回拒绝的错误信息。- 应用场景:
Promise.race()
用于并发执行多个异步操作,并返回最先完成的那个Promise
,不管它是成功还是失败。 - 1)示例: 网络请求超时,设置一个超时时间,如果超时则返回超时错误。
- 2)示例: 在多个异步操作中,只需要一个操作成功即可。
- 应用场景:
5.3 Async/Await
5.3.1
async
和await
的语法async
:用来声明一个函数为异步函数。异步函数总是返回一个Promise
。如果函数内有return
值,则返回的Promise
会被解析为这个值。jsasync function myAsyncFunction() { return 42; // 实际上是 Promise.resolve(42) }
await
:只能在async
函数内部使用,用于等待一个Promise
完成。它会暂停async
函数的执行,直到Promise
被解决(resolved)或拒绝(rejected)。如果Promise
被解决,await
表达式的结果就是Promise
的解决值;如果Promise
被拒绝,await
会抛出一个错误,需要使用try...catch
来捕获。jsasync function myAsyncFunction() { try { const result = await somePromise(); console.log(result); } catch (error) { console.error(error); } }
5.3.2 异步函数的执行行为:
async
函数会立即返回一个Promise
,而不会阻塞主线程。在async
函数内部使用await
可以等待异步操作完成,而不需要使用回调函数或Promise.then()
。5.3.3 异步函数的返回值:
Promise
async
函数返回的始终是一个Promise
,即使函数内部没有明确返回一个Promise
。如果函数返回一个非Promise
的值,Promise
会自动将其包装为Promise.resolve(value)
。
jsasync function fetchData() { return "data"; // 等价于 Promise.resolve('data') } fetchData().then((data) => console.log(data)); // 'data'
5.3.4 错误处理:
try...catch
和await
- 使用
await
时,可以通过try...catch
来处理异步操作中的错误。这种方式比传统的Promise .catch()
更加简洁和直观。
jsasync function getData() { try { let data = await fetch("https://api.example.com"); let json = await data.json(); console.log(json); } catch (error) { console.error("Error fetching data:", error); // 捕获并处理错误 } }
- 使用
5.3.5
await
的执行时机和阻塞行为执行时机:
await
表达式会等待一个Promise
对象的解析,直到该Promise
被解决或者拒绝。如果await
后的Promise
是已解决的,它会立即返回该值;如果Promise
被拒绝,则会抛出异常。jsasync function waitForIt() { console.log("Start"); await new Promise((resolve) => setTimeout(resolve, 1000)); // 等待1秒 console.log("End"); } waitForIt(); // "Start" -> wait 1s -> "End"
阻塞行为:
await
会阻塞当前异步函数的执行,直到等待的Promise
被解决。然而,它不会阻塞整个线程或主线程,其他并发任务仍然可以继续执行。jsasync function example() { console.log("Start"); await someAsyncOperation(); // 阻塞此函数的继续执行 console.log("End"); } // 在上面的代码中,'Start' 会首先被打印,之后函数会等待 someAsyncOperation 完成,最后打印 'End'。
6. 高级概念
6.1 函数式编程 (Functional Programming)
6.1.1 高阶函数:函数作为参数和返回值
- 定义:高阶函数是指接收一个或多个函数作为参数,或者返回一个函数的函数。
- 本质:高阶函数利用了 JavaScript 的函数作为一等公民的特性,允许函数操作其他函数,从而提供更高的抽象和灵活性。
- 应用:
- 函数作为参数:将函数作为参数传递,可以实现不同功能的复用和定制。
- 函数作为返回值:函数可以返回另一个函数,从而实现延迟执行、闭包等特性。
6.1.2 函数组合与柯里化(Currying)
函数组合:函数组合是将多个函数连接成一个新的函数,新函数的输出是前一个函数的输出,通常用于函数链式调用。
jsconst add = (x) => x + 1; const multiply = (x) => x * 2; const addThenMultiply = (x) => multiply(add(x)); // 先加1,再乘2 console.log(addThenMultiply(3)); // 8
柯里化(Currying):柯里化是将一个接受多个参数的函数转化为一系列接受一个参数的函数,并逐步传入参数。
- 本质:柯里化是函数式编程中的一种技术,使得函数的参数可以逐步传入,这样可以生成更灵活的函数和组合逻辑。
jsfunction multiply(a) { return function (b) { return a * b; }; } const multiplyBy2 = multiply(2); console.log(multiplyBy2(3)); // 6
6.1.3 不可变性(Immutability)与纯函数(Pure Function)
- 不可变性(Immutability):在函数式编程中,不可变性是指数据一旦被创建就不能改变。对于需要修改数据的操作,可以通过返回一个新的数据结构来代替修改原数据。js
const person = { name: "Alice", age: 25 }; // 修改需要返回新对象,而不是修改原对象 const updatedPerson = { ...person, age: 26 }; console.log(person); // { name: 'Alice', age: 25 } console.log(updatedPerson); // { name: 'Alice', age: 26 }
- 纯函数:
- 相同输入始终产生相同输出。
- 没有副作用,不会修改外部状态或数据。
jsfunction add(a, b) { return a + b; // 纯函数:相同的输入永远返回相同的输出,且不修改外部状态 }
- 不可变性(Immutability):在函数式编程中,不可变性是指数据一旦被创建就不能改变。对于需要修改数据的操作,可以通过返回一个新的数据结构来代替修改原数据。
6.1.4
map()
,filter()
,reduce()
的应用map()
:对数组中的每个元素应用一个函数,并返回一个新数组。filter()
:根据一个函数的返回值过滤数组中的元素,并返回一个新数组。reduce()
:对数组中的元素进行累积操作,并返回一个单一的值。
6.2 异步模式与设计模式
6.2.1 发布订阅模式(Pub/Sub)
本质:发布订阅模式解耦了消息的发送者和接收者,允许系统中的各个部分独立工作而不直接依赖于彼此。
应用:事件监听、消息队列、WebSockets 等。
js// 简单实现 class EventEmitter { constructor() { this.events = {}; } on(event, listener) { if (!this.events[event]) { this.events[event] = []; } this.events[event].push(listener); } emit(event, data) { if (this.events[event]) { this.events[event].forEach((listener) => listener(data)); } } } const emitter = new EventEmitter(); emitter.on("data", (data) => console.log(`Received: ${data}`)); emitter.emit("data", "Hello, World!"); // 输出: "Received: Hello, World!"
6.2.2 观察者模式(Observer)
本质:观察者模式是一种设计模式,它定义了对象间的一对多依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知并自动更新。
应用:DOM 事件、数据绑定、状态管理(如 Vue 的响应式系统)等。
jsclass Subject { constructor() { this.observers = []; } addObserver(observer) { this.observers.push(observer); } notify() { this.observers.forEach((observer) => observer.update()); } } class Observer { update() { console.log("Observer notified"); } } const subject = new Subject(); const observer1 = new Observer(); subject.addObserver(observer1); subject.notify(); // 输出: "Observer notified"
6.2.3 中介者模式(Mediator)
本质:中介者模式是一种行为设计模式,它通过一个中介对象来封装对象之间的交互,从而减少对象之间的直接依赖关系。
应用:GUI 框架、事件总线、聊天室等。
jsclass Mediator { constructor() { this.components = []; } register(component) { this.components.push(component); } notify(sender, message) { this.components.forEach((component) => { if (component !== sender) { component.receiveMessage(message); } }); } } class Component { constructor(name, mediator) { this.name = name; this.mediator = mediator; } sendMessage(message) { console.log(`${this.name} sends message: ${message}`); this.mediator.notify(this, message); } receiveMessage(message) { console.log(`${this.name} received message: ${message}`); } } const mediator = new Mediator(); const componentA = new Component("Component A", mediator); const componentB = new Component("Component B", mediator); mediator.register(componentA); mediator.register(componentB); componentA.sendMessage("Hello, World!");
6.2.4 状态模式(State Pattern)
本质:状态模式是一种行为设计模式,它允许对象在其内部状态改变时改变其行为。状态模式将对象的行为封装在不同的状态对象中,使得对象的行为可以根据其状态而改变。
应用:有限状态机、UI 状态切换、游戏角色状态等。
jsclass Order { constructor() { this.state = new PendingState(); // 初始状态 } setState(state) { this.state = state; } handle() { this.state.handle(this); } } class PendingState { handle(order) { console.log("Order is pending"); order.setState(new ShippedState()); // 改变状态 } } class ShippedState { handle(order) { console.log("Order is shipped"); order.setState(new DeliveredState()); } } class DeliveredState { handle(order) { console.log("Order is delivered"); } } const order = new Order(); order.handle(); // 输出: Order is pending order.handle(); // 输出: Order is shipped order.handle(); // 输出: Order is delivered
6.3 内存管理与性能优化
6.3.1 垃圾回收机制(GC)
6.3.1 垃圾回收(Garbage Collection)
JavaScript 的内存管理和清理是通过 垃圾回收机制 来实现的。垃圾回收负责清除不再被引用的数据,以避免内存泄漏。两种常见的垃圾回收策略是:
引用计数(Reference Counting):跟踪每个对象被引用的次数,当引用次数为 0 时,对象可以被回收。
标记-清除(Mark-and-Sweep):遍历所有的对象,将不再引用的对象标记为“可回收”,然后清除它们。
相关问题:
- JavaScript 是如何管理内存和执行垃圾回收的?
- JavaScript 自动管理内存,通过垃圾回收机制定期回收不再使用的内存,通常使用标记清除算法和引用计数来判断哪些对象需要销毁。
- 垃圾回收与引用数据类型的关系是什么?为什么引用数据类型的生命周期更难管理?
- 引用数据类型的内存管理依赖于对象的引用关系,只有当没有其他引用指向该对象时,垃圾回收机制才会回收其占用的内存。
- 引用数据类型的生命周期受多个引用控制,可能导致内存泄漏(如循环引用、闭包或事件监听器未及时清除),使得垃圾回收器难以准确判断何时回收。
- JavaScript 是如何管理内存和执行垃圾回收的?
6.3.2 闭包与内存泄漏
6.3.3 事件委托与优化 DOM 操作
- 事件委托 是一种将事件处理器绑定到父元素上,而不是直接绑定到每个子元素上的技术。通过事件冒泡机制,事件处理器可以捕获到子元素的事件。
- 减少事件监听器的数量:避免在每个子元素上添加事件监听器,而是在父元素上使用一个统一的事件处理器,降低内存开销。
- 动态添加的子元素也能响应事件:因为事件是由父元素捕获的,任何新增的子元素都可以直接响应事件。
- 优化 DOM 操作 的目标是减少直接对 DOM 的访问,避免触发多次重排(reflow)和重绘(repaint),提升页面性能。
- 事件委托 是一种将事件处理器绑定到父元素上,而不是直接绑定到每个子元素上的技术。通过事件冒泡机制,事件处理器可以捕获到子元素的事件。
6.3.2 栈和堆的内存管理
栈和堆 是 JavaScript 存储数据的两种不同方式,基本数据类型和引用数据类型的存储方式分别对应这两者。
栈(Stack):用于存储基本数据类型(如 number, boolean, undefined 等)和函数调用的上下文(执行环境)。栈的特点是:快速、内存使用高效、遵循 LIFO(后进先出)原则。数据的生命周期通常较短,生命周期由函数调用的栈帧决定。
堆(Heap):用于存储引用数据类型(如 Object, Array, Function 等)。堆内存分配较慢,但能够存储更复杂的数据结构。引用数据类型的生命周期通常由引用计数(或者垃圾回收机制)管理。
相关问题:
- 为什么栈中的数据访问速度比堆中的数据更快?
- 栈中的数据是顺序存储并且具有固定大小,CPU 可以通过栈指针直接访问,速度更快,而堆中的数据需要通过指针间接访问,增加了额外的寻址和内存管理开销。
- 为什么引用数据类型存储在堆中而不是栈中?堆中内存的管理和回收是如何进行的?
- 引用数据类型的大小不固定,且可能会动态改变,栈空间有限且大小固定,因此无法存储此类数据,而堆空间更适合动态分配和存储较大的、不定大小的对象。
- 堆内存的管理通常通过垃圾回收机制(如标记清除、引用计数等)来自动回收不再使用的对象,确保不会出现内存泄漏,减少不再使用的内存占用。
- 为什么栈中的数据访问速度比堆中的数据更快?
7. 浏览器与 DOM 操作
7.1 DOM 操作
7.1.1 选择器:
getElementById()
: 通过元素的id
获取单个元素。querySelector()
: 通过 CSS 选择器获取匹配的第一个元素,支持更灵活的选择方式。querySelectorAll()
: 获取所有匹配的元素,返回一个 NodeList。
7.1.2 DOM 元素创建与修改:
createElement()
: 创建一个新的 DOM 元素,但还未添加到页面中。innerHTML
: 设置或获取元素的 HTML 内容,支持直接操作 HTML 结构,但需要注意 XSS 攻击。appendChild()
: 向父元素添加子元素,将一个节点添加为子节点。removeChild()
: 删除父元素中的子元素,返回被删除的节点。
7.1.3 事件处理:
addEventListener()
: 为元素绑定事件监听器,可以指定事件类型(如click
,mouseenter
),以及是否启用事件冒泡或捕获。- 事件冒泡与捕获:
- 冒泡:事件从目标元素逐级向上传递到
document
。 - 捕获:事件从
document
向下传递到目标元素。
- 冒泡:事件从目标元素逐级向上传递到
相关提示:事件冒泡与捕获:何时发生,浏览器差异
1. 冒泡(Bubbling)
事件冒泡是事件传播的默认顺序。在事件触发时,事件会从目标元素开始,逐级向上传播,经过其父元素,直到 document
或 window
。冒泡是浏览器默认的事件传播方式。
什么时候会冒泡:
- 当你没有显式指定
capture
参数或设置useCapture
为false
时,事件会按默认的冒泡顺序向上传递。 - 例如,点击一个嵌套的
<div>
元素,点击事件会先触发目标元素,然后向上传递给父元素,直到document
。
- 当你没有显式指定
示例:
html<div id="parent"> <div id="child">Click me!</div> </div> <script> document.getElementById("parent").addEventListener("click", function () { console.log("Parent clicked"); }); document.getElementById("child").addEventListener("click", function () { console.log("Child clicked"); }); </script>
- 点击
child
时,输出:Child clicked Parent clicked
- 点击
2. 捕获(Capturing)
捕获是指事件从 document
(或者 window
)开始,逐级向下传递,直到到达目标元素。捕获是事件传播的反向过程。
什么时候会捕获:
- 捕获行为需要显式设置。通过在
addEventListener
中设置useCapture
为true
,或设置capture
参数为true
,可以启用捕获阶段。
- 捕获行为需要显式设置。通过在
示例:
html<div id="parent"> <div id="child">Click me!</div> </div> <script> document.getElementById("parent").addEventListener( "click", function () { console.log("Parent clicked during capture"); }, true ); // 第三个参数为 true 表示捕获阶段 document.getElementById("child").addEventListener( "click", function () { console.log("Child clicked"); }, true ); // 第三个参数为 true 表示捕获阶段 </script>
- 点击
child
时,输出:Parent clicked during capture Child clicked
- 点击
3. 浏览器差异
- 事件流顺序(Capture vs Bubbling):大多数现代浏览器(如 Chrome、Firefox、Edge)都遵循标准的事件流顺序:捕获阶段 -> 目标阶段 -> 冒泡阶段。
- 默认情况下,事件会冒泡,但如果在
addEventListener
中显式设置为捕获,事件会从外层元素捕获到目标元素。
- 默认情况下,事件会冒泡,但如果在
- 旧版浏览器(如 IE)差异:
- IE 事件模型:早期的 Internet Explorer(特别是 IE 8 及更早版本)使用了不同的事件模型,只有冒泡事件,没有捕获机制。
- IE 9+:开始支持标准的事件模型(包括捕获和冒泡),但是对于
addEventListener
需要添加浏览器前缀,或者使用attachEvent
(仅支持冒泡)。
4. 事件顺序总结
- 默认(冒泡):事件从目标元素开始,逐级向上传递到
document
。 - 捕获:事件从
document
开始,逐级向下传递到目标元素。 - 通过设置
useCapture
或capture
参数来切换捕获和冒泡。
5. 如何避免事件冒泡或捕获
- 可以通过
event.stopPropagation()
阻止事件的传播,无论是冒泡还是捕获阶段。 - 还可以使用
event.stopImmediatePropagation()
来阻止当前事件及后续事件的触发。
小结:
- 冒泡是默认的事件传播方式,事件从目标元素开始向上传递。
- 捕获需要显式设置为
true
,事件从document
向目标元素传递。 - 现代浏览器遵循标准事件流,但旧版 IE 浏览器只支持冒泡。
7.2 浏览器特性
7.2.1 事件循环与异步请求:
setTimeout()
: 在指定时间延迟后执行一次指定的函数,常用于定时任务。setInterval()
: 每隔指定时间执行一次指定函数,适用于重复执行的定时任务。requestAnimationFrame()
: 在下次浏览器重绘时执行指定的回调函数,通常用于动画效果,它比setTimeout()
更高效,因为它可以和浏览器的渲染同步。
7.2.2 本地存储:
localStorage
: 用于持久化存储数据,数据会在浏览器关闭后保持。sessionStorage
: 用于存储数据,数据在页面会话结束后清除,即浏览器或标签页关闭时数据丢失。cookies
: 用于存储小型数据,通常用于会话跟踪、身份验证等,每个请求都会带上相关的 Cookie 数据。
SessionID(会话标识符)是什么?
SessionID 是一个用于唯一标识用户与服务器之间会话(Session)的标识符。它通常是在用户访问网站时由服务器生成并发送给客户端,客户端通过该标识符将自己的请求与会话数据关联起来。SessionID 通常存储在客户端的 Cookie 中,或者通过 URL 参数传递。
本质与原理
- 会话管理:
- 会话(Session) 是指用户与服务器之间的一段时间内的交互过程。通常,一个会话从用户登录到退出、关闭浏览器或会话过期为止。
- SessionID 用于标识和跟踪同一用户在整个会话中的活动。每当用户发送请求到服务器时,服务器通过 SessionID 获取该用户的会话数据,保持用户的状态。
- 如何工作:
- 当用户访问网站时,服务器会生成一个唯一的 SessionID,并将它发送给客户端(通常是通过 Cookie)。
- 客户端(浏览器)每次向服务器发送请求时,都会将该 SessionID 发送回服务器。
- 服务器通过该 SessionID 查找与之相关联的会话数据,从而为用户提供一致的体验,例如保持登录状态、购物车内容等。
SessionID 的生成
- 随机生成:服务器通常会通过随机算法生成一个唯一的 SessionID,确保每个用户的会话是唯一的。SessionID 通常是一个长字符串,包含字母、数字或其他字符。
- 不可预测:为了避免 SessionID 被猜测和滥用,生成的 SessionID 应该是不可预测的。
SessionID 的存储
- Cookie:最常见的存储方式是将 SessionID 存储在浏览器的 Cookie 中,这样每次用户发送请求时,浏览器会自动将 SessionID 附带在请求头中。
- URL 参数:在一些情况下,SessionID 也可能通过 URL 参数传递(虽然这种方式不常用,因为可能泄漏 SessionID)。
SessionID 的安全性
- 防止劫持:由于 SessionID 是用于识别用户会话的关键,因此如果被恶意用户窃取,就可能导致会话劫持(Session Hijacking)。为了防止这种情况,可以采取以下安全措施:
- 使用 HTTPS 加密通信,防止 SessionID 在传输过程中被窃取。
- 定期更换 SessionID,避免长期使用一个 SessionID。
- 设置 HttpOnly 和 Secure 标志,确保 Cookie 在 JavaScript 中不可访问,且只能通过 HTTPS 发送。
- 使用 SameSite Cookie 属性,限制第三方网站不能在跨站点请求中携带 SessionID。
与 Cookie 和 Token 的区别
- SessionID 与 Cookie:
- SessionID 常常存储在 Cookie 中,但它们是不同的概念:Cookie 是存储在客户端的数据,而 SessionID 是服务器用来标识用户会话的唯一标识符。
- SessionID 与 Token(如 JWT):
- SessionID 通常存储在服务器端(如数据库或内存中),与客户端发送的 SessionID 进行匹配。
- Token(如 JWT) 是一种自包含的标识符,通常存储在客户端,服务器无需存储状态,因此不需要管理会话数据。
小结
SessionID 是一个用于标识用户会话的唯一标识符,它帮助服务器识别和管理用户的会话状态。它通常通过 Cookie 发送给客户端,并随着每个请求被返回给服务器。通过 SessionID,服务器可以在没有存储用户状态的情况下,为用户提供一致的体验。
Cache Storage 是什么?
Cache Storage 是浏览器提供的一种存储机制,属于 Web Storage API 的一部分,用于存储 HTTP 请求的响应数据,通常用于离线应用和提升网页加载性能。
主要特点:
缓存请求和响应:它允许你存储和检索 请求(Request) 和 响应(Response) 对象,使得应用能够缓存网络资源。这样,当网络不可用或请求相同资源时,可以直接从缓存中获取响应,而无需重新发起请求。
服务工作者的核心组成部分:Cache Storage 是 Service Worker(服务工作者)用于离线支持和网络请求拦截的核心 API。服务工作者可以拦截网络请求,检查缓存是否有匹配的响应,并决定是否返回缓存的响应或继续进行网络请求。
离线能力:通过使用 Cache Storage,Web 应用可以实现 离线功能,即使没有网络连接,用户也能访问缓存的内容。
Key-Value 存储:Cache Storage 将数据按键(URL)存储,键是请求的 URL,值是缓存的响应。它的 API 提供了方法来存储、查找和删除缓存的数据。
关键方法:
caches.open(cacheName)
:打开一个缓存存储区域(如果不存在则创建)。cache.put(request, response)
:将指定的请求和响应存储到缓存中。cache.match(request)
:查找缓存中是否存在与请求匹配的响应。caches.delete(cacheName)
:删除指定的缓存存储区域。
示例代码:
javascript// 打开或创建一个缓存 caches.open("my-cache").then((cache) => { // 缓存一个请求的响应 cache.put("/path/to/resource", new Response("Hello, world!")); // 检查缓存中是否有该请求的响应 cache.match("/path/to/resource").then((response) => { if (response) { console.log("Found in cache:", response); } }); });
使用场景:
- 提升页面加载速度:通过缓存静态资源(如图片、CSS、JavaScript 文件),可以减少对网络的请求,提高网页加载速度。
- 离线支持:对于某些 Web 应用,可以使用 Cache Storage 来缓存数据,确保用户在没有网络时仍然可以访问应用的某些功能。
- 更好的网络管理:通过在 Service Worker 中使用 Cache Storage,可以精细控制缓存的策略,比如只缓存特定的资源,或在某些条件下更新缓存。
总的来说,Cache Storage 提供了一种高效的方式来管理 Web 应用中的缓存,提高了性能,尤其是在离线和低网速环境下。
特性 Cookie localStorage sessionStorage Cache Storage 存储大小 每个 cookie 大约 4KB 通常为 5MB 或更大(取决于浏览器) 通常为 5MB 或更大(取决于浏览器) 可以存储大量的资源(通常没有严格大小限制) 存储位置 浏览器内存与硬盘(随请求发送) 浏览器本地存储(硬盘) 浏览器会话存储(浏览器内存) 浏览器缓存(硬盘) 数据存储时间 默认会在到期时间后过期,可以设置过期时间 持久存储,直到用户手动清除 会话存储,页面关闭后数据会丢失 可以存储请求和响应,通常由开发者控制生命周期 数据访问方式 通过 document.cookie
获取通过 localStorage
API 获取通过 sessionStorage
API 获取通过 caches
API 获取是否与请求一起发送 是,每次请求时自动携带 否,除非手动传递 否,除非手动传递 否,除非由服务工作者或 JavaScript 手动处理 支持的存储类型 只能存储字符串数据 存储字符串类型数据 存储字符串类型数据 存储请求与响应(可以是对象、数组等) 是否有跨页面共享 是,所有页面共享 cookies 是,所有页面共享 localStorage
数据否,仅在当前会话(窗口/标签页)内共享 是,所有使用相同缓存名称的页面或 service worker 共享 支持的浏览器 所有浏览器 所有现代浏览器 所有现代浏览器 所有现代浏览器 安全性 不安全(数据可通过网络中介拦截) 不安全(数据可被客户端 JS 访问) 不安全(数据可被客户端 JS 访问) 安全(由 service worker 管理,避免恶意脚本访问) 为什么 Service Worker 被认为提供了一定的安全性?
Service Worker 被认为提供了一定的安全性,主要是因为它的工作方式和与页面的隔离性。具体来说,有以下几个原因:
1. 受限的访问权限
Service Worker 与普通的 JavaScript 执行环境(即浏览器主线程)是分离的,它在后台独立运行。它并不直接访问 DOM,因此无法直接操作页面上的内容,这减少了潜在的恶意脚本攻击。
2. 可以防止跨站脚本攻击 (XSS)
Service Worker 提供了一种拦截和控制网络请求的机制,它可以通过 Fetch API 来缓存静态资源或自定义响应。由于服务工作者可以自行管理缓存和拦截请求,恶意脚本无法通过普通的请求篡改响应内容(例如,篡改一个 API 响应或修改资源)。这比直接将缓存内容暴露给浏览器中的普通 JavaScript 更安全。
3. 只允许通过 HTTPS 激活
Service Worker 只能在 HTTPS 环境下激活,这意味着它的功能不容易受到中间人攻击(Man-in-the-Middle Attack)。HTTPS 强制加密数据传输,防止了在数据传输过程中被篡改或窃取。
4. 不直接暴露给网页脚本
Service Worker 无法直接通过网页的 JavaScript 访问,它只能通过与页面的特定接口进行通信(例如,
postMessage()
)。这样即使攻击者通过 XSS 注入了恶意脚本,服务工作者的内容和行为也不会轻易受到影响。5. 缓存隔离与精确控制
Service Worker 对缓存的管理更为精细,开发者可以明确地指定哪些资源需要缓存,哪些不需要。缓存存储不再是单纯的浏览器默认行为,而是开发者可控的。即使攻击者通过其他手段修改了缓存,Service Worker 也能够基于设定的策略重新加载正确的资源。
总结:
由于 Service Worker 的执行在浏览器主线程之外,它有严格的访问权限控制,并且只能在 HTTPS 环境中启用,这使得它比传统的 JavaScript 更加安全。它的工作方式有效地减少了恶意代码对缓存内容和网络请求的篡改,提升了整体的安全性。
7.2.3 Fetch API 与 XMLHttpRequest:
Fetch API: 新的异步请求接口,比
XMLHttpRequest
更简洁且支持Promise
,用于进行网络请求,支持更强大的功能和更好的异步处理。XMLHttpRequest: 传统的网络请求方法,支持同步和异步请求,但相较于
Fetch
,语法较为繁琐,并且不支持Promise
,更容易产生回调地狱。axios
是基于Promise
的 HTTP 请求库,它在浏览器中使用XMLHttpRequest
,在 Node.js 中使用http/https
模块,并通过封装这些底层技术,提供了更简洁、功能强大的接口。
axios
和fetch
的一些常见用法对比下面是
axios
和fetch
的一些常见用法对比,展示它们在发起 HTTP 请求时的具体写法。1. GET 请求
使用
axios
: javascriptaxios .get("https://api.example.com/data") .then((response) => { console.log(response.data); // 成功时打印响应数据 }) .catch((error) => { console.error(error); // 错误处理 });
使用
fetch
: javascriptfetch("https://api.example.com/data") .then((response) => { if (!response.ok) { // 检查响应是否成功 throw new Error("Network response was not ok"); } return response.json(); // 解析 JSON 响应体 }) .then((data) => { console.log(data); // 成功时打印响应数据 }) .catch((error) => { console.error(error); // 错误处理 });
2. POST 请求
使用
axios
: javascriptaxios .post("https://api.example.com/data", { name: "John Doe", age: 30, }) .then((response) => { console.log(response.data); // 成功时打印响应数据 }) .catch((error) => { console.error(error); // 错误处理 });
使用
fetch
: javascriptfetch("https://api.example.com/data", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ name: "John Doe", age: 30, }), }) .then((response) => { if (!response.ok) { // 检查响应是否成功 throw new Error("Network response was not ok"); } return response.json(); // 解析 JSON 响应体 }) .then((data) => { console.log(data); // 成功时打印响应数据 }) .catch((error) => { console.error(error); // 错误处理 });
3. 请求头设置
使用
axios
: javascriptaxios .get("https://api.example.com/data", { headers: { Authorization: "Bearer your-token", }, }) .then((response) => { console.log(response.data); }) .catch((error) => { console.error(error); });
使用
fetch
: javascriptfetch("https://api.example.com/data", { method: "GET", headers: { Authorization: "Bearer your-token", }, }) .then((response) => { if (!response.ok) { throw new Error("Network response was not ok"); } return response.json(); }) .then((data) => { console.log(data); }) .catch((error) => { console.error(error); });
4. 发送
FormData
(用于上传文件) 使用
axios
: javascriptconst formData = new FormData(); formData.append("file", fileInput.files[0]); axios .post("https://api.example.com/upload", formData) .then((response) => { console.log(response.data); }) .catch((error) => { console.error(error); });
使用
fetch
: javascriptconst formData = new FormData(); formData.append("file", fileInput.files[0]); fetch("https://api.example.com/upload", { method: "POST", body: formData, }) .then((response) => { if (!response.ok) { throw new Error("Network response was not ok"); } return response.json(); }) .then((data) => { console.log(data); }) .catch((error) => { console.error(error); });
5. 并行请求
使用
axios
: javascriptaxios .all([ axios.get("https://api.example.com/data1"), axios.get("https://api.example.com/data2"), ]) .then( axios.spread((response1, response2) => { console.log(response1.data); console.log(response2.data); }) ) .catch((error) => { console.error(error); });
使用
fetch
: javascriptPromise.all([ fetch("https://api.example.com/data1"), fetch("https://api.example.com/data2"), ]) .then((responses) => Promise.all(responses.map((response) => response.json())) ) .then((data) => { console.log(data[0]); // 第一个请求的响应 console.log(data[1]); // 第二个请求的响应 }) .catch((error) => { console.error(error); });
6. 请求取消(通过
CancelToken
或AbortController
) 使用
axios
: javascriptconst cancelTokenSource = axios.CancelToken.source(); axios .get("https://api.example.com/data", { cancelToken: cancelTokenSource.token, }) .then((response) => { console.log(response.data); }) .catch((error) => { if (axios.isCancel(error)) { console.log("Request cancelled", error.message); } else { console.error(error); } }); // 取消请求 cancelTokenSource.cancel("Request was cancelled");
使用
fetch
: javascriptconst controller = new AbortController(); const signal = controller.signal; fetch("https://api.example.com/data", { signal }) .then((response) => { if (!response.ok) { throw new Error("Network response was not ok"); } return response.json(); }) .then((data) => { console.log(data); }) .catch((error) => { if (error.name === "AbortError") { console.log("Request was cancelled"); } else { console.error(error); } }); // 取消请求 controller.abort();
总结:
axios
提供了更多的功能和简洁的 API,如请求和响应拦截器、取消请求、自动转换 JSON 等,使用起来非常方便。fetch
更为轻量,是原生的 API,语法简洁,但不支持像axios
那样丰富的功能,处理上需要额外的工作,例如错误处理和 JSON 转换等。
8. 工具与框架
8.1 调试与测试
8.1.1 调试工具
- 浏览器控制台:
- 浏览器自带的开发者工具提供了强大的调试功能,通常包括 Console, Network, Elements, Sources 等面板。
- Console 用于输出日志、警告和错误信息,支持直接交互式运行 JavaScript 代码。
- Sources 面板用于调试代码,设置断点并逐步执行。
- Network 面板用于查看网络请求和响应,调试 AJAX 请求等。
- 调试语句:
- console.log(): 输出普通日志信息,帮助查看变量、对象的值。
- console.error(): 输出错误日志信息,通常用于调试出现问题的部分,浏览器会以红色显示。
- console.warn(): 输出警告信息,浏览器会以黄色显示,适用于调试潜在的问题。
- console.table(): 输出格式化表格,适用于查看数组或对象的结构。
8.1.2 单元测试
Jest:
- Jest 是一个由 Facebook 开发的 JavaScript 测试框架,支持 单元测试、集成测试 和 端到端测试。Jest 提供内置的断言库,支持快照测试和异步测试,易于集成到 React 和其他框架中。
- 特点:自动化模拟(mock)、异步支持、集成良好的代码覆盖工具。
Mocha:
- Mocha 是一个灵活的 JavaScript 测试框架,通常与断言库(如 Chai)配合使用。Mocha 提供了强大的测试控制,如定时器支持、钩子(before, after)等。
- 特点:支持异步测试,可以与各种断言库和模拟工具(如 Sinon)配合使用。
Chai:
- Chai 是一个断言库,通常与 Mocha 配合使用。它支持三种不同风格的断言:assert、expect 和 should,帮助编写更加可读和直观的测试代码。
8.1.3 自动化测试与集成测试
自动化测试:
- 自动化测试指的是通过编写脚本来自动执行测试用例,减少人工测试的成本和时间。常见的自动化测试工具有 Selenium、Puppeteer 等。
- Puppeteer:一个 Node.js 库,提供了一个高级 API 用于控制 Chrome 或 Chromium 浏览器,适用于自动化浏览器操作、生成页面截图等。
集成测试:
- 集成测试是验证多个模块或组件在一起工作时是否能按预期协同工作。它通常涉及到较大范围的测试,包括网络请求、数据库操作等。
- 在 React 或 Vue 等现代前端框架中,集成测试经常涉及到测试组件之间的数据流和交互,确保多个部分能够正确协作。
8.2 构建工具与模块打包
此版块内容有专题文章进行讲解,此处不再赘述。
Webpack
,Parcel
,Rollup
等构建工具- Babel 转译与 Polyfill
- NPM 与 Yarn 包管理工具