2025面试大全(34)

109 阅读49分钟

1. '1'.toString()为什么不会报错?

在JavaScript中,'1'.toString()不会报错的原因是因为字符串类型在JavaScript中有一个toString()方法。这个方法用于将字符串转换为字符串,虽然这看起来似乎有些多余,但它确实存在,并且可以调用。 当你对字符串'1'调用toString()方法时,它实际上返回的是字符串'1'本身。因此,这个操作是有效的,不会引发错误。 然而,如果你尝试对数字字面量(而不是数字对象)直接调用toString()方法,比如1.toString(),那么会报错。这是因为JavaScript解析器会将点号.解释为小数点,而不是方法调用的部分。所以1.toString()会被解析为1.,后面跟着toString(),这显然是不合法的语法。 为了避免这种错误,你可以将数字字面量转换为对象,然后再调用toString()方法,例如:

(1).toString(); // 返回 '1'
Number(1).toString(); // 返回 '1'

这样就不会报错了,因为现在你是在对一个数字对象调用toString()方法。 总结来说,'1'.toString()不会报错是因为字符串类型有toString()方法,而数字字面量直接调用toString()会报错是因为语法解析的问题。

2. null是对象吗?为什么?

是的,null 在 JavaScript 中被视为对象。这是由于 JavaScript 的历史遗留问题导致的。 在 JavaScript 的最初版本中,设计者希望将 null 作为一种特殊的对象值,用于表示“缺少值”或“默认情况”。因此,null 被设计为可以转换为对象,但本身不包含任何属性或方法。 然而,这种设计选择后来被认为是一个错误。因为 null 本质上应该表示“没有对象”,而不是一个特殊的对象值。但为了保持向后兼容性,这个错误一直保留至今。 这就是为什么当你使用 typeof 操作符检查 null 的类型时,会得到 "object" 的结果:

console.log(typeof null); // 输出 "object"

尽管 null 被视为对象,但它并不具有对象的基本特征,比如没有原型(null 的原型是 null itself)和不能添加属性。因此,在实际使用中,null 通常用于表示缺少值或默认情况,而不是作为一个真正的对象来使用。 需要注意的是,虽然 null 被视为对象,但它并不属于任何具体的对象子类型,如数组或函数。它只是一个特殊的对象值,用于表示“没有值”或“默认情况”。

3. 什么是内存泄漏?什么原因会导致呢?

内存泄漏是指在程序运行过程中,由于疏忽或错误导致程序未能释放已经不再使用的内存。内存泄漏会导致可用内存逐渐减少,从而影响程序的性能,甚至导致程序崩溃。 内存泄漏的原因多种多样,以下是一些常见的原因

  1. 忘记释放内存
    • 程序员在手动管理内存时,可能忘记释放已分配的内存。
  2. 循环引用
    • 在对象之间存在循环引用时,垃圾回收器可能无法正确判断这些对象是否不再被使用,从而导致内存泄漏。
  3. 全局变量
    • 全局变量在程序运行期间始终存在,如果不当使用全局变量存储大量数据,可能导致内存泄漏。
  4. 闭包
    • 闭包可以捕获外部函数的变量,如果闭包长时间存在而不被释放,其捕获的变量也会导致内存泄漏。
  5. 事件监听器
    • 如果在对象上添加了事件监听器,但在对象不再需要时没有移除这些监听器,可能导致内存泄漏。
  6. DOM引用
    • 在Web开发中,如果JavaScript代码中保留了对DOM元素的引用,而这些元素已经从DOM树中移除,可能导致内存泄漏。
  7. 定时器
    • 使用setTimeout或setInterval设置的定时器,如果不在适当的时候清除,也可能导致内存泄漏。
  8. 第三方库或框架
    • 使用第三方库或框架时,如果库或框架本身存在内存泄漏问题,也会影响到使用它们的程序。 如何避免内存泄漏
  • 避免全局变量,使用局部变量。
  • 及时清理不再需要的事件监听器、定时器和DOM引用。
  • 注意闭包的使用,避免长时间持有无用变量。
  • 使用现代JavaScript引擎的垃圾回收机制,减少手动管理内存的需求。
  • 定期进行性能分析和内存泄漏检测。 通过良好的编程习惯和适当的工具辅助,可以有效地减少内存泄漏的发生。

4. 怎么进行移动端的调试?

移动端调试是开发移动应用或移动网页时的重要环节。以下是一些常用的移动端调试方法:

1. 使用移动设备直接调试

  • 开发者选项:在Android设备上,可以启用“开发者选项”来启用USB调试,然后通过USB连接到电脑进行调试。
  • Safari开发者工具:对于iOS设备,可以使用Safari的开发者工具。在Mac上打开Safari,然后在“偏好设置”>“高级”中启用“显示开发菜单”。通过USB连接iOS设备后,可以在开发菜单中找到设备并进行调试。
  • Chrome远程调试:Chrome浏览器提供了远程调试功能,可以连接Android设备进行调试。

2. 使用模拟器调试

  • Android Studio模拟器:Android Studio自带的模拟器可以模拟各种设备进行调试。
  • Xcode模拟器:Xcode提供了iOS模拟器,可以模拟不同的iOS设备进行调试。
  • 其他第三方模拟器:如Genymotion等,提供了更多的定制选项和性能。

3. 使用浏览器开发者工具

  • Chrome开发者工具:Chrome浏览器提供了强大的开发者工具,可以模拟移动设备的环境进行调试。
  • Firefox开发者工具:Firefox也提供了类似的工具,可以模拟移动设备进行调试。

4. 使用远程调试工具

  • ** Weinre**:是一个远程Web调试工具,可以通过网络连接到移动设备进行调试。
  • ** Eruda**:是一个专为移动端设计的调试面板,可以方便地在移动设备上进行调试。

5. 使用真机调试服务

  • Fiddler:可以捕获和分析移动设备上的网络请求,进行网络层面的调试。
  • Charles:另一个网络抓包工具,可以用于移动设备的网络调试。

6. 使用日志和错误报告

  • Console日志:在移动设备的浏览器或应用中输出日志,可以帮助开发者了解运行情况。
  • 错误报告服务:如Sentry、Bugly等,可以收集和分析移动应用中的错误报告。

7. 使用性能分析工具

  • Android Profiler:Android Studio提供的性能分析工具,可以分析CPU、内存、网络等性能指标。
  • Instruments:Xcode提供的性能分析工具,可以用于iOS应用的性能分析。

调试步骤示例(以Chrome远程调试为例):

  1. 准备设备:确保Android设备已启用USB调试,并连接到电脑。
  2. 打开Chrome:在电脑上打开Chrome浏览器,并进入开发者工具(F12或右键选择“检查”)。
  3. 连接设备:在开发者工具中,点击“远程设备”标签页,然后点击“刷新”以查找连接的设备。
  4. 开始调试:选择要调试的设备上的标签页,然后点击“Inspectable”开始调试。 通过以上方法,可以有效地进行移动端的调试,确保移动应用或移动网页在不同设备上的兼容性和性能。

5. 前后端分离是什么?

前后端分离是一种现代的网页应用开发模式,它将原本整合在一起的前端(用户界面)和后端(服务器逻辑、数据库操作等)分开处理,使得它们可以独立开发、测试和部署。这种模式的主要特点包括:

1. 职责分离

  • 前端:负责页面的布局、交互和视觉效果。前端通常使用HTML、CSS和JavaScript等技术,并且可能会使用前端框架或库(如React、Vue、Angular等)来构建用户界面。
  • 后端:负责业务逻辑、数据存储和数据处理。后端可能使用各种服务器端语言和框架(如Node.js、Python的Django、Ruby on Rails等)来处理请求和提供数据。

2. 接口通信

前后端通过定义清晰的API(如RESTful API、GraphQL等)进行通信。前端通过这些API发送请求到后端,后端处理请求后返回数据给前端。

3. 独立部署

前后端可以独立部署,不需要同时更新。这提高了开发效率,减少了部署时的相互依赖。

4. 技术栈无关

前后端分离允许前端和后端团队选择最适合各自部分的技术栈,而不必受限于对方的技术选择。

5. 提升开发效率

由于前后端开发可以并行进行,而且各自的修改不会直接影响到对方,因此可以显著提高开发效率。

6. 更好的可维护性

分离的架构使得前后端代码更加模块化,易于维护和扩展。

7. 用户体验优化

前端可以更加专注于用户体验的优化,而不必担心后端逻辑的复杂性。

实施前后端分离的挑战:

  • API设计:需要精心设计稳定、可扩展的API。
  • 跨域问题:由于前后端分离,可能会遇到跨域资源共享(CORS)的问题。
  • 性能优化:需要分别对前后端进行性能优化。
  • 团队协作:需要前后端团队密切协作,确保API的一致性和及时更新。 总的来说,前后端分离是一种灵活、高效的应用开发模式,但同时也需要团队在技术、协作和流程上进行相应的调整和优化。

6. 工厂模式

在JavaScript中,工厂模式是一种设计模式,用于创建对象。这种模式通常用于创建具有相似属性和方法的多个对象,而不需要每次都显式地使用构造函数。工厂模式可以提供一个通用的接口来创建对象,允许你根据需要创建不同类型的对象。 以下是一个简单的JavaScript工厂模式的示例:

// 工厂函数
function createPerson(name, age, job) {
  // 创建一个新对象
  var person = {
    name: name,
    age: age,
    job: job,
    sayHello: function() {
      console.log('Hello, my name is ' + this.name + ', I am ' + this.age + ' years old and I am a ' + this.job + '.');
    }
  };
  // 返回新创建的对象
  return person;
}
// 使用工厂函数创建对象
var person1 = createPerson('Alice', 30, 'Developer');
var person2 = createPerson('Bob', 25, 'Designer');
// 调用对象的方法
person1.sayHello(); // 输出: Hello, my name is Alice, I am 30 years old and I am a Developer.
person2.sayHello(); // 输出: Hello, my name is Bob, I am 25 years old and I am a Designer.

在这个例子中,createPerson函数是一个工厂函数,它接受三个参数(nameagejob),并返回一个包含这些属性和一个sayHello方法的新对象。通过调用这个工厂函数,我们可以轻松地创建多个具有相似结构的对象。 工厂模式的优点包括:

  • 解耦:将对象的创建过程与使用过程分离,减少模块间的耦合。
  • 复用:可以复用创建对象的逻辑,避免重复代码。
  • 易于扩展:如果需要创建不同类型的对象,可以轻松地扩展工厂函数或添加新的工厂函数。 然而,工厂模式也有其缺点,比如可能会增加代码的复杂性,并且难以追踪对象的类型和继承关系。在现代JavaScript中,类和构造函数提供了更清晰的方式来创建对象,但在某些情况下,工厂模式仍然是一个有用的工具。

7. 单例模式

单例模式是一种设计模式,它确保一个类只有一个实例,并提供一个全局访问点来访问这个实例。在JavaScript中,单例模式通常用于管理全局状态、配置或资源,如数据库连接、日志服务等。

实现单例模式的基本步骤:

  1. 创建一个类:定义你想要成为单例的类。
  2. 创建一个静态方法:这个方法负责创建类的实例。
  3. 检查实例是否存在:在静态方法中,检查是否已经创建了类的实例。
  4. 返回实例:如果实例存在,返回它;如果不存在,创建一个新的实例并返回。

示例代码:

class Singleton {
  // 单例的实例
  static instance = null;
  // 构造函数
  constructor() {
    // 初始化单例的属性
    this.data = 'Some data';
  }
  // 静态方法,用于获取单例的实例
  static getInstance() {
    if (!Singleton.instance) {
      Singleton.instance = new Singleton();
    }
    return Singleton.instance;
  }
  // 其他方法
  someMethod() {
    console.log(this.data);
  }
}
// 使用单例
const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();
console.log(instance1 === instance2); // true,表明它们是同一个实例

单例模式的特点:

  • 单一实例:确保类只有一个实例。
  • 全局访问:提供一个全局访问点来获取实例。
  • 懒加载:实例在第一次被请求时才创建。

单例模式的应用场景:

  • 管理全局状态:如用户配置、应用设置等。
  • 资源管理:如数据库连接、文件系统访问等。
  • 日志服务:确保全局只有一个日志服务实例。

注意事项:

  • 全局变量:单例模式本质上是一种全局变量,应谨慎使用,避免滥用导致代码难以维护。
  • 并发问题:在多线程环境中,需要考虑线程安全问题,确保不会创建多个实例。
  • 可测试性:单例模式可能会使得单元测试变得复杂,因为全局状态难以模拟和隔离。 单例模式是一种简单但强大的设计模式,正确使用可以带来很多好处,但也要注意其潜在的问题。

8. 数据Mock是什么?

数据Mock,即数据模拟,是一种在开发过程中用于模拟真实数据的技术。它允许开发者在没有实际数据源或实际数据源不可用的情况下,使用模拟数据来进行开发、测试和演示。 数据Mock的常见应用场景包括:

  1. 前端开发:在前端开发中,开发者经常需要模拟后端接口返回的数据,以便能够独立于后端进行开发。通过Mock数据,前端开发者可以快速构建和测试用户界面,而不必等待后端接口的完成。
  2. 测试:在编写自动化测试时,Mock数据可以用来模拟复杂的系统环境或外部依赖,确保测试的稳定性和可重复性。
  3. 演示:在产品演示或原型设计中,Mock数据可以用来展示系统的功能和界面,而不需要实际的数据支持。
  4. API设计:在设计和开发API时,Mock数据可以帮助开发者定义和验证API的接口规范。 数据Mock可以通过多种方式实现,包括:
  • 手动编写Mock数据:开发者可以直接在代码中硬编码模拟数据。
  • 使用Mock框架:有许多专门的框架和工具可以帮助开发者生成和管理Mock数据,例如Mock.js、JSON Server等。
  • API模拟服务:一些在线服务提供了API模拟功能,允许开发者创建、共享和测试Mock API。 数据Mock的优势包括:
  • 提高开发效率:允许前后端并行开发,减少等待时间。
  • 隔离外部依赖:使开发测试不受外部系统的影响。
  • 控制测试环境:可以精确控制测试数据,确保测试的一致性。 然而,过度依赖Mock数据也可能带来一些问题,比如Mock数据可能与真实数据存在差异,导致开发出的功能在接入真实数据时出现问题。因此,合理使用数据Mock,并在适当的时候与真实数据进行集成测试是非常重要的。

9. 设计模式的六大原则

设计模式的六大原则是软件开发中的一些基本指导原则,它们帮助开发者创建更灵活、可维护和可扩展的代码。以下是这些原则的简要描述,以及如何在JavaScript中应用它们:

1. 单一职责原则(Single Responsibility Principle, SRP)

描述:一个类应该只有一个引起它变化的原因,即一个类应该只负责一项功能。 JavaScript示例

// 不符合SRP
class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }
  save() {
    // 保存用户到数据库
  }
  sendEmail() {
    // 发送邮件
  }
}
// 符合SRP
class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }
  save() {
    // 保存用户到数据库
  }
}
class EmailService {
  sendEmail(user) {
    // 发送邮件
  }
}

2. 开放封闭原则(Open/Closed Principle, OCP)

描述:软件实体(类、模块、函数等)应当对扩展开放,对修改封闭。 JavaScript示例

// 不符合OCP
class Rectangle {
  constructor(width, height) {
    this.width = width;
    this.height = height;
  }
  area() {
    return this.width * this.height;
  }
}
// 如果要添加圆形,需要修改area函数
// 符合OCP
class Shape {
  area() {}
}
class Rectangle extends Shape {
  constructor(width, height) {
    super();
    this.width = width;
    this.height = height;
  }
  area() {
    return this.width * this.height;
  }
}
class Circle extends Shape {
  constructor(radius) {
    super();
    this.radius = radius;
  }
  area() {
    return Math.PI * this.radius * this.radius;
  }
}

3. 里氏替换原则(Liskov Substitution Principle, LSP)

描述:子类必须能够替换掉它们的基类,即子类应该继承父类的一切属性和方法,并且可以替换父类而不影响程序的运行。 JavaScript示例

// 符合LSP
class Bird {
  fly() {
    console.log("I can fly!");
  }
}
class Sparrow extends Bird {
  // Sparrow可以替换Bird,因为Sparrow也有fly方法
}
class Ostrich extends Bird {
  fly() {
    throw new Error("I cannot fly!");
  }
  // Ostrich替换Bird会违反LSP,因为它不能飞
}

4. 依赖倒置原则(Dependency Inversion Principle, DIP)

描述:高层模块不应该依赖低层模块,两者都应该依赖抽象;抽象不应该依赖细节,细节应该依赖抽象。 JavaScript示例

// 不符合DIP
class MySQLDatabase {
  connect() {
    // 连接MySQL数据库
  }
}
class UserService {
  constructor() {
    this.db = new MySQLDatabase();
  }
  getUser() {
    this.db.connect();
    // 获取用户数据
  }
}
// 符合DIP
class Database {
  connect() {}
}
class MySQLDatabase extends Database {
  connect() {
    // 连接MySQL数据库
  }
}
class UserService {
  constructor(db) {
    this.db = db;
  }
  getUser() {
    this.db.connect();
    // 获取用户数据
  }
}
// 使用
const db = new MySQLDatabase();
const userService = new UserService(db);

5. 接口隔离原则(Interface Segregation Principle, ISP)

描述:客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。 JavaScript示例

// 不符合ISP
class Animal {
  eat() {}
  fly() {}
  swim() {}
}
class Dog extends Animal {
  fly() {
    throw new Error(" Dogs cannot fly!");
  }
  swim() {
    // Dogs can swim
  }
}
// 符合ISP
class Eat {
  eat() {}
}
class Fly {
  fly() {}
}
class Swim {
  swim() {}
}
class Dog {
  constructor() {
    this.eat = new Eat();
    this.swim = new Swim();
  }
}

10. css加载会造成阻塞吗?

CSS加载会造成阻塞,但具体情况取决于CSS是如何加载的。 1. 同步加载的CSS

  • 当CSS以同步方式加载时(例如,通过<link>标签直接在HTML中引入),它会阻塞页面的渲染。这是因为浏览器需要确保在渲染页面之前,已经应用了所有的CSS规则,以避免出现FOUC(Flash of Unstyled Content,即页面短暂显示无样式的内容)。
  • 同步加载的CSS还会阻塞JavaScript的执行。根据HTML解析规则,浏览器会先解析DOM,然后解析CSSOM,最后执行JavaScript。如果CSS尚未加载完成,浏览器会暂停解析DOM和执行JavaScript,直到CSS加载完毕。 2. 异步加载的CSS
  • 可以通过JavaScript动态创建<link>标签,并设置其rel属性为stylesheet,以异步方式加载CSS。这种方式不会阻塞DOM的解析,但仍然会阻塞页面的渲染,直到CSS加载并解析完成。
  • 还可以使用rel="preload"预加载CSS,这样可以在页面渲染之前加载CSS,但不会立即应用样式。之后,可以再通过JavaScript将预加载的CSS应用到页面中,从而减少渲染阻塞时间。 3. 内联CSS
  • 将CSS直接内联到HTML中,可以避免CSS加载造成的阻塞。但这种方式会增加HTML文件的大小,可能不适用于大型项目。 总结
  • 同步加载的CSS会阻塞页面的渲染和JavaScript的执行。
  • 异步加载的CSS不会阻塞DOM的解析,但仍然会阻塞页面的渲染。
  • 内联CSS可以避免加载阻塞,但可能会增加HTML文件的大小。 为了优化性能,建议尽量减少同步加载的CSS数量,使用异步加载或预加载的方式加载CSS,并在可能的情况下使用内联CSS。同时,还可以利用浏览器缓存、压缩CSS文件等方法进一步减少CSS加载时间。

11. 设计模式分类

设计模式是一种在软件开发中用来解决常见问题的可复用解决方案。它们可以分为三大类:创建型模式、结构型模式和行为型模式。以下是这些模式分类的简要描述以及它们在JavaScript中的实现示例:

创建型模式

创建型模式关注对象的创建过程,旨在创建灵活的、可复用的对象。

  1. 单例模式(Singleton)
    • 确保一个类只有一个实例,并提供一个全局访问点。
    • JavaScript示例:
      class Singleton {
        constructor() {
          if (!Singleton.instance) {
            Singleton.instance = this;
          }
          return Singleton.instance;
        }
      }
      const instance1 = new Singleton();
      const instance2 = new Singleton();
      console.log(instance1 === instance2); // true
      
  2. 工厂模式(Factory)
    • 创建一个对象的接口,但让子类决定要实例化的类是哪一个。
    • JavaScript示例:
      class Dog {
        speak() {
          return 'Woof!';
        }
      }
      class Cat {
        speak() {
          return 'Meow!';
        }
      }
      function AnimalFactory(type) {
        switch (type) {
          case 'dog':
            return new Dog();
          case 'cat':
            return new Cat();
          default:
            return null;
        }
      }
      const myDog = AnimalFactory('dog');
      console.log(myDog.speak()); // 'Woof!'
      
  3. 抽象工厂模式(Abstract Factory)
    • 提供一个接口,用于创建相关或依赖对象的家族,而不需要明确指定具体类。
    • JavaScript示例:
      // Similar to the Factory pattern, but with multiple factories
      
  4. 建造者模式(Builder)
    • 将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
    • JavaScript示例:
      class House {
        constructor() {
          this.doors = 0;
          this.windows = 0;
          this.rooms = 0;
        }
      }
      class HouseBuilder {
        constructor() {
          this.house = new House();
        }
        addDoor() {
          this.house.doors += 1;
          return this;
        }
        addWindow() {
          this.house.windows += 1;
          return this;
        }
        addRoom() {
          this.house.rooms += 1;
          return this;
        }
        build() {
          return this.house;
        }
      }
      const myHouse = new HouseBuilder()
        .addDoor()
        .addWindow()
        .addRoom()
        .build();
      
  5. 原型模式(Prototype)
    • 用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
    • JavaScript示例:
      function Animal(name) {
        this.name = name;
      }
      Animal.prototype.speak = function() {
        return `My name is ${this.name}`;
      };
      const dog = new Animal('Rex');
      const cat = new Animal('Whiskers');
      console.log(dog.speak()); // 'My name is Rex'
      console.log(cat.speak()); // 'My name is Whiskers'
      

结构型模式

结构型模式关注类和对象的组织,旨在通过组合类和对象来形成更大的结构。

  1. 适配器模式(Adapter)
    • 允许将一个类的接口转换成客户期望的另一个接口,适配器让原本接口不兼容的类可以合作无间。
    • JavaScript示例:
      class OldAPI {
        request(data) {
          //... legacy code
        }
      }
      class NewAPI {
        fetch(data) {
          //... new code
        }
      }
      class Adapter {
        constructor() {
          this.newAPI = new NewAPI();
        }
        request(data) {
          return this.newAPI.fetch(data);
        }
      }
      const adapter = new Adapter();
      adapter.request('data');
      
  2. 装饰器模式(Decorator)
    • 动态地给一个对象添加一些额外的职责。
    • JavaScript示例:
      function car(name) {
        this.name = name;
      }
      car.prototype.drive = function() {
        return `${this.name} is driving`;
      };
      function carWithAutoPilot(car) {
        this.car = car;
      }
      carWithAutoPilot.prototype.drive = function() {
        return `${this.car.drive()} with auto-pilot`;
      };
      const myCar = new car('Tesla');
      const myCarWithAutoPilot = new carWithAutoPilot(myCar);
      console.log(myCarWithAutoPilot.drive()); // 'Tesla is driving with auto-pilot'
      

12. 实现有并行限制的Promise调度器

要实现一个有并行限制的Promise调度器,我们需要一个管理器来控制同时执行的Promise数量。以下是一个简单的实现:

class PromiseScheduler {
  constructor(maxLimit) {
    this.maxLimit = maxLimit; // 最大并行数
    this.currentCount = 0; // 当前正在执行的Promise数量
    this.taskQueue = []; // 任务队列
  }
  // 添加任务
  add(task) {
    return new Promise((resolve, reject) => {
      this.taskQueue.push({ task, resolve, reject });
      this._schedule();
    });
  }
  // 调度任务
  _schedule() {
    if (this.currentCount < this.maxLimit && this.taskQueue.length) {
      const { task, resolve, reject } = this.taskQueue.shift();
      this.currentCount++;
      Promise.resolve(task())
        .then(resolve)
        .catch(reject)
        .finally(() => {
          this.currentCount--;
          this._schedule(); // 完成后继续调度
        });
    }
  }
}
// 使用示例
function taskFactory(index) {
  return () => {
    return new Promise((resolve) => {
      const time = Math.random() * 2000;
      setTimeout(() => {
        console.log(`Task ${index} completed in ${time}ms`);
        resolve(index);
      }, time);
    });
  };
}
const scheduler = new PromiseScheduler(2); // 限制最多2个任务并行
for (let i = 0; i < 5; i++) {
  scheduler.add(taskFactory(i)).then((result) => {
    console.log(`Task ${result} resolved`);
  });
}

在这个实现中,PromiseScheduler类负责管理任务的执行。它有一个add方法,用于添加任务到队列中。每个任务是一个返回Promise的函数。_schedule方法负责从队列中取出任务并执行,同时确保当前执行的任务数量不超过设定的最大限制。 当任务完成时,无论是resolve还是reject,都会调用finally钩子来减少当前计数并尝试调度下一个任务。 这个调度器可以确保在任何时候,并行的Promise数量不会超过指定的最大限制。这在处理大量异步任务时,尤其是那些可能受到资源限制(如并发请求限制)的任务时非常有用。

13. 实现 Promise.all

Promise.all 是一个内置的 JavaScript 方法,它接受一个 promise 数组作为输入,并返回一个新的 promise。这个新 promise 会在所有输入的 promises 成功完成时成功完成,如果任何一个输入的 promise 失败,它就会失败。 下面是一个简单的 Promise.all 的实现:

function promiseAll(promises) {
  return new Promise((resolve, reject) => {
    if (!Array.isArray(promises)) {
      return reject(new TypeError('Promise.all accepts an array of promises'));
    }
    const results = [];
    let completedPromises = 0;
    for (let i = 0; i < promises.length; i++) {
      Promise.resolve(promises[i]).then(value => {
        results[i] = value;
        completedPromises++;
        if (completedPromises === promises.length) {
          resolve(results);
        }
      }).catch(reject);
    }
    if (promises.length === 0) {
      resolve(results);
    }
  });
}
// 使用示例
const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'foo');
});
promiseAll([promise1, promise2, promise3]).then(values => {
  console.log(values); // [3, 42, "foo"]
});

在这个实现中,我们做了以下几件事:

  1. 检查输入是否是一个数组,如果不是,则拒绝返回的 promise。
  2. 初始化一个数组 results 来存储每个 promise 的结果。
  3. 使用一个计数器 completedPromises 来跟踪已经完成的 promise 数量。
  4. 遍历输入的 promises 数组,对每个 promise 调用 Promise.resolve 来确保它是一个 promise,然后添加 thencatch 处理程序。
  5. then 处理程序中,将结果存储在 results 数组的相应位置,并增加 completedPromises 计数器。如果所有 promises 都完成了,解析返回的 promise 并传递结果数组。
  6. 如果任何一个 promise 失败,catch 处理程序会拒绝返回的 promise。
  7. 如果输入数组为空,立即解析返回的 promise 并传递一个空数组。 这个实现模仿了 Promise.all 的行为,但是在实际应用中,你通常会直接使用内置的 Promise.all 方法,因为它更优化且经过了广泛的测试。这个自定义实现主要用于理解 Promise.all 的工作原理。

14. 字符串解析问题

字符串解析问题通常涉及到将字符串转换为特定的数据结构或提取字符串中的特定信息。这类问题在编程中非常常见,例如解析JSON字符串、解析CSV文件、提取URL参数等。 如果你有一个具体的字符串解析问题,请提供更多的细节,例如你想要解析的字符串格式、你希望得到的结果等。这样我可以提供更具体的帮助和代码示例。 例如,如果你想要解析一个简单的CSV字符串并将其转换为JavaScript对象数组,可以这样做:

function parseCSV(csvString) {
  const lines = csvString.split('\n');
  const result = [];
  const headers = lines[0].split(',');
  for (let i = 1; i < lines.length; i++) {
    if (!lines[i]) continue; // 跳过空行
    const data = lines[i].split(',');
    const obj = {};
    for (let j = 0; j < headers.length; j++) {
      obj[headers[j]] = data[j];
    }
    result.push(obj);
  }
  return result;
}
// 示例CSV字符串
const csvString = `name,age,city
Alice,25,New York
Bob,30,Los Angeles
Charlie,35,Chicago`;
// 解析CSV字符串
const parsedData = parseCSV(csvString);
console.log(parsedData);

这个示例函数parseCSV将CSV字符串转换为对象数组,每个对象代表CSV中的一行,对象的键是CSV的头部字段。 如果你有其他类型的字符串解析需求,请具体说明,我将根据你的需求提供相应的解决方案。

15. 实现 event模块

在JavaScript中,实现一个简单的事件模块通常涉及到创建一个能够处理事件订阅、发布和取消订阅的类或对象。以下是一个基本的事件模块实现:

class EventEmitter {
  constructor() {
    this.events = {};
  }
  // 订阅事件
  on(event, listener) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(listener);
  }
  // 发布事件
  emit(event, ...args) {
    if (this.events[event]) {
      this.events[event].forEach(listener => {
        listener(...args);
      });
    }
  }
  // 取消订阅事件
  off(event, listenerToRemove) {
    if (this.events[event]) {
      this.events[event] = this.events[event].filter(listener => listener !== listenerToRemove);
    }
  }
  // 订阅事件,但只触发一次
  once(event, listener) {
    const onceWrapper = (...args) => {
      listener(...args);
      this.off(event, onceWrapper);
    };
    this.on(event, onceWrapper);
  }
}
// 使用示例
const eventEmitter = new EventEmitter();
// 定义事件处理函数
const handleHello = (name) => {
  console.log(`Hello, ${name}!`);
};
// 订阅事件
eventEmitter.on('hello', handleHello);
// 发布事件
eventEmitter.emit('hello', 'Alice'); // 输出: Hello, Alice!
// 取消订阅事件
eventEmitter.off('hello', handleHello);
// 再次发布事件,此时不会触发handleHello
eventEmitter.emit('hello', 'Bob'); // 无输出
// 订阅一次性事件
eventEmitter.once('greet', (name) => {
  console.log(`Greet, ${name}!`);
});
// 发布一次性事件
eventEmitter.emit('greet', 'Charlie'); // 输出: Greet, Charlie!
// 再次发布一次性事件,此时不会触发
eventEmitter.emit('greet', 'Dave'); // 无输出

这个EventEmitter类提供了基本的事件处理功能:

  • on方法用于订阅事件,可以多次触发。
  • emit方法用于发布事件,执行所有订阅该事件的监听器。
  • off方法用于取消订阅事件,移除特定的监听器。
  • once方法用于订阅事件,但事件只会被触发一次。 这个简单的事件模块可以在许多场景下使用,例如在模块间进行通信、实现观察者模式等。根据实际需求,可以进一步扩展和优化这个事件模块,例如添加错误处理、支持事件命名空间等。

16. 实现AJAX

AJAX(Asynchronous JavaScript and XML)是一种技术,用于在网页中实现异步数据交换。以下是使用原生JavaScript实现AJAX的基本步骤:

  1. 创建XMLHttpRequest对象。
  2. 配置请求(设置请求方法、URL等)。
  3. 发送请求。
  4. 处理响应。 以下是一个简单的AJAX实现示例:
function ajax(url, method = 'GET', data = null, callback) {
  // 创建XMLHttpRequest对象
  const xhr = new XMLHttpRequest();
  // 配置请求
  xhr.open(method, url, true);
  // 设置请求头(如果需要)
  xhr.setRequestHeader('Content-Type', 'application/json');
  // 设置响应类型(如果需要)
  // xhr.responseType = 'json';
  // 处理响应
  xhr.onreadystatechange = function() {
    if (xhr.readyState === 4) { // 请求完成
      if (xhr.status === 200) { // 请求成功
        const response = xhr.responseText; // 获取响应内容
        callback(null, response); // 调用回调函数,传递响应内容
      } else {
        callback(new Error(xhr.statusText), null); // 请求失败,传递错误信息
      }
    }
  };
  // 发送请求
  if (method === 'POST' && data) {
    xhr.send(JSON.stringify(data)); // 发送POST请求时,需要将数据转换为JSON字符串
  } else {
    xhr.send(); // 发送GET请求
  }
}
// 使用示例
ajax('https://api.example.com/data', 'GET', null, (error, response) => {
  if (error) {
    console.error('AJAX请求失败:', error);
  } else {
    console.log('AJAX请求成功,响应内容:', response);
  }
});
// 发送POST请求的示例
ajax('https://api.example.com/submit', 'POST', { name: 'Alice', age: 30 }, (error, response) => {
  if (error) {
    console.error('AJAX请求失败:', error);
  } else {
    console.log('AJAX请求成功,响应内容:', response);
  }
});

在这个示例中,ajax函数是一个通用的AJAX请求函数,它接受URL、请求方法、请求数据和回调函数作为参数。函数内部创建了一个XMLHttpRequest对象,并配置了请求。通过监听onreadystatechange事件来处理响应。当请求完成且成功时,调用回调函数并传递响应内容;如果请求失败,则传递错误信息。 请注意,这个示例使用了回调函数来处理异步操作。在现代JavaScript中,通常会使用Promise或async/await来处理异步操作,以提高代码的可读性和可维护性。以下是一个使用Promise的AJAX实现示例:

function ajax(url, method = 'GET', data = null) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open(method, url, true);
    xhr.setRequestHeader('Content-Type', 'application/json');
    xhr.onreadystatechange = () => {
      if (xhr.readyState === 4) {
        if (xhr.status === 200) {
          resolve(xhr.responseText);
        } else {
          reject(new Error(xhr.statusText));
        }
      }
    };
    if (method === 'POST' && data) {
      xhr.send(JSON.stringify(data));
    } else {
      xhr.send();
    }
  });
}
// 使用示例
ajax('https://api.example.com/data')
  .then(response => {
    console.log('AJAX请求成功,响应内容:', response);
  })
  .catch(error => {
    console.error('AJAX请求失败:', error);
  });
// 发送POST请求的示例
ajax('https://api.example.com/submit', 'POST', { name: 'Alice', age: 30 })
  .then(response => {
    console.log('AJAX请求成功,响应内容:', response);
  })
  .catch(error => {
    console.error('AJAX请求失败:', error);
  });

在这个使用Promise的示例中,ajax函数返回一个Promise对象,该对象在请求成功时被解析,在请求失败时被拒绝。这样可以使用.then().catch()方法来处理成功和失败的情况。

17. 实现JSONP

JSONP(JSON with Padding)是一种允许跨域请求的技术,它利用了<script>标签的跨域特性。JSONP不是AJAX的一种,但可以用来实现类似AJAX的异步数据获取。 以下是实现JSONP的基本步骤:

  1. 创建一个<script>标签。
  2. 设置<script>标签的src属性为请求的URL,并附加一个回调函数参数。
  3. <script>标签插入到文档中。
  4. 定义回调函数来处理响应数据。 以下是一个简单的JSONP实现示例:
function jsonp(url, callbackName, callback) {
  // 定义回调函数
  window[callbackName] = function(data) {
    callback(data); // 调用回调函数,传递数据
    document.body.removeChild(script); // 移除<script>标签
    delete window[callbackName]; // 删除全局回调函数
  };
  // 创建<script>标签
  const script = document.createElement('script');
  script.src = `${url}?callback=${callbackName}`; // 设置请求URL和回调函数名称
  // 处理错误情况
  script.onerror = function() {
    callback(new Error('JSONP request failed')); // 请求失败,传递错误信息
    document.body.removeChild(script); // 移除<script>标签
    delete window[callbackName]; // 删除全局回调函数
  };
  // 将<script>标签插入到文档中
  document.body.appendChild(script);
}
// 使用示例
jsonp('https://api.example.com/data', 'myCallback', function(data) {
  if (data instanceof Error) {
    console.error('JSONP请求失败:', data);
  } else {
    console.log('JSONP请求成功,响应内容:', data);
  }
});
// 假设服务器响应的JSONP格式如下:
// myCallback({ name: "Alice", age: 30 });

在这个示例中,jsonp函数接受三个参数:请求的URL、回调函数的名称和回调函数本身。函数内部定义了一个全局回调函数,该函数名称由callbackName参数指定。然后创建了一个<script>标签,并将其src属性设置为请求的URL,附加了回调函数名称作为参数。将<script>标签插入到文档中后,服务器响应的JSONP数据将会调用这个全局回调函数。 请注意,JSONP存在一些安全问题,比如容易受到XSS攻击,因为它允许执行任意的JavaScript代码。因此,在使用JSONP时,确保信任提供JSONP服务的服务器。 另外,现代Web开发中,通常推荐使用CORS(Cross-Origin Resource Sharing)来处理跨域请求,因为它提供了更安全、更灵活的跨域解决方案。JSONP主要是在不支持CORS的旧浏览器中使用的。

18. 实现 Promise

实现一个简单的Promise类需要遵循Promise/A+规范。以下是一个基本的Promise实现,它包含了resolverejectthen方法以及状态管理。这个实现不包含所有Promise/A+规范的细节,但可以作为一个起点来理解Promise的工作原理。

class SimplePromise {
  static PENDING = 'pending';
  static FULFILLED = 'fulfilled';
  static REJECTED = 'rejected';
  constructor(executor) {
    this.status = SimplePromise.PENDING;
    this.value = null;
    this.reason = null;
    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks = [];
    const resolve = (value) => {
      if (this.status === SimplePromise.PENDING) {
        this.status = SimplePromise.FULFILLED;
        this.value = value;
        this.onFulfilledCallbacks.forEach(fn => fn());
      }
    };
    const reject = (reason) => {
      if (this.status === SimplePromise.PENDING) {
        this.status = SimplePromise.REJECTED;
        this.reason = reason;
        this.onRejectedCallbacks.forEach(fn => fn());
      }
    };
    try {
      executor(resolve, reject);
    } catch (error) {
      reject(error);
    }
  }
  then(onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
    onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason };
    let promise2 = new SimplePromise((resolve, reject) => {
      if (this.status === SimplePromise.FULFILLED) {
        setTimeout(() => {
          try {
            let x = onFulfilled(this.value);
            this.resolvePromise(promise2, x, resolve, reject);
          } catch (error) {
            reject(error);
          }
        });
      } else if (this.status === SimplePromise.REJECTED) {
        setTimeout(() => {
          try {
            let x = onRejected(this.reason);
            this.resolvePromise(promise2, x, resolve, reject);
          } catch (error) {
            reject(error);
          }
        });
      } else if (this.status === SimplePromise.PENDING) {
        this.onFulfilledCallbacks.push(() => {
          setTimeout(() => {
            try {
              let x = onFulfilled(this.value);
              this.resolvePromise(promise2, x, resolve, reject);
            } catch (error) {
              reject(error);
            }
          });
        });
        this.onRejectedCallbacks.push(() => {
          setTimeout(() => {
            try {
              let x = onRejected(this.reason);
              this.resolvePromise(promise2, x, resolve, reject);
            } catch (error) {
              reject(error);
            }
          });
        });
      }
    });
    return promise2;
  }
  resolvePromise(promise2, x, resolve, reject) {
    if (promise2 === x) {
      return reject(new TypeError('Chaining cycle detected for promise'));
    }
    let called;
    if (x != null && (typeof x === 'object' || typeof x === 'function')) {
      try {
        let then = x.then;
        if (typeof then === 'function') {
          then.call(x, y => {
            if (called) return;
            called = true;
            this.resolvePromise(promise2, y, resolve, reject);
          }, r => {
            if (called) return;
            called = true;
            reject(r);
          });
        } else {
          resolve(x);
        }
      } catch (error) {
        if (called) return;
        called = true;
        reject(error);
      }
    } else {
      resolve(x);
    }
  }
}
// 使用示例
new SimplePromise((resolve, reject) => {
  setTimeout(() => {
    resolve('成功');
  }, 1000);
}).then(value => {
  console.log(value); // 输出:成功
}, reason => {
  console.log(reason);
});

这个简单的Promise实现包括了以下关键点:

  • status属性表示Promise的状态,可以是pendingfulfilledrejected
  • valuereason属性分别存储resolve和reject的值。
  • onFulfilledCallbacksonRejectedCallbacks数组存储未决状态的回调函数。
  • resolvereject函数用于改变Promise的状态并执行相应的回调。
  • then方法用于添加回调函数,并返回一个新的Promise实例,以支持链式调用。
  • resolvePromise函数用于处理Promise解析过程,包括判断是否形成了循环引用以及处理thenable对象。 请注意,这个实现是简化的,没有完全遵循Promise/A+规范的所有细节,例如没有实现catchfinally方法,也没有处理所有边缘情况。在实际应用中,建议使用原生的Promise

19. 实现深拷贝

深拷贝是一个将对象或数组复制到另一个变量中的过程,同时保持其结构和数据不变,且不与原对象或数组共享引用。以下是一个简单的深拷贝函数的实现,它使用了递归来处理对象和数组的深拷贝:

function deepCopy(obj) {
  // 如果是基本数据类型,直接返回
  if (typeof obj !== 'object' || obj === null) {
    return obj;
  }
  // 初始化返回结果
  let copy;
  // 判断是否是数组
  if (Array.isArray(obj)) {
    copy = [];
    // 遍历数组中的每一项
    for (let i = 0, len = obj.length; i < len; i++) {
      copy[i] = deepCopy(obj[i]); // 递归调用深拷贝函数
    }
  } else {
    // 如果是对象,则初始化为一个新对象
    copy = {};
    // 遍历对象中的每一个键值对
    for (let key in obj) {
      // 确保key不是原型的属性
      if (obj.hasOwnProperty(key)) {
        copy[key] = deepCopy(obj[key]); // 递归调用深拷贝函数
      }
    }
  }
  return copy;
}
// 使用示例
const original = {
  num: 0,
  str: 'original',
  bool: true,
  arr: [0, 1, 2],
  obj: { key: 'value' },
  func: function() { return 'deepCopy'; }
};
const copy = deepCopy(original);
console.log(copy); // 输出深拷贝后的对象
console.log(original === copy); // 输出false,表示它们是不同的对象
console.log(original.obj === copy.obj); // 输出false,表示它们是不同的对象

这个deepCopy函数可以处理以下情况:

  • 基本数据类型(数字、字符串、布尔值、null、undefined)的直接复制。
  • 数组的深拷贝,包括数组中的嵌套数组或对象。
  • 对象的深拷贝,包括对象中的嵌套对象或数组。
  • 函数的复制(注意:函数是共享引用,不是真正的深拷贝)。 然而,这个简单的实现并没有处理一些复杂的情况,例如:
  • 循环引用会导致栈溢出错误。
  • 特殊对象类型(如Date、RegExp、Map、Set等)的深拷贝。
  • Symbol类型的属性。 为了处理这些复杂情况,可以实现一个更完整的深拷贝函数,或者使用现有的库,如Lodash的_.cloneDeep方法。以下是一个更完整的深拷贝函数的实现,它处理了循环引用和特殊对象类型:
function deepCopy(obj, hash = new WeakMap()) {
  if (obj === null) return null; // null 的情况
  if (typeof obj !== 'object') return obj; // 基本数据类型直接返回
  if (obj instanceof Date) return new Date(obj); // 日期对象直接返回一个新的日期对象
  if (obj instanceof RegExp) return new RegExp(obj); // 正则对象直接返回一个新的正则对象
  if (hash.has(obj)) return hash.get(obj); // 如果已经存在当前对象,说明是循环引用,直接返回已经拷贝的结果
  // 处理数组和对象
  let cloneObj = new obj.constructor();
  hash.set(obj, cloneObj); // 存储拷贝的对象,用于处理循环引用
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) { // 确保key不是原型的属性
      cloneObj[key] = deepCopy(obj[key], hash); // 递归拷贝
    }
  }
  return cloneObj;
}
// 使用示例
const original = {
  num: 0,
  str: 'original',
  bool: true,
  arr: [0, 1, 2],
  obj: { key: 'value' },
  func: function() { return 'deepCopy'; },
  date: new Date(),
  reg: new RegExp('/test/i')
};
original.self = original; // 添加循环引用
const copy = deepCopy(original);
console.log(copy); // 输出深拷贝后的对象
console.log(original === copy); // 输出false
console.log(original.obj === copy.obj); // 输出false
console.log(original.date === copy.date); // 输出false
console.log(original.reg === copy.reg); // 输出false
console.log(original.self === copy.self); // 输出false,循环引用也被正确处理

在这个更完整的实现中,我们使用了WeakMap来处理循环引用的问题,并且对特殊对象类型进行了处理。这样,我们就可以更安全地进行

20. 实现Object.assign

Object.assign 是一个ES6引入的JavaScript原生方法,用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它返回修改后的目标对象。如果你想要自己实现一个类似Object.assign的功能,可以按照以下步骤进行:

  1. 定义一个函数,接受一个目标对象和多个源对象作为参数。
  2. 遍历每个源对象的所有可枚举自有属性。
  3. 将每个属性复制到目标对象上。
  4. 返回目标对象。 以下是一个简单的Object.assign的polyfill实现:
if (typeof Object.assign !== 'function') {
  // 实现Object.assign
  Object.assign = function(target) {
    // 首先判断目标对象是否为非null的对象
    if (target === null || typeof target !== 'object') {
      throw new TypeError('Cannot convert undefined or null to object');
    }
    // 将目标对象转换为可修改的对象
    var to = Object(target);
    // 遍历源对象
    for (var index = 1; index < arguments.length; index++) {
      var nextSource = arguments[index];
      // 如果源对象是非null的对象,则进行属性复制
      if (nextSource !== null && typeof nextSource === 'object') {
        // 遍历源对象的所有可枚举自有属性
        for (var nextKey in nextSource) {
          // 确保属性不是原型的属性
          if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
            // 将属性复制到目标对象
            to[nextKey] = nextSource[nextKey];
          }
        }
      }
    }
    // 返回修改后的目标对象
    return to;
  };
}
// 使用示例
var obj1 = { a: 1 };
var obj2 = { b: 2 };
var obj3 = { c: 3 };
var result = Object.assign(obj1, obj2, obj3);
console.log(result); // { a: 1, b: 2, c: 3 }

这个实现包括了以下特点:

  • 检查Object.assign是否已经存在,以避免覆盖原生的实现。
  • 检查目标对象是否为null或非对象,如果是,则抛出TypeError
  • 将目标对象转换为可修改的对象,以避免直接修改null或基本数据类型。
  • 遍历所有源对象,并将它们的可枚举自有属性复制到目标对象上。
  • 使用Object.prototype.hasOwnProperty.call来检查属性是否为自有属性,以避免原型链上的属性被复制。 请注意,这个实现没有处理一些边缘情况,比如处理Symbol属性、不可枚举属性等。原生的Object.assign会忽略那些不可枚举的属性,并且会复制Symbol属性。如果你需要处理这些情况,可以实现更复杂的逻辑来满足这些需求。

21. 实现 instanceof

instanceof 是 JavaScript 中的一个二元运算符,用于测试一个对象是否是一个类的实例。实现 instanceof 的关键在于理解原型链(prototype chain)的工作方式。以下是一个自定义的 instanceof 函数的实现:

function myInstanceof(left, right) {
  // 获取对象的原型
  let proto = Object.getPrototypeOf(left);
  // 获取构造函数的prototype对象
  let prototype = right.prototype;
  // 判断构造函数的prototype对象是否在对象的原型链上
  while (true) {
    if (!proto) return false; // 如果对象的原型为null,则返回false
    if (proto === prototype) return true; // 如果找到相同的原型,则返回true
    // 如果没有找到,继续向原型链的上一层查找
    proto = Object.getPrototypeOf(proto);
  }
}
// 使用示例
function Person(name) {
  this.name = name;
}
var p = new Person('Alice');
console.log(myInstanceof(p, Person)); // 应该输出true
console.log(myInstanceof(p, Object)); // 应该输出true
console.log(myInstanceof(p, Array)); // 应该输出false

这个 myInstanceof 函数的工作原理如下:

  1. 获取左侧对象的原型,即 Object.getPrototypeOf(left)
  2. 获取右侧构造函数的 prototype 属性。
  3. 使用一个循环遍历左侧对象的原型链。
  4. 如果在原型链中找到了右侧构造函数的 prototype,则返回 true
  5. 如果遍历到了原型链的末端(即 protonull),则返回 false。 这个实现考虑了原型链的查找,能够正确地判断一个对象是否是某个构造函数的实例。需要注意的是,instanceof 操作符在判断基本数据类型(如 numberstringboolean 等)时不会返回 true,因为这些类型不是对象,没有原型链。在上面的实现中,我们假设了 left 参数是一个对象,如果传入的是基本数据类型,Object.getPrototypeOf(left) 会返回 undefined,循环将不会执行,函数会返回 false

22. 函数珂里化

函数柯里化(Currying)是一种函数转换技术,它将一个接受多个参数的函数转换成一系列接受单一参数的函数。这种技术通常用于函数式编程,可以让函数的调用更加灵活和可组合。 简单来说,柯里化就是将一个多参数函数拆分成多个单参数函数,每个函数都返回一个新的函数,直到所有参数都被提供,最终返回结果。 以下是一个简单的柯里化函数的实现:

function curry(func) {
  return function curried(...args) {
    if (args.length >= func.length) {
      return func.apply(this, args);
    } else {
      return function(...args2) {
        return curried.apply(this, args.concat(args2));
      };
    }
  };
}
// 使用示例
function sum(a, b, c) {
  return a + b + c;
}
const curriedSum = curry(sum);
console.log(curriedSum(1)(2)(3)); // 输出 6
console.log(curriedSum(1, 2)(3));  // 输出 6
console.log(curriedSum(1)(2, 3));  // 输出 6

在这个例子中,curry 函数接受一个多参数函数 func 作为参数,并返回一个新的函数 curriedcurried 函数可以接受一部分参数,如果提供的参数数量足够(即达到或超过原函数的参数长度),则直接调用原函数并返回结果。如果参数数量不足,则返回一个新的函数,这个新函数可以继续接受参数,直到所有参数都被提供。 柯里化的优点包括:

  1. 参数复用:柯里化可以固定某一部分参数,然后复用这些参数。
  2. 延迟执行:柯里化可以将函数的执行延迟到所有参数都被提供之后。
  3. 函数组合:柯里化可以使函数更容易组合,形成更复杂的函数。 柯里化是函数式编程中的一个重要概念,它可以提高代码的模块性和重用性。

23. 实现 throttle(节流)函数

节流(Throttle)函数是一种限制函数执行频率的技术,确保函数在指定的时间间隔内只执行一次。这种技术常用于处理高频事件,如窗口调整、滚动、键盘输入等,以避免函数在短时间内被过度调用。 以下是一个简单的节流函数的实现:

function throttle(func, limit) {
  let lastFunc;
  let lastRan;
  return function() {
    const context = this;
    const args = arguments;
    if (!lastRan) {
      func.apply(context, args);
      lastRan = Date.now();
    } else {
      clearTimeout(lastFunc);
      lastFunc = setTimeout(function() {
        if ((Date.now() - lastRan) >= limit) {
          func.apply(context, args);
          lastRan = Date.now();
        }
      }, limit - (Date.now() - lastRan));
    }
  }
}
// 使用示例
function logEvent() {
  console.log('Event triggered');
}
const throttledLog = throttle(logEvent, 2000);
window.addEventListener('resize', throttledLog);

在这个例子中,throttle 函数接受两个参数:要节流的函数 func 和时间间隔 limit(以毫秒为单位)。throttle 函数返回一个新的函数,这个新函数会在指定的时间间隔内最多执行一次原函数。 节流函数的工作原理如下:

  1. 使用 lastRan 变量记录上一次函数执行的时间。
  2. 如果 lastRanundefined(即函数尚未执行过),则立即执行函数,并更新 lastRan
  3. 如果函数已经执行过,则使用 setTimeout 来延迟函数的执行,直到达到指定的时间间隔。
  4. 如果在延迟期间再次触发事件,setTimeout 会被清除并重新设置,确保函数在下一个时间间隔内执行。 这种实现方式确保了在连续的事件触发中,函数的执行频率不会超过指定的时间间隔。 节流函数在实际应用中非常有用,可以避免因为事件触发过于频繁而导致性能问题。

24. 实现 debounce(防抖)函数

防抖(Debounce)函数是一种限制函数执行频率的技术,确保在事件被触发后一定时间内没有再次被触发的情况下才执行函数。这种技术常用于处理高频事件,如窗口调整、滚动、键盘输入等,以避免函数在短时间内被过度调用。 以下是一个简单的防抖函数的实现:

function debounce(func, delay) {
  let timer;
  return function() {
    const context = this;
    const args = arguments;
    clearTimeout(timer);
    timer = setTimeout(function() {
      func.apply(context, args);
    }, delay);
  };
}
// 使用示例
function logEvent() {
  console.log('Event triggered');
}
const debouncedLog = debounce(logEvent, 2000);
window.addEventListener('resize', debouncedLog);

在这个例子中,debounce 函数接受两个参数:要防抖的函数 func 和延迟时间 delay(以毫秒为单位)。debounce 函数返回一个新的函数,这个新函数会在事件触发后等待指定的延迟时间,如果在这段时间内事件没有再次被触发,则执行原函数。 防抖函数的工作原理如下:

  1. 使用 timer 变量来存储定时器。
  2. 当事件触发时,清除现有的定时器(如果存在)。
  3. 设置一个新的定时器,在指定的延迟时间后执行原函数。
  4. 如果在延迟时间内事件再次被触发,定时器会被清除,并重新设置。 这种实现方式确保了只有在事件触发后的一段时间内没有再次触发事件时,函数才会执行。 防抖函数在实际应用中非常有用,可以避免因为事件触发过于频繁而导致性能问题,特别是在处理如输入验证、AJAX请求等场景时。

25. 实现 Function.prototype.bind

Function.prototype.bind 是一个内置的 JavaScript 方法,它创建一个新的函数,这个新函数的 this 关键字被绑定到提供的值,并且可以预设函数的参数。如果你想要自己实现这个方法,以下是一个简单的实现:

if (!Function.prototype.bind) {
  Function.prototype.bind = function(oThis) {
    if (typeof this !== 'function') {
      // closest thing possible to the ECMAScript 5
      // internal IsCallable function
      throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
    }
    var aArgs = Array.prototype.slice.call(arguments, 1),
        fToBind = this,
        fNOP = function() {},
        fBound = function() {
          return fToBind.apply(this instanceof fNOP
                                 ? this
                                 : oThis,
                               aArgs.concat(Array.prototype.slice.call(arguments)));
        };
    if (this.prototype) {
      // Function.prototype doesn't have a prototype property
      fNOP.prototype = this.prototype;
    }
    fBound.prototype = new fNOP();
    return fBound;
  };
}
// 使用示例
function greet(greeting, punctuation) {
  console.log(greeting + ' ' + this.user + punctuation);
}
var user = { user: 'Guest' };
var greetBound = greet.bind(user, 'Hello');
greetBound('!'); // 输出:Hello Guest!

这个实现包括以下关键点:

  1. 检查调用 bind 的对象是否是函数,如果不是,则抛出 TypeError
  2. 使用 Array.prototype.slice.call(arguments, 1) 获取除了第一个参数(绑定 this 的对象)之外的所有参数,这些参数将作为预设参数传递给绑定后的函数。
  3. 创建一个空函数 fNOP,用于继承原函数的原型链。
  4. 创建绑定后的函数 fBound,它将调用原函数 fToBind,并使用 apply 方法将 this 绑定到指定的对象 oThis,同时将预设参数和调用时传入的参数合并传递。
  5. 如果原函数有原型对象,则通过 fNOP 继承原函数的原型,以确保绑定后的函数的实例可以继承原函数的原型方法。 这个实现确保了 bind 方法的功能与内置的 Function.prototype.bind 方法相似,包括处理 new 操作符调用绑定后的构造函数的情况。

26. 实现 Function.prototype.call

Function.prototype.call 方法允许你在一个对象上调用一个函数,并且可以传递一个参数列表。以下是一个简单的实现:

Function.prototype.myCall = function(context) {
  // 如果调用myCall的不是函数,则抛出错误
  if (typeof this !== 'function') {
    throw new TypeError('Error');
  }
  // context为null或undefined时,this指向window
  context = context || window;
  // 给context添加一个属性,这个属性名是唯一的,防止与context自身的属性名冲突
  const fnSymbol = Symbol();
  context[fnSymbol] = this;
  // 处理参数,获取调用myCall时传入的参数,从第二个参数开始
  const args = [...arguments].slice(1);
  // 调用函数并返回结果
  const result = context[fnSymbol](...args);
  // 删除添加的属性
  delete context[fnSymbol];
  // 返回函数调用的结果
  return result;
};
// 使用示例
function greet(greeting, punctuation) {
  console.log(greeting + ' ' + this.user + punctuation);
}
var user = { user: 'Guest' };
greet.myCall(user, 'Hello', '!'); // 输出:Hello Guest!

这个实现包括以下关键点:

  1. 检查调用 myCall 的对象是否是函数,如果不是,则抛出 TypeError
  2. 如果传递的 contextnullundefined,则将 context 设置为 window 对象。
  3. 使用 Symbol 创建一个唯一的属性名,这样就不会与 context 对象上现有的属性名冲突。
  4. 将当前函数(this)赋值给 context 对象的该唯一属性。
  5. 使用扩展运算符(...)和 slice 方法获取传递给 myCall 的其余参数。
  6. 调用 context 对象上的该唯一属性,即调用原始函数,并传递参数。
  7. 删除 context 对象上添加的属性,以避免对原始对象造成污染。
  8. 返回函数调用的结果。 这个实现模拟了 Function.prototype.call 的基本行为,允许你改变函数执行时的 this 指向,并传递参数。

27. 实现 Function.prototype.apply()

Function.prototype.apply 方法与 call 方法类似,但它允许你传递一个参数数组而不是单个参数。以下是一个简单的实现:

Function.prototype.myApply = function(context, args) {
  // 如果调用myApply的不是函数,则抛出错误
  if (typeof this !== 'function') {
    throw new TypeError('Error');
  }
  // context为null或undefined时,this指向window
  context = context || window;
  // 给context添加一个属性,这个属性名是唯一的,防止与context自身的属性名冲突
  const fnSymbol = Symbol();
  context[fnSymbol] = this;
  // 如果args不是数组,则抛出错误
  if (!Array.isArray(args)) {
    throw new TypeError('Error');
  }
  // 调用函数并返回结果
  const result = context[fnSymbol](...args);
  // 删除添加的属性
  delete context[fnSymbol];
  // 返回函数调用的结果
  return result;
};
// 使用示例
function greet(greeting, punctuation) {
  console.log(greeting + ' ' + this.user + punctuation);
}
var user = { user: 'Guest' };
greet.myApply(user, ['Hello', '!']); // 输出:Hello Guest!

这个实现包括以下关键点:

  1. 检查调用 myApply 的对象是否是函数,如果不是,则抛出 TypeError
  2. 如果传递的 contextnullundefined,则将 context 设置为 window 对象。
  3. 使用 Symbol 创建一个唯一的属性名,这样就不会与 context 对象上现有的属性名冲突。
  4. 将当前函数(this)赋值给 context 对象的该唯一属性。
  5. 检查第二个参数 args 是否是数组,如果不是,则抛出 TypeError
  6. 使用扩展运算符(...)将参数数组展开,并调用 context 对象上的该唯一属性,即调用原始函数。
  7. 删除 context 对象上添加的属性,以避免对原始对象造成污染。
  8. 返回函数调用的结果。 这个实现模拟了 Function.prototype.apply 的基本行为,允许你改变函数执行时的 this 指向,并传递一个参数数组。

28. 实现 Array.prototype.reduce()

Array.prototype.reduce() 方法对数组中的每个元素执行一个由你提供的.reducer函数(接受四个参数:累加器accumulator, 当前值currentValue, 当前索引currentIndex, 源数组array),将其结果汇总为单个返回值。 以下是一个简单的实现:

Array.prototype.myReduce = function(callback, initialValue) {
  // 如果调用myReduce的不是数组,则抛出错误
  if (this === null || this === undefined) {
    throw new TypeError('Array.prototype.myReduce called on null or undefined');
  }
  
  // 如果callback不是函数,则抛出错误
  if (typeof callback !== 'function') {
    throw new TypeError(callback + ' is not a function');
  }
  // 将数组转换为对象
  var o = Object(this);
  // 获取数组长度
  var len = o.length >>> 0;
  // 如果数组为空且没有提供初始值,则抛出TypeError
  if (len === 0 && arguments.length < 2) {
    throw new TypeError('Reduce of empty array with no initial value');
  }
  // 初始值为数组第一个元素或者提供的初始值
  var accumulator = initialValue;
  var k = 0;
  // 如果没有提供初始值,则使用数组的第一个元素作为初始值
  if (arguments.length < 2) {
    accumulator = o[k++];
  }
  // 遍历数组,执行回调函数
  while (k < len) {
    if (k in o) {
      accumulator = callback.call(undefined, accumulator, o[k], k, o);
    }
    k++;
  }
  // 返回累加结果
  return accumulator;
};
// 使用示例
var numbers = [1, 2, 3, 4];
var sum = numbers.myReduce(function(accumulator, currentValue) {
  return accumulator + currentValue;
}, 0);
console.log(sum); // 输出:10

这个实现包括以下关键点:

  1. 检查调用 myReduce 的对象是否是数组,如果不是,则抛出 TypeError

  2. 检查提供的回调函数 callback 是否是函数,如果不是,则抛出 TypeError

  3. 将数组转换为对象,并获取其长度。

  4. 如果数组为空且没有提供初始值,则抛出 TypeError

  5. 初始值 accumulator 可以是数组第一个元素或者提供的初始值。

  6. 如果没有提供初始值,则使用数组的第一个元素作为初始值,并从第二个元素开始遍历。

  7. 遍历数组,对每个元素执行回调函数,并将结果赋值给 accumulator

  8. 返回累加结果。 这个实现模拟了 Array.prototype.reduce 的基本行为,允许你将数组元素累加到单个值中。注意,这个实现没有处理回调函数中可能抛出的错误,也没有处理数组中包含 holes(即数组中未定义的元素)的情况。在实际使用中,你可能需要根据具体需求进行更详细的错误处理和边界条件检查。

29. 实现 Array.prototype.forEach()

Array.prototype.forEach() 方法对数组的每个元素执行一次给定的函数。以下是一个简单的实现:

Array.prototype.myForEach = function(callback, thisArg) {
  // 如果调用myForEach的不是数组,则抛出错误
  if (this === null || this === undefined) {
    throw new TypeError('Array.prototype.myForEach called on null or undefined');
  }
  
  // 如果callback不是函数,则抛出错误
  if (typeof callback !== 'function') {
    throw new TypeError(callback + ' is not a function');
  }
  // 将数组转换为对象
  var o = Object(this);
  // 获取数组长度
  var len = o.length >>> 0;
  // 遍历数组,执行回调函数
  for (var i = 0; i < len; i++) {
    if (i in o) {
      // 调用回调函数,可以指定thisArg作为回调函数的this值
      callback.call(thisArg, o[i], i, o);
    }
  }
};
// 使用示例
var numbers = [1, 2, 3, 4];
numbers.myForEach(function(value, index, array) {
  console.log(value, index, array);
});
// 输出:
// 1 0 [1, 2, 3, 4]
// 2 1 [1, 2, 3, 4]
// 3 2 [1, 2, 3, 4]
// 4 3 [1, 2, 3, 4]

这个实现包括以下关键点:

  1. 检查调用 myForEach 的对象是否是数组,如果不是,则抛出 TypeError
  2. 检查提供的回调函数 callback 是否是函数,如果不是,则抛出 TypeError
  3. 将数组转换为对象,并获取其长度。
  4. 遍历数组,对每个元素执行回调函数。
  5. 使用 call 方法调用回调函数,允许传递 thisArg 作为回调函数的 this 值。
  6. 检查当前索引 i 是否存在于数组中,以处理数组中可能存在的“holes”(即未定义的元素)。 这个实现模拟了 Array.prototype.forEach 的基本行为,允许你为数组中的每个元素执行一个函数。注意,这个实现没有处理回调函数中可能抛出的错误。在实际使用中,你可能需要根据具体需求进行更详细的错误处理和边界条件检查。 此外,forEach 方法不会等待异步操作完成,如果回调函数中有异步操作,它们将不会按顺序执行。如果需要处理异步操作,可以考虑使用 for...of 循环或其他方法。

30. 实现 Array.prototype.map()

Array.prototype.map() 方法创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后的返回值。以下是一个简单的实现:

Array.prototype.myMap = function(callback, thisArg) {
  // 如果调用myMap的不是数组,则抛出错误
  if (this === null || this === undefined) {
    throw new TypeError('Array.prototype.myMap called on null or undefined');
  }
  
  // 如果callback不是函数,则抛出错误
  if (typeof callback !== 'function') {
    throw new TypeError(callback + ' is not a function');
  }
  // 将数组转换为对象
  var o = Object(this);
  // 获取数组长度
  var len = o.length >>> 0;
  // 创建新数组来存储映射结果
  var mappedArray = new Array(len);
  // 遍历数组,执行回调函数,并将结果存储在新数组中
  for (var i = 0; i < len; i++) {
    if (i in o) {
      // 调用回调函数,可以指定thisArg作为回调函数的this值
      mappedArray[i] = callback.call(thisArg, o[i], i, o);
    }
  }
  // 返回新数组
  return mappedArray;
};
// 使用示例
var numbers = [1, 2, 3, 4];
var doubled = numbers.myMap(function(value, index, array) {
  return value * 2;
});
console.log(doubled); // 输出:[2, 4, 6, 8]

这个实现包括以下关键点:

  1. 检查调用 myMap 的对象是否是数组,如果不是,则抛出 TypeError
  2. 检查提供的回调函数 callback 是否是函数,如果不是,则抛出 TypeError
  3. 将数组转换为对象,并获取其长度。
  4. 创建一个新数组 mappedArray 来存储映射结果。
  5. 遍历原数组,对每个元素执行回调函数,并将返回值存储在新数组中。
  6. 使用 call 方法调用回调函数,允许传递 thisArg 作为回调函数的 this 值。
  7. 检查当前索引 i 是否存在于数组中,以处理数组中可能存在的“holes”(即未定义的元素)。
  8. 返回新数组。 这个实现模拟了 Array.prototype.map 的基本行为,允许你为数组中的每个元素执行一个函数,并返回一个新数组,其中包含每个回调函数的返回值。注意,这个实现没有处理回调函数中可能抛出的错误。在实际使用中,你可能需要根据具体需求进行更详细的错误处理和边界条件检查。 与 forEach 不同,map 方法会返回一个新数组,不会改变原数组。此外,map 方法也是同步的,不会等待异步操作完成。如果回调函数中有异步操作,它们将不会按顺序执行,并且新数组中对应的元素将会是 undefined,直到异步操作完成。如果需要处理异步操作,可以考虑使用 Promise.allmap 结合使用,或者使用其他方法如 for...of 循环。