likes
comments
collection
share

2023.05.22 更新前端面试问题总结(8道题)

作者站长头像
站长
· 阅读数 16

2023.05.16 - 2023.05.22 更新前端面试问题总结(8道题) 获取更多面试相关问题可以访问 github 地址: github.com/pro-collect… gitee 地址: gitee.com/yanleweb/in…

目录:

  • 中级开发者相关问题【共计 3 道题】

    • 361.函数柯里化了解多少【热度: 529】【JavaScript】【出题公司: 京东】
    • 362.手写 JSON.stringify 和 手写 JSON.parse 实现【热度: 134】【JavaScript】【出题公司: 网易】
    • 366.浏览器有读写能力吗?【浏览器】
  • 高级开发者相关问题【共计 5 道题】

    • 359.DNS解析过程【网络】【出题公司: 百度】
    • 360.WebSocket 协议的底层原理是什么【热度: 1,805】【网络】【出题公司: 百度】
    • 363.模版引擎实现原理【热度: 1,241】【JavaScript】【出题公司: 阿里巴巴】
    • 364.如何优化大规模 dom 操作的场景【热度: 1,012】【浏览器】【出题公司: 阿里巴巴】
    • 367.react native 工作原理是什么?【web应用场景】

中级开发者相关问题【共计 3 道题】

361.函数柯里化了解多少【热度: 529】【JavaScript】【出题公司: 京东】

关键词:函数柯里化、柯里化应用场景、柯里化优势

函数柯里化是什么?

函数柯里化(Currying)是一种在函数式编程中使用的技术,其主要目的是将一个接受多个参数的函数转换成一系列使用一个参数的函数。 这样做的好处是允许你创建一些部分应用的函数,预先固定一些参数,使得代码更简洁,便于复用和组合。

以下是一个简单的柯里化函数的例子:

function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    } else {
      return function(...args2) {
        return curried.apply(this, args.concat(args2));
      };
    }
  };
}

// 使用 curry 函数的例子
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
console.log(curriedSum(1, 2, 3)); // 6

在这个例子中,我们创建了一个 curry 函数,该函数接受一个普通的多参数函数(如 sum)作为输入,并返回一个新的柯里化函数。 这个柯里化函数可以用多种方式调用,其参数可以一次性传递,也可以分批传递。

柯里化有哪些应用场景和优势

函数柯里化在函数式编程中有很多应用场景和优势。以下是一些常见的应用场景和优势:

  1. 参数复用:柯里化可以使我们预先固定一些参数,形成一个部分应用的函数,这样可以将相同参数的重复使用降到最低。这有利于减少参数传递的冗余,使代码更简洁。

例:

function multiply(a, b) {
  return a * b;
}

const double = curry(multiply)(2);
const triple = curry(multiply)(3);

console.log(double(5)); // 10
console.log(triple(5)); // 15

  1. 延迟计算:柯里化允许我们将函数调用分批进行,而不是一次性传递所有参数。这样,我们可以在需要的时候进行最后的计算,提高性能。

例:

const data = [1, 2, 3, 4, 5];
const curriedFilter = curry((predicate, arr) => arr.filter(predicate));

const greaterThanThree = (num) => num > 3;
const filterGreaterThanThree = curriedFilter(greaterThanThree);

// 延迟计算:先创建过滤函数,最后传入数据时才执行
const result = filterGreaterThanThree(data);
console.log(result); // [4, 5]

  1. 代码组合和复用:柯里化有助于创建可以被复用或组合成更复杂形式的函数。这使我们能够构建更加模块化和可扩展的代码库。

例:

const curriedMap = curry((fn, arr) => arr.map(fn));

const doubleAll = curriedMap(double);
const tripleAll = curriedMap(triple);

console.log(doubleAll([1, 2, 3])); // [2, 4, 6]
console.log(tripleAll([1, 2, 3])); // [3, 6, 9]

  1. 更易读的代码:柯里化技术可以让我们的代码更加模块化和函数式,进而提高代码的可读性。柯里化函数更加聚焦于单一职责,这样可以让代码逻辑更清晰。

函数柯里化有助于提高代码的可读性、可维护性和模块化程度,同时减少参数传递的冗余,使代码更简洁。在函数式编程场景中,柯里化是一种非常实用的技术。

362.手写 JSON.stringify 和 手写 JSON.parse 实现【热度: 134】【JavaScript】【出题公司: 网易】

关键词:手写 JSON.stringify、手写 JSON.parse

手写JSON.stringify

JSON.stringify 是一个将 JavaScript 对象或值转换为 JSON 字符串的函数。下面是一个简化的实现,主要考虑以下几种类型:字符串、数字、布尔值、对象和数组。

function jsonStringify(value) {
  const type = typeof value;

  if (type === 'string') {
    return `"${value}"`;
  }

  if (type === 'number' || type === 'boolean' || value === null) {
    return String(value);
  }

  if (type === 'object') {
    if (Array.isArray(value)) {
      const arrayItems = value.map((item) => jsonStringify(item)).join(',');
      return `[${arrayItems}]`;
    } else {
      const objectKeys = Object.keys(value);
      const objectItems = objectKeys.map((key) => {
        const keyValue = jsonStringify(value[key]);
        return keyValue !== undefined ? `"${key}":${keyValue}` : undefined;
      }).filter((item) => item !== undefined).join(',');
      return `{${objectItems}}`;
    }
  }

  return undefined; // 这里省略了对函数、Symbol、循环引用等类型的处理
}

// 使用示例
const obj = {
  a: "hello",
  b: 42,
  c: true,
  d: { e: "world", f: [1, 2, 3] },
};

console.log(jsonStringify(obj)); // {"a":"hello","b":42,"c":true,"d":{"e":"world","f":[1,2,3]}}

请注意,这个实现有很多限制,适用于简单场景。它没有处理循环引用、函数、Symbol 类型等复杂情况。实际项目中,你还是应该使用内置的 JSON.stringify 函数。

手写 JSON.parse

JSON.parse 是一个将 JSON 字符串转换为 JavaScript 对象或值的函数。手写一个简化版的 JSON.parse 可能不会涵盖所有的细节和兼容性问题,这里提供一个基于 JavaScript 的 eval 函数实现的简单版本。请注意,在实际项目中应使用原生的 JSON.parse 函数以保证安全性和性能。

function jsonParse(jsonString) {
  return eval("(" + jsonString + ")");
}

// 使用示例
const jsonString = '{"a": "hello", "b": 42, "c": true, "d": {"e": "world", "f": [1, 2, 3]}}';

console.log(jsonParse(jsonString));
/* 输出:
{
  a: "hello",
  b: 42,
  c: true,
  d: { e: "world", f: [1, 2, 3] },
}
*/

虽然使用 eval 函数能简单地实现 JSON 字符串的解析,但在实践过程中使用 eval 并不安全,因为它会执行任意字符串中包含的 JavaScript 代码。因此,强烈建议实际项目中使用 JSON.parseJSON.stringify 函数。

366.浏览器有读写能力吗?【浏览器】

在一般情况下,浏览器本身不具备直接的读写能力。浏览器是用于显示网页内容的客户端应用程序,其主要功能是发送HTTP请求,接收和渲染服务器返回的HTML、CSS和JavaScript等资源。然而,浏览器提供了一些特定的API,允许开发人员在浏览器中进行读写操作。

下面是一些允许浏览器进行读写操作的API:

  1. Web Storage API:通过localStorage和sessionStorage提供了在浏览器中存储数据的能力。开发人员可以使用这些API将数据以键值对的形式存储在浏览器本地,读取和修改数据。

  2. IndexedDB API:IndexedDB是浏览器提供的一种高性能的非关系型数据库API。开发人员可以使用IndexedDB API在浏览器中创建和管理数据库,进行复杂的数据存储、查询和索引操作。

  3. File API:File API允许浏览器读取和处理本地文件。开发人员可以使用File API选择本地文件并读取其内容,也可以通过FileWriter将数据写入本地文件。

  4. Web Sockets:WebSocket是一种在浏览器和服务器之间实现全双工通信的协议。通过WebSocket API,浏览器可以与服务器建立持久的双向连接,并进行实时的数据读写操作。

需要注意的是,浏览器的读写能力受到一些限制,如同源策略、跨域限制等。为了保障安全性和用户隐私,浏览器会限制对本地文件系统的直接读写访问。读写操作通常是通过浏览器提供的特定API进行,并且需要经过用户的授权和同意。

高级开发者相关问题【共计 5 道题】

359.DNS解析过程【网络】【出题公司: 百度】

DNS(Domain Name System,域名系统)解析是将域名转换为对应的IP地址的过程。下面是DNS解析的一般步骤:

  1. 用户输入域名:用户在浏览器或其他应用程序中输入要访问的域名,例如 "www.example.com"。

  2. 本地缓存查找:操作系统首先会检查本地的DNS缓存,看是否已经缓存了该域名的IP地址。如果有匹配的缓存记录,且仍在有效期内,将直接返回对应的IP地址。

  3. 本地域名服务器查询:如果本地缓存中没有找到对应的IP地址,操作系统会向配置的本地域名服务器(通常由ISP提供)发送查询请求。本地域名服务器是存储了大量DNS记录的服务器,通常可以快速响应查询请求。

  4. 递归查询或迭代查询:本地域名服务器接收到查询请求后,会根据自身的配置进行递归查询或迭代查询。

    • 递归查询:本地域名服务器会代表客户端进行完整的查询过程,直到找到目标域名的IP地址。如果本地域名服务器已经缓存了目标域名的IP地址,它将直接返回结果给客户端。

    迭代查询:本地域名服务器向根域名服务器发送查询请求,根域名服务器返回顶级域名服务器(TLD)的地址。然后本地域名服务器再向TLD发送查询请求,TLD返回该域名的授权域名服务器的地址。最后,本地域名服务器向授权域名服务器发送查询请求,授权域名服务器返回目标域名的IP地址。

  5. 返回IP地址:经过递归或迭代查询后,本地域名服务器会将获取到的IP地址返回给操作系统,然后操作系统将该IP地址存储在本地DNS缓存中,并将IP地址传递给应用程序。

  6. 应用程序访问目标IP地址:应用程序收到IP地址后,可以直接使用该IP地址与目标服务器建立连接,并进行相应的网络请求。

DNS解析过程中还涉及到DNS记录的缓存、DNS服务器层级结构、域名的分级管理等概念和机制,以保证高效的解析过程和互联网的正常运行。

360.WebSocket 协议的底层原理是什么【热度: 1,805】【网络】【出题公司: 百度】

关键词:WebSocket 协议、WebSocket 与 http 区别、全双工通信的协议

WebSocket 通信原理

WebSocket 是一种在Web浏览器和服务器之间进行全双工通信的协议,它通过一个长久的、双向的通信通道来实现实时数据传输。

下面是WebSocket协议的底层原理:

握手(Handshake):WebSocket连接的建立需要通过HTTP握手来升级到WebSocket协议。客户端首先发送一个HTTP请求,其中包含一些特定的头部信息,表明客户端希望升级到WebSocket协议。服务器收到请求后,如果支持WebSocket协议,就会返回一个带有特定头部的HTTP响应,表示握手成功。握手完成后,连接从HTTP协议切换到了WebSocket协议。

  1. 数据帧(Data Frames):一旦握手成功,WebSocket连接就处于打开状态,可以进行数据传输。数据以数据帧的形式在客户端和服务器之间进行传输。数据帧是WebSocket协议中的基本单位,它包含了有效负载(payload)和一些控制信息。有效负载可以是文本数据或二进制数据。

  2. 帧格式(Frame Format):WebSocket数据帧的格式相对简单。它以字节流的形式进行传输,通常由以下几个部分组成:

    • FIN(1 bit):表示消息是否已完成,如果消息只占用一个帧,该位为1,否则为0。
    • RSV1、RSV2、RSV3(各占1 bit):用于扩展使用,目前很少使用。
    • Opcode(4 bits):表示消息类型,例如文本数据、二进制数据、连接关闭等。
    • Mask(1 bit):指示是否对有效负载进行掩码处理。
    • Payload Length(7 bits或16 bits或64 bits):表示有效负载的长度。
    • Masking Key(0或32 bits):如果Mask位为1,表示用于对有效负载进行掩码处理的密钥。
    • Payload Data:实际的有效负载数据。

数据传输:数据通过TCP连接进行传输。WebSocket建立在TCP协议之上,利用TCP的可靠性和双向通信能力来传输数据。客户端和服务器可以随时发送数据帧,数据帧可以被分割成多个TCP包进行传输,接收方会将这些包重新组装成完整的数据帧。

  1. 心跳机制:为了保持连接的活跃状态,WebSocket使用心跳机制来定期发送心跳消息。这些心跳消息可以是空的数据帧或特定的控制帧,服务器可以通过检测心跳消息来确定连接是否仍然有效。

通过以上步骤,WebSocket协议能够在浏览器和服务器之间建立一个持久的、全双工的通信通道,实现实时的双向数据传输。相比传统的HTTP请求,WebSocket减少了通信的延迟,并且能够更高效地进行实时数据交换。

WebSocket 协议 和 http 协议有什么区别

WebSocket协议和HTTP协议有以下几个主要区别:

  1. 连接方式:HTTP协议是基于请求-响应模式的,每次请求都需要建立一个新的连接,并在响应完成后立即关闭连接。而WebSocket协议通过一次握手连接后,保持长久的双向连接,允许服务器主动向客户端推送数据,实现实时的双向通信。

  2. 数据格式:HTTP协议传输的数据一般采用文本或二进制的形式,但每次请求和响应都需要包含HTTP头部信息,使得数据传输的开销较大。WebSocket协议支持以原始的二进制格式进行数据传输,减少了数据传输的开销,并且提供了更低的延迟。

  3. 通信效率:由于HTTP协议每次请求都需要建立和关闭连接,对于频繁的数据交换场景效率较低。而WebSocket协议通过保持长连接,避免了多次建立连接的开销,从而提高了通信的效率。

  4. 服务器推送:HTTP协议是一种单向的协议,客户端需要不断地向服务器发送请求以获取数据。而WebSocket协议支持服务器主动向客户端推送数据,服务器可以随时向客户端发送消息,实现实时的双向通信。

综上所述,WebSocket协议相比HTTP协议在实时通信和双向通信方面更加高效和灵活,适用于需要实时数据传输和双向交互的应用场景,如在线聊天、实时游戏、股票行情等。而HTTP协议则适用于传统的请求-响应模式的数据交换,如网页浏览、文件下载等。

363.模版引擎实现原理【热度: 1,241】【JavaScript】【出题公司: 阿里巴巴】

关键词:模版引擎

前端模板引擎实现原理

前端模板引擎是一种用于处理 HTML 字符串的工具,它允许开发人员在 HTML 中嵌入特殊语法,然后使用模板引擎把数据与这些语法结合,生成最终的 HTML 字符串。这种方式有助于实现数据与表示的分离,使得代码更易于维护。

前端模板引擎的实现原理通常包括以下几个步骤:

  1. 编译模板:将模板字符串解析成模板语法(如变量、循环、条件等)和普通文本。这个过程通常涉及到词法分析和语法分析两个阶段。词法分析将模板字符串切分成多个标记(Token),再通过语法分析将这些标记组织成抽象语法树(AST)。

  2. 生成代码:将抽象语法树转换成 JavaScript 代码。这个过程通常包括将语法节点(AST Nodes)转换成相应的 JavaScript 语句,以渲染数据的形式。

  3. 执行代码:对生成的 JavaScript 代码进行求值,通过传入模板数据,渲染最终的 HTML 字符串。

下面是一个简单的模板引擎实现示例:

function simpleTemplateEngine(template, data) {
  const variableRegex = /{{\s*([\w]+)\s*}}/g; // 匹配变量插值

  let match;
  let lastIndex = 0;
  let result = '';

  while ((match = variableRegex.exec(template)) !== null) {
    result += template.slice(lastIndex, match.index); // 添加文本
    result += data[match[1]]; // 添加变量值
    lastIndex = match.index + match[0].length;
  }

  result += template.slice(lastIndex); // 添加尾部文本
  return result;
}

// 使用示例
const template = 'Hello, {{name}}! Today is {{day}}.';
const data = {
  name: 'John',
  day: 'Monday',
};

console.log(simpleTemplateEngine(template, data)); // 输出:Hello, John! Today is Monday.

这个简化的示例仅支持变量插值,完整的模板引擎需要考虑循环、条件、自定义函数等更复杂的语法和性能优化。在实际项目中,可以选择成熟的模板引擎库,例如 Handlebars、Mustache 或者 Lodash 的 template 函数。

如何在模板引擎中实现条件判断

要在模板引擎中实现条件判断,你需要扩展模板引擎的语法支持和解析能力。以 Handlebars 为例,其中的 ifelse 助手语法可以实现条件判断。首先,我们需要修改匹配变量的正则表达式以识别条件判断语句。接着,在解析过程中,根据条件判断结果添加相应的内容。

以下代码实现了一个简化的模板引擎,支持条件判断:

function parseTemplate(template, data) {
  const tokenRegex = /{{\s*(\/?[\w\s]+\/?)\s*}}/g; // 匹配模板语法 token
  const keywords = /^(if|\/if|else)$/;
  let result = '';
  const stack = [];

  let lastIndex = 0;
  let match;

  while ((match = tokenRegex.exec(template)) !== null) {
    let staticContent = template.substring(lastIndex, match.index);
    result += staticContent;
    lastIndex = match.index + match[0].length;

    const token = match[1].trim();
    const keywordMatch = token.match(keywords);

    if (!keywordMatch) { // 处理变量插值
      result += data[token];
      continue;
    }

    switch (keywordMatch[0]) {
      case 'if':
        stack.push('if');
        const ifCondition = data[token.split(' ')[1]];
        if (ifCondition) {
          tokenRegex.lastIndex += processSubTemplate(stack, tokenRegex, template, data);
        }
        break;
      case 'else':
        stack.push('else');
        tokenRegex.lastIndex += processSubTemplate(stack, tokenRegex, template, data);
        break;
      case '/if':
        stack.pop();
        break;
    }
  }

  result += template.substring(lastIndex);
  return result;
}

function processSubTemplate(stack, tokenRegex, template, data) {
  let subTemplate = '';
  let cursor = tokenRegex.lastIndex;

  while (stack.length && cursor < template.length) {
    cursor++;
    const char = template[cursor];
    subTemplate += char;

    if (char === '}' && template[cursor - 1] === '}') {
      const lastTwo = template.substring(cursor - 2, cursor);
      if (lastTwo === '{{') {
        const match = subTemplate.match(/{{\s*(\/?[\w\s]+\/?)\s*}}/);
        if (match) {
          const token = match[1].trim();
          const keywordMatch = token.match(/^(if|\/if|else)$/);
          if (keywordMatch) {
            if (keywordMatch[0] === stack[stack.length - 1]) {
              stack.pop();
            } else {
              stack.push(keywordMatch[0]);
            }
          }
        }
      }
    }
  }

  if (stack[stack.length - 1] === 'else') {
    stack.pop();
  }

  return subTemplate.length;
}

// 使用示例
const template = `
  {{name}},
  {{if isMember}}
    Welcome back, {{name}}!
  {{else}}
    Please join us!
  {{/if}}
`;

const data = {
  name: "John",
  isMember: true,
};

console.log(parseTemplate(template, data).trim());

这个简化示例说明了如何在模板中实现条件判断。不过请注意,这个实现并没有经过优化,性能可能不佳。在实际项目中,推荐使用成熟的模板引擎库,如 Handlebars、Mustache 等。

364.如何优化大规模 dom 操作的场景【热度: 1,012】【浏览器】【出题公司: 阿里巴巴】

关键词:dom 操作性能、dom 操作优化

在处理大规模DOM操作的场景中,可以采取以下一些优化策略:

  1. 使用批量操作:避免频繁地进行单个DOM操作,而是将多个操作合并为一个批量操作。例如,使用DocumentFragment 来创建一个离线的DOM片段,将多个元素一次性添加到片段中,然后再将整个片段插入到文档中。这样可以减少DOM操作的次数,提高性能。

  2. 避免重复访问和查询:避免在循环或递归操作中重复访问和查询DOM元素。在执行循环或递归操作前,先将需要操作的DOM元素保存在变量中,以减少重复查询的开销。

  3. 使用虚拟DOM(Virtual DOM):虚拟DOM是一种将真实DOM结构映射到JavaScript对象的技术。通过在JavaScript中对虚拟DOM进行操作,然后再将变更应用到真实DOM上,可以减少对真实DOM的直接操作次数,提高性能。常见的虚拟DOM库有React和Vue等。

  4. 分割任务:将大规模DOM操作拆分成多个小任务,并使用requestAnimationFramesetTimeout等方法在每个任务之间进行异步处理,以避免长时间阻塞主线程,提高页面的响应性能。

  5. 使用事件委托:利用事件冒泡机制,将事件处理程序绑定到DOM结构的父元素上,通过事件委托的方式处理子元素的事件。这样可以减少事件处理程序的数量,提高性能。

  6. 避免频繁的重绘和重排:DOM的重绘(Repaint)和重排(Reflow)是比较昂贵的操作,会导致页面重新布局和重新渲染。尽量避免频繁地修改样式属性,可以使用CSS类进行批量的样式变更,或使用display: none 将元素隐藏起来进行操作,最后再显示出来。

  7. 使用合适的工具和库:选择合适的工具和库来处理大规模DOM操作的场景。例如,使用专门的数据绑定库或UI框架,如React、Vue或Angular等,它们提供了高效的组件化和数据更新机制,能够优化DOM操作的性能。

通过以上优化策略,可以减少对DOM的频繁操作,提高大规模DOM操作场景下的性能和响应性能。

367.react native 工作原理是什么?【web应用场景】

React Native是一种基于JavaScript的开发框架,用于构建移动应用程序。它允许开发人员使用React的组件化开发模式来构建原生移动应用,同时跨平台共享代码。

工作原理如下:

  1. JavaScript线程:React Native的应用程序逻辑是通过JavaScript代码来编写的。React Native应用在运行时会创建一个专用的JavaScript线程,负责处理JavaScript代码的解析和执行。

  2. 原生桥(Native Bridge):React Native应用通过原生桥(Native Bridge)连接JavaScript线程和原生平台,使得JavaScript代码能够与原生代码进行通信和交互。原生桥是一个双向通信通道,它将JavaScript的调用转发给原生平台,并将原生平台的事件和回调传递回JavaScript。

  3. Virtual DOM:React Native使用Virtual DOM(虚拟DOM)机制来描述和管理UI的状态和变化。在React Native中,组件的UI层由React组件树构建而成,每个组件都有一个相应的虚拟DOM表示。

  4. 原生渲染:React Native将虚拟DOM的变化映射到相应的原生UI组件上。通过与原生平台的交互,React Native会根据虚拟DOM的变化更新相应的原生UI组件,实现界面的渲染和更新。

  5. 原生组件:React Native提供了一系列的原生组件,这些组件直接映射到原生平台上的真实UI控件,例如文本、图像、按钮等。开发人员可以使用这些原生组件来构建用户界面。

  6. 原生模块:React Native还提供了原生模块的概念,允许开发人员编写原生平台相关的功能和逻辑。通过原生模块,开发人员可以访问设备功能、原生API和第三方库等。

总体来说,React Native通过JavaScript线程和原生桥实现了JavaScript代码和原生平台之间的通信。它利用虚拟DOM机制来管理UI的状态和变化,并通过与原生平台的交互实现UI的渲染和更新。开发人员可以使用React Native提供的原生组件和原生模块来构建跨平台的移动应用程序。