# 面试题

​ 这套面试题涵盖了从基础知识到高级概念的全面内容,包括变量、数据类型、运算符、条件语句、循环、函数、作用域、数组、对象、字符串、日期、错误处理、调试技巧、DOM 操作、事件处理、异步编程、ES6+ 特性、 BOMJavaScript 库和框架、调试和性能优化、与其他技术整合等。按照计划逐步复习,可以系统地掌握 JavaScript 编程语言,并为进一步深入学习和应用打下坚实的基础。

# 介绍 JavaScript 的基本数据类型,并解释它们之间的区别。

  • Number 用于表示数值,包括整数和浮点数。
  • String 用于表示文本数据,由一串字符组成。
  • Boolean 表示真或假的逻辑值,只有两个可能的取值:truefalse
  • Undefined 表示未定义的值,通常是声明了变量但未赋值时的默认值。
  • Null 表示空值或不存在的对象。
  • Symbol 表示唯一且不可变的值,通常用作对象属性的标识符。

# 如何判断一个变量的数据类型?

​ 可以使用typeof运算符来判断变量的数据类型,还可以使用instanceof运算符、Object.prototype.toString.call()方法进行类型判断。

# 什么是块级作用域?

​ 块级作用域是指在一对花括号中定义的作用域范围,限定了变量的可见性仅限于当前的代码块内部。在 JavaScript 中,使用 letconst 关键字可以创建块级作用域,使变量的作用域仅限于当前代码块,而不是整个函数或全局作用域。块级作用域提供了更细粒度的变量控制,帮助避免变量污染和冲突,并提高代码的可读性和可维护性。

# 什么是变量作用域?

​ 变量作用域是指在代码中定义的变量所能被访问的范围。它决定了变量的可见性和生命周期。全局作用域是指在整个代码中都可访问的变量,而局部作用域是指在特定代码块内部声明的变量,只能在其所在的代码块内部被访问。

# 什么是作用域链?

​ 作用域链是 JavaScript 中的一种机制,用于查找和访问变量。它是由作用域的嵌套关系决定的,沿着嵌套的作用域逐级查找变量,直到找到变量或到达全局作用域。作用域链的顺序是从内部向外部逐级查找的。这种机制确保变量的访问按照作用域的层级关系进行,保证了变量的可见性和封装性。

# 什么是闭包?

​ 闭包是指在函数内部创建的函数,它可以访问外部函数的变量和作用域,即使外部函数已经执行完毕,闭包仍然可以保留对外部函数作用域的引用。

  1. 保护变量:闭包可以创建一个封闭的环境,使内部函数可以访问外部函数的变量,同时又保护了这些变量不受外界干扰。这提供了一种封装数据的方式,防止全局污染。
  2. 保存状态:由于闭包可以保留对外部函数作用域的引用,因此可以通过闭包来保存变量的状态。每次调用外部函数时,都会创建一个新的闭包实例,因此可以在闭包中保存状态,并在每次调用内部函数时使用这些状态。
  3. 实现私有变量和方法:通过使用闭包,可以模拟私有变量和方法。外部函数内部的变量和函数只能通过内部函数进行访问,外部作用域无法直接访问,从而实现了一种封装和隐藏的效果。
  4. 延长变量的生命周期:由于闭包会保留对外部函数作用域的引用,使得外部函数的变量在内部函数中仍然可用,因此可以延长变量的生命周期,即使外部函数已经执行完毕。

# 什么是箭头函数?

​ 箭头函数是一种简洁的函数定义语法,用于创建函数。它使用箭头 => 来代替传统的 function 关键字,可以减少代码量并提供更简洁的函数定义方式。箭头函数自动绑定外部作用域的 this 值,适用于简单的函数表达式、回调函数和避免 this 问题的场景。

  1. 简洁的语法:箭头函数的语法更为简洁,通常可以用更少的代码量来定义函数。
  2. 自动绑定 this 值:箭头函数没有自己的 this 值,它会继承外部作用域的 this 值。这意味着在箭头函数内部使用的 this 将自动指向外部函数的 this 值,避免了传统函数中 this 绑定的复杂性。
  3. 适用于回调函数和高阶函数:由于箭头函数的简洁性和对 this 的处理,它们特别适用于作为回调函数或高阶函数的参数,提供更简单和清晰的函数定义。

# 什么是立即执行函数?

​ 立即执行函数是指在定义后立即执行的函数。它可以创建一个独立的作用域,避免全局变量的污染,还可以用于模块化和封装代码。

# 介绍常用的对象方法

  1. Object.keys(obj) 返回一个包含给定对象的所有可枚举属性的数组。数组中的元素是对象的属性名。
  2. Object.values(obj) 返回一个包含给定对象的所有可枚举属性的值的数组。数组中的元素是对象的属性值。
  3. Object.entries(obj) 返回一个包含给定对象的所有可枚举属性的键值对的数组。数组中的元素是由键和值组成的数组。
  4. Object.assign(target, source) 用于将一个或多个源对象的属性复制到目标对象中。它将源对象的属性合并到目标对象,并返回目标对象。
  5. Object.getOwnPropertyNames(obj) 返回一个包含给定对象的所有属性名称(包括不可枚举属性)的数组。
  6. Object.hasOwnProperty(prop) 用于检查对象是否具有指定名称的自身属性。
  7. Object.freeze(obj) 用于冻结一个对象,阻止对其进行更改。被冻结的对象的属性无法被修改、删除或添加。
  8. Object.seal(obj) 用于封闭一个对象,阻止对其属性的添加和删除,但允许修改属性的值。

# 什么是原型链(Prototype Chain)?

​ 每个对象(包括函数)都有一个 __proto__ 属性,它指向该对象的原型。当我们访问一个对象的属性或方法时,如果该对象本身没有该属性或方法,JavaScript 引擎就会沿着原型链往上查找,直到找到对应的属性或方法或者到达原型链的顶端(即 Object.prototype)。

​ 这样,通过原型链,一个对象可以访问和继承其原型上的属性和方法。如果原型对象也有自己的原型,那么就会形成一个原型链,多个对象通过原型链相互关联。

# 如何使用原型链实现继承?

​ 在 JavaScript 中,可以通过原型链来实现对象之间的继承关系。通过原型链,一个对象可以继承另一个对象的属性和方法。下面是使用原型链实现继承的一般步骤:

  1. 创建一个基础对象(父类)。
  2. 定义基础对象的属性和方法,将它们添加到基础对象的原型上。
  3. 创建一个派生对象(子类)。
  4. 将派生对象的原型指向基础对象,建立原型链关系。
// 基础对象(父类)
function Animal(name) {
  this.name = name;
}

// 在基础对象的原型上添加方法
Animal.prototype.sayHello = function() {
  console.log("Hello, I'm " + this.name);
};

// 派生对象(子类)
function Dog(name, breed) {
  Animal.call(this, name); // 调用父类构造函数,继承父类的属性
  this.breed = breed;
}

// 将派生对象的原型指向基础对象,建立原型链关系
Dog.prototype = Object.create(Animal.prototype);

// 在派生对象的原型上添加方法
Dog.prototype.bark = function() {
  console.log("Woof!");
};

// 创建派生对象实例
var dog = new Dog("Buddy", "Golden Retriever");

// 调用继承自基础对象的方法
dog.sayHello(); // 输出: Hello, I'm Buddy

// 调用派生对象自己的方法
dog.bark(); // 输出: Woof!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

​ 在上述示例中,Animal 是基础对象(父类),它定义了 name 属性和 sayHello 方法,并将 sayHello 方法添加到原型上。Dog 是派生对象(子类),通过调用 Animal 的构造函数并使用 Object.create 方法将 Dog 的原型指向 Animal 的原型,建立了原型链关系。这样,Dog 实例就可以继承 Animal 的属性和方法。

# 什么是异步编程?

​ 异步编程是一种编程模型,其中任务的执行顺序不是按照代码的顺序进行的。相反,任务是在后台执行,不会阻塞程序的其他部分。当任务完成时,通常会触发一个回调或返回一个 Promise,以便处理任务的结果。

​ 在 JavaScript 中,异步编程非常重要,因为 JavaScript 是一门单线程的语言。这意味着 JavaScript 一次只能执行一个任务,如果某个任务需要花费很长时间,会导致程序的其他部分无法执行,造成页面冻结和不响应的情况。

​ 异步编程允许在执行耗时的操作(如网络请求、文件读写、数据库查询等)时,将控制权交还给 JavaScript 的运行环境,以便同时执行其他任务。当异步操作完成时,通过回调函数、Promiseasync/await 等方式,处理异步操作的结果。

# 什么是 Promise ?

PromiseJavaScript 中用于处理异步操作的对象,表示一个异步操作的最终结果。它有三种状态:pending(进行中)、fulfilled(已完成)和 rejected(已拒绝)。Promise 提供了链式的方法 .then() 来处理异步操作的结果,并解决了回调地狱问题。它在异步编程中的作用是简化代码编写、统一处理异步操作结果和支持错误处理。

# 什么是 async/await ?

async/awaitJavaScript 中用于处理异步操作的特性。使用 async 关键字定义一个函数时,它会返回一个 Promise 对象。await 关键字可以暂停函数的执行,等待 Promise 对象解析,并获取其结果。async/await 简化了异步编程,使代码更直观、可读性更高,避免了回调地狱的问题,并提供了方便的错误处理和灵活的逻辑控制。

# 什么是回调函数?

​ 回调函数是在 JavaScript 中用于处理异步操作的函数,具有以下特点:异步执行、作为参数传递、响应事件和错误处理。它们广泛用于定时器、AJAX 请求、事件处理和异步操作等场景。回调函数的使用使得 JavaScript 可以处理异步操作和事件驱动的编程。

# 什么是异步链式调用?

​ 异步链式调用是一种通过连接多个异步操作的方式,以便按照特定顺序执行它们。使用 Promise 可以实现异步链式调用,通过 .then() 方法将多个 Promise 对象连接在一起,处理每个操作的结果,并返回新的 Promise 对象,以便继续添加更多操作。.catch() 方法用于处理链式调用中的错误。异步链式调用使得异步操作的顺序和依赖关系更清晰,避免了回调地狱问题。

# 什么是 Promise.all()

Promise.all() 是一个静态方法,用于并行执行多个 Promise 对象,并在所有 Promise 都成功解析后返回一个新的 Promise 对象。它接收一个包含多个 Promise 对象的数组作为参数,并返回一个 Promise 对象。当所有 Promise 都成功解析时,返回的 Promise 对象解析为一个包含所有解析值的数组。如果其中任何一个 Promise 被拒绝,返回的 Promise 对象将立即被拒绝,并传递拒绝原因。Promise.all() 可以同时触发多个异步操作,并等待它们全部完成后进行处理。

# 解释异步函数的工作原理和执行顺序

​ 异步函数通过使用 asyncawait 关键字来处理异步操作。当调用异步函数时,它会立即返回一个 Promise 对象,并在后台开始执行。在异步函数内部,使用 await 关键字来等待异步操作的完成。当遇到 await 表达式时,异步函数会暂停执行,直到表达式中的 Promise 被解析或拒绝。异步函数的执行顺序是按顺序执行代码,遇到 await 表达式时暂停执行,并在 Promise 解析后恢复执行。

# 异步编程模式

​ 异步编程模式提供了不同的方式来组织和管理异步代码,使其更具可读性、可维护性和扩展性。

  1. 发布/订阅模式是一种用于实现解耦的异步编程模式,也被称为事件模型或消息模型。它基于一个中心主题(或事件总线),允许发布者发布事件或消息,而订阅者可以订阅并接收这些事件或消息。

    // 创建事件总线对象
    const eventBus = {
      events: {}, // 存储事件和对应的订阅者回调函数
    
      // 订阅事件
      subscribe(eventType, callback) {
        if (!this.events[eventType]) {
          this.events[eventType] = []; // 如果事件不存在,则创建一个空的订阅者列表
        }
        this.events[eventType].push(callback); // 将订阅者的回调函数添加到事件的订阅者列表中
      },
    
      // 发布事件
      publish(eventType, data) {
        if (this.events[eventType]) {
          this.events[eventType].forEach((callback) => {
            callback(data); // 执行每个订阅者的回调函数,并传递数据
          });
        }
      },
    };
    
    // 订阅者A
    function subscriberA(data) {
      console.log('Subscriber A received:', data);
    }
    
    // 订阅者B
    function subscriberB(data) {
      console.log('Subscriber B received:', data);
    }
    
    // 订阅事件
    eventBus.subscribe('message', subscriberA);
    eventBus.subscribe('message', subscriberB);
    
    // 发布事件
    eventBus.publish('message', 'Hello, World!');
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
  2. 观察者模式通常由一个主题和多个观察者组成,定义了一种对象之间的依赖关系,当一个对象状态发生变化时,所有依赖它的对象都会得到通知并自动更新。

    // 定义主题对象
    class Subject {
      constructor() {
        this.observers = []; // 存储观察者列表
      }
    
      // 添加观察者
      addObserver(observer) {
        this.observers.push(observer);
      }
    
      // 通知观察者
      notify(data) {
        this.observers.forEach((observer) => {
          observer.update(data); // 调用观察者的更新方法,并传递数据
        });
      }
    }
    
    // 定义观察者对象
    class Observer {
      constructor(name) {
        this.name = name;
      }
    
      // 观察者的更新方法
      update(data) {
        console.log(`${this.name} received: ${data}`);
      }
    }
    
    // 创建主题和观察者对象
    const subject = new Subject();
    const observerA = new Observer('Observer A');
    const observerB = new Observer('Observer B');
    
    // 添加观察者到主题中
    subject.addObserver(observerA);
    subject.addObserver(observerB);
    
    // 主题通知观察者
    subject.notify('Hello, World!');
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
  3. 生成器模式通过生成器函数 Generator Function 创建可暂停和恢复执行的迭代器 Iterator 返回的是一个迭代器对象,通过 yield 关键字暂停函数的执行,并返回一个中间结果,稍后可以从上次停止的地方继续执行。可以使用 next() 方法逐步执行函数并获取每个中间结果。

    function* numberGenerator(start, end) {
      for (let i = start; i <= end; i++) {
        yield i; // 暂停函数的执行,并返回当前的值
      }
    }
    
    // 创建迭代器对象
    const iterator = numberGenerator(1, 5);
    
    // 使用迭代器遍历值
    console.log(iterator.next().value); // 输出: 1
    console.log(iterator.next().value); // 输出: 2
    console.log(iterator.next().value); // 输出: 3
    console.log(iterator.next().value); // 输出: 4
    console.log(iterator.next().value); // 输出: 5
    console.log(iterator.next().value); // 输出: undefined
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
  4. Promise 是一种处理异步操作的对象,通过状态的改变进行后续处理,可以使用链式调用来处理连续的异步操作。

# 什么是异步任务队列 Event Loop

Event LoopJavaScript 引擎的一部分,它负责处理异步任务的执行顺序。它包括宏任务队列和微任务队列,通过循环不断地从宏任务队列中选择任务执行,并在合适的时机执行微任务队列中的任务。Event Loop 的作用是确保异步任务按照正确的顺序执行,避免阻塞主线程,提高程序的性能和响应性。

# 使用 XMLHttpRequest 对象来发送请求

  1. 创建 XMLHttpRequest 对象。
  2. 使用 open() 方法设置请求的方法和 URL。
  3. 注册 onreadystatechange 事件处理程序。
  4. 使用 send() 方法发送请求。
  5. 监听 readyStatestatus 属性的变化,检查请求状态和响应状态。
  6. 如果请求成功(status 值为 200),使用 responseTextresponseXML 属性获取响应数据。
var xhr = new XMLHttpRequest();
xhr.open("GET", "https://api.example.com/data", true);

xhr.onreadystatechange = function() {
  if (xhr.readyState === 4 && xhr.status === 200) {
    var responseData = xhr.responseText;
    // 处理响应数据
    console.log(responseData);
  }
};

xhr.send();
1
2
3
4
5
6
7
8
9
10
11
12

# 使用 Fetch() 来发送请求

Fetch API 是一种现代的、基于 Promise 的网络请求 API,用于进行网络通信和数据交换。具有语法简洁、基于 Promise、内置 JSON 解析、更全面的错误处理、跨域请求处理、更强大的请求和响应控制等优势。

fetch('https://api.example.com/data')
  .then(response => response.json())
  .then(data => {
    // 处理获取到的数据
    console.log(data);
  })
  .catch(error => {
    // 处理错误
    console.error(error);
  });
1
2
3
4
5
6
7
8
9
10

# 大型文件下载

fetch('https://example.com/largefile.pdf')
  .then(response => {
    if (!response.ok) {
      throw new Error('下载文件失败');
    }
    return response.blob();
  })
  .then(blob => {
    // 处理下载的文件
    const url = URL.createObjectURL(blob);
    const link = document.createElement('a');
    link.href = url;
    link.download = 'largefile.pdf';
    link.click();
    URL.revokeObjectURL(url);
  })
  .catch(error => {
    console.error(error);
  });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 大型文件分片上传

// 将文件分割为固定大小的块
function sliceFile(file, chunkSize) {
  var chunks = [];
  var offset = 0;

  while (offset < file.size) {
    var chunk = file.slice(offset, offset + chunkSize);
    chunks.push(chunk);
    offset += chunkSize;
  }

  return chunks;
}

// 上传文件块
function uploadChunk(url, chunk) {
  return fetch(url, {
    method: 'POST',
    body: chunk
  }).then(response => response.json());
}

// 分块上传大型文件
function uploadLargeFile(file, chunkSize, uploadUrl) {
  var chunks = sliceFile(file, chunkSize);
  var promises = [];

  chunks.forEach(chunk => {
    promises.push(uploadChunk(uploadUrl, chunk));
  });

  return Promise.all(promises);
}

// 选择文件并进行分块上传
var fileInput = document.getElementById('file-input');
fileInput.addEventListener('change', function(event) {
  var file = event.target.files[0];
  var chunkSize = 1024 * 1024; // 1MB
  var uploadUrl = 'https://example.com/upload';

  uploadLargeFile(file, chunkSize, uploadUrl)
    .then(responses => {
      // 处理上传结果
      console.log(responses);
    })
    .catch(error => {
      console.error(error);
    });
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

# 后台处理大文件

const fs = require('fs');
const path = require('path');
const { Readable } = require('stream');
const { createReadStream } = fs;

// 读取大型文件并返回 Blob 对象
function readFileAsBlob(filePath) {
  const stream = createReadStream(filePath);
  const chunks = [];

  return new Promise((resolve, reject) => {
    stream.on('data', (chunk) => {
      chunks.push(chunk);
    });

    stream.on('end', () => {
      const fileData = Buffer.concat(chunks);
      const blob = new Blob([fileData], { type: 'application/octet-stream' });
      resolve(blob);
    });

    stream.on('error', (error) => {
      reject(error);
    });
  });
}

// 示例:将文件转换为 Blob 对象并发送给前端
const filePath = path.join(__dirname, 'path/to/largefile.pdf');

readFileAsBlob(filePath)
  .then((blob) => {
    // 发送给前端
    // 可以使用 Express 或其他 Node.js 框架发送响应
    res.setHeader('Content-Type', 'application/octet-stream');
    res.setHeader('Content-Disposition', 'attachment; filename="largefile.pdf"');
    res.send(blob);
  })
  .catch((error) => {
    console.error(error);
    // 处理错误
  });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

# JavaScript中的深拷贝和浅拷贝有什么区别?

  • 浅拷贝(Shallow Copy):

    • 浅拷贝是创建一个新对象或数组,但仅复制引用而不复制实际的值。这意味着原始对象和新对象会共享相同的内部值,对其中一个对象所做的更改也会影响到另一个对象。

    • 浅拷贝只复制了对象或数组的第一层,对于嵌套的对象或数组,仍然是共享引用。

    • 使用 JavaScript 中的一些方法和运算符(如 Object.assign()Array.prototype.slice()、扩展运算符等)可以实现浅拷贝。

  • 深拷贝(Deep Copy):

    • 深拷贝是创建一个全新的对象或数组,同时递归复制所有的嵌套对象和数组,确保原始对象和新对象之间没有任何引用关系。

    • 深拷贝会复制所有层级的对象或数组,并创建它们的独立副本。

    • 实现深拷贝通常需要自定义递归函数或使用第三方库(如 lodash 的 cloneDeep() 方法)来实现。

# 如何实现深拷贝?

function deepCopy(obj) {
  if (obj === null || typeof obj !== 'object') {
    return obj; // 非对象或 null,直接返回
  }
  
  let copy;
  if (obj instanceof Array) {
    copy = [];
    for (let i = 0; i < obj.length; i++) {
      copy[i] = deepCopy(obj[i]); // 递归拷贝数组元素
    }
  } else {
    copy = {};
    for (let key in obj) {
      if (obj.hasOwnProperty(key)) {
        copy[key] = deepCopy(obj[key]); // 递归拷贝对象属性
      }
    }
  }
  
  return copy;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# HTTP 请求头

  1. Host:指定要访问的服务器主机名和端口号。
  2. User-Agent:标识发送请求的客户端(浏览器、应用程序等)的相关信息。
  3. Accept:指定客户端能够接受的响应内容类型。
  4. Content-Type:指定请求体的媒体类型。
  5. Authorization:提供身份验证凭据,用于访问受保护的资源。
  6. Cookie:包含客户端的会话信息,用于与服务器进行状态管理。
  7. Referer:指示请求的来源 URL。
  8. Cache-Control:指定缓存机制的行为和规则。
  9. If-Modified-Since:用于条件请求,检查资源是否已经被修改。
  10. Origin:指示发起请求的源(用于跨域请求)。