今天是咱们Javascript面试题的高级篇,也是最后一篇。
20. 执行上下文、执行栈、变量对象和作用域链究竟是啥?
执行上下文:这指的是代码执行的环境,包括作用域、变量对象以及“this”关键字的值。每当一个函数被执行时,就会创建一个执行上下文,里面包含了该函数的所有变量或属性。
JavaScript中有三种类型的执行上下文:
- 全局执行上下文
- 函数执行上下文
- eval函数执行上下文
执行栈:也被称为“调用栈”,是一个遵循LIFO(后进先出)原则的数据结构,存储了所有正在进行中的函数调用的执行上下文。当函数被调用时,会创建一个新的执行上下文并压入栈中。当函数执行完毕,其上下文就会从栈中弹出。
引擎会执行位于栈顶的执行上下文中的函数。当这个函数执行完毕,它的执行栈就会从当前栈中弹出,控制权转移到当前栈中下一个上下文中。
执行上下文在创建阶段形成,此阶段会发生以下事情:
- 创建词法环境组件。
- 创建变量环境组件。
变量对象:它是执行上下文的一部分,包含了该上下文中定义的所有变量、函数声明和参数。
作用域链:这是JavaScript中用于解析变量值的一种机制。当引用一个变量时,JavaScript引擎首先在当前执行上下文的变量对象中查找该变量。如果在那里找不到,它会继续向外层执行上下文搜索,沿着作用域链进行,直到找到该变量或到达全局执行上下文为止。
21. 回调、Promise、setTimeout、process.nextTick() 的执行优先级是怎样的?
根据事件循环和不同异步操作处理的顺序,我们可以理解它们的执行优先级:
- process.nextTick():使用
process.nextTick()
安排的回调具有最高优先级。当你使用process.nextTick()
时,回调会在当前操作完成后但事件循环进入下一阶段之前立即执行,确保函数能以最早可能的时间点在事件循环中执行。 - Promise:Promise通常在
process.nextTick()
之后执行,但它们的优先级高于通过setTimeout()
安排的回调。 - setTimeout():通过
setTimeout()
安排的回调被放置在事件循环的计时器阶段。它们将在当前操作、Promise以及任何先前安排的setTimeout()
回调完成之后执行。 - 回调:常规回调(非
process.nextTick()
安排的)具有最低优先级。它们在事件循环处理完process.nextTick()
、Promise和setTimeout()
回调之后执行。
22. 工厂函数和生成器函数是啥东东?
工厂函数在JavaScript中是指返回一个对象的函数。这是一种直接且有序创建对象的模式。与使用构造函数和new
关键字创建新对象不同,工厂函数封装了对象创建过程并返回新对象。
function createPerson(name, age) {
return {
name: name,
age: age,
greet: function() {
return `哈喽,我是${this.name},今年${this.age}岁。`;
}
};
}
const person1 = createPerson('小明', 25);
const person2 = createPerson('小红', 30);
console.log(person1.greet()); // 输出:哈喽,我是小明,今年25岁。
console.log(person2.greet()); // 输出:哈喽,我是小红,今年30岁。
生成器函数是JavaScript中一种特殊类型的函数,可以在执行过程中暂停和恢复。生成器函数产生一系列结果,而非单一值。
当调用生成器函数时,它会返回一个生成器对象,通过调用next()方法可以控制函数的执行,使其暂停或继续。
在函数体内部,可以使用yield关键字暂停执行,随后可以从暂停点继续执行。
function* numberGenerator() {
let i = 0;
while (true) {
yield i++;
}
}
const gen = numberGenerator();
console.log(gen.next().value); // 输出:0
console.log(gen.next().value); // 输出:1
console.log(gen.next().value); // 输出:2
这种方式为创建迭代器和处理异步代码提供了强大工具。
23. 复制(浅拷贝和深拷贝)对象的不同方法有哪些?
浅拷贝是指创建的对象其引用与原对象相同。这意味着如果你修改了浅拷贝对象的某个属性值,原对象相应属性的值也会改变。
const user = { name: "小王", age: 28, job: "网页开发者" };
const clone = user;
深拷贝则是指创建的对象其引用与原对象不同。即使你修改了深拷贝对象的属性值,也不会影响到原对象的属性值。
实现深拷贝的方法有多种:
a)JSON.parse和JSON.stringify:适用于嵌套对象,但不能处理函数和循环引用。
const originalObject = { name: "小李", age: 25 };
const deepCopy = JSON.parse(JSON.stringify(originalObject));
b)structuredClone:
const myDeepCopy = structuredClone(myOriginal);
c)展开运算符(…):不适用于包含嵌套对象的情况。
const originalObject = { name: "小张", age: 25 };
const deepCopy = { ...originalObject };
deepCopy.name = "小刚";
console.log("originalObject", originalObject.name); // 小张
d)Object.assign():适合于没有嵌套对象的情况。
const originalObject = { name: "小赵", age: 25 };
const shallowCopy = Object.assign({}, originalObject);
e)递归:
function deepCopy(obj) {
if (typeof obj !== 'object' || obj === null) {
return obj;
}
const newObj = Array.isArray(obj) ? [] : {};
for (let key in obj) {
if (Object.hasOwnProperty.call(obj, key)) {
newObj[key] = deepCopy(obj[key]);
}
}
return newObj;
}
const originalObject = { name: "小王", nested: { age: 25 } };
const deepCopy = deepCopy(originalObject);
24. 如何使对象变得不可变?(使用seal和freeze方法)
在JavaScript中,你可以使用Object.seal()
和Object.freeze()
方法使对象变为不可变。
Object.freeze():(完全不可变)此方法冻结一个对象,使其密封并标记所有属性为只读。一旦对象被冻结,其属性便无法修改、添加或删除。
const obj = { name: '小王', age: 25 };
Object.freeze(obj);
obj.name = '小李'; // 不允许
obj.address = '123 街道'; // 不允许(不能添加新属性)
delete obj.age; // 不允许(不能删除现有属性)
Object.seal():(部分不可变)此方法密封一个对象,阻止添加新属性并标记所有现有属性为不可配置。不过,你仍然可以修改那些可写的现有属性值。
const obj = { name: '小王', age: 25 };
Object.seal(obj);
obj.name = '小李'; // 允许
obj.address = '123 街道'; // 不允许(不能添加新属性)
delete obj.age; // 不允许(不能删除现有属性)
25. 事件和事件流、事件冒泡及事件捕获是啥?
在JavaScript中,事件流定义了一个事件(如点击或按键)在网页上或由浏览器处理的接收顺序。事件流包含两个阶段:事件捕获和事件冒泡。
当你点击嵌套在多个其他元素中的一个元素时,在你的点击实际上到达目标元素之前,它必须先为每个父元素触发点击事件,从顶层的全局window对象开始。
以这个例子说明事件流:
- 事件捕获阶段:点击按钮时,事件从顶部(文档根部)开始向下传播至目标元素。在这个例子中,事件从文档根部传播到
<div>
(父元素),再到<button>
(子元素)。这就是捕获阶段。 - 事件目标阶段:事件到达目标元素,即本例中的
<button>
。 - 事件冒泡阶段:到达目标后,事件开始向上冒泡。它从
<button>
回到<div>
,最终回到文档根部。这称为冒泡阶段。
以下是一个简单的JavaScript代码示例来演示这一过程:
document.getElementById('parent').addEventListener('click', function() {
console.log('Div clicked (capture phase)');
}, true); // 参数'true'指定了捕获阶段。
document.getElementById('child').addEventListener('click', function() {
console.log('Button clicked (target phase)');
});
document.getElementById('parent').addEventListener('click', function() {
console.log('Div clicked (bubble phase)');
});
点击按钮时,控制台将按以下顺序显示这些消息:
- “Div clicked (capture phase)”
- “Button clicked (target phase)”
- “Div clicked (bubble phase)”
26. 事件委托是啥?
事件委托是一种JavaScript编程技术,优化了对多个元素的事件处理。
它不是为每个单独的元素都附加事件监听器,而是将单个事件监听器附加到DOM(文档对象模型)层次结构中更高一层的共同祖先元素上。
当后代元素上的事件发生时,它会“冒泡”到该共同祖先处,那里事件监听器正在等待。
事件委托是监听事件的一种方式,你将一个父元素作为所有子元素内发生的事件的监听者。
var list = document.getElementById('items');
list.addEventListener('click', function(event) {
if(event.target.tagName.toLowerCase() === 'li') {
console.log('Clicked on ' + event.target.textContent);
}
}, false);
27. 服务器发送事件(Server-Sent Events, SSE)是啥?
服务器发送事件(SSE)是一种简单而高效的技术,能够通过单一HTTP连接从服务器向客户端提供实时更新。
SSE使得服务器能在新信息可用时立即推送给Web客户端(通常是浏览器),非常适合需要实时更新而不依赖复杂协议或第三方库的场景。
- SSE提供了从服务器到客户端的单向数据流。服务器发起通信,发送更新给客户端。
- SSE使用基于文本的协议,意味着服务器发送给客户端的数据通常是文本格式(通常是JSON或纯文本)。
- SSE自动处理重新连接。
- SSE建立客户端与服务器之间的持久连接,允许服务器向客户端发送事件流。每个事件都可以有唯一类型和相关联的数据。
- EventSource对象用于接收服务器发送的事件通知。例如,可以通过以下方式接收服务器的消息:
if (typeof EventSource !== "undefined") {
var source = new EventSource("updates.example.com");
source.onmessage = function(event) {
document.getElementById("updateArea").innerHTML += event.data + "<br>";
};
}
- SSE还提供了一系列事件(如onopen、onmessage、onerror)供使用。
28. JavaScript中的Web Worker或Service Worker是啥?
Web Worker和Service Worker是JavaScript中的两种不同概念,
Web Worker设计用于后台的并发JavaScript执行,而Service Worker则用于构建具有离线功能的渐进式Web应用(Progressive Web App, PWA)及其他高级特性。两者都是提升Web应用性能和功能的关键工具。
各自在Web开发中扮演着不同的角色:
Web Worker:
- 并发性:Web Worker是浏览器功能,允许你在后台运行独立于主浏览器线程的JavaScript代码。这使得任务可以并发执行,而不阻塞用户界面。
- 应用场景:Web Worker常用于计算密集型或耗时的任务,如数据处理、图像操作或复杂计算。通过在单独的线程中运行这些任务,它们不会影响网页的响应性。
- 通信:Web Worker通过消息系统与主线程通信,可以发送和接收消息,实现主线程和Worker之间的协调。
- 浏览器支持:大多数现代浏览器都支持Web Worker。
Service Worker:
- 离线能力:Service Worker是更高级的功能,用于创建具有离线功能的渐进式Web应用。它们充当代理服务器,在后台运行,可以拦截和缓存网络请求,从而实现离线内容提供等功能。
- 应用场景:Service Worker主要用于实现离线访问、推送通知和后台同步等特性。它们让Web应用即便在没有互联网连接的情况下也能运行。
- 生命周期:Service Worker有自己的生命周期,包含
install
、activate
和fetch
等事件。它们通常在Web应用启动时注册。 - 浏览器支持:Service Worker得到现代浏览器的支持,是创建可靠且吸引人的Web应用的关键技术。
29. 在JavaScript中如何比较两个JSON对象?
a) 一种简单的方法是使用JSON.stringify
将它们转换成字符串然后比较字符串。
function isEqual(obj1, obj2) {
return JSON.stringify(obj1) === JSON.stringify(obj2);
}
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = { a: 1, b: { c: 2 } };
console.log(isEqual(obj1, obj2)); // 输出: true
b) 也可以使用Ramda库来比较两个JSON对象。Ramda提供了一个名为equals
的函数。
const R = require('ramda');
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = { a: 1, b: { c: 2 } };
console.log(R.equals(obj1, obj2)); // 输出: true
c) 另一个选项是使用如Lodash这样的库,它提供了深层比较对象的方法。
const _ = require('lodash');
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = { a: 1, b: { c: 2 } };
console.log(_.isEqual(obj1, obj2)); // 输出: true
希望您喜欢这篇文章。非常感谢您的阅读。