2023前端面试题总结:JavaScript篇完整版(二):https://developer.aliyun.com/article/1415760
如何自己实现call,apply,bind函数?
实现 call 函数:
Function.prototype.myCall = function(context, ...args) { context = context || window; // 如果未传入上下文,则使用全局对象 const uniqueKey = Symbol(); // 使用一个唯一的键来避免覆盖原有属性 context[uniqueKey] = this; // 将当前函数设置为上下文的属性 const result = context[uniqueKey](...args); // 调用函数并传递参数 delete context[uniqueKey]; // 删除添加的属性 return result; // 返回函数执行的结果 }; function greet(name) { console.log(`Hello, ${name}! I am ${this.name}.`); } const person = { name: "Alice" }; greet.myCall(person, "Bob"); // 输出:Hello, Bob! I am Alice.
实现 apply 函数:
Function.prototype.myApply = function(context, args) { context = context || window; const uniqueKey = Symbol(); context[uniqueKey] = this; const result = context[uniqueKey](...args); delete context[uniqueKey]; return result; }; function greet(name) { console.log(`Hello, ${name}! I am ${this.name}.`); } const person = { name: "Alice" }; greet.myApply(person, ["Bob"]); // 输出:Hello, Bob! I am Alice.
实现 bind 函数:
Function.prototype.myBind = function(context, ...args1) { const originalFunction = this; return function(...args2) { context = context || window; const combinedArgs = args1.concat(args2); // 合并传入的参数 return originalFunction.apply(context, combinedArgs); }; }; function greet(name) { console.log(`Hello, ${name}! I am ${this.name}.`); } const person = { name: "Alice" }; const boundGreet = greet.myBind(person); boundGreet("Bob"); // 输出:Hello, Bob! I am Alice.
需要注意,使用 Symbol
来确保添加到上下文中的属性不会与现有属性冲突。这只是一种简化的实现,实际的 call、apply 和 bind 方法还包括更多的错误处理和边界情况考虑。
什么是异步编程,有何作用?
异步编程是一种编程范式,用于处理在执行过程中需要等待的操作,如网络请求、文件读写、数据库查询等。在传统的同步编程中,代码会按顺序一行一行执行,如果遇到需要等待的操作,整个程序可能会被阻塞,直到操作完成。而异步编程允许程序在执行等待操作的同时继续执行其他任务,以提高程序的效率和响应性。
异步编程的作用有以下几点:
提高响应性:
在等待长时间操作(如网络请求)时,程序不会被阻塞,用户仍然可以与界面交互,提升用户体验。优化资源利用:
在等待 IO 操作时,CPU 不会被空闲浪费,可以继续执行其他任务,充分利用系统资源。降低延迟:
在需要等待的操作完成后,程序可以立即继续执行,而不需要等待整个操作序列完成。并行处理:
异步编程使得程序能够同时执行多个任务,从而更有效地利用多核处理器。
在 JavaScript 中,异步编程通常使用以下方式来实现:
回调函数:
将需要在操作完成后执行的代码封装在一个回调函数中,并在操作完成后调用回调函数。Promise:
通过 Promise 对象封装异步操作,并通过链式调用 .then() 方法处理操作完成后的结果。async/await:
使用 async 和 await 关键字来编写更具同步风格的异步代码,使代码更易读和维护。事件监听:
将需要在操作完成时执行的代码绑定到事件,当事件触发时执行相应的代码。
异步编程对于处理现代 Web 应用中的网络请求、数据库查询、文件操作等任务非常重要。它能够提高应用的性能、响应性和用户体验,同时还有助于避免程序因等待操作而阻塞。
什么是Promise,如何使用?
Promise 是 JavaScript 中处理异步操作的一种机制,它表示一个异步操作的最终完成(或失败),并可以在操作完成后进行处理。Promise 提供了一种更优雅和结构化的方式来处理回调地狱(Callback Hell),使异步代码更加可读和可维护。
一个 Promise 可以处于以下三个状态之一:
Pending(进行中):
初始状态,表示异步操作正在进行中。Fulfilled(已完成):
表示异步操作已成功完成,结果值可用。Rejected(已拒绝):
表示异步操作失败,错误信息可用。
使用 Promise 的基本流程如下:
- 创建一个 Promise 对象,传入一个执行函数,该函数接收两个参数:resolve 和 reject。
- 在执行函数中执行异步操作,并根据操作结果调用 resolve 或 reject。
以下是一个简单的使用 Promise 的示例:
function fetchData() { return new Promise((resolve, reject) => { setTimeout(() => { const data = { message: "Data fetched successfully" }; if (data) { resolve(data); // 成功时调用 resolve 并传递数据 } else { reject(new Error("Failed to fetch data")); // 失败时调用 reject 并传递错误信息 } }, 1000); }); } // 使用 Promise fetchData() .then((result) => { console.log(result.message); // 处理成功的情况 }) .catch((error) => { console.error(error.message); // 处理失败的情况 });
在这个示例中,fetchData 函数返回一个 Promise 对象。通过 .then() 方法处理异步操作成功的情况,通过 .catch() 方法处理异步操作失败的情况。
如何自己实现一个Promise?
实现一个完整的 Promise 并不是一件简单的事情,因为 Promise 的内部需要处理异步操作的状态管理、回调函数的注册和调用、错误处理等复杂逻辑。以下是一个简化版本的 Promise 实现,用于演示其基本原理:
class MyPromise { constructor(executor) { this.state = "pending"; // 初始状态 this.value = undefined; // 保存异步操作的结果 this.callbacks = []; // 存储成功和失败回调 const resolve = (value) => { if (this.state === "pending") { this.state = "fulfilled"; // 成功状态 this.value = value; this.callbacks.forEach(callback => callback.onFulfilled(value)); } }; const reject = (reason) => { if (this.state === "pending") { this.state = "rejected"; // 失败状态 this.value = reason; this.callbacks.forEach(callback => callback.onRejected(reason)); } }; try { executor(resolve, reject); } catch (error) { reject(error); } } then(onFulfilled, onRejected) { if (this.state === "fulfilled") { onFulfilled(this.value); } else if (this.state === "rejected") { onRejected(this.value); } else if (this.state === "pending") { this.callbacks.push({ onFulfilled, onRejected }); } } } // 使用自己实现的 Promise const promise = new MyPromise((resolve, reject) => { setTimeout(() => { resolve("Promise resolved"); // reject("Promise rejected"); }, 1000); }); promise.then( value => console.log("Fulfilled:", value), reason => console.error("Rejected:", reason) );
这只是一个简化的 Promise 实现,它涵盖了 pending、fulfilled 和 rejected 状态以及 then 方法的基本逻辑。实际的 Promise 还需要考虑更多的细节,如链式调用、错误处理、异步操作的处理等。要在生产环境中使用,建议使用原生的 Promise 或第三方库来获得更完善和稳定的功能。
什么是async/await,如何使用?
async/await 是 JavaScript 中用于处理异步操作的一种现代化的语法特性。它使得异步代码的编写和理解更接近于同步代码,使得异步操作的流程更加清晰和可读。async/await 是建立在 Promise 之上的一种语法糖,用于更方便地处理 Promise 的链式调用。
async:
- async 是一个关键字,用于定义一个函数,该函数返回一个 Promise 对象。
- async 函数内部可以包含 await 表达式,用于暂停函数的执行,直到 await 后的表达式完成并返回结果。
await:
- await 是一个关键字,只能在 async 函数内部使用。
- await 后面跟着一个返回 Promise 对象的表达式,该表达式可以是异步函数调用、Promise 对象等。
- 在 await 后的表达式完成之前,函数的执行会被暂停,直到 Promise 对象状态变为 fulfilled。
使用 async/await 的基本示例:
function fetchData() { return new Promise(resolve => { setTimeout(() => { resolve("Data fetched successfully"); }, 1000); }); } async function fetchAndPrintData() { try { const data = await fetchData(); // 等待异步操作完成 console.log(data); } catch (error) { console.error("Error:", error); } } fetchAndPrintData(); // 调用 async 函数
在这个示例中,fetchAndPrintData 是一个 async 函数,内部使用 await 暂停了执行,直到 fetchData 异步操作完成。使用 try/catch 来处理异步操作的结果或错误。
async/await和Promise有何区别,有何优势?
async/await 和 Promise 都是用于处理异步操作的方式,但它们在语法和使用上有一些区别,以及一些优势和劣势。
区别:
语法:
- Promise 使用链式调用的方式,通过 .then() 和 .catch() 来处理异步操作的结果和错误。
- async/await 使用更类似同步代码的语法,通过 async 关键字定义异步函数,内部使用 await 暂停执行,并使用 try/catch 处理错误。
返回值:
- Promise 的 .then() 和 .catch() 方法返回的仍然是 Promise 对象,允许进行链式调用。
- async/await 中,await 后的表达式返回的是 Promise 的结果,而 async 函数本身返回的也是一个 Promise 对象。
优势:
可读性:
async/await 的语法更接近同步代码,使异步操作的流程更加清晰和易读,减少了回调地狱(Callback Hell)的问题。
错误处理:
在 Promise 中,错误处理需要使用 .catch() 方法,而 async/await 可以使用 try/catch 来处理错误,使得错误处理更加直观和统一。
调试:
async/await 在调试时更容易跟踪代码执行流程,因为它更接近同步代码的写法。
链式调用:
Promise 的链式调用对于一系列的异步操作很有用,但可能会造成代码深度嵌套,难以维护。
async/await 也可以链式调用,但代码的可读性更高,不易产生回调地狱。
并发:
使用 Promise.all() 可以同时处理多个 Promise,在 async/await 中可以使用 Promise.all() 实现类似的功能。
选择使用时的考虑:
- 如果代码中已经广泛使用了 Promise,并且不打算重构,那么继续使用 Promise 可能更合适。
- 如果希望代码更易读、易于调试,并且要处理复杂的异步操作流程,那么可以考虑使用 async/await。
需要注意的是,async/await 内部仍然基于 Promise,因此它们并不是互斥的,而是可以相互配合使用的。
怎么理解JavaScript中的面向对象?
JavaScript 中的面向对象(Object-Oriented)编程是一种编程范式,它将程序的组织方式从简单的函数和数据结构转变为更加模块化和抽象化的方式。在面向对象编程中,程序被组织为一组对象,每个对象都具有属性(数据)和方法(函数),这些对象可以相互交互和协作,从而构建出更复杂的系统。
面向对象编程的主要概念包括:
类(Class):
类是对象的蓝图或模板,定义了对象的属性和方法。类描述了对象的特征和行为,可以看作是一个抽象的概念。对象(Object):
对象是类的实例,具有类定义的属性和方法。对象是面向对象编程中的基本单位,代表现实世界中的实体。封装(Encapsulation):
封装是将数据和操作封装在一个对象中,隐藏对象的内部细节,只暴露必要的接口供外部使用。继承(Inheritance):
继承是通过创建一个新的类来扩展已有的类,子类继承了父类的属性和方法,并可以添加自己的属性和方法。多态(Polymorphism):
多态允许不同的类实现相同的接口或方法,并可以以相同的方式进行调用,提高了代码的灵活性和可复用性。
在 JavaScript 中,虽然它是一门基于原型的编程语言,但也支持面向对象编程。通过使用构造函数、原型链、类、继承等特性,可以在 JavaScript 中实现面向对象的编程风格。ES6 引入了更现代的类和继承语法,使得 JavaScript 的面向对象编程更加清晰和直观。
以下是一个简单的 JavaScript 面向对象编程的示例:
// 定义一个类 class Animal { constructor(name) { this.name = name; } speak() { console.log(`${this.name} makes a sound.`); } } // 定义一个子类继承自 Animal class Dog extends Animal { speak() { console.log(`${this.name} barks.`); } } // 创建对象并调用方法 const dog = new Dog("Buddy"); dog.speak(); // 输出:Buddy barks.
前端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库
JavaScript中的垃圾回收机制是什么?
JavaScript 中的垃圾回收(Garbage Collection)是一种自动管理内存的机制,用于检测和回收不再使用的内存,以防止内存泄漏和资源浪费。在 JavaScript 中,开发人员不需要手动释放内存,因为垃圾回收机制会自动处理不再使用的内存对象。
JavaScript 中的垃圾回收主要基于以下两个概念:
引用计数垃圾回收:
- 引用计数是一种最简单的垃圾回收机制,它通过维护每个对象的引用计数来判断对象是否可达。
- 当一个对象被引用时,它的引用计数加一;当一个对象的引用被移除时,它的引用计数减一。
- 如果一个对象的引用计数变为零,说明没有任何引用指向它,该对象不再可达,垃圾回收机制会将其回收。
标记-清除垃圾回收:
- 标记-清除是一种更高级的垃圾回收算法,通过判断对象是否可达来确定对象是否应该回收。
- 垃圾回收器首先从根对象(如全局对象、活动函数的局部变量等)开始,标记所有可达对象。
- 之后,垃圾回收器会遍历所有对象,清除没有被标记为可达的对象,将其内存回收。
需要注意的是,引用计数垃圾回收在解决循环引用(两个对象相互引用)方面存在问题,因为即使对象之间相互引用,它们的引用计数也不会变为零。而标记-清除垃圾回收则能够处理循环引用。
现代 JavaScript 引擎通常使用基于标记-清除的垃圾回收机制。例如,V8 引擎(Chrome、Node.js)采用了分代垃圾回收策略,将对象分为新生代和老生代,以更高效地进行垃圾回收。垃圾回收是一种重要的机制,它确保在代码执行期间不会出现内存泄漏问题,提高了应用的性能和稳定性。
JavaScript有哪些情况会导致内存泄漏?
JavaScript 中的内存泄漏是指程序中已不再使用的内存没有被正确释放,导致内存占用不断增加,最终可能导致应用程序的性能下降甚至崩溃。以下是一些可能导致内存泄漏的常见情况:
循环引用:
当两个或多个对象彼此相互引用,而且没有其他对象引用它们,就会形成循环引用。这会阻止垃圾回收器回收这些对象,导致内存泄漏。
未清理的定时器和事件监听:
如果使用 setTimeout、setInterval 或 addEventListener 注册了定时器或事件监听器,但在不再需要时未进行清理,这些定时器和监听器将继续持有对象的引用,阻止垃圾回收。
闭包:
闭包可以使函数内部的变量在函数执行结束后仍然被引用,导致这些变量的内存无法释放。特别是在循环中创建闭包时,可能会导致内存泄漏。
未使用的全局变量:
如果创建了全局变量,但没有及时销毁或解除引用,这些变量将继续占用内存,即使在不再需要它们的情况下也是如此。
DOM 元素引用:
在 JavaScript 中,保留对 DOM 元素的引用,即使这些元素从页面中删除,也会导致内存泄漏。必须确保在不需要 DOM 元素时解除引用。
大量缓存:
缓存数据和对象可以提高性能,但如果缓存不受限制地增长,会导致内存泄漏。必须定期清理缓存,删除不再需要的数据。
忘记释放资源:
例如,在使用了底层资源(如数据库连接、文件句柄等)后,没有显式地关闭或释放这些资源,可能导致资源泄漏。
循环引用的事件处理器:
如果在 DOM 元素上注册了事件处理器,而这些事件处理器引用了其他对象,且这些对象又引用了相同的 DOM 元素,可能会导致循环引用,阻止垃圾回收。
为了避免内存泄漏,开发者应该注意适时释放不再需要的资源、解除引用,确保定时器和事件监听器的正确清理,以及注意循环引用的情况。使用浏览器的开发者工具和内存分析工具可以帮助识别内存泄漏问题。
什么是防抖,节流函数,如何实现?
防抖(Debouncing)和节流(Throttling)是两种常用的优化技术,用于限制频繁触发的事件的执行次数,从而提升性能和用户体验。
防抖
是指在事件触发后,等待一段时间(例如 200 毫秒),如果在这段时间内没有再次触发事件,那么执行相应的操作。如果在等待时间内又触发了事件,那么等待时间会被重置。节流
是指在事件触发后,一段时间内只执行一次相应的操作。无论触发多少次事件,都会在每个固定的时间间隔内执行一次。
防抖的实现:
function debounce(func, delay) { let timerId; return function (...args) { clearTimeout(timerId); timerId = setTimeout(() => { func.apply(this, args); }, delay); }; } // 使用示例 const debouncedFn = debounce(() => { console.log('Debounced function called'); }, 200); window.addEventListener('scroll', debouncedFn);
节流的实现:
function throttle(func, delay) { let lastTime = 0; return function (...args) { const currentTime = new Date().getTime(); if (currentTime - lastTime >= delay) { func.apply(this, args); lastTime = currentTime; } }; } // 使用示例 const throttledFn = throttle(() => { console.log('Throttled function called'); }, 200); window.addEventListener('scroll', throttledFn);
浅拷贝,深拷贝是什么?如何实现一个深拷贝函数?
浅拷贝和深拷贝是两种不同的对象复制方式,涉及到对象的引用关系。下面分别对浅拷贝和深拷贝进行解释:
浅拷贝:
在浅拷贝中,创建一个新对象,然后将原始对象的属性值复制到新对象中。如果属性值是基本类型,那么直接复制其值;如果属性值是对象或数组等引用类型,那么只是复制其引用,新对象和原始对象共享这些引用。浅拷贝只复制对象的一层属性。深拷贝:
在深拷贝中,递归地复制一个对象及其嵌套的所有属性,直到所有嵌套的属性都是基本类型为止。这样创建了一个完全独立的对象副本,原始对象和新对象之间没有任何引用关系。
以下是一个简单的深拷贝函数的示例:
function deepClone(obj) { if (obj === null || typeof obj !== 'object') { return obj; } let clone = Array.isArray(obj) ? [] : {}; for (let key in obj) { if (obj.hasOwnProperty(key)) { clone[key] = deepClone(obj[key]); } } return clone; } const originalObj = { a: 1, b: { c: 2 }, d: [3, 4] }; const copiedObj = deepClone(originalObj); console.log(copiedObj); // 深拷贝后的新对象 console.log(copiedObj === originalObj); // false,新对象和原始对象无关联 console.log(copiedObj.b === originalObj.b); // false,嵌套对象也是深拷贝
需要注意的是,上述深拷贝函数只是一个简单的实现示例,可能在某些特定情况下存在性能问题。在实际项目中,可以使用成熟的深拷贝工具库,如 lodash 的 cloneDeep 方法,来处理深拷贝的需求。
深拷贝如何解决循环引用的问题?
深拷贝是在创建一个对象的副本时,递归地复制其所有嵌套属性和子对象。在深拷贝过程中,如果对象存在循环引用,即某个对象引用了自身或与其他对象形成了循环引用关系,那么简单的递归拷贝可能会导致无限循环,甚至造成内存溢出。
为了解决循环引用问题,你可以在深拷贝过程中使用一些策略。以下是一种常见的方法
- 使用一个缓存对象来跟踪已经被拷贝的对象,以及它们在新对象中的引用。这可以防止无限递归。
- 在每次拷贝一个对象之前,先检查缓存对象,如果该对象已经被拷贝,则直接返回缓存中的引用,而不是递归拷贝。
下面是一个使用JavaScript实现深拷贝并解决循环引用问题的示例
function deepCopyWithCycles(obj, cache = new WeakMap()) { if (obj === null || typeof obj !== 'object') { return obj; } if (cache.has(obj)) { return cache.get(obj); } if (obj instanceof Date) { return new Date(obj); } if (obj instanceof RegExp) { return new RegExp(obj); } const copy = Array.isArray(obj) ? [] : {}; cache.set(obj, copy); for (const key in obj) { if (obj.hasOwnProperty(key)) { copy[key] = deepCopyWithCycles(obj[key], cache); } } return copy; } const objA = { name: 'Object A' }; const objB = { name: 'Object B' }; objA.circularRef = objA; objB.circularRef = objA; const copiedObjA = deepCopyWithCycles(objA); console.log(copiedObjA.name); // 输出: Object A console.log(copiedObjA.circularRef.name); // 输出: Object A console.log(copiedObjA.circularRef === copiedObjA); // 输出: true console.log(copiedObjA.circularRef === copiedObjA.circularRef.circularRef); // 输出: true
在上面的示例中,deepCopyWithCycles 函数使用了一个 WeakMap 来缓存已经拷贝的对象,以及它们在新对象中的引用。这样,即使对象存在循环引用,也可以正确地处理。同时,这个函数也考虑了处理特殊类型如 Date 和 RegExp。
什么是事件委托,为什么要使用事件委托?
事件委托(Event Delegation)是一种在 Web 开发中常用的事件处理技术,它通过将事件监听器绑定到父元素而不是每个子元素上,以达到优化性能、简化代码和处理动态内容的目的。
事件委托的原理是利用了事件冒泡机制。当子元素上的事件被触发时,该事件会向上冒泡到父元素,父元素可以捕获并处理这个事件。通过在父元素上监听事件,可以捕获到子元素触发的事件,从而实现对子元素的事件处理。
事件委托的优势和原因:
减少事件监听器的数量:
当页面上存在大量的子元素时,为每个子元素都绑定事件监听器会增加内存占用和性能开销。使用事件委托可以减少事件监听器的数量,只需在父元素上绑定一个监听器。动态添加的元素也能被处理:
对于通过 JavaScript 动态添加到页面的元素,无需再次绑定事件监听器,因为它们是父元素的后代,事件会冒泡到父元素。性能优化:
由于事件监听器较少,减少了事件冒泡的层级,可以提升页面的响应速度和性能。方便维护:
在有大量相似子元素的情况下,使用事件委托可以简化代码,使代码更易维护和理解。
如何实现数组扁平化?
- 递归方法:使用递归遍历数组的每个元素,如果元素是数组,则递归处理;如果是基本类型,则添加到结果数组中。
function flattenArray(arr) { const result = []; for (const item of arr) { if (Array.isArray(item)) { result.push(...flattenArray(item)); } else { result.push(item); } } return result; } const nestedArray = [1, [2, 3, [4, 5]], 6]; const flattenedArray = flattenArray(nestedArray); console.log(flattenedArray); // [1, 2, 3, 4, 5, 6]
- 使用 Array.prototype.flat 方法(ES6):flat 方法用于将嵌套的数组扁平化,可以指定扁平化的深度,默认为 1。
const nestedArray = [1, [2, 3, [4, 5]], 6]; const flattenedArray = nestedArray.flat(Infinity); console.log(flattenedArray); // [1, 2, 3, 4, 5, 6]
- 使用 reduce 方法:使用 reduce 方法将多维数组逐一展开。
function flattenArray(arr) { return arr.reduce((result, item) => { return result.concat(Array.isArray(item) ? flattenArray(item) : item); }, []); } const nestedArray = [1, [2, 3, [4, 5]], 6]; const flattenedArray = flattenArray(nestedArray); console.log(flattenedArray); // [1, 2, 3, 4, 5, 6]
- 使用扩展运算符(ES6):使用扩展运算符和递归来实现数组扁平化。
function flattenArray(arr) { return [].concat(...arr.map(item => Array.isArray(item) ? flattenArray(item) : item)); } const nestedArray = [1, [2, 3, [4, 5]], 6]; const flattenedArray = flattenArray(nestedArray); console.log(flattenedArray); // [1, 2, 3, 4, 5, 6]
前端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库
什么是函数柯里化?
函数柯里化(Currying)是一种将一个接受多个参数的函数转化为一系列接受一个参数的函数的技术。通过函数柯里化,可以将一个函数的调用过程分解为一系列较小的函数调用,每个函数接受一个参数,并返回一个新的函数,直至所有参数都被收集完毕,最后返回最终结果。
函数柯里化的主要思想是,将多参数函数转化为一系列只接受一个参数的函数,这样可以在每个步骤中捕获部分参数,生成一个新的函数来处理这些参数,最终产生一个累积所有参数的结果。
function add(a) { return function(b) { return a + b; }; } const add5 = add(5); // 第一个参数 5 固定 console.log(add5(3)); // 输出 8,传入第二个参数 3
在这个示例中,add 函数接受一个参数 a,然后返回一个新的函数,这个新函数接受参数 b,并返回 a + b 的结果。通过调用 add(5),可以固定第一个参数为 5,然后返回一个接受一个参数的函数 add5。随后调用 add5(3),将参数 3 传递给 add5 函数,得到结果 8。
函数柯里化的好处包括:
- 参数复用:可以将一些参数在多个场景中复用,生成新的函数。
- 函数组合:可以更方便地将多个函数组合在一起,实现更复杂的功能。
- 延迟执行:可以在柯里化的过程中,只传递一部分参数,然后在后续调用中传递剩余参数,实现延迟执行。
什么是函数的链式调用?
函数的链式调用是指通过在函数调用的结果上连续调用其他函数,形成一个函数调用的链条。每次调用都会返回一个新的对象或值,从而允许在一个连续的调用序列中执行多个操作。链式调用在很多 JavaScript 库和框架中广泛使用,它能够使代码更具有可读性和流畅性。
链式调用的好处在于可以在一个语句中完成多个操作,从而减少了中间变量的使用,使代码更加紧凑和易于理解。这种方式尤其在操作对象或调用方法链时非常有用。
class Calculator { constructor(value = 0) { this.value = value; } add(num) { this.value += num; return this; // 返回实例以支持链式调用 } subtract(num) { this.value -= num; return this; } multiply(num) { this.value *= num; return this; } divide(num) { this.value /= num; return this; } getValue() { return this.value; } } const result = new Calculator(10) .add(5) .multiply(2) .subtract(3) .divide(2) .getValue(); console.log(result); // 输出 7,(((10 + 5) * 2 - 3) / 2)
在上述示例中,Calculator 类具有一系列的方法,每个方法都会修改实例的属性并返回实例自身,以支持链式调用。通过链式调用,可以在一行中完成一系列的数学操作,使代码更加简洁和易读。
什么是严格模式,开启严格模式有什么作用?
严格模式(Strict Mode)是一种在 JavaScript 中的一种运行模式,它使得代码在执行时遵循更严格的语法和规则,从而减少一些常见的错误,并提供更好的错误检测和调试机制。
要开启严格模式,可以在脚本的顶部或函数的内部添加以下语句:"use strict"
开启严格模式有以下作用:
- 禁止使用全局变量:在严格模式下,不允许隐式地创建全局变量。如果没有通过 var、let 或 const 声明的变量被赋值,会抛出引用错误。
- 禁止删除变量:在严格模式下,使用 delete 关键字删除变量或函数会导致语法错误。
- 禁止重复的参数名:在严格模式下,函数参数不能有重复的名称,否则会导致语法错误。
- 禁止使用八进制字面量:在严格模式下,不允许使用八进制字面量(以 0 开头)。
- 禁止对只读属性赋值:在严格模式下,不允许对只读属性赋值,否则会抛出类型错误。
- 强制 this 为 undefined:在严格模式下,函数内部的 this 不会自动指向全局对象,而是为 undefined。
- 禁止使用 with 语句:在严格模式下,不允许使用 with 语句,因为它会导致代码的不确定性和难以维护性。
更严格的错误检测:严格模式提供了更严格的错误检测机制,例如对未声明的变量赋值会抛出引用错误,而非严格模式下会创建一个全局变量。
ES6新特性
前端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库
ES6之后出现了哪些新特性?
- 箭头函数(Arrow Functions):更简洁的函数定义语法,绑定了词法作用域中的 this。
- let 和 const 关键字:引入了块级作用域的变量声明方式,let 用于声明可变变量,const 用于声明常量。
- 模板字面量(Template Literals):更方便的字符串拼接方式,支持多行字符串和嵌入表达式。
- 解构赋值(Destructuring Assignment):通过模式匹配从数组或对象中提取值并赋给变量。
- 默认参数值(Default Parameters):在函数声明时为参数提供默认值,简化函数调用。
- 扩展操作符(Spread Operator):用于数组、对象等的展开操作,可用于复制、合并等。
- 剩余参数(Rest Parameters):允许将一组参数封装成一个数组,用于处理变长参数列表。
- 类(Classes):引入类和继承的语法糖,更接近传统的面向对象编程语言。
- Promise:提供了一种更优雅的处理异步操作的方式,用于处理回调地狱。
- 模块(Modules):引入了模块化的语法,允许将代码拆分成多个文件并进行导入和导出。
- 迭代器和生成器(Iterators and Generators):迭代器用于迭代数据结构,生成器用于简化异步编程。
- Symbol:引入了一种原始数据类型,表示唯一的标识符,用于对象属性的键名。
- Map 和 Set:引入了 Map 和 Set 数据结构,提供更灵活的数据存储和查找方式。
- 数组和对象的新方法:引入了许多用于处理数组和对象的新方法,如 map、filter、find、includes 等。
- 函数的扩展:引入了箭头函数、bind、call、apply 等函数的新特性和用法。
- 模块化加载:通过 import 和 export 关键字实现模块化的代码加载。
- Proxy 和 Reflect:Proxy 对象用于创建一个拦截目标对象操作的代理,Reflect 对象提供了操作对象的方法,可以用于代替一些原生对象方法。
- Async/Await:基于 Promise 的语法糖,更方便地编写异步代码,使其更接近同步代码的写法。
- Iterator 和 for...of 循环:引入了迭代器协议和 for...of 循环,使遍历数据结构变得更加简洁和灵活。
- 函数式编程特性:引入了许多函数式编程的特性,如箭头函数、map、filter、reduce 等,使得处理数据更加方便和高效。
- 数组的方法:ES6 引入了许多新的数组方法,如 find、findIndex、some、every 等,用于更方便地处理数组数据。
- 字符串的新方法:引入了一些新的字符串方法,如 startsWith、endsWith、includes、模板字符串中的标签函数等。
- 数字的新方法:引入了一些新的数字方法,如 Number.isNaN、Number.isInteger、Number.parseFloat 等。
- Map 和 Set 的方法:Map 和 Set 数据结构也引入了一些新的方法,如 keys、values、entries 等。
- 对象的新方法:对象也引入了一些新的方法,如 Object.assign、Object.keys、Object.values、Object.entries 等。
- 模板标签函数:可以在模板字符串前使用自定义函数,对模板进行处理,用于实现字符串的定制化输出。
- 默认导出和命名导出:在模块中可以使用 export default 和 export 来定义默认导出和命名导出。
- BigInt:引入了 BigInt 数据类型,用于表示任意精度的整数。
- 函数参数的默认值:函数参数可以指定默认值,简化函数调用时的参数传递。
- 函数参数的解构赋值:函数参数可以使用解构赋值,直接从传入的对象中提取属性值。
let、const、var的区别?
作用域:
var:使用 var 声明的变量具有函数作用域,即在函数内部声明的变量在整个函数体内都可见。
let 和 const:使用 let 和 const 声明的变量具有块级作用域,即在 {} 块内声明的变量只在该块内有效。
声明提升:
var:var 声明的变量会发生声明提升,即变量的声明会被提升至当前作用域的顶部,但初始化的值会保留在原位置。
let 和 const:虽然也会发生声明提升,但使用 let 和 const 声明的变量在初始化之前是不可访问的。
重复声明:
var:可以重复声明同名变量,不会引发错误。
let 和 const:不能在同一个作用域内重复声明同名变量,否则会引发错误。
可变性:
var 和 let:声明的变量是可变的,可以重新赋值。
const:声明的变量是常量,一旦赋值就不能再修改。
全局对象属性:
var 声明的变量会成为全局对象的属性,例如在浏览器环境中是 window 的属性。
let 和 const 声明的变量不会成为全局对象的属性。
临时死区:
let 和 const 声明的变量会在声明之前存在一个“临时死区”,在该区域内访问变量会引发错误。
箭头函数与普通函数的区别是什么?
语法简洁性:
- 箭头函数的语法更为简洁,可以在一些情况下省略大括号、return 关键字和参数括号。
- 普通函数的语法相对繁琐,需要使用 function 关键字、大括号和参数括号。
this 绑定:
- 箭头函数的 this 绑定是词法作用域的,它会捕获当前上下文中的 this 值,无法通过 call、apply 或 bind 改变。
- 普通函数的 this 绑定是动态的,取决于函数的调用方式和上下文。
arguments 对象:
- 箭头函数没有自己的 arguments 对象,它会继承外层作用域的 arguments 对象(如果有的话)。
- 普通函数具有自己的 arguments 对象,其中包含了函数调用时传递的参数。
构造函数:
- 箭头函数不能用作构造函数,无法通过 new 关键字实例化对象。
- 普通函数可以用作构造函数,可以通过 new 关键字实例化对象。
递归:
- 由于箭头函数没有自己的 arguments 对象和函数名称,所以递归调用时相对不方便。
- 普通函数可以更方便地进行递归调用。
- 前端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库
命名函数表达式:
- 箭头函数不能被命名,只能使用匿名函数表达式。
- 普通函数可以被命名,也可以使用匿名函数表达式。
扩展运算符的作用及使用场景有哪些?
扩展运算符(Spread Operator)是 ES6 引入的一种语法,用于展开(拆分)可迭代对象(如数组、字符串、对象等)为独立的元素,以便在函数调用、数组字面量、对象字面量等地方使用。扩展运算符的作用和使用场景包括以下几点:
函数调用中的参数传递:
扩展运算符可以用于将一个数组展开为一个函数的参数列表,这对于传递动态数量的参数非常有用。
function add(a, b, c) { return a + b + c; } const numbers = [1, 2, 3]; const result = add(...numbers); // 等同于 add(1, 2, 3)
数组字面量中的元素合并:
扩展运算符可以将一个数组的元素合并到另一个数组中。
const array1 = [1, 2, 3]; const array2 = [4, 5, 6]; const mergedArray = [...array1, ...array2]; // 合并为 [1, 2, 3, 4, 5, 6]
复制数组和对象:
扩展运算符可以用于浅复制数组和对象,创建一个新的数组或对象,而不是引用同一个内存地址。
const originalArray = [1, 2, 3]; const copiedArray = [...originalArray]; // 创建新数组,值相同但引用不同 const originalObject = { key1: 'value1', key2: 'value2' }; const copiedObject = { ...originalObject }; // 创建新对象,值相同但引用不同
字符串转为字符数组:
扩展运算符可以将字符串转换为字符数组,以便逐个访问字符。
const str = 'hello'; const chars = [...str]; // 转为字符数组 ['h', 'e', 'l', 'l', 'o']
对象字面量中的属性合并:
扩展运算符可以将一个对象的属性合并到另一个对象中。
const obj1 = { a: 1, b: 2 }; const obj2 = { c: 3, d: 4 }; const mergedObject = { ...obj1, ...obj2 }; // 合并为 { a: 1, b: 2, c: 3, d: 4 }
模板字符串如何使用?
模板字符串(Template Strings)是 ES6 引入的一种字符串语法,它允许在字符串中插入变量、表达式以及换行符,使字符串的拼接和格式化更加方便。模板字符串使用反引号()来界定字符串,并在 `${}`` 内部插入表达式或变量。
const name = 'Alice'; const age = 30; // 使用模板字符串插入变量和表达式 const greeting = `Hello, my name is ${name} and I am ${age} years old.`; console.log(greeting); // 输出:Hello, my name is Alice and I am 30 years old.
模板字符串还支持多行文本:
const multilineText = ` This is a multiline text that spans across multiple lines. It's easy to format and read. `; console.log(multilineText); /* 输出: This is a multiline text that spans across multiple lines. It's easy to format and read. */
前端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库
Set方法有哪些应用场景?
Set 是 ES6 引入的一种数据结构,它类似于数组,但不允许有重复的值,且没有固定的索引。Set 内部的值是唯一的,不会存在重复的元素。Set 对象的方法和特性使它在许多场景下非常有用,以下是一些常见的 Set 方法的应用场景:
去重:
最常见的用途就是用于去除数组中的重复元素。将数组转换为 Set,再将 Set 转换回数组,就能轻松去重。
const array = [1, 2, 2, 3, 3, 4, 5]; const uniqueArray = [...new Set(array)]; // 去重后的数组:[1, 2, 3, 4, 5]
判断元素是否存在:
使用 Set 的 has 方法可以快速判断一个元素是否存在于集合中。
const set = new Set([1, 2, 3]); console.log(set.has(2)); // true console.log(set.has(4)); // false
交集、并集、差集等操作:
通过将多个 Set 对象转换为数组,然后利用数组的方法进行交集、并集、差集等操作。
const set1 = new Set([1, 2, 3]); const set2 = new Set([2, 3, 4]); const intersection = [...set1].filter(item => set2.has(item)); // 交集:[2, 3] const union = [...set1, ...set2]; // 并集:[1, 2, 3, 4] const difference = [...set1].filter(item => !set2.has(item)); // 差集:[1]
存储任意类型的数据:
Set 可以存储任意类型的const set1 = new Set([1, 2, 3]);
const set2 = new Set([2, 3, 4]);
const intersection = [...set1].filter(item => set2.has(item)); // 交集:[2, 3]
const union = [...set1, ...set2]; // 并集:[1, 2, 3, 4]
const difference = [...set1].filter(item => !set2.has(item)); // 差集:[1]
- 数据,包括基本数据类型和对象等。
const mixedSet = new Set(); mixedSet.add(1); mixedSet.add('hello'); mixedSet.add({ key: 'value' });
迭代:
使用 for...of 循环可以迭代 Set 中的每个元素,且顺序与添加顺序一致。
const set = new Set([1, 2, 3]); for (const item of set) { console.log(item); }
前端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库
defienProperty与proxy有何作用,区别是什么?
Object.defineProperty:
Object.defineProperty 是一个用于直接在一个对象上定义一个新属性或修改现有属性的方法。它允许你精确地控制属性的各种特性,如可枚举性、可配置性、可写性等。主要作用如下:
- 定义新属性或修改现有属性的特性。
- 可以通过 get 和 set 方法实现属性的自定义读取和写入操作。
- 适用于修改已有对象的属性特性。
const obj = {}; Object.defineProperty(obj, 'name', { value: 'Alice', writable: false, // 不可写 enumerable: true, // 可枚举 configurable: true // 可配置 });
Proxy:
Proxy 是 ES6 引入的一种代理机制,用于拦截对象上的操作,提供了更强大的操作对象的能力。通过使用 Proxy,可以捕获并自定义对象的各种操作,如读取属性、写入属性、删除属性、函数调用等。主要作用如下:
- 拦截对象的各种操作,实现自定义逻辑。
- 用于创建一个代理对象,而不是直接修改现有对象
const target = { name: 'Alice' }; const handler = { get: function(target, property) { console.log(`Getting property ${property}`); return target[property]; } }; const proxy = new Proxy(target, handler); console.log(proxy.name); // 触发 get 操作,并输出 "Getting property name"
区别:
- Object.defineProperty 只能修改现有对象的属性特性,而 Proxy 则是创建一个代理对象,可以捕获和拦截各种操作。
- Object.defineProperty 只能拦截属性的读取和写入操作,而 Proxy 可以拦截更多的操作,包括函数调用、删除属性等。
- Object.defineProperty 的适用范围更窄,主要用于修改现有对象的属性特性。Proxy 则可以在更底层、更广泛地拦截和修改对象操作。
综上所述,Object.defineProperty 和 Proxy 在功能上有一定的重叠,但 Proxy 提供了更灵活和强大的能力,适用于更广泛的对象操作需求
Object.assign和扩展运算符是浅拷贝还是深拷贝?
Object.assign 方法和扩展运算符都是进行浅拷贝(Shallow Copy),而不是深拷贝(Deep Copy)。
浅拷贝
是指在拷贝对象时,只拷贝对象的一层属性,而不会递归地拷贝嵌套对象的属性。拷贝后的对象和原始对象会共享嵌套对象的引用。
深拷贝
是指在拷贝对象时,会递归地拷贝所有嵌套对象的属性,从而创建一个完全独立的副本,两者之间没有引用关系。
使用 Object.assign 进行浅拷贝:
const originalObj = { a: 1, b: { c: 2 } }; const copiedObj = Object.assign({}, originalObj); console.log(copiedObj); // { a: 1, b: { c: 2 } } console.log(copiedObj === originalObj); // false console.log(copiedObj.b === originalObj.b); // true,嵌套对象的引用相同
使用扩展运算符进行浅拷贝:
const originalObj = { a: 1, b: { c: 2 } }; const copiedObj = { ...originalObj }; console.log(copiedObj); // { a: 1, b: { c: 2 } } console.log(copiedObj === originalObj); // false console.log(copiedObj.b === originalObj.b); // true,嵌套对象的引用相同
ES6和commonJS的导入导出有何区别?
ES6 模块:
ES6 引入了一种新的模块系统,它在语言层面提供了模块的支持。ES6 模块的导入和导出使用 import 和 export 关键字。
- 导出:使用 export 关键字将某个值、变量、函数或类导出为一个模块。
// 导出单个值 export const myVariable = 42; // 导出多个值 export { foo, bar };
- 导入:使用 import 关键字引入其他模块导出的内容。
// 导入单个值 import { myVariable } from './myModule'; // 导入多个值 import { foo, bar } from './myModule';
CommonJS:
CommonJS 是一种用于服务器端 JavaScript 的模块系统,它被广泛用于 Node.js。CommonJS 使用 require 来导入模块,使用 module.exports 或 exports 来导出模块。
- 导出:使用 module.exports 或 exports 将一个值、变量、函数或类导出为一个模块。
// 导出单个值 module.exports = 42; // 导出多个值 exports.foo = foo; exports.bar = bar;
- 导入:使用 require 来引入其他模块导出的内容。
// 导入单个值 const myVariable = require('./myModule'); // 导入多个值 const { foo, bar } = require('./myModule');
区别:
- 编程语言层面 vs 运行时层面:ES6 模块是 JavaScript 语言规范的一部分,在语言层面提供了模块支持。而 CommonJS 是一种运行时模块系统,主要用于服务器端。
- 静态 vs 动态:ES6 模块的导入和导出在静态分析阶段就能确定,因此可以进行静态优化。而 CommonJS 的导入和导出是在运行时动态执行的。
- 浏览器环境:ES6 模块在现代浏览器中得到了支持。CommonJS 在浏览器中需要使用工具(如 Browserify 或 webpack)进行转换。
- 导入导出语法:ES6 模块使用 import 和 export,而 CommonJS 使用 require 和 module.exports。
前端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库