原型链、逼到、异步、AMD/CMD/COMMONJS/ECM Monorepo 上下文 垃圾回收 Promise 防抖节流 ES6/ES7... React18
['1', '2', '3'].map(parseInt) what & why ?
当执行['1', '2', '3'].map(parseInt)
时,结果可能会令人意外。这是因为parseInt
函数在接受两个参数时的行为与预期不同。
parseInt
函数接受两个参数:要解析的字符串和基数(可选,默认为 10)。然而,Array.map()
在迭代数组时会传递三个参数给回调函数:当前迭代的元素、当前元素的索引和原始数组本身。
在上述代码中,['1', '2', '3'].map(parseInt)
的实际执行过程如下:
parseInt('1', 0, ['1', '2', '3'])
:根据基数 0 解析字符串'1',由于基数为 0,parseInt
会根据字符串的前缀决定其解析方式,'1'没有前缀,因此按十进制解析,返回数字 1。parseInt('2', 1, ['1', '2', '3'])
:根据基数 1 解析字符串'2',由于基数为 1,parseInt
会尝试将字符串解析为一个无效的数字,返回NaN
。parseInt('3', 2, ['1', '2', '3'])
:根据基数 2 解析字符串'3',由于基数为 2,parseInt
会尝试将字符串解析为一个二进制数字,但字符串中包含超出二进制表示范围的字符'3',因此解析失败,返回NaN
。
因此,实际的输出将是[1, NaN, NaN]
。
这个结果可能不是我们期望的,因为parseInt
的行为与我们在Array.map()
中使用时的预期不同。为了避免此类问题,可以使用箭头函数或传递单个参数给parseInt
来达到预期的结果,如下所示:
["1", "2", "3"].map(Number); // 使用 Number 函数替代 parseInt
// 或
["1", "2", "3"].map((item) => parseInt(item));
这样可以得到预期的输出[1, 2, 3]
,将字符串转换为对应的数字。
parseInt 不同的参数返回的结果有什么不同
parseInt
函数在接受不同的参数时会有不同的行为和返回结果。
只有一个参数: 当
parseInt
函数只接受一个参数时,它会尝试将该参数解析为整数。如果参数是一个字符串,它将解析字符串中的数字部分,忽略前导空格。如果参数无法解析为有效的整数,则返回NaN
。例如:parseInt('123')
返回数字 123。parseInt('123abc')
返回数字 123。parseInt('abc')
返回NaN
。
两个参数: 当
parseInt
函数接受两个参数时,第一个参数是要解析的字符串,第二个参数是基数(可选,默认为 10)。基数表示要解析的数字的进制,可以是2 到 36之间的整数。parseInt
会根据基数解析字符串,并返回相应的整数值。例如:parseInt('1010', 2)
将二进制字符串'1010'解析为十进制整数 10。parseInt('FF', 16)
将十六进制字符串'FF'解析为十进制整数 255。parseInt('11', 8)
将八进制字符串'11'解析为十进制整数 9。- 如果基数为 0,则
parseInt
会根据字符串的前缀决定解析方式。如果没有前缀或前缀为 0x,则按照十进制解析;如果前缀为 0,则按照八进制解析;如果前缀为 0b,则按照二进制解析。例如:parseInt('10')
根据前缀为 0x,解析为十进制整数 16。parseInt('010')
根据前缀为 0,解析为八进制整数 8。parseInt('0b10')
根据前缀为 0b,解析为二进制整数 2。
需要注意的是,parseInt
函数在解析过程中会忽略字符串中的非数字字符,并且停止解析到第一个非数字字符为止。如果字符串的第一个字符无法解析为数字,则返回NaN
。例如:
parseInt('123abc')
返回数字 123,忽略了字符串中的'abc'部分。parseInt('abc123')
返回NaN
,因为字符串的第一个字符无法解析为数字。
此外,parseInt
函数还可以处理负数、浮点数、科学计数法表示的数字等情况。对于非整数的情况,它会将其转换为整数,并丢弃小数部分。例如:
parseInt('-10')
返回整数-10。parseInt('3.14')
返回整数 3。parseInt('1e3')
返回整数 1,因为它是科学计数法的表示形式,将小数部分丢弃。
需要根据具体的需求和输入来选择适合的parseInt
参数使用方式,以获得正确的解析结果。
什么是防抖和节流?有什么区别?如何实现?
一个很有意思的比喻:防抖等于回城,节流等于放技能
防抖(Debounce)和节流(Throttle)都是用于控制函数触发频率的技术,但其核心原理和应用场景有所不同。
防抖(Debounce):
防抖是指在频繁触发一个函数时,在一定时间间隔内只执行一次函数,并且在该时间间隔内如果再次触发,则重新计时。也就是说,当连续触发函数时,只有在最后一次触发后的等待时间过去后才执行该函数。
应用场景:
- 浏览器窗口的调整大小(resize)事件,只在停止调整大小之后重新计算布局。
- 输入框输入内容验证,只在用户停止输入一段时间后进行验证。
实现方式: 可以通过使用定时器和事件处理函数来实现防抖。当触发事件时,先清除之前的定时器,然后设置一个新的定时器,在指定的时间间隔过后执行函数。
示例代码(JavaScript):
function debounce(func, delay) {
let timeoutId;
return function () {
const context = this;
const args = arguments;
clearTimeout(timeoutId);
timeoutId = setTimeout(function () {
func.apply(context, args);
}, delay);
};
}
// 使用防抖包装事件处理函数
const debouncedEventHandler = debounce(eventHandler, 300);
// 添加事件监听
element.addEventListener("input", debouncedEventHandler);
节流(Throttle):
节流是指在一定时间间隔内,无论触发频率多高,只执行一次函数。它会以固定的频率触发函数,而不管实际触发的频率是多高。
应用场景:
- 页面滚动事件(scroll),限制函数的触发频率,减少滚动事件处理的执行次数。
- 频繁点击按钮,限制按钮的响应频率,防止重复提交。
实现方式: 可以使用定时器和事件处理函数来实现节流。当触发事件时,首先检查上次执行函数的时间。如果已经过去了指定的时间间隔,则执行函数,并更新上次执行的时间;否则忽略该次触发。
示例代码(JavaScript):
function throttle(func, delay) {
let lastExecutionTime = 0;
return function () {
const context = this;
const args = arguments;
const currentTime = Date.now();
if (currentTime - lastExecutionTime >= delay) {
func.apply(context, args);
lastExecutionTime = currentTime;
}
};
}
// 使用节流包装事件处理函数
const throttledEventHandler = throttle(eventHandler, 300);
// 添加事件监听
element.addEventListener("scroll", throttledEventHandler);
区别:
- 防抖是在一系列连续触发的事件中只执行最后一次触发后的函数,而节流是以固定的频率执行函数。
- 防抖适用于需要在连续触发事件后等待一段时间再执行函数的场景,而节流适用于需要限制函数触发频率的场景。
- 防抖执行函数的延迟期是固定的,而节流执行函数的频率是固定的。
需要根据具体的需求来选择使用防抖还是节流。
介绍下 Set、Map、WeakSet 和 WeakMap 的区别?
Set、Map、WeakSet 和 WeakMap 是 JavaScript 中的四种集合类型,它们在特性和使用方式上有一些区别。
Set(集合):
- Set 是一种有序的、不重复的值的集合。
- Set 中的值是唯一的,即不允许重复的元素。
- 可以通过
add()
方法向 Set 添加元素,通过delete()
方法删除元素,通过has()
方法检查元素是否存在,通过size
属性获取 Set 的元素数量。 - Set 是可迭代对象,可以使用
for...of
循环遍历 Set 中的元素。 - Set 没有键和值的概念,元素就是它的键和值,因此不能直接通过键来获取值。
Map(映射):
- Map 是一种键值对的集合,其中键和值可以是任意类型的数据。
- Map 中的键是唯一的,每个键对应一个值。
- 可以通过
set()
方法向 Map 添加键值对,通过get()
方法获取键对应的值,通过delete()
方法删除键值对,通过has()
方法检查键是否存在,通过size
属性获取 Map 中键值对的数量。 - Map 是可迭代对象,可以使用
for...of
循环遍历 Map 中的键值对。 - Map 可以使用任意类型的值作为键,而 Set 只能使用对象作为值。
WeakSet(弱引用集合):
- WeakSet 是一种存储对象引用的集合。
- WeakSet 中的对象是弱引用,即如果没有其他引用指向该对象,垃圾回收机制会自动回收该对象。
- WeakSet 不允许重复的对象引用。
- WeakSet 没有
size
属性,也没有办法遍历其中的元素。 - WeakSet 的主要用途是存储临时对象,例如临时的事件处理程序或缓存。
WeakMap(弱引用映射):
- WeakMap 是一种映射关系的集合,其中键是对象,值可以是任意类型的数据。
- WeakMap 中的键是弱引用,当对象没有其他引用时,垃圾回收机制会自动回收键值对。
- WeakMap 不允许重复的键。
- WeakMap 没有
size
属性,也没有办法遍历其中的键值对。 - WeakMap 的主要用途是存储附加数据,其中键是对象,值是与该对象相关联的数据。
需要根据具体的需求来选择使用 Set、Map、WeakSet 还是 WeakMap,它们各自适用于不同的场景和数据结构的需求。
介绍下深度优先遍历和广度优先遍历,如何实现?
深度优先遍历(Depth-First Search,DFS)和广度优先遍历(Breadth-First Search,BFS)是图和树等数据结构中常用的两种搜索和遍历算法。
深度优先遍历(DFS):
深度优先遍历是一种先探索(深入)到尽可能远的节点,再回溯到之前的节点继续探索的算法。具体过程如下:
- 从起始节点开始,访问该节点。
- 若存在未访问过的相邻节点,选择其中一个未访问过的相邻节点,将其标记为已访问,并递归地对该节点进行深度优先遍历。
- 重复步骤 2,直到当前节点的所有相邻节点都被访问过。
- 如果当前节点没有未访问过的相邻节点,回溯到上一个节点,继续选择未访问过的相邻节点进行探索。
- 重复步骤 4,直到遍历完所有节点或找到目标节点。
广度优先遍历(BFS):
广度优先遍历是一种先访问当前节点的所有相邻节点,再逐层向外扩展的算法。具体过程如下:
- 从起始节点开始,将其放入队列。
- 重复以下步骤直到队列为空:
- 从队列中取出一个节点。
- 访问该节点。
- 将该节点的所有未访问过的相邻节点放入队列。
- 当队列为空时,表示所有节点都已经访问过。
实现方式:
深度优先遍历和广度优先遍历可以通过递归或迭代的方式来实现。
深度优先遍历的递归实现:
def dfs_recursive(graph, start, visited):
visited.add(start)
print(start)
for neighbor in graph[start]:
if neighbor not in visited:
dfs_recursive(graph, neighbor, visited)
# 使用示例
graph = {
'A': ['B', 'C'],
'B': ['D', 'E'],
'C': ['F'],
'D': [],
'E': ['F'],
'F': []
}
visited = set()
dfs_recursive(graph, 'A', visited)
广度优先遍历的迭代实现:
from collections import deque
def bfs_iterative(graph, start):
visited = set()
queue = deque([start])
while queue:
node = queue.popleft()
if node not in visited:
print(node)
visited.add(node)
queue.extend(graph[node])
# 使用示例
graph = {
'A': ['B', 'C'],
'B': ['D', 'E'],
'C': ['F'],
'D': [],
'E': ['F'],
'F': []
}
bfs_iterative(graph, 'A')
以上代码示例是使用 Python 来实现深度优先遍历和广度优先遍历的简单示例。在实际应用中,可以根据具体的数据结构和问题进行相应的调整和扩展。
ES5/ES6 的继承除了写法以外还有什么区别?
ES5 和 ES6 提供了不同的继承方式,除了语法上的差异,它们在实现继承的方式和功能上也存在一些区别。
ES5 继承(原型链继承):
- 在 ES5 中,通过使用原型链实现继承。通过将父类的实例赋值给子类的原型对象,使子类能够继承父类的属性和方法。
- 使用 ES5 继承时,子类的实例会共享父类原型上的方法和属性。这意味着如果修改了一个实例上的属性或方法,会影响到其他实例。
- 在 ES5 中,无法直接传递参数给父类的构造函数,因此在子类的构造函数中需要调用父类的构造函数来实现属性的初始化。
ES6 继承(类继承):
- 在 ES6 中引入了
class
和extends
关键字,使得继承更加直观和易于理解。使用class
定义类,使用extends
关键字指定父类,实现继承。 - ES6 继承使用了更先进的内部机制,称为"类继承"。它是基于原型的,但语法和行为更接近传统的面向对象编程语言。
- 使用 ES6 继承时,子类通过
super
关键字来调用父类的构造函数,从而实现属性的初始化。可以向父类的构造函数传递参数。 - ES6 继承引入了
constructor
方法,用于定义和初始化对象的属性。 - 在 ES6 中,子类可以重写父类的方法,并使用
super
关键字来调用父类的方法。
总结:
- ES6 继承提供了更直观、易读的语法,更接近传统的面向对象编程语言。
- ES6 继承通过
extends
和super
关键字提供了更灵活的构造函数、方法的调用和重写方式。 - ES6 继承避免了 ES5 继承中实例共享属性的问题,每个子类实例都有自己的属性副本。
需要注意的是,尽管 ES6 引入了类和继承的语法糖,但在底层仍然是基于原型的继承机制。ES6 的类只是原型继承的一种语法封装,实质上并没有引入真正的类概念。
setTimeout、Promise、Async/Await 的区别
setTimeout
、Promise
和Async/Await
是 JavaScript 中处理异步操作的三种不同机制。它们在语法和使用方式上有所不同。
setTimeout:
setTimeout
是一个函数,用于在一定的延迟时间后执行回调函数。- 它是最早引入的异步操作机制之一,在浏览器环境和 Node.js 中都可用。
setTimeout
通过设置一个定时器,在指定的延迟时间后将回调函数放入任务队列,等待事件循环机制执行。setTimeout
的回调函数是使用普通的函数声明或函数表达式定义的,执行时不会阻塞其他代码的执行。
Promise:
Promise
是 ES6 引入的一种处理异步操作的机制。Promise
是一个代表异步操作最终完成或失败的对象。- 它通过
new Promise()
构造函数创建一个Promise
实例,并提供两个回调函数:resolve
和reject
。 resolve
用于将Promise
标记为成功,并传递结果值;reject
用于将Promise
标记为失败,并传递错误信息。Promise
可以通过then()
方法来处理操作成功的情况,并通过catch()
方法来处理操作失败的情况。Promise
提供了链式调用的能力,可以通过then()
方法返回新的Promise
实例,从而实现串行和并行的异步操作。
Async/Await:
Async/Await
是 ES8(ES2017)引入的一种处理异步操作的机制。Async/Await
是基于Promise
的语法糖,使异步代码的编写和阅读更加简洁和直观。Async/Await
通过在函数前面加上async
关键字来定义一个异步函数。异步函数内部可以使用await
关键字等待一个Promise
的解决或拒绝。await
关键字可以暂停异步函数的执行,直到Promise
状态变为解决(resolved)并返回结果,或者变为拒绝(rejected)并抛出错误。Async/Await
可以使用try/catch
语句来捕获和处理异步操作的错误。Async/Await
使得异步代码的编写和理解更加接近同步代码的风格。
总结:
setTimeout
用于在指定时间后执行回调函数,是最早的异步操作机制。Promise
是 ES6 引入的处理异步操作的机制,通过resolve
和reject
处理成功和失败状态。Async/Await
是基于Promise
的语法糖,使异步代码的编写和阅读更加简洁和直观。它通过async
和await
关键字来处理异步操作。Promise
和Async/Await
都建立在setTimeout
等底层机制之上,并提供更强大、更灵活的异步编程能力。
Async/Await 如何通过同步的方式实现异步
Async/Await
是一种在语法上以同步方式编写异步代码的机制。它通过使用async
和await
关键字,让异步操作看起来像同步操作,提供更直观和易读的代码风格。
下面是使用Async/Await
实现异步操作的示例:
async function fetchData() {
try {
const response = await fetch("https://api.example.com/data"); // 异步操作
const data = await response.json(); // 异步操作
console.log(data); // 同步操作,只有在前面的异步操作完成后才会执行
} catch (error) {
console.log(error);
}
}
fetchData();
在上述示例中,fetchData
函数使用了async
关键字来定义异步函数。在函数内部,使用await
关键字暂停函数的执行,等待异步操作的完成。
- 第一个
await
关键字暂停函数的执行,等待fetch
函数返回一个Promise
对象,并将其解析为response
对象。 - 第二个
await
关键字暂停函数的执行,等待response.json()
方法返回一个Promise
对象,并将其解析为data
对象。 - 在每个
await
关键字之后,代码看起来像同步代码一样,等待前面的异步操作完成后再继续执行。
需要注意的是,使用await
关键字的函数必须在其定义前加上async
关键字。await
只能在async
函数内部使用。
通过使用Async/Await
,我们可以以更直观和易读的方式编写异步代码,避免了回调函数或 Promise 链的嵌套。它使得异步代码的流程控制更加清晰和可读,类似于同步代码的写法。
异步笔试题
请写出下面代码的运行结果
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2");
}
console.log("script start");
setTimeout(function () {
console.log("setTimeout");
}, 0);
async1();
new Promise(function (resolve) {
console.log("promise1");
resolve();
}).then(function () {
console.log("promise2");
});
console.log("script end");
以下是代码的运行结果:
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
这道题主要考察的是事件循环中函数执行顺序的问题,其中包括 async ,await,setTimeout,Promise 函数。下面来说一下本题中涉及到的知识点。
任务队列
首先我们需要明白以下几件事情:
- JS 分为同步任务和异步任务
- 同步任务都在主线程上执行,形成一个执行栈
- 主线程之外,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放置一个事件。
- 一旦执行栈中的所有同步任务执行完毕(此时 JS 引擎空闲),系统就会读取任务队列,将可运行的异步任务添加到可执行栈中,开始执行。
根据规范,事件循环是通过任务队列的机制来进行协调的。一个 Event Loop 中,可以有一个或者多个任务队列(task queue),一个任务队列便是一系列有序任务(task)的集合;每个任务都有一个任务源(task source),源自同一个任务源的 task 必须放到同一个任务队列,从不同源来的则被添加到不同队列。 setTimeout/Promise 等 API 便是任务源,而进入任务队列的是他们指定的具体执行任务。
宏任务
(macro)task(又称之为宏任务),可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)。
浏览器为了能够使得 JS 内部(macro)task 与 DOM 任务能够有序的执行,会在一个(macro)task 执行结束后,在下一个(macro)task 执行开始前,对页面进行重新渲染,流程如下:
(macro)task->渲染->(macro)task->...
(macro)task 主要包含:script(整体代码)、setTimeout、setInterval、I/O、UI 交互事件、postMessage、MessageChannel、setImmediate(Node.js 环境)
微任务
microtask(又称为微任务),可以理解是在当前 task 执行结束后立即执行的任务。也就是说,在当前 task 任务后,下一个 task 之前,在渲染之前。
所以它的响应速度相比 setTimeout(setTimeout 是 task)会更快,因为无需等渲染。也就是说,在某一个 macrotask 执行完后,就会将在它执行期间产生的所有 microtask 都执行完毕(在渲染前)。
microtask 主要包含:Promise.then、MutaionObserver、process.nextTick(Node.js 环境)
运行机制
在事件循环中,每进行一次循环操作称为 tick,每一次 tick 的任务处理模型是比较复杂的,但关键步骤如下:
- 执行一个宏任务(栈中没有就从事件队列中获取)
- 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
- 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
- 当前宏任务执行完毕,开始检查渲染,然后 GUI 线程接管渲染
- 渲染完毕后,JS 线程继续接管,开始下一个宏任务(从事件队列中获取)
流程图如下:
Promise 和 async 中的立即执行
我们知道 Promise 中的异步体现在then
和catch
中,所以写在 Promise 中的代码是被当做同步任务立即执行的。而在 async/await 中,在出现 await 出现之前,其中的代码也是立即执行的。那么出现了 await 时候发生了什么呢?
await 做了什么
从字面意思上看 await 就是等待,await 等待的是一个表达式,这个表达式的返回值可以是一个 promise 对象也可以是其他值。
很多人以为 await 会一直等待之后的表达式执行完之后才会继续执行后面的代码,实际上 await 是一个让出线程的标志。await 后面的表达式会先执行一遍,将 await 后面的代码加入到 microtask 中,然后就会跳出整个 async 函数来执行后面的代码。
这里感谢@chenjigeng的纠正:
由于因为 async await 本身就是 promise+generator 的语法糖。所以 await 后面的代码是 microtask。所以对于本题中的
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
等价于
async function async1() {
console.log("async1 start");
Promise.resolve(async2()).then(() => {
console.log("async1 end");
});
}
回到本题
以上就本道题涉及到的所有相关知识点了,下面我们再回到这道题来一步一步看看怎么回事儿。
首先,事件循环从宏任务(macrotask)队列开始,这个时候,宏任务队列中,只有一个 script(整体代码)任务;当遇到任务源(task source)时,则会先分发任务到对应的任务队列中去。所以,上面例子的第一步执行如下图所示:
然后我们看到首先定义了两个 async 函数,接着往下看,然后遇到了
console
语句,直接输出script start
。输出之后,script 任务继续往下执行,遇到setTimeout
,其作为一个宏任务源,则会先将其任务分发到对应的队列中:script 任务继续往下执行,执行了 async1()函数,前面讲过 async 函数中在 await 之前的代码是立即执行的,所以会立即输出
async1 start
。遇到了 await 时,会将 await 后面的表达式执行一遍,所以就紧接着输出
async2
,然后将 await 后面的代码也就是console.log('async1 end')
加入到 microtask 中的 Promise 队列中,接着跳出 async1 函数来执行后面的代码。script 任务继续往下执行,遇到 Promise 实例。由于 Promise 中的函数是立即执行的,而后续的
.then
则会被分发到 microtask 的Promise
队列中去。所以会先输出promise1
,然后执行resolve
,将promise2
分配到对应队列。script 任务继续往下执行,最后只有一句输出了
script end
,至此,全局任务就执行完毕了。根据上述,每次执行完一个宏任务之后,会去检查是否存在 Microtasks;如果有,则执行 Microtasks 直至清空 Microtask Queue。
因而在 script 任务执行完毕之后,开始查找清空微任务队列。此时,微任务中,
Promise
队列有的两个任务async1 end
和promise2
,因此按先后顺序输出async1 end,promise2
。当所有的 Microtasks 执行完毕之后,表示第一轮的循环就结束了。第二轮循环开始,这个时候就会跳回 async1 函数中执行后面的代码,然后遇到了同步任务console
语句,直接输出async1 end
。这样第二轮的循环就结束了。(也可以理解为被加入到 script 任务队列中,所以会先与 setTimeout 队列执行)第二轮循环依旧从宏任务队列开始。此时宏任务中只有一个
setTimeout
,取出直接输出即可,至此整个流程结束。
下面我会改变一下代码来加深印象。
变式一
在第一个变式中我将 async2 中的函数也变成了 Promise 函数,代码如下:
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
//async2做出如下更改:
new Promise(function (resolve) {
console.log("promise1");
resolve();
}).then(function () {
console.log("promise2");
});
}
console.log("script start");
setTimeout(function () {
console.log("setTimeout");
}, 0);
async1();
new Promise(function (resolve) {
console.log("promise3");
resolve();
}).then(function () {
console.log("promise4");
});
console.log("script end");
可以先自己看看输出顺序会是什么,下面来公布结果:
script start
async1 start
promise1
promise3
script end
promise2
async1 end
promise4
setTimeout
在第一次 macrotask 执行完之后,也就是输出script end
之后,会去清理所有 microtask。所以会相继输出promise2
, async1 end
,promise4
,其余不再多说。
变式二
在第二个变式中,我将 async1 中 await 后面的代码和 async2 的代码都改为异步的,代码如下:
async function async1() {
console.log("async1 start");
await async2();
//更改如下:
setTimeout(function () {
console.log("setTimeout1");
}, 0);
}
async function async2() {
//更改如下:
setTimeout(function () {
console.log("setTimeout2");
}, 0);
}
console.log("script start");
setTimeout(function () {
console.log("setTimeout3");
}, 0);
async1();
new Promise(function (resolve) {
console.log("promise1");
resolve();
}).then(function () {
console.log("promise2");
});
console.log("script end");
可以先自己看看输出顺序会是什么,下面来公布结果:
script start
async1 start
promise1
script end
promise2
setTimeout3
setTimeout2
setTimeout1
在输出为promise2
之后,接下来会按照加入 setTimeout 队列的顺序来依次输出,通过代码我们可以看到加入顺序为3 2 1
,所以会按 3,2,1 的顺序来输出。
变式三
变式三是我在一篇面经中看到的原题,整体来说大同小异,代码如下:
async function a1() {
console.log("a1 start");
await a2();
console.log("a1 end");
}
async function a2() {
console.log("a2");
}
console.log("script start");
setTimeout(() => {
console.log("setTimeout");
}, 0);
Promise.resolve().then(() => {
console.log("promise1");
});
a1();
let promise2 = new Promise((resolve) => {
resolve("promise2.then");
console.log("promise2");
});
promise2.then((res) => {
console.log(res);
Promise.resolve().then(() => {
console.log("promise3");
});
});
console.log("script end");
无非是在微任务那块儿做点文章,前面的内容如果你都看懂了的话这道题一定没问题的,结果如下:
script start
a1 start
a2
promise2
script end
promise1
a1 end
promise2.then
promise3
setTimeout
JS 异步解决方案的发展历程以及优缺点。
JavaScript 异步解决方案的发展历程如下:
回调函数:最初的 JavaScript 异步编程解决方案是使用回调函数。通过将回调函数传递给异步操作,在操作完成时调用回调函数来处理结果。这种方式简单直接,但容易产生回调地狱问题,代码可读性差,难以维护。
Promise:ES6 引入了 Promise 作为一种更优雅的异步解决方案。Promise 是一个代表异步操作最终完成或失败的对象。它可以通过链式调用的方式来处理异步操作的结果,避免了回调地狱问题。Promise 提供了
then
和catch
方法来处理成功和失败的情况,还有finally
方法用于添加无论成功还是失败都会执行的逻辑。Promise 的优点是可读性好、可链式调用、容易捕获错误,但仍然需要手动管理异步操作的状态。Generator:ES6 引入的 Generator 函数提供了一种更灵活的控制流程的方式。Generator 函数可以通过
yield
关键字将函数的执行暂停,并且可以通过next
方法恢复执行。Generator 函数可以用于编写可暂停和可恢复的异步操作,但使用 Generator 函数仍然需要手动编写控制流程的逻辑。Async/Await:ES2017 引入了 Async/Await 语法,是目前最常用的异步解决方案。通过将
async
关键字放在函数前面,可以定义一个返回 Promise 的异步函数。使用await
关键字可以等待一个 Promise 对象的解析,并暂停函数的执行。Async/Await 通过类似同步代码的写法,解决了异步代码的可读性和维护性问题。它消除了回调地狱,更容易理解和调试。然而,Async/Await 并不能解决所有异步问题,不能并发执行多个异步操作,需要通过 Promise 或其他方式来实现。
优缺点总结如下:
优点:
- 可读性好:异步代码的流程控制更接近同步代码,易于理解和维护。
- 避免回调地狱:通过链式调用或类似同步代码的写法,减少了回调函数的嵌套层级。
- 错误处理方便:可以使用
catch
或try/catch
来捕获和处理异步操作中的错误。 - 更好的代码组织:异步操作的代码可以更清晰地组织在一个函数内部,提高可读性和可维护性。
缺点:
- 学习曲线:某些异步解决方案,如 Generator 和 Async/Await,可能需要一些时间来理解和掌握。
- 语法限制:某些解决方案可能需要特定的语法支持,而不是所有环境都支持这些语法。
- 不适用于所有情况:某些特定的异步问题可能需要其他解决方案,如事件发布/订阅模式或流式处理等。
- 可能导致性能问题:某些情况下,使用异步解决方案可能会导致性能问题,如使用过多的异步操作导致并发问题。
总体而言,Promise 和 Async/Await 是目前主流的异步解决方案,能够提供更好的可读性和代码组织,但需要根据具体情况选择合适的方案。
Promise 构造函数是同步执行还是异步执行,那么 then 方法呢?
Promise 构造函数是同步执行的,而then
方法是异步执行的。
当创建一个 Promise 对象时,Promise 构造函数会立即执行,并且传入的执行器函数会被同步调用。执行器函数中的异步操作并不会阻塞 Promise 构造函数的执行,它们会被放入事件队列中等待执行。
然而,then
方法是在 Promise 对象状态变为已解决(fulfilled)或已拒绝(rejected)后,被放入微任务队列中异步执行的。微任务队列中的任务会在主线程空闲时执行,并且在同步任务执行完毕后立即执行。这就意味着then
方法中的回调函数会在当前同步任务执行完毕后被调用,而不会立即执行。
下面是一个示例来说明 Promise 构造函数和then
方法的执行顺序:
console.log("start");
const promise = new Promise((resolve, reject) => {
console.log("promise executor");
resolve();
});
promise.then(() => {
console.log("promise resolved");
});
console.log("end");
上述代码的输出结果将是:
start
promise executor
end
promise resolved
从输出结果可以看出,Promise 构造函数中的执行器函数会在同步任务中被执行,而then
方法中的回调函数会在主线程空闲时异步执行。
需要注意的是,Promise 的异步执行特性使得我们可以将异步操作的结果通过then
方法链式操作,而不会阻塞主线程。这种特性可以帮助我们更好地处理和组织异步代码。
有以下 3 个判断数组的方法,请分别介绍它们之间的区别和优劣
Object.prototype.toString.call() 、 instanceof 以及 Array.isArray()
这三种方法用于判断一个值是否为数组,它们之间有一些区别和优劣势,下面逐个介绍:
Object.prototype.toString.call()
:- 这种方法通过调用
Object.prototype.toString
方法,返回一个表示对象类型的字符串,例如"[object Array]"
。 - 通过使用
call()
方法将要判断的值作为this
参数传递给toString
方法,可以判断该值的类型是否为数组。 - 优势:这种方法可以准确地判断一个值是否为数组,适用于所有类型的值。
- 劣势:使用起来相对繁琐,需要记住具体的方法调用方式,代码可读性较差。
- 这种方法通过调用
instanceof
:- 这种方法使用
instanceof
运算符,检查一个对象是否属于某个类的实例。对于数组来说,可以使用value instanceof Array
来判断。 - 优势:简单直观,语法简洁,易于理解。
- 劣势:只能准确判断一个对象是否是由特定类创建的实例,无法判断继承自其他类的数组。
- 这种方法使用
Array.isArray()
:- 这是一个静态方法,直接调用
Array.isArray(value)
,返回一个布尔值,表示给定的值是否为数组。 - 优势:简单方便,语法清晰,易于理解。这是 ECMAScript 5(ES5)引入的标准方法,广泛支持。
- 劣势:在 ES5 之前的环境中可能不被支持,需要使用其他方法进行兼容处理。
- 这是一个静态方法,直接调用
总结:
Object.prototype.toString.call()
是一种通用且准确的方法,适用于所有类型的值,但使用起来较繁琐。instanceof
是一种简洁直观的方法,可以判断一个对象是否是由特定类创建的实例,但无法判断继承自其他类的数组。Array.isArray()
是一种简单方便的方法,语法清晰易懂,但在 ES5 之前的环境中可能不被支持。
一般来说,推荐使用Array.isArray()
方法,因为它既简单又直观,并且在大多数现代 JavaScript 环境中都得到支持。如果需要考虑到兼容性或判断其他类型的值,可以选择使用Object.prototype.toString.call()
方法。而instanceof
则更适合用于判断对象是否是某个特定类的实例。
介绍下观察者模式和订阅-发布模式的区别,各自适用于什么场景
观察者模式和订阅-发布模式(也称为发布-订阅模式)都是用于实现组件、模块或对象间的解耦和通信的设计模式。它们有一些区别,适用于不同的场景。
观察者模式:
- 定义:观察者模式中,存在一个被观察者(主题)和多个观察者。当被观察者的状态发生改变时,它会自动通知所有的观察者,使它们能够做出相应的响应。
- 结构:被观察者(主题)维护一个观察者列表,并提供注册、注销和通知的方法。观察者通过注册自身到被观察者上来接收通知。
- 通信方式:被观察者直接调用观察者的方法,将状态信息传递给观察者。
- 优点:实现了松耦合,被观察者和观察者之间的关系可以动态建立和解除。观察者可以选择性地订阅感兴趣的事件。
- 适用场景:适用于一对多的通信模式,当一个对象的状态变化需要通知其他多个对象时,可以使用观察者模式。常见的应用场景包括事件监听器、UI 组件通信等。
订阅-发布模式:
- 定义:订阅-发布模式中,存在一个消息中心(或事件总线),订阅者可以订阅感兴趣的消息,而发布者可以发布消息到消息中心,消息中心负责将消息分发给所有订阅者。
- 结构:消息中心(或事件总线)维护一个订阅者列表和消息队列,订阅者通过订阅感兴趣的消息来接收通知,发布者通过发布消息到消息中心来触发通知。
- 通信方式:发布者通过将消息发布到消息中心,而不需要直接引用或调用订阅者的方法。
- 优点:解耦了发布者和订阅者之间的关系,发布者只关心发布消息,而不需要知道具体的订阅者。订阅者可以选择性地订阅感兴趣的消息。
- 适用场景:适用于一对多或多对多的通信模式,当多个对象之间需要进行消息通信,但彼此之间无需直接了解对方时,可以使用订阅-发布模式。常见的应用场景包括事件系统、消息队列、异步编程等。
总结:
- 观察者模式适用于一对多的通信模式,适合于对象间的状态变化通知。
- 订阅-发布模式适用于一对多或多对多的通信模式,适合于解耦发布者和订阅者之间的关系。
需要根据具体的应用场景和需求来选择合适的模式。有时两种模式可以结合使用,例如使用观察者模式实现订阅-发布模式的消息中心。
说说浏览器和 Node 事件循环的区别
浏览器和 Node.js 在事件循环方面有一些区别。以下是它们之间的主要区别:
实现机制:
- 浏览器中的事件循环是基于浏览器的 Web API 实现的,例如 DOM API 和定时器 API 等。浏览器使用一个事件队列(Event Queue)来管理事件和回调函数的执行。
- Node.js 中的事件循环是基于 libuv 库实现的。Node.js 使用一个事件循环机制,该机制允许 Node.js 在执行非阻塞 I/O 操作时继续处理其他任务。
触发方式:
- 在浏览器中,事件循环的触发通常是由用户交互(例如点击、滚动)或者计时器(setTimeout、setInterval)等触发的。
- 在 Node.js 中,事件循环的触发通常是由 I/O 操作(例如文件读写、网络请求)或者定时器触发的。
宏任务和微任务:
- 浏览器中的事件循环将任务分为宏任务(macrotasks)和微任务(microtasks)。宏任务包括用户交互、定时器等,而微任务包括 Promise、MutationObserver 等。
- Node.js 中的事件循环也有宏任务和微任务的概念,但它们的实现与浏览器中的略有不同。在 Node.js 中,宏任务包括 I/O 操作和定时器,而微任务则包括 Promise 和 process.nextTick。
执行顺序:
- 在浏览器中,事件循环的执行顺序是先执行所有的微任务,然后执行一个宏任务,然后再执行微任务,依此类推。
- 在 Node.js 中,事件循环的执行顺序是先执行所有的微任务,然后执行当前的宏任务,然后再执行微任务,依此类推。
总结: 浏览器和 Node.js 的事件循环有一些区别,包括实现机制、触发方式、宏任务和微任务的定义以及执行顺序等。了解这些区别对于编写可移植性高的代码以及理解事件驱动的工作方式是很重要的。
介绍模块化发展历程
可从 IIFE、AMD、CMD、CommonJS、UMD、webpack(require.ensure)、ES Module、<script type="module">
这几个角度考虑。
模块化主要是用来抽离公共代码,隔离作用域,避免变量冲突等。
IIFE: 使用自执行函数来编写模块化,特点:在一个单独的函数作用域中执行代码,避免变量冲突。
(function(){
return {
data:[]
}
})()
AMD: 使用 requireJS 来编写模块化,特点:依赖必须提前声明好。
define('./index.js',function(code){
// code 就是index.js 返回的内容
})
CMD: 使用 seaJS 来编写模块化,特点:支持动态引入依赖文件。
define(function(require, exports, module) {
var indexCode = require('./index.js');
});
CommonJS: nodejs 中自带的模块化。
var fs = require('fs');
UMD:兼容 AMD,CommonJS 模块化语法。
webpack(require.ensure):webpack 2.x 版本中的代码分割。
ES Modules: ES6 引入的模块化,支持 import 来引入另一个 js 。
import a from 'a';
全局作用域中,用 const 和 let 声明的变量不在 window 上,那到底在哪里?如何去获取?
在全局作用域中,使用const
和let
声明的变量不会被添加到全局对象(如浏览器中的window
对象)上。它们的作用范围仅限于声明它们的块级作用域(例如函数内部或全局作用域)。
要获取在全局作用域中使用const
和let
声明的变量,可以直接在相应的作用域内访问它们。例如,在全局作用域中声明一个变量x
:
let x = 10;
在同一个作用域中,你可以通过直接引用变量名x
来获取它的值:
console.log(x); // 输出: 10
在其他作用域中,如果要访问全局作用域中的变量,可以使用变量名来获取它。例如,在函数内部访问全局作用域中的变量x
:
function example() {
console.log(x); // 输出: 10
}
需要注意的是,在严格模式下,如果在全局作用域中使用let
或const
声明变量,它们将不会自动成为全局对象的属性。这种行为是为了提供更好的变量封装和避免全局命名冲突。
如果你确实需要在全局作用域中将变量绑定到全局对象上,可以使用var
关键字来声明变量,例如:
var globalVariable = 20;
console.log(window.globalVariable); // 输出: 20 (在浏览器环境中)
但是,推荐的最佳实践是尽量避免在全局作用域中声明过多的全局变量,而是使用模块化的方式来组织和封装代码,以减少全局命名冲突和提高代码的可维护性。
cookie 和 token 都存放在 header 中,为什么不会劫持 token?
虽然 Cookie 和 Token 都可以存放在 HTTP 请求的 Header 中,但它们在用途和机制上有一些重要的区别,这些区别导致了 Token 比 Cookie 更具安全性,使得 Token 不容易被劫持。
存储位置:
- Cookie 在浏览器中以文本形式存储,作为域名的一部分被保存在浏览器的 Cookie 存储中。每次请求都会自动包含 Cookie 信息,无需手动处理。
- Token 通常被存储在客户端(例如 LocalStorage 或 SessionStorage)或者在前端应用程序的内存中,而不是被自动包含在每个请求中。需要在适当的时候手动将 Token 附加到请求的 Header 中。
安全性:
- Cookie 具有一些安全风险,因为浏览器在每个请求中自动包含 Cookie 信息,如果 Cookie 被劫持,攻击者可以获取到 Cookie 的值,并冒充用户身份进行访问。
- Token 的安全性更高,因为它们并不会自动包含在每个请求中,需要手动添加到请求 Header 中。Token 通常使用加密算法进行签名,防止篡改和伪造。此外,可以使用 HTTPS 来加密通信,进一步增强 Token 的安全性。
有效期管理:
- Cookie 可以设置过期时间,可以长期存储在浏览器中,并且具有自动过期和自动更新的机制。这使得攻击者有更多机会窃取 Cookie。
- Token 可以设置较短的有效期,并且需要手动管理和更新。在有效期结束后,Token 将不再有效,需要重新获取新的 Token。这种机制减少了 Token 被窃取的时间窗口。
总的来说,虽然 Cookie 和 Token 都可以存放在 Header 中,但 Token 相对于 Cookie 更加安全。Token 需要手动添加到请求中,使用签名进行验证,可以设置较短的有效期,并且可以使用加密通信来进一步增强安全性。这些特性使得 Token 比 Cookie 更难以被劫持和滥用。
setTimeout 参数的用法
setTimeout()
函数是 JavaScript 中的一个定时器函数,用于在指定的延迟时间后执行一次特定的代码。
setTimeout()
函数接受两个参数:
- 第一个参数是一个函数或要执行的代码块,可以是一个函数引用或一个匿名函数。
- 第二个参数是延迟的时间,以毫秒为单位。
除了这两个参数之外,setTimeout()
函数还可以接受可选的参数,用于传递给回调函数。
下面是setTimeout()
函数的语法:
setTimeout(callback, delay, param1, param2, ...);
callback
:要执行的函数或代码块。可以是一个函数引用,如myFunction
,或者是一个匿名函数,如function() { ... }
。delay
:延迟的时间,以毫秒为单位。表示在多少毫秒后执行回调函数。param1, param2, ...
:可选参数,用于传递给回调函数的参数。
当延迟时间到达后,setTimeout()
函数会触发回调函数的执行。如果提供了可选参数,那么这些参数会作为回调函数的参数传递。
下面是使用setTimeout()
函数的示例:
function sayHello(name) {
console.log("Hello, " + name);
}
setTimeout(sayHello, 2000, "John");
上述示例中,sayHello
函数会在延迟 2 秒后执行,并输出"Hello, John"。在setTimeout()
函数的第三个参数中,我们传递了"name"作为回调函数的参数。
需要注意的是,setTimeout()
函数会返回一个计时器 ID,可以通过该 ID 来取消定时器的执行,使用clearTimeout()
函数来实现。
下面的代码打印什么内容,为什么?
(function A() {
console.log(A); // [Function A]
A = 1;
console.log(window.A); // undefined
console.log(A); // [Function A]
})();
先说结论: 第一段代码中 A = 1
试图修改自由变量 A
的值,但没有生效!
(function A() {
console.log(A); // [Function A]
A = 1;
console.log(window.A); // undefined
console.log(A); // [Function A]
})();
这是一个立即执行的函数表达式(Immediately-invoked function expression, IIFE),更特殊的,该函数表达式是一个具名函数表达式(Named function expression, NFE)。
NFE 有两个好玩的特性:
- 作为函数名的标识符(在这里是
A
)只能从函数体内部访问,在函数外部访问不到 (IE9+)。(kangax 有一篇 博客 详细讨论了 NFE 以及 IE6~8 的 JScript bug,推荐阅读!) ES5 Section 13 特别提及了这一点:
The Identifier in a FunctionExpression can be referenced from inside the FunctionExpression's FunctionBody to allow the function to call itself recursively. However, unlike in a FunctionDeclaration, the Identifier in a FunctionExpression cannot be referenced from and does not affect the scope enclosing the FunctionExpression.
- 绑定为函数名的标识符(在这里是
A
)不能再绑定为其它值,即该标识符绑定是不可更改的(immutable),所以在 NFE 函数体内对A
重新赋值是无效的。ES5 Section 13 详细描述了创建 NFE 的机制:
The production FunctionExpression : function Identifier ( FormalParameterListopt ) { FunctionBody } is evaluated as follows:
- Let funcEnv be the result of calling NewDeclarativeEnvironment passing the running execution context’s Lexical Environment as the argument
- Let envRec be funcEnv’s environment record.
- Call the CreateImmutableBinding concrete method of envRec passing the String value of Identifier as the argument.
- Let closure be the result of creating a new Function object as specified in 13.2 with parameters specified by FormalParameterListopt and body specified by FunctionBody. Pass in funcEnv as the Scope. Pass in true as the Strict flag if the FunctionExpression is contained in strict code or if its FunctionBody is strict code.
- Call the InitializeImmutableBinding concrete method of envRec passing the String value of Identifier and closure as the arguments.
- Return closure.
注意步骤 3 和 5,分别调用了 createImmutableBinding 和 InitializeImmutableBinding 内部方法,创建的是不可更改的绑定。
要理解这两个特性,最重要的是搞清楚标识符 A
的绑定记录保存在哪里。让我们问自己几个问题:
- 标识符
A
与 该 NFE 是什么关系? 两层关系:首先,该 NFE 的name
属性是 字符串'A'
;更重要的是,A
是该 NFE 的一个自由变量。在函数体内部,我们引用了A
,但A
既不是该 NFE 的形参,又不是它的局部变量,那它不是自由变量是什么!解析自由变量,要从函数的 [[scope]] 内部属性所保存的词法环境 (Lexical Environment) 中查找变量的绑定记录。 - 标识符
A
保存在全局执行环境(Global Execution Context)的词法环境(Lexical Environment)中吗? 答案是否。如果你仔细看过 ES5 Section 13 这一节,会发现创建 NFE 比创建 匿名函数表达式 (Anonymous Function Expression, AFE) 和 函数声明 (Function Declaration) 的过程要复杂得多:
对于 创建 AFE 和 FD, 步骤是这样:
The production FunctionDeclaration : function Identifier ( FormalParameterListopt ) { FunctionBody } is instantiated as follows during Declaration Binding instantiation (10.5):
Return the result of creating a new Function object as specified in 13.2 with parameters specified by FormalParameterListopt, and body specified by FunctionBody. Pass in the VariableEnvironment of the running execution context as the Scope. Pass in true as the Strict flag if the FunctionDeclaration is contained in strict code or if its FunctionBody is strict code.
The production FunctionExpression : function ( FormalParameterListopt ) { FunctionBody } is evaluated as follows:
Return the result of creating a new Function object as specified in 13.2 with parameters specified by FormalParameterListopt and body specified by FunctionBody. Pass in the LexicalEnvironment of the running execution context as the Scope. Pass in true as the Strict flag if the FunctionExpression is contained in strict code or if its FunctionBody is strict code.
比创建 NFE 简单多了有木有?那为么子创建 NFE 要搞得那么复杂呢?就是为了实现 NFE 的只能从函数内部访问A
,而不能从外部访问这一特性!咋实现的? 创建 NFE 时,创建了一个专门的词法环境用于保存 A
的绑定记录(见上面步骤 1~3)!对于 NFE, 有如下关系:
A.[[scope]] ---> Lexical Environment {'environment record': {A: function ...}, outer: --}----> Lexical Environment of Global Context {'environment record': {...}, outer --}---> null
可见,A
的绑定记录不在全局执行上下文的词法环境中,故不能从外部访问!
简单改造下面的代码,使之分别打印 10 和 20。
var b = 10;
(function b() {
b = 20;
console.log(b);
})();
1)打印 10
var b = 10;
(function b(b) {
window.b = 20;
console.log(b);
})(b);
或者
var b = 10;
(function b(b) {
b.b = 20;
console.log(b);
})(b);
或者
var b = 10;
(function () {
console.log(b);
b = 20;
})();
2)打印 20
var b = 10;
(function b(b) {
b = 20;
console.log(b);
})(b);
或
var b = 10;
(function b() {
var b = 20;
console.log(b);
})();
或
var b = 10;
(function () {
b = 20;
console.log(b);
})();
下面代码中 a 在什么情况下会打印 1?
var a = ?;
if(a == 1 && a == 2 && a == 3){
console.log(1);
}
利用 toString
let a = {
i: 1,
toString() {
return a.i++;
},
};
if (a == 1 && a == 2 && a == 3) {
console.log("1");
}
利用 valueOf
let a = {
i: 1,
valueOf() {
return a.i++;
},
};
if (a == 1 && a == 2 && a == 3) {
console.log("1");
}
数组这个就有点妖了
var a = [1, 2, 3];
a.join = a.shift;
if (a == 1 && a == 2 && a == 3) {
console.log("1");
}
ES6 的 symbol
let a = {
[Symbol.toPrimitive]: (
(i) => () =>
++i
)(0),
};
if (a == 1 && a == 2 && a == 3) {
console.log("1");
}
下面代码输出什么
var a = 10;
(function () {
console.log(a);
a = 5;
console.log(window.a);
var a = 20;
console.log(a);
})();
依次输出:undefined -> 10 -> 20
解析:
在立即执行函数中,var a = 20; 语句定义了一个局部变量 a,由于 js 的变量声明提升机制,局部变量 a 的声明会被提升至立即执行函数的函数体最上方,且由于这样的提升并不包括赋值,因此第一条打印语句会打印 undefined,最后一条语句会打印 20。
由于变量声明提升,a = 5; 这条语句执行时,局部的变量 a 已经声明,因此它产生的效果是对局部的变量 a 赋值,此时 window.a 依旧是最开始赋值的 10,
扩展:
var a = 10;
(function () {
console.log(a);
a = 5;
console.log(window.a);
console.log(a);
})();
打印的是:10 -> 5 -> 5
使用 sort() 对数组 [3, 15, 8, 29, 102, 22] 进行排序,输出结果
[3, 15, 8, 29, 102, 22].sort()//[102, 15, 22, 29, 3, 8]
[3, 15, 8, 29, 102, 22] .sort((a,b) => a-b) // [3,8,15,22,29,102]
[3, 15, 8, 29, 102, 22] sort((a,b) => b-a) //[102, 29, 22, 15, 8, 3]
输出以下代码执行的结果并解释为什么
var obj = {
2: 3,
3: 4,
length: 2,
splice: Array.prototype.splice,
push: Array.prototype.push,
};
obj.push(1);
obj.push(2);
console.log(obj);
输出:
涉及知识点:
- 类数组(ArrayLike):
一组数据,由数组来存,但是如果要对这组数据进行扩展,会影响到数组原型,ArrayLike 的出现则提供了一个中间数据桥梁,ArrayLike 有数组的特性, 但是对 ArrayLike 的扩展并不会影响到原生的数组。
- push 方法:
push 方法有意具有通用性。该方法和 call() 或 apply() 一起使用时,可应用在类似数组的对象上。push 方法根据 length 属性来决定从哪里开始插入给定的值。如果 length 不能被转成一个数值,则插入的元素索引为 0,包括 length 不存在时。当 length 不存在时,将会创建它。 唯一的原生类数组(array-like)对象是 Strings,尽管如此,它们并不适用该方法,因为字符串是不可改变的。
- 对象转数组的方式:
Array.from()、splice()、concat()等
题分析:
这个 obj 中定义了两个 key 值,分别为 splice 和 push 分别对应数组原型中的 splice 和 push 方法,因此这个 obj 可以调用数组中的 push 和 splice 方法,调用对象的 push 方法:push(1),因为此时 obj 中定义 length 为 2,所以从数组中的第二项开始插入,也就是数组的第三项(下表为 2 的那一项),因为数组是从第 0 项开始的,这时已经定义了下标为 2 和 3 这两项,所以它会替换第三项也就是下标为 2 的值,第一次执行 push 完,此时 key 为 2 的属性值为 1,同理:第二次执行 push 方法,key 为 3 的属性值为 2。此时的输出结果就是: Object(4) [empty × 2, 1, 2, splice: ƒ, push: ƒ]----> [ 2: 1, 3: 2, length: 4, push: ƒ push(), splice: ƒ splice() ]
因为只是定义了 2 和 3 两项,没有定义 0 和 1 这两项,所以前面会是 empty。 如果讲这道题改为:
var obj = {
2: 3,
3: 4,
length: 0,
splice: Array.prototype.splice,
push: Array.prototype.push,
};
obj.push(1);
obj.push(2);
console.log(obj);
此时的打印结果就是:
原理:此时 length 长度设置为 0,push 方法从第 0 项开始插入,所以填充了第 0 项的 empty 至于为什么对象添加了 splice 属性后并没有调用就会变成类数组对象这个问题,这是控制台中 DevTools 猜测类数组的一个方式: https://github.com/ChromeDevTools/devtools-frontend/blob/master/front_end/event_listeners/EventListenersUtils.js#L330
回答来源:https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/76#issuecomment-478881665
call 和 apply 的区别是什么,哪个性能更好一些
call
和 apply
都是 JavaScript 中函数对象的方法,用于改变函数执行时的上下文(即 this
的指向)。
区别如下:
参数传递方式:
call
方法接受的参数是按照参数列表传递的,即按顺序一个个传递。apply
方法接受的参数是以数组或类数组对象的形式传递的,即作为一个数组传递。
使用方式:
call
方法的语法:function.call(thisArg, arg1, arg2, ...)
apply
方法的语法:function.apply(thisArg, [argsArray])
性能方面,一般来说,call
的性能要比 apply
稍微好一些。这是因为在 call
的参数传递方式中,参数是按顺序一个个传递的,而在 apply
的参数传递方式中,参数需要以数组或类数组对象的形式传递,这会涉及到参数的封装和解包操作,稍微增加了一些开销。
然而,这种性能差异通常是微不足道的,只有在非常大量的参数传递时才会稍微有所体现。在正常的开发中,优先考虑代码的可读性和可维护性,而不是过度关注微小的性能差异。
需要根据具体的使用场景和需求来选择使用 call
还是 apply
,根据参数是否方便以数组形式传递来决定使用哪个方法。
为什么通常在发送数据埋点请求的时候使用的是 1x1 像素的透明 gif 图片?
在发送数据埋点请求时使用 1x1 像素的透明 GIF 图片有以下几个原因:
尺寸小且加载快速:1x1 像素的透明 GIF 图片非常小,文件大小通常只有几个字节,因此可以快速下载和加载,对用户体验影响极小。
兼容性好:GIF 格式是一种广泛支持的图片格式,几乎所有的浏览器和设备都可以正常加载和显示 GIF 图片。
跨域支持:由于浏览器的同源策略限制,JavaScript 在发送跨域请求时受到限制。但是,通过在页面中插入 img 标签并设置 src 属性为外部资源的 URL,浏览器可以发送 GET 请求并获取资源,从而实现跨域数据传输。
不阻塞页面加载:由于 1x1 像素的透明 GIF 图片非常小且加载速度快,它可以在后台异步加载,不会阻塞页面的渲染和加载过程。
数据收集:透明 GIF 图片的 src 属性可以包含查询参数,用于传递数据。服务器可以根据这些参数来解析和提取所需的数据,并进行相应的数据分析和统计。
需要注意的是,现代的数据埋点技术有多种选择,如使用 XMLHttpRequest、Fetch API、Beacon API 等。选择使用 1x1 像素的透明 GIF 图片作为数据埋点请求的方式主要是为了兼容性、加载速度和不阻塞页面加载等方面的考虑。然而,随着技术的发展,根据具体的需求和环境,选择适合的数据埋点方式是很重要的。
怎么让一个 div 水平垂直居中
要让一个 <div>
水平垂直居中,可以使用 CSS 的 flex 布局或绝对定位来实现。以下是两种常用的方法:
使用 Flex 布局:
<div class="container">
<div class="content">
<!-- 内容 -->
</div>
</div>.container {
display: flex;
justify-content: center; /* 水平居中 */
align-items: center; /* 垂直居中 */
/* 可选:设置容器宽高或使用父容器的宽高来约束 */
width: 100vw; /* 100% 视口宽度 */
height: 100vh; /* 100% 视口高度 */
}使用绝对定位:
<div class="container">
<div class="content">
<!-- 内容 -->
</div>
</div>.container {
position: relative; /* 父容器设置相对定位 */
/* 可选:设置容器宽高或使用父容器的宽高来约束 */
width: 100vw; /* 100% 视口宽度 */
height: 100vh; /* 100% 视口高度 */
}
.content {
position: absolute; /* 内容区域设置绝对定位 */
top: 50%; /* 上边距为父容器高度的一半 */
left: 50%; /* 左边距为父容器宽度的一半 */
transform: translate(-50%, -50%); /* 平移至中心位置 */
}
通过以上方式,可以将 <div>
元素水平垂直居中。使用 Flex 布局时,将父容器设置为 flex 容器,并使用 justify-content: center
和 align-items: center
分别实现水平和垂直居中。使用绝对定位时,将父容器设置为相对定位,然后将内容区域通过绝对定位和 top: 50%
、left: 50%
的方式居中,并使用 transform: translate(-50%, -50%)
进行微调。
注意,以上示例中使用的是视口单位(vw 和 vh)来设置容器的宽高,可以根据实际需求使用其他单位或具体数值来约束容器的尺寸。
输出以下代码的执行结果并解释为什么
var a = { n: 1 };
var b = a;
a.x = a = { n: 2 };
console.log(a.x);
console.log(b.x);
这个需要结合 JS 引擎对堆内存和栈内存的管理来理解,即可一目了然。
var a = {n: 1};
这一行都明白,声明一个变量 a,并为其赋值一个对象。
var b = a;
这一行也好理解, 创建一个变量 b,为其赋值对象 a。在栈内存中,a 与 b 是不同的,是两个变量,但是他们的指针是相同的,指向同一个堆。
a.x = a = {n: 2};
这一行比较复杂。先获取等号左侧的 a.x,但 a.x 并不存在,于是 JS 为(堆内存中的)对象创建一个新成员 x,这个成员的初始值为 undefined,
(这也是为什么直接引用一个未定义的变量会报错,但是直接引用一个对象的不存在的成员时,会返回 undefined.)
创建完成后,目标指针已经指向了这个新成员 x,并会先挂起,单等等号右侧的内容有结果了,便完成赋值。
接着执行赋值语句的右侧,发现 a={n:2}是个简单的赋值操作,于是 a 的新值等于了{n:2}。
这里特别注意,这个 a 已经不是开头的那个 a,而是一个全新的 a,这个新 a 指针已经不是指向原来的值的那个堆内存,而是分配了一个新的堆内存。但是原来旧的堆内存因为还有 b 在占用,所以并未被回收。
然后,将这个新的对象 a 的堆内存指针,赋值给了刚才挂起的新成员 x,此时,对象成员 x 便等于了新的对象 a。
所以,现在b={n:1,x:{n:2}};a={n:2};a===b.x
(true,注意对象的相等,不是值的相等,而是引用的相等,也就是说,相等表示指针是指向同一个堆内存。)
console.log(a.x);
console.log(b);
/最后这就好理解了,a.x 当然没有了,但是因为对象 a 存在,所以 JS 不会报错,a.x 等于 undefined/
总结一下:
关键点一:a.x 即完成了 x 的声明,其值为 undefined。
关键点二:对象成员等待赋值时,锁定的赋值目标是成员,而非对象。
关键点三:对象重新赋值时,并非是修改原堆内存的值,而是重新分配堆内存,栈内存中的指针会做相应修改。(如果原堆内存有多个栈内存指向它,由于引用还存在,原堆内存的数据不会消失。如果堆内存再无其它引用,则会被 JS 的垃圾回收机制回收。对象的成员对象也一样。PS:引用类型应该都如此)
好多资料里为了表示方便,把对象的子集都画在一起,其实他们在内存中的物理位置不一定是连续的,要记住引用类型的特点:栈+堆。
答案来自:https://www.zhihu.com/question/41220520/answer/151955851
冒泡排序如何实现,时间复杂度是多少, 还可以如何改进?
冒泡排序是一种简单的排序算法,它的基本思想是通过相邻元素的比较和交换,将较大的元素逐渐向右侧移动,从而实现排序。以下是冒泡排序的实现步骤:
- 从数组的第一个元素开始,依次比较相邻的两个元素。
- 如果前一个元素大于后一个元素,则交换它们的位置。
- 继续向后比较,重复执行步骤 2,直到将最大的元素移动到了数组的最后位置。
- 重复执行步骤 1~3,但是每次循环时,不再考虑已经排序好的最后一个元素。
以下是冒泡排序的 JavaScript 实现代码:
function bubbleSort(array) {
var len = array.length;
for (let i = len - 1; i > 0; i--) {
for (let j = 0; j < i; j++) {
if (array[j] > array[j + 1]) {
[array[j], array[j + 1]] = [array[j + 1], array[j]];
}
}
}
return array;
}
i 是数组右索引,不断收缩数组范围。j 是从左到右的索引,用来循环数组排序。
以下是对冒泡排序进行改进的代码:
function bubbleSort2(array) {
let i = array.length;
while (i > 0) {
let pos = 0;
for (let j = 0; j < i; j++) {
if (array[j] > array[j + 1]) {
pos = j;
[array[j], array[j + 1]] = [array[j + 1], array[j]];
}
}
i = pos;
}
return array;
}
改进点主要是如果右侧已经排序了,可以标记一下最后排完序的位置,直接缩小 i 的大小,也就是缩小循环数组的范围,来避免重复。
某公司 1 到 12 月份的销售额存在一个对象里面
如下:{1:222, 2:123, 5:888}
,请把数据处理为如下结构:[222, 123, null, null, 888, null, null, null, null, null, null, null]
。
const result = Array.from({ length: 12 }).map(
(_, index) => obj[index + 1] || null
);
箭头函数与普通函数(function)的区别是什么?构造函数(function)可以使用 new 生成实例,那么箭头函数可以吗?为什么?
箭头函数和普通函数(function)有以下几点区别:
语法形式:箭头函数使用箭头(=>)来定义函数,而普通函数使用关键字
function
定义函数。this 绑定:箭头函数没有自己的
this
绑定,它会捕获所在上下文的this
值。而普通函数的this
值是在运行时动态确定的,根据函数的调用方式决定。arguments 对象:箭头函数没有自己的
arguments
对象,它会捕获所在上下文的arguments
对象。普通函数内部可以通过arguments
对象访问传递给函数的参数。构造函数:箭头函数不能用作构造函数,不能通过
new
关键字生成实例。普通函数可以使用new
关键字创建实例,生成一个新的对象,并将该对象作为函数的this
值。
总结来说,箭头函数相比普通函数具有更简洁的语法形式和特殊的 this 绑定规则,但缺少了一些普通函数的特性,如构造函数能够生成实例。
由于箭头函数没有自己的 this 绑定,并且不能通过 new 关键字生成实例,所以箭头函数本身不能用作构造函数来创建对象。如果尝试使用 new 关键字调用箭头函数,会抛出一个 TypeError 错误。
例如,以下是箭头函数和普通函数的比较示例:
const arrowFunc = () => {
console.log(this); // 箭头函数的 this 绑定是在定义时确定的
};
function normalFunc() {
console.log(this); // 普通函数的 this 绑定是在运行时确定的
}
const arrowInstance = new arrowFunc(); // TypeError: arrowFunc is not a constructor
const normalInstance = new normalFunc(); // 正常生成实例对象
因此,箭头函数不能用作构造函数,无法通过 new 关键字生成实例。如果需要创建对象实例,应使用普通函数(function)。
已知如下代码,如何修改才能让图片宽度为 300px ?注意下面代码不可修改。
<img src="1.jpg" style="width:480px!important;”>
总结一下吧:
1.css方法
max-width:300px;覆盖其样式;
transform: scale(0.625);按比例缩放图片;
2.js方法
document.getElementsByTagName("img")[0].setAttribute("style","width:300px!important;")
3. 利用盒模型的padding,box-sizing: content-box; padding-left:90px; padding-right: 90px;
如何设计实现无缝轮播
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>carousel</title>
<style>
body {
box-sizing: border-box;
}
.carousel {
position: relative;
width: 400px;
height: 300px;
/* overflow-x: scroll; */
overflow: hidden;
}
.carousel-board {
/* position: absolute; */
position: relative;
list-style: none;
width: 5000px;
height: 300px;
padding: 0;
left: 0;
/* transition: left 0.5s linear; */
}
.carousel-board-item {
float: left;
width: 400px;
height: 300px;
line-height: 300px;
text-align: center;
font-size: 30px;
}
.carousel-btn {
position: absolute;
z-index: 100;
cursor: pointer;
}
.carousel-prev {
top: 50%;
left: 10px;
}
.carousel-next {
top: 50%;
right: 10px;
}
.carousel-dots {
padding: 0;
/* width: 100px; */
list-style: none;
position: absolute;
bottom: 10px;
left: 50%;
margin-left: -24px;
z-index: 100;
}
.carousel-dots li {
float: left;
width: 8px;
height: 8px;
background-color: #aaa;
margin-right: 4px;
border-radius: 4px;
cursor: pointer;
}
</style>
</head>
<body>
<div class="carousel">
<ul class="carousel-board">
<li class="carousel-board-item" style="background-color:green">4</li>
<li class="carousel-board-item" style="background-color:red">1</li>
<li class="carousel-board-item" style="background-color:yellow">2</li>
<li class="carousel-board-item" style="background-color:blue">3</li>
<li class="carousel-board-item" style="background-color:green">4</li>
<li class="carousel-board-item" style="background-color:red">1</li>
</ul>
<span class="carousel-btn carousel-prev"> < </span>
<span class="carousel-btn carousel-next">> </span>
<ul class="carousel-dots">
<li date-index="1"></li>
<li date-index="2"></li>
<li date-index="3"></li>
<li date-index="4"></li>
</ul>
</div>
<script>
(function () {
let prev = document.getElementsByClassName("carousel-prev")[0];
let next = document.getElementsByClassName("carousel-next")[0];
let board = document.getElementsByClassName("carousel-board")[0];
let panels = Array.from(
document.getElementsByClassName("carousel-board-item")
);
board.style.left = "-400px";
let index = 1; //设置默认的index值
next.addEventListener(
"click",
throttle(function () {
index++;
console.log(index);
animate(-400);
//如果当前在 1fake 的位置
if (index === panels.length - 1) {
setTimeout(() => {
console.log("settimeout");
board.style.transition = "0s";
let width = 4 * 400;
board.style.left = parseInt(board.style.left) + width + "px";
}, 600);
index = 1;
}
}, 500)
);
prev.addEventListener(
"click",
throttle(() => {
index--;
console.log(index);
animate(400);
if (index === 0) {
console.log("settimeout");
setTimeout(() => {
board.style.transition = "0s";
let width = -4 * 400;
board.style.left = parseInt(board.style.left) + width + "px";
}, 600);
index = 4;
}
}, 500)
);
// 节流函数 防止点击过快出现空白
function animate(width = 400) {
board.style.transition = "0.5s";
board.style.left || (board.style.left = 0);
board.style.left = parseInt(board.style.left) + width + "px";
}
// 节流函数 防止点击过快出现空白
function throttle(func, wait) {
var context, args;
var previous = 0;
return function () {
var now = +new Date();
context = this;
args = arguments;
if (now - previous > wait) {
func.apply(context, args);
previous = now;
}
};
}
})();
</script>
</body>
</html>
说明:其实就是在循环项的前后拼接了 2 个复制的项,利用 css 切换时,会触发入 animation 或者 transition,当移动到最后 1 个时,在逻辑里偷偷把样式指向第一个项,但是不触发动画。这样视觉上还是 1 个东西,但是其实 dom 已经变了,这样来实现无缝滚动。 https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/108#issuecomment-525607654
a.b.c.d
和 a['b']['c']['d']
,哪个性能更高?
应该是 a.b.c.d
比 a['b']['c']['d']
性能高点,后者还要考虑 [ ]
中是变量的情况,再者,从两种形式的结构来看,显然编译器解析前者要比后者容易些,自然也就快一点。
下图是两者的 AST 对比:
答案:https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/111#issuecomment-487432867
ES6 代码转成 ES5 代码的实现思路是什么
将 ES6 代码转换为 ES5 代码的一种常见实现思路是使用 Babel 这样的转译工具。Babel 是一个广泛使用的 JavaScript 编译器,可以将较新版本的 JavaScript 代码转换为向后兼容的 ES5 代码,以便在不支持新语法和特性的旧版浏览器或环境中运行。
下面是一个基本的实现思路:
安装 Babel:首先,需要在项目中安装 Babel 相关的包。可以使用 npm 或者 yarn 进行安装。
配置 Babel:创建一个名为
.babelrc
的文件,用于配置 Babel 的转译规则。在配置文件中,你可以指定需要使用的 Babel 插件和预设,以及其他相关的设置。转译代码:运行 Babel 命令或通过构建工具(如 Webpack、Parcel 等)来处理代码转译。Babel 将读取项目中的源代码文件,并根据配置文件中的规则将其转换为 ES5 代码。
输出转译后的代码:Babel 会将转译后的代码输出到指定的目录或文件中。你可以根据需要配置输出的路径和文件名。
需要注意的是,Babel 可以根据配置文件中的规则进行自定义的转译。你可以根据项目需求选择不同的插件和预设,以支持特定的 ES6+ 语法和功能。
以下是一个简单的 .babelrc
配置文件示例:
{
"presets": ["@babel/preset-env"]
}
在这个示例中,我们使用了 @babel/preset-env
预设,它根据目标环境自动确定需要的转译规则,以确保尽可能向后兼容。你可以根据需要添加其他插件或预设,以支持更具体的语法和功能。
通过使用 Babel,你可以将 ES6 代码转换为 ES5 代码,并在更广泛的环境中运行。这样,你就可以使用较新的 JavaScript 语法和特性,而无需担心兼容性问题。
如何把一个字符串的大小写取反(大写变小写小写变大写),例如 ’AbC' 变成 'aBc' 。
要将一个字符串的大小写取反,可以使用字符串的 toUpperCase()
和 toLowerCase()
方法,结合循环遍历字符串的每个字符,并对每个字符进行大小写转换。
以下是一个示例函数,可以将字符串的大小写取反:
function invertCase(str) {
let result = "";
for (let i = 0; i < str.length; i++) {
const char = str.charAt(i);
if (char === char.toUpperCase()) {
result += char.toLowerCase();
} else {
result += char.toUpperCase();
}
}
return result;
}
const input = "AbC";
const inverted = invertCase(input);
console.log(inverted); // 输出 'aBc'
在上述示例中,我们定义了一个名为 invertCase
的函数,它接受一个字符串作为参数,并返回大小写取反后的新字符串。
函数中的主要逻辑如下:
初始化一个空字符串
result
,用于存储取反后的结果。使用循环遍历输入字符串的每个字符。
对于每个字符,通过
charAt()
方法获取其值。使用条件语句判断字符的大小写,如果是大写字母,则将其转换为小写字母,并追加到
result
中;如果是小写字母,则将其转换为大写字母,并追加到result
中。循环结束后,返回最终的
result
字符串。
在示例中,我们将字符串 'AbC'
传入 invertCase
函数,并将返回结果 'aBc'
打印到控制台上。
请注意,该示例函数只对字母字符进行大小写转换,对于非字母字符不做处理。如果需要处理非字母字符,可以在循环中添加相应的逻辑来处理特定的字符情况。
正则方式:
str.replace(/[a-zA-Z]/g, (s) =>
/[a-z]/.test(s) ? s.toUpperCase() : s.toLowerCase()
);
js 中 slice 和 splice 的区别
slice
和 splice
都是 JavaScript 数组提供的方法,用于对数组进行操作,但它们的功能和用法有所不同。
slice(start, end)
方法用于创建一个新数组,其中包含原数组中指定范围的元素。它接受两个参数:start
和end
,分别表示要提取的元素的起始索引和结束索引(不包括结束索引本身)。slice
方法不会修改原始数组,而是返回一个包含被提取元素的新数组。
const fruits = ["apple", "banana", "orange", "grapefruit"];
const sliced = fruits.slice(1, 3);
console.log(sliced); // 输出 ['banana', 'orange']
console.log(fruits); // 输出 ['apple', 'banana', 'orange', 'grapefruit']
splice(start, deleteCount, item1, item2, ...)
方法用于修改原数组,可以删除、替换或插入元素。它接受多个参数:start
表示修改的起始索引,deleteCount
表示要删除的元素个数,后续参数item1
,item2
, ... 表示要插入到数组的新元素。splice
方法会返回一个包含被删除元素的数组。
const fruits = ["apple", "banana", "orange", "grapefruit"];
const spliced = fruits.splice(1, 2, "kiwi", "melon");
console.log(spliced); // 输出 ['banana', 'orange']
console.log(fruits); // 输出 ['apple', 'kiwi', 'melon', 'grapefruit']
在上述示例中,slice
方法从索引 1 开始提取到索引 3(不包括索引 3)的元素,返回一个新的数组。原数组 fruits
保持不变。
而 splice
方法从索引 1 开始删除 2 个元素,并插入 'kiwi'
和 'melon'
,返回被删除的元素组成的数组。原数组 fruits
被修改为 ['apple', 'kiwi', 'melon', 'grapefruit']
。
综上所述,slice
用于提取数组的一部分并返回新数组,不会修改原数组;而 splice
则用于删除、替换或插入数组元素,并返回被删除的元素数组,同时会对原数组进行修改。
js 中 substr 和 substring 的区别
在 JavaScript 中,substr
和 substring
都是用于提取字符串中的子串的方法,但它们的参数和行为有所不同。
substr(start, length)
方法用于从字符串中提取指定长度的子串。它接受两个参数:start
表示开始提取的索引位置,length
表示要提取的字符数。如果省略length
参数,则会提取从start
索引开始到字符串末尾的所有字符。需要注意的是,substr
方法的第二个参数是表示要提取的字符数,而不是结束索引。
const str = "Hello, World!";
const substr = str.substr(7, 5);
console.log(substr); // 输出 "World"
在上述示例中,substr
方法从索引 7 开始提取长度为 5 的子串,即从字符 'W' 开始,提取了五个字符,结果为 "World"。
substring(start, end)
方法用于从字符串中提取指定范围的子串。它接受两个参数:start
表示开始提取的索引位置,end
表示结束提取的索引位置(不包括结束索引本身)。如果end
参数被省略,或者end
的值大于字符串长度,则会提取从start
索引开始到字符串末尾的所有字符。
const str = "Hello, World!";
const substring = str.substring(7, 12);
console.log(substring); // 输出 "World"
在上述示例中,substring
方法从索引 7 开始提取到索引 12(不包括索引 12)的子串,结果为 "World"。
需要注意的是,当 start
参数大于 end
参数时,substring
方法会自动交换这两个参数的值,以确保起始索引在前、结束索引在后。例如,substring(12, 7)
会被解释为 substring(7, 12)
。
总结来说,substr
方法通过起始索引和长度来提取子串,而 substring
方法通过起始索引和结束索引(不包括结束索引本身)来提取子串。在使用时,根据具体需求选择合适的方法。
为什么普通 for 循环的性能远远高于 forEach 的性能,请解释其中的原因。
for 循环没有任何额外的函数调用栈和上下文;
forEach 函数签名实际上是
array.forEach(function(currentValue, index, arr), thisValue)
它不是普通的 for 循环的语法糖,还有诸多参数和上下文需要在执行的时候考虑进来,这里可能拖慢性能;forEach 里操作了 toObject 以及判断是否终止循环条件比 for loop 复杂一点
什么是 Proxy 和 Reflect,为什么需要 Reflect?
引言
EcmaScript 2015 中引入了 Proxy 代理 与 Reflect 反射 两个新的内置模块。
我们可以利用 Proxy 和 Reflect 来实现对于对象的代理劫持操作,类似于 Es 5 中 Object.defineProperty()的效果,不过 Reflect & Proxy 远远比它强大。
大多数开发者都了解这两个 Es6 中的新增内置模块,可是你也许并不清楚为什么 Proxy 一定要配合 Reflect 使用。
这里,文章通过几个通俗易懂的例子来讲述它们之间相辅相成的关系。
前置知识
- Proxy 代理,它内置了一系列”陷阱“用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。
- Reflect 反射,它提供拦截 JavaScript 操作的方法。这些方法与 Proxy 的方法相同。
简单来说,我们可以通过 Proxy 创建对于原始对象的代理对象,从而在代理对象中使用 Reflect 达到对于 JavaScript 原始操作的拦截。
如果你还不了解 & ,那么赶快去 MDN 上去补习他们的知识吧。
毕竟大名鼎鼎的 VueJs/Core 中核心的响应式模块就是基于这两个 Api 来实现的。
单独使用 Proxy
开始的第一个例子,我们先单独使用 Proxy 来烹饪一道简单的开胃小菜:
const obj = {
name: "wang.haoyu",
};
const proxy = new Proxy(obj, {
// get陷阱中target表示原对象 key表示访问的属性名
get(target, key) {
console.log("劫持你的数据访问" + key);
return target[key];
},
});
proxy.name; // 劫持你的数据访问name -> wang.haoyu
看起来很简单对吧,我们通过 Proxy 创建了一个基于 obj 对象的代理,同时在 Proxy 中声明了一个 get 陷阱。
当访问我们访问 proxy.name 时实际触发了对应的 get 陷阱,它会执行 get 陷阱中的逻辑,同时会执行对应陷阱中的逻辑,最终返回对应的 target[key]
也就是所谓的 wang.haoyu
.
Proxy 中的 receiver
上边的 Demo 中一切都看起来顺风顺水没错吧,细心的同学在阅读 Proxy 的 MDN 文档上可能会发现其实 Proxy 中 get 陷阱中还会存在一个额外的参数 receiver 。
那么这里的 receiver 究竟表示什么意思呢?大多数同学会将它理解成为代理对象,但这是不全面的。
接下来同样让我们以一个简单的例子来作为切入点:
const obj = {
name: "wang.haoyu",
};
const proxy = new Proxy(obj, {
// get陷阱中target表示原对象 key表示访问的属性名
get(target, key, receiver) {
console.log(receiver === proxy);
return target[key];
},
});
// log: true
proxy.name;
上述的例子中,我们在 Proxy 实例对象的 get 陷阱上接收了 receiver 这个参数。
同时,我们在陷阱内部打印 console.log(receiver === proxy);
它会打印出 true ,表示这里 receiver 的确是和代理对象相等的。
所以 receiver 的确是可以表示代理对象,但是这仅仅是 receiver 代表的一种情况而已。
接下来我们来看另外一个例子:
const parent = {
get value() {
return "19Qingfeng";
},
};
const proxy = new Proxy(parent, {
// get陷阱中target表示原对象 key表示访问的属性名
get(target, key, receiver) {
console.log(receiver === proxy);
return target[key];
},
});
const obj = {
name: "wang.haoyu",
};
// 设置obj继承与parent的代理对象proxy
Object.setPrototypeOf(obj, proxy);
// log: false
obj.value;
关于原型上出现的 get/set 属性访问器的“屏蔽”效果,我在这篇文章中进行了详细阐述。这里我就不展开讲解了。
我们可以看到,上述的代码同样我在 proxy 对象的 get 陷阱上打印了 console.log(receiver === proxy);
结果却是 false 。
那么你可以稍微思考下这里的 receiver 究竟是什么呢? 其实这也是 proxy 中 get 陷阱第三个 receiver 存在的意义。
它是为了传递正确的调用者指向,你可以看看下方的代码:
...
const proxy = new Proxy(parent, {
// get陷阱中target表示原对象 key表示访问的属性名
get(target, key, receiver) {
- console.log(receiver === proxy) // log:false
+ console.log(receiver === obj) // log:true
return target[key];
},
});
...
其实简单来说,get 陷阱中的 receiver 存在的意义就是为了正确的在陷阱中传递上下文。
涉及到属性访问时,不要忘记 get 陷阱还会触发对应的属性访问器,也就是所谓的 get 访问器方法。
我们可以清楚的看到上述的 receiver 代表的是继承与 Proxy 的对象,也就是 obj。
看到这里,我们明白了 Proxy 中 get 陷阱的 receiver 不仅仅代表的是 Proxy 代理对象本身,同时也许他会代表继承 Proxy 的那个对象。
其实本质上来说它还是为了确保陷阱函数中调用者的正确的上下文访问,比如这里的 receiver 指向的是 obj 。
当然,你不要将 revceiver 和 get 陷阱中的 this 弄混了,陷阱中的 this 关键字表示的是代理的 handler 对象。
比如:
const parent = {
get value() {
return "19Qingfeng";
},
};
const handler = {
get(target, key, receiver) {
console.log(this === handler); // log: true
console.log(receiver === obj); // log: true
return target[key];
},
};
const proxy = new Proxy(parent, handler);
const obj = {
name: "wang.haoyu",
};
// 设置obj继承与parent的代理对象proxy
Object.setPrototypeOf(obj, proxy);
// log: false
obj.value;
Reflect 中的 receiver
在清楚了 Proxy 中 get 陷阱的 receiver 后,趁热打铁我们来聊聊 Reflect 反射 API 中 get 陷阱的 receiver。
我们知道在 Proxy 中(以下我们都以 get 陷阱为例)第三个参数 receiver 代表的是代理对象本身或者继承与代理对象的对象,它表示触发陷阱时正确的上下文。
const parent = {
name: "19Qingfeng",
get value() {
return this.name;
},
};
const handler = {
get(target, key, receiver) {
return Reflect.get(target, key);
// 这里相当于 return target[key]
},
};
const proxy = new Proxy(parent, handler);
const obj = {
name: "wang.haoyu",
};
// 设置obj继承与parent的代理对象proxy
Object.setPrototypeOf(obj, proxy);
// log: false
console.log(obj.value);
我们稍微分析下上边的代码:
- 当我们调用 obj.value 时,由于 obj 本身不存在 value 属性。
- 它继承的 proxy 对象中存在 value 的属性访问操作符,所以会发生屏蔽效果。
- 此时会触发 proxy 上的 get value() 属性访问操作。
- 同时由于访问了 proxy 上的 value 属性访问器,所以此时会触发 get 陷阱。
- 进入陷阱时,target 为源对象也就是 parent ,key 为 value 。
- 陷阱中返回
Reflect.get(target,key)
相当于target[key]
。 - 此时,不知不觉中 this 指向在 get 陷阱中被偷偷修改掉了!!
- 原本调用方的 obj 在陷阱中被修改成为了对应的 target 也就是 parent 。
- 自然而然打印出了对应的
parent[value]
也就是 19Qingfeng 。
这显然不是我们期望的结果,当我访问 obj.value 时,我希望应该正确输出对应的自身上的 name 属性也就是所谓的 obj.value => wang.haoyu 。
那么,Relfect 中 get 陷阱的 receiver 就大显神通了。
const parent = {
name: '19Qingfeng',
get value() {
return this.name;
},
};
const handler = {
get(target, key, receiver) {
- return Reflect.get(target, key);
+ return Reflect.get(target, key, receiver);
},
};
const proxy = new Proxy(parent, handler);
const obj = {
name: 'wang.haoyu',
};
// 设置obj继承与parent的代理对象proxy
Object.setPrototypeOf(obj, proxy);
// log: wang.haoyu
console.log(obj.value);
上述代码原理其实非常简单:
- 首先,之前我们提到过在 Proxy 中 get 陷阱的 receiver 不仅仅会表示代理对象本身同时也还有可能表示继承于代理对象的对象,具体需要区别与调用方。这里显然它是指向继承与代理对象的 obj 。
- 其次,我们在 Reflect 中 get 陷阱中第三个参数传递了 Proxy 中的 receiver 也就是 obj 作为形参,它会修改调用时的 this 指向。
你可以简单的将
Reflect.get(target, key, receiver)
理解成为target[key].call(receiver)
,不过这是一段伪代码,但是这样你可能更好理解。
相信看到这里你已经明白 Relfect 中的 receiver 代表的含义是什么了,没错它正是可以修改属性访问中的 this 指向为传入的 receiver 对象。
总结
相信看到这里大家都已经明白了,为什么 Proxy 一定要配合 Reflect 使用。恰恰是为什么触发代理对象的劫持时保证正确的 this 上下文指向。
我们再来稍稍回忆一下,针对于 get 陷阱(当然 set 其他之类涉及到 receiver 的陷阱同理):
- Proxy 中接受的 Receiver 形参表示代理对象本身或者继承与代理对象的对象。
- Reflect 中传递的 Receiver 实参表示修改执行原始操作时的 this 指向。
结尾
这里就到了文章的结尾了,至于为什么会突然提到 Proxy & Reflect 的话题。
其实是笔者最近在阅读 Vue/corejs 的源代码内容,刚好它内部大量应用于 Proxy & Reflect 所以就产生了这篇文章。
关于 Proxy 为什么一定要配合 Reflect 使用,具体结合 VueJs 中响应式模块的依赖收集其实会更好理解一些。不过这里为了照顾不太熟悉 VueJs 的同学所以就没有展开了。
当然,最近我也在阅读 VueJs 的过程中尝试书写一些阶段性总结文章。之后在文章中也会详细讲解这一过程,有兴趣的同学可以持续关注我的最新动态~
结尾,谢谢每一个小伙伴。我们一起加油~
setPrototypeOf 与 Object.create 区别
Object.setPrototypeOf()
和 Object.create()
是 JavaScript 中用于设置对象原型的两种不同方法,它们之间有以下区别:
功能不同:
Object.setPrototypeOf(obj, prototype)
用于将指定对象obj
的原型设置为prototype
。它会改变对象的原型链,将对象连接到指定的原型上。Object.create(prototype)
创建一个新对象,并将该对象的原型设置为prototype
。它是通过指定原型对象来创建新对象,而不是改变已有对象的原型。
用法不同:
Object.setPrototypeOf(obj, prototype)
是一个函数,接受两个参数:要修改原型的对象obj
和指定的原型对象prototype
。Object.create(prototype)
是一个静态方法,接受一个参数:要作为新对象原型的对象prototype
。
返回值不同:
Object.setPrototypeOf(obj, prototype)
并没有返回任何值,它直接修改了传入的对象的原型。Object.create(prototype)
返回一个新对象,该对象的原型被设置为指定的prototype
。
性能和兼容性差异:
Object.setPrototypeOf(obj, prototype)
的性能相对较差,因为它需要修改现有对象的原型链。此外,它在一些老旧的 JavaScript 引擎中可能不受支持。Object.create(prototype)
的性能较好,因为它直接创建了一个新对象,并将其原型设置为指定的原型对象。此方法在大多数现代 JavaScript 引擎中都得到支持。
总结来说,Object.setPrototypeOf(obj, prototype)
用于修改现有对象的原型,而 Object.create(prototype)
用于创建一个新对象,并将其原型设置为指定的原型对象。在实际使用中,根据具体的需求来选择合适的方法。如果需要在已有对象上修改原型,则使用 Object.setPrototypeOf()
;如果需要创建一个新对象,并设置其原型,则使用 Object.create()
。
数组里面有 10 万个数据,取第一个元素和第 10 万个元素的时间相差多少
在理想情况下,取第一个元素和第 10 万个元素的时间相差不会很大,因为数组的访问时间复杂度是 O(1)。这是因为 JavaScript 中的数组是基于索引的数据结构,可以直接通过索引访问元素,而不需要遍历整个数组。
所以,取第一个元素和第 10 万个元素的时间差应该是非常小的,几乎可以忽略不计。无论数组中有多少个元素,访问特定索引的元素所需的时间基本上是固定的。
然而,需要注意的是,如果数组中存在复杂的嵌套结构或者元素是引用类型,那么取第一个元素和第 10 万个元素的时间相差可能会有所增加,因为访问第 10 万个元素时需要经过更多的嵌套层级或者引用解析。但是这个增加的时间应该是很小的,不会随着数组长度的增加而线性增长。
综上所述,在大多数情况下,取第一个元素和第 10 万个元素的时间相差非常小,可以认为是几乎相同的。
JavaScript 中, JSArray 继承自 JSObject ,或者说它就是一个特殊的对象,内部是以 key-value 形式存储数据,所以 JavaScript 中的数组可以存放不同类型的值。它有两种存储方式,快数组与慢数组,初始化空数组时,使用快数组,快数组使用连续的内存空间,当数组长度达到最大时,JSArray 会进行动态的扩容,以存储更多的元素,相对慢数组,性能要好得多。当数组中 hole 太多时,会转变成慢数组,即以哈希表的方式( key-value 的形式)存储数据,以节省内存空间。
部分参考:https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/124#issuecomment-619546197
输出以下代码运行结果
// example 1
var a={}, b='123', c=123;
a[b]='b';
a[c]='c';
console.log(a[b]);
---------------------
// example 2
var a={}, b=Symbol('123'), c=Symbol('123');
a[b]='b';
a[c]='c';
console.log(a[b]);
---------------------
// example 3
var a={}, b={key:'123'}, c={key:'456'};
a[b]='b';
a[c]='c';
console.log(a[b]);
第三个,如果都是对象,会调用 toString,
var b = { key: "123" };
b.toString();
//'[object Object]'
所以 key 都是[object Object]
input 搜索如何防抖,如何处理中文输入
防抖就不说了,主要是这里提到的中文输入问题,其实看过 elementui 框架源码的童鞋都应该知道,elementui 是通过 compositionstart & compositionend 做的中文输入处理: 相关代码:
<input ref="input" @compositionstart="handleComposition" @compositionupdate="handleComposition" @compositionend="handleComposition">
这 3 个方法是原生的方法,这里简单介绍下,官方定义如下 compositionstart 事件触发于一段文字的输入之前(类似于 keydown 事件,但是该事件仅在若干可见字符的输入之前,而这些可见字符的输入可能需要一连串的键盘操作、语音识别或者点击输入法的备选词) 简单来说就是切换中文输入法时在打拼音时(此时 input 内还没有填入真正的内容),会首先触发 compositionstart,然后每打一个拼音字母,触发 compositionupdate,最后将输入好的中文填入 input 中时触发 compositionend。触发 compositionstart 时,文本框会填入 “虚拟文本”(待确认文本),同时触发 input 事件;在触发 compositionend 时,就是填入实际内容后(已确认文本),所以这里如果不想触发 input 事件的话就得设置一个 bool 变量来控制。
根据上图可以看到
输入到 input 框触发 input 事件 失去焦点后内容有改变触发 change 事件 识别到你开始使用中文输入法触发 compositionstart 事件 未输入结束但还在输入中触发 compositionupdate 事件 输入完成(也就是我们回车或者选择了对应的文字插入到输入框的时刻)触发 compositionend 事件。
那么问题来了 使用这几个事件能做什么? 因为 input 组件常常跟 form 表单一起出现,需要做表单验证
为了解决中文输入法输入内容时还没将中文插入到输入框就验证的问题
我们希望中文输入完成以后才验证
答案:https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/129#issue-446888391
介绍下 Promise.all 使用、原理实现及错误处理
Promise.all 是一个 JavaScript 的 Promise 方法,它接收一个 Promise 实例的数组作为参数,并返回一个新的 Promise 实例。这个新的 Promise 实例在数组中的所有 Promise 实例都成功解析(resolved)时才会被解析,否则只要有一个 Promise 实例被拒绝(rejected),该 Promise 实例就会被拒绝。
使用 Promise.all 的基本语法如下:
Promise.all([promise1, promise2, ...])
.then(([result1, result2, ...]) => {
// 所有 Promise 实例都成功解析时的处理逻辑
})
.catch((error) => {
// 任一 Promise 实例被拒绝时的错误处理逻辑
});
下面是 Promise.all 的原理实现步骤:
- 创建一个新的 Promise 实例,并返回该实例。
- 遍历传入的 Promise 数组,对每个 Promise 实例进行处理。
- 如果所有的 Promise 实例都成功解析,将解析的结果按照 Promise 数组中的顺序保存下来,并使用新的 Promise 实例解析这些结果。
- 如果任一 Promise 实例被拒绝,使用新的 Promise 实例拒绝并传递被拒绝的原因。
错误处理在 Promise.all 中的工作方式如下:
- 如果传入的 Promise 数组中的任一 Promise 实例被拒绝,新的 Promise 实例也会被拒绝。
- 如果你在 Promise.all 的后续链式调用中使用了
.catch()
方法,它将捕获到的第一个被拒绝的 Promise 实例的错误。 - 如果你在 Promise.all 的后续链式调用中使用了
.then()
方法,它将在所有 Promise 实例都成功解析时执行,且它接收一个数组参数,包含所有 Promise 实例解析的结果。
需要注意的是,Promise.all 的错误处理方式是“一旦有 Promise 被拒绝就拒绝整个 Promise.all”,因此如果你需要对每个 Promise 实例的错误进行个别处理,你可以在传入的 Promise 数组中对每个 Promise 实例单独进行错误处理,或者使用 .catch()
方法捕获整个 Promise.all 的错误。
Promise.all 原理实现
function promiseAll(promises) {
return new Promise(function (resolve, reject) {
if (!Array.isArray(promises)) {
return reject(new TypeError("argument must be anarray"));
}
var countNum = 0;
var promiseNum = promises.length;
var resolvedvalue = new Array(promiseNum);
for (var i = 0; i < promiseNum; i++) {
(function (i) {
Promise.resolve(promises[i]).then(
function (value) {
countNum++;
resolvedvalue[i] = value;
if (countNum === promiseNum) {
return resolve(resolvedvalue);
}
},
function (reason) {
return reject(reason);
}
);
})(i);
}
});
}
var p1 = Promise.resolve(1),
p2 = Promise.resolve(2),
p3 = Promise.resolve(3);
promiseAll([p1, p2, p3]).then(function (value) {
console.log(value);
});
var、let 和 const 区别的实现原理是什么
var
、let
和const
是在 JavaScript 中声明变量的关键字,它们之间有几个重要的区别,包括作用域、变量提升和可变性。
作用域:
var
:var
声明的变量具有函数作用域,它在声明的函数内部有效,如果在函数外部声明,则成为全局变量。let
和const
:let
和const
声明的变量具有块级作用域,它们在声明的块(例如,if 语句块、循环块等)内部有效。
变量提升:
var
:var
声明的变量存在变量提升的特性,即在作用域内的任何位置声明的变量都会被提升到作用域的顶部。但是,变量的赋值操作仍然会留在原来的位置。这意味着你可以在变量声明之前访问变量,但它的值将是undefined
。let
和const
:let
和const
声明的变量不会被提升。如果你在声明之前访问变量,会抛出一个ReferenceError
。
可变性:
var
和let
:var
和let
声明的变量可以被重新赋值和修改。const
:const
声明的变量是常量,一旦被赋值,就不能再被修改。它具有块级作用域,并且必须在声明时进行初始化。
这些区别的实现原理可以通过 JavaScript 引擎的工作方式来解释:
var
的作用域和变量提升是基于早期的 JavaScript 实现,它将变量声明提升到作用域的顶部,并在运行时解析变量的赋值和访问。let
和const
是 ES6 引入的新特性,它们的作用域和变量提升是基于块级作用域的概念,在编译阶段进行静态分析,而不是在运行时进行。
总结起来,var
具有函数作用域和变量提升的特性,而let
和const
具有块级作用域、不存在变量提升以及const
声明的变量不可变的特性。这些关键字的不同特性使得我们可以更好地控制变量的作用范围和可变性,提高代码的可读性和可维护性。
在输入框中如何判断输入的是一个正确的网址。
要判断输入的内容是否是一个正确的网址,可以使用正则表达式来进行匹配和验证。正则表达式是一种用于描述字符串模式的工具,可以用来检查输入是否符合特定的模式。
以下是一个简单的正则表达式来验证网址的格式:
function isValidURL(url) {
const urlPattern = /^(https?:\/\/)?([\w.-]+)\.([a-z]{2,})(\/\S*)?$/i;
return urlPattern.test(url);
}
// 示例测试
const inputURL = "https://www.example.com";
const isValid = isValidURL(inputURL);
console.log(isValid); // 输出: true
在这个例子中,我们定义了一个正则表达式 urlPattern
,它包含以下部分:
^
:匹配字符串的开始位置。(https?:\/\/)?
:可选的协议部分,匹配http://
或https://
。([\w.-]+)
:匹配主机名部分,可以包含字母、数字、下划线、点和连字符。\.([a-z]{2,})
:匹配域名部分,要求至少包含两个字母。(\/\S*)?
:可选的路径部分,匹配以斜杠开头的任意非空字符序列。$
:匹配字符串的结束位置。
通过调用 test()
方法并传入待验证的网址,我们可以检查输入的网址是否与正则表达式匹配。如果匹配成功,说明输入的网址格式正确,返回 true
;否则,返回 false
。
请注意,这只是一个简单的验证方法,仅检查网址的基本格式是否正确。如果需要更严格的验证,可能需要使用更复杂的正则表达式或结合其他的验证方法。
额外取巧的一个方法:
function isUrl(url) {
try {
new URL(url);
return true;
} catch (err) {
return false;
}
}
模拟实现一个深拷贝,并考虑对象相互引用以及 Symbol 拷贝的情况
要实现一个深拷贝函数,考虑到对象相互引用和 Symbol 拷贝的情况,可以使用递归和一些辅助变量来处理。以下是一个简单的深拷贝函数的实现示例:
function deepCopy(obj, visited = new WeakMap()) {
// 处理基本数据类型和特殊类型
if (obj === null || typeof obj !== "object") {
return obj;
}
// 处理 Symbol 类型
if (typeof obj === "symbol") {
return Object(Symbol.prototype.valueOf.call(obj));
}
// 处理循环引用
if (visited.has(obj)) {
return visited.get(obj);
}
// 创建新对象
const clone = Array.isArray(obj) ? [] : {};
// 记录访问过的对象
visited.set(obj, clone);
// 递归拷贝属性
for (let key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
clone[key] = deepCopy(obj[key], visited);
}
}
return clone;
}
在上述代码中,我们定义了一个函数 deepCopy()
,它接收一个要拷贝的对象 obj
和一个 visited
的 WeakMap,用于记录已经访问过的对象,以处理循环引用。
在函数内部,我们首先处理基本数据类型和特殊类型(如 null)的情况,直接返回原始值。
然后,我们处理 Symbol 类型,通过调用 Object(Symbol.prototype.valueOf.call(obj))
来创建一个新的 Symbol 对象。
接下来,我们检查是否已经访问过当前对象,如果是,则直接返回已经拷贝过的对象,以避免无限循环。
然后,我们根据对象的类型创建一个新的对象(数组或对象),并将当前对象和对应的克隆对象记录到 visited
中。
最后,我们使用递归来拷贝对象的属性,并将拷贝后的属性赋值给对应的克隆对象。
最后,返回深拷贝后的对象。
这个深拷贝函数能够处理对象相互引用的情况,因为它使用 visited
来记录已经访问过的对象,如果遇到循环引用,就直接返回之前已经拷贝过的对象。
请注意,这只是一个简单的实现示例,对于更复杂的情况,比如处理函数、正则表达式、循环结构等,可能需要更多的处理逻辑。因此,在实际应用中,你可能需要根据具体的需求进行定制和扩展。
介绍下前端加密的常见场景和方法
前端加密在前端开发中有许多常见的场景,用于保护用户数据、提供数据传输的安全性以及确保数据的完整性。下面是一些常见的前端加密场景和方法的介绍:
用户密码加密: 在用户注册或登录过程中,密码是一项敏感信息。为了保护用户密码的安全,可以使用哈希函数和加盐的方式进行加密存储。常见的密码哈希函数有 bcrypt、scrypt 和 PBKDF2。这些函数都会将密码和一个随机生成的盐值混合运算,生成一个不可逆的哈希值。这样即使数据库泄露,攻击者也无法轻易还原密码。
数据传输加密: 当前端与后端进行数据传输时,特别是通过网络进行传输时,加密是至关重要的。常见的做法是使用 HTTPS(HTTP over SSL/TLS)协议来保护数据的机密性和完整性。HTTPS 使用 SSL/TLS 协议对通信进行加密,防止中间人攻击和数据窃取。
加密敏感数据: 在前端应用中,有时需要对敏感数据进行加密,例如客户端存储的用户信息或敏感的业务数据。常见的方法是使用对称加密算法(如 AES)或非对称加密算法(如 RSA)来对数据进行加密和解密。对称加密使用相同的密钥进行加解密,而非对称加密使用一对密钥(公钥和私钥)进行加解密。
数字签名: 数字签名用于验证数据的来源和完整性,以防止数据被篡改。前端可以使用非对称加密算法生成一对密钥,将私钥用于对数据进行签名,而公钥则用于验证签名的有效性。这样,接收方可以使用公钥验证签名,确保数据的完整性和来源的可信性。
客户端加密: 在某些情况下,前端应用可能需要在客户端本地进行加密操作,例如加密本地存储的数据或在客户端上执行一些敏感操作。这可以通过使用 JavaScript 加密库来实现,例如 CryptoJS 或 Web Crypto API。这些库提供了各种加密算法和功能,可以在客户端上进行数据加密和解密操作。
需要注意的是,前端加密只是保护数据在传输和存储过程中的安全性,但无法阻止恶意用户或攻击者通过其他途径获取到前端代码或密钥。因此,综合考虑,加密操作应该在安全的环境下进行,并且结合其他安全措施,如身份验证、访问控制等,以提供更全面的安全保护。
写出如下代码的打印结果
function changeObjProperty(o) {
o.siteUrl = "http://www.baidu.com";
o = new Object();
o.siteUrl = "http://www.google.com";
}
let webSite = new Object();
changeObjProperty(webSite);
console.log(webSite.siteUrl);
代码的打印结果是 "http://www.baidu.com"。
在代码中,我们定义了一个名为 changeObjProperty
的函数,它接收一个对象 o
作为参数。在函数内部,首先给 o
对象添加了一个名为 siteUrl
的属性,并将其值设置为 "http://www.baidu.com"。
接下来,我们重新创建了一个新的空对象,并将其赋值给 o
。然后,我们给这个新对象添加一个名为 siteUrl
的属性,并将其值设置为 "http://www.google.com"。
在主程序中,我们创建了一个名为 webSite
的对象,并将其传递给 changeObjProperty
函数进行处理。
然而,需要注意的是,在 JavaScript 中,对象是通过引用传递的。这意味着当我们将 webSite
对象作为参数传递给 changeObjProperty
函数时,函数内部对于 o
的修改实际上是在修改 webSite
对象本身。
虽然在函数内部我们重新创建了一个新对象并将其赋值给 o
,但这并不会对外部的 webSite
对象产生影响。因此,最终打印的结果是 webSite.siteUrl
,即 "http://www.baidu.com"。
请写出如下代码的打印结果
function Foo() {
Foo.a = function () {
console.log(1);
};
this.a = function () {
console.log(2);
};
}
Foo.prototype.a = function () {
console.log(3);
};
Foo.a = function () {
console.log(4);
};
Foo.a();
let obj = new Foo();
obj.a();
Foo.a();
代码的打印结果如下:
4
2
1
解释如下:
首先,执行
Foo.a()
。这是一个直接调用Foo
函数的静态方法。因此,会打印出 4。接着,创建了一个名为
obj
的新对象,通过new Foo()
来调用Foo
构造函数。在构造函数内部,Foo.a
被赋值为一个新的函数,同时this.a
实例化到了 obj 上,它会打印出 2。所以,obj.a()
调用的是构造函数内部的this.a
,输出 2。最后,再次调用
Foo.a()
。由于在构造函数内部重新赋值了Foo.a
,它会打印出 1。
因此,代码的打印结果是 "4"、"2" 和 "1"。
js 中 new 操作符干了什么
在 JavaScript 中,new
操作符用于创建一个对象实例。它执行以下操作:
创建一个空对象。这个对象会继承自构造函数的原型对象(也就是构造函数的
prototype
属性)。将新创建的对象作为
this
关键字绑定到构造函数上,这样构造函数内部的代码可以引用并操作这个新对象。执行构造函数内部的代码。在构造函数内部,可以对新对象进行属性赋值、方法定义等操作。
如果构造函数没有显式地返回一个对象,则返回这个新创建的对象。如果构造函数有返回值且返回的是一个对象,则返回该对象。如果返回的是其他类型的值(如基本类型),则忽略该返回值,仍然返回新创建的对象。
通过使用 new
操作符,我们可以创建一个与构造函数相关联的对象实例,并且可以在构造函数内部对对象进行初始化和设置。这使得我们可以通过构造函数来创建多个具有相同属性和方法的对象。
js 中 typeof 与 instanceof 区别
typeof
和 instanceof
是 JavaScript 中用于检查数据类型和对象实例的运算符,它们有以下区别:
typeof
运算符用于检查给定值的数据类型。它返回一个表示值类型的字符串。typeof
对于基本数据类型(如字符串、数字、布尔值等)和函数类型会返回相应的字符串("string"、"number"、"boolean"、"function")。- 对于
null
返回 "object",这是因为在 JavaScript 中,null
被认为是一个空对象引用。 - 对于数组返回 "object",这是因为数组在 JavaScript 中被认为是对象的一种特殊形式。
- 对于对象和未定义的变量返回 "object" 和 "undefined"。
- 对于 ES6 中的 Symbol 类型返回 "symbol"。
示例:
typeof "Hello"; // "string"
typeof 42; // "number"
typeof true; // "boolean"
typeof undefined; // "undefined"
typeof null; // "object"
typeof []; // "object"
typeof {}; // "object"
typeof function () {}; // "function"
typeof Symbol("symbol"); // "symbol"instanceof
运算符用于检查一个对象是否是某个构造函数的实例,或者是否属于某个类的实例。它返回一个布尔值。object instanceof constructor
返回true
表示object
是constructor
的实例,或者object
是constructor
的子类的实例。object
是constructor
的实例时,也可以说object
属于constructor
类型。
示例:
const str = "Hello";
console.log(str instanceof String); // false,因为 str 是基本字符串类型,不是 String 类的实例
const arr = [1, 2, 3];
console.log(arr instanceof Array); // true,arr 是 Array 类的实例
function Person() {}
const person = new Person();
console.log(person instanceof Person); // true,person 是 Person 类的实例
class Animal {}
class Dog extends Animal {}
const dog = new Dog();
console.log(dog instanceof Animal); // true,dog 是 Animal 类的实例
console.log(dog instanceof Dog); // true,dog 是 Dog 类的实例
总结:
typeof
用于检查值的数据类型,返回一个字符串。instanceof
用于检查对象是否是某个构造函数的实例,或者是否属于某个类的实例,返回一个布尔值。
typeof 与 instanceof 都是判断数据类型的方法,区别如下:
- typeof 会返回一个变量的基本类型,instanceof 返回的是一个布尔值
- instanceof 可以准确地判断复杂引用数据类型,但是不能正确判断基础数据类型
而 typeof 也存在弊端,它虽然可以判断基础数据类型(null 除外),但是引用数据类型中,除了 function 类型以外,其他的也无法判断 可以看到,上述两种方法都有弊端,并不能满足所有场景的需求 如果需要通用检测数据类型,可以采用 Object.prototype.toString 调用该方法,统一返回格式 [object xxx] 的字符串
Object.defineProperty 真的不能监听数组的变化吗?
其实 Object.defineProperty
是可以监听数组的变化的。
首先这种直接通过下标获取数组元素的场景就比较少,其次即便通过了 Object.defineProperty
对数组进行监听,但也监听不了 push、pop、shift 等对数组进行操作的方法,所以还是需要通过对数组原型上的那 7 个方法进行重写监听。所以为了性能考虑 vue2 直接弃用了使用 Object.defineProperty
对数组进行监听的方案。
如何确保你的构造函数只能被 new 调用,而不能被普通调用?
明确函数的双重用途
JavaScript
中的函数一般有两种使用方式:
- 当作构造函数使用:
new Func()
- 当作普通函数使用:
Func()
但 JavaScript
内部并没有区分两者的方式,我们人为规定构造函数名首字母要大写作为区分。也就是说,构造函数被当成普通函数调用不会有报错提示(心痛记录~~~)。
空口无凭,下面来举个栗子:
// 定义构造函数 Person
function Person(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
this.fullName = this.firstName + this.lastName;
}
// 使用 new 调用
console.log(new Person("战场", "小包"));
// 当作普通函数调用
console.log(Person("战场", "小包"));
输出结果:
通过输出结果可以发现,定义的构造函数被当作普通函数来调用,没有任何错误提示。这很不合理,万一那次在粗心大意,就很容易复现小包的情况。
使用 instanceof 实现
instanceof 基础知识
instanceof
运算符用于检测构造函数的 prototype
属性是否出现在某个实例对象的原型链上。
使用语法:
object instanceof constructor;
我们可以使用 instanceof
检测某个对象是不是另一个对象的实例,例如 new Person() instanceof Person --> true
new 绑定/ 默认绑定
- 通过
new
来调用构造函数,会生成一个新对象,并且把这个新对象绑定为调用函数的this
。 - 如果普通调用函数,非严格模式
this
指向window
,严格模式指向undefined
function Test() {
console.log(this);
}
// Window {...}
console.log(Test());
// Test {}
console.log(new Test());
使用 new
调用函数和普通调用函数最大的区别在于函数内部 this
指向不同: new
调用后 this
指向实例,普通调用则会指向 window
。
instanceof
可以检测某个对象是不是另一个对象的实例。如果为 new
调用, this
指向实例,this instanceof 构造函数 返回值为 true
,普通调用返回值为 false
。
代码实现
function Person(firstName, lastName) {
// this instanceof Person
// 如果返回值为 false,说明为普通调用
// 返回类型错误信息——当前构造函数需要使用 new 调用
if (!(this instanceof Person)) {
throw new TypeError(
'Function constructor A cannot be invoked without "new"'
);
}
this.firstName = firstName;
this.lastName = lastName;
this.fullName = this.firstName + this.lastName;
}
// 当作普通函数调用
// Uncaught TypeError: Function constructor A cannot be invoked without "new"
console.log(Person("战场", "小包"));
通过输出结果,我们可以发现,定义的 Person
构造函数已经无法被普通调用了。撒花~~~
但这种方案并不是完美的,存在一点小小的瑕疵。我们可以通过伪造实例的方法骗过构造函数里的判断。
具体实现: JavaScript
提供的 apply/call
方法可以修改 this
指向,如果调用时将 this
指向修改为 Person
实例,就可以成功骗过上面的语法。
// 输出结果 undefined
console.log(Person.call(new Person(), "战场", "小包"));
这点瑕疵虽说无伤大雅,但经过小包的学习,ES6
中提供了更好的方案。
new.target
JavaScript
官方也发现了这个让人棘手的问题,因此 ES6
中提供了 new.target
属性。
《ECMAScript 6 入门》中讲到: ES6
为 new
命令引入了一个 new.target
属性,该属性一般用在构造函数之中,返回 new
命令作用于的那个构造函数。如果构造函数不是通过 new
命令或 Reflect.construct()
调用的,new.target
会返回 undefined
,因此这个属性可以用来确定构造函数是怎么调用的。
new.target
就是为确定构造函数的调用方式而生的,太符合这个场景了,我们来试一下 new.target
的用法。
function Person() {
console.log(new.target);
}
// new: Person {}
console.log("new: ", new Person());
// not new: undefined
console.log("not new:", Person());
所以我们就可以使用 new.target
来非常简单的实现对构造函数的限制。
function Person() {
if (!new.target) {
throw new TypeError(
'Function constructor A cannot be invoked without "new"'
);
}
}
// Uncaught TypeError: Function constructor A cannot be invoked without "new"
console.log("not new:", Person());
使用 ES6 Class
小包为什么上面不在 ES6 Class
中进行尝试那?因为小包发现类也具备限制构造函数只能用 new
调用的作用。
ES6
提供 Class
作为构造函数的语法糖,来实现语义化更好的面向对象编程,并且对 Class
进行了规定:类的构造器必须使用 new 来调用(悔不当初~~~)。
因此后续在进行面向对象编程时,强烈推荐使用 ES6
的 Class
。 Class
修复了很多 ES5
面向对象编程的缺陷,例如类中的所有方法都是不可枚举的;类的所有方法都无法被当作构造函数使用等。
class Person {
constructor(name) {
this.name = name;
}
}
// Uncaught TypeError: Class constructor Person cannot be invoked without 'new'
console.log(Person());
学到这里我就不由得好奇了,既然 Class
必须使用 new
来调用,那提供 new.target
属性的意义在哪里?
new.target 实现抽象类
首先来看一下 new.target
在类中使用会返回什么?
class Person {
constructor(name) {
this.name = name;
console.log(new.target);
}
}
new Person();
输出结果:
Class
内部调用 new.target
,会返回当前 Class
。
《ECMAScript 6 入门》中又讲到: 需要注意的是,子类继承父类时,new.target
会返回子类。继承中的 new.target
好像有不一样的花样,我们来试一下。
class Animal {
constructor(type, name, age) {
this.type = type;
this.name = name;
this.age = age;
console.log(new.target);
}
}
// extends 是 Class 中实现继承的关键字
class Dog extends Animal {
constructor(name, age) {
super("dog", "baobao", "1");
}
}
const dog = new Dog();
输出结果:
通过上面案例,我们可以发现子类调用和父类调用的返回结果是不同的,我们利用这个特性,就可以实现父类不可调用而子类可以调用的情况——面向对象中的抽象类
抽象类实现
什么是抽象类那?我们以动物世界为例。
我们定义了一个动物类 Animal
,并且通过这个类来创建动物,动物是个抽象概念,当你提到动物类时,你并不知道我会创建什么动物。只有将动物实体化,比如说猫,狗,猪啊,这才是具体的动物,并且每个动物的行为都会有所不同。因此我们不应该通过创建 Animal
实例来生成动物,Animal
只是动物抽象概念的集合。
Animal
就是一个抽象类,我们不应该通过它来生成动物,而是通过它的子类,例如 Dog、Cat
等来生成对应的 dog/cat
实例。
new.target
子类调用和父类调用的返回值是不同的,所以我们可以借助 new.target
实现抽象类
抽象类也可以理解为不能独立使用、必须继承后才能使用的类。
class Animal {
constructor(type, name, age) {
if (new.target === Animal) {
throw new TypeError("abstract class cannot new");
}
this.type = type;
this.name = name;
this.age = age;
}
}
// extends 是 Class 中实现继承的关键字
class Dog extends Animal {
constructor(name, age) {
super("dog", "baobao", "1");
}
}
// Uncaught TypeError: abstract class cannot new
const dog = new Animal("dog", "baobao", 18);
总结
本文小包学习了三种限制构造函数只能被 new
调用的方案
- 借助
instanceof
和new
绑定的原理,适用于低版本浏览器 - 借助
new.target
属性,可与class
配合定义抽象类 - 面向对象编程使用
ES6 class
——最佳方案
怎么理解 Proxy handler 中 receiver 指向的是当前操作正确上的下文呢?
正常情况下,receiver
指向的是 当前的代理对象
特殊情况下,receiver
指向的是 引发当前操作的对象
- 通过
Object.setPrototypeOf()
方法将代理对象proxy
设置为普通对象obj
的原型 - 通过
obj.name
访问其不存在的name
属性,由于原型链的存在,最终会访问到proxy.name
上,即触发get
捕获器
在 Reflect
的方法中通常只需要传递 target、key、newVal
等,但为了能够处理上述提到的特殊情况,一般也需要传递 receiver
参数,因为 Reflect 方法中传递的 receiver 参数代表执行原始操作时的 this
指向,比如:Reflect.get(target, key , receiver)
、Reflect.set(target, key, newVal, receiver)
。
总结:Reflect
是为了在执行对应的拦截操作的方法时能 传递正确的 this
上下文。
什么是 commonJS / AMD / CMD / UMD / ES6
什么是模块化:
可以简单的理解为将原来繁重复杂的整个 js
文件按照功能 或者按模块拆成一个个单独的 js
文件,然后将每一个 js
文件中的某些方法抛出去,给别的 js
文件引用和依赖
node.js
采取commonJS
规范,因为是服务器编程,模块文件一般都已经存在本地硬盘,加载比较快,采用同步加载模块- 浏览器端一般采用
AMD
规范,浏览器环境要从服务器端加载模块,这时就必须采用异步模式,出的早,可以指定回调函数 CMD
规范专门用于浏览器端,模块的加载是异步的,模块使用时才会加载执行(延迟加载)。CMD
规范整合了commonJS
和AMD
规范的特点
commonJS
模块是运行时加载,它输出一个值的拷贝(模块内改变不会影响输出的这个值)
e6
模块是编译时输出接口,是输出一个值得引用(引用会改变原值)
AMD
AMD(Asynchronous Module Definition):异步模块定义。采用异步方式加载模块,模块的加载不影响后续语句的执行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。
requireJS
是一个遵守 AMD
规范工具库,用于客户端的模块管理。requireJS 的基本思想是,通过 define 方法,将代码定义为模块;通过 require 方法,实现代码的模块加载。
// AMD 默认推荐的是
define(["./a", "./b"], function (a, b) {
// 依赖必须一开始就写好
a.doSomething();
// 此处略去 100 行
b.doSomething();
// ...
});
简单的理解为 AMD 定义模块必须提前声明
CMD
CMD(Common Module Definition):通用模块定义。用于浏览器端,是除 AMD 以外的另一种模块组织规范。结合了 AMD 与 CommonJs 的特点。也是异步加载模块。 与 AMD 不同的是:AMD 推崇的是依赖前置,而 CMD 是依赖就近,延迟执行。
// CMD
define(function (require, exports, module) {
var a = require("./a");
a.doSomething();
// 此处略去 100 行
var b = require("./b");
// 依赖可以就近书写
b.doSomething();
// ...
});
CMD 则是用到的时候再声明
CommonJS
Node 应用由模块组成,采用 CommonJS 模块规范。每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。在服务器端,模块的加载是运行时同步加载的;在浏览器端,模块需要提前编译打包处理。
CommonJs 有 4
个比较重要的变量:module
、require
、exports
、global
ES6
ES modules(ESM)是 JavaScript 官方的标准化模块系统。ES6 模块设计的思想是尽量的静态化,使得编译时就能知道模块的依赖关系,以及输入和输出的变量。有两个主要的命令:export 和 import。export 用于对外暴露接口,import 用于引入其他模块。
ES6 模块的特点:
- 严格模式:ES6 的模块自动采用严格模式
- import
read-only
特性: import 的属性是只读的,不能赋值,类似于 const 的特性 - export / import 提升:
import / export
必须位于模块顶级
,不能位于作用域内;其次对于模块内的import / export
会提升到模块顶部,这是在编译阶段完成的 - 兼容在 node 环境下运行
- ES modules 输出的是
值的引用
,输出接口动态绑定,而 CommonJS 输出的是值的拷贝
UMD
UMD 是一种通用模块定义规范,旨在兼容多个模块化规范。它可以在不同的环境(如浏览器和 Node.js)下无缝运行。UMD 根据当前环境的不同,选择使用 CommonJS、AMD 或全局变量来定义和加载模块。
总结
- AMD:异步加载模块,允许指定回调函数。AMD 规范是依赖前置的。一般浏览器端会采用 AMD 规范。但是开发成本高,代码阅读和书写比较困难。
- CMD:异步加载模块。CMD 规范是依赖就近,延迟加载。一般也是用于浏览器端。
- CommonJs:同步加载模块,一般用于服务器端。对外暴露的接口是值的拷贝
- ES6:实现简单。对外暴露的接口是值的引用。可以用于浏览器端和服务端。
什么是 ES6 中的类
在 ES6(ECMAScript 2015)中,引入了官方的类(Class)语法,使得 JavaScript 支持基于类的面向对象编程。ES6 中的类提供了更简洁、清晰的语法来定义对象模板,并支持继承和其他面向对象的概念。
ES6 中的类具有以下特点和语法:
类的定义: 使用
class
关键字来定义一个类,并使用大驼峰命名法为类命名。类的定义包含在一对大括号{}
内。class MyClass {
// 类的定义
}构造函数: 使用
constructor
方法定义类的构造函数。构造函数在创建类的实例时自动调用,并用于初始化对象的状态。class MyClass {
constructor() {
// 构造函数
}
}属性和方法: 类的属性和方法可以在类的定义内部声明。属性通过直接赋值给
this
关键字来定义,方法通过将函数定义为类的方法来声明。class MyClass {
constructor() {
this.property = value; // 属性
}
method() {
// 方法
}
}继承: 使用
extends
关键字来实现类之间的继承。一个类可以继承另一个类的属性和方法,并可以添加自己的属性和方法。class ChildClass extends ParentClass {
// 子类的定义
}实例化: 使用
new
关键字和类的构造函数来创建类的实例。const myObj = new MyClass(); // 创建类的实例
ES6 中的类提供了一种更直观和易于理解的方式来实现面向对象编程,使得 JavaScript 开发者可以更方便地使用类、继承和对象实例化等概念。类语法的引入使得 JavaScript 在语言层面上更接近传统面向对象编程语言,并提供了更强大的工具来构建复杂的应用程序。
es5 和 es6 构造实例的区别
ES5:
// Person为构造函数
function Person (name, age) {
this.name = name,
this.age = age
}
// 在构造函数的原型上添加方法
Person.prototype.getName = function () {
return this.name
}
// 构造一个实例
var huhaha = new Person('huhaha', 21)
// 在实例上调用该方法
var myName = huhaha.getName()
console.log(myName) // huhaha
ES6:
// 类中的this指向创建的实例
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
getName() {
return this.name;
}
}
// 构造一个实例
let huhaha = new Person("huhaha", 21);
// 在实例上调用该方法
const myName = huhaha.getName();
console.log(myName); // huhaha
简述原型与原型链,原型链的作用有哪些?
- 构造函数:用来初始化新创建的对象的函数是构造函数。在例子中,
Foo()
函数是构造函数 - 原型对象:构造函数有一个
prototype
属性,指向实例对象的原型对象。通过同一个构造函数实例化的多个对象具有相同的原型对象。经常使用原型对象来实现继承。 - 实例对象:通过构造函数的
new
操作创建的对象是实例对象。可以用一个构造函数,构造多个实例对象
每一个实例对象都有一个 原型对象,es6 里叫 [[Prototype]]: Object
)
prototype
中有一个隐式 __proto__
属性(隐式原型),默认值是构造函数的 prototype
__proto__
的作用就是当访问一个对象的属性时,如果该对象内部不存在这个属性,那么就会去它的 __proto__
属性所指向的那个对象(父对象)里找,一直找直到 __proto__
属性的终点 null
,再往上找就相当于在 null
上取值就会报错。
通过 __proto__
属性将对象连接起来的这条链路即我们所谓的原型链。
- 一切对象都是继承自
Object
对象,Object
对象直接继承根源对象null
- 一切的函数对象(包括
Object
对象),都是继承自Function
对象 Object
对象直接继承自Function
对象Function
对象的__proto__
会指向自己的原型对象,最终还是继承自Object
对象
讲一下闭包
闭包让你可以在一个内层函数中访问到其外层函数的作用域
一个函数和词法环境的引用捆绑在一起,这样的组合就是闭包(closure)。
一般就是一个 func A
,return 其内部的 func B
,被 return
出去的 func B
能够在外部访问 func A
内部的变量,这时候就形成了一个 func B
的变量背包, func A
执行结束后这个变量背包也不会被销毁,并且这个变量背包在 func A
外部只能通过 func B
访问。
function A() {
let a1 = 1;
return function B() {
return a1;
};
}
// 外部如何访问到 func B,我们只需要将 func B 作为 func A 的返回值返回,
// 这样我们不就能在 func A 外部访问到 func A 内部的变量了
var result = A();
result(); // 1
闭包形成的原理: 作用域链,当前作用域可以访问上级作用域中的变量。 闭包解决的问题: 能够让函数作用域中的变量在函数执行结束之后不被销毁,同时也能在函数外部可以访问函数内部的局部变量。 闭包带来的问题: 由于垃圾回收器不会将闭包中变量销毁,于是就造成了内存泄露,内存泄露积累多了就容易导致内存溢出,不过这取决与写代码的人。 闭包的应用场景: 防抖函数应用到了闭包,能够模仿块级作用域,能够实现柯里化,在构造函数中定义特权方法、Vue 中数据响应式 Observer 中使用闭包等。
函数作为参数:柯里化函数 柯里化的目的在于避免频繁调用具有相同参数函数的同时,又能够轻松的重用
// 假设我们有一个求长方形面积的函数
function getArea(width, height) {
return width * height;
}
// 如果我们碰到的长方形的宽老是10
const area1 = getArea(10, 20);
const area2 = getArea(10, 30);
const area3 = getArea(10, 40);
// 我们可以使用闭包柯里化这个计算面积的函数
function getArea(width) {
return (height) => {
return width * height;
};
}
const getTenWidthArea = getArea(10);
// 之后碰到宽度为10的长方形就可以这样计算面积
const area1 = getTenWidthArea(20);
// 而且如果遇到宽度偶尔变化也可以轻松复用
const getTwentyWidthArea = getArea(20);
在 script 标签上使用 defer 和 async 的区别
在 <script>
标签上使用 defer
和 async
属性可以控制脚本的加载和执行方式,它们之间有以下区别:
加载方式:
- 使用
defer
属性:脚本将按照文档顺序加载,但是脚本的执行会延迟到整个文档解析完成后再执行。多个带有defer
属性的脚本会按照它们在文档中出现的顺序依次执行。 - 使用
async
属性:脚本的加载和文档解析是异步进行的,脚本加载完毕后立即执行,不会阻塞文档的解析和其他资源的加载。
- 使用
执行时机:
defer
属性:脚本的执行时机是在文档完全解析之后,DOMContentLoaded
事件触发之前。这意味着脚本可以访问完整的 DOM 结构。async
属性:脚本的执行时机是在脚本加载完成后立即执行,不管文档是否解析完毕。这可能意味着脚本在访问 DOM 时可能会因为文档尚未完全解析而导致错误。
执行顺序:
defer
属性:多个带有defer
属性的脚本会按照它们在文档中出现的顺序依次执行,保持了它们在文档中的相对顺序。async
属性:多个带有async
属性的脚本的执行顺序是不确定的,它们可以并行加载和执行,先加载完成的脚本会先执行。
依赖关系:
defer
属性:脚本的加载和执行不会阻塞后续脚本的加载和执行,适合有依赖关系的脚本。async
属性:脚本的加载和执行是异步的,不会保证脚本的加载和执行顺序,适合独立的、无依赖的脚本。
综上所述,使用 defer
属性可以确保脚本在文档解析完成后执行,保持脚本的执行顺序,适合有依赖关系的脚本;而使用 async
属性可以实现异步加载和执行脚本,适合独立的、无依赖的脚本,并且可以提高页面加载性能。根据具体需求和脚本之间的依赖关系,选择适合的属性来加载和执行脚本。
图片懒加载原理
原理
一张图片就是一个标签,浏览器是否发起请求图片是根据的 src 属性,所以实现懒加载的关键就是,在图片没有进入可视区域时,先不给的 src 赋值,这样浏览器就不会发送请求了,等到图片进入可视区域再给 src 赋值
- 图片的
src
不设置真实的路径 - 图片的真实路径设置在其他属性中比如
data-src
- 通过
js
判断图片是否进入可视区域 - 如果图片进入可是区域将图片
src
换成真实路径
实现
其他方式已经大致实现懒加载,但是,它们都有一个缺点,就是一当发生滚动事件时,就发生了大量的循环和判断操作判断图片是否可视区里。这自然是不太好的,那是否有解决方法。 这里就引入了一个叫 Intersection Observer 观察器接口,它是是浏览器原生提供的构造函数,使用它能省到大量的循环和判断。当然它的兼容可能不太好,看情况使用。
Intersection Observer
是什么呢?这个构造函数的作用是它能够观察可视窗口与目标元素产生的交叉区域。
简单来说就是当用它观察我们的图片时,当图片出现或者消失在可视窗口,它都能知道并且会执行一个特殊的 回调函数
,我们就利用这个回调函数实现我们的操作。
概念枯燥难懂,直接看下面例子:
const images = document.getElementsByTagName("img");
function callback(entries) {
for (let i of entries) {
if (i.isIntersecting) {
let img = i.target;
let trueSrc = img.getAttribute("data-src");
img.setAttribute("src", trueSrc);
observer.unobserve(img);
}
}
}
const observer = new IntersectionObserver(callback);
for (let i of images) {
observer.observe(i);
}
普通函数,箭头函数的区别
普通函数和箭头函数是 JavaScript 中的两种不同的函数定义方式,它们在语法和行为上有一些区别。
以下是普通函数和箭头函数之间的主要区别:
语法:
- 普通函数:使用
function
关键字声明函数,并指定函数名和参数列表。函数体由花括号{}
包裹。 - 箭头函数:使用箭头
=>
表示函数,并可以省略function
关键字。参数列表在括号()
中指定。函数体可以是一个表达式或者一个代码块(由花括号{}
包裹)。
- 普通函数:使用
this 的绑定:
- 普通函数:每个函数都有自己的
this
值,它在函数被调用时根据调用上下文确定。可以使用call
、apply
或者bind
方法来手动绑定this
。 - 箭头函数:箭头函数没有自己的
this
,它继承自外部作用域的this
值。箭头函数的this
始终指向定义时所在的上下文,无法通过call
、apply
或者bind
改变其上下文。
- 普通函数:每个函数都有自己的
arguments 对象:
- 普通函数:普通函数内部可以使用
arguments
对象来访问传入的参数列表。arguments
是一个类数组对象,包含了函数的所有参数。 - 箭头函数:箭头函数没有自己的
arguments
对象。它继承外部作用域的arguments
对象,即使用外部函数的arguments
。
- 普通函数:普通函数内部可以使用
构造函数:
- 普通函数:普通函数可以作为构造函数使用,通过
new
关键字创建实例对象。在构造函数中,this
指向新创建的实例对象。 - 箭头函数:箭头函数不能用作构造函数,不能通过
new
关键字创建实例对象。如果尝试使用new
调用箭头函数,会抛出错误。
- 普通函数:普通函数可以作为构造函数使用,通过
作为方法:
- 普通函数:普通函数可以作为对象的方法,并且在调用时绑定正确的
this
值。this
指向调用该方法的对象。 - 箭头函数:箭头函数可以作为对象的方法,但是箭头函数的
this
始终指向定义时的上下文,而不是调用时的对象。
- 普通函数:普通函数可以作为对象的方法,并且在调用时绑定正确的
需要注意的是,箭头函数的语法简洁,适合用于简单的函数表达式和回调函数。然而,由于箭头函数没有自己的 this
值,不适合在需要动态绑定 this
的场景中使用,比如作为对象的方法或者构造函数。
综上所述,普通函数和箭头函数在语法和行为上有一些区别,特别是对于 this
的处理方式。选择使用哪种函数形式应根据具体的需求和上下文来决定。
promise 有几种状态
Promise 在 JavaScript 中有三种状态:
Pending(进行中): 初始状态为 Pending。这表示 Promise 的操作正在进行中,尚未成功也未失败。
Fulfilled(已成功): 当 Promise 成功完成操作时,状态会从 Pending 变为 Fulfilled。在 Fulfilled 状态下,Promise 会保存成功的结果,并且可以通过
then()
方法来访问和处理该结果。Rejected(已失败): 当 Promise 在操作过程中遇到错误或失败时,状态会从 Pending 变为 Rejected。在 Rejected 状态下,Promise 会保存失败的原因,并且可以通过
catch()
或then()
方法中的第二个回调函数来处理错误。
一旦 Promise 进入 Fulfilled 或 Rejected 状态,它就是最终状态,不能再次改变。如果 Promise 成功完成操作,它将保持在 Fulfilled 状态;如果 Promise 遇到错误或失败,它将保持在 Rejected 状态。
Promise 的状态转换是单向的,只能从 Pending 转变为 Fulfilled 或 Rejected,一旦转变为其中一种状态,就无法再改变。这种特性使得 Promise 可以更好地处理异步操作和处理结果的链式调用。
promise 如何解决地狱回调
Promise 解决了回调地狱问题,使得异步操作的处理更加清晰和可读。通过 Promise,可以使用链式调用来处理异步操作,避免嵌套的回调函数。
以下是 Promise 如何解决回调地狱的几个关键点:
链式调用: Promise 提供了
then()
方法,使得可以将多个异步操作串联起来,形成一个链式调用。每个then()
方法返回一个新的 Promise,可以在其上继续调用下一个异步操作。返回新的 Promise: 每个
then()
方法都返回一个新的 Promise 对象,这样可以在每个步骤中返回结果或者传递错误,以供后续的then()
方法处理。这种方式使得可以在链式调用中捕获错误并进行统一的错误处理。使用 catch() 方法: Promise 提供了
catch()
方法,用于捕获链式调用中的任何错误。如果链式调用中的任何一个 Promise 被拒绝(Rejected),错误会被传递到最近的catch()
方法进行处理,从而避免了多层嵌套回调函数的问题。
通过使用 Promise 的链式调用和错误处理机制,可以将异步操作的处理流程表达为一系列清晰的步骤,避免了回调地狱的问题。这使得代码更易读、维护和扩展,同时提高了异步操作的可靠性和可控性。
以下是使用 Promise 链式调用来解决回调地狱问题的示例代码:
asyncFunction1()
.then((result1) => {
// 处理第一个异步操作的结果
return asyncFunction2(result1);
})
.then((result2) => {
// 处理第二个异步操作的结果
return asyncFunction3(result2);
})
.then((result3) => {
// 处理第三个异步操作的结果
console.log("最终结果:", result3);
})
.catch((error) => {
// 处理任何一个步骤中的错误
console.error("发生错误:", error);
});
在这个示例中,每个 then()
方法接收上一步骤的结果,并将其传递给下一个异步操作。如果任何一个步骤中出现错误,它将被捕获并传递到 catch()
方法进行处理。这样,可以通过简单的链式调用来处理复杂的异步操作,避免了回调地狱的问题。
promise 有哪些方法?应用场景是什么?
Promise.all()
Promise.allSettled()
Promise.any()
Promise.prototype.catch()
Promise.prototype.finally()
Promise.race()
Promise.reject()
Promise.resolve()
Promise.prototype.then()
Promise.withResolvers()
Promise 对象提供了以下几个常用的方法:
then(onFulfilled, onRejected): 用于注册 Promise 成功(Fulfilled)和失败(Rejected)时的回调函数。第一个参数
onFulfilled
是当 Promise 成功时调用的回调函数,第二个参数onRejected
是当 Promise 失败时调用的回调函数。then()
方法返回一个新的 Promise 对象,可用于链式调用。catch(onRejected): 用于注册 Promise 失败(Rejected)时的回调函数,相当于
then(null, onRejected)
。catch()
方法返回一个新的 Promise 对象,可用于链式调用。finally(onFinally): 用于注册 Promise 不论成功还是失败都会执行的回调函数。
finally()
方法返回一个新的 Promise 对象,可用于链式调用。Promise.resolve(value): 返回一个已解析(Resolved)为给定值的 Promise 对象。如果传入的值本身就是一个 Promise 对象,则直接返回该对象。
Promise.reject(reason): 返回一个拒绝(Rejected)指定原因的 Promise 对象。
Promise.all(iterable): 接收一个可迭代对象(如数组或类数组对象),返回一个新的 Promise 对象。该 Promise 对象在可迭代对象中所有 Promise 都成功解析后才会成功,否则只要有一个 Promise 失败就会拒绝。
Promise.race(iterable): 接收一个可迭代对象,返回一个新的 Promise 对象。该 Promise 对象在可迭代对象中的任意一个 Promise 解析或拒绝后立即解析或拒绝。
应用场景:
使用
then()
方法可以处理异步操作成功和失败的情况,并对结果进行处理。适用于需要对异步操作结果进行后续处理的场景。使用
catch()
方法可以捕获 Promise 链中的错误,进行错误处理。适用于需要统一处理 Promise 链中的错误的场景。使用
finally()
方法可以在 Promise 链中的任何位置执行一些清理操作,无论 Promise 成功还是失败。适用于需要在 Promise 执行结束后执行一些清理逻辑的场景。使用
Promise.resolve()
可以将一个值(包括 Promise 对象)转换为已解析的 Promise 对象,适用于需要将同步操作转换为 Promise 对象的场景。使用
Promise.reject()
可以创建一个已拒绝的 Promise 对象,适用于需要立即拒绝一个 Promise 的场景。使用
Promise.all()
可以等待多个 Promise 对象全部成功解析后再进行处理,适用于需要等待多个异步操作全部完成后再进行下一步处理的场景。使用
Promise.race()
可以等待多个 Promise 对象中的任意一个解析或拒绝后立即进行处理,适用于需要对多个异步操作中最快完成的结果进行处理的场景。
这些方法使得 Promise 更加灵活和强大,可以更好地处理异步操作和构建可靠的异步处理流程。
Promise.all 和 Promise.allSettled 有什么区别
Promise.all()
和 Promise.allSettled()
是两个用于处理多个 Promise 的方法,它们有以下区别:
处理结果的方式:
Promise.all()
: 当所有的 Promise 都成功解析(Fulfilled)时,返回一个成功解析的 Promise,并将所有 Promise 的结果作为一个数组传递。如果任何一个 Promise 失败(Rejected),则返回一个拒绝(Rejected)的 Promise,并将第一个失败的 Promise 的原因作为拒绝的原因。Promise.allSettled()
: 返回一个 Promise,该 Promise 在所有的 Promise 都已解析或拒绝后解析,并将每个 Promise 的结果作为一个对象组成的数组传递。每个对象包含有关每个 Promise 的解析状态和结果(无论成功与否)。
对于拒绝的 Promise 的处理:
Promise.all()
: 如果任何一个 Promise 失败(Rejected),立即返回一个拒绝的 Promise,并将第一个失败的 Promise 的原因作为拒绝的原因。这意味着只要有一个 Promise 失败,整个 Promise.all() 就会失败。Promise.allSettled()
: 不管 Promise 是否成功或失败,都会等待所有的 Promise 完成。它会返回一个解析的 Promise,并将一个对象数组作为结果,其中每个对象表示每个 Promise 的状态和结果。这样可以获取到每个 Promise 的最终状态,无论成功还是失败。
返回值的类型:
Promise.all()
: 返回一个 Promise 对象,该对象在所有的 Promise 都成功解析时才会成功,并返回一个包含所有 Promise 结果的数组。Promise.allSettled()
: 返回一个 Promise 对象,在所有的 Promise 都完成后才会解析,并返回一个包含每个 Promise 的状态和结果的对象数组。
应用场景:
Promise.all()
: 适用于需要等待多个 Promise 全部成功解析后才进行处理的情况,例如多个并行的异步操作,可以同时发起请求,然后等待所有请求都成功返回后再进行下一步处理。Promise.allSettled()
: 适用于需要等待多个 Promise 全部完成后获取每个 Promise 的最终状态和结果的情况,不管成功还是失败。这对于需要获取所有 Promise 结果的情况很有用,即使其中某些 Promise 失败,也能获取到失败的原因和结果。
总结来说,Promise.all()
用于等待多个 Promise 全部成功解析或有一个失败时立即拒绝,而 Promise.allSettled()
用于等待多个 Promise 全部完成,无论成功还是失败,都返回一个包含每个 Promise 的最终状态和结果的数组。
Promise.any 和 Promise.race 有什么区别
Promise.any()
和 Promise.race()
是两个用于处理多个 Promise 的方法,它们有以下区别:
处理结果的方式:
Promise.any()
: 返回一个 Promise,该 Promise 在多个 Promise 中只要有一个成功解析(Fulfilled)时就会成功解析,并将第一个成功解析的 Promise 的结果作为解析值。如果所有的 Promise 都失败(Rejected),则返回一个拒绝(Rejected)的 Promise,并将所有 Promise 的失败原因作为拒绝的原因。Promise.race()
: 返回一个 Promise,该 Promise 在多个 Promise 中只要有一个解析或拒绝时就会解析或拒绝。它会返回第一个解析或拒绝的 Promise 的结果或原因。
对于拒绝的 Promise 的处理:
Promise.any()
: 如果所有的 Promise 都失败(Rejected),则返回一个拒绝的 Promise,并将所有 Promise 的失败原因作为拒绝的原因。只要有一个 Promise 成功解析,即使其他 Promise 失败,整个Promise.any()
就会成功。Promise.race()
: 如果第一个解析或拒绝的 Promise 是拒绝的,则返回一个拒绝的 Promise,并将第一个拒绝的 Promise 的原因作为拒绝的原因。
返回值的类型:
Promise.any()
: 返回一个 Promise 对象,该对象在多个 Promise 中只要有一个成功解析时就会成功,并返回第一个成功解析的 Promise 的结果。Promise.race()
: 返回一个 Promise 对象,该对象在多个 Promise 中只要有一个解析或拒绝时就会解析或拒绝,并返回第一个解析或拒绝的 Promise 的结果或原因。
应用场景:
Promise.any()
: 适用于需要等待多个 Promise 中的任意一个成功解析的情况,例如多个并行的异步操作,可以同时发起请求,然后只要有一个请求成功返回就进行下一步处理。Promise.race()
: 适用于需要等待多个 Promise 中的最快完成的结果的情况,可以用于设置超时、竞速等场景,返回第一个完成的 Promise 的结果或原因。
总结来说,Promise.any()
用于等待多个 Promise 中任意一个成功解析的情况,而 Promise.race()
用于等待多个 Promise 中第一个解析或拒绝的情况。
说一说 eventLoop(事件循环)宏任务与微任务
浏览器的事件循环: 执行 js
代码的时候,遇见同步任务,直接推入调用栈中执行,遇到异步任务,将该任务挂起,等到异步任务有返回之后推入到任务队列中,当调用栈中的所有同步任务全部执行完成,将任务队列中的任务按顺序一个一个的推入并执行,重复执行这一系列的行为被称为事件循环。 异步任务又分为宏任务和微任务。
- 先执行执行栈中的同步任务
- 异步任务(回调函数)放入任务队列中
- 一旦执行栈中的所有同步任务执行完毕,系统就会按顺序读取任务队列中的异步任务,被读取的异步任务结束等待进入执行栈中执行
宏任务:任务队列中的任务称为宏任务,每个宏任务中都包含了一个微任务队列。
微任务:等宏任务中的主要功能都完成后,渲染引擎不急着去执行下一个宏任务,而是执行当前宏任务中的微任务
宏任务包含: 执行 script
标签内部代码、setTimeout / setInterval、ajax 请求、postMessageMessageChannel、setImmediate,I/O(Node.js)
微任务包含: Promise、MutonObserver、Object.observe、process.nextTick(Node.js)
this 指向哪里
this
对象是是执行上下文中的一个属性,它指向最后一次调用这个方法的对象,在全局函数中,this
等于window
,而当函数被作为某个对象调用时,this 等于那个对象。 在实际开发中,this
的指向可以通过几种调用模式来判断。
- 函数调用,当一个函数不是一个对象的属性时,直接作为函数来调用时,
this
指向全局对象。 - 方法调用,如果一个函数作为一个对象的方法来调用时,
this
指向这个对象。 - 构造函数调用,
this
指向这个用new
新创建的对象。 - 第四种是
apply 、 call 和 bind
调用模式,这三个方法都可以显示的指定调用函数的 this 指向。apply
接收参数的是数组,call
接受参数列表,`bind
方法通过传入一个对象,返回一个this
绑定了传入对象的新函数。这个函数的this
指向除了使用new
时会被改变,其他情况下都不会改变。 - 箭头函数: 箭头函数中的
this
不会被绑定到任何特定的对象,而是继承自其外部作用域。它会捕获在创建时的上下文,并保持不变。 - 事件处理函数: 当函数被用作事件处理函数时,
this
指向触发事件的元素。
详细解释 js 实现继承的几种方式,并包含实例
当涉及到 JavaScript 中的继承时,有几种常见的方式可以实现。下面将详细解释每一种方式,并提供相应的实例代码。
- 原型链继承: 原型链继承是 JavaScript 中最基本的继承方式之一。通过将子类的原型对象设置为父类的实例,实现继承。这样子类就可以访问父类的属性和方法。然而,这种方式存在的一个问题是,子类的所有实例将共享同一个原型对象。
function Parent() {
this.name = "Parent";
}
Parent.prototype.sayHello = function () {
console.log("Hello, I am " + this.name);
};
function Child() {
this.name = "Child";
}
Child.prototype = new Parent();
var child1 = new Child();
child1.sayHello(); // Output: Hello, I am Child
var child2 = new Child();
child2.sayHello(); // Output: Hello, I am Child
- 构造函数继承:
构造函数继承通过在子类构造函数中使用
call
或apply
方法,将父类的构造函数应用于子类实例,从而继承父类的属性。这种方式只能继承父类的实例属性,无法继承原型上的方法。
function Parent(name) {
this.name = name;
}
Parent.prototype.sayHello = function () {
console.log("Hello, I am " + this.name);
};
function Child(name) {
Parent.call(this, name);
}
var child = new Child("Child");
child.sayHello(); // Error: child.sayHello is not a function
- 组合继承: 组合继承是结合原型链继承和构造函数继承的方式。通过调用父类构造函数继承属性,同时将子类的原型设置为父类的实例,实现继承父类的属性和方法。这种方式避免了原型链上属性的共享问题。
function Parent(name) {
this.name = name;
}
Parent.prototype.sayHello = function () {
console.log("Hello, I am " + this.name);
};
function Child(name) {
Parent.call(this, name);
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
var child = new Child("Child");
child.sayHello(); // Output: Hello, I am Child
- 原型式继承: 原型式继承通过创建一个临时构造函数,将父类的实例作为临时构造函数的原型,然后返回新的实例。通过这种方式可以创建一个新对象,并继承父类的属性和方法。
function createObject(proto) {
function Temp() {}
Temp.prototype = proto;
return new Temp();
}
var parent = {
name: "Parent",
sayHello: function () {
console.log("Hello, I am " + this.name);
},
};
var child = createObject(parent);
child.name = "Child";
child.sayHello(); // Output: Hello, I am Child
- 寄生式继承: 寄生式继承在原型式继承的基础上,增强新对象,添加额外的方法或属性,然后返回新对象。这种方式可以在不修改父类的情况下,对继承的对象进行扩展。
function createObject(proto) {
var obj = Object.create(proto);
obj.sayHello = function () {
console.log("Hello, I am " + this.name);
};
return obj;
}
var parent = {
name: "Parent",
};
var child = createObject(parent);
child.name = "Child";
child.sayHello(); // Output: Hello, I am Child
- 类继承(ES6):
使用 ES6 中的
class
和extends
关键字,通过定义类和子类来实现继承。子类通过extends
关键字继承父类,并可以使用super
关键字调用父类的构造函数和方法。
class Parent {
name = "Parent";
sayHello() {
console.log("Hello, I am " + this.name);
}
}
class Child extends Parent {
constructor(name) {
super(name);
}
}
let child = new Child("Child");
child.sayHello(); // Output: Hello, I am Child
寄生式组合继承: 寄生式组合继承是一种继承方式,它通过借用构造函数继承属性,同时使用寄生式继承来继承父类的原型方法,避免了组合继承中重复调用父类构造函数的问题。
function inheritPrototype(child, parent) {
var prototype = Object.create(parent.prototype);
prototype.constructor = child;
child.prototype = prototype;
}
function Parent(name) {
this.name = name;
}
Parent.prototype.sayHello = function () {
console.log("Hello, I am " + this.name);
};
function Child(name) {
Parent.call(this, name);
}
inheritPrototype(Child, Parent);
var child = new Child("Child");
child.sayHello(); // Output: Hello, I am Child
在上述示例中,inheritPrototype
函数用于将子类的原型设置为父类的实例,并确保子类的原型的 constructor
属性指向子类自身。这样,子类就可以继承父类的原型方法,而不会影响到父类或其他子类的实例。
使用寄生式组合继承,我们可以同时继承父类的属性和方法,而无需重复调用父类构造函数。这是一种常用且高效的继承方式。
这些是 JavaScript 中常用的几种实现继承的方式。每种方式都有其特点和适用场景,根据具体需求选择合适的继承方式。
什么 ajax,ajax 的实现原理
Ajax(Asynchronous JavaScript and XML)是一种在 Web 应用程序中使用的技术,可以通过在后台与服务器进行异步数据交换,实现无需刷新整个页面的动态更新。它可以通过 JavaScript 的 XMLHttpRequest 对象发送 HTTP 请求,并处理服务器返回的数据。
Ajax 的实现原理如下:
创建 XMLHttpRequest 对象:通过 JavaScript 创建一个 XMLHttpRequest 对象,该对象用于和服务器进行通信。
发送请求:使用 XMLHttpRequest 对象的 open()方法设置 HTTP 请求的类型(GET、POST 等)、URL 和是否异步发送请求的参数。
接收响应:当服务器返回响应时,XMLHttpRequest 对象会触发 onreadystatechange 事件,我们可以通过监听该事件来处理服务器返回的数据。
处理响应:在 onreadystatechange 事件中,通过 XMLHttpRequest 对象的 readyState 属性可以获取当前的状态,通过 status 属性可以获取 HTTP 请求的状态码。当 readyState 为 4 且 status 为 200 时,表示请求成功,可以通过 XMLHttpRequest 对象的 responseText 或 responseXML 属性获取服务器返回的数据。
更新页面:根据服务器返回的数据,使用 JavaScript 来更新页面的内容,而无需刷新整个页面。
下面是一个简单的示例,演示了如何使用 Ajax 发送 GET 请求并处理服务器返回的数据:
// 创建XMLHttpRequest对象
var xhr = new XMLHttpRequest();
// 监听onreadystatechange事件
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
// 请求成功,处理响应数据
var response = xhr.responseText;
console.log(response);
// 在这里可以更新页面内容
}
};
// 发送GET请求
xhr.open("GET", "https://api.example.com/data", true);
xhr.send();
上述示例中,我们创建了一个 XMLHttpRequest 对象,并使用 open()方法设置了一个 GET 请求,将数据发送到指定的 URL。然后,通过监听 onreadystatechange 事件,当 readyState 为 4 且 status 为 200 时,表示请求成功,我们可以通过 xhr.responseText 获取服务器返回的数据。根据需要,我们可以使用获取到的数据来更新页面的内容。
Ajax 的实现原理允许我们在不刷新整个页面的情况下,与服务器进行异步通信,并实时更新页面的内容,提供了更好的用户体验。
什么是事件捕获和事件冒泡?什么是事件委托?
事件捕获和事件冒泡是两种不同的事件传播机制,而事件委托是一种利用事件冒泡的技术。
事件捕获: 事件捕获是一种事件传播的方式,它从文档根元素开始,逐级向下传播到触发事件的目标元素。在事件捕获阶段,父元素会先接收到事件,然后依次传递给子元素,直到达到目标元素。
事件冒泡: 事件冒泡是另一种事件传播的方式,它与事件捕获相反。在事件冒泡阶段,事件会从触发事件的目标元素开始,逐级向上冒泡到文档根元素。也就是说,父元素会后于子元素接收到事件。
综上所述,事件捕获和事件冒泡是指事件在 DOM 树中传播的两个阶段。事件捕获从上到下传播,而事件冒泡从下到上传播。
事件委托: 事件委托是利用事件冒泡机制的一种技术,通过将事件处理程序绑定到一个父元素上,来统一处理多个子元素的事件。当事件发生时,事件会冒泡到父元素,然后通过事件冒泡机制触发相应的事件处理程序。
使用事件委托的好处是可以减少事件处理程序的数量,提高性能和代码简洁性。相比于为每个子元素都绑定事件处理程序,通过委托父元素来处理子元素的事件可以避免重复的事件处理逻辑。这对于动态生成的元素特别有用,因为我们不需要为每个新元素都显式绑定事件。
例如,假设我们有一个父元素包含多个子元素,需要为每个子元素的点击事件添加处理程序。我们可以将点击事件的处理程序绑定到父元素上,然后根据事件冒泡阶段中的目标元素来确定具体点击的是哪个子元素。
通过事件委托,我们可以在父元素上统一处理子元素的事件,而不需要为每个子元素都绑定事件处理程序。这样可以简化代码,提高性能和可维护性。
跨域通信的几种方式
跨域通信指的是在浏览器中进行跨域(不同源)之间的数据交互。由于浏览器的同源策略的限制,直接进行跨域通信是不被允许的。以下是几种常见的跨域通信方式:
JSONP(JSON with Padding): JSONP 是一种通过动态创建
<script>
标签来实现跨域通信的技术。通过在请求 URL 中指定一个回调函数名,服务器返回的数据将被包裹在该函数的调用中,从而使前端能够获取到数据。JSONP 只支持 GET 请求,并且服务器需要返回一段可执行的 JavaScript 代码。CORS(Cross-Origin Resource Sharing): CORS 是一种由浏览器实施的跨域通信机制。通过在服务器端设置响应头,如
Access-Control-Allow-Origin
,可以指定允许访问的跨域来源。前端可以直接使用 XMLHttpRequest 或 fetch 等方式进行跨域请求,浏览器会在请求发送前进行预检请求(OPTIONS 请求),以确定是否允许跨域请求。WebSocket: WebSocket 协议本身支持跨域通信。使用 WebSocket 可以在浏览器与服务器之间建立持久化的双向通信连接,从而实现实时的跨域数据传输。前端使用
WebSocket
对象进行连接和消息传递,后端需要实现 WebSocket 服务器来处理连接和消息。代理服务器: 前端可以通过在自己的域名下设置代理服务器来进行跨域通信。前端将请求发送到自己的代理服务器,代理服务器再将请求转发到目标服务器,并将响应返回给前端。这种方式需要前端自行搭建和维护代理服务器。
PostMessage:
postMessage
是 HTML5 中新增的跨窗口通信方式。它允许在不同窗口(甚至不同域名)之间安全地传递消息。前端可以使用postMessage
方法向目标窗口发送消息,并通过监听message
事件来接收消息。跨域资源共享(Cross-Origin Resource Sharing): CORS 也可以用于非简单请求的跨域通信。对于复杂请求(如包含自定义头信息或使用 PUT、DELETE 等方法的请求),浏览器会先发送一个预检请求(OPTIONS 请求),服务器返回的响应中需要包含合适的 CORS 头信息来允许跨域请求。
这些跨域通信方式各有特点,可以根据具体的需求选择适合的方式来实现跨域数据交互。需要注意的是,跨域通信的安全性需要进行适当的控制和验证,以防止恶意的跨域请求和数据泄露。
能不能说一说浏览器的本地存储?各自优劣如何?
浏览器的本地存储是指浏览器提供的在客户端(用户设备)上存储数据的机制,用于在浏览器会话之间或在不同页面之间保持数据。以下是几种常见的浏览器本地存储方式及其优劣:
Cookies(Cookie):
- 优点:
- 历史悠久,广泛支持,适用于大多数浏览器。
- 可以存储较小的数据量(通常为 4KB),在每个 HTTP 请求中自动发送到服务器。
- 可以设置过期时间,实现持久化存储。
- 缺点:
- 每次请求都会将所有 Cookie 信息发送到服务器,增加数据传输量。
- 存储容量有限,每个域名下的 Cookie 数量和大小都受限制。
- 存储在 Cookie 中的数据可以被用户轻易修改和删除。
- 只能存储字符串类型的数据,需要手动进行编码和解码。
- 优点:
Web Storage(localStorage 和 sessionStorage):
- 优点:
- 可以在浏览器端存储较大的数据量(通常为 5MB 或更大)。
- API 简单易用,提供了 setItem、getItem、removeItem 等方法进行数据操作。
- 数据存储在浏览器中,不会自动发送到服务器,减少网络流量。
- 可以通过 localStorage 实现持久化存储,通过 sessionStorage 实现会话级别的存储。
- 缺点:
- 仅在同一浏览器中共享数据,在不同浏览器或设备上无法共享。
- 存储在 Web Storage 中的数据可以被用户修改,但不容易被其他网站获取。
- 只能存储字符串类型的数据,需要手动进行编码和解码。
- 优点:
IndexedDB:
- 优点:
- 支持存储大量的结构化数据,容量较大(通常为几百 MB 或更大)。
- 提供了丰富的查询功能,支持索引和事务操作。
- 数据存储在浏览器中,不会自动发送到服务器。
- 可以在后台进行异步操作,不会阻塞主线程。
- 缺点:
- API 较为复杂,学习和使用成本较高。
- 对于简单的数据存储需求,使用 IndexedDB 可能会显得繁琐。
- 兼容性较差,不同浏览器对 IndexedDB 的支持程度有所差异。
- 优点:
WebSQL:
- 优点:
- 基于 SQL 语法,易于处理结构化数据。
- 提供了事务支持和查询功能。
- 数据存储在浏览器中,不会自动发送到服务器。
- 缺点:
- 不再是 W3C 标准,已经被废弃,不再被新的浏览器支持。
- 兼容性差,不同浏览器之间存在差异。
- 优点:
总体而言,选择适合的浏览器本地存储方式取决于具体的需求。Cookies 适合存储少量数据和与服务器交互的场景。Web Storage 适合在不同页面之间存储较大的数据量。IndexedDB 适合存储大量结构化数据并进行复杂查询。需要注意的是,无论哪种存储方式,敏感信息仍需要进行加密和其他安全措施以保护数据的安全性。
如何实现一个 jsonp?
JSONP(JSON with Padding)是一种跨域数据请求的技术,它通过动态创建<script>
标签来实现跨域请求,并利用回调函数将数据传递回来。下面是一个简单的 JSONP 实现示例:
function jsonp(url, callback) {
// 生成唯一的回调函数名
const callbackName = "jsonp_callback_" + Math.floor(Math.random() * 100000);
// 创建一个全局的回调函数
window[callbackName] = function (data) {
// 在回调函数中执行用户定义的回调函数
callback(data);
// 删除全局的回调函数和相关的标签
delete window[callbackName];
script.parentNode.removeChild(script);
};
// 创建一个<script>标签
const script = document.createElement("script");
script.src = url + "&callback=" + callbackName;
document.body.appendChild(script);
}
上述代码定义了一个名为jsonp
的函数,它接收两个参数:URL 和回调函数。函数内部会生成一个唯一的回调函数名,然后创建一个全局的回调函数。接着,创建一个<script>
标签,并将 URL 和回调函数名作为查询参数添加到 URL 中,以便服务器返回数据时能够调用该回调函数。最后,将<script>
标签添加到页面中,浏览器会自动发送跨域请求。
当服务器返回数据时,会调用回调函数,并将数据作为参数传递给回调函数。在回调函数中,我们可以执行用户定义的回调函数,并在执行完毕后删除全局的回调函数和相关的<script>
标签,以清理资源。
使用该 JSONP 函数的示例:
jsonp("http://api.example.com/data", function (data) {
console.log(data);
});
在上述示例中,我们调用jsonp
函数,并传入服务器的 URL 和一个回调函数。当服务器返回数据时,回调函数会被执行,并将数据打印到控制台上。
需要注意的是,JSONP 存在一些安全性和可靠性的问题,因为它将信任服务器返回的脚本内容,并且无法处理错误和异常。在使用 JSONP 时,要确保请求的 URL 可信,并且服务器返回的数据是可靠的。另外,现代的 Web 开发中,推荐使用更为安全和可靠的跨域请求技术,如 CORS(跨域资源共享)和代理服务器等。
防抖和节流有什么区别?
防抖(debounce)和节流(throttle)是两种不同的处理函数,它们都可以用于限制函数执行的频率。防抖主要用于处理连续触发,而节流主要用于处理批量触发。
防抖(debounce)的原理是在一定时间内,如果函数连续触发,那么后面的触发会被抑制,直到一段时间后才会执行最新的触发。这样可以在一定时间内确保函数执行不超过一次。防抖通常用于处理一些不会产生副作用的函数,或者至少确保这些函数在短时间内只执行一次。
节流(throttle)的原理是在一定时间内,如果函数批量触发,那么只会执行第一个触发,其他的触发会被清除,直到下一次间隔满足条件时,才会执行下一个触发。这样可以在一定时间内确保函数执行不超过一次,但是与防抖不同,节流更适合处理一些可能会产生副作用的函数,或者可以确保在较短的时间内只执行一次。
区别:
- 防抖:函数在一定时间内只执行一次,无论触发次数多少。
- 节流:函数在一定时间内只执行一次,但触发次数超过一定值时会被清除。
什么是 Symbol?Symbol 的 key 有什么作用?
Symbol 是 ECMAScript 6 引入的一种新的基本数据类型。它表示一个独一无二的标识符,用于创建唯一的属性键(key)。
Symbol 的创建方式是通过调用全局的 Symbol 函数并传递一个可选的描述参数来实现的,例如:Symbol('myKey')
。描述参数只是一个可选的字符串,用于在调试和识别 Symbol 时提供一些可读性的信息,但它并不影响 Symbol 的唯一性。
Symbol 的主要特点是其值是唯一且不可变的,因此每个 Symbol 值都是独立的,不会与其他 Symbol 值相等,即使它们具有相同的描述参数。这使得 Symbol 在创建对象属性时非常有用,可以确保属性名的唯一性,避免命名冲突。
Symbol 的作用主要体现在以下几个方面:
创建唯一的属性键:Symbol 的主要用途是作为对象属性的键(key),用于确保属性名的唯一性。由于 Symbol 值是唯一的,因此可以避免属性名冲突,特别是在使用第三方库或多人协作开发时,可以有效避免不同模块之间的属性名冲突。
隐藏属性:由于 Symbol 值是唯一且不可变的,因此可以用作对象属性的私有标识符。通过使用 Symbol 作为属性键,可以隐藏属性,使其不容易被意外访问或覆盖。
定义常量:Symbol 值可以用作常量,因为它们是唯一的,不会与其他值相等。
定义内置符号:ECMAScript 中有一组内置的 Symbol 值,称为内置符号(Well-known Symbols),它们具有特殊的语义和行为。例如,
Symbol.iterator
用于定义可迭代对象的默认迭代器,Symbol.toStringTag
用于指定对象默认的字符串表示等。
总结来说,Symbol 是一种唯一且不可变的数据类型,用于创建独一无二的属性键,确保属性名的唯一性,并提供其他一些特殊的语义和行为。它在防止属性名冲突、隐藏属性和定义常量等方面具有重要的作用。
如果两个 symbol 的 key 相同,symbol 相同吗
不,两个 Symbol 的 key 相同并不意味着它们是相同的 Symbol。
Symbol 是一种唯一且不可变的数据类型,每个 Symbol 值都是独一无二的。即使两个 Symbol 的 key 相同,它们仍然是不同的 Symbol 实例。
例如,考虑以下示例:
const symbol1 = Symbol("myKey");
const symbol2 = Symbol("myKey");
console.log(symbol1 === symbol2); // false
在上述示例中,我们创建了两个具有相同 key 值('myKey')的 Symbol 实例。尽管它们的 key 相同,但它们仍然是两个不同的 Symbol 实例,因此比较它们的严格相等性(===)返回的结果是false
。
Symbol 的唯一性使得它在创建对象属性的时候非常有用,可以确保属性名的唯一性,避免命名冲突。但是要注意,相同的 Symbol key 只是标识符,不保证其实例的相等性。
前端需要注意哪些 SEO
前端在开发过程中,可以采取一些措施来优化网站的搜索引擎优化(SEO)。以下是前端需要注意的一些 SEO 方面:
网页结构和语义化: 使用合适的 HTML 标签和正确的结构来组织网页内容。使用语义化的标签(如
<header>
、<nav>
、<article>
等)来明确页面的结构和内容,有助于搜索引擎理解和索引网页。关键词优化: 在网页的标题、描述、标签、正文等位置合理地使用关键词,这有助于搜索引擎了解页面的主题和内容。但要注意避免过度使用关键词,以免被搜索引擎认为是垃圾信息或作弊行为。
URL 优化: 使用简洁、有意义的 URL,包含相关关键词,便于搜索引擎和用户理解页面内容。避免使用过长、含有特殊字符或无意义的 URL。
页面加载速度: 优化网页的加载速度,包括压缩和缓存静态资源、合理使用图片、减少 HTTP 请求数量等。快速加载的网页对搜索排名和用户体验都有积极影响。
移动友好性: 确保网站在移动设备上具有良好的显示和用户体验。移动友好性对搜索引擎排名越来越重要,而且符合移动设备的响应式设计有助于提升用户满意度。
合理的内部链接和导航: 在网站内部进行合理的链接和导航设置,使搜索引擎能够更好地爬行和索引网站的页面。使用相关的锚文本和链接结构可以提高页面的权重和相关性。
合理的标记和描述: 使用合适的标记(如标题标签、段落标签等)来突出重要内容,并编写有吸引力的页面描述(Meta Description),这有助于提高点击率和搜索引擎对页面的理解。
良好的用户体验: 提供良好的用户体验对 SEO 很重要。确保网页易于导航、内容易读、布局合理,并且有良好的交互性和响应性。用户满意度对搜索引擎排名有一定的影响。
合理的社交媒体整合: 在网站上合理整合社交媒体,包括分享按钮、嵌入社交媒体内容等,可以增加网站的曝光度和流量。
合理的页面标题和描述: 为每个页面设置唯一、相关的标题和描述,使其能够准确地描述页面内容,这有助于搜索引擎对页面的理解和排名。
以上是一些前端需要注意的 SEO 方面。然而,SEO 是一个综合性的工作,除了前端方面,还需要考虑后端、内容质量、外部链接等因素。因此,综合全面的 SEO 策略是提升网站在搜索引擎中排名的关键。
实现一个页面操作不会整页刷新的网站,并且能在浏览器前进、后 退时正确响应。给出你的技术实现方案?
要实现一个页面操作不会整页刷新的网站,并且能够正确响应浏览器的前进和后退操作,可以采用以下技术实现方案:
使用 AJAX 进行异步加载: 使用 AJAX(Asynchronous JavaScript and XML)技术,通过 JavaScript 向服务器发送异步请求,获取数据或更新页面的部分内容,而不需要整页刷新。可以使用原生的 XMLHttpRequest 对象或现代的 fetch API 来实现 AJAX 请求。
使用前端框架或库: 借助流行的前端框架或库,如 React、Angular 或 Vue.js 等,可以更方便地实现页面局部刷新。这些框架提供了组件化的开发方式,可以将页面划分为多个独立的组件,在需要更新时只更新相应的组件,而不会影响其他部分。
使用历史记录管理: 使用浏览器的 History API,可以在页面操作时管理浏览器的历史记录。通过操作浏览器的历史记录,可以实现在浏览器前进和后退时正确响应,而不需要整页刷新。可以使用
pushState()
方法将新的 URL 添加到历史记录中,并使用popstate
事件监听浏览器的前进和后退操作。使用单页应用(Single-Page Application)架构: 单页应用是一种通过动态地更新页面的部分内容来模拟多页面效果的应用。在单页应用中,页面的切换和内容更新是通过 AJAX 加载和前端路由管理实现的,不需要整页刷新。可以使用前端框架如 React、Angular 或 Vue.js 来构建单页应用。
合理使用缓存: 对于一些不经常变化的数据或资源,可以使用浏览器缓存机制,通过设置适当的缓存策略和 HTTP 头信息,使浏览器在后续请求时直接从缓存中获取数据,减少对服务器的请求,提高页面加载速度。
综合使用以上技术实现方案,可以构建一个页面操作不会整页刷新的网站,并能够正确响应浏览器的前进和后退操作。这样的网站能够提供更流畅的用户体验,并提高网站的性能和可用性。
Reflect 对象创建目的?
Reflect 对象是 JavaScript 的内置对象之一,它提供了一组用于操作对象的方法。Reflect 对象的创建目的是为了统一和提供一种更简洁、一致的方式来执行常见的对象操作,以替代一些原来分散在其他对象上的操作方法。
Reflect 对象的创建目的可以总结如下:
提供统一的对象操作方法: Reflect 对象提供了一组与对象操作相关的方法,如读取属性、设置属性、调用函数等。这些方法的设计和用法都经过了统一和简化,使得开发者在处理对象操作时可以更加一致和直观。
替代部分 Object 对象上的方法: Reflect 对象可以替代部分 Object 对象上的方法,例如
Reflect.get()
可以替代obj[property]
访问属性的方式,Reflect.set()
可以替代obj[property] = value
设置属性的方式,等等。通过使用 Reflect 对象的方法,可以使代码更加简洁和易读。支持面向对象编程的元编程操作: Reflect 对象提供了一些元编程操作的方法,使得开发者可以在运行时动态地操作对象,例如创建、修改、扩展对象等。这种元编程能力有助于实现高级的编程模式和功能。
提供一些无法直接通过语言语法实现的操作: Reflect 对象提供了一些无法直接通过语言语法实现的对象操作,例如
Reflect.construct()
用于实例化对象,Reflect.apply()
用于调用函数,Reflect.defineProperty()
用于定义属性等。这些方法扩展了语言的功能,使得开发者能够进行更灵活和强大的对象操作。
总的来说,Reflect 对象的创建目的是为了提供一组统一、简洁、一致的对象操作方法,使得开发者能够更方便地处理对象的读取、设置、调用等操作,并支持元编程和一些高级的对象操作需求。
内部属性 [[Class]]
是什么?
内部属性 [[Class]]
是 JavaScript 中每个对象内部都有的一个属性,它用于标识对象的类型信息。它的值是一个字符串,表示对象的内部类别。
[[Class]]
属性是内部使用的,不可直接访问或修改,但可以通过一些方式间接地获取对象的类别信息。
在规范中,[[Class]]
属性被用于区分不同对象的类型,帮助识别对象的种类。例如,[[Class]]
的值可以是 "Object"、"Array"、"Date"、"RegExp" 等,用于表示相应对象的类别。
通过内部属性 [[Class]]
,JavaScript 引擎可以确定对象的基本类型,并在进行类型检查、原型链查找等操作时判断对象的行为和特性。
需要注意的是,[[Class]]
属性只是规范中的概念,对于开发者来说,并没有直接的方式获取或操作该属性。开发者通常会使用其他方式来判断对象的类型,如使用 typeof
操作符、instanceof
运算符、Object.prototype.toString()
方法等。这些方法会基于 [[Class]]
属性或其他方式来提供对象类型的信息。
什么是堆?什么是栈?它们之间有什么区别和联系?
在计算机科学中,"堆"(Heap)和"栈"(Stack)是两个常用的术语,用于描述内存中的不同存储区域。
堆(Heap):
- 堆是一种动态分配的内存区域,用于存储动态创建的对象和数据结构。
- 堆的内存空间由操作系统分配和释放,通常比较大。
- 堆的数据存储方式是无序的,对象在堆中的分配和释放是通过动态内存管理函数(如
malloc()
和free()
)或垃圾回收机制来控制的。 - 堆中的对象可以在程序的任何地方被访问,而不受代码块或函数的限制。
栈(Stack):
- 栈是一种静态分配的内存区域,用于存储函数调用、局部变量和临时数据。
- 栈的内存空间由编译器自动分配和释放,大小通常有限。
- 栈的数据存储方式是有序的,采用先进后出(LIFO)的原则,即最后进栈的数据首先出栈。
- 栈中的数据只能通过入栈和出栈操作来访问,而访问的范围局限于代码块或函数的作用域。
区别和联系:
- 存储方式:堆存储无序的动态对象,栈存储有序的函数调用和局部变量。
- 内存管理:堆的内存由开发者手动管理或由垃圾回收机制自动管理,栈的内存由编译器自动分配和释放。
- 空间大小:堆的空间通常比较大,栈的空间大小有限。
- 数据访问:堆中的对象可以在程序的任何地方被访问,而栈中的数据只能在其作用域内被访问。
- 数据存储方式:堆中的数据存储是无序的,栈中的数据存储是有序的。
堆和栈在内存管理和数据存储方式上有明显的区别。堆用于存储动态分配的对象,对内存的操作相对灵活;而栈用于存储函数调用和局部变量,对内存的操作相对简单且有限。两者在程序中发挥不同的作用,相互补充和支持。
isNaN 和 Number.isNaN 函数的区别?
isNaN
和 Number.isNaN
都是 JavaScript 中用于判断一个值是否为 NaN(Not-a-Number)的函数,但它们在行为上有一些区别。
isNaN:
isNaN
是全局函数,在早期版本的 JavaScript 中存在。- 它接受一个参数,将该参数转换为数字并检查是否为 NaN。
- 如果参数无法转换为数字,或者转换后的结果是 NaN,则返回
true
,否则返回false
。 - 在判断之前,
isNaN
会先尝试将参数转换为数字类型,可能会导致一些意外的结果。
Number.isNaN:
Number.isNaN
是一个静态方法,从 ECMAScript 2015(ES6)开始引入。- 它接受一个参数,严格检查该参数是否为 NaN。
- 只有当参数的值是 NaN 时,才返回
true
,否则返回false
。 Number.isNaN
不会进行任何类型转换,只有在参数的值是 NaN 时才返回true
,其他情况下都返回false
。
主要区别:
isNaN
会尝试将参数转换为数字类型,可能导致意外的结果,而Number.isNaN
不会进行任何类型转换。- 对于非数字类型的参数(如字符串、布尔值、对象等),
isNaN
会将其转换为数字,然后进行判断,而Number.isNaN
会直接返回false
。 isNaN
是全局函数,而Number.isNaN
是Number
对象的静态方法,因此调用方式不同。
示例:
isNaN("123"); // true,"123" 被转换为数字 123,不是 NaN
isNaN("hello"); // true,"hello" 无法被转换为数字,被视为 NaN
Number.isNaN("123"); // false,"123" 不是 NaN
Number.isNaN("hello"); // false,"hello" 不是 NaN
总之,使用 Number.isNaN
更可靠,因为它不会进行类型转换,只有在参数的值是 NaN 的情况下才返回 true
。
什么情况下会发生布尔值的隐式强制类型转换?
在 JavaScript 中,布尔值的隐式强制类型转换会在以下情况下发生:
逻辑运算符: 当使用逻辑运算符(如逻辑与
&&
、逻辑或||
、逻辑非!
)时,操作数会被隐式转换为布尔值。- 对于逻辑与
&&
和逻辑或||
,如果操作数不是布尔值,则会先将其转换为布尔值,然后根据逻辑运算的规则返回结果。 - 对于逻辑非
!
,操作数会被直接转换为布尔值,并返回相反的结果。
- 对于逻辑与
条件语句: 在条件语句(如
if
语句、三元表达式)中,条件表达式的结果会被隐式转换为布尔值。- 如果条件表达式的值不是布尔值,则会先将其转换为布尔值,然后根据转换后的结果执行相应的代码块。
比较操作符: 在使用比较操作符(如相等比较
==
、严格相等比较===
、大于>
、小于<
等)进行比较时,操作数可能会被隐式转换为布尔值。- 某些比较操作符会进行类型转换,将操作数转换为相同的类型后再进行比较。例如,使用相等比较
==
时,如果操作数的类型不同,会进行类型转换后再进行比较。
- 某些比较操作符会进行类型转换,将操作数转换为相同的类型后再进行比较。例如,使用相等比较
需要注意的是,隐式类型转换可能会导致一些意想不到的结果和错误,因此在编写代码时要注意类型的一致性,避免依赖于隐式类型转换。建议使用显式的类型转换操作符(如 Boolean()
、Number()
、String()
)来明确地进行类型转换,以提高代码的可读性和可维护性。
undefined 与 undeclared 的区别?
在 JavaScript 中,"undefined" 和 "undeclared" 是两个不同的概念。
Undefined(未定义):
- "Undefined" 是一个特殊的值,在 JavaScript 中表示一个未初始化的变量或未赋值的属性。
- 当一个变量被声明但未被赋予任何值时,它的值被默认设置为 "undefined"。
- 未定义的变量在访问时会返回 "undefined"。例如:
let x;
console.log(x); // 输出: undefined
Undeclared(未声明):
- "Undeclared" 指的是在当前作用域中没有声明的变量。
- 当我们尝试访问一个未在当前作用域中声明的变量时,JavaScript 引擎会抛出一个 "ReferenceError"。
- 未声明变量的访问会被认为是一个错误,因为引擎无法找到该变量的定义。
console.log(y); // 抛出 ReferenceError: y is not defined
区别:
- "Undefined" 是一个特殊的值,表示变量已声明但未赋值,或者属性存在但未定义值。
- "Undeclared" 指的是在当前作用域中根本没有声明的变量。
- 对于未定义的变量,访问时会返回 "undefined";而对于未声明的变量,访问会抛出 "ReferenceError"。
在编程中,建议始终声明变量,并为其赋予适当的值,以避免出现未定义或未声明的情况。使用严格模式("use strict")可以帮助捕获未声明变量的错误,提高代码的健壮性。
如何封装一个 javascript 的类型判断函数?
要封装一个 JavaScript 的类型判断函数,可以使用以下方法:
function getType(value) {
const type = typeof value;
if (type !== "object") {
// 基本类型(除了 null)
return type;
}
if (value === null) {
// null
return "null";
}
// 复杂类型的判断
const objectType = Object.prototype.toString.call(value);
// objectType 的形式为 "[object Type]"
// 截取中间的 Type 部分并转为小写
const typeString = objectType.slice(8, -1).toLowerCase();
return typeString;
}
使用示例:
console.log(getType(42)); // "number"
console.log(getType("Hello")); // "string"
console.log(getType(true)); // "boolean"
console.log(getType(null)); // "null"
console.log(getType(undefined)); // "undefined"
console.log(getType([])); // "array"
console.log(getType({})); // "object"
console.log(getType(new Date())); // "date"
console.log(getType(/regex/)); // "regexp"
上述代码中,getType
函数接受一个参数 value
,并根据其类型返回相应的字符串。基本类型直接使用 typeof
运算符判断,而复杂类型使用 Object.prototype.toString
方法获取对象的内部属性 [[Class]]
,并截取出具体的类型信息。
需要注意的是,上述封装函数是一种简化的实现方式,但并不是完全准确。JavaScript 中的类型判断是一个复杂的主题,涉及到原始值、对象、函数、数组等不同类型的细分。如果需要更精确和全面的类型判断,可以考虑使用第三方库,如 lodash
或 typeof
等。
作用域和作用域链
作用域(Scope)是指在程序中定义变量的区域,它决定了变量的可见性和生命周期。在不同的编程语言中,作用域的规则可能会有所不同,但基本概念是相通的。
作用域链(Scope Chain)是指变量查找时的一种机制,它是由多个嵌套的作用域组成的链式结构。当访问一个变量时,解释器会沿着作用域链逐级查找,直到找到该变量或者到达全局作用域为止。
下面是作用域和作用域链的一些关键概念和特点:
1. 作用域的类型:
- 全局作用域(Global Scope): 全局作用域是在程序中任何地方都可访问的作用域。在浏览器环境中,全局作用域通常是指
window
对象。 - 局部作用域(Local Scope): 局部作用域是在特定代码块(如函数内部)中定义的作用域。局部作用域中的变量只能在该代码块内部访问。
2. 作用域链的形成:
- 当一个变量在某个作用域中被引用时,解释器会先在当前作用域中查找该变量。
- 如果在当前作用域中找不到该变量,解释器会沿着作用域链向上查找,逐级检查外层作用域,直到找到该变量或者到达全局作用域。
- 作用域链的形成是由代码中的嵌套关系决定的,内部作用域可以访问外部作用域的变量,但外部作用域不能访问内部作用域的变量。
3. 作用域链的特点:
- 作用域链是静态的,它在函数定义时就确定了,不会随着函数的调用而变化。
- 内部作用域可以访问外部作用域的变量,但外部作用域不能直接访问内部作用域的变量。
- 如果两个作用域中存在同名的变量,内部作用域的变量会屏蔽外部作用域的变量。
作用域和作用域链在编程中起着重要的作用。它们帮助我们理解变量的可见性和访问规则,避免命名冲突和变量溢出的问题。正确理解和使用作用域和作用域链对于编写可维护和可扩展的代码非常重要。
setTimeout、Promise、Async/Await 的区别
setTimeout
、Promise
和async/await
是 JavaScript 中用于处理异步操作的三种常见方式,它们在语法和用法上有一些区别。
1. setTimeout:
setTimeout
是一个由浏览器和 Node.js 提供的函数,用于在指定时间后执行一段代码。
- 它是基于回调函数的方式来处理异步操作。
- 通过设置一个定时器,在指定的时间间隔后触发回调函数的执行。
- 不会阻塞主线程,定时器会在指定的时间间隔后将回调函数推入事件队列中。
setTimeout(() => {
// 在指定时间后执行的代码
}, delay);
2. Promise:
Promise
是 ES6 引入的一种处理异步操作的机制,它提供了更优雅的方式来处理异步代码。
- 它是基于 Promise 对象的状态(pending、fulfilled、rejected)和状态改变时的回调函数来处理异步操作。
- 可以链式调用
then
方法注册处理成功的回调函数,以及catch
方法注册处理失败的回调函数。 - 可以使用
Promise.resolve
和Promise.reject
创建已解决或已拒绝的 Promise 对象。 - 可以使用
Promise.all
和Promise.race
处理多个异步操作的并行或竞争。
const promise = new Promise((resolve, reject) => {
// 异步操作
if (/* 异步操作成功 */) {
resolve(result); // 触发成功状态
} else {
reject(error); // 触发失败状态
}
});
promise
.then(result => {
// 处理成功状态
})
.catch(error => {
// 处理失败状态
});
3. Async/Await:
async/await
是 ES8 引入的一种基于 Promise 的异步编程语法糖,使异步代码看起来更像同步代码。
async
函数用于定义一个返回 Promise 对象的异步函数。await
关键字用于等待一个 Promise 对象的解决,并暂停函数的执行,直到 Promise 对象解决或拒绝。- 可以使用
try/catch
块来捕获和处理 Promise 对象的拒绝。
async function myFunction() {
try {
const result = await asyncOperation(); // 等待异步操作的完成
// 处理成功返回的结果
} catch (error) {
// 处理异步操作的错误
}
}
区别总结:
setTimeout
是最早引入的处理异步代码的方式,基于回调函数。Promise
是 ES6 引入的处理异步代码的机制,通过 Promise 对象的状态和回调函数来处理。async/await
是 ES8 引入的基于 Promise 的异步编程语法糖,使异步代码更像同步代码。
Promise
和 async/await
都是基于 Promise 对象的方式来处理异步操作,相对于 setTimeout
更加灵活和可读性强。async/await
在写法上更加直观和简洁,适用于需要顺序执行异步操作的场景。
Async/Await 如何通过同步的方式实现异步
Async/Await 就是一个自执行的 generate 函数。利用 generate 函数的特性把异步的代码写成“同步”的形式,第一个请求的返回值作为后面一个请求的参数,其中每一个参数都是一个 promise 对象.
common.js 和 es6 中模块引入的区别?
CommonJS 和 ES6 模块是在不同的 JavaScript 环境中使用的两种模块引入规范,它们具有一些区别:
语法差异: CommonJS 使用
require()
函数和module.exports
对象来引入和导出模块,而 ES6 模块使用import
和export
关键字来完成相同的操作。CommonJS 引入模块的语法:
const module = require("module");
const { namedExport } = require("module");
module.exports = value;ES6 模块引入的语法:
import module from "module";
import { namedExport } from "module";
export default value;运行时加载 vs 静态加载: CommonJS 模块是在运行时加载的,即在代码执行过程中动态地加载和执行模块。而 ES6 模块是在静态编译阶段进行加载的,编译器可以在编译时静态分析模块的依赖关系。
这意味着在 CommonJS 中,模块的加载是同步的,它会阻塞后续代码的执行,直到模块加载完成。而在 ES6 模块中,模块的加载是异步的,不会阻塞后续代码的执行。
模块作用域: CommonJS 模块在运行时会创建一个模块作用域,并且模块中的变量和函数默认是私有的,需要通过
module.exports
或exports
显式导出。在引入模块时,会拷贝模块的值或引用。ES6 模块使用严格的模块作用域,每个模块都有自己的作用域,并且模块中的变量和函数默认是私有的,需要通过
export
显式导出。在引入模块时,会创建一个只读的引用。浏览器支持: CommonJS 模块主要用于服务器端的 JavaScript 运行环境(如 Node.js),而 ES6 模块是浏览器原生支持的模块系统。大多数现代浏览器已经支持 ES6 模块,而不需要使用工具进行转换。
需要注意的是,由于 CommonJS 和 ES6 模块有不同的语法和加载机制,它们在使用时需要根据具体的环境和需求来选择合适的模块引入方式。在现代的前端开发中,ES6 模块已经成为主流,特别是在使用工具链(如 Webpack、Babel 等)进行构建和打包的项目中。
cookie token 和 session 的区别
Cookie、Token 和 Session 是在 Web 应用中用于身份验证和状态管理的常见机制,它们有以下区别:
存储位置: Cookie 是在客户端(浏览器)存储的小型文本文件,通过 HTTP 响应头中的 Set-Cookie 头部发送给客户端并存储在浏览器中。Token 通常是在客户端存储的,可以存储在浏览器的 LocalStorage、SessionStorage 或者在移动应用中的本地存储等。Session 则是在服务器端存储的,通常以服务器的内存、数据库或缓存等形式存储。
数据内容: Cookie 是包含在 HTTP 请求头中发送给服务器的键值对,可以存储少量的用户信息。Token 是一串加密的字符串,通常包含了用户的身份认证信息和其他的相关数据,比如权限、过期时间等。Session 是服务器端存储的用户会话信息,可以包含用户的身份认证信息、状态和其他相关数据。
安全性: Cookie 相对较不安全,因为它存储在客户端,容易受到跨站脚本攻击(XSS)和跨站请求伪造(CSRF)等攻击。Token 可以使用签名和加密等方式增加安全性,并且可以通过 HTTPS 传输以保护数据。Session 存储在服务器端,相对较安全,但仍然需要注意保护会话 ID,并采取适当的安全措施。
无状态 vs 有状态: Cookie 和 Session 是有状态的机制,服务器需要在每个请求中根据会话 ID 来识别用户状态。而 Token 是无状态的,服务器不需要存储任何会话信息,只需验证 Token 的有效性即可。这使得 Token 更适合于分布式系统和服务端无状态的 API 设计。
跨域支持: Cookie 在同源策略下可以自动发送,但在跨域请求时需要设置特定的响应头部。Token 可以通过 HTTP 请求头(如 Authorization 头)或请求参数传递,可以更灵活地在跨域环境中使用。
需要根据具体的应用场景和需求选择合适的机制。Cookie 通常用于存储少量的用户信息和实现会话跟踪,Token 则适用于分布式系统和无状态的 API 设计。Session 则适用于需要在服务器端存储大量用户会话数据和状态的情况。
如何选择图片格式,例如 png, webp
图片格式 | 压缩方式 | 透明度 | 动画 | 浏览器兼容 | 适应场景 |
---|---|---|---|---|---|
JPEG | 有损压缩 | 不支持 | 不支持 | 所有 | 复杂颜色及形状、尤其是照片 |
GIF | 无损压缩 | 支持 | 支持 | 所有 | 简单颜色,动画 |
PNG | 无损压缩 | 支持 | 不支持 | 所有 | 需要透明时 |
APNG | 无损压缩 | 支持 | 支持 | FirefoxSafariiOS Safari | 需要半透明效果的动画 |
WebP | 有损压缩 | 支持 | 支持 | ChromeOperaAndroid ChromeAndroid Browser | 复杂颜色及形状浏览器平台可预知 |
SVG | 无损压缩 | 支持 | 支持 | 所有(IE8 以上) | 简单图形,需要良好的放缩体验需要动态控制图片特效 |
Symbol.toStringTag 有什么用
Symbol.toStringTag
的主要作用是自定义对象在调用 toString()
方法时返回的字符串表示。
默认情况下,对象的 toString()
方法返回的是 "[object Object]",并没有提供有关对象类型的详细信息。通过使用 Symbol.toStringTag
,我们可以自定义对象的 toString()
方法返回的字符串,以提供更有用和可读性的信息。
例如,假设我们有一个自定义的类 MyClass
:
class MyClass {
constructor() {
// constructor code
}
[Symbol.toStringTag]() {
return "MyClass";
}
}
const obj = new MyClass();
console.log(obj.toString()); // 输出 "MyClass"
在上述示例中,通过在 MyClass
类中实现 Symbol.toStringTag
方法,我们可以自定义对象在调用 toString()
方法时返回的字符串,使之更加描述性。
Symbol.toStringTag
对于调试和输出对象时提供了更好的可读性和理解性。通过自定义返回的字符串,我们可以更清晰地了解对象的类型或身份,有助于代码的调试和维护。
需要注意的是,并非所有对象都支持 Symbol.toStringTag
,只有部分内置对象(例如 Array
、Date
、RegExp
等)以及一些内置类(例如 Promise
、Generator
)支持它。同时,我们也可以自定义类和对象来使用 Symbol.toStringTag
。
如何判断 0.1 + 0.2 与 0.3 相等?
前言
0.1 + 0.2 是否等于 0.3 作为一道经典的面试题,已经广外熟知,说起原因,大家能回答出这是浮点数精度问题导致,也能辩证的看待这并非是 ECMAScript 这门语言的问题,今天就是具体看一下背后的原因。
数字类型
ECMAScript 中的 Number 类型使用 IEEE754 标准来表示整数和浮点数值。所谓 IEEE754 标准,全称 IEEE 二进制浮点数算术标准,这个标准定义了表示浮点数的格式等内容。
在 IEEE754 中,规定了四种表示浮点数值的方式:单精确度(32 位)、双精确度(64 位)、延伸单精确度、与延伸双精确度。像 ECMAScript 采用的就是双精确度,也就是说,会用 64 位来储存一个浮点数。
浮点数转二进制
我们来看下 1020 用十进制的表示:
1020 = 1 10^3 + 0 10^2 + 2 10^1 + 0 10^0
所以 1020 用十进制表示就是 1020……(哈哈)
如果 1020 用二进制来表示呢?
1020 = 1 2^9 + 1 2^8 + 1 2^7 + 1 2^6 + 1 2^5 + 1 2^4 + 1 2^3 + 1 2^2 + 0 2^1 + 0 2^0
所以 1020 的二进制为 1111111100
那如果是 0.75 用二进制表示呢?同理应该是:
0.75 = a 2^-1 + b 2^-2 + c 2^-3 + d 2^-4 + ...
因为使用的是二进制,这里的 abcd……的值的要么是 0 要么是 1。
那怎么算出 abcd…… 的值呢,我们可以两边不停的乘以 2 算出来,解法如下:
0.75 = a 2^-1 + b 2^-2 + c 2^-3 + d 2^-4...
两边同时乘以 2
1 + 0.5 = a 2^0 + b 2^-1 + c 2^-2 + d 2^-3... (所以 a = 1)
剩下的:
0.5 = b 2^-1 + c 2^-2 + d * 2^-3...
再同时乘以 2
1 + 0 = b 2^0 + c 2^-2 + d * 2^-3... (所以 b = 1)
所以 0.75 用二进制表示就是 0.ab,也就是 0.11
然而不是所有的数都像 0.75 这么好算,我们来算下 0.1:
0.1 = a * 2^-1 + b * 2^-2 + c * 2^-3 + d * 2^-4 + ...
0 + 0.2 = a * 2^0 + b * 2^-1 + c * 2^-2 + ... (a = 0)
0 + 0.4 = b * 2^0 + c * 2^-1 + d * 2^-2 + ... (b = 0)
0 + 0.8 = c * 2^0 + d * 2^-1 + e * 2^-2 + ... (c = 0)
1 + 0.6 = d * 2^0 + e * 2^-1 + f * 2^-2 + ... (d = 1)
1 + 0.2 = e * 2^0 + f * 2^-1 + g * 2^-2 + ... (e = 1)
0 + 0.4 = f * 2^0 + g * 2^-1 + h * 2^-2 + ... (f = 0)
0 + 0.8 = g * 2^0 + h * 2^-1 + i * 2^-2 + ... (g = 0)
1 + 0.6 = h * 2^0 + i * 2^-1 + j * 2^-2 + ... (h = 1)
....
然后你就会发现,这个计算在不停的循环,所以 0.1 用二进制表示就是 0.00011001100110011……
浮点数的存储
虽然 0.1 转成二进制时是一个无限循环的数,但计算机总要储存吧,我们知道 ECMAScript 使用 64 位来储存一个浮点数,那具体是怎么储存的呢?这就要说回 IEEE754 这个标准了,毕竟是这个标准规定了存储的方式。
这个标准认为,一个浮点数 (Value) 可以这样表示:
Value = sign exponent fraction
看起来很抽象的样子,简单理解就是科学计数法……
比如 -1020,用科学计数法表示就是:
-1 10^3 1.02
sign 就是 -1,exponent 就是 10^3,fraction 就是 1.02
对于二进制也是一样,以 0.1 的二进制 0.00011001100110011…… 这个数来说:
可以表示为:
1 2^-4 1.1001100110011……
其中 sign 就是 1,exponent 就是 2^-4,fraction 就是 1.1001100110011……
而当只做二进制科学计数法的表示时,这个 Value 的表示可以再具体一点变成:
V = (-1)^S (1 + Fraction) 2^E
(如果所有的浮点数都可以这样表示,那么我们存储的时候就把这其中会变化的一些值存储起来就好了)
我们来一点点看:
(-1)^S
表示符号位,当 S = 0,V 为正数;当 S = 1,V 为负数。
再看 (1 + Fraction)
,这是因为所有的浮点数都可以表示为 1.xxxx * 2^xxx 的形式,前面的一定是 1.xxx,那干脆我们就不存储这个 1 了,直接存后面的 xxxxx 好了,这也就是 Fraction 的部分。
最后再看 2^E
如果是 1020.75,对应二进制数就是 1111111100.11,对应二进制科学计数法就是 1 1.11111110011 2^9,E 的值就是 9,而如果是 0.1 ,对应二进制是 1 1.1001100110011…… 2^-4, E 的值就是 -4,也就是说,E 既可能是负数,又可能是正数,那问题就来了,那我们该怎么储存这个 E 呢?
我们这样解决,假如我们用 8 位来存储 E 这个数,如果只有正数的话,储存的值的范围是 0 ~ 254,而如果要储存正负数的话,值的范围就是 -127~127,我们在存储的时候,把要存储的数字加上 127,这样当我们存 -127 的时候,我们存 0,当存 127 的时候,存 254,这样就解决了存负数的问题。对应的,当取值的时候,我们再减去 127。
所以呢,真到实际存储的时候,我们并不会直接存储 E,而是会存储 E + bias,当用 8 位的时候,这个 bias 就是 127。
所以,如果要存储一个浮点数,我们存 S 和 Fraction 和 E + bias 这三个值就好了,那具体要分配多少个位来存储这些数呢?IEEE754 给出了标准:
在这个标准下:
我们会用 1 位存储 S,0 表示正数,1 表示负数。
用 11 位存储 E + bias,对于 11 位来说,bias 的值是 2^(11-1) - 1,也就是 1023。
用 52 位存储 Fraction。
举个例子,就拿 0.1 来看,对应二进制是 1 1.1001100110011…… 2^-4, Sign 是 0,E + bias 是 -4 + 1023 = 1019,1019 用二进制表示是 1111111011,Fraction 是 1001100110011……
对应 64 位的完整表示就是:
0 01111111011 1001100110011001100110011001100110011001100110011010
同理, 0.2 表示的完整表示是:
0 01111111100 1001100110011001100110011001100110011001100110011010
所以当 0.1 存下来的时候,就已经发生了精度丢失,当我们用浮点数进行运算的时候,使用的其实是精度丢失后的数。
浮点数的运算
关于浮点数的运算,一般由以下五个步骤完成:对阶、尾数运算、规格化、舍入处理、溢出判断。我们来简单看一下 0.1 和 0.2 的计算。
首先是对阶,所谓对阶,就是把阶码调整为相同,比如 0.1 是 1.1001100110011…… * 2^-4
,阶码是 -4,而 0.2 就是 1.10011001100110...* 2^-3
,阶码是 -3,两个阶码不同,所以先调整为相同的阶码再进行计算,调整原则是小阶对大阶,也就是 0.1 的 -4 调整为 -3,对应变成 0.11001100110011…… * 2^-3
接下来是尾数计算:
0.1100110011001100110011001100110011001100110011001101
+ 1.1001100110011001100110011001100110011001100110011010
————————————————————————————————————————————————————————
10.0110011001100110011001100110011001100110011001100111
我们得到结果为 10.0110011001100110011001100110011001100110011001100111 * 2^-3
将这个结果处理一下,即结果规格化,变成 1.0011001100110011001100110011001100110011001100110011(1) * 2^-2
括号里的 1 意思是说计算后这个 1 超出了范围,所以要被舍弃了。
再然后是舍入,四舍五入对应到二进制中,就是 0 舍 1 入,因为我们要把括号里的 1 丢了,所以这里会进一,结果变成
1.0011001100110011001100110011001100110011001100110100 * 2^-2
本来还有一个溢出判断,因为这里不涉及,就不讲了。
所以最终的结果存成 64 位就是
0 01111111101 0011001100110011001100110011001100110011001100110100
将它转换为 10 进制数就得到 0.30000000000000004440892098500626
因为两次存储时的精度丢失加上一次运算时的精度丢失,最终导致了 0.1 + 0.2 !== 0.3
其他
// 十进制转二进制
parseFloat(0.1).toString(2);
=> "0.0001100110011001100110011001100110011001100110011001101"
// 二进制转十进制
parseInt(1100100,2)
=> 100
// 以指定的精度返回该数值对象的字符串表示
(0.1 + 0.2).toPrecision(21)
=> "0.300000000000000044409"
(0.3).toPrecision(21)
=> "0.299999999999999988898"
https://github.com/mqyqingfeng/Blog/issues/155#issue-581956811
JavaScript 是如何运行的?解释型语言和编译型语言的差异是什么?
JavaScript 是一种解释型语言,它在运行时由解释器逐行解释和执行代码。下面是 JavaScript 运行的一般过程:
解析(Parsing): JavaScript 代码首先由解析器进行词法分析和语法分析,将其转换为抽象语法树(AST)的形式。
编译与执行: 解析后的代码进入解释器的执行阶段。解释器会逐行解释代码并执行相应的操作。在执行过程中,解释器会根据需要动态地分配内存,并处理变量、函数调用、异常等。
即时编译(Just-in-Time Compilation,JIT): 为了提高性能,现代的 JavaScript 解释器通常使用即时编译技术。它会将热点代码(经常执行的代码)转换为机器码,并进行优化,以提高代码的执行速度。
解释型语言和编译型语言的主要差异在于代码执行的方式:
解释型语言: 解释型语言的代码在运行时逐行解释和执行,不需要显式的编译过程。解释器会逐行读取代码,并将其转换为机器码或字节码,然后执行。每次执行都需要解释器的参与,因此解释型语言的执行速度通常较慢。JavaScript 是一种典型的解释型语言。
编译型语言: 编译型语言的代码在运行之前需要先经过编译过程。编译器会将源代码一次性地转换为机器码,生成可执行文件。编译后的代码可以直接在目标平台上运行,无需再次进行翻译。由于编译过程的开销只发生一次,编译型语言的执行速度通常较快。C、C++ 是编译型语言的例子。
需要注意的是,现代的解释型语言通常会使用即时编译等技术来提高执行性能,使其在某些情况下接近或超过编译型语言的性能。此外,还存在混合型语言,如 Java、C# 等,它们既包含解释性质的部分,又包含编译性质的部分。
JavaScript 中的数组和函数在内存中是如何存储的?
① 数组,JS 里的数组主要就是 以连续内存形式存储的FixedArray
、以哈希表形式存储的HashTable
。
② 函数,函数属于引用数据类型,存储在堆中,在栈内存中只是存了一个地址来表示对堆内存中的引用。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体
ES6 Modules 相对于 CommonJS 的优势是什么?
- CommonJS 和 ES6 Module 都可以对引入的对象进行赋值,即对对象内部属性的值进行改变;
- CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。即 ES6 Module 只存只读,不能改变其值,具体点就是指针指向不能变;
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
- CommonJS 模块的 require()是同步加载模块,ES6 模块的 import 命令是异步加载,有一个独立的模块依赖的解析阶段。
- import 的接口是 read-only(只读状态),不能修改其变量值。 即不能修改其变量的指针指向,但可以改变变量内部指针指向,可以对 commonJS 对重新赋值(改变指针指向),但是对 ES6 Module 赋值会编译报错。
优势: CommonJS 加载的是一个对象(即 module.exports 属性),该对象只有在脚本运行完才会生成。而 ES6 Modules 不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成
。
如何处理浏览器中表单项的密码自动填充问题?
处理浏览器中表单项的密码自动填充问题可以采取以下方法:
- 禁用自动填充: 在涉及密码输入的表单项中,可以使用
autocomplete
属性来禁用浏览器的自动填充功能。将autocomplete
属性设置为 "off" 可以告诉浏览器不要自动填充该表单项。例如:
<input type="password" name="password" autocomplete="off" />
请注意,浏览器可能会忽略这个设置,因为它们有自己的自动填充策略。
- 隐藏密码字段: 可以使用两个密码字段,一个用于实际的密码输入,另一个用于隐藏实际密码输入框。这样,浏览器的自动填充功能将无法找到实际的密码输入框,从而避免自动填充。例如:
<input type="password" name="password" style="display:none;" />
<input type="password" name="password_fake" autocomplete="off" />
在后台处理时,可以获取 password_fake
字段的值而不是实际的密码字段。
- 使用随机的字段名称: 如果你的表单项名称在每次加载时都是随机生成的,浏览器的自动填充功能将无法识别这些字段,并且不会自动填充密码。例如:
<input type="password" name="4utYHg3" autocomplete="off" />
在后台处理时,需要通过随机字段名称来获取密码的值。
请注意,以上方法可以减少浏览器密码自动填充的影响,但并不能完全消除浏览器自动填充的问题,因为不同浏览器和版本可能有不同的自动填充行为。为了确保安全性,建议在敏感操作中使用其他身份验证机制,如多因素身份验证。
另外,尽管上述方法可以减少密码自动填充的问题,但它们可能会对用户体验产生一定的影响。在实施这些方法之前,应该权衡安全性和用户体验之间的平衡,并根据具体情况选择适当的方法。
Hash 和 History 路由的区别和优缺点?
hash
路由模式的实现主要是基于下面几个特性:
- URL 中 hash 值只是客户端的一种状态,也就是说当向服务器端发出请求时,hash 部分不会被发送;
- hash 值的改变,都会在浏览器的访问历史中增加一个记录。因此我们能通过浏览器的回退、前进按钮控制 hash 的切换;
- 可以通过 a 标签,并设置 href 属性,当用户点击这个标签后,URL 的 hash 值会发生改变;或者使用 JavaScript 来对 loaction.hash 进行赋值,改变 URL 的 hash 值;
- 我们可以使用 hashchange 事件来监听 hash 值的变化,从而对页面进行跳转(渲染)。
history
路由模式的实现主要基于存在下面几个特性:
- pushState 和 repalceState 两个 API 来操作实现 URL 的变化 ;
- 我们可以使用 popstate 事件来监听 url 的变化,从而对页面进行跳转(渲染);
- history.pushState() 或 history.replaceState() 不会触发 popstate 事件,这时我们需要手动触发页面跳转(渲染)。
JavaScript 中的 const 数组可以进行 push 操作吗?为什么?
在 JavaScript 中,使用 const
声明的数组是具有恒定引用的常量。这意味着一旦数组被赋值,它的引用将保持不变,无法重新分配给其他数组或其他类型的值。虽然 const
声明的数组本身是不可变的,但数组中的元素可以被修改。
因此,尽管不能重新分配整个数组,但可以使用 const
声明的数组进行 push 操作来添加新元素。push
方法是向数组末尾添加元素的常用方法,它会直接修改数组本身,而不会改变数组的引用。
以下是一个使用 const
声明的数组进行 push 操作的示例:
const numbers = [1, 2, 3];
numbers.push(4); // 添加新元素到数组末尾
console.log(numbers); // 输出: [1, 2, 3, 4]
在上述示例中,虽然 numbers
是使用 const
声明的常量数组,但我们可以使用 push
方法将新元素 4 添加到数组末尾。这是因为 push
方法修改的是数组本身,而不是重新分配一个新的数组。
需要注意的是,尽管可以对 const
数组使用 push
方法,但不能对 const
数组重新赋值为另一个数组。例如,以下操作将导致错误:
const numbers = [1, 2, 3];
numbers = [4, 5, 6]; // 错误,无法重新赋值给 const 数组
总结起来,const
声明的数组是具有恒定引用的常量,不允许重新分配给其他数组,但可以使用 push
方法修改数组本身,向其添加新元素。
JavaScript 中对象的属性描述符有哪些?分别有什么作用?
在 JavaScript 中,对象的属性描述符用于描述和定义对象属性的特性。每个属性都有一个相关联的属性描述符对象,该对象包含了属性的各种特性和行为。下面是属性描述符的几个常见属性:
value(值): 属性的值。可以是任何有效的 JavaScript 值。默认为
undefined
。writable(可写): 指示属性是否可被修改。如果设置为
true
,则属性的值可以被赋值运算符改变。默认为true
。enumerable(可枚举): 指示属性是否可通过
for...in
循环或Object.keys()
方法进行枚举。如果设置为true
,则属性可以被枚举。默认为true
。configurable(可配置): 指示属性是否可以被删除,以及属性描述符是否可以被修改。如果设置为
true
,则属性可以被删除或修改。默认为true
。get(获取器函数): 一个函数,用于获取属性的值。当访问属性时,会调用该函数,并返回其返回值作为属性的值。
set(设置器函数): 一个函数,用于设置属性的值。当给属性赋值时,会调用该函数,并传递新的值作为参数。
这些属性描述符可以通过 Object.defineProperty()
方法或对象字面量的方式进行定义和修改。例如:
// 使用 Object.defineProperty() 定义属性描述符
Object.defineProperty(obj, "propertyName", {
value: "propertyValue",
writable: false,
enumerable: true,
configurable: false,
});
// 对象字面量的方式定义属性描述符
const obj = {
propertyName: {
value: "propertyValue",
writable: false,
enumerable: true,
configurable: false,
},
};
通过使用属性描述符,可以更精确地定义对象属性的行为和特性。例如,可以创建只读属性、控制属性的枚举性,以及限制对属性的修改和删除。这样可以提供更严格的对象属性访问和管理。
JavaScript 中 console 有哪些 api ?
JavaScript 中的 console 对象提供了一组用于在开发过程中进行调试和输出信息的 API。下面是一些常用的 console API:
console.log(): 输出普通信息到控制台。
console.error(): 输出错误信息到控制台,通常用于显示错误消息。
console.warn(): 输出警告信息到控制台,用于显示警告消息。
console.info(): 输出一般信息到控制台,通常用于显示一般性的提示信息。
console.debug(): 输出调试信息到控制台,用于显示调试相关的消息。
console.clear(): 清除控制台上的所有输出。
console.table(): 以表格形式在控制台中显示数组或对象的内容。
console.assert(): 对一个表达式进行评估,如果结果为 false,则输出错误消息到控制台。
console.count(): 记录特定标签被调用的次数,并将次数输出到控制台。
console.time() 和 console.timeEnd(): 用于计算代码块的执行时间。
console.group() 和 console.groupEnd(): 在控制台中创建一个分组,用于组织和显示相关的日志信息。
这只是一小部分常用的 console API,还有其他一些方法和属性可用于进行更高级的调试和日志记录。console 对象是开发过程中非常有用的工具,可以帮助开发人员进行调试、输出信息和监控代码执行。
Object.defineProperty 和 ES6 的 Proxy 有什么区别?
Proxy 的优势如下
- Proxy 可以直接监听整个对象而非属性。
- Proxy 可以直接监听数组的变化。
- Proxy 有 13 中拦截方法,如 ownKeys、deleteProperty、has 等是 Object.defineProperty 不具备的。
- Proxy 返回的是一个新对象,我们可以只操作新的对象达到目的,而 Object.defineProperty 只能遍历对象属性直接修改;
- Proxy 做为新标准将受到浏览器产商重点持续的性能优化,也就是传说中的新标准的性能红利。
Object.defineProperty 的优势如下
- 兼容性好,支持 IE9,而 Proxy 的存在浏览器兼容性问题,而且无法用 polyfill 磨平。
Object.defineProperty 不足在于:
- Object.defineProperty 只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。
- Object.defineProperty 不能监听数组。是通过重写数据的那 7 个可以改变数据的方法来对数组进行监听的。
- Object.defineProperty 也不能对 es6 新产生的 Map,Set 这些数据结构做出监听。
- Object.defineProperty 也不能监听新增和删除操作,通过 Vue.set()和 Vue.delete 来实现响应式的。
什么是事件代理(事件委托) 有什么好处
事件代理(也称为事件委托)是一种在 JavaScript 中处理事件的技术。它的工作原理是将事件监听器添加到父元素,而不是直接添加到实际的目标元素。当事件触发时,事件会冒泡到父元素,父元素可以处理该事件。
事件代理有以下好处:
内存效率:由于事件处理器是在父级元素上注册的,因此无论子元素的数量如何,都只需要一个处理器。这可以显著减少事件处理器的数量,从而提高内存效率。
动态元素:如果你的页面上有新的元素被动态添加,那么你不需要为这些新元素添加新的事件处理器。父元素的事件处理器会自动处理这些新元素的事件。
代码简洁:使用事件代理,你可以减少需要管理的事件处理器的数量,使代码更加简洁。
这是一个事件代理的示例代码:
document
.querySelector("#parent-element")
.addEventListener("click", function (event) {
if (event.target.matches("#child-element")) {
// 处理事件
}
});
在这个例子中,我们在#parent-element
上添加了一个事件监听器,而不是直接在#child-element
上添加。当#child-element
被点击时,事件会冒泡到#parent-element
,然后我们可以在那里处理事件。这就是事件代理的工作原理。希望这个解释对你有所帮助!
addEventListener 默认是捕获还是冒泡
addEventListener
方法的第三个参数是一个可选参数,用于指定事件是否在捕获阶段处理。如果该参数为true
,则事件在捕获阶段处理;如果为false
或省略,则事件在冒泡阶段处理。
因此,addEventListener
默认是在冒泡阶段处理事件的。这是一个例子:
element.addEventListener(
"click",
function (event) {
// 处理事件
},
false
); // 默认值是false,表示在冒泡阶段处理事件
在这个例子中,如果你点击了element
,事件处理器会在事件冒泡阶段被触发。希望这个解释对你有所帮助!
apply call bind 区别
apply
,call
和bind
都是 JavaScript 中的函数方法,它们都可以改变函数的this
指向。但是,它们在使用方式和场景上有所不同:
- call:
call
方法可以改变函数的this
指向,并立即执行这个函数。call
方法的第一个参数是this
要指向的对象,后面的参数是函数的参数。例如:
function greet() {
console.log(`Hello, ${this.name}`);
}
let obj = { name: "Alice" };
greet.call(obj); // 输出:Hello, Alice
- apply:
apply
方法和call
方法非常相似,也可以改变函数的this
指向,并立即执行这个函数。不同的是,apply
方法的第二个参数是一个数组,这个数组中的元素会作为函数的参数。例如:
function greet(greeting, punctuation) {
console.log(`${greeting}, ${this.name}${punctuation}`);
}
let obj = { name: "Alice" };
greet.apply(obj, ["Hello", "!"]); // 输出:Hello, Alice!
- bind:
bind
方法可以改变函数的this
指向,但是它不会立即执行这个函数,而是返回一个新的函数。这个新的函数在被调用时,this
会指向bind
方法的第一个参数。例如:
function greet() {
console.log(`Hello, ${this.name}`);
}
let obj = { name: "Alice" };
let boundGreet = greet.bind(obj);
boundGreet(); // 输出:Hello, Alice
总结一下,call
和apply
都是立即执行函数,但参数的传递方式不同;bind
则是返回一个新的函数,等待调用。希望这个解释对你有所帮助!
举出闭包实际场景运用的例子
闭包在 JavaScript 中有很多实际的应用场景。以下是一些例子:
- 数据封装和私有变量:闭包可以用来创建私有变量,从而实现数据的封装和保护。例如:
function createCounter() {
let count = 0;
return function () {
count += 1;
return count;
};
}
let counter = createCounter();
console.log(counter()); // 输出:1
console.log(counter()); // 输出:2
在这个例子中,count
变量被封装在createCounter
函数内部,外部无法直接访问。只能通过调用返回的函数来操作count
。
- 函数记忆(Memoization):闭包可以用来实现函数记忆,即保存函数的计算结果,避免重复计算。例如:
function memoize(fn) {
let cache = {};
return function (x) {
if (cache[x] === undefined) {
cache[x] = fn(x);
}
return cache[x];
};
}
let fastFactorial = memoize(function (n) {
if (n === 0) {
return 1;
} else {
return n * fastFactorial(n - 1);
}
});
console.log(fastFactorial(5)); // 输出:120
在这个例子中,memoize
函数返回一个新的函数,这个新的函数会记住fn
函数的计算结果,避免重复计算。
- 定时器和回调函数:闭包常常用在定时器和异步回调函数中,来访问和控制外部的变量。例如:
function delayLog(message, delay) {
setTimeout(function () {
console.log(message);
}, delay);
}
delayLog("Hello, world!", 1000);
在这个例子中,setTimeout
的回调函数是一个闭包,它可以访问和控制外部的message
变量。
这些只是闭包的一些应用场景,实际上,闭包的应用非常广泛,几乎在所有需要使用到函数的地方,都可能会用到闭包。希望这个解释对你有所帮助!
RAF 和 RIC 是什么
RAF
和RIC
是两种浏览器提供的 API,用于优化 JavaScript 的运行性能。
RAF
全称是requestAnimationFrame
,它是浏览器提供的一种方法,用于在下一次重绘之前请求执行一个动画帧。这个方法接受一个回调函数作为参数,这个回调函数会在下一次重绘之前被调用。requestAnimationFrame
的优点是它可以让浏览器自动优化动画的运行速度,避免动画卡顿或者跳帧。RIC
全称是requestIdleCallback
,它是浏览器提供的一种方法,用于在浏览器空闲时执行后台或低优先级的任务。这个方法接受一个回调函数作为参数,这个回调函数会在浏览器空闲时被调用。requestIdleCallback
的优点是它可以让我们利用浏览器的空闲时间来执行那些不紧急的任务,从而提高页面的响应速度。
这两种方法都是为了提高 JavaScript 的运行性能,让页面更加流畅。希望这个解释对你有所帮助!
ES6 的 let 实现原理
原始 es6 代码
var funcs = [];
for (let i = 0; i < 10; i++) {
funcs[i] = function () {
console.log(i);
};
}
funcs[0](); // 0
babel 编译之后的 es5 代码(polyfill)
var funcs = [];
var _loop = function _loop(i) {
funcs[i] = function () {
console.log(i);
};
};
for (var i = 0; i < 10; i++) {
_loop(i);
}
funcs[0](); // 0
其实我们根据 babel 编译之后的结果可以看得出来 let 是借助闭包和函数作用域来实现块级作用域的效果的 在不同的情况下 let 的编译结果是不一样的
如何设计实现一个渲染引擎
设计和实现一个渲染引擎是一个复杂的过程,涉及到计算机图形学、数据结构、算法等多个领域的知识。以下是一种可能的高级步骤:
解析 HTML/CSS:首先,你需要解析 HTML 和 CSS 文件,将它们转换为可以被程序处理的数据结构。例如,HTML 可以被解析为 DOM(文档对象模型),CSS 可以被解析为 CSSOM(CSS 对象模型)。
构建渲染树:然后,你需要将 DOM 和 CSSOM 合并为一个渲染树(Render Tree)。渲染树中的每个节点都包含了一个 DOM 元素以及它的样式信息。
布局:在有了渲染树之后,你需要计算每个节点在最终页面上的位置。这个过程也被称为"布局"或"重排"。
绘制:最后,你需要遍历渲染树,使用图形 API(例如 WebGL 或 Canvas API)将每个节点绘制到屏幕上。这个过程也被称为"绘制"或"重绘"。
以上只是一个简化的过程。在实际的渲染引擎中,还会有许多优化的步骤,例如层的创建和合成、事件处理、JavaScript 的执行等等。此外,实现一个渲染引擎需要深入理解浏览器的工作原理,需要对计算机图形学有一定的了解。希望这个解释对你有所帮助!
require 具体实现原理是什么
require
是 Node.js 中用于导入模块的函数。它的实现原理可以分为以下几个步骤:
路径分析:
require
函数接受一个模块标识符作为参数,这个标识符可能是文件路径,也可能是模块名。require
会分析这个标识符,确定要加载的模块的确切位置。文件读取:一旦确定了模块的位置,
require
就会读取该模块的源代码。这通常是通过文件系统 API 来完成的。编译执行:读取源代码后,Node.js 会将其编译为可执行的 JavaScript 代码,并执行这段代码。
缓存:为了提高效率,
require
会将加载和编译后的模块缓存起来。这样,下次再加载同一个模块时,就可以直接从缓存中获取,而不需要再次读取和编译。
值得注意的是,require
函数还有一些其他的特性。例如,它是同步的,这意味着在加载模块的过程中,其他的 JavaScript 代码必须等待。此外,require
还提供了一些其他的 API,如require.resolve
、require.main
等,用于处理模块的路径和主模块。
总的来说,require
的实现原理涉及到模块路径的解析、文件的读取和编译、以及模块的缓存等多个步骤。希望这个解释对你有所帮助!
JavaScript 中数组是如何存储的?
在 JavaScript 中,数组是一种特殊的对象,设计用来表示一组有序的数据。虽然在使用上类似于其他编程语言中的数组,但其底层实现和存储机制有一些不同。
JavaScript 数组的存储方式
动态数组: JavaScript 数组在底层通常实现为动态数组。这意味着数组的大小不是固定的,可以根据需要动态调整。当元素添加到数组中时,如果数组的容量不够,底层会分配一个更大的存储空间并将现有元素复制到新的存储空间中。
连续内存块 vs. 散列表:
- 连续内存块:当数组是稀疏的(大多数元素是未定义的),或者当数组的索引是连续的(0, 1, 2, ...),JavaScript 引擎可能会使用连续内存块来存储数组。这样可以提高访问速度,因为可以通过索引快速访问元素。
- 散列表:当数组包含大量的稀疏元素(即索引不连续),JavaScript 引擎可能会使用散列表来存储数组。这种情况下,数组的索引作为键,元素作为值存储在散列表中。这样可以节省内存,但访问速度可能会稍慢,因为需要通过键查找值。
JavaScript 引擎的优化
JavaScript 引擎会对数组的存储进行优化,具体的优化策略可能因不同的引擎而异(如 V8 引擎、SpiderMonkey 引擎等)。以下是一些常见的优化策略:
元素类型一致性:如果数组中的元素类型一致(如全是数字或全是字符串),引擎可以使用更加高效的存储方式。如果元素类型不一致,引擎可能会选择一种更通用但效率较低的存储方式。
稠密数组与稀疏数组:引擎会区分稠密数组(大多数索引都有元素)和稀疏数组(多数索引没有元素)。稠密数组通常会使用连续内存块存储,而稀疏数组可能会使用散列表。
示例解释
以下是一些示例,说明数组的存储和访问方式:
let denseArray = [1, 2, 3, 4, 5]; // 稠密数组,可能使用连续内存块
let sparseArray = []; // 稀疏数组,初始为空
sparseArray[1000] = "hello"; // 数组中只有一个元素,索引为1000,可能使用散列表
在上述示例中,denseArray
是一个稠密数组,可能会被存储在连续的内存块中。sparseArray
是一个稀疏数组,只有索引为 1000 的位置有元素,因此可能会使用散列表进行存储。
总结
JavaScript 数组的底层实现和存储机制是复杂的,并且会根据数组的具体使用情况进行优化。了解这些机制有助于编写性能更高的 JavaScript 代码,尤其是在处理大量数据时。
JavaScript 中的 Array 和 Node.js 中的 Buffer 有什么区别?
JavaScript 中的 Array
和 Node.js 中的 Buffer
都是用于存储和操作数据的,但它们有不同的用途和特点。以下是它们之间的主要区别:
JavaScript 中的 Array
类型:
Array
是一种通用的数据结构,可以存储任何类型的元素,包括数字、字符串、对象、甚至其他数组。
用途:
Array
用于存储和操作任何类型的有序集合,在前端和后端 JavaScript 编程中都非常常用。
可变长度:
Array
的长度是动态的,可以随着元素的添加和删除自动调整。
内存管理:
Array
的内存管理由 JavaScript 引擎自动处理,基于动态数组的实现。
操作方法:
Array
提供了丰富的内置方法来操作元素,如push
、pop
、shift
、unshift
、splice
、map
、filter
、reduce
等。
性能:
- 由于是通用结构,
Array
的性能在操作大量数据时可能不如专门的数据结构高效。
- 由于是通用结构,
Node.js 中的 Buffer
类型:
Buffer
是一种专门的数据结构,用于处理二进制数据。它可以存储二进制数据流,而不是通用数据类型。
用途:
Buffer
主要用于在 Node.js 中处理文件 I/O、网络通信、数据流等涉及二进制数据的操作。
固定长度:
Buffer
的大小是固定的,一旦创建就无法改变长度。你需要在创建时指定其大小。
内存管理:
Buffer
直接分配在 V8 引擎的堆外内存中,以提高性能和减少垃圾回收的负担。
操作方法:
Buffer
提供了一些专门的方法来操作二进制数据,如readInt8
、writeInt8
、slice
、copy
、fill
等。
性能:
Buffer
的设计目标是高效地处理和操作二进制数据,因此在处理文件和网络数据时性能非常高效。
示例比较
Array 示例:
let arr = [1, "string", { key: "value" }];
arr.push(4);
console.log(arr); // [1, "string", { key: "value" }, 4]
Buffer 示例:
let buffer = Buffer.alloc(10); // 创建一个长度为10的Buffer
buffer.write("hello");
console.log(buffer.toString("utf8")); // 输出: hello
总结
- Array 是一种通用的数据结构,用于存储和操作各种类型的有序集合,适用于大多数日常的编程任务。
- Buffer 是一种专门的数据结构,用于高效地处理和操作二进制数据,主要用于 Node.js 环境下的文件操作、网络通信等需要处理原始二进制数据的场景。
理解这两者的区别和用途有助于在适当的场景下选择正确的数据结构,从而提高代码的性能和可维护性。
JavaScript 中的数组为什么可以不需要分配固定的内存空间?
JavaScript 中的数组不需要分配固定的内存空间,主要是因为 JavaScript 数组是动态数组,具有灵活的内存管理机制。这种动态特性来源于几方面的原因和底层实现方式:
1. 动态数组的特性
JavaScript 数组是动态的,意味着数组的长度可以在运行时随时改变。你可以随意添加或删除元素,而无需在数组创建时指定其长度。这种灵活性由 JavaScript 引擎自动管理内存分配和重新分配来实现。
2. 数组的底层实现
JavaScript 引擎(如 V8)通常使用动态数组和哈希表的结合来实现数组。这允许数组既可以像连续内存块一样存储数据,又可以高效地处理稀疏数组(即数组中的大部分索引是空的)。这里是一些关键点:
- 连续内存块:当数组是密集的(大多数索引都有值),JavaScript 引擎会尽量使用连续的内存块来存储数据。这种情况下,数组的行为类似于其他语言中的动态数组,当需要扩展时,会重新分配更大的内存块,并将现有数据复制到新块中。
- 哈希表:当数组是稀疏的(大部分索引为空),JavaScript 引擎可能会将数组转化为类似于哈希表的结构来存储。这种方式避免了为大量空元素分配内存,节省了空间。
3. 自动内存管理
JavaScript 有自动垃圾回收机制(Garbage Collection),这意味着程序员不需要手动管理内存分配和释放。当数组增长时,JavaScript 引擎会自动分配更多的内存;当数组缩小时,未使用的内存会被垃圾回收机制回收。
4. 内置的数组方法
JavaScript 提供了丰富的数组方法,如 push
、pop
、shift
、unshift
等,这些方法能够方便地增加或移除数组元素,而不需要显式地管理内存。这些方法利用了底层的动态数组实现,自动处理内存分配和数据迁移。
示例
let arr = [1, 2, 3]; // 创建数组
arr.push(4); // 动态增加元素
console.log(arr); // [1, 2, 3, 4]
arr[100] = 5; // 稀疏数组
console.log(arr.length); // 101
console.log(arr[50]); // undefined
在上述示例中,数组 arr
可以动态增长(使用 push
),也可以变得稀疏(直接设置高索引的值)。JavaScript 引擎在后台处理这些操作,确保数组仍然有效。
总结
JavaScript 数组不需要分配固定的内存空间是由于其动态数组的特性和底层灵活的实现机制。这种设计使得数组在使用时非常方便,但同时也要求 JavaScript 引擎进行复杂的内存管理和优化以保持高效的性能。这种动态特性和灵活性是 JavaScript 数组在编写前端和后端代码时广受欢迎的原因之一。
JavaScript 中数组是否可以理解为特殊的对象?
JavaScript 中的数组何时是连续存储的,何时是哈希存储的?
在 JavaScript 中,数组的底层存储方式可以是连续存储(Dense Array)或哈希存储(Sparse Array)。JavaScript 引擎(例如 V8 引擎)会根据数组的使用情况动态选择最适合的存储方式。
连续存储(Dense Array)
数组在以下情况下通常会使用连续存储:
索引连续且从 0 开始:如果数组的元素索引是从 0 开始并且是连续的,那么数组会使用连续存储。这种情况下,数组的元素会存储在连续的内存块中,访问速度较快。
元素类型一致:当数组中的元素类型一致时,JavaScript 引擎可以进行优化,将数组视为一种高效的连续内存块。
元素数量较少:当数组元素数量较少时,连续存储更加高效,因为它减少了内存管理的复杂性。
示例:
let denseArray = [1, 2, 3, 4, 5]; // 连续存储
哈希存储(Sparse Array)
数组在以下情况下通常会使用哈希存储:
稀疏数组:如果数组中大部分元素为空,即索引不连续或存在较大的索引间隙,JavaScript 引擎会将其视为稀疏数组并使用哈希存储。这种方式可以节省内存,因为它不需要为未使用的索引分配内存。
索引类型不一致:如果数组的索引不是整数(如字符串索引),或者数组中混合了属性和索引,JavaScript 引擎会使用哈希存储。
大数组:对于非常大的数组,即使索引是连续的,如果内存空间不足,JavaScript 引擎也可能使用哈希存储来优化内存使用。
示例:
let sparseArray = [];
sparseArray[1000] = "value"; // 哈希存储
在这个例子中,sparseArray
是稀疏数组,因为它只有一个元素,其索引为 1000,因此使用哈希存储。
存储方式的动态调整
JavaScript 引擎会根据数组的操作动态调整其存储方式。例如,如果一个数组开始是稠密的,但后来变得稀疏,JavaScript 引擎可能会将其从连续存储转换为哈希存储,反之亦然。这种动态调整使得数组在大多数情况下都能高效运行。
总结
- 连续存储(Dense Array):用于索引连续且从 0 开始,元素类型一致且数量较少的数组。
- 哈希存储(Sparse Array):用于稀疏数组,索引类型不一致或属性和索引混合,或者非常大的数组。
JavaScript 引擎根据数组的具体使用情况动态选择最适合的存储方式,以确保性能和内存使用的最佳平衡。
哈希存储的键冲突(散列碰撞)可以有哪些解决方案(开链法、线性探测法、红黑树等)?
哈希存储中的键冲突(也称为散列碰撞)是指两个不同的键被哈希函数映射到同一个哈希表位置(桶)的情况。处理哈希冲突的常见解决方案包括:
1. 开放地址法(Open Addressing)
a. 线性探测(Linear Probing)
- 当发生冲突时,线性探测通过检查哈希表中的下一个位置来寻找空位。
- 如果位置已被占用,则继续检查下一个位置,直到找到一个空位。
h(k, i) = (h'(k) + i) % m
其中 h(k, i)
是探测函数,h'(k)
是初始哈希函数,i
是探测的步数,m
是哈希表的大小。
b. 二次探测(Quadratic Probing)
- 解决冲突时,使用二次探测,通过检查表中与冲突位置间隔为平方数的位置来寻找空位。
h(k, i) = (h'(k) + c1*i + c2*i^2) % m
其中 c1
和 c2
是常数。
c. 双重哈希(Double Hashing)
- 使用两个不同的哈希函数,当第一个哈希函数发生冲突时,使用第二个哈希函数来计算新的位置。
h(k, i) = (h1(k) + i*h2(k)) % m
其中 h1(k)
和 h2(k)
是两个不同的哈希函数。
2. 链地址法(Chaining)
- 每个哈希表的位置(桶)存储一个链表,所有映射到同一个位置的元素都存储在该链表中。
- 当发生冲突时,新元素被添加到对应位置的链表中。
array[hash(key)] -> LinkedList
3. 分离链接法(Separate Chaining)
- 这是链地址法的另一种描述方式。每个桶包含一个链表或其他动态数据结构,如平衡树或自适应树。
- 每当发生冲突时,新元素被添加到对应桶的链表或其他数据结构中。
4. 再哈希法(Rehashing)
- 当哈希表的负载因子(已存储元素数量与哈希表大小的比率)超过某个阈值时,扩展哈希表并重新计算所有元素的哈希值,将它们插入到新的哈希表中。
- 这种方法可以减少冲突的发生概率。
5. 均匀散列法(Consistent Hashing)
- 常用于分布式系统,确保哈希值分布尽可能均匀,减少冲突和热点。
- 在节点(桶)数量变化时,尽量减少需要重新分配的键值对数量。
6. 完美哈希(Perfect Hashing)
- 设计一个哈希函数,使得没有冲突。
- 通常在键的集合是静态且已知的情况下使用,通过预先计算和设计哈希函数。
具体示例
链地址法示例(Chaining):
class HashTable {
constructor(size = 53) {
this.keyMap = new Array(size);
}
_hash(key) {
let total = 0;
let WEIRD_PRIME = 31;
for (let i = 0; i < Math.min(key.length, 100); i++) {
let char = key[i];
let value = char.charCodeAt(0) - 96;
total = (total * WEIRD_PRIME + value) % this.keyMap.length;
}
return total;
}
set(key, value) {
let index = this._hash(key);
if (!this.keyMap[index]) {
this.keyMap[index] = [];
}
this.keyMap[index].push([key, value]);
}
get(key) {
let index = this._hash(key);
if (this.keyMap[index]) {
for (let i = 0; i < this.keyMap[index].length; i++) {
if (this.keyMap[index][i][0] === key) {
return this.keyMap[index][i][1];
}
}
}
return undefined;
}
}
let ht = new HashTable();
ht.set("hello", "world");
ht.set("goodbye", "earth");
console.log(ht.get("hello")); // "world"
console.log(ht.get("goodbye")); // "earth"
在这个示例中,我们使用链地址法(Chaining)来处理哈希冲突。每个哈希表的位置包含一个链表(在 JavaScript 中,我们使用数组来实现这个链表),所有映射到同一个位置的元素都存储在该链表中。
总结来说,处理哈希冲突的方法有多种,选择合适的方法取决于具体的应用场景和需求。
聊聊继承以及说说 ES5 和 ES6 继承的区别?
在 JavaScript 中,继承是一种对象获取另一个对象的属性和方法的能力。这使得代码复用和层次结构组织变得更容易。JavaScript 提供了不同的机制来实现继承,包括原型链继承和基于类的继承。ES5 和 ES6 在继承机制上有一些显著的区别。
ES5 继承
在 ES5 中,继承是通过原型链实现的。每个对象都有一个私有属性(称为 [[Prototype]]
),指向另一个对象(即原型)。原型对象也有自己的原型,形成一个原型链。当试图访问对象的属性时,如果对象本身没有这个属性,JavaScript 引擎会沿着原型链查找,直到找到该属性或到达原型链的末尾。
ES5 继承示例
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHello = function () {
console.log("Hello, my name is " + this.name);
};
function Student(name, age, grade) {
Person.call(this, name, age); // 调用父类构造函数
this.grade = grade;
}
// 继承父类的原型方法
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;
Student.prototype.study = function () {
console.log(this.name + " is studying.");
};
let student = new Student("John", 20, "A");
student.sayHello(); // Hello, my name is John
student.study(); // John is studying.
在这个例子中,Student
继承了 Person
,通过 Object.create
创建一个新的原型对象,从而继承 Person
的原型方法。然后将 Student.prototype.constructor
指向 Student
构造函数。
ES6 继承
ES6 引入了基于类的继承语法,使得继承更加直观和类似于其他面向对象编程语言(如 Java 或 C++)。ES6 类继承是通过 class
和 extends
关键字实现的。
ES6 继承示例
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
sayHello() {
console.log("Hello, my name is " + this.name);
}
}
class Student extends Person {
constructor(name, age, grade) {
super(name, age); // 调用父类构造函数
this.grade = grade;
}
study() {
console.log(this.name + " is studying.");
}
}
let student = new Student("John", 20, "A");
student.sayHello(); // Hello, my name is John
student.study(); // John is studying.
在这个例子中,Student
继承了 Person
,使用 extends
关键字和 super
调用父类的构造函数。这种方式更简洁和清晰,特别是对于有 OOP 背景的开发者来说。
ES5 和 ES6 继承的区别
语法:
- ES5:继承是通过函数构造和原型链实现的,代码比较冗长和复杂。
- ES6:使用
class
和extends
关键字,语法简洁、易读且直观。
构造函数:
- ES5:子类构造函数必须显式调用父类构造函数(使用
call
或apply
),并手动设置原型链。 - ES6:子类通过
super
调用父类构造函数,继承机制更加自然。
- ES5:子类构造函数必须显式调用父类构造函数(使用
原型链设置:
- ES5:通过
Object.create
手动设置原型链。 - ES6:
extends
关键字自动处理原型链设置。
- ES5:通过
super
关键字:- ES5:没有
super
关键字,需要手动调用父类方法。 - ES6:提供了
super
关键字,可以方便地调用父类的构造函数和方法。
- ES5:没有
静态方法和静态属性:
- ES5:静态方法和属性必须手动添加到构造函数上。
- ES6:使用
static
关键字定义静态方法和属性。
示例:
ES5 静态方法
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.sayHello = function () {
console.log("Hello");
};
Person.sayHello(); // Hello
ES6 静态方法
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
static sayHello() {
console.log("Hello");
}
}
Person.sayHello(); // Hello
总结
ES5 的继承机制依赖于函数构造和原型链,代码较为复杂和冗长。而 ES6 提供了更直观和简洁的类继承语法,使得继承变得更加简单和易读。ES6 的 class
和 extends
关键字,以及 super
的引入,使得 JavaScript 的继承机制更符合现代面向对象编程的习惯。
是否了解 JavaScript 中的包装类型?
是的,我了解 JavaScript 中的包装类型(Wrapper Types)。
在 JavaScript 中,有三种基本类型:字符串(String)、数字(Number)和布尔值(Boolean)。每种基本类型都有对应的包装类型:String、Number 和 Boolean。这些包装类型允许我们在基本类型上使用对象的方法和属性。
当我们使用基本类型值调用一个方法或访问一个属性时,JavaScript 会自动创建一个对应的包装类型对象,执行相应的操作,然后自动销毁该对象。这个过程称为自动包装(Auto Boxing)。
例如,当我们使用字符串字面量调用字符串对象的方法时,JavaScript 会自动创建一个 String 包装类型对象,执行方法后立即销毁该对象。类似地,对数字和布尔值进行操作时也会自动进行包装和拆包的过程。
下面是一些示例:
var str = "Hello";
var result = str.toUpperCase(); // 自动创建 String 包装类型对象并调用 toUpperCase 方法
console.log(result); // 输出 "HELLO"
var num = 42;
console.log(num.toString()); // 自动创建 Number 包装类型对象并调用 toString 方法
var bool = true;
console.log(bool.valueOf()); // 自动创建 Boolean 包装类型对象并调用 valueOf 方法
需要注意的是,虽然包装类型使得基本类型可以像对象一样访问方法和属性,但它们实际上是临时创建的对象,并不是真正的基本类型值。因此,对包装类型对象做比较时需要注意类型和值的匹配。
哪些情况会导致内存泄漏
内存泄漏在计算机程序中是一种常见的问题,它指的是应用程序中已分配的内存空间无法被释放,导致内存占用不断增加,最终耗尽系统资源。以下是一些常见的导致内存泄漏的情况:
循环引用:当两个或多个对象相互引用,并且这些对象之间没有被其他部分引用时,垃圾回收机制无法清除它们占用的内存。这种情况常见于事件监听器、缓存或数据结构等场景,需要确保在不再需要时解除对象之间的引用。
未释放的资源:例如打开文件、数据库连接、网络连接或操作系统资源(如图形界面组件),如果在使用完毕后没有及时释放这些资源,就会导致内存泄漏。
定时器和回调函数:使用定时器或回调函数时,如果没有正确地清理或取消它们,就会导致内存泄漏。例如,未清除的定时器会导致函数重复执行,而且回调函数中引用的变量可能无法被垃圾回收机制释放。
缓存导致的内存泄漏:不正确地使用缓存机制可能导致内存泄漏。当缓存中的对象长时间不被使用,但仍然存储在内存中,就会造成内存的浪费。
DOM 引用:在 JavaScript 中,对 DOM 元素的引用会导致内存泄漏。如果在代码中持有对 DOM 元素的引用,即使从页面中删除了该元素,它仍然无法被垃圾回收机制释放。
大对象或大数据集:如果应用程序中存在大对象或大数据集,并且这些对象在使用后仍然保持在内存中,就会占用大量的内存资源,导致内存泄漏。
为避免内存泄漏,开发人员应该注意及时释放不再使用的资源、避免循环引用、正确处理定时器和回调函数、合理使用缓存等。同时,使用工具进行性能分析和内存泄漏检测也是发现和解决内存泄漏问题的有效方式。
请介绍一下 JavaScript 中的垃圾回收站机制
JavaScript 中的垃圾回收(Garbage Collection)是一种自动的内存管理机制,它负责检测和释放不再使用的内存资源,以避免内存泄漏和内存溢出的问题。以下是 JavaScript 中常见的垃圾回收机制:
标记清除(Mark and Sweep):这是 JavaScript 中最常用的垃圾回收算法。它的基本原理是通过标记活动对象,然后清除未标记的对象。垃圾回收器首先会假定所有的对象都是不可达的,然后从根对象(如全局对象、当前执行上下文、闭包等)开始进行遍历,标记所有可达对象。在标记完成后,垃圾回收器会清除那些没有被标记的对象,并回收它们所占用的内存空间。
引用计数(Reference Counting):这是一种简单的垃圾回收算法,它通过计算对象的引用数量来确定是否释放内存。每当有一个引用指向对象时,引用计数就加一;当引用失效或被赋予新的值时,引用计数就减一。当引用计数为零时,表示该对象不再被使用,可以被回收。然而,引用计数算法无法解决循环引用的问题,导致循环引用的对象无法被回收,因此在现代 JavaScript 引擎中很少使用这种算法。
分代回收(Generational Collection):这是一种基于观察到的对象生命周期模式的优化策略。它根据对象的存活时间将内存划分为不同的代(Generation),一般分为新生代(Young Generation)和老生代(Old Generation)。新生代中的对象生命周期较短,垃圾回收频率较高;而老生代中的对象生命周期较长,垃圾回收频率较低。分代回收机制根据这种特点,采用不同的垃圾回收算法和策略来提高回收效率。
现代的 JavaScript 引擎(如 V8、SpiderMonkey、JavaScriptCore)通常会使用多种垃圾回收技术的组合,以提供更高效的内存管理。这些引擎会根据具体的场景和对象特征进行动态调整和优化。
需要注意的是,垃圾回收机制是自动进行的,开发人员通常无需显式地操作。然而,了解垃圾回收的工作原理和机制,可以帮助我们编写更高效、避免内存泄漏的 JavaScript 代码。
说说对原生 JavaScript 的理解?
原生 JavaScript 是指使用 JavaScript 语言本身提供的功能和特性,而不依赖于任何第三方库或框架的开发方式。它是 JavaScript 作为一种独立的编程语言的核心部分。
以下是对原生 JavaScript 的一些理解:
语法和基本特性:原生 JavaScript 包含了一套完整的语法规则和基本特性,如变量、数据类型(字符串、数字、布尔值等)、运算符、控制流(条件语句、循环)、函数定义和调用等。开发人员可以利用这些语言特性来实现各种功能和逻辑。
DOM 操作:原生 JavaScript 提供了与文档对象模型(DOM)交互的能力,使开发人员能够通过 JavaScript 操纵网页的结构、样式和内容。可以通过原生 JavaScript 获取和修改元素属性、添加、删除或修改元素、处理事件等。
AJAX 和数据交互:原生 JavaScript 提供了使用异步 JavaScript 和 XML(AJAX)进行数据交互的能力。开发人员可以通过原生 JavaScript 发起 HTTP 请求,获取和发送数据,并在页面上动态更新内容,而无需刷新整个页面。
内置对象和方法:JavaScript 内置了许多对象和方法,如字符串对象、数组对象、Math 对象、Date 对象等。这些内置对象和方法提供了丰富的功能,可以用来处理字符串、数组、日期、数学计算等各种任务。
错误处理和异常处理:原生 JavaScript 提供了错误处理和异常处理的机制,通过 try-catch-finally 语句和 throw 关键字,开发人员可以捕获和处理代码中的错误和异常,从而实现更可靠的代码执行。
模块化和封装:原生 JavaScript 支持模块化和封装的编程方式,通过使用函数和闭包,可以创建私有作用域、封装代码逻辑,并暴露必要的接口供外部使用,实现代码的可维护性和重用性。
原生 JavaScript 是开发 Web 应用程序的基础,它提供了丰富的功能和灵活性。使用原生 JavaScript 可以编写跨浏览器的代码,同时也有助于理解 JavaScript 语言本身的特性和工作原理。当然,为了提高开发效率和代码质量,开发人员也可以结合合适的库和框架来辅助开发。
谈谈你对 TypeScript 的理解?
TypeScript 是一种开源的编程语言,它是 JavaScript 的超集,通过为 JavaScript 添加静态类型和其他特性,提供了更强大的开发工具和语言特性支持。以下是对 TypeScript 的一些理解:
静态类型系统:TypeScript 引入了静态类型系统,开发人员可以在代码中明确声明变量的类型,并进行类型检查。这使得代码更加可靠和易于维护,可以在开发阶段就发现一些潜在的类型错误,减少运行时错误的概率。
类型注解和类型推断:在 TypeScript 中,可以使用类型注解来明确变量、函数的参数和返回值的类型。同时,TypeScript 也支持类型推断,即根据代码上下文自动推断出变量的类型,减少了冗余的类型注解。
类和接口:TypeScript 支持类和接口的定义,可以使用面向对象编程的方式组织和结构化代码。类可以定义属性、方法和构造函数,而接口可以定义对象的结构和行为规范。
泛型:TypeScript 提供了泛型(Generics)的支持,允许开发人员编写可以适用于不同类型的可重用代码。通过泛型,可以编写更加灵活和通用的函数、类和数据结构。
代码提示和自动补全:使用 TypeScript 编写代码时,编辑器可以根据变量的类型和上下文提供更准确的代码提示和自动补全功能。这可以提高开发效率,减少错误。
第三方库支持:TypeScript 兼容现有的 JavaScript 代码和库,可以直接使用 JavaScript 生态系统中的第三方库。同时,许多流行的 JavaScript 库和框架都提供了专门为 TypeScript 编写的类型声明文件,提供了更好的类型检查和代码提示。
编译时类型检查:TypeScript 代码需要经过编译器的转换,将 TypeScript 代码转换为可执行的 JavaScript 代码。在这个过程中,编译器会进行严格的类型检查,检查类型错误和潜在的问题,并提供错误提示和警告。
通过引入类型系统和其他特性,TypeScript 提供了更好的可维护性、可扩展性和代码质量。它适用于大型项目、团队协作和复杂的前端应用程序开发,可以提供更好的开发体验和代码健壮性。同时,TypeScript 仍然兼容 JavaScript,可以逐步迁移现有的 JavaScript 项目。
JavaScript 中几种迭代语法在 Chrome 等现代浏览器中的性能差异?
在现代浏览器中,JavaScript 提供了多种迭代语法,如 for 循环、forEach 方法、for...of 循环等。这些迭代语法在性能方面可能存在一些差异,具体取决于浏览器的优化实现。以下是一些常见的迭代语法及其性能方面的考虑:
for 循环:传统的 for 循环是 JavaScript 中最基本的迭代语法,具有良好的性能。它允许开发人员完全控制迭代过程,包括迭代起点、终点和步长。在大多数情况下,for 循环在性能方面是最高效的选择。
forEach 方法:forEach 是数组提供的迭代方法,它接受一个回调函数作为参数,对数组中的每个元素执行相应的操作。虽然 forEach 提供了更简洁和易读的语法,但在性能方面可能略低于传统的 for 循环。这是因为 forEach 方法无法进行一些优化,比如循环的提前终止或跳过。
for...of 循环:for...of 循环是 ES6 引入的一种迭代语法,用于遍历可迭代对象(如数组、字符串、Set、Map 等)。它提供了简洁的语法和更直观的迭代过程,但在性能方面与传统的 for 循环相比可能稍慢。这是因为 for...of 循环需要额外的迭代器对象,并且无法进行一些优化,如循环的提前终止或跳过。
需要注意的是,现代浏览器的 JavaScript 引擎在优化和执行代码时具有复杂的优化策略和算法。它们会根据具体的代码和上下文进行动态优化,并尽可能地提供更高效的执行。因此,在实际情况中,迭代语法的性能差异可能会受到多个因素的影响,如迭代次数、具体操作、编译器优化等。
总的来说,对于大多数常规的迭代操作,性能差异可能很小,且难以察觉。选择合适的迭代语法应更多地考虑代码的可读性和维护性。如果性能是一个关键问题,并且对迭代次数较多的大型数据集进行操作,那么传统的 for 循环可能是最佳选择。
如何提升 JavaScript 变量的存储性能?
要提升 JavaScript 变量的存储性能,可以考虑以下几个方面:
使用合适的变量类型:选择合适的变量类型可以减少内存占用并提高存储性能。JavaScript 提供了多种数据类型,包括原始类型(如数字、字符串、布尔值)和引用类型(如对象、数组)。根据变量的特性和需求,选择最合适的数据类型可以有效地管理内存和提高性能。
减少变量的作用域:合理管理变量的作用域可以降低内存的使用。在编写 JavaScript 代码时,应尽量将变量的作用域限制在需要的范围内,避免将变量定义在全局作用域中。通过使用块级作用域(使用 let 或 const 关键字)和函数作用域,可以及时释放不再需要的变量,减少内存占用。
及时释放不需要的变量:当变量不再需要时,应及时将其赋值为 null 或 undefined,以便垃圾回收器能够回收相关的内存空间。这样可以防止不必要的内存占用,并提高整体的存储性能。
使用对象池和缓存:对于需要频繁创建和销毁的对象,可以考虑使用对象池或缓存机制。通过重复使用已有的对象,避免频繁的内存分配和释放操作,可以减少内存管理的开销,提高存储性能。
压缩和优化代码:使用压缩工具和优化技术可以减小 JavaScript 代码的体积,从而减少变量的存储空间。可以使用压缩工具(如 UglifyJS、Terser)对代码进行压缩和混淆,去除不必要的空格和注释,并使用优化技术(如代码分割、懒加载)来减少代码的加载量。
避免过度使用闭包:虽然闭包在 JavaScript 中非常有用,但过度使用闭包可能导致内存泄漏和存储性能下降。在使用闭包时,应注意及时释放不再需要的引用,避免长期占用内存空间。
需要根据具体的应用场景和需求来综合考虑这些优化方法。在实际应用中,使用性能分析工具和进行基准测试可以更好地评估和改进 JavaScript 变量的存储性能。
浏览器和 Node.js 的事件循环机制有什么区别?
浏览器和 Node.js 在事件循环机制上存在一些区别。以下是它们的主要区别:
实现机制:浏览器和 Node.js 在底层实现事件循环的机制上有所不同。浏览器使用浏览器引擎(如 Blink、WebKit)来实现事件循环,而 Node.js 使用 V8 引擎和 libuv 库来实现事件循环。
触发方式:浏览器中的事件循环主要由用户交互事件(如鼠标点击、键盘输入)、定时器事件和网络请求等触发。而 Node.js 的事件循环主要由 I/O 操作(如文件读写、网络请求)和定时器事件触发。
默认事件:浏览器的事件循环会默认处理 UI 渲染和响应用户交互,因此它会在事件循环中包含渲染和绘制的步骤。而 Node.js 的事件循环主要关注于 I/O 操作,不会包含 UI 渲染相关的步骤。
宏任务和微任务:浏览器和 Node.js 都将任务划分为宏任务(Macrotasks)和微任务(Microtasks)。在浏览器中,宏任务包括用户交互事件、定时器事件和网络请求等,而微任务包括 Promise 的回调函数、MutationObserver 和 Object.observe 等。在 Node.js 中,宏任务包括 I/O 操作和定时器事件,而微任务包括 Promise 的回调函数和 process.nextTick。
优先级:浏览器的事件循环中微任务的执行优先级高于宏任务,即在每个宏任务执行完毕之后,会先处理完当前微任务队列中的所有微任务,然后再执行下一个宏任务。而 Node.js 的事件循环中,宏任务的执行优先级高于微任务,即每个宏任务完成后,会立即执行下一个宏任务,微任务会在下一个宏任务之前执行完毕。
需要注意的是,最新的浏览器版本和 Node.js 版本可能会有一些差异,因为这些平台的实现和行为可能会随着时间的推移而更新和改变。在编写跨平台的 JavaScript 代码时,应注意考虑不同平台的事件循环机制和行为差异。
在 JavaScript 中如何实现对象的私有属性?
在 JavaScript 中,没有内置的私有属性的概念,但可以使用一些技巧来模拟私有属性的效果。下面是几种常见的实现方式:
- 闭包:使用闭包可以创建私有变量。通过在对象的构造函数或方法中定义变量,并返回一个包含公共方法的对象,从而将这些变量封装在闭包中,使其对外部不可见。这样,在闭包内部的方法可以访问和修改这些私有变量,而外部无法直接访问。
function createObject() {
var privateVariable = "private";
return {
getPrivateVariable: function () {
return privateVariable;
},
setPrivateVariable: function (value) {
privateVariable = value;
},
};
}
var obj = createObject();
console.log(obj.getPrivateVariable()); // 输出 'private'
obj.setPrivateVariable("updated");
console.log(obj.getPrivateVariable()); // 输出 'updated'
- WeakMap:使用 WeakMap 可以实现私有属性。WeakMap 是一种键值对的集合,其中键是对象,值可以是任意类型。利用 WeakMap 的特性,可以将对象作为键,私有属性作为值存储在 WeakMap 中。由于 WeakMap 的键只对该键的引用形成弱引用,当对象被垃圾回收时,对应的私有属性也会被自动清除。
var privateVariable = new WeakMap();
function MyClass() {
privateVariable.set(this, "private");
}
MyClass.prototype.getPrivateVariable = function () {
return privateVariable.get(this);
};
MyClass.prototype.setPrivateVariable = function (value) {
privateVariable.set(this, value);
};
var obj = new MyClass();
console.log(obj.getPrivateVariable()); // 输出 'private'
obj.setPrivateVariable("updated");
console.log(obj.getPrivateVariable()); // 输出 'updated'
- Symbol:使用 Symbol 可以创建唯一的私有属性键。将私有属性作为对象的 Symbol 属性,由于 Symbol 是唯一的,外部无法直接获取到该属性。
var privateVariable = Symbol("private");
function MyClass() {
this[privateVariable] = "private";
}
MyClass.prototype.getPrivateVariable = function () {
return this[privateVariable];
};
MyClass.prototype.setPrivateVariable = function (value) {
this[privateVariable] = value;
};
var obj = new MyClass();
console.log(obj.getPrivateVariable()); // 输出 'private'
obj.setPrivateVariable("updated");
console.log(obj.getPrivateVariable()); // 输出 'updated'
这些方法都可以用来模拟私有属性,但需要注意的是,它们都有各自的限制和特性。选择适合你的需求和场景的方法来实现私有属性。
在 JavaScript 可以有哪几种形式实现继承,各有什么优缺点?
类型 | 优缺点 |
---|---|
构造函数模式 | 可以创建不同实例属性的副本,包括引用类型的实例属性,但是不能共享方法 |
原型模式 | 引用类型的属性对于实例对象而言共享同一个物理空间,因此可以共享方法 |
原型链 | 对父类实现方法和属性继承的过程中,父类实例对象的引用类型属性在子类的实例中共享同一个物理空间,因为父类的实例对象指向了子类的原型对象 |
借用构造函数 | 解决了继承中的引用值类型共享物理空间的问题,但是没法实现方法的共享 |
组合继承 | 属性的继承使用借用构造函数方法,方法的继承使用原型链技术,即解决了引用值类型共享的问题,又实现了方法的共享,但是子类的原型对象中还存在父类实例对象的实例属性 |
寄生组合继承 | 组合继承已经可以解决大部分问题,但是也有缺陷,就是会调用两次父类的构造函数,一次是实现原型时使子类的原型等于父类的实例对象调用了父类构造函数(同时在子类的原型对象中还存在了父类实例对象的实例属性),一次是使用子类构造函数时调用了一次父类构造函数。寄生组合式继承可以解决在继承的过程中子类的原型对象中还存在父类实例对象的实例属性的问题。 |
AMD 、CMD 和 CommonJS 区别?
AMD(Asynchronous Module Definition)、CMD(Common Module Definition)和 CommonJS 是用于 JavaScript 模块化开发的不同规范和方案。
AMD(Asynchronous Module Definition): AMD 是由 RequireJS 提出的一种模块定义规范。它的主要特点是支持异步加载模块,在浏览器环境下广泛应用。AMD 规范使用
define
函数来定义模块,使用require
函数来异步加载依赖的模块。AMD 模块的依赖关系在模块定义时声明,因此模块的加载顺序可以在运行时动态确定。示例代码:
// 定义模块
define(["dependency1", "dependency2"], function (dep1, dep2) {
// 模块代码
});
// 异步加载模块
require(["module1", "module2"], function (mod1, mod2) {
// 回调函数,在模块加载完成后执行
});CMD(Common Module Definition): CMD 是由 SeaJS 提出的一种模块定义规范。CMD 与 AMD 类似,也支持异步加载模块,但在模块定义和加载时采用了更加懒执行和就近依赖的策略。CMD 规范使用
define
函数来定义模块,使用require
函数来异步加载依赖的模块。CMD 模块的依赖关系在模块使用时声明,模块的加载顺序是按需加载的。示例代码:
// 定义模块
define(function (require, exports, module) {
var dep1 = require("dependency1");
var dep2 = require("dependency2");
// 模块代码
});
// 异步加载模块
require.async(["module1", "module2"], function (mod1, mod2) {
// 回调函数,在模块加载完成后执行
});CommonJS: CommonJS 是一种用于服务器端 JavaScript 的模块化规范,主要用于 Node.js 环境。CommonJS 规范使用
require
函数来同步加载模块,使用module.exports
导出模块,使用exports
对象扩展导出的成员。CommonJS 模块的加载是同步的,适用于服务器环境中的模块加载。示例代码:
// 导出模块
exports.func1 = function () {
// 模块代码
};
// 导入模块
var module = require("module");
module.func1();
总体上,AMD 和 CMD 都是浏览器端的模块化方案,主要用于解决浏览器环境下的模块依赖和加载问题,而 CommonJS 主要用于服务器端 JavaScript,用于编写模块化的 Node.js 代码。它们的差异主要体现在模块定义和加载的方式上,适用的场景和使用方式也略有不同。
common.js 和 ES 6 中模块引入的区别?
CommonJS 和 ES6(ECMAScript 6)中的模块引入有一些重要的区别:
语法差异:
- CommonJS 使用
require()
函数来引入模块,使用module.exports
或exports
来导出模块。 - ES6 使用
import
关键字来引入模块,使用export
关键字来导出模块。
- CommonJS 使用
动态 vs. 静态:
- CommonJS 模块是在运行时加载和执行的,模块的依赖关系在运行时动态确定。
- ES6 模块是在编译时静态解析的,模块的依赖关系在模块定义时就确定了。
值的复制 vs. 值的引用:
- CommonJS 在模块引入时会将整个模块的值复制一份给导入的变量,后续对导入变量的修改不会影响原始模块的值。
- ES6 模块在引入时是值的引用,导入变量指向原始模块的值,对导入变量的修改会影响原始模块的值。
异步 vs. 同步:
- CommonJS 模块加载是同步的,即模块会立即加载并执行完后再继续执行后续代码。
- ES6 模块加载是异步的,模块的加载是在需要时进行的,模块的执行顺序由模块依赖关系决定。
浏览器兼容性:
- CommonJS 是为服务器端设计的模块系统,在浏览器端使用需要借助工具(如 Browserify、Webpack)进行转换和打包。
- ES6 模块是浏览器原生支持的,不需要额外的工具转换,但需要较新的浏览器版本支持。
需要注意的是,尽管 CommonJS 和 ES6 模块有这些区别,但现代的前端开发中通常使用构建工具(如 Webpack、Rollup)将 ES6 模块转换为适用于浏览器环境的代码,以便兼容性和性能的考虑。因此,无论是使用 CommonJS 还是 ES6 模块,在实际开发中都可以通过工具进行适配和转换。
如何设计突发大规模并发架构?
设计突发大规模并发架构是一个复杂的任务,需要综合考虑多个方面的因素。以下是一些常见的设计原则和策略,可以帮助应对突发大规模并发的情况:
水平扩展:通过增加服务器数量来扩展系统的处理能力。将系统拆分为多个独立的服务单元,每个服务单元负责处理一部分请求。可以使用负载均衡器将请求分发到不同的服务器上,从而提高系统的吞吐量和并发处理能力。
异步处理:将耗时的操作和资源密集型任务异步化,以避免阻塞主线程或进程。例如,使用消息队列将请求放入队列中,由后台工作进程异步处理。这样可以提高系统的响应速度和并发处理能力。
缓存机制:使用缓存来减轻对后端系统的压力。合理使用缓存可以减少对数据库或其他外部服务的频繁访问,提高系统的响应速度。可以使用分布式缓存(如 Redis)来存储常用的数据或计算结果。
数据库优化:对数据库进行优化,提高数据库的读写性能和并发处理能力。使用索引、分表、分库等技术来减少数据库的负载。可以考虑使用主从复制、分布式数据库等方案来提高数据的读写性能和可用性。
限流和熔断:实施流量控制和限流策略,防止系统被过多的请求压垮。可以设置最大并发连接数、请求速率限制等机制来控制系统的负载。同时,考虑实施熔断机制,当系统超出负荷范围时,暂时拒绝服务或提供降级功能,保护核心业务的稳定性。
弹性设计:设计具备弹性的架构,能够在系统负载增加时自动扩展资源,并在负载减少时自动缩减资源。云计算平台提供的自动扩展功能可以帮助实现弹性设计。
监控和预警:建立全面的监控系统,实时监测系统的性能指标、负载情况和服务可用性。通过预警机制,及时发现系统的异常情况并采取相应的应对措施。
容灾和备份:设计容灾和备份策略,保证系统在突发情况下的可用性和数据的安全性。可以采用多机房部署、数据冗余和备份等措施来提高系统的可靠性。
压力测试和优化:进行全面的压力测试,模拟突发大规模并发的场景,发现系统的瓶颈和性能瓶颈。根据测试结果进行系统的优化和调整,提高系统的吞吐量和并发处理能力。
以上是一些常见的设计原则和策略,实际的架构设计需要根据具体的业务需求、系统规模和技术栈来进行综合考虑和调整。另外,持续的监控和优化是保持系统在突发大规模并发情况下稳定运行的关键。
介绍一下 js 的数据类型有哪些,值是如何存储的
JavaScript 一共有 8 种数据类型,其中有 7 种基本数据类型:Undefined、Null、Boolean、Number、String、Symbol(es6 新增,表示独一无二的值)和 BigInt(es10 新增);
1 种引用数据类型——Object(Object 本质上是由一组无序的名值对组成的)。里面包含 function、Array、Date 等。JavaScript 不支持任何创建自定义类型的机制,而所有值最终都将是上述 8 种数据类型之一。
原始数据类型:直接存储在栈(stack)中,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储。
引用数据类型:同时存储在栈(stack)和堆(heap)中,占据空间大、大小不固定。引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。
{}和 [] 的 valueOf 和 toString 的结果是什么?
{} 的 valueOf 结果为 {} ,toString 的结果为 "[object Object]"
[] 的 valueOf 结果为 [] ,toString 的结果为 ""
谈谈你对 this、call、apply 和 bind 的理解
- 在浏览器里,在全局范围内 this 指向 window 对象;
- 在函数中,this 永远指向最后调用他的那个对象;
- 构造函数中,this 指向 new 出来的那个新的对象;
- call、apply、bind 中的 this 被强绑定在指定的那个对象上;
- 箭头函数中 this 比较特殊, 箭头函数 this 为父作用域的 this,不是调用时的 this. 要知道前四种方式, 都是调用时确定, 也就是动态的, 而箭头函数的 this 指向是静态的, 声明的时候就确定了下来;
- apply、call、bind 都是 js 给函数内置的一些 API,调用他们可以为函数指定 this 的执行, 同时也可以传参。
什么是闭包,为什么要用它?
- 能够访问其它函数内部变量的函数,称为闭包
- 能够访问自由变量的函数,称为闭包
场景 至于闭包的使用场景,其实在日常开发中使用到是非常频繁的
- 防抖节流函数
- 定时器回调
- 等就不一一列举了
优点 闭包帮我们解决了什么问题呢 内部变量是私有的,可以做到隔离作用域,保持数据的不被污染性
缺点
同时闭包也带来了不小的坏处
说到了它的优点内部变量是私有的,可以做到隔离作用域
,那也就是说垃圾回收机制是无法清理闭包中内部变量的,那最后结果就是内存泄漏
三种事件模型是什么?
事件 是用户操作网页时发生的交互动作或者网页本身的一些操作,现代浏览器一共有三种事件模型。
- DOM0 级模型: ,这种模型不会传播,所以没有事件流的概念,但是现在有的浏览器支持以冒泡的方式实现,它可以在网页中直接定义监听函数,也可以通过 js 属性来指定监听函数。这种方式是所有浏览器都兼容的。
- IE 事件模型: 在该事件模型中,一次事件共有两个过程,事件处理阶段,和事件冒泡阶段。事件处理阶段会首先执行目标元素绑定的监听事件。然后是事件冒泡阶段,冒泡指的是事件从目标元素冒泡到 document,依次检查经过的节点是否绑定了事件监听函数,如果有则执行。这种模型通过 attachEvent 来添加监听函数,可以添加多个监听函数,会按顺序依次执行。
- DOM2 级事件模型: 在该事件模型中,一次事件共有三个过程,第一个过程是事件捕获阶段。捕获指的是事件从 document 一直向下传播到目标元素,依次检查经过的节点是否绑定了事件监听函数,如果有则执行。后面两个阶段和 IE 事件模型的两个阶段相同。这种事件模型,事件绑定的函数是 addEventListener,其中第三个参数可以指定事件是否在捕获阶段执行。
js 数组和字符串有哪些原生方法, 列举一下
简单介绍一下 V8 引擎的垃圾回收机制
v8 的垃圾回收机制基于分代回收机制,这个机制又基于世代假说,这个假说有两个特点,一是新生的对象容易早死,另一个是不死的对象会活得更久。基于这个假说,v8 引擎将内存分为了新生代和老生代。
新创建的对象或者只经历过一次的垃圾回收的对象被称为新生代。经历过多次垃圾回收的对象被称为老生代。
新生代被分为 From 和 To 两个空间,To 一般是闲置的。当 From 空间满了的时候会执行 Scavenge 算法进行垃圾回收。当我们执行垃圾回收算法的时候应用逻辑将会停止,等垃圾回收结束后再继续执行。这个算法分为三步:
(1)首先检查 From 空间的存活对象,如果对象存活则判断对象是否满足晋升到老生代的条件,如果满足条件则晋升到老生代。如果不满足条件则移动 To 空间。
(2)如果对象不存活,则释放对象的空间。
(3)最后将 From 空间和 To 空间角色进行交换。
新生代对象晋升到老生代有两个条件:
(1)第一个是判断是对象否已经经过一次 Scavenge 回收。若经历过,则将对象从 From 空间复制到老生代中;若没有经历,则复制到 To 空间。
(2)第二个是 To 空间的内存使用占比是否超过限制。当对象从 From 空间复制到 To 空间时,若 To 空间使用超过 25%,则对象直接晋升到老生代中。设置 25% 的原因主要是因为算法结束后,两个空间结束后会交换位置,如果 To 空间的内存太小,会影响后续的内存分配。
老生代采用了标记清除法和标记压缩法。标记清除法首先会对内存中存活的对象进行标记,标记结束后清除掉那些没有标记的对象。由于标记清除后会造成很多的内存碎片,不便于后面的内存分配。所以了解决内存碎片的问题引入了标记压缩法。
由于在进行垃圾回收的时候会暂停应用的逻辑,对于新生代方法由于内存小,每次停顿的时间不会太长,但对于老生代来说每次垃圾回收的时间长,停顿会造成很大的影响。 为了解决这个问题 V8 引入了增量标记的方法,将一次停顿进行的过程分为了多步,每次执行完一小步就让运行逻辑执行一会,就这样交替运行。
哪些操作会造成内存泄漏?
- 意外的全局变量
- 被遗忘的计时器或回调函数
- 脱离 DOM 的引用
- 闭包
get 请求传参长度的误区
参考回答: 误区:我们经常说 get 请求参数的大小存在限制,而 post 请求的参数大小是无限制的。实际上 HTTP 协议从未规定 GET/POST 的请求长度限制是多少。对 get 请求参数的限制是来源与浏览器或 web 服务器,浏览器或 web 服务器限制了 url 的长度。为了明确这个概念,我们必须再次强调下面几点:
- HTTP 协议未规定 GET 和 POST 的长度限制。
- GET 的最大长度显示是因为浏览器和 web 服务器限制了 URI 的长度。
- 不同的浏览器和 WEB 服务器,限制的最大长度不一样。
- 要支持 IE,则最大长度为 2083byte,若只支持 Chrome,则最大长度 8182byte
补充 get 和 post 请求在缓存方面的区别
参考回答: get 请求类似于查找的过程,用户获取数据,可以不用每次都与数据库连接,所以可以 使用缓存。post 不同,post 做的一般是修改和删除的工作,所以必须与数据库交互,所以不能使用 缓存。因此 get 请求适合于请求缓存。
说说前端中的事件流
参考回答: HTML 中与 javascript 交互是通过事件驱动来实现的,例如鼠标点击事件 onclick、页面的滚动事件 onscroll 等等,可以向文档或者文档中的元素添加事件侦听器来预订事件。想要知道这些事件是在什么时候进行调用的,就需要了解一下“事件流”的概念。
什么是事件流:事件流描述的是从页面中接收事件的顺序,DOM2 级事件流包括下面几个阶段。
- 事件捕获阶段
- 处于目标阶段
- 事件冒泡阶段
addEventListener:addEventListener
是 DOM2 级事件新增的指定事件处理程序的操作, 这个方法接收 3 个参数:要处理的事件名、作为事件处理程序的函数和一个布尔值。最 后这个布尔值参数如果是 true,表示在捕获阶段调用事件处理程序;如果是 false,表示 在冒泡阶段调用事件处理程序。
IE 只支持事件冒泡。
如何让事件先冒泡后捕获
参考回答: 在 DOM 标准事件模型中,是先捕获后冒泡。但是如果要实现先冒泡后捕获的效果,对于同一个事件,监听捕获和冒泡,分别对应相应的处理函数,监听到捕获事件,先暂缓执行,直到冒泡事件被捕获后再执行捕获之间。
说一下图片的懒加载和预加载
参考回答:
- 预加载:提前加载图片,当用户需要查看时可直接从本地缓存中渲染。
- 懒加载:懒加载的主要目的是作为服务器前端的优化,减少请求数或延迟请求数。 两种技术的本质:两者的行为是相反的,一个是提前加载,一个是迟缓甚至不加载。懒加载对服务器前端有一定的缓解压力作用,预加载则会增加服务器前端压力。
mouseover 和 mouseenter 的区别
参考回答:
- mouseover:当鼠标移入元素或其子元素都会触发事件,所以有一个重复触发,冒泡的过程。对应的移除事件是 mouseout。
- mouseenter:当鼠标移除元素本身(不包含元素的子元素)会触发事件,也就是不会冒泡,对应的移除事件是 mouseleave。
JS 的各种位置,比如 clientHeight,scrollHeight,offsetHeight ,以及 scrollTop,offsetTop,clientTop 的区别?
参考回答:
- clientHeight:表示的是可视区域的高度,不包含 border 和滚动条。
- offsetHeight:表示可视区域的高度,包含了 border 和滚动条。
- scrollHeight:表示了所有区域的高度,包含了因为滚动被隐藏的部分。
- clientTop:表示边框 border 的厚度,在未指定的情况下一般为 0。
- scrollTop:滚动后被隐藏的高度,获取对象相对于由 offsetParent 属性指定的父坐标(css 定位的元素或 body 元素)距离顶端的高度。
JS 拖拽功能的实现
参考回答:
- 首先是三个事件,分别是
mousedown,mousemove,mouseup
- 当鼠标点击按下的时候,需要一个 tag 标识此时已经按下,可以执行 mousemove 里面的具体方法。
- clientX,clientY 标识的是鼠标的坐标,分别标识横坐标和纵坐标,并且我们用 offsetX 和 offsetY 来表示元素的元素的初始坐标,移动的举例应该是:鼠标移动时候的坐标-鼠标按下去时候的坐标。也就是说定位信息为:鼠标移动时候的坐标-鼠标按下去时候的坐标+元素初始情况下的 offetLeft.还有一点也是原理性的东西,也就是拖拽的同时是绝对定位,我们改变的是绝对定位条件下的 left 以及 top 等等值。 补充:也可以通过 html5 的拖放(Drag 和 drop)来实现。
Ajax 解决浏览器缓存问题
参考回答:
- 在 ajax 发送请求前加上
anyAjaxObj.setRequestHeader("If-Modified-Since","0")
。 - 在 ajax 发送请求前加上
anyAjaxObj.setRequestHeader("Cache-Control","no-cache")
。 - 在 URL 后面加上一个随机数:
"fresh=" + Math.random()
。 - 在 URL 后面加上时间搓:
"nowtime=" + new Date().getTime()
。 - 如果是使用 jQuery,直接这样就可以了
$.ajaxSetup({cache:false})
。这样页面的所有 ajax 都会执行这条语句就是不需要保存缓存记录。
如何理解前端模块化
参考回答: 前端模块化就是复杂的文件编程一个一个独立的模块,比如 JS 文件等等,分成独立的模块有利于重用(复用性)和维护(版本迭代),这样会引来模块之间相互依赖的问题, 所以有了 commonJS 规范,AMD,CMD 规范等等,以及用于 JS 打包(编译等处理)的工具 webpack
如何实现一个私有变量,用 getName 方法可以访问,不能直接访问
// (1)通过defineProperty 来实现
obj = {
name: yuxiaoliang,
getName: function () {
return this.name;
},
};
object.defineProperty(obj, "name", {
//不可枚举不可配置
});
// (2)通过函数的创建形式
function product() {
var name = "yuxiaoliang";
this.getName = function () {
return name;
};
}
var obj = new product();
==
和===
、以及 Object.is 的区别
参考回答:
// (1) ==
// 主要存在:强制转换成number,null==undefined
" " == 0; //true
"0" == 0; //true
" " != "0"; //true
123 == "123"; //true
null == undefined; //true
// (2)Object.js
// 主要的区别就是+0!=-0 而NaN==NaN
// (相对比===和==的改进)
setTimeout、setInterval 和 requestAnimationFrame 之间的区别
参考回答: 与 setTimeout 和 setInterval 不同,requestAnimationFrame 不需要设置时间间隔, 大多数电脑显示器的刷新频率是 60Hz,大概相当于每秒钟重绘 60 次。大多数浏览器都 会对重绘操作加以限制,不超过显示器的重绘频率,因为即使超过那个频率用户体验也 不会有提升。因此,最平滑动画的最佳循环间隔是 1000ms/60,约等于 16.6ms。 RAF 采用的是系统时间间隔,不会因为前面的任务,不会影响 RAF,但是如果前面的 任务多的话,会响应 setTimeout 和 setInterval 真正运行时的时间间隔。
特点:
- requestAnimationFrame 会把每一帧中的所有 DOM 操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率。
- 在隐藏或不可见的元素中,requestAnimationFrame 将不会进行重绘或回流,这当然就意味着更少的 CPU、GPU 和内存使用量
- requestAnimationFrame 是由浏览器专门为动画提供的 API,在运行时浏览器会自动优化方法的调用,并且如果页面不是激活状态下的话,动画会自动暂停,有效节省了 CPU 开销。
promise+Generator+Async 的使用
参考回答:
Promise 解决的问题:回调地狱
Promise 规范:
- promise 有三种状态,等待(pending)、已完成(fulfilled/resolved)、已拒绝(rejected).Promise 的状态只能从“等待”转到“完成”或者“拒绝”,不能逆向转换,同时“完成”和“拒绝”也不能相互转换.
- promise 必须提供一个 then 方法以访问其当前值、终值和据因。promise.then(resolve,reject),resolve 和 reject 都是可选参数。如果 resolve 或 reject 不是函数,其必须被忽略.then 方法必须返回一个 promise 对象.
/**使用:
实例化promise 对象需要传入函数(包含两个参数),resolve 和reject,内部确定状态.resolve
和reject 函数可以传入参数在回调函数中使用.
resolve 和reject 都是函数,传入的参数在then 的回调函数中接收.
*/
var promise = new Promise(function (resolve, reject) {
setTimeout(function () {
resolve("好哈哈哈哈");
});
});
promise.then(function (val) {
console.log(val);
});
// then 接收两个函数,分别对应resolve 和reject 状态的回调,函数中接收实例化时传入的参数.
promise.then(
(val) => {
//resolved
},
(reason) => {
//rejected
}
);
/**catch 相当于.then(null, rejection)
当then 中没有传入rejection 时,错误会冒泡进入catch 函数中,若传入了rejection,则错误会
被rejection 捕获,而且不会进入catch.此外,then 中的回调函数中发生的错误只会在下一级
的then 中被捕获,不会影响该promise 的状态.*/
new Promise((resolve, reject) => {
throw new Error("错误");
})
.then(null, (err) => {
console.log(err, 1); //此处捕获
})
.catch((err) => {
console.log(err, 2);
});
// 对比
new Promise((resolve, reject) => {
throw new Error("错误");
})
.then(null, null)
.catch((err) => {
console.log(err, 2); //此处捕获
});
// 错误示例
new Promise((resolve, reject) => {
resolve("正常");
})
.then(
(val) => {
throw new Error("回调函数中错误");
},
(err) => {
console.log(err, 1);
}
)
.then(null, (err) => {
console.log(err, 2); //此处捕获,也可用catch
});
// 两者不等价的情况:
// 此时,catch 捕获的并不是p1 的错误,而是p2 的错误,
p1()
.then((res) => {
return p2(); //p2 返回一个promise 对象
})
.catch((err) => console.log(err));
/** 一个错误捕获的错误用例:
该函数调用中即使发生了错误依然会进入then 中的resolve 的回调函数,因为函数p1 中实
例化promise 对象时已经调用了catch,若发生错误会进入catch 中,此时会返回一个新的
promise,因此即使发生错误依然会进入p1 函数的then 链中的resolve 回调函数.*/
function p1(val) {
return new Promise((resolve, reject) => {
if (val) {
var len = val.length; //传入null 会发生错误,进入catch 捕获错
resolve(len);
} else {
reject();
}
}).catch((err) => {
console.log(err);
});
}
p1(null)
.then(
(len) => {
console.log(len, "resolved");
},
() => {
console.log("rejected");
}
)
.catch((err) => {
console.log(err, "catch");
});
/** Promise 回调链:
promise 能够在回调函数里面使用return 和throw, 所以在then 中可以return 出一个
promise 对象或其他值,也可以throw 出一个错误对象,但如果没有return,将默认返回
undefined,那么后面的then 中的回调参数接收到的将是undefined.*/
function p1(val) {
return new Promise((resolve, reject) => {
val == 1 ? resolve(1) : reject();
});
}
function p2(val) {
return new Promise((resolve, reject) => {
val == 2 ? resolve(2) : reject();
});
}
let promimse = new Promise(function (resolve, reject) {
resolve(1);
})
.then(function (data1) {
return p1(data1); //如果去掉return,则返回undefined 而不是p1 的返回值,会导致报错
})
.then(function (data2) {
return p2(data2 + 1);
})
.then((res) => console.log(res));
Generator 函数:
generator 函数使用:
- 分段执行,可以暂停
- 可以控制阶段和每个阶段的返回值
- 可以知道是否执行到结尾
function* g() {
var o = 1;
yield o++;
yield o++;
}
var gen = g();
console.log(gen.next()); // Object {value: 1, done: false}
var xxx = g();
console.log(gen.next()); // Object {value: 2, done: false}
console.log(xxx.next()); // Object {value: 1, done: false}
console.log(gen.next()); // Object {value: undefined, done: true}
generator 和异步控制: 利用 Generator 函数的暂停执行的效果,可以把异步操作写在 yield 语句里面,等到调用 next 方法时再往后执行。这实际上等同于不需要写回调函数了,因为异步操作的后续操 作可以放在 yield 语句下面,反正要等到调用 next 方法时再执行。所以,Generator 函数 的一个重要实际意义就是用来处理异步操作,改写回调函数。
async 和异步: async 表示这是一个 async 函数,await 只能用在这个函数里面。 await 表示在这里等待异步操作返回结果,再继续执行。 await 后一般是一个 promise 对象 示例:async 用于定义一个异步函数,该函数返回一个 Promise。 如果 async 函数返回的是一个同步的值,这个值将被包装成一个理解 resolve 的 Promise, 等同于 return Promise.resolve(value)。 await 用于一个异步操作之前,表示要“等待”这个异步操作的返回值。await 也可以用 于一个同步的值。
let timer = async function timer() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("500");
}, 500);
});
};
timer()
.then((result) => {
console.log(result); //500
})
.catch((err) => {
console.log(err.message);
});
//返回一个同步的值
let sayHi = async function sayHi() {
let hi = await "hello world";
return hi; //等同于return Promise.resolve(hi);
};
sayHi().then((result) => {
console.log(result);
});
JSONP 的缺点
- JSON 只支持 get,因为 script 标签只能使用 get 请求;
- JSONP 需要后端配合返回指定格式的数据。
跨域(jsonp,ajax)
JSONP:ajax 请求受同源策略影响,不允许进行跨域请求,而 script 标签 src 属性中的链 接却可以访问跨域的 js 脚本,利用这个特性,服务端不再返回 JSON 格式的数据,而是 返回一段调用某个函数的 js 代码,在 src 中进行了调用,这样实现了跨域。
知道 PWA 吗
PWA 全称 Progressive Web App,即渐进式 WEB 应用。一个 PWA 应用首先是一个网页,可以通过 Web 技术编写出一个网页应用. 随后添加上 App Manifest 和 Service Worker 来实现 PWA 的安装和离线等功能
Rem, Em
rem 单位如何转换为像素值 当使用 rem 单位的时候,页面转换为像素大小取决于叶根元素的字体大小,即 HTML 元素的字体大小。根元素字体大小乘 rem 的值。例如,根元素的字体大小为 16px,那么 10rem 就等同于 10*16=160px。
em 是如何转换成 px 的 当使用 em 单位的时候,像素值是将 em 值乘以使用 em 单位的元素的字体大小。例如一 个 div 的字体为 18px,设置它的宽高为 10em,那么此时宽高就是 18px*10em=180px。
.test {
width: 10em;
height: 10em;
background-color: #ff7d42;
font-size: 18px;
}
/**
一定要记住的是,em 是根据使用它的元素的font-size 的大小来变化的,而不是根据父
元素字体大小。有些元素大小是父元素的多少倍那是因为继承了父元素中font-size 的设
定,所以才起到的作用。
*/
em 单位的继承效果 使用 em 单位存在继承的时候,每个元素将自动继承其父元素的字体大小,继承的效果 只能被明确的字体单位覆盖,比如 px 和 vw。只要父级元素上面一直有 fontsize 为 em 单 位,则会一直继承,但假如自己设置了 font-size 的单位为 px 的时候,则会直接使用自 己的 px 单位的值。
根 html 的元素将会继承浏览器中设置的字体大小 除非显式的设置固定值去覆盖。所以 html 元素的字体大小虽然是直接确定 rem 的值, 但这个字体大小首先是来源于浏览器的设置。(所以一定要设置 html 的值的大小,因 为有可能用户的浏览器字体大小是不一致的。)
当 em 单位设置在 html 元素上时 它将转换为 em 值乘以浏览器字体大小的设置。
html {
font-size: 1.5em;
}
/**
可以看到,因为浏览器默认字体大小为16px,所以当设置HTML 的fontsize 的值为1.5em
的售后,其对应的px 的值为16*1.5=24px
所以此时,再设置其他元素的rem 的值的时候,其对应的像素值为n*24px。
例如,test 的rem 的值为10,
*/
.test {
width: 10rem;
height: 10rem;
background-color: #ff7d42;
}
/**
总结:
1. rem 单位翻译为像素值的时候是由html 元素的字体大小决定的。此字体大小会
被浏览器中字体大小的设置影响,除非显式的在html 为font-size 重写一个单位。
2. em 单位转换为像素值的时候,取决于使用它们的元素的font-size 的大小,但是有
因为有继承关系,所以比较复杂。
优缺点
em 可以让我们的页面更灵活,更健壮,比起到处写死的px 值,em 似乎更有张力,改
动父元素的字体大小,子元素会等比例变化,这一变化似乎预示了无限可能,
em 做弹性布局的缺点还在于牵一发而动全身,一旦某个节点的字体大小发生变化,那
么其后代元素都得重新计算
*/
jsonp 安全问题
CSRF 攻击
前端构造一个恶意页面,请求 JSONP 接口,收集服务端的敏感信息。如果 JSONP 接口还涉及一些敏感操作或信息(如登陆、删除等操作)就更不安全了。
解决办法:验证 JSONP 的调用来源(Referer),服务端判断 Referer 是否是白名单,或者部署随机 Token 来防御;避免敏感接口使用 JSONP 方法。
XSS 漏洞
不严谨的 content-type 导致的 XSS 漏洞,如果没有严格定义好 content-type(content-type:application/json),再加上没有过滤 callback 的参数,直接当 html 解析,就是一个赤裸裸的 XSS。
解决办法:严格定义 content-type:application/json,然后严格过滤 callback 后的参数并限制长度(进行字符转译),这样返回的脚本内容会变成文本格式,脚本将不会执行)
关于 es5 和 es6 之间的继承关系
Javascript中的继承一直是个比较麻烦的问题,prototype、constructor、proto在构造函数,实例和原型之间有的复杂的关系,不仔细捋下很难记得牢固。ES6中又新增了class和extends,和ES5搅在一起,加上平时很少自己写继承,简直乱成一锅粥。不过还好,画个图一下就清晰了,下面不说话了,直接上图,上代码。
ES5
ES5中的继承,看图:
function F () { }
F.prototype.method = function () {
console.log('Hi!');
}
var f = new F();
F.prototype.constructor === F; // ② true
f.constructor === F; // ④ true
f.__proto__ === F.prototype; // ⑤ true
ES6
ES6中的继承,看图:
class Super {}
class Sub extends Super {}
var sub = new Sub();
Sub.prototype.constructor === Sub; // ② true
sub.constructor === Sub; // ④ true
sub.__proto__ === Sub.prototype; // ⑤ true
Sub.__proto__ === Super; // ⑥ true
Sub.prototype.__proto__ === Super.prototype; // ⑦ true
ES6和ES5的继承是一模一样的,只是多了class
和extends
,ES6的子类和父类,子类原型和父类原型,通过__proto__
连接。
Commonjs 中 exports 与 module.exports 的区别
exports
是 modules.exports
的引用,等价于
const exports = module.exports
那如下结果会导出什么?
module.exports = 100
exports = 3
很显然会导出 100,毕竟 exports 进行了重定向
node 的源码:
源码地址:https://github.com/nodejs/node/blob/master/lib/internal/modules/cjs/loader.js#L1252
众所周知,node 中所有的模块都被包裹在这个函数内
(function(exports, require, module, __filename, __dirname) {
exports.a = 3
})()
而以下的源码指出,exports 是如何得来的
const dirname = path.dirname(filename);
const require = makeRequireFunction(this, redirects);
let result;
// 从这里可以看出来 exports 的实质
const exports = this.exports;
const thisValue = exports;
const module = this;
if (requireDepth === 0) statCache = new Map();
if (inspectorWrapper) {
result = inspectorWrapper(compiledWrapper, thisValue, exports,
require, module, filename, dirname);
} else {
// 这里是模块包装函数
result = compiledWrapper.call(thisValue, exports, require, module,
filename, dirname);
}
Proxy 相比较于 defineProperty 的优势
Object.defineProperty
是监听对象的字段而非对象本身,因此对于动态插入对象的字段,它无能为了,只能手动为其设置设置监听属性。
同时,Object.defineProperty
无法监听对象中数组的变化,因此其他基于 Object.defineProperty
都对数组做了一定的 Hack 处理。
Proxy
叫做代理器,它可以为一个对象设置代理,即监听对象本身,任何访问当前被监听的对象的操作,无论是对象本身亦或是对象的字段,都会被 Proxy 拦截,因此可以使用它来做一些双向绑定的操作。
鉴于兼容性的问题,目前仍然主要是使用 Object.defineProperty
更多,但是随着 Vue/3 的发布,Proxy 应该会逐渐淘汰 Object.defineProperty
。