欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 新闻 > 焦点 > 2025年01月10日浙江鑫越系统科技前端面试

2025年01月10日浙江鑫越系统科技前端面试

2025/5/14 14:29:10 来源:https://blog.csdn.net/uperficialyu/article/details/147934449  浏览:    关键词:2025年01月10日浙江鑫越系统科技前端面试

目录

  1. vue2 和 vue3 的区别
  2. vue 怎么封装组件
  3. js 怎么把一个数组置空
  4. 怎么组件自己调用自己的组件
  5. v-bind:attribute 和 v-bind=“{attribute}” 的区别
  6. var let const 的区别
  7. this 指向
  8. 作用域链
  9. 闭包
  10. 原型链
  11. 事件循环

1. vue2 和 vue3 的区别

Vue 2 和 Vue 3 在多个方面存在区别,以下从架构设计、语法与 API、性能、生态系统等方面进行详细介绍:

架构设计
  • 响应式系统
    • Vue 2:基于 Object.defineProperty() 实现响应式。这种方式有一定局限性,例如无法检测对象属性的添加和删除,对于数组,部分方法(如通过索引修改元素)也不能触发响应式更新。
    • Vue 3:采用 Proxy 对象实现响应式系统。Proxy 可以劫持整个对象,并能拦截更多操作,解决了 Vue 2 中响应式的一些限制,能更好地检测对象属性的变化,包括属性的添加、删除以及数组元素的修改等。
  • 代码组织
    • Vue 2:主要使用选项式 API(Options API),将不同的逻辑(如数据、方法、生命周期钩子等)分散在不同的选项中,在处理复杂组件时,可能会导致代码碎片化,逻辑分散难以维护。
    • Vue 3:引入了组合式 API(Composition API),允许开发者根据逻辑关注点来组织代码,将相关的逻辑封装在一起,提高了代码的复用性和可维护性,尤其适合大型项目。
语法与 API
  • 组件定义
    • Vue 2:使用 Vue.extend() 或单文件组件(SFC)来定义组件,通过 export default 导出一个包含各种选项的对象。
    • Vue 3:仍然支持单文件组件,但在组合式 API 中,可以使用 <script setup> 语法糖来简化组件的定义,减少样板代码。
<!-- Vue 2 组件定义 -->
<template><div>{{ message }}</div>
</template><script>
export default {data() {return {message: 'Hello, Vue 2!'};}
};
</script><!-- Vue 3 组件定义(<script setup>) -->
<template><div>{{ message }}</div>
</template><script setup>
import { ref } from 'vue';
const message = ref('Hello, Vue 3!');
</script>
  • 生命周期钩子
    • Vue 2:有 beforeCreatecreatedbeforeMountmountedbeforeUpdateupdatedbeforeDestroydestroyed 等生命周期钩子。
    • Vue 3:部分钩子名称发生了变化,beforeDestroy 改为 beforeUnmountdestroyed 改为 unmounted,并且在组合式 API 中可以使用 onBeforeMountonMounted 等函数来注册生命周期钩子。
// Vue 2 生命周期钩子
export default {created() {console.log('Vue 2: Component created');}
};// Vue 3 组合式 API 生命周期钩子
import { onMounted } from 'vue';export default {setup() {onMounted(() => {console.log('Vue 3: Component mounted');});}
};
  • 响应式数据定义
    • Vue 2:在 data 选项中定义响应式数据,使用 this 来访问。
    • Vue 3:使用 ref()reactive() 函数来创建响应式数据。ref() 用于创建单个值的响应式数据,reactive() 用于创建对象的响应式数据。
// Vue 2 响应式数据定义
export default {data() {return {count: 0};},methods: {increment() {this.count++;}}
};// Vue 3 响应式数据定义
import { ref } from 'vue';export default {setup() {const count = ref(0);const increment = () => {count.value++;};return {count,increment};}
};
性能
  • 渲染性能
    • Vue 2:渲染器在更新 DOM 时,使用虚拟 DOM 进行比较和更新,在处理大型组件树时,可能会有一定的性能开销。
    • Vue 3:重写了渲染器,采用了静态提升、PatchFlag 等优化技术,减少了虚拟 DOM 的比较范围,提高了渲染性能,尤其是在处理大型组件和频繁更新的场景下表现更优。
  • 内存占用
    • Vue 2:由于响应式系统的实现方式,在创建大量响应式对象时,可能会占用较多的内存。
    • Vue 3:Proxy 实现的响应式系统在内存使用上更加高效,减少了不必要的内存开销。
生态系统
  • 插件兼容性
    • Vue 2:拥有丰富的插件生态系统,但部分插件可能需要进行适配才能在 Vue 3 中使用。
    • Vue 3:随着时间的推移,越来越多的插件开始支持 Vue 3,但在过渡期间,可能会面临一些插件兼容性问题。
  • 工具链支持
    • Vue 2:与之配套的工具链(如 Vue CLI)已经非常成熟。
    • Vue 3:官方推出了 Vite 作为构建工具,它具有更快的冷启动和热更新速度,更适合现代前端开发。

2. vue 怎么封装组件

在 Vue 里封装组件可以提升代码复用性与可维护性。下面为你详细介绍封装组件的步骤和示例。

步骤
  1. 创建组件文件:在项目里创建一个新的 .vue 文件,此文件即为组件。
  2. 定义组件结构:在 <template> 标签里定义组件的 HTML 结构。
  3. 编写组件逻辑:在 <script> 标签里编写组件的 JavaScript 逻辑。
  4. 添加样式:在 <style> 标签里添加组件的 CSS 样式。
  5. 使用组件:在需要使用该组件的地方引入并注册它。
示例

下面是一个简单的按钮组件封装示例:

1. 创建组件文件 ButtonComponent.vue
<template><button :class="classes" @click="handleClick">{{ label }}</button>
</template><script>
export default {name: 'ButtonComponent',props: {label: {type: String,default: 'Click me'},primary: {type: Boolean,default: false}},computed: {classes() {return {'bg-blue-500 text-white py-2 px-4 rounded': this.primary,'bg-gray-500 text-white py-2 px-4 rounded': !this.primary};}},methods: {handleClick() {this.$emit('click');}}
};
</script><style scoped>
button {cursor: pointer;
}
</style>
2. 使用组件
<template><div><ButtonComponent label="Primary Button" primary @click="onPrimaryClick" /><ButtonComponent label="Secondary Button" @click="onSecondaryClick" /></div>
</template><script>
import ButtonComponent from './ButtonComponent.vue';export default {name: 'App',components: {ButtonComponent},methods: {onPrimaryClick() {console.log('Primary button clicked');},onSecondaryClick() {console.log('Secondary button clicked');}}
};
</script><style scoped>
/* 全局样式 */
</style>
解释
  • <template>:定义了组件的 HTML 结构,这里是一个按钮元素。
  • <script>:定义了组件的 JavaScript 逻辑,包含 props(接收外部传入的数据)、computed(计算属性)和 methods(方法)。
  • <style>:定义了组件的 CSS 样式,scoped 属性保证样式仅应用于当前组件。
  • 使用组件:在需要使用组件的地方引入并注册它,然后在 <template> 中使用组件标签。

通过以上步骤,你就可以在 Vue 项目中封装并使用组件了。

3. js 怎么把一个数组置空

在 JavaScript 里,有多种方法能将数组置空,下面为你详细介绍常见的几种方式及其示例代码。

方法一:将数组的长度设为 0

通过把数组的 length 属性设置成 0,能够直接清空数组,此操作会移除数组里的所有元素。

let arr = [1, 2, 3, 4, 5];
arr.length = 0;
console.log(arr); // 输出: []
方法二:重新赋值为空数组

直接把数组变量重新赋值为空数组 [],不过要留意这种方法会让原数组失去引用,若有其他变量也引用了原数组,这些变量不会受到影响。

let arr = [1, 2, 3, 4, 5];
let anotherArr = arr;
arr = [];
console.log(arr); // 输出: []
console.log(anotherArr); // 输出: [1, 2, 3, 4, 5]
方法三:使用 splice 方法

splice 方法可用于从数组里添加或删除元素。当你把起始位置设为 0,删除数量设为数组的长度时,就能清空数组。

let arr = [1, 2, 3, 4, 5];
arr.splice(0, arr.length);
console.log(arr); // 输出: []

综上所述,若你想直接清空原数组且让所有引用该数组的变量都变为空数组,推荐使用将 length 属性设为 0 或者 splice 方法;若你仅想让当前变量指向一个新的空数组,可使用重新赋值为空数组的方法。

4. 怎么组件自己调用自己的组件

在前端开发里,组件自己调用自己也就是实现组件的递归调用,这种方式常用于处理树形结构的数据,像菜单、文件目录这类场景。下面分别以 Vue 和 React 为例,介绍如何实现组件的递归调用。

Vue 中实现组件递归调用

在 Vue 里实现组件递归调用,需确保组件自身可以在模板里引用自己。以下是一个简单的树形菜单组件示例:

<template><ul><li v-for="item in menuItems" :key="item.id">{{ item.label }}<!-- 若存在子菜单,递归调用组件 --><TreeMenu v-if="item.children && item.children.length > 0" :menuItems="item.children" /></li></ul>
</template><script>
export default {name: 'TreeMenu',props: {menuItems: {type: Array,required: true}}
};
</script><style scoped>
ul {list-style-type: none;padding-left: 20px;
}
</style>    
<template><div id="app"><TreeMenu :menuItems="menuData" /></div>
</template><script>
import TreeMenu from './TreeMenu.vue';export default {name: 'App',components: {TreeMenu},data() {return {menuData: [{id: 1,label: 'Menu Item 1',children: [{id: 2,label: 'Submenu Item 1',children: [{id: 3,label: 'Sub - submenu Item 1'}]}]},{id: 4,label: 'Menu Item 2'}]};}
};
</script>    
React 中实现组件递归调用

在 React 里实现组件递归调用,同样是在组件的渲染函数中调用自身。以下是一个对应的树形菜单组件示例:

import React from 'react';const TreeMenu = ({ menuItems }) => {return (<ul>{menuItems.map((item) => (<li key={item.id}>{item.label}{/* 若存在子菜单,递归调用组件 */}{item.children && item.children.length > 0 && (<TreeMenu menuItems={item.children} />)}</li>))}</ul>);
};export default TreeMenu;    
import React from 'react';
import TreeMenu from './TreeMenu';const App = () => {const menuData = [{id: 1,label: 'Menu Item 1',children: [{id: 2,label: 'Submenu Item 1',children: [{id: 3,label: 'Sub - submenu Item 1'}]}]},{id: 4,label: 'Menu Item 2'}];return (<div><TreeMenu menuItems={menuData} /></div>);
};export default App;    

上述代码展示了在 Vue 和 React 中实现组件递归调用的方法,你可以依据实际需求对代码进行修改和扩展。

5. v-bind:attribute 和 v-bind=“{attribute}” 的区别

在Vue.js里,v-bind:attributev-bind="{attribute}" 这两种语法存在明显差异,下面为你详细介绍:

1. v-bind:attribute(简写为 :attribute

这种语法用于把单个表达式的值绑定到某个特定的属性上。就像下面这样:

<template><div><!-- 把isActive变量的值绑定到disabled属性 --><button :disabled="isActive">提交</button><!-- 把title变量的值绑定到title属性 --><img :src="imageUrl" :title="imageTitle" /></div>
</template>
  • 要是isActive的值为true,按钮就会被禁用。
  • imageUrlimageTitle分别对应图片的srctitle属性。
2. v-bind="{attribute}"(对象语法)

该语法会对一个对象进行解构操作,然后把对象里的所有属性都绑定到元素上。示例如下:

<template><div><!-- 假设userData = { name: 'John', age: 30 } --><UserProfile v-bind="userData" /><!-- 等同于 --><UserProfile :name="userData.name" :age="userData.age" /></div>
</template>
  • 当使用v-bind="userData"时,userData对象中的所有属性(像nameage)都会被当作props传递给UserProfile组件。
  • 这一语法常用于批量传递props或者动态绑定多个属性。
主要区别
特性v-bind:attributev-bind=“{…}”
绑定数量一次只能绑定一个属性可以同时绑定多个属性
表达式类型支持任意类型的表达式要求必须是对象类型
应用场景适用于绑定单个属性适合批量绑定多个属性
实际应用示例
<template><div><!-- 情况1:绑定单个属性 --><input :value="message" /><!-- 情况2:批量绑定多个属性 --><input v-bind="inputConfig" /></div>
</template><script>
export default {data() {return {message: 'Hello',inputConfig: {type: 'text',placeholder: '请输入内容',disabled: false}}}
}
</script>
总结
  • 若你需要绑定单个属性,就使用:attribute="expression"
  • 若你要批量绑定多个属性,建议使用v-bind="object"

合理运用这两种语法,能够让你的Vue代码变得更加简洁和具有可读性。

6. var let const 的区别

在 JavaScript 中,varletconst 是用于声明变量的关键字,它们的主要区别体现在作用域、变量提升、可变性和暂时性死区等方面。以下是详细对比:

1. 作用域规则
  • var:函数作用域(或全局作用域)
    • 在函数内部声明的变量只能在函数内部访问,在全局作用域声明的变量会成为全局对象(如浏览器中的 window)的属性。
  • letconst:块级作用域({} 内有效)
    • ifforwhile 等代码块中声明的变量,外部无法访问。

示例对比

function testScope() {if (true) {var x = 10;    // 函数作用域let y = 20;    // 块级作用域const z = 30;  // 块级作用域}console.log(x);  // ✅ 输出 10console.log(y);  // ❌ ReferenceErrorconsole.log(z);  // ❌ ReferenceError
}
2. 变量提升(Hoisting)
  • var:存在变量提升,可在声明前访问(值为 undefined
  • letconst:存在暂时性死区(TDZ),声明前访问会报错

示例对比

console.log(a);  // ✅ undefined(var 提升但未赋值)
console.log(b);  // ❌ ReferenceError(TDZ)
console.log(c);  // ❌ ReferenceError(TDZ)var a = 1;
let b = 2;
const c = 3;
3. 可变性
  • varlet:变量值可修改
  • const:常量值不可修改(必须在声明时赋值,且不能重新赋值)

示例对比

var a = 1;
a = 2;  // ✅ 允许修改let b = 3;
b = 4;  // ✅ 允许修改const c = 5;
c = 6;  // ❌ TypeError(不能重新赋值)// 注意:const 声明对象/数组时,对象属性或数组元素可修改
const obj = { name: 'Alice' };
obj.name = 'Bob';  // ✅ 允许修改属性
obj = {};          // ❌ TypeError(不能重新赋值对象)
4. 重复声明
  • var:允许在同一作用域重复声明同名变量(后面的会覆盖前面的)
  • letconst:不允许在同一作用域重复声明同名变量

示例对比

var a = 1;
var a = 2;  // ✅ 允许,a 变为 2let b = 3;
let b = 4;  // ❌ SyntaxError(重复声明)const c = 5;
const c = 6;  // ❌ SyntaxError(重复声明)
5. 全局作用域行为
  • var:在全局作用域声明的变量会成为全局对象的属性
  • letconst:在全局作用域声明的变量不会成为全局对象的属性

示例对比

var x = 10;
console.log(window.x);  // ✅ 输出 10let y = 20;
const z = 30;
console.log(window.y);  // ❌ undefined
console.log(window.z);  // ❌ undefined
推荐使用场景
  • let:需要重新赋值的变量,尤其是在块级作用域中(如循环、条件语句)。
  • const:不需要重新赋值的变量(默认优先使用),如常量、对象、函数引用等。
  • var:尽量避免使用,除非需要兼容旧代码或特殊场景(如函数作用域)。
总结表格
特性varletconst
作用域函数作用域块级作用域块级作用域
变量提升存在(值为 undefined存在(TDZ)存在(TDZ)
可变性可变可变不可变(常量)
重复声明允许不允许不允许
全局对象属性

通过合理使用 letconst,可以减少变量污染和提升代码的健壮性,这也是现代 JavaScript 开发的最佳实践。

7. this 指向

在 JavaScript 中,this 关键字的指向是动态的,它取决于函数的调用方式。这一点与其他语言有很大不同,也是 JavaScript 的一个难点。下面将详细介绍 this 的指向规则及其应用场景。

1. 全局作用域中的 this

在全局作用域中,this 指向全局对象(在浏览器中是 window 对象)。

console.log(this === window); // true(在浏览器环境中)var globalVar = 'global';
console.log(this.globalVar);  // 'global'(全局变量是全局对象的属性)// 严格模式下,全局作用域中的 this 仍为全局对象
function test() {'use strict';console.log(this === window); // true
}
test();
2. 函数调用中的 this

普通函数调用时,this 指向全局对象(非严格模式)或 undefined(严格模式)。

function showThis() {console.log(this);
}showThis(); // window(非严格模式)或 undefined(严格模式)// 严格模式示例
function strictThis() {'use strict';console.log(this); // undefined
}
strictThis();
3. 方法调用中的 this

当函数作为对象的方法调用时,this 指向调用该方法的对象。

const person = {name: 'Alice',greet() {console.log(`Hello, ${this.name}`);}
};person.greet(); // 'Hello, Alice'(this 指向 person 对象)// 嵌套对象示例
const obj = {outer: {inner: {method() {console.log(this); // 指向 inner 对象}}}
};obj.outer.inner.method(); // 输出 inner 对象
4. 构造函数中的 this

使用 new 调用构造函数时,this 指向新创建的实例对象。

function Car(color) {this.color = color;this.showColor = function() {console.log(this.color);};
}const redCar = new Car('red');
redCar.showColor(); // 'red'(this 指向 redCar 实例)
5. 箭头函数中的 this

箭头函数不绑定自己的 this,而是捕获其所在上下文的 this 值。

const obj = {name: 'Bob',regular() {console.log(this.name); // 'Bob'},arrow: () => {console.log(this.name); // undefined(箭头函数的 this 继承自全局作用域)}
};obj.regular(); // 'Bob'
obj.arrow();   // undefined// 常见应用:在回调函数中保持 this 指向
const timer = {seconds: 10,start() {setInterval(() => {this.seconds--; // 箭头函数的 this 指向 timer 对象console.log(this.seconds);}, 1000);}
};timer.start();
6. callapplybind 方法

这三个方法可以显式地绑定函数的 this 值。

function greet(message) {console.log(`${message}, ${this.name}`);
}const person1 = { name: 'Alice' };
const person2 = { name: 'Bob' };// call():直接调用并指定 this
greet.call(person1, 'Hi'); // 'Hi, Alice'
greet.call(person2, 'Hello'); // 'Hello, Bob'// apply():类似 call,但参数以数组形式传递
greet.apply(person1, ['Hi']); // 'Hi, Alice'// bind():创建一个新函数,永久绑定 this
const greetAlice = greet.bind(person1);
greetAlice('Hey'); // 'Hey, Alice'
7. DOM 事件处理中的 this

在 DOM 事件处理函数中,this 通常指向触发事件的元素。

<button id="myButton">Click me</button>
<script>const button = document.getElementById('myButton');button.addEventListener('click', function() {this.style.backgroundColor = 'red'; // this 指向 button 元素});
</script>
8. 类中的 this

在 ES6 类中,this 的指向与构造函数和方法的调用方式一致。

class Counter {constructor() {this.count = 0;}increment() {this.count++;console.log(this.count);}
}const counter = new Counter();
counter.increment(); // 1(this 指向 counter 实例)
总结:this 指向的判断规则
  1. 全局作用域this 指向全局对象(如 window)。
  2. 函数调用:非严格模式下指向全局对象,严格模式下为 undefined
  3. 方法调用this 指向调用该方法的对象。
  4. 构造函数this 指向新创建的实例对象。
  5. 箭头函数this 继承自父级上下文。
  6. 显式绑定:使用 callapplybind 方法指定 this
  7. DOM 事件this 指向触发事件的元素。
常见问题与注意事项
  • 箭头函数与普通函数的区别:箭头函数没有自己的 this,适合用于需要保留上下文的场景(如回调函数);普通函数的 this 取决于调用方式。
  • 丢失 this 绑定
    const obj = {name: 'Alice',greet() {console.log(`Hello, ${this.name}`);}
    };const greetFunc = obj.greet;
    greetFunc(); // 错误:this 指向全局对象或 undefined
    
    解决方法:使用 bind() 绑定 this,或使用箭头函数。

通过理解 this 的指向规则,你可以更准确地编写 JavaScript 代码,避免因 this 指向不明导致的错误。

9. 闭包

在 JavaScript 里,闭包是一个强大且重要的概念。下面为你详细解释 JavaScript 中的闭包。

定义

闭包是指有权访问另一个函数作用域中变量的函数。简单来说,即使外部函数执行完毕,其作用域内的变量也不会被销毁,而是会被闭包“捕获”并保留,使得这些变量能在外部函数之外被访问和修改。

形成条件

闭包的形成需要满足以下两个关键条件:

  1. 函数嵌套:必须存在一个外部函数和至少一个内部函数。
  2. 内部函数引用外部函数的变量:内部函数使用了外部函数作用域内的变量。
作用

闭包在 JavaScript 中有多种重要作用:

  • 读取函数内部的变量:外部函数执行结束后,其内部变量会被闭包保存,可通过闭包在外部访问这些变量。
  • 让这些变量的值始终保持在内存中:变量不会因外部函数执行完毕而被销毁,而是持续存在于内存里,方便后续使用。
  • 封装私有变量和方法:可以使用闭包来创建私有变量和方法,避免全局作用域的污染。
示例
function outerFunction() {// 外部函数的变量let counter = 0;// 内部函数,形成闭包function innerFunction() {counter++;return counter;}return innerFunction;
}// 创建闭包实例
const closure = outerFunction();// 调用闭包
console.log(closure()); // 输出: 1
console.log(closure()); // 输出: 2
console.log(closure()); // 输出: 3

在这个示例中,outerFunction 是外部函数,innerFunction 是内部函数。innerFunction 引用了 outerFunction 作用域内的 counter 变量,从而形成了闭包。当 outerFunction 执行完毕后,counter 变量不会被销毁,而是被 innerFunction 捕获并保留。每次调用 closure 函数时,counter 变量的值都会增加。

闭包的潜在问题

虽然闭包功能强大,但也可能带来一些问题,比如内存泄漏。由于闭包会让变量一直存在于内存中,如果闭包使用不当,可能会导致内存占用过高。因此,在使用闭包时,需要注意内存的使用情况,避免不必要的内存消耗。

10. 原型链

原型链是JavaScript中实现继承和对象属性查找的一种机制。以下是关于原型链的详细介绍:

原型的概念

在JavaScript中,每个对象都有一个原型(prototype)。原型也是一个对象,它可以包含一些属性和方法。当访问一个对象的属性或方法时,如果该对象本身没有这个属性或方法,JavaScript引擎就会去它的原型对象中查找。

原型链的形成
  • 所有的对象都默认从 Object.prototype 继承属性和方法。例如,toString()valueOf() 等方法就是从 Object.prototype 继承来的。
  • 当创建一个函数时,JavaScript会自动为这个函数添加一个 prototype 属性,这个属性指向一个对象,称为该函数的原型对象。当使用构造函数创建一个新对象时,新对象的 __proto__ 属性(也称为原型链指针)会指向构造函数的原型对象。这样就形成了一条链,从新对象开始,通过 __proto__ 不断指向它的原型对象,直到 Object.prototype,这条链就是原型链。
原型链的作用
  • 实现继承:通过原型链,一个对象可以继承另一个对象的属性和方法。例如,定义一个 Animal 构造函数,再定义一个 Dog 构造函数,让 Dog 的原型指向 Animal 的实例,这样 Dog 的实例就可以继承 Animal 的属性和方法。
  • 属性和方法的共享:多个对象可以共享原型对象上的属性和方法,节省内存空间。比如,所有数组对象都共享 Array.prototype 上的 push()pop() 等方法。
示例代码
// 定义一个构造函数
function Person(name) {this.name = name;
}// 在构造函数的原型上添加方法
Person.prototype.sayHello = function() {console.log(`Hello, my name is ${this.name}`);
};// 创建一个Person的实例
const person1 = new Person('John');// 访问实例的属性和方法,先在实例本身查找,找不到就去原型上查找
person1.sayHello(); // 输出 "Hello, my name is John"
console.log(person1.__proto__ === Person.prototype); // 输出 true

在这个例子中,person1Person 构造函数的实例,它的 __proto__ 属性指向 Person.prototype。当调用 person1.sayHello() 时,由于 person1 本身没有 sayHello 方法,JavaScript会沿着原型链在 Person.prototype 上找到该方法并执行。

11. 事件循环

以下是对 JavaScript 事件循环的更深入解释:

基本概念
  • 单线程执行模型:JavaScript 是单线程的,即在同一时间内只能执行一个任务。这意味着 JavaScript 代码按顺序执行,不会出现多个任务同时执行的情况。但为了处理异步操作,JavaScript 引入了事件循环机制,使它可以在等待某些操作完成时继续执行其他代码。
核心组件
  • 执行栈(Call Stack)
    • 执行栈是一个后进先出(LIFO)的数据结构,用于存储当前正在执行的函数调用。
    • 当一个函数被调用时,它会被压入执行栈;当函数执行完成,它会从栈中弹出。
    • 例如:
function first() {second();
}
function second() {third();
}
function third() {console.log('Hello, World!');
}
first();
- 调用 `first()` 时,`first` 函数会被压入执行栈;`first` 函数调用 `second()`,`second` 函数会被压入执行栈;`second` 函数调用 `third()`,`third` 函数会被压入执行栈;`third` 函数执行并打印 `Hello, World!`,然后 `third` 函数从栈中弹出,接着 `second` 函数弹出,最后 `first` 函数弹出。
  • 任务队列(Task Queue)
    • 任务队列存储着等待执行的任务,主要是异步操作的回调函数。
    • 任务队列可以分为宏任务队列(Macrotask Queue)和微任务队列(Microtask Queue)。
宏任务与微任务
  • 宏任务(Macrotasks)

    • 常见的宏任务包括 setTimeoutsetIntervalsetImmediate(Node.js)、I/O 操作、UI 渲染等。
    • 宏任务的执行顺序是一个接一个的,即执行完一个宏任务后,才会开始执行下一个宏任务。
    • 例如,setTimeout 函数会将其回调函数添加到宏任务队列中,当达到设定的延迟时间后,该回调函数会等待被执行。
  • 微任务(Microtasks)

    • 常见的微任务包括 Promise.then()Promise.catch()process.nextTick(Node.js)、queueMicrotask 等。
    • 微任务的优先级高于宏任务。在当前宏任务执行结束后,会优先执行微任务队列中的所有微任务,直到微任务队列为空。
    • 例如,Promise.resolve().then() 会将其回调函数添加到微任务队列中,该回调函数会在当前宏任务完成后立即执行,而不是等待下一个宏任务。
事件循环的执行流程
  1. 检查执行栈是否为空。
    • 如果执行栈不为空,继续执行栈中的函数调用。
    • 如果执行栈为空,进入下一步。
  2. 检查微任务队列是否为空。
    • 如果微任务队列不为空,按顺序依次执行微任务队列中的任务,直到微任务队列为空。
    • 如果微任务队列也为空,进入下一步。
  3. 从宏任务队列中取出一个任务,将其添加到执行栈中并执行。
  4. 重复上述步骤。
示例代码及详细解释
console.log('Start');setTimeout(() => {console.log('Timeout 1');Promise.resolve().then(() => {console.log('Promise inside Timeout 1');});
}, 0);Promise.resolve().then(() => {console.log('Promise 1');setTimeout(() => {console.log('Timeout inside Promise 1');}, 0);
});console.log('End');
  • 代码执行顺序如下:
    1. 首先,console.log('Start') 是同步代码,直接执行,输出 Start
    2. setTimeout(() => {...}, 0) 是宏任务,其回调函数被添加到宏任务队列中。
    3. Promise.resolve().then(() => {...}) 是微任务,其回调函数被添加到微任务队列中。
    4. console.log('End') 是同步代码,直接执行,输出 End
    5. 此时执行栈为空,检查微任务队列,发现 Promise.resolve().then(() => {...}) 的回调函数,执行该微任务,输出 Promise 1,并将另一个 setTimeout 回调添加到宏任务队列。
    6. 微任务队列已空,从宏任务队列中取出 setTimeout(() => {...}) 的回调函数,执行该宏任务,输出 Timeout 1,同时将内部的 Promise.then() 微任务添加到微任务队列。
    7. 再次检查微任务队列,执行内部的 Promise.then() 微任务,输出 Promise inside Timeout 1
    8. 最后,执行之前添加到宏任务队列的 setTimeout(() => {...}) 回调函数,输出 Timeout inside Promise 1
事件循环的重要性和应用场景
  • 重要性

    • 事件循环使 JavaScript 能够高效处理异步操作,避免因等待某些操作(如网络请求、文件读取等)而阻塞代码执行,保证程序的流畅性。
    • 理解事件循环有助于避免一些常见的异步编程错误,如竞态条件、回调地狱等。
  • 应用场景

    • 网络请求:当使用 fetchXMLHttpRequest 进行网络请求时,请求完成后的回调函数会被添加到任务队列中,等待执行。
    • 用户交互:点击事件、输入事件等用户交互的处理函数会被添加到任务队列中,在用户触发事件后等待执行。
    • 定时器操作:使用 setTimeoutsetInterval 等定时器,其回调函数会在设定的时间后添加到任务队列中。

在面试中,可以这样回答:“JavaScript 事件循环是一种处理异步操作的机制,它基于单线程执行模型。核心组件包括执行栈和任务队列,任务队列又分为宏任务队列和微任务队列。宏任务如 setTimeoutsetInterval 等,微任务如 Promise.then() 等。事件循环的执行流程是先检查执行栈是否为空,若为空,检查微任务队列,若微任务队列不为空,执行微任务直到为空,再从宏任务队列取一个任务执行,不断重复这个过程。这一机制使 JavaScript 可以在等待异步操作时继续执行其他代码,避免阻塞,同时保证了执行顺序。例如在处理网络请求、用户交互和定时器操作等场景中,事件循环能确保这些异步操作的回调函数在适当的时间得到执行,同时避免因等待而影响程序的流畅性。”

通过这样的详细解释和示例,可以清晰地阐述 JavaScript 事件循环的概念、流程、重要性和应用场景,让面试官了解你对该知识点的深入理解和掌握程度。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词