JavaScript面试题系列(4)每篇10题

1.对 Promise 的理解

Promise 是 JavaScript 中处理异步操作的一种机制。它代表了一个异步操作的最终完成或失败,并可以返回一个值。Promise 对象具有以下特点:

  1. 状态(State):Promise 有三种状态,分别是 pending(进行中)、fulfilled(已完成)和 rejected(已失败)。初始状态是 pending,当异步操作完成时,Promise 可以变为 fulfilled 状态,表示操作成功;如果异步操作失败,则变为 rejected 状态。

  2. 值的传递:Promise 在状态改变时可以传递一个值。当 Promise 变为 fulfilled 状态时,会将最终的结果值传递给后续处理函数;当 Promise 变为 rejected 状态时,会传递一个错误对象给后续处理函数。

  3. 链式调用:Promise 提供了链式调用的方式,可以通过 .then() 方法来注册处理成功的回调函数,通过 .catch() 方法来注册处理失败的回调函数。这种链式调用的方式可以有效地处理异步操作的结果。

下面是一个简单的示例来说明 Promise 的使用:

function fetchData() {
  return new Promise((resolve, reject) => {
    // 异步操作
    setTimeout(() => {
      const data = 'Some data';
      if (data) {
        resolve(data); // 异步操作成功
      } else {
        reject('Error'); // 异步操作失败
      }
    }, 2000);
  });
}

fetchData()
  .then(result => {
    console.log('成功:', result);
  })
  .catch(error => {
    console.log('失败:', error);
  });

在这个示例中,fetchData() 函数返回一个 Promise 对象。当异步操作完成时,通过调用 resolve(data) 来表示成功,并传递最终的结果值;如果异步操作失败,则调用 reject('Error') 来表示失败,并传递错误信息。然后可以通过 .then() 方法注册成功的回调函数,在回调函数中处理成功的结果值;通过 .catch() 方法注册失败的回调函数,在回调函数中处理失败的情况。

Promise 的优势在于它提供了一种更优雅、可读性更高的方式来处理异步操作,避免了回调地狱(callback hell)的问题,使异步代码更易于理解和维护。

2.对于async 函数的理解

 Async 函数是 JavaScript 中用于处理异步操作的一种特殊函数。它的定义通过在函数前加上 async 关键字来标识。Async 函数使用起来比较简洁和直观,它基于 Promise 对象,并通过 await 关键字来等待 Promise 对象的解析结果。

下面是一个简单的示例来说明 Async 函数的使用:

function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const data = 'Some data';
      if (data) {
        resolve(data);
      } else {
        reject('Error');
      }
    }, 2000);
  });
}

async function getData() {
  try {
    const result = await fetchData();
    console.log('成功:', result);
  } catch (error) {
    console.log('失败:', error);
  }
}

getData();

在这个示例中,fetchData() 函数返回一个 Promise 对象,用于模拟异步操作。getData() 函数是一个 Async 函数,其中使用 await 关键字等待 fetchData() 函数的结果。当 await 表达式等待的 Promise 对象被解析(即状态变为 fulfilled),它将返回 Promise 的解析值,并继续执行后续代码。如果 Promise 被拒绝(即状态变为 rejected),则会抛出一个错误,可以使用 try...catch 来捕获错误并进行处理。

Async 函数的特点如下:

  1. 语法简洁:Async 函数通过使用 asyncawait 关键字,使异步代码的编写更加简洁和直观。它能够让开发者以同步的方式编写异步代码,提高代码的可读性和可维护性。

  2. 基于 Promise:Async 函数基于 Promise 对象,它内部使用了 Promise 的机制来处理异步操作。await 关键字可以等待一个 Promise 对象的解析结果,并将解析值作为返回值。

  3. 错误处理:Async 函数使用 try...catch 来捕获可能抛出的错误,并进行错误处理。在 try 块中使用 await 来等待 Promise 对象的解析结果,如果出现错误,则会被 catch 块捕获并进行相应处理。

Async 函数是处理异步操作的一种简洁且强大的方式,它在现代 JavaScript 开发中广泛应用于处理网络请求、文件操作、数据库查询等需要异步操作的场景。它相比于传统的回调函数和 Promise 链式调用,能够更直观地表达代码的意图,使异步代码更易于编写和维护。

3.Promise 解决了什么问题

Promise 解决了 JavaScript 中处理异步操作时存在的一些问题,包括以下方面:

  1. 回调地狱(Callback Hell):在传统的回调函数方式中,当存在多个异步操作需要依次执行时,代码往往会出现多层嵌套的回调函数,导致代码可读性差、难以理解和维护。这种嵌套的结构被称为回调地狱。Promise 使用链式调用的方式,通过 .then() 方法将回调函数连接起来,避免了回调函数嵌套的问题,使代码结构更加清晰。

  2. 异步操作结果的处理:在传统回调函数中,处理异步操作的结果需要在回调函数中进行处理。这样会导致逻辑散落在多个回调函数中,使代码难以阅读和维护。Promise 提供了一个统一的方式来处理异步操作的结果,通过 .then() 方法注册处理成功的回调函数,通过 .catch() 方法注册处理失败的回调函数,使逻辑集中在一个地方,更易于管理和维护。

  3. 错误处理:在传统回调函数中,错误处理通常是通过回调函数的参数来传递错误信息,需要在每个回调函数中手动检查错误。这样容易出现遗漏或混乱的情况。Promise 提供了 .catch() 方法用于捕获异步操作的错误,使错误处理变得更加统一和简单。

  4. 并发异步操作的控制:在某些场景下,需要控制多个异步操作的执行顺序或并发数量。传统回调函数方式中需要手动管理这些异步操作的状态和控制逻辑。Promise 提供了一些方法(例如 Promise.all()Promise.race())来方便地控制并发异步操作的执行顺序和结果处理。

总之,Promise 提供了一种更优雅、更易于理解和维护的方式来处理异步操作,避免了回调地狱问题,统一了异步操作的处理方式,并提供了更好的错误处理和并发控制机制。它在异步编程中扮演着重要的角色,并成为现代 JavaScript 开发中常用的工具之一。

4.对象创建的方式有哪些?

在 JavaScript 中,有多种方式可以创建对象。以下是常见的对象创建方式:

1.对象字面量:使用对象字面量表示法(花括号 {})直接创建对象。

const obj = { 
  key1: value1,
  key2: value2,
  // ...
};

2.构造函数:通过定义构造函数来创建对象,并使用 new 关键字实例化对象。

function Person(name, age) {
  this.name = name;
  this.age = age;
}

const person = new Person('John', 25);

3.Object.create():使用 Object.create() 方法创建一个新对象,并将其原型指向另一个对象。

const obj = Object.create(proto);

4.类(ES6+):使用类和构造函数的概念创建对象,通过 class 关键字定义类,并使用 new 实例化对象。

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}

const person = new Person('John', 25);

 5.工厂函数:通过定义一个函数,函数内部创建并返回一个对象。

function createPerson(name, age) {
  return {
    name: name,
    age: age,
  };
}

const person = createPerson('John', 25);

 5.对象继承的方式有哪些? 

在 JavaScript 中,有多种方式可以实现对象之间的继承。以下是常见的对象继承方式:

1.原型链继承:通过让一个对象的原型指向另一个对象,实现继承关系。这样,子对象可以访问父对象的属性和方法。

function Parent() {
  this.name = 'Parent';
}
Parent.prototype.sayHello = function() {
  console.log('Hello');
};
function Child() {
  this.name = 'Child';
}
Child.prototype = new Parent();
const child = new Child();
child.sayHello(); // 输出: Hello

2.构造函数继承:通过在子类的构造函数中调用父类的构造函数,实现属性的继承。这样,每个实例都会拥有自己的属性副本。

function Parent(name) {
  this.name = name;
}
function Child(name) {
  Parent.call(this, name);
}
const child = new Child('Child');
console.log(child.name); // 输出: Child

3.组合继承:结合原型链继承和构造函数继承的方式,实现同时继承属性和方法。

function Parent(name) {
  this.name = name;
}

Parent.prototype.sayHello = function() {
  console.log('Hello');
};

function Child(name) {
  Parent.call(this, name);
}

Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;

const child = new Child('Child');
child.sayHello(); // 输出: Hello
console.log(child.name); // 输出: Child

4.ES6 类继承:使用 class 关键字和 extends 关键字实现类继承,子类继承父类的属性和方法。 

class Parent {
  constructor(name) {
    this.name = name;
  }

    sayHello() {
    console.log('Hello');
  }
}
class Child extends Parent {
  constructor(name) {
    super(name);
  }
}
const child = new Child('Child');
child.sayHello(); // 输出: Hello
console.log(child.name); // 输出: Child

6.哪些情况会导致内存泄漏 

内存泄漏是指应用程序中分配的内存无法被垃圾回收机制释放,导致内存占用持续增加,最终耗尽可用内存的情况。以下是一些常见导致内存泄漏的情况:

  1. 无限制的引用:对象之间形成了循环引用,导致垃圾回收器无法判断对象是否可回收。例如,两个对象互相引用,并且没有其他引用指向它们之外。

  2. 未及时清理的计时器和事件监听器:未及时清理的定时器、间隔器或事件监听器可能会导致对象持续存在内存中。如果定时器或事件监听器没有正确地被取消或移除,对象将继续保持活动状态并占用内存。

  3. 大量缓存数据:缓存数据通常存储在内存中,如果缓存数据没有被正确管理和清理,它们会持续占用内存空间,导致内存泄漏。

  4. 未关闭的数据库连接或文件句柄:在使用数据库连接或文件句柄时,如果忘记关闭它们,这些资源将一直占用内存,导致内存泄漏。

  5. 不正确使用的闭包:在 JavaScript 中,闭包是一种强大的特性,但如果不正确使用,可能会导致内存泄漏。在闭包中引用的外部变量不会被垃圾回收,除非闭包被显式地释放。

  6. DOM 引用:保留对 DOM 元素的引用,而这些元素已经不再需要时,可能会导致内存泄漏。在移除或替换 DOM 元素时,应确保相关的引用被正确清理。

  7. 大对象的创建和销毁:如果频繁创建和销毁大型对象,可能会导致内存泄漏。这是因为垃圾回收器可能无法及时回收大对象所占用的内存。

以上只是一些常见的情况,导致内存泄漏的原因还有其他多种情况。要避免内存泄漏,开发人员应该注意正确地管理内存资源,包括释放不再使用的对象、及时清理计时器和事件监听器、关闭不再需要的连接和句柄等。使用开发者工具和内存分析工具可以帮助识别和解决潜在的内存泄漏问题。

7.闭包的理解和优缺点

闭包是指函数以及其被声明时的词法环境的组合。简单来说,闭包是一个函数,它可以访问自身词法作用域以及包含它的外部作用域的变量。

理解闭包的概念需要了解以下几个要点:

  1. 词法作用域:JavaScript 中的作用域是由函数定义时所处的位置决定的。函数内部可以访问外部函数或全局作用域的变量,但外部函数或全局作用域不能访问函数内部的变量。这种嵌套的作用域链形成了闭包的基础。

  2. 函数作为值:在 JavaScript 中,函数可以作为值进行传递和赋值。我们可以将一个函数作为另一个函数的参数,也可以将函数赋值给变量。

  3. 函数内部的函数:JavaScript 允许在一个函数内部定义另一个函数。内部函数可以访问外部函数的变量,即使外部函数已经执行完毕,这种访问依然存在。

闭包的优点包括:

  1. 封装性:闭包允许创建私有变量和函数,通过闭包内部的作用域,可以隐藏一些数据和实现细节,只暴露出有限的接口。这有助于模块化开发和防止命名冲突。

  2. 数据持久性:闭包可以使函数访问其词法作用域外部的变量,即使外部函数执行完毕,这些变量依然可以被访问。这使得在函数外部创建的数据在函数执行完毕后仍然保持存在,可以实现对数据的持久化操作。

  3. 实现回调和异步操作:闭包在处理回调和异步操作时非常有用。可以将函数作为参数传递给其他函数,并在内部函数中访问外部函数的变量。这使得在异步环境中,内部函数仍然可以访问到它所需的上下文数据。

然而,闭包也有一些缺点:

  1. 内存消耗:由于闭包会保留对外部作用域的引用,可能会导致内存占用过高,特别是在使用闭包的地方创建了大量的函数实例时。如果闭包没有正确释放,可能会导致内存泄漏。

  2. 性能影响:由于闭包涉及访问外部作用域的变量,需要进行额外的作用域链查找,这可能导致一定的性能开销。

  3. 滥用的潜在问题:闭包可以被滥用,过度使用闭包可能会导致代码复杂性增加,可读性和维护性降低。需要慎重使用闭包,确保其带来的优点超过潜在的缺点。

因此,在使用闭包时,需要仔细权衡其优缺点,并确保合理使用,避免潜在的问题。

8.说一下JS中的原型链的理解:是什么,有什么用,怎么用,优缺点 

原型链是 JavaScript 中实现对象继承的机制。它基于对象之间的原型关系,允许对象通过继承来访问和共享属性和方法。理解原型链需要了解以下几个概念:

  1. 原型对象:每个 JavaScript 对象都有一个原型对象(prototype)。原型对象是一个普通的对象,它包含可供对象实例共享的属性和方法。

  2. [[Prototype]] 链:每个对象在内部都有一个指向其原型对象的链接([[Prototype]])。当我们访问对象的属性或方法时,如果对象本身没有这个属性或方法,JavaScript 引擎会沿着原型链向上查找,直到找到或者到达原型链的顶端(null)。

原型链的用途包括:

  1. 属性和方法的继承:通过原型链,对象可以继承其原型对象的属性和方法。当访问对象的属性或方法时,如果对象本身没有,则会在其原型对象中查找,直到找到相应的属性或方法。

  2. 实现对象的共享属性和方法:通过将属性和方法定义在原型对象上,多个对象实例可以共享相同的属性和方法。这样可以减少内存消耗,提高代码的效率和可维护性。

  3. 动态属性和方法的添加和修改:通过修改原型对象,可以动态地添加和修改对象的属性和方法,而不需要修改每个对象实例的定义。

使用原型链时,可以通过以下方式定义和使用:

  1. 构造函数和原型对象:通过构造函数创建对象,使用 new 关键字实例化对象。在构造函数内部,可以定义对象实例的属性。通过将属性和方法定义在构造函数的原型对象上,可以实现属性和方法的继承和共享。

  2. 访问原型对象的属性和方法:通过对象实例的 __proto__ 属性或 Object.getPrototypeOf() 方法,可以访问对象的原型对象。可以使用点语法或方括号语法来访问原型对象中的属性和方法。

原型链的优点包括:

  1. 实现了对象的继承:原型链允许对象通过继承共享属性和方法,减少了重复代码的编写。

  2. 支持动态添加和修改属性和方法:通过修改原型对象,可以动态地添加和修改对象的属性和方法,对于已经存在的对象实例也能够反映变化。

  3. 提高了代码的效率和可维护性:原型链允许多个对象实例共享相同的属性和方法,减少了内存消耗和代码冗余,提高了代码的效率和可维护性。

原型链的缺点包括:

  1. 继承的共享特性:通过原型链继承,属性和方法是共享的,如果修改了原型对象的属性和方法,会影响到所有继承自该原型对象的对象实例。

  2. 原型链的层级嵌套:当原型链的层级嵌套过深时,查找属性和方法的效率可能会降低,因为需要一层层向上查找。

  3. 隐式依赖关系:使用原型链继承时,对象的属性和方法可能存在隐式依赖关系,当原型链的结构复杂或修改不当时,可能会引发意外的问题。

需要注意的是,在现代 JavaScript 中,还有其他实现对象继承的方式,如使用类和 extends 关键字来实现继承。原型链是一种基于原型的继承机制,在某些场景下可能不是最优选择。开发人员应根据实际需求和情况,选择合适的继承方式。

9.说一下JS继承(含ES6的)的理解--或者人家这样问有两个类A和B,B怎么继承A? 

在 JavaScript 中,可以使用不同的方式实现继承,包括原型链继承、构造函数继承、组合继承和 ES6 中的类继承。下面我将详细介绍 ES6 中类继承的概念和使用方式。

在 ES6 中,可以使用 class 关键字和 extends 关键字来实现类继承。类继承是一种基于原型的继承方式,它允许子类继承父类的属性和方法,并且可以添加自己的属性和方法。

以下是一个示例,演示了如何使用 ES6 类继承实现 B 类继承 A 类的过程:

class A {
  constructor(propA) {
    this.propA = propA;
  }

  methodA() {
    console.log('Method A');
  }
}

class B extends A {
  constructor(propA, propB) {
    super(propA);
    this.propB = propB;
  }

  methodB() {
    console.log('Method B');
  }
}

const instanceB = new B('Property A', 'Property B');
instanceB.methodA(); // 输出: Method A
instanceB.methodB(); // 输出: Method B
console.log(instanceB.propA); // 输出: Property A
console.log(instanceB.propB); // 输出: Property B

 在上面的示例中,我们定义了两个类 A 和 B。类 B 使用 extends 关键字继承了类 A。在类 B 的构造函数中,我们使用 super 关键字调用父类 A 的构造函数,并传递需要的参数。这样,在实例化 B 类时,会调用 A 类的构造函数来初始化父类的属性。

通过类继承,类 B 继承了类 A 的属性和方法,并且可以在类 B 中添加自己的属性和方法。在示例中,instanceB 是类 B 的实例,可以调用继承自类 A 的 methodA() 方法和类 B 自身的 methodB() 方法。同时,instanceB 也具有继承自类 A 的 propA 属性和类 B 自身的 propB 属性。

ES6 类继承的优点包括:

  1. 语法简洁:使用 classextends 关键字,可以以更清晰、直观的方式定义类和实现继承关系。

  2. 支持构造函数:通过 super 关键字,在子类的构造函数中调用父类的构造函数,方便进行属性的初始化。

  3. 支持实例和静态方法:类继承可以继承父类的实例方法和静态方法,使得代码更加组织化和结构化。

  4. 易于理解和维护:类继承提供了更接近传统面向对象编程的语法,使得代码更易于理解、调试和维护。

需要注意的是,虽然 ES6 中的类继承提供了便利和语法糖,但其本质上还是基于原型链的继承。在某些情况下,原型链继承可能更适合特定的需求。开发人员应根据具体情况和需求,选择合适的继承方式。

10.说一下JS原生事件如何绑定

在 JavaScript 中,可以使用多种方式来绑定原生事件。下面是几种常见的方法:

addEventListener() 方法:addEventListener() 是 DOM 元素的方法,用于添加事件监听器。它接受两个参数:事件类型和事件处理函数。可以通过多次调用 addEventListener() 来绑定多个事件监听器。

const element = document.getElementById('myElement');

function handleClick(event) {
  console.log('点击事件触发');
}

element.addEventListener('click', handleClick);

on 事件属性:DOM 元素还提供了一系列的 on 事件属性,如 onclickonkeydownonmouseover 等。可以将事件处理函数直接赋值给相应的 on 事件属性。

const element = document.getElementById('myElement');

function handleClick(event) {
  console.log('点击事件触发');
}

element.onclick = handleClick;

注意,使用 on 事件属性绑定事件处理函数时,只能同时绑定一个处理函数。如果多次赋值给同一个 on 事件属性,后续的赋值会覆盖之前的赋值。

HTML 属性:在 HTML 元素上可以直接使用内联的事件属性来绑定事件处理函数。通过在元素标签中添加 on 前缀的属性,如 onclickonkeydownonmouseover 等。

<button onclick="handleClick(event)">点击按钮</button>

<script>
  function handleClick(event) {
    console.log('点击事件触发');
  }
</script>

需要注意的是,使用 HTML 属性绑定事件处理函数时,函数名需要是全局可访问的,因此需要确保函数已经定义在全局作用域中。

这些是常见的绑定原生事件的方式。使用 addEventListener() 方法是推荐的方式,它提供了更灵活的事件绑定和解绑能力,可以同时绑定多个事件监听器,并且不会覆盖之前的绑定。但根据项目需求和个人偏好,可以选择合适的方式来绑定原生事件。

猜你喜欢

转载自blog.csdn.net/weixin_52003205/article/details/131744337
今日推荐