Truthy、Falsy与true、false的区别
Details
- truthy和true还是不一样的,隐含有true属性的变量不可以认为它是true,它不是boolean类型!
像很多语言一样,javascript也支持boolean数据类型(有true和false两个值),不过特别的是,javascript中的任何对象都还隐含一个boolean值,这便是大家所说的truthy和falsy原则。我们可以很方便的使用这个隐含的属性,特别是在变量比较上(if条件句)。掌握好这些特别的规则有助于调试我们的前端代码。
- 以下的值都隐含有false属性:
- false
- 0(零)
- "" (空串)
- null
- undefined
- NaN(Not-a-Number)注意,这是个number类型!用来表示变量不是number的number类型,有些拗口
if ( [] ) {
// 这里的代码将会执行
} if ( [] == true ) {
// 这里的代码不会执行
} if ( [] == false ) {
// 这里的代码将会执行
}错误捕捉的方式
Details
JavaScript中可以使用try...catch语句来捕捉错误,try块中放置可能抛出错误的代码,catch块中处理错误。
- 使用try块包裹可能出错的代码,可以防止程序崩溃。
- catch块可以捕获错误并执行相应的处理逻辑,如记录错误信息或提示用户。
- 还可以使用finally块来执行无论是否发生错误都需要执行的代码,如清理资源。
reflect有几个参数
Details
Reflect对象的方法通常接受两个或三个参数,具体取决于方法。
- Reflect.apply(target, thisArgument, argumentsList)用于调用函数。
- Reflect.get(target, propertyKey, receiver)用于获取对象属性。
- Reflect.set(target, propertyKey, value, receiver)用于设置对象属性。
内存泄露怎么解决
Details
内存泄露可以通过及时释放不再使用的对象、避免全局变量、使用WeakMap和WeakSet等方式来解决。
- 定期检查和清理不再使用的对象,确保它们可以被垃圾回收。
- 避免使用全局变量,尽量将变量的作用域限制在必要的范围内。
- 使用WeakMap和WeakSet可以防止内存泄露,因为它们对对象的引用是弱引用,允许垃圾回收。
new 的实现原理
Details
new操作符会创建一个新对象,设置其原型为构造函数的prototype,并执行构造函数,最后返回新对象。
new操作符的第一步是创建一个空对象。- 然后将新对象���原型指向构造函数的prototype。
- 接着执行构造函数,并将
this指向新对象。 - 最后返回新对象,或者如果构造函数返回的是对象,则返回该对象。
apply bind和call的区别
Details
apply和call都用于改变函数的上下文,apply接受一个数组作为参数,而call接受多个参数。bind返回一个新的函数,绑定了特定的上下文。
apply适用于需要传入参数数组的情况,特别是在参数数量不确定时。call适用于已知参数数量的情况,可以直接传入参数。bind创建一个新函数,允许在稍后调用时指定this的值和初始参数。
bind中返回的函数中做什么
Details
bind返回的函数可以在调用时传入参数,这些参数会被预先绑定到原函数的参数上。
- 通过
bind,可以创建一个新的函数,固定某些参数,简化后续调用。 - 这种方式常用于事件处理和回调函数中,确保
this指向正确。 - 还可以在返回的函数中添加额外的参数,进一步增强灵活性。
原型与原型链
Details
原型是对象的一个属性,指向另一个对象,原型链是由多个对象的原型组成的链条,用于实现继承。
- 每个JavaScript对象都有一个原型,可通过
__proto__访问。 - 原型链允许对象访问其原型及其原型的原型,形成一个链条。
- 这种机制使得JavaScript能够实现继承和共享属性/方法。
作用域和作用域链
Details
作用域是变量的可访问范围,作用域链是由多个作用域组成的链条,用于查找变量。
- 作用域分为全局作用域和局部作用域,局部作用域可以嵌套。
- 当访问变量时,JavaScript会从当前作用域开始查找,直到找到为止。
- 如果在所有作用域中都找不到变量,则会抛出错误。
垃圾回收机制的方法
Details
JavaScript使用标记清除和引用计数等垃圾回收机制来自动管理内存。
- 标记清除:垃圾回收器会标记所有可达的对象,然后清除未被标记的对象。
- 引用计数:通过跟踪对象的引用数量来判断对象是否可以被回收。
- 现代JavaScript引擎通常使用标记清除作为主要的垃圾回收策略。
引用计数的弊端
Details
引用计数的弊端在于无法处理循环引用的情况,导致内存泄露。
- 当两个对象相互引用时,即使它们不再被使用,引用计数也不会降为零。
- 这会导致内存无法被回收,造成内存泄露。
- 现代JavaScript引擎通常结合使用标记清除来解决这个问题。
闭包
Details
闭包是指一个函数可以访问其外部作用域的变量,即使外部函数已经返回。
- 闭包允许函数保持对其外部变量的引用,形成一个持久的作用域。
- 这在实现私有变量和封装时非常有用。
- 闭包也可以用于创建工厂函数和回调函数。
array set weakSet的区别
Details
Array是有序的集合,Set是无序的唯一值集合,WeakSet只允许对象作为成员,并且对其不保持强引用。
- Array支持索引访问,适合存储有序数据。
- Set用于存储唯一值,避免重复,支持快速查找。
- WeakSet的成员是弱引用,允许垃圾回收,适合存储临时对象。
map weakMap的区别
Details
Map是键值对集合,WeakMap的键必须是对象,并且对其不保持强引用,防止内存泄露。
- Map支持任意类型的键,适合存储复杂数据结构。
- WeakMap的键是弱引用,适合存储与DOM元素相关的数据,避免内存泄露。
- WeakMap不支持遍历,适合用于私有数据存储。
异步
Promise 报错如何拦截
Details
在JavaScript中,Promise是用于处理异步操作的对象。Promise的错误处理可以通过catch方法来实现,或者在async/await语法使用try...catch来捕获错误。
使用
catch方法:当Promise被拒绝(rejected)时,可以使用
catch方法来处理错误。catch方法会接收一个回调函数,该函数会在Promise被拒绝时执行。示例:
javascriptconst promise = new Promise((resolve, reject) => { // 模拟异步操作 setTimeout(() => { reject(new Error('Something went wrong!')); }, 1000); }); promise .then(result => { console.log(result); }) .catch(error => { console.error('Error caught:', error.message); });
使用
async/await和try...catch:在使用
async/await语法时,可以将Promise放在try块中,并在catch块中捕获错误。这种方式使得异步代码的错误处理更加直观。
示例:
javascriptconst asyncFunction = async () => { try { const result = await promise; console.log(result); } catch (error) { console.error('Error caught:', error.message); } }; asyncFunction();
全局错误处理:
对于未处理的Promise拒绝,可以使用
window.onunhandledrejection事件来捕获。这可以帮助开发者捕获所有未处理���Promise错误,进行统一处理。
示例:
javascriptwindow.onunhandledrejection = (event) => { console.error('Unhandled promise rejection:', event.reason); };
总结:通过
catch方法、async/await结合try...catch,以及全局错误处理,可以有效地拦截和处理Promise中的错误,提升代码的健壮性。
代码使用太多try不优雅怎么办
Details
在使用async/await时,频繁使用try...catch可能会导致代码变得冗长和不优雅。以下是一些优化建议,可以帮助减少try...catch的使用频率,并提高代码的可读性。
封装错误处理:
可以将错误处理逻辑封装到一个单独的函数中,这样在调用异步函数时只需处理一次错误。
示例:
javascriptconst handleAsync = async (asyncFunc) => { try { return await asyncFunc(); } catch (error) { console.error('Error caught:', error.message); } }; const fetchData = async () => { return await handleAsync(async () => { const response = await fetch('https://api.example.com/data'); return response.json(); }); };
使用中间件模式:
在某些情况下,可以使用中间件模式来处理错误���尤其是在处理多个异步操作时。
示例:
javascriptconst asyncMiddleware = async (req, res, next) => { try { await next(); } catch (error) { console.error('Error caught:', error.message); res.status(500).send('Internal Server Error'); } }; const fetchData = async () => { await asyncMiddleware({}, {}, async () => { const response = await fetch('https://api.example.com/data'); return response.json(); }); };
使用Promise.all:
如果有多个异步操作,可以使用
Promise.all来并行处理,并在外层捕获错误。示例:
javascriptconst fetchMultipleData = async () => { try { const [data1, data2] = await Promise.all([ fetch('https://api.example.com/data1').then(res => res.json()), fetch('https://api.example.com/data2').then(res => res.json()), ]); console.log(data1, data2); } catch (error) { console.error('Error caught:', error.message); } };
使用自定义错误类:
创建自定义错误类,以便在捕获错误时提供更多上下文信息。
示例:
javascriptclass CustomError extends Error { constructor(message) { super(message); this.name = 'CustomError'; } } const fetchData = async () => { try { const response = await fetch('https://api.example.com/data'); if (!response.ok) { throw new CustomError('Failed to fetch data'); } return await response.json(); } catch (error) { console.error('Error caught:', error.message); } };
总结:通过封装错误处理、使用中间件模式、并行处理多个异步操作、创建自定义错误类等方式,可以减少
try...catch的使用频率,提高代码的可读性和优雅性。
ES6 提出的块级作用域解决了什么问题,结合 JS 调用栈
Details
ES6 引入的块级作用域主要通过 let 和 const 关键字来实现,解决了以下几个问题:
变量提升问题:
在 ES5 中,使用
var声明的变量会被提升到函数的顶部,导致在块级作用域内的变量在块外也可访问。这可能导致意外的行为和难以调试的错误。例如:
javascriptif (true) { var x = 10; } console.log(x); // 输出 10使用
let和const声明的变量只在其块级作用域内有效,避免了这种提升问题。javascriptif (true) { let y = 20; } console.log(y); // ReferenceError: y is not defined
循环中的作用域问题:
在使用
var声明循环变量时,所有的循环迭代共享同一个变量,导致在异步操作中可能出现意外的结果。例如:
javascriptfor (var i = 0; i < 3; i++) { setTimeout(() => { console.log(i); // 输出 3,循环结束后 i 的值 }, 100); }使用
let声明循环变量时,每次迭代都会创建一个新的作用域,解决了这个问题。javascriptfor (let j = 0; j < 3; j++) { setTimeout(() => { console.log(j); // 输出 0, 1, 2 }, 100); }
避免全局变量污染:
- 在 ES5 中,使用
var声明的变量如果在全局作用域中声明,可能会导致全局变量的污染。块级作用域可以帮助限制变量的作用范围,减少全局命名冲突的风险。
- 在 ES5 中,使用
更清晰的代码结构:
- 块级作用域使得代码的结构更加清晰,变量的作用范围更加明确,便于理解和维护。
结合 JS 调用栈
调用栈: JavaScript 的调用栈用于管理函数调用的执行顺序。当一个函数被调用时,它会被推入栈中,执行完成后从栈中弹出。
作用域链: 每个函数都有自己的作用域,变量的查找是通过作用域链进行的。块级作用域的引入使得在调用栈中,变量的查找更加明确,避免了因变量提升和共享作用域导致的混淆。
ES6 引入的块级作用域通过
let和const解决了变量提升、循环作用域、全局变量污染等问题,使得代码更加清晰和可维护。结合 JS 调用栈,块级作用域的引入使得变量的查找和管理更加高效,减少了潜在的错误。
Date.now() 与 performance.now()
Details
performance.now() 与 Date.now() 是 JavaScript 中用于获取时间戳的两个方法,但它们在精度、用途和底层实现上有显著区别:
- 时间起点
- Date.now()
- 返回自 1970 年 1 月 1 日 00:00:00 UTC(UNIX 纪元) 以来的毫秒数,表示绝对时间点。
- performance.now()
- 返回从 页面导航开始(即 performance.timing.navigationStart)或 Performance API 初始化 到当前时刻的时间,单位为毫秒(但精度更高)。在 Web Worker 中,起点是 Worker 初始化的时间。
- Date.now()
- 精度
- Date.now()
- 精度为 毫秒级(整数),受系统时钟调整(如用户修改时间)影响。
- performance.now()
- 精度通常为 微秒级(浮点数),且基于单调递增时钟,不受系统时间调整或时钟漂移影响,确保时间差值准确。
- Date.now()
- 用途
- Date.now()
- 适用于记录事件发生的绝对时间点(如日志时间戳)。
- performance.now()
- 专为高精度测量时间间隔设计(如函数执行耗时、动画帧率),适合性能分析和需要稳定增量的场景。
- Date.now()
- 浏览器支持
- Date.now()
- 支持广泛(ECMAScript 5+)。
- performance.now()
- 需较新浏览器(如 IE10+、现代浏览器),在 Web Worker 中可用,但精度可能因浏览器策略(如防安全漏洞)受限。
- Date.now()
- 返回值示例javascript
console.log(Date.now()); // 输出类似 1625097600000(整数,毫秒) console.log(performance.now()); // 输出类似 1234.567(浮点数,微秒级) - 抗干扰性
Date.now() 受系统时间变化影响,若时钟回拨可能导致负时间差。
performance.now() 单调递增,确保时间差始终非负。
何时选择?
- 用 Date.now():
- 需要记录事件发生的绝对时间(如日志、数据存储)。
- 用 performance.now():
- 需要精确测量代码性能、动画循环耗时等短期时间间隔,且要求结果不受系统时间干扰。
示例对比
// 使用 Date.now() 可能受系统时间影响
const dateStart = Date.now();
doTask();
const dateEnd = Date.now();
console.log(`Date 耗时:${dateEnd - dateStart}ms`); // 可能不准(如系统时间被调整)
// 使用 performance.now() 更可靠
const perfStart = performance.now();
doTask();
const perfEnd = performance.now();
console.log(`Performance 耗时:${perfEnd - perfStart}ms`); // 高精度且稳定| 总结 | Date.now() | performance.now() |
|---|---|---|
| 时间起点 | UNIX 纪元 | 页面导航/Worker 初始化 |
| 精度 | 毫秒(整数) | 微秒(浮点数,可能受浏览器限制) |
| 抗系统时间干扰 | ❌ 受系统时间调整影响 | ✅ 单调递增,不受影响 |
| 典型用途 | 记录绝对时间 | 测量高精度时间间隔 |
for...of 中的异步迭代语法区别
Details
for (const event of await fetch(offset)) 和 for await (const event of fetch(offset)) 这两种语法有重要区别:
第一种:for (const event of await fetch(offset))
for (const event of await fetch(offset)) {
// 处理 event
}工作原理:
- 首先等待
fetch(offset)完全执行完毕 - 该函数必须返回一个同步可迭代对象(如数组)
- 然后对这个结果进行常规的同步迭代
特点:
- 一次性获取所有数据后再开始迭代
- 适用于返回数组等同步可迭代对象的函数
- 内存使用可能较高(一次性加载所有数据)
第二种:for await (const event of fetch(offset))
for await (const event of fetch(offset)) {
// 处理 event
}工作原理:
fetch(offset)返回一个异步迭代器(AsyncIterable)- 逐个等待每一项的产生
- 支持流式处理,边获取边处理
特点:
- 流式处理,可以边获取数据边处理
- 内存效率更高
- 适用于大量数据或实时数据流
- 需要函数返回 AsyncIterable 对象
实际示例对比
第一种情况的函数实现:
async function fetch(offset) {
const response = await fetch(`/api/events?offset=${offset}`);
const data = await response.json();
return data.events; // 返回数组
}第二种情况的函数实现:
async function* fetch(offset) {
let currentOffset = offset;
while (true) {
const response = await fetch(`/api/events?offset=${currentOffset}&limit=10`);
const data = await response.json();
if (data.events.length === 0) break;
for (const event of data.events) {
yield event; // 逐个产生事件
}
currentOffset += data.events.length;
}
}使用场景建议
- 使用第一种:当你需要一次性获取所有数据,且数据量不是很大时
- 使用第二种:当处理大量数据、实时流数据,或者希望实现分页流式处理时
选择哪种方式主要取决于你的 fetch 函数的实现和你的具体需求。