前端面试题汇总 -- JS篇

165 阅读20分钟
1.Map和Set的区别,Map和Object的区别

Map和Set是JavaScript中两种常见的集合类型,它们都用于存储一组唯一的值。但它们之间存在一些区别。

  1. 区别:结构和存储方式 - Map是键值对的集合,其中每个键都是唯一的。键可以是任意数据类型(包括对象或原始值),并且按插入顺序排列。 - Set是值的集合,其中每个值都是唯一的。值可以是任意数据类型(包括对象或原始值),并且按插入顺序排列。

  2. 区别:获取和查找元素 - Map通过键来获取、设置和删除元素。你可以使用get(key)方法获取与指定键关联的值,并使用set(key, value)方法设置键值对,使用delete(key)方法删除指定键及其对应的值。 - Set只有一个操作——判断一个特定的元素是否存在于集合中。你可以使用has(value)方法检查一个特定的元素是否存在于Set中,并使用add(value)方法将元素添加到Set中,使用delete(value)方法从Set中删除一个特定的元素。

  3. 区别:遍历方式 - Map提供了多种遍历方式,包括forEach()for...of循环以及通过迭代器进行遍历。 - Set也提供了多种遍历方式,同样包括forEach()for...of循环以及通过迭代器进行遍历。

  4. 区别:应用场景 - 当需要以某种方式将值与唯一标识符相关联时,例如将学生ID与学生信息相关联时,通常会选择Map。 - 当需要存储不重复的集合并忽略其顺序时,例如过滤重复项或检查某个元素是否已存在时,通常会选择Set。 关于Map和Object之间的区别: - Object是JavaScript中最基本的数据类型之一,并且在许多情况下被广泛使用。 - Object是无序属性集合,其中属性由字符串或符号表示,并具有相应的值。属性名必须唯一。 - Map也是一个集合,但它提供了比Object更强大和灵活的功能。 - 相对于Object只能使用字符串作为键名,在Map中可以使用更广范围的数据类型作为键名。 - 另外,在处理Object时要小心继承属性可能导致意外结果;而在Map中不会出现这样问题。 总结来说: Map适用于需要根据特定键来访问、修改或删除数据项,并且保持插入顺序非常重要的情况。而Set则适用于需要保持唯一性并忽略顺序的情况。Object则是JavaScript语言内置对象,并且在很多场景下都被广泛应用。

2.数组的filter、every、flat的作用是什么

数组的filtereveryflat是JavaScript数组提供的三个常用方法,它们分别具有以下作用:

  1. filter: 根据指定条件筛选数组元素 - filter()方法创建一个新数组,其中包含满足指定条件的所有元素。 - 传入一个函数作为参数,该函数接受当前元素作为参数,并返回一个布尔值(true或false)来指示是否保留该元素。 - 只有满足条件返回true的元素才会被保留到新数组中。
  2. every: 检查数组中所有元素是否都满足特定条件 - every()方法对数组中的每个元素应用给定的函数,如果对于每个元素该函数都返回true,则返回true;否则返回false。 - 类似于逻辑运算符“与”的行为,只有当所有元素都满足给定条件时,结果为true;否则结果为false。
  3. flat: 展平嵌套的数组结构 - flat()方法将多维嵌套数组展平成一维数组。 - 默认情况下,仅展平一层嵌套。可以在调用flat()方法时传入一个可选参数depth来指定要展开的嵌套层数。 - 返回一个新的展平后的一维数组。 这些方法在实际应用中非常有用: - 使用filter()可以轻松地根据某种条件过滤出需要的数据项。 - 使用every()可以进行快速且简洁地检查是否所有数据项都满足特定条件。 - 使用flat()可以方便地处理多层级嵌套数组,并将其转换为更易处理和操作的一维形式。

例如,假设我们有一个包含学生对象的数组students,并且想要筛选出年龄大于18岁且成绩优秀(大于90分)的学生,可以使用filter()方法: javascript const filteredStudents = students.filter(student => student.age > 18 && student.score > 90); 又如,在某些情况下我们可能会使用every()验证用户输入是否合法(例如:必填字段必须填写): javascript const inputs = ['name', 'email', 'address']; const isValid = inputs.every(input => document.getElementById(input).value !== ''); 最后,使用flat()方法可以很方便地将多层级嵌套数组转换成一维形式: javascript const nestedArray = [1, 2, [3, 4, [5, 6]]]; const flattenedArray = nestedArray.flat(); // 结果: [1, 2, 3, 4, [5, 6]]

3.Promise的all和race有什么区别

Promise.all()Promise.race()是两个常用的Promise方法,它们之间有以下区别:

  1. 功能: - Promise.all()接收一个由Promise对象组成的可迭代对象(如数组),并返回一个新的Promise对象。 - 这个新的Promise对象在所有传入的Promise都成功(即状态为fulfilled)时变为fulfilled,并以包含所有传入Promise解决值的数组作为结果。 - 如果任意一个传入的Promise被拒绝(即状态为rejected),则该新Promise立即被拒绝,并返回第一个被拒绝的 Promise 的拒因。 - Promise.race()也接收一个由Promise对象组成的可迭代对象,并返回一个新的Promise对象。 - 这个新的Promise对象在第一个传入的 Promise 状态改变时(无论是fulfilled还是rejected)就会跟随该 Promise 的状态。 - 返回的 Promise将具有与第一个完成的 Promise 相同的结构,包括解析值或拒因。

  2. 执行方式: - Promise.all()会等待所有传入的 Promise 都完成后才会返回结果。只有当所有 Promise 都执行成功才会触发回调函数,并且按照传入顺序组成结果数组。如果有任何一个 Promise 被拒绝,那么整个 Promise.all() 就会立即失败并触发失败回调。 - Promise.race()只要有任何一个传入的 Promise 完成(无论成功还是失败),就会返回对应结果或原因。无论完成时间早晚,只要第一个完成就会触发回调函数。 使用示例: javascript const promise1 = new Promise((resolve, reject) => { setTimeout(() => resolve('promise 1'), 1000); }); const promise2 = new Promise((resolve, reject) => { setTimeout(() => resolve('promise 2'), 2000); }); const promise3 = new Promise((resolve, reject) => { setTimeout(() => reject('promise 3 error'), 1500); }); // 使用 Promise.all() Promise.all([promise1, promise2]) .then(results => { console.log(results); // 输出: ['promise 1', 'promise 2'] }) .catch(error => { console.error(error); }); // 使用 Promsie.race() Promsie.race([promise1, promise3]) .then(result => { console.log(result); // 输出: 'promise 1' }) .catch(error => { console.error(error); // 永远不会被执行 });

总结来说: - Promise.all()等待多个任务全部完成,而Promsie.race()只要其中任意一个任务完成就会返回结果或原因。 - 当需要等待多个异步操作都完成后才进行下一步操作时,可以使用Promsie.all();而在需要最快得到结果或处理竞争条件时,可以使用Promsie.race()

4.箭头函数和普通函数的区别

箭头函数和普通函数是JavaScript中两种不同的函数定义方式,它们之间存在一些区别。

  1. 语法形式: - 普通函数使用function关键字进行定义,后跟圆括号括起来的参数列表和大括号括起来的函数体。 - 箭头函数使用箭头(=>)符号进行定义,通常省略了function关键字。参数列表和函数体可以根据需要省略或简化。

  2. 绑定上下文: - 普通函数有自己的this上下文,它根据调用方式动态绑定到不同对象,并且可以使用bind()call()apply()方法来显式绑定上下文。 - 箭头函数没有自己的this上下文,它会捕获并继承外层作用域中最近的父级作用域的this值。

  3. 构造器: - 普通函数可以通过构造器方式创建新对象,并且具有其自己的原型对象。 - 箭头函数不能被用作构造器,因此不能通过new关键字创建实例。

  4. 返回值: - 普通函数可以使用带有返回值的return语句来返回一个值,默认情况下返回undefined。 - 箭头函数以隐式返回(implicit return)形式工作,如果没有指定大括号,则自动返回单个表达式的结果。如果需要返回多个表达式或执行其他额外逻辑,则需要使用大括号包裹代码块,并显式指定返回值。

  5. arguments对象: - 普通函数内部可以访问到一个特殊变量arguments,该变量为类数组对象,包含所有传入该函数的参数。 - 箭头函数没有自己独立的arguments对象,它继承外层作用域中最近父级作用域中的arguments对象。

总结来说: - 箭头函数相对于普通函数更加简洁和精炼,在一些场景下可以提供更清晰、更易读、更易理解代码。 - 普通函数则提供了更强大、更灵活和功能更完全的特性,例如构造器、this绑定和arguments访问等。 - 在选择使用箭头函数还是普通函数时,需要考虑具体需求、上下文和预期行为。

5.let、var和const的区别?如果希望const定义的对象的属性也不能被修改该怎么做?

letvarconst是JavaScript中用于声明变量的关键字,它们之间有以下区别:

  1. 变量作用域: - var关键字声明的变量具有函数作用域或全局作用域。 - letconst关键字声明的变量具有块级作用域,即只在包含它们的代码块内有效。

  2. 变量提升(Hoisting): - 使用var声明的变量会进行变量提升,即在代码执行前会被放置在其所在作用域的顶部。但是赋值操作仍然发生在原始位置之后。 - 使用letconst声明的变量不会被提升,它们存在于词法环境中定义的地方。

  3. 重复声明: - 使用var可以多次重复声明同一个变量,而后面的声明会覆盖前面的声明。 - 使用letconst 不能重复声明同一个变量,在同一作用域下再次使用相同名称会引发错误。

  4. 赋值和重新赋值: - 使用 var , let, 和 const 都可以对变量进行赋值。 - 使用 var , 和 let , 可以对已经赋值过的变量进行重新赋值。 - 使用 const , 则对已经赋值过的常量进行重新赋值将引发错误。 至于如何确保通过 const 定义的对象属性不可修改,常见方法有两种:

    4.1. 冻结对象:使用Object.freeze()方法可以冻结整个对象,使其属性不可修改。这样就无法改变对象属性的值、添加新属性或删除现有属性。 javascript const obj = { name: 'John', age: 30 }; Object.freeze(obj); obj.name = 'Mike'; // 报错:Cannot assign to read only property 'name' of object console.log(obj); // 输出: { name: 'John', age: 30 }

    4.2. 使用类似Immutable.js这样的库:Immutable.js 是一个流行且功能强大的 JavaScript 库,它提供了持久性数据类型来处理不可变数据。借助这个库,你可以创建一个不可更改(immutable)版本的对象,并使用该库提供的API来执行修改操作而不更改原始对象本身。 javascript import { Map } from 'immutable'; const immutableObj = Map({ name: 'John', age: 30 }); const updatedObj = immutableObj.set('name', 'Mike'); console.log(updatedObj.get('name')); // 输出: Mike console.log(immutableObj.get('name')); // 输出: John

5.总结来说: - let和const都是ES6引入的块级作用域变量;而var是旧版JavaScript中使用函数级或全局级作用域定义变量。 - let允许重新赋值但不能重复定义;而const定义常量时必须立即初始化,并且不能再次赋值。 - 如果希望const定义的对象属性也不能被修改,则可使用Object.freeze()冻结整个对象或借助Immutable.js等库创建不可更改版本来管理数据。

6. 堆和栈的区别

堆和栈是计算机内存中两种不同的数据分配方式,它们之间有以下区别:

  1. 数据结构: - 栈(Stack)通常采用线性结构,它是一种后进先出(LIFO)的数据结构。在栈中插入或删除元素都发生在栈顶。 - 堆(Heap)通常采用树状结构,它是一种动态分配内存空间的方式。堆可以通过指针相互连接的方式来组织和访问数据。

  2. 分配方式: - 栈内存(Stack Memory)由编译器自动分配和释放,用于存储局部变量、函数调用和程序执行过程中的临时数据。 - 堆内存(Heap Memory)需要手动进行分配和释放。通常用于存储动态创建的对象、大型数据结构等。

  3. 空间大小: - 栈内存通常具有固定大小,并且由系统自动管理。 - 堆内存通常没有固定大小,并且必须手动管理其分配和释放。

  4. 访问速度: - 栈内存的读写速度更快,因为它使用了简单、紧凑的数据结构,并且具有固定大小。 - 堆内存的读写速度较慢,因为它使用了复杂、散列的数据结构,并且可能会存在数据碎片化问题。

  5. 生命周期: - 栈变量在其所属函数或代码块执行结束后自动被销毁。 - 堆中分配的对象不会自动销毁,直到显式地释放其占用的内存空间。

  6. 使用场景: - 栈主要用于处理函数调用、局部变量等短时间存在并频繁创建和销毁的数据。 - 堆主要用于保存大量对象或需要长期存在而不易预测生命周期的数据。

总结来说: - 栈是一种先进后出(LIFO)的线性数据结构,用于处理函数调用和局部变量等短期数据。 - 堆是一种通过指针相互连接组织和访问数据的非线性结构,在堆中进行对象创建和释放,并适合保存长期存在而生命周期难以预测的大量数据。

7. 闭包的原理

闭包是指函数能够访问其词法作用域外部的变量的一种特性。具体原理如下:

  1. 作用域链:每当JavaScript函数被创建时,会同时创建一个作用域链(Scope Chain)。作用域链是一个保存所有可访问变量和函数的列表。

  2. 词法作用域:JavaScript中的作用域是通过词法作用域(Lexical Scope)确定的,即根据代码在书写时定义的位置来确定变量的可访问范围。

  3. 内部函数:在JavaScript中,函数可以嵌套在其他函数内部,形成内部函数。内部函数可以访问外部函数的变量和参数。

  4. 函数引用:当内部函数引用了外部函数中的变量或参数时,就会形成闭包。闭包本质上是由一个函数和对该函数所处词法环境(包括其自身定义的变量和外部环境中可访问到的变量)的引用组成。

  5. 保留状态:闭包允许外部函数中声明的变量保持在内存中,并且不受外界干扰。这意味着即使外部函数已经执行完毕,闭包仍然可以使用并访问它们。

  6. 生命周期延长:由于闭包保留了对外部变量和参数的引用,当存在对闭包的引用时,这些变量将一直存在于内存中,直到没有任何对它们的引用为止。

简而言之,闭包通过将内部函数与其所处环境绑定在一起来实现对外层作用域中变量和参数的访问和保留。这使得我们能够创建类似私有变量、封装数据等高级功能,并且允许我们在程序运行期间动态地操作这些数据。

8. instanceof的实现原理

instanceof 是 JavaScript 中的一个运算符,用于检查一个对象是否属于某个特定类或构造函数的实例。它的实现原理如下:

  1. 首先,检查对象自身是否具有指定类或构造函数的原型链属性。每个 JavaScript 对象都有一个内部属性 [[Prototype]],它是指向该对象原型的引用。

  2. 如果对象的原型等于指定类或构造函数的原型,则返回 true。这表示对象是该类或构造函数的实例。

  3. 如果对象原型不等于指定类或构造函数的原型,则继续往上遍历原型链,重复步骤 2 直到找到匹配或者遍历到最顶层(Object.prototype)。如果在整个原型链上没有找到匹配,则返回 false。 这种实现方式利用了 JavaScript 的原型继承机制。每个对象都可以通过 __proto__ 属性访问其原型,并通过 Object.getPrototypeOf(obj) 方法获取其原型。通过比较原型链上的对象和指定类或构造函数的原型,我们可以确定其关系。 以下是一个使用 instanceof 运算符判断对象类型的示例: javascript class Animal { // ... } class Dog extends Animal { // ... } const animal = new Animal(); const dog = new Dog(); console.log(animal instanceof Animal); // true console.log(dog instanceof Animal); // true console.log(dog instanceof Dog); // true console.log(animal instanceof Dog); // false

注意:instanceof 运算符只能判断对象是否为某个类或构造函数的实例,不能判断基本数据类型(如数字、字符串、布尔值)是否为包装类型(如 Number、String、Boolean)的实例。

9. JS 数据类型有哪些?

JavaScript的数据类型包括以下几种:

  1. 基本数据类型: - 数值(Number):包括整数和浮点数。 - 字符串(String):表示文本。 - 布尔值(Boolean):表示真或假。 - 空值(Null):表示一个没有值的特殊类型。 - 未定义(Undefined):表示未赋值的变量。

  2. 引用数据类型: - 对象(Object):一组键值对的集合。可以是内置对象、自定义对象或者由其他对象派生出来的对象。 - 数组(Array):表示一组有序的数据集合。 - 函数(Function):具有可执行代码块的对象,可以被调用执行。

除了上述基本类型和引用类型外,还存在一些特殊的数据类型,如Symbol、BigInt等。Symbol是ES6中引入的一种新的数据类型,它表示唯一标识符。BigInt是ES2020中引入的一种新的数据类型,用于表示任意精度整数。

需要注意的是,在JavaScript中变量是无需声明其类型的,而是根据其值自动推断出对应的数据类型。例如:

javascript let num = 10; // number let str = "Hello"; // string let bool = true; // boolean let obj = {name: "John", age: 30}; // object let arr = [1, 2, 3]; // array function sayHello() { console.log("Hello!"); } typeof num; // "number" typeof str; // "string" typeof bool; // "boolean" typeof obj; // "object" typeof arr; // "object" (数组也属于对象) typeof sayHello; // "function"

这里使用typeof操作符可以获取变量或表达式的数据类型。

10.JQuery实现链式调用的原理是什么

jQuery实现链式调用的原理是利用每个jQuery方法返回的是一个jQuery对象本身,这样就可以在该对象上继续调用其他方法,形成链式调用。 具体原理如下:

  1. 每个jQuery方法都返回一个包含一组DOM元素的jQuery对象。这个对象拥有与其所选取的元素相关联的一些属性和方法。

  2. 当调用一个jQuery方法时,它会对所选取的元素进行相应操作,并返回包含这些元素的新的jQuery对象。

  3. 返回的jQuery对象仍然可以调用其他jQuery方法,因为它们也返回了相同类型的jQuery对象。

  4. 这样就形成了一条连续地使用不同方法对DOM元素进行操作并且返回结果相互关联、可以无限延伸的链式调用。

例如: javascript $("div") // 选择所有div元素,返回一个包含这些div元素的jQuery对象 .addClass("highlight") // 在这些div元素上添加highlight类,并返回修改后的jQuery对象 .css("color", "red") // 将这些div元素文本颜色设置为红色,并返回修改后的jQuery对象 .slideUp(); // 对这些div元素进行向上滑动动画效果,并返回修改后的jQuery对象 通过链式调用,我们可以在不创建多个中间变量或重复选择DOM元素的情况下,便捷地对一组DOM元素进行多个操作。同时,链式调用也提高了代码可读性和简洁性。

11.分别介绍一下原型、原型链、作用域和作用域链的含义和使用场景

原型(Prototype)是JavaScript中的一个重要概念,每个对象都有一个原型。原型是一个对象,包含了共享的属性和方法。当访问对象的属性或方法时,如果该对象本身没有,则会通过原型链去查找。 原型链(Prototype Chain)是由一系列链接起来的对象组成的,当访问某个对象的属性或方法时,如果该对象自身没有,则会沿着原型链向上查找,直到找到对应的属性或方法。 作用域(Scope)指定义变量和访问变量的范围。在JavaScript中,作用域可以分为全局作用域和函数作用域。全局作用域是指在整个程序执行过程中都可以访问的变量,在全局作用域中声明的变量可以被程序任何地方使用。函数作用域是指在函数内部定义的变量只在该函数内部有效,在函数外部无法访问。 作用域链(Scope Chain)描述了一个函数嵌套结构下所形成的多层级链式关系,它决定了每个执行上下文中变量和函数的可见性和可访问性。当在当前执行上下文中访问一个变量时,如果当前环境没有该变量,则会通过作用域链逐级向上查找,直到找到该变量或者到达全局作用域。 使用场景: - 原型和原型链:通过原型机制实现继承。可以使用原型来共享方法和属性,并且可以通过原型链进行属性和方法的继承。 - 作用域和作用域链:控制变量和函数声明的可见性及生命周期。合理利用作用域划分代码模块以及避免命名冲突;同时,在嵌套结构较深时能够正确地获取外层环境定义的数据。 总结: - 原型与原型链主要涉及面向对象编程、继承、共享等概念; - 作用域与作用于连主要与变量/函数声明、可见性、生命周期等相关; - 它们都是JavaScript语言核心概念之一,在编写JavaScript代码时需要充分理解并运用好这些概念。