2025面试大全(22)

190 阅读39分钟

1. vue中computed和watch区别

在Vue.js中,computedwatch都是用于响应数据变化的机制,但它们在使用场景和功能上有所区别:

computed(计算属性)

  1. 定义:计算属性是基于它们的依赖进行缓存的。只有当依赖发生变化时,计算属性才会重新计算。
  2. 用途:适用于基于其他数据计算新值的情况,如模板中的复杂逻辑、过滤数据等。
  3. 特点
    • 缓存:计算属性是基于它们的响应式依赖进行缓存的。只要依赖未变,计算属性不会重新计算,这可以提高性能。
    • 同步:计算属性中的函数应该是同步的,没有异步操作。
    • 自动更新:当依赖的响应式属性变化时,计算属性会自动更新。

watch(侦听器)

  1. 定义:侦听器用于观察和响应Vue实例上的数据变动。
  2. 用途:适用于执行异步操作、昂贵的操作或基于数据变化进行复杂逻辑处理的情况。
  3. 特点
    • 异步:侦听器可以执行异步操作。
    • 深度观察:可以设置深度观察,以侦听对象内部值的变化。
    • 灵活性:可以执行任意逻辑,而不仅仅是计算新值。

区别总结

  • 缓存 vs. 不缓存:计算属性是基于依赖缓存的,而侦听器每次数据变化都会执行。
  • 同步 vs. 异步:计算属性是同步的,侦听器可以处理异步操作。
  • 自动更新 vs. 手动处理:计算属性自动更新,侦听器需要手动编写处理逻辑。
  • 使用场景:计算属性适用于简单计算和依赖跟踪,侦听器适用于复杂逻辑和异步操作。

示例

new Vue({
  el: '#app',
  data: {
    firstName: 'Foo',
    lastName: 'Bar',
    fullName: ''
  },
  computed: {
    // 计算属性,基于firstName和lastName计算fullName
    computedFullName: function () {
      return this.firstName + ' ' + this.lastName;
    }
  },
  watch: {
    // 侦听器,当firstName或lastName变化时,更新fullName
    firstName: function (val) {
      this.fullName = val + ' ' + this.lastName;
    },
    lastName: function (val) {
      this.fullName = this.firstName + ' ' + val;
    }
  }
});

在上述示例中,computedFullName是一个计算属性,它基于firstNamelastName自动计算。而watch部分则定义了当firstNamelastName变化时,如何手动更新fullName。 根据具体的需求和场景,选择使用computedwatch。通常,优先考虑使用计算属性,因为它们的缓存机制可以提高性能。当需要处理异步操作或复杂逻辑时,再考虑使用侦听器。

2. vue-loader做了哪些事情?

vue-loader 是一个用于处理 .vue 文件的 Webpack 加载器。它允许你以一种组件化的方式来编写 Vue 应用,其中每个 .vue 文件包含一个单独的组件。vue-loader 做了以下几件事情:

  1. 模板编译
    • .vue 文件中的 <template> 部分编译成渲染函数。这个过程会使用 Vue 的模板编译器,将模板字符串转换成虚拟 DOM 渲染函数。
  2. 样式处理
    • 提取 .vue 文件中的 <style> 部分,并使用 CSS 预处理器(如 Sass、Less 或 Stylus)进行编译,如果配置了相关预处理器的话。
    • 支持将样式模块化,使得样式只作用于当前组件,避免全局污染。
    • 支持自动添加 CSS 前缀,以及 CSS Modules 等功能。
  3. 脚本处理
    • .vue 文件中的 <script> 部分编译成 JavaScript,并使其可以通过 importrequire 语法在其它文件中使用。
    • 支持使用 ES2015+ 语法,并通过 Babel 进行转译。
  4. 单文件组件
    • 允许将模板、脚本和样式写在一个 .vue 文件中,使得组件的代码更加组织化和模块化。
  5. 异步组件
    • 支持 Webpack 的异步加载功能,允许按需加载组件,减少初始加载时间。
  6. 热重载
    • 集成 Webpack 的热重载(Hot Module Replacement, HMR)功能,使得在开发过程中可以实时预览更改,而不需要手动刷新浏览器。
  7. 资源路径解析
    • 自动处理 <template><style> 中的资源路径,如图片、字体等,将其转换为正确的 URL。
  8. 自定义块
    • 支持 .vue 文件中的自定义块,如 <docs>, <config> 等,允许开发者扩展 .vue 文件的功能。
  9. 插件系统
    • 提供了一个插件系统,允许开发者通过插件来扩展 vue-loader 的功能。
  10. 性能优化
    • 通过各种配置选项,可以进行代码分割、tree-shaking 等优化,以减少最终打包文件的体积。 vue-loader 的这些功能使得开发 Vue 应用变得更加高效和便捷,它已经成为 Vue 生态系统中的重要组成部分。通过 vue-loader,开发者可以以一种声明式、组件化的方式来构建用户界面。

3. AST语法树是什么?

**AST(Abstract Syntax Tree,抽象语法树)**是一种用于表示程序代码结构的树形数据结构。它以一种抽象的方式展示了代码的语法结构,而不涉及具体的语法细节。AST 的每个节点都代表了程序代码中的一个语法结构,如表达式、语句、函数定义等。

AST 的主要特点:

  1. 抽象性
    • AST 抽象掉了具体的语法细节,如括号、分号等,只保留了代码的结构信息。
  2. 树形结构
    • AST 以树的形式表示代码,其中每个节点都有子节点,形成了层次结构。
  3. 节点类型
    • AST 中的节点分为多种类型,每种类型对应一种特定的语法结构,如标识符、字面量、表达式、语句等。
  4. 属性
    • 每个节点都有一些属性,这些属性包含了节点相关的信息,如标识符的名称、字面量的值等。

AST 的用途:

  1. 编译器
    • 在编译器中,AST 用于表示源代码的结构,是编译过程中的重要数据结构。
  2. 代码分析
    • AST 可以用于静态代码分析,如查找代码中的潜在错误、优化代码等。
  3. 代码转换
    • AST 可以用于代码转换,如将一种编程语言转换为另一种编程语言,或者实现代码的自动重构。
  4. 代码生成
    • AST 可以用于生成代码,如根据 AST 自动生成文档、测试用例等。
  5. IDE 功能
    • 在集成开发环境(IDE)中,AST 用于实现代码补全、语法高亮、代码导航等功能。
  6. 前端框架
    • 在一些前端框架中,如 Vue、React 等,AST 用于解析模板,生成虚拟 DOM。

AST 的生成:

AST 通常由解析器(Parser)生成。解析器读取源代码,分析其语法结构,然后构建出对应的 AST。这个过程分为两个主要步骤:

  1. 词法分析
    • 将源代码分解成一系列的标记(Token),每个标记代表一个最小的语法单元,如关键字、标识符、字面量等。
  2. 语法分析
    • 根据语言的语法规则,将标记组合成 AST 的节点,最终构建出完整的 AST。 AST 是计算机科学中的一个重要概念,它在程序设计、编译原理、代码分析等领域都有广泛的应用。通过 AST,我们可以更深入地理解代码的结构,从而实现更高级的代码处理功能。

4. 什么是DNS劫持?

DNS劫持(DNS Hijacking)是一种网络攻击技术,通过篡改或拦截域名系统(DNS)的解析结果,将用户对某个域名的访问请求重定向到攻击者指定的IP地址。这种攻击可以发生在用户的设备上、本地网络中或互联网服务提供商(ISP)的层面上。

DNS劫持的工作原理:

  1. 正常DNS解析
    • 当用户在浏览器中输入一个域名时,设备会向DNS服务器发送请求,以获取该域名对应的IP地址。
  2. 劫持发生
    • 在DNS劫持的情况下,攻击者会篡改或拦截这个请求,返回一个错误的IP地址,这个地址指向攻击者的服务器或任何攻击者想要用户访问的地方。
  3. 用户被重定向
    • 用户的服务器,从而可能导致用户数据泄露、账户被盗、恶意软件感染等安全风险。

DNS劫持的类型:

  1. 本地DNS劫持
    • 攻击者在用户的设备上安装恶意软件,篡改DNS设置。
  2. 路由器DNS劫持
    • 攻击者入侵用户的路由器,更改其DNS设置。
  3. ISP DNS劫持
    • 互联网服务提供商在未告知用户的情况下,篡改DNS解析结果,通常用于广告投放或流量监控。
  4. 中间人攻击
    • 攻击者在用户和DNS服务器之间拦截通信,篡改DNS响应。

DNS劫持的影响:

  • 隐私泄露:用户可能被重定向到恶意网站,导致个人信息泄露。
  • 数据安全:敏感数据可能被窃取或篡改。
  • 服务中断:合法服务可能无法访问,影响用户体验。
  • 恶意软件感染:用户可能被诱导下载恶意软件。

防范DNS劫持的措施:

  • 使用安全的DNS服务器:选择信誉良好的DNS服务提供商,如Google Public DNS、Cloudflare DNS等。
  • 保持软件更新:定期更新操作系统、浏览器和网络安全软件,以修补安全漏洞。
  • 使用VPN:虚拟私人网络(VPN)可以加密网络通信,减少被劫持的风险。
  • 检查路由器安全:确保路由器固件更新,使用强密码,并关闭远程管理功能。
  • 安装安全软件:使用防病毒和防恶意软件工具,检测和阻止潜在的DNS劫持攻击。 DNS劫持是一种隐蔽且危险的攻击手段,用户和企业都需要采取相应的安全措施来防范这种攻击。

5. flexible.js实现移动端适配的原理是什么?

flexible.js 是一种常见的移动端适配解决方案,其核心原理是通过动态修改<html>标签的字体大小来适应不同尺寸的屏幕,从而实现页面的等比缩放。以下是flexible.js实现移动端适配的主要原理和步骤:

1. 动态设置根字体大小

flexible.js 会根据设备的屏幕宽度(document.documentElement.clientWidth)动态计算并设置根字体大小(<html>标签的font-size)。通常,会设定一个基准值,例如在屏幕宽度为375px时,根字体大小为20px。然后,根据实际屏幕宽度与基准宽度的比例来调整根字体大小。

2. 使用rem单位

在CSS中,使用rem单位来定义元素的大小。rem单位是相对于根字体大小来计算的,因此,当根字体大小变化时,使用rem单位的元素大小也会相应地缩放。

3. 适配不同屏幕

通过上述两步,实现了在不同屏幕尺寸下,页面元素的等比缩放。这样,无论在何种尺寸的设备上,页面的布局和元素大小都能保持一致的比例,从而实现适配。

4. 兼容性处理

flexible.js 还会处理一些兼容性问题,例如在旧版浏览器中不支持rem单位的情况,会通过JavaScript来动态计算并设置元素的像素值。

具体实现步骤:

  1. 引入flexible.js: 在HTML文件中引入flexible.js脚本。
  2. 设置基准值: 在flexible.js中,可以设置基准屏幕宽度和基准字体大小。
  3. 计算根字体大小: 根据实际屏幕宽度与基准宽度的比例,计算并设置根字体大小。
  4. 使用rem单位: 在CSS中,使用rem单位来定义元素的大小。

示例代码:

// flexible.js 示例代码
(function flexible(window, document) {
  var docEl = document.documentElement;
  var dpr = window.devicePixelRatio || 1;
  // 设置根字体大小
  function setRemUnit() {
    var rem = docEl.clientWidth / 10; // 假设基准屏幕宽度为375px,基准字体大小为20px
    docEl.style.fontSize = rem + 'px';
  }
  setRemUnit();
  // 监听屏幕尺寸变化
  window.addEventListener('resize', setRemUnit);
  window.addEventListener('pageshow', function(e) {
    if (e.persisted) {
      setRemUnit();
    }
  });
  // 兼容旧版浏览器
  if (dpr >= 2) {
    var fakeBody = document.createElement('body');
    var testElement = document.createElement('div');
    testElement.style.border = '.5px solid transparent';
    fakeBody.appendChild(testElement);
    docEl.appendChild(fakeBody);
    if (testElement.offsetHeight === 1) {
      docEl.classList.add('hairlines');
    }
    docEl.removeChild(fakeBody);
  }
})(window, document);
/* CSS 示例代码 */
body {
  font-size: 0.5rem; /* 相对于根字体大小的一半 */
}
.container {
  width: 5rem; /* 相对于根字体大小的5倍 */
  height: 3rem; /* 相对于根字体大小的3倍 */
}

通过这种方式,flexible.js能够实现移动端的适配,使页面在不同尺寸的设备上都能保持良好的布局和视觉效果。

6. JavaScript中的 sort 方法是怎么实现的?

JavaScript中的sort方法是对数组元素进行排序的内置方法。其具体实现依赖于浏览器的JavaScript引擎,但通常是基于快速排序(QuickSort)或归并排序(MergeSort)等高效的排序算法。以下是sort方法的基本使用和实现原理:

基本使用

let array = [5, 3, 8, 1];
array.sort(); // 默认按字符串Unicode码点排序
console.log(array); // 输出: [1, 3, 5, 8]
// 使用比较函数
array.sort((a, b) => a - b); // 升序排序
console.log(array); // 输出: [1, 3, 5, 8]
array.sort((a, b) => b - a); // 降序排序
console.log(array); // 输出: [8, 5, 3, 1]

实现原理

  1. 默认排序
    • 如果没有提供比较函数,数组元素会转换为字符串,并按照字符串的Unicode码点进行排序。
    • 这种默认排序方式对于数字数组可能不会得到预期结果,因为数字会被转换为字符串进行比较。
  2. 自定义比较函数
    • 可以提供一个比较函数作为sort方法的参数,该函数定义了排序的规则。
    • 比较函数接受两个参数(通常是数组中的两个元素),并根据返回值决定元素的排序顺序:
      • 如果返回值小于0,则第一个参数排在前面。
      • 如果返回值等于0,则保持原有顺序。
      • 如果返回值大于0,则第二个参数排在前面。
  3. 排序算法
    • 不同浏览器引擎可能使用不同的排序算法。例如,V8引擎(Chrome和Node.js使用)在过去使用快速排序,但在某些情况下会切换到插入排序以提高性能。
    • 现代的JavaScript引擎会根据数组的特性(如大小、元素类型等)选择最合适的排序算法。

示例:自定义比较函数

function compareNumbers(a, b) {
  return a - b;
}
let numberArray = [10, 5, 40, 25, 100];
numberArray.sort(compareNumbers);
console.log(numberArray); // 输出: [5, 10, 25, 40, 100]

在这个示例中,compareNumbers函数用于比较两个数字,根据返回值对数组进行升序排序。

注意事项

  • sort方法会修改原数组,而不是创建一个新的排序数组。
  • 如果比较函数不正确,可能会导致排序结果不符合预期。
  • 对于大型数组或特定排序需求,可能需要实现自定义排序算法。

浏览器实现

浏览器的具体实现可能会随着时间和版本更新而变化。例如,V8引擎在处理小型数组时可能使用插入排序,而在处理大型数组时使用快速排序或归并排序。这些优化是为了在不同情况下提供最佳性能。 总之,sort方法的实现细节由JavaScript引擎决定,但基本原理是提供一种通用的排序机制,允许通过自定义比较函数来定义排序规则。

7. 回文子串

在JavaScript中,要找到一个字符串中的所有回文子串,可以通过多种方法实现。下面是一种简单的方法,它使用双指针技术来检查每个可能的子串是否为回文,并将所有回文子串存储在一个数组中。

实现步骤:

  1. 初始化结果数组:用于存储所有找到的回文子串。
  2. 遍历字符串:对于字符串中的每个字符,将其作为回文中心的起点。
  3. 扩展中心:从中心向两边扩展,检查左右字符是否相等,从而找到所有以当前字符为中心的回文子串。
  4. 考虑奇数和偶数长度的回文:回文可以是奇数长度(如"aba")或偶数长度(如"abba"),因此需要分别处理。

代码实现:

function findPalindromeSubstrings(s) {
  let result = [];
  // 辅助函数,用于从中心扩展并找到所有回文子串
  function expandAroundCenter(left, right) {
    while (left >= 0 && right < s.length && s[left] === s[right]) {
      result.push(s.substring(left, right + 1));
      left--;
      right++;
    }
  }
  // 遍历字符串,找到所有回文子串
  for (let i = 0; i < s.length; i++) {
    // 找到以s[i]为中心的奇数长度回文子串
    expandAroundCenter(i, i);
    // 找到以s[i]和s[i+1]为中心的偶数长度回文子串
    expandAroundCenter(i, i + 1);
  }
  return result;
}
// 示例
let input = "abba";
let palindromes = findPalindromeSubstrings(input);
console.log(palindromes); // 输出: ["a", "b", "b", "a", "abba", "bb"]

解释:

  • 辅助函数expandAroundCenter:接受两个参数leftright,表示回文的左右边界。函数内部使用while循环向两边扩展,直到不再满足回文条件为止,并将每个找到的回文子串添加到结果数组中。
  • 主函数findPalindromeSubstrings:遍历输入字符串的每个字符,将其作为回文中心的起点,分别检查奇数和偶数长度的回文子串。

注意事项:

  • 这个实现会找到所有可能的回文子串,包括单字符的回文。
  • 如果只需要找到最长的回文子串,可以在扩展时记录最长回文子串的信息,而不是存储所有回文子串。 这个方法的时间复杂度是O(n^2),其中n是字符串的长度,因为需要遍历每个字符并从每个字符开始扩展。对于大多数实际应用来说,这个复杂度是可以接受的。如果需要更高效的算法,可以考虑使用Manacher算法,它可以在O(n)时间复杂度内找到最长的回文子串。

8. 爱吃香蕉的珂珂

题目描述: 珂珂喜欢吃香蕉。这里有N堆香蕉,第i堆中有piles[i]根香蕉。警卫已经离开了,将在H小时后回来。 珂珂可以决定她吃香蕉的速度K(根/小时)。每个小时,她将会选择一堆香蕉,从中吃掉K根。如果这堆香蕉少于K根,她将吃掉这堆的所有香蕉,然后这一小时内不会再吃更多的香蕉。 珂珂喜欢慢慢吃,但仍然想在警卫回来前吃掉所有的香蕉。 返回她可以在H小时内吃掉所有香蕉的最小速度KK为整数)。 示例

输入: piles = [3,6,11,7], H = 8
输出: 4

思路

  1. 确定搜索范围:珂珂吃香蕉的速度K的最小值是1,最大值是香蕉堆中香蕉最多的那一堆的数量。
  2. 二分查找:在确定的搜索范围内使用二分查找,找到满足条件的最小K值。 步骤
  3. 计算吃香蕉的时间:对于给定的K值,计算吃完所有香蕉需要的时间。
  4. 二分查找:通过二分查找调整K值,找到满足条件的最小K代码实现
function minEatingSpeed(piles, H) {
    // 辅助函数,计算在速度K下吃完所有香蕉需要的时间
    function canFinish(K) {
        let hours = 0;
        for (let pile of piles) {
            hours += Math.ceil(pile / K);
        }
        return hours <= H;
    }
    let left = 1;
    let right = Math.max(...piles);
    while (left < right) {
        let mid = Math.floor((left + right) / 2);
        if (canFinish(mid)) {
            right = mid; // 尝试更小的K
        } else {
            left = mid + 1; // 增加K
        }
    }
    return left;
}
// 示例
let piles = [3, 6, 11, 7];
let H = 8;
console.log(minEatingSpeed(piles, H)); // 输出: 4

解释

  1. 辅助函数canFinish(K):计算在速度K下吃完所有香蕉需要的时间。如果所需时间小于或等于H,则返回true,否则返回false
  2. 二分查找:初始化left为1,right为香蕉堆中香蕉最多的数量。在循环中,计算中间值mid,并使用canFinish(mid)判断是否可以在H小时内吃完所有香蕉。根据判断结果调整leftright的值,直到找到满足条件的最小K。 这个方法的时间复杂度主要由二分查找和计算吃香蕉时间决定,为O(N log M),其中N是香蕉堆的数量,M是香蕉堆中香蕉最多的数量。这种效率对于大多数输入来说是可行的。

9. 爬楼梯

题目描述: 假设你正在爬楼梯。需要n阶你才能到达楼顶。 每次你可以爬1阶或2阶。你有多少种不同的方法可以爬到楼顶呢? 示例

输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
1.  1 阶 + 1 阶
2.  2 阶

思路: 这个问题实际上是一个斐波那契数列问题。要到达第n阶楼梯,可以从第n-1阶楼梯爬1阶到达,或者从第n-2阶楼梯爬2阶到达。因此,到达第n阶楼梯的方法数等于到达第n-1阶和第n-2阶的方法数之和。 递推公式

f(n) = f(n-1) + f(n-2)

其中,f(1) = 1f(2) = 2代码实现

function climbStairs(n) {
    if (n === 1) return 1;
    if (n === 2) return 2;
    let a = 1; // f(1)
    let b = 2; // f(2)
    let c;
    for (let i = 3; i <= n; i++) {
        c = a + b; // f(i) = f(i-1) + f(i-2)
        a = b; // 更新f(i-2)
        b = c; // 更新f(i-1)
    }
    return c;
}
// 示例
console.log(climbStairs(2)); // 输出:2

解释

  1. 初始条件:如果只有1阶楼梯,显然只有1种方法;如果有2阶楼梯,有2种方法。
  2. 循环计算:从第3阶开始,使用递推公式计算每一阶的方法数,直到第n阶。
  3. 变量更新:在每一步中,更新ab的值,分别表示f(i-2)f(i-1)。 这种方法的时间复杂度是O(n),空间复杂度是O(1),因为它只使用了常数空间来存储中间结果。这种效率对于大多数输入来说是可行的。

10. cookie、localStorage和sessionStorage 三者之间有什么区别

cookielocalStoragesessionStorage 都是Web存储技术,用于在浏览器中存储数据,但它们之间有一些关键的区别:

1. 存储大小

  • Cookie:通常限制为4KB左右。
  • LocalStorageSessionStorage:通常限制为5MB或更多。

2. 有效期

  • Cookie:可以设置过期时间,默认情况下,当浏览器关闭时cookie会被删除,但也可以设置为在特定日期后过期。
  • LocalStorage:数据永久存储,除非用户手动清除或通过脚本清除。
  • SessionStorage:数据只在当前会话(浏览器标签页)中有效,当标签页关闭时数据会被清除。

3. 数据访问

  • Cookie:每次HTTP请求都会携带cookie,无论请求的页面是否需要这些cookie。
  • LocalStorageSessionStorage:仅通过JavaScript访问,不会自动发送到服务器。

4. 数据共享

  • Cookie:可以在多个标签页和窗口之间共享。
  • LocalStorage:可以在多个标签页和窗口之间共享。
  • SessionStorage:仅在创建它的标签页中有效,不能在多个标签页之间共享。

5. 安全性

  • Cookie:可以通过设置HTTPOnly和Secure属性来增加安全性,防止JavaScript访问和仅通过HTTPS传输。
  • LocalStorageSessionStorage:不提供类似的安全属性,但可以通过其他方式(如CSP)来限制。

6. 使用场景

  • Cookie:通常用于身份验证、会话管理、用户偏好设置等。
  • LocalStorage:适用于存储不需要频繁更新、大容量、长期存在的数据,如用户设置、缓存数据等。
  • SessionStorage:适用于存储临时数据,如用户在表单输入的数据、临时状态等。

7. 兼容性

  • Cookie:几乎所有的浏览器都支持。
  • LocalStorageSessionStorage:现代浏览器都支持,但旧版浏览器可能不支持。

8. API使用

  • Cookie:通过document.cookie属性进行读写,操作相对复杂。
  • LocalStorageSessionStorage:提供了更简单的API,如setItem, getItem, removeItem, clear等。

示例代码

// Cookie
document.cookie = "username=John Doe; expires=Thu, 18 Dec 2043 12:00:00 GMT; path=/";
// LocalStorage
localStorage.setItem("username", "John Doe");
let username = localStorage.getItem("username");
localStorage.removeItem("username");
// SessionStorage
sessionStorage.setItem("username", "John Doe");
let username = sessionStorage.getItem("username");
sessionStorage.removeItem("username");

在选择使用哪种存储技术时,应根据具体的需求和场景来决定。例如,如果需要存储大量数据且不希望每次请求都发送到服务器,可以使用localStorage;如果数据只是临时需要,可以使用sessionStorage;如果需要与服务器进行交互,如身份验证,则可能需要使用cookie

11. 使用js实现二分查找

二分查找是一种在有序数组中查找特定元素的搜索算法。下面是一个使用JavaScript实现的二分查找函数:

function binarySearch(arr, target) {
  let left = 0;
  let right = arr.length - 1;
  while (left <= right) {
    // 计算中间位置
    const mid = left + Math.floor((right - left) / 2);
    // 检查中间元素是否为目标值
    if (arr[mid] === target) {
      return mid; // 找到目标值,返回索引
    } else if (arr[mid] < target) {
      left = mid + 1; // 目标值在右侧子数组中
    } else {
      right = mid - 1; // 目标值在左侧子数组中
    }
  }
  return -1; // 未找到目标值,返回-1
}
// 示例使用
const myArray = [1, 3, 5, 7, 9];
const targetValue = 5;
const index = binarySearch(myArray, targetValue);
if (index !== -1) {
  console.log(`元素 ${targetValue} 在数组中的索引为: ${index}`);
} else {
  console.log(`元素 ${targetValue} 不在数组中`);
}

解释:

  1. 初始化指针left 指向数组的第一个元素,right 指向最后一个元素。
  2. 循环条件:只要 left 不超过 right,就继续循环。
  3. 计算中间位置midleftright 之间的中间位置。
  4. 比较中间元素
    • 如果 arr[mid] 等于 target,则找到目标值,返回 mid
    • 如果 arr[mid] 小于 target,则目标值在右侧子数组中,将 left 移动到 mid + 1
    • 如果 arr[mid]] 大于 target,则目标值在左侧子数组中,将 right 移动到 mid - 1
  5. 未找到目标值:如果循环结束仍未找到目标值,返回 -1

注意事项:

  • 二分查找仅适用于有序数组。
  • 在实际应用中,确保数组不会在查找过程中被修改,否则可能会影响查找结果。 这个实现是二分查找的基本版本,适用于大多数情况。根据具体需求,还可以进行一些优化和调整。

12. 前端怎么实现跨域请求?

前端实现跨域请求的常见方法有以下几种:

1. CORS(跨源资源共享)

CORS是一种允许服务器指定哪些源可以访问其资源的机制。通过在服务器响应头中设置Access-Control-Allow-Origin等字段,可以允许不同源的请求。 示例: 服务器端设置响应头:

Access-Control-Allow-Origin: http://example.com

2. JSONP(JSON with Padding)

JSONP是一种利用<script>标签不受同源策略限制的特性来实现跨域请求的方法。它只能用于GET请求。 示例: 前端代码:

<script>
function handleResponse(data) {
  console.log(data);
}
</script>
<script src="http://example.com/api?callback=handleResponse"></script>

服务器端返回:

handleResponse({ "name": "Alice" });

3. Proxy(代理)

通过设置代理服务器,将前端请求发送到代理服务器,再由代理服务器转发到目标服务器,从而实现跨域。 示例: 使用Nginx作为代理服务器:

server {
  location /api {
    proxy_pass http://example.com;
  }
}

4. WebSocket

WebSocket协议是一种全双工通信协议,可以用于实现跨域请求。 示例: 前端代码:

const socket = new WebSocket('ws://example.com');
socket.onmessage = function(event) {
  console.log(event.data);
};
socket.send('Hello, Server!');

5. PostMessage

window.postMessage方法可以用于在不同窗口或iframe之间传递消息,实现跨域通信。 示例: 发送消息:

window.postMessage('Hello from the parent!', 'http://example.com');

接收消息:

window.addEventListener('message', function(event) {
  if (event.origin === 'http://example.com') {
    console.log(event.data);
  }
});

6. document.domain

对于主域名相同但子域名不同的页面,可以通过设置document.domain为相同的主域名来实现跨域。 示例: 页面A:

document.domain = 'example.com';

页面B:

document.domain = 'example.com';

7. Node.js中间件

使用Node.js作为中间件,接收前端请求并转发到目标服务器,再将响应返回给前端。 示例: Node.js中间件代码:

const express = require('express');
const request = require('request');
const app = express();
app.use('/api', (req, res) => {
  const url = 'http://example.com' + req.url;
  req.pipe(request(url)).pipe(res);
});
app.listen(3000);

选择方法

选择哪种跨域方法取决于具体需求和场景:

  • 如果服务器支持CORS,优先使用CORS。
  • 对于GET请求,可以考虑使用JSONP。
  • 如果需要更灵活的解决方案,可以使用代理或Node.js中间件。
  • 对于实时通信,可以考虑使用WebSocket或PostMessage。 在实际开发中,根据具体情况选择最合适的方法来实现跨域请求。

13. 301、302、303、307、308 这些状态码有什么区别?

HTTP状态码301、302、303、307和308都是重定向状态码,用于指示浏览器请求的资源已被移动到新的位置。它们之间的区别主要在于重定向的类型、是否保留原始请求方法以及重定向的持久性。以下是每个状态码的详细解释:

301 Moved Permanently(永久移动)

  • 含义:请求的资源已被永久移动到新位置。
  • 特点:浏览器会自动将请求重定向到新位置,并且会记住这个新位置,后续请求将直接访问新位置。
  • 用途:用于网站结构变更、域名变更等永久性重定向。

302 Found(临时移动)

  • 含义:请求的资源临时从不同位置响应请求。
  • 特点:浏览器会自动将请求重定向到新位置,但不会记住新位置,后续请求仍会访问原始位置。
  • 用途:用于负载均衡、临时维护等临时性重定向。

303 See Other(查看其他位置)

  • 含义:请求的资源可以在另一个URL上找到,且应使用GET方法获取资源。
  • 特点:无论原始请求使用何种方法(POST、GET等),重定向后的请求都会使用GET方法。
  • 用途:用于在POST请求后重定向到另一个资源,以避免重复提交表单。

307 Temporary Redirect(临时重定向)

  • 含义:请求的资源临时从不同位置响应请求。
  • 特点:与302类似,但要求重定向后的请求使用与原始请求相同的方法。
  • 用途:用于确保重定向过程中请求方法不被改变,适用于临时性重定向。

308 Permanent Redirect(永久重定向)

  • 含义:请求的资源已被永久移动到新位置。
  • 特点:与301类似,但要求重定向后的请求使用与原始请求相同的方法。
  • 用途:用于确保重定向过程中请求方法不被改变,适用于永久性重定向。

总结

  • 持久性:301和308是永久重定向,302、303和307是临时重定向。
  • 请求方法:301和302可能会改变请求方法(通常是POST变GET),而303总是改变为GET,307和308保留原始请求方法。
  • 用途:301和308用于永久性变更,302和307用于临时性变更,303用于特定场景下的重定向,如POST后的重定向。 选择使用哪个状态码取决于重定向的意图和需求。永久性重定向应使用301或308,以通知搜索引擎和浏览器更新链接。临时性重定向应使用302、303或307,以避免搜索引擎错误地更新链接。

14. 行内元素和块级元素有什么区别

行内元素(Inline Elements)和块级元素(Block Elements)是HTML中的两种基本元素类型,它们在布局和显示方式上有着显著的区别:

行内元素(Inline Elements)

  1. 布局:行内元素不会独占一行,它们可以和其他行内元素在同一行上显示。
  2. 宽度与高度:行内元素不能设置宽度和高度,它们的宽度和高度由内容决定。
  3. 边距:行内元素可以设置水平方向的边距(margin-left和margin-right),但不能设置垂直方向的边距(margin-top和margin-bottom)。
  4. 对齐:行内元素可以通过vertical-align属性设置垂直对齐方式。
  5. 常见元素<span>, <a>, <img>, <input>, <label>, <button>等。

块级元素(Block Elements)

  1. 布局:块级元素会独占一行,其他元素不能与它在同一行上显示。
  2. 宽度与高度:块级元素可以设置宽度和高度,如果不设置,宽度通常为父元素的100%,高度由内容决定。
  3. 边距:块级元素可以设置所有四个方向的边距(margin)。
  4. 对齐:块级元素可以通过margin属性实现水平居中,但不能通过vertical-align属性设置垂直对齐。
  5. 常见元素<div>, <p>, <header>, <footer>, <section>, <article>, <nav>, <ul>, <ol>, <li>, <table>等。

其他区别

  • 内部元素:块级元素可以包含行内元素和其他块级元素,而行内元素通常只能包含文本或其他行内元素。
  • 格式化上下文:块级元素可以建立新的块级格式化上下文(BFC),而行内元素不能。
  • 换行:行内元素中的文本不会自动换行,而块级元素中的文本会根据需要自动换行。

示例

<!-- 行内元素 -->
<span>This is an inline element.</span><span>This is another inline element.</span>
<!-- 块级元素 -->
<div>This is a block element.</div>
<div>This is another block element.</div>

在上述示例中,<span>元素是行内元素,它们会在同一行上显示。而<div>元素是块级元素,每个<div>都会独占一行。 了解这些区别有助于更好地进行网页布局和设计。

15. 两个 Node.js 进程如何通信?

在Node.js中,两个进程之间的通信可以通过多种方式实现,以下是几种常见的通信方法:

1. IPC(Inter-Process Communication)

Node.js提供了内置的IPC支持,可以通过child_process模块创建子进程,并使用IPC通道进行通信。

const { spawn } = require('child_process');
const child = spawn('node', ['child.js'], {
  stdio: ['inherit', 'inherit', 'inherit', 'ipc']
});
child.on('message', (msg) => {
  console.log('Received from child:', msg);
});
child.send({ hello: 'world' });

在子进程child.js中:

process.on('message', (msg) => {
  console.log('Received from parent:', msg);
});
process.send({ hello: 'from child' });

2. HTTP/HTTPS

可以使用HTTP或HTTPS服务器和客户端进行通信。

// server.js
const http = require('http');
const server = http.createServer((req, res) => {
  if (req.url === '/message') {
    res.end('Message received');
  }
});
server.listen(3000, () => {
  console.log('Server is listening on port 3000');
});
// client.js
const http = require('http');
const options = {
  hostname: 'localhost',
  port: 3000,
  path: '/message',
  method: 'GET'
};
const req = http.request(options, (res) => {
  let data = '';
  res.on('data', (chunk) => {
    data += chunk;
  });
  res.on('end', () => {
    console.log(data);
  });
});
req.end();

3. TCP

使用TCP套接字进行通信。

// server.js
const net = require('net');
const server = net.createServer((socket) => {
  socket.on('data', (data) => {
    console.log('Received:', data.toString());
    socket.write('Message received');
  });
});
server.listen(3000, () => {
  console.log('Server is listening on port 3000');
});
// client.js
const net = require('net');
const client = new net.Socket();
client.connect(3000, 'localhost', () => {
  console.log('Connected');
  client.write('Hello, server!');
});
client.on('data', (data) => {
  console.log('Received from server:', data.toString());
  client.destroy(); // 关闭连接
});

4. Unix Domain Sockets

类似于TCP,但用于同一主机上的进程间通信。

// server.js
const net = require('net');
const fs = require('fs');
const socketPath = '/tmp/my.sock';
if (fs.existsSync(socketPath)) {
  fs.unlinkSync(socketPath);
}
const server = net.createServer((socket) => {
  socket.on('data', (data) => {
    console.log('Received:', data.toString());
    socket.write('Message received');
  });
});
server.listen(socketPath, () => {
  console.log('Server is listening on', socketPath);
});
// client.js
const net = require('net');
const socketPath = '/tmp/my.sock';
const client = net.createConnection(socketPath, () => {
  console.log('Connected');
  client.write('Hello, server!');
});
client.on('data', (data) => {
  console.log('Received from server:', data.toString());
  client.end(); // 关闭连接
});

5. Message Queues

使用消息队列(如RabbitMQ、Redis等)进行异步通信。

6. Shared Memory

通过共享内存进行通信,但这种方式在Node.js中不常见,因为Node.js的非阻塞特性。

7. Files

通过读写文件进行通信,但这种方式效率较低,不推荐用于高频率通信。 选择哪种通信方式取决于具体的应用场景和需求。例如,对于同一主机上的进程间通信,可以使用IPC或Unix Domain Sockets;对于分布式系统,可以使用HTTP、TCP或消息队列。

16. 怎么实现图片懒加载?

图片懒加载是一种优化网页加载时间的技术,它只在图片进入视口(即用户可以看到的区域)时才加载图片。以下是一些实现图片懒加载的常见方法:

1. 原生JavaScript实现

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lazy Loading Images</title>
<style>
  img {
    width: 100%;
    height: auto;
    display: block;
  }
</style>
</head>
<body>
<img class="lazy-load" data-src="image1.jpg" alt="Image 1">
<img class="lazy-load" data-src="image2.jpg" alt="Image 2">
<!-- 更多图片 -->
<script>
document.addEventListener("DOMContentLoaded", function() {
  var lazyImages = [].slice.call(document.querySelectorAll("img.lazy-load"));
  if ("IntersectionObserver" in window) {
    let lazyImageObserver = new IntersectionObserver(function(entries, observer) {
      entries.forEach(function(entry) {
        if (entry.isIntersecting) {
          let lazyImage = entry.target;
          lazyImage.src = lazyImage.dataset.src;
          lazyImage.classList.remove("lazy-load");
          lazyImageObserver.unobserve(lazyImage);
        }
      });
    });
    lazyImages.forEach(function(lazyImage) {
      lazyImageObserver.observe(lazyImage);
    });
  } else {
    // Fallback for browsers without IntersectionObserver support
    lazyImages.forEach(function(lazyImage) {
      lazyImage.src = lazyImage.dataset.src;
    });
  }
});
</script>
</body>
</html>

2. 使用HTML的loading属性

现代浏览器支持loading属性,可以简单地实现懒加载:

<img src="placeholder.jpg" data-src="image1.jpg" alt="Image 1" loading="lazy">
<img src="placeholder.jpg" data-src="image2.jpg" alt="Image 2" loading="lazy">
<!-- 更多图片 -->

浏览器会自动处理带有loading="lazy"属性的图片,只有当图片接近视口时才会加载。

3. 使用第三方库

有许多JavaScript库可以帮助实现懒加载,例如:

  • LazyLoad: 一个轻量级的懒加载库。
  • lazysizes: 一个高性能的懒加载库,支持响应式图片和多种特性。
使用LazyLoad库的示例:

首先,引入LazyLoad库:

<script src="https://cdnjs.cloudflare.com/ajax/libs/lazyload/2.0.0-beta.2/lazyload.min.js"></script>

然后,初始化LazyLoad:

<img class="lazyload" data-src="image1.jpg" alt="Image 1">
<img class="lazyload" data-src="image2.jpg" alt="Image 2">
<!-- 更多图片 -->
<script>
  lazyload();
</script>

4. 使用CSS和JavaScript

可以通过CSS将图片隐藏,然后在JavaScript中检测滚动事件来显示图片:

<img class="lazy-load" data-src="image1.jpg" alt="Image 1" style="display: none;">
<img class="lazy-load" data-src="image2.jpg" alt="Image 2" style="display: none;">
<!-- 更多图片 -->
<script>
window.addEventListener("scroll", function() {
  var lazyImages = document.querySelectorAll("img.lazy-load");
  lazyImages.forEach(function(img) {
    if (imgInView(img)) {
      img.src = img.dataset.src;
      img.style.display = "block";
    }
  });
});
function imgInView(img) {
  var rect = img.getBoundingClientRect();
  return (
    rect.bottom >= 0 &&
    rect.right >= 0 &&
    rect.top <= (window.innerHeight || document.documentElement.clientHeight) &&
    rect.left <= (window.innerWidth || document.documentElement.clientWidth)
  );
}
</script>

注意事项

  • 占位图:在图片加载完成之前,可以使用占位图来提供更好的用户体验。
  • 错误处理:考虑添加错误处理逻辑,例如在图片加载失败时显示备用图片。
  • 性能:避免在滚动事件中执行复杂的操作,以防止性能问题。 选择哪种方法取决于项目需求、浏览器兼容性和个人偏好。现代浏览器支持的loading="lazy"属性是最简单的方法,而使用IntersectionObserver API则提供了更多的灵活性和控制。第三方库可以简化实现过程,但可能会增加额外的依赖。

17. 浏览器为什么要请求并发数限制?

浏览器限制请求并发数的主要原因包括:

  1. 资源管理:每个浏览器进程或线程都有其资源限制,如内存和CPU。限制并发请求数量可以防止浏览器消耗过多资源,从而避免浏览器崩溃或变慢。
  2. 服务器压力:如果浏览器同时发送大量请求到同一服务器,可能会对服务器造成巨大压力,导致服务器过载。限制并发请求数量有助于减轻服务器的负担。
  3. 网络拥堵:过多的并发请求可能会占用大量网络带宽,导致网络拥堵,影响其他用户的网络体验。
  4. HTTP/1.1协议限制:HTTP/1.1协议本身对每个主机名的连接数有限制,通常最多允许同时打开2个连接。这是为了防止过多的连接占用服务器资源。
  5. 用户体验:过多的并发请求可能会导致浏览器响应变慢,影响用户的浏览体验。限制并发请求数量可以确保浏览器能够及时响应用户的操作。
  6. 避免资源竞争:在某些情况下,过多的并发请求可能会导致资源竞争,如多个请求同时请求同一资源,可能导致资源锁定或冲突。
  7. 缓存效率:限制并发请求数量可以提高缓存的效率。如果同时有大量请求,可能会导致缓存频繁失效,从而降低缓存的效果。
  8. 安全性考虑:限制并发请求数量也可以作为一种安全措施,防止恶意用户通过发送大量请求进行拒绝服务攻击(DoS攻击)。 不同的浏览器可能会有不同的并发请求限制策略,而且这些限制可能会随着浏览器版本和网络环境的变化而变化。现代浏览器和HTTP/2协议在一定程度上通过多路复用等技术缓解了这些限制,但并发请求的管理仍然是一个重要的考虑因素。

18. 我现在有一个canvas,上面随机布着一些黑块,请实现方法,计算canvas上有多少个黑块。

要计算Canvas上随机分布的黑块数量,可以通过以下步骤实现:

  1. 获取Canvas图像数据:使用Canvas的getContext('2d')方法获取2D渲染上下文,然后使用getImageData()方法获取Canvas上的图像数据。
  2. 遍历图像数据:图像数据是一个包含红色、绿色、蓝色和透明度(RGBA)值的数组。遍历这个数组,检查每个像素的颜色。
  3. 识别黑块:定义一个阈值来判断一个像素是否为黑色。通常,如果一个像素的红色、绿色和蓝色值都非常低(例如,都小于某个阈值),可以认为它是黑色的。
  4. 计数黑块:遍历图像数据,对每个黑色像素进行计数。为了确保只计算每个黑块一次,可以采用一些策略,比如标记已计数的黑块像素。
  5. 优化算法(可选):如果黑块较大,可以采用一些图像处理算法,如连通组件标记算法,来更高效地计数黑块。 以下是一个简单的JavaScript示例,展示了如何实现这个方法:
function countBlackBlocks(canvas) {
  const context = canvas.getContext('2d');
  const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
  const data = imageData.data;
  let blackBlockCount = 0;
  // 定义黑色阈值
  const blackThreshold = 50; // 例如,RGB值都小于50认为是黑色
  // 遍历图像数据
  for (let i = 0; i < data.length; i += 4) {
    const red = data[i];
    const green = data[i + 1];
    const blue = data[i + 2];
    const alpha = data[i + 3];
    // 检查像素是否为黑色
    if (red < blackThreshold && green < blackThreshold && blue < blackThreshold && alpha > 128) {
      // 标记这个黑块已经被计数
      data[i + 3] = 0; // 将透明度设置为0,避免重复计数
      // 可以在这里添加更多的逻辑来识别整个黑块
      blackBlockCount++;
    }
  }
  // 注意:这个简单的示例只计数了黑色像素,没有考虑黑块的实际大小和形状
  return blackBlockCount;
}
// 假设你有一个Canvas元素
const canvas = document.getElementById('myCanvas');
const blackBlocks = countBlackBlocks(canvas);
console.log('Canvas上的黑块数量:', blackBlocks);

请注意,这个示例代码非常简单,它只是计数了黑色像素,并没有真正识别出每个独立的黑块。要准确计数每个黑块,需要更复杂的图像处理逻辑,比如使用连通组件标记算法来识别和计数每个独立的黑块。这种算法可以遍历图像数据,将相邻的黑色像素组合成一个黑块,并确保每个黑块只被计数一次。

19. 怎么解决canvas中获取跨域图片数据的问题?

在Canvas中获取跨域图片数据时,通常会遇到安全限制,导致无法读取图像数据。这是由于同源策略(Same-Origin Policy)的限制,旨在防止恶意网站访问敏感数据。为了解决这个问题,可以采取以下几种方法:

1. CORS(跨源资源共享)

确保图片服务器支持CORS,并且在响应头中包含Access-Control-Allow-Origin。你可以将这个值设置为*以允许所有域,或者指定特定的域名。

Access-Control-Allow-Origin: *

或者在Canvas中使用图片之前,确保图片的CORS设置正确:

const image = new Image();
image.crossOrigin = 'anonymous'; // 或者指定域名
image.src = 'https://example.com/image.png';
image.onload = () => {
  // 在这里使用图片
  const canvas = document.getElementById('myCanvas');
  const context = canvas.getContext('2d');
  context.drawImage(image, 0, 0);
  // 现在可以安全地获取图像数据
};

2. 服务器端代理

如果无法修改图片服务器的CORS设置,可以在自己的服务器上设置一个代理,通过代理来获取图片。这样,图片的来源就变成了你的服务器,从而避免了跨域问题。

// 服务器端代码示例(使用Node.js和Express)
const express = require('express');
const request = require('request');
const app = express();
app.get('/proxy-image', (req, res) => {
  const imageUrl = req.query.url;
  request({ url: imageUrl }).pipe(res);
});
app.listen(3000, () => {
  console.log('Proxy server running on http://localhost:3000');
});

然后在客户端使用代理地址:

const image = new Image();
image.src = 'http://localhost:3000/proxy-image?url=https://example.com/image.png';
image.onload = () => {
  // 在这里使用图片
};

3. 使用第三方服务

有一些第三方服务提供了CORS代理的功能,可以将图片URL通过这些服务来获取,从而绕过跨域限制。

4. 本地文件

如果图片是用户上传的,可以先将图片保存到本地服务器,然后再从本地服务器加载到Canvas中。

5. 转换为Base64

如果图片数据不是特别大,可以先将图片转换为Base64编码的字符串,然后在Canvas中使用这个字符串。这种方法不适用于大图片,因为会导致性能问题。

function convertToBase64(url, callback) {
  const canvas = document.createElement('canvas');
  const context = canvas.getContext('2d');
  const image = new Image();
  image.crossOrigin = 'Anonymous';
  image.onload = () => {
    canvas.width = image.width;
    canvas.height = image.height;
    context.drawImage(image, 0, 0);
    const dataURL = canvas.toDataURL('image/png');
    callback(dataURL);
  };
  image.src = url;
}
convertToBase64('https://example.com/image.png', (base64) => {
  const image = new Image();
  image.src = base64;
  image.onload = () => {
    // 在这里使用图片
  };
});

请注意,使用Base64编码的图片仍然需要CORS设置正确,否则toDataURL方法会失败。 选择哪种方法取决于你的具体需求和服务器配置。通常,CORS是最直接和最标准的方法,但如果无法修改服务器设置,可以考虑使用代理或第三方服务。

20. es5 中的类和es6中的class有什么区别?

在ES5中,并没有类的概念,但开发者可以通过构造函数和原型链来模拟类的行为。而在ES6中,引入了class关键字,提供了更简洁、更直观的语法来定义类。以下是ES5中的“类”和ES6中的class的主要区别:

1. 定义方式

ES5:

function Person(name) {
  this.name = name;
}
Person.prototype.sayHello = function() {
  console.log('Hello, my name is ' + this.name);
};

ES6:

class Person {
  constructor(name) {
    this.name = name;
  }
  sayHello() {
    console.log(`Hello, my name is ${this.name}`);
  }
}

2. 继承

ES5:

function Student(name, grade) {
  Person.call(this, name); // 调用父构造函数
  this.grade = grade;
}
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;
Student.prototype.sayGrade = function() {
  console.log(`I am in grade ${this.grade}`);
};

ES6:

class Student extends Person {
  constructor(name, grade) {
    super(name); // 调用父构造函数
    this.grade = grade;
  }
  sayGrade() {
    console.log(`I am in grade ${this.grade}`);
  }
}

3. 方法定义

ES5: 方法需要定义在构造函数的.prototype上。 ES6: 方法可以直接在类体内定义,不需要使用.prototype。

4. 静态方法

ES5:

Person.create = function(name) {
  return new Person(name);
};

ES6:

static create(name) {
  return new this(name);
}

5. 属性定义

ES5: 属性通常在构造函数中定义。 ES6: 除了在构造函数中定义属性,还可以使用类字段(Class Fields)的语法(这是一个较新的提案,可能需要使用Babel等转译器):

class Person {
  name = 'Unknown'; // 类字段
  constructor(name) {
    this.name = name;
  }
}

6. 可见性

ES5: 没有内置的语法来定义私有属性或方法,通常使用命名约定(如_privateMethod)来表示。 ES6: 可以使用#前缀来定义私有属性(这是一个较新的提案,可能需要使用Babel等转译器):

class Person {
  #privateName; // 私有属性
  constructor(name) {
    this.#privateName = name;
  }
}

7. 实例化

ES5: 使用new关键字调用构造函数来创建实例。 ES6: 同样使用new关键字,但语法上更接近传统面向对象语言的类。

8. 子类中的this

ES5: 在子构造函数中,必须先调用父构造函数,然后再使用thisES6: 在子类的构造函数中,必须先调用super(),然后再使用this

总结

ES6的class语法提供了更清晰、更简洁的方式来定义类和继承,使得JavaScript的面向对象编程更加直观和易于理解。然而,ES6的类仍然是基于原型链的,class语法只是提供了更高级的抽象。在底层,ES6的类和ES5的构造函数加原型链的实现方式是相似的。

21. 背包问题

背包问题是一个经典的算法问题,通常分为0/1背包问题和完全背包问题。这里我将给出0/1背包问题的JavaScript实现。 0/1背包问题的描述是:给定一组物品,每种物品都有自己的重量和价值,背包总容量有限,要求在不超过背包容量的情况下,使背包中物品的总价值最大。 以下是0/1背包问题的动态规划解法:

function knapsack(weights, values, capacity) {
  const n = weights.length;
  // 创建一个二维数组来存储中间结果
  const dp = Array.from({ length: n + 1 }, () => Array(capacity + 1).fill(0));
  // 动态规划填表
  for (let i = 1; i <= n; i++) {
    for (let w = 1; w <= capacity; w++) {
      if (weights[i - 1] <= w) {
        // 选择当前物品和不选择当前物品的最大值
        dp[i][w] = Math.max(
          values[i - 1] + dp[i - 1][w - weights[i - 1]], // 选择当前物品
          dp[i - 1][w] // 不选择当前物品
        );
      } else {
        // 当前物品重量超过背包容量,不能选择
        dp[i][w] = dp[i - 1][w];
      }
    }
  }
  // 返回最大价值
  return dp[n][capacity];
}
// 示例
const weights = [2, 3, 4, 5]; // 物品的重量
const values = [3, 4, 5, 6]; // 物品的价值
const capacity = 5; // 背包容量
console.log(knapsack(weights, values, capacity)); // 输出最大价值

这个函数knapsack接受三个参数:物品的重量数组weights,物品的价值数组values,以及背包的容量capacity。它返回在不超过背包容量的情况下,能够获得的最大价值。 动态规划表dp的行表示物品,列表示容量。dp[i][w]表示在考虑前i个物品,且背包容量为w时能够获得的最大价值。 请注意,这个实现假设weightsvalues数组的长度相同,并且每个物品只能被选择一次(0/1背包问题的特性)。如果需要解决完全背包问题,需要对动态规划的填表过程进行适当的修改。

22. 全排列

全排列是指从n个不同元素中任取m(m≤n)个元素,按照一定的顺序排列起来,叫做从n个不同元素中取出m个元素的一个排列。当m=n时,所有排列情况叫做全排列。 下面是一个使用JavaScript实现全排列的函数,它使用了回溯算法:

function permute(nums) {
  const result = [];
  const backtrack = (path, options) => {
    if (path.length === nums.length) {
      result.push([...path]); // 制作副本并添加到结果中
      return;
    }
    for (let i = 0; i < options.length; i++) {
      path.push(options[i]); // 选择
      backtrack(path, options.filter((_, index) => index !== i)); // 递归,去掉已选择的元素
      path.pop(); // 撤销选择
    }
  };
  backtrack([], nums);
  return result;
}
// 示例
const nums = [1, 2, 3];
console.log(permute(nums));

这个permute函数接受一个数组nums,它包含所有需要排列的元素。函数返回一个数组,其中包含所有可能的全排列。 函数内部定义了一个backtrack函数,它是一个递归函数,用于生成排列。path参数表示当前排列的路径,options参数表示剩余可选的元素。当path的长度等于nums的长度时,表示找到一个完整的排列,将其添加到结果数组result中。 在backtrack函数中,我们遍历options数组,对于每个元素,我们将其添加到path中,然后递归调用backtrack,传递新的path和去掉当前元素的options。递归结束后,我们通过path.pop()撤销选择,以便进行下一次选择。 这个实现的时间复杂度是O(n!),因为共有n!种排列方式,空间复杂度是O(n),用于存储递归调用的栈。

23. ES6有哪些新特性?

ES6,也称为ECMAScript 2015,是JavaScript语言的一个重要更新,引入了许多新特性和语法改进。以下是一些主要的ES6新特性:

  1. let和const
    • let允许声明块级作用域的变量,避免了var带来的变量提升问题。
    • const用于声明常量,其值一旦被设定后不可被修改。
  2. 箭头函数(Arrow Functions):
    • 提供了更简洁的函数声明方式,并且自动绑定this值到外围上下文。
  3. 模板字符串(Template Strings):
    • 允许使用反引号(`)来创建字符串,支持多行字符串和字符串插值。
  4. 解构赋值(Destructuring Assignment):
    • 允许从数组或对象中提取多个属性,并直接赋值给变量。
  5. 扩展运算符(Spread Operator):
    • 允许将一个数组或对象扩展为多个元素,用于函数调用、数组字面量或对象字面量。
  6. 剩余参数(Rest Parameters):
    • 允许将多个参数收集到一个数组中,用于函数定义。
  7. 默认参数值(Default Parameters):
    • 允许在函数定义时为参数设置默认值。
  8. (Classes):
    • 引入了类的概念,提供了更简洁的语法来创建对象和继承。
  9. 模块(Modules):
    • 引入了模块系统,允许将代码分割成不同的文件,并通过importexport语句进行模块的导入和导出。
  10. Promise
    • 提供了更强大的异步编程解决方案,允许以更优雅的方式处理异步操作。
  11. 生成器(Generators):
    • 允许创建一个函数,该函数可以暂停执行,并在需要时恢复执行。
  12. 迭代器和for...of循环
    • 引入了迭代器协议,允许自定义对象的迭代行为,for...of循环用于遍历可迭代对象。
  13. Map和Set
    • Map是一种键值对集合,Set是一种值的无重复集合。
  14. Symbol
    • 引入了一种新的原始数据类型Symbol,用于创建唯一的标识符。
  15. Proxy和Reflect
    • Proxy用于创建一个对象的代理,可以拦截并定义对象的基本操作。
    • Reflect提供了一种方法来统一操作对象,与Proxy配合使用。
  16. 尾调用优化(Tail Call Optimization):
    • 允许某些函数的尾调用被优化,以避免增加调用栈的大小。 这些新特性使得JavaScript更加现代化,提高了开发效率和代码的可读性。随着ES6的普及,这些特性在现代JavaScript开发中得到了广泛的应用。

24. jquery的链式调用是怎么实现的?

jQuery的链式调用是通过在方法中返回对象本身(通常是thisjQuery对象)来实现的。这样,当一个方法执行完成后,它返回的对象可以继续调用其他方法,形成链式调用。下面是链式调用实现的一些关键点:

  1. 方法返回this: jQuery对象的方法通常会在执行完操作后返回this,即当前jQuery对象。这样就可以继续调用该对象的其他方法。
    $.fn.myMethod = function() {
        // 方法操作
        return this; // 返回当前jQuery对象,以支持链式调用
    };
    
  2. 方法返回新的jQuery对象: 有些方法会返回一个新的jQuery对象,例如选择器方法。这样也可以继续链式调用新对象的方法。
    $.fn.find = function(selector) {
        // 根据选择器找到元素,并包装成新的jQuery对象
        var newjQueryObject = /* ... */;
        return newjQueryObject; // 返回新的jQuery对象,以支持链式调用
    };
    
  3. 确保每个方法都支持链式调用: 为了实现链式调用,jQuery中的每个方法都需要小心设计,确保它们在执行完操作后返回合适的对象。
  4. 结束链式调用: 有些方法可能不需要或不支持链式调用,例如某些获取信息的方法(如.length())。这些方法会返回非jQuery对象的数据(如数字、字符串等),从而结束链式调用。
  5. 链式调用的优点
    • 简洁:减少了代码量,使代码更简洁易读。
    • 高效:减少了变量声明,提高了代码执行效率。
  6. 链式调用的注意事项
    • 避免过度使用:虽然链式调用很方便,但过度使用可能导致代码可读性下降,尤其是当链式调用过长时。
    • 理解每个方法的返回值:在使用链式调用时,需要清楚每个方法返回的是什么,以确保链式调用正确进行。 通过这些方式,jQuery实现了其著名的链式调用特性,使得操作DOM元素变得更加流畅和高效。

25. 使用原生js实现以下效果:点击容器内的图标,图标边框变成border:1px solid red,点击空白处重置

要实现这个效果,我们可以使用原生JavaScript来添加事件监听器。以下是一个简单的实现步骤:

  1. 为容器添加点击事件监听器。
  2. 在事件处理函数中,检查点击事件的目标元素(event.target)。
  3. 如果目标元素是图标,则改变其边框样式。
  4. 如果点击的是容器内的空白处,则重置所有图标的边框样式。 下面是具体的代码实现:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Icon Border Change</title>
<style>
  .container {
    width: 300px;
    height: 300px;
    border: 1px solid #ccc;
    position: relative;
  }
  .icon {
    width: 50px;
    height: 50px;
    background-color: #blue;
    position: absolute;
    cursor: pointer;
  }
  /* 初始边框样式 */
  .icon.active {
    border: 1px solid red;
  }
</style>
</head>
<body>
<div class="container">
  <div class="icon" style="top: 20px; left: 20px;"></div>
  <div class="icon" style="top: 20px; right: 20px;"></div>
  <!-- 更多图标 -->
</div>
<script>
  // 获取容器元素
  var container = document.querySelector('.container');
  // 获取所有图标元素
  var icons = document.querySelectorAll('.icon');
  // 为容器添加点击事件监听器
  container.addEventListener('click', function(event) {
    // 检查点击的是否为图标
    if (event.target.classList.contains('icon')) {
      // 重置所有图标的边框
      icons.forEach(function(icon) {
        icon.classList.remove('active');
      });
      // 为点击的图标添加红色边框
      event.target.classList.add('active');
    } else {
      // 点击的是空白处,重置所有图标的边框
      icons.forEach(function(icon) {
        icon.classList.remove('active');
      });
    }
  });
</script>
</body>
</html>

在这个示例中,我们有一个容器div.container和多个图标div.icon。我们为容器添加了一个点击事件监听器,当点击事件发生时,我们检查点击的目标是否具有icon类。如果是,我们首先移除所有图标的active类(这将移除红色边框),然后只为被点击的图标添加active类。如果点击的是容器内的空白处,我们则移除所有图标的active类,从而重置边框样式。 请注意,这个示例中的图标是用div元素表示的,并且有一个icon类。你可以根据实际情况调整选择器和其他细节。

26. CSS 垂直居中有哪些实现方式?

CSS中实现垂直居中有多种方法,以下是几种常见的实现方式:

1. 使用line-height

适用于单行文本的垂直居中。

.parent {
  height: 200px;
  line-height: 200px; /* 与高度相同 */
}
.child {
  display: inline-block;
  vertical-align: middle;
}

2. 使用vertical-aligntable-cell

将父元素设置为display: table-cell,并使用vertical-align: middle

.parent {
  display: table-cell;
  vertical-align: middle;
  height: 200px;
}
.child {
  display: inline-block;
}

3. 使用flexbox

利用Flexbox布局的align-items属性。

.parent {
  display: flex;
  align-items: center; /* 垂直居中 */
  height: 200px;
}
.child {
  /* 子元素自动居中 */
}

4. 使用grid

利用CSS Grid布局的align-items属性。

.parent {
  display: grid;
  align-items: center; /* 垂直居中 */
  height: 200px;
}
.child {
  /* 子元素自动居中 */
}

5. 使用positiontransform

结合绝对定位和变换属性。

.parent {
  position: relative;
  height: 200px;
}
.child {
  position: absolute;
  top: 50%;
  transform: translateY(-50%); /* 向上偏移自身高度的一半 */
}

6. 使用positionmargin

结合绝对定位和负边距。

.parent {
  position: relative;
  height: 200px;
}
.child {
  position: absolute;
  top: 50%;
  height: 50px; /* 子元素高度 */
  margin-top: -25px; /* 向上偏移自身高度的一半 */
}

7. 使用display: inline-blockvertical-align

结合inline-blockvertical-align

.parent {
  font-size: 0; /* 解决inline-block之间的空隙问题 */
}
.child {
  display: inline-block;
  vertical-align: middle;
}
.spacer {
  height: 100%;
  display: inline-block;
  vertical-align: middle;
}
<div class="parent">
  <span class="spacer"></span>
  <div class="child">Content</div>
</div>

8. 使用calc()position

结合calc()函数和绝对定位。

.parent {
  position: relative;
  height: 200px;
}
.child {
  position: absolute;
  top: calc(50% - 25px); /* 50%减去子元素高度的一半 */
}

选择哪种方法取决于你的具体需求,例如是否需要支持旧版浏览器,或者是元素的布局和内容类型。现代布局通常推荐使用Flexbox或Grid,因为它们提供了更强大和灵活的布局选项。

27. Vue 中,假设 data 中有一个数组对象,修改数组元素时,是否会触发视图更新?

在Vue中,当你修改数组中的元素时,Vue可以检测到某些类型的数组变动并触发视图更新,但也有一些情况Vue无法检测到数组的变化。

Vue可以检测到的数组变动:

  1. 使用push()pop()shift()unshift()splice()sort()reverse()等方法修改数组。这些方法会改变数组本身,并且Vue可以检测到这些变化。
    // 示例
    this.items.push({ message: '新消息' });
    this.items.splice(index, 1);
    
  2. 使用Vue.setthis.$set实例方法向数组中添加新项。这可以确保新添加的属性也是响应式的,并且能够触发视图更新。
    // 示例
    this.$set(this.items, index, { message: '新消息' });
    

Vue无法检测到的数组变动:

  1. 直接通过索引设置数组项,例如this.items[index] = newValue
    // 这种修改不会触发视图更新
    this.items[1] = { message: '新消息' };
    
  2. 修改数组的长度,例如this.items.length = newLength
    // 这种修改不会触发视图更新
    this.items.length = 0;
    

解决方法:

对于Vue无法检测到的数组变动,你可以采取以下几种方法来触发视图更新:

  • 使用Vue.setthis.$set
    this.$set(this.items, index, newValue);
    
  • 使用数组的方法替换整个数组
    this.items.splice(index, 1, newValue);
    
  • 使用新数组替换旧数组
    this.items = [...this.items.slice(0, index), newValue, ...this.items.slice(index + 1)];
    
  • 对于修改数组长度的情况,可以使用splice
    this.items.splice(newLength);
    

总之,为了保证Vue能够检测到数组的变化并更新视图,应避免直接通过索引修改数组元素或修改数组长度,而是使用Vue提供的方法或数组原生方法来确保响应性。

28. vuex中的辅助函数怎么使用?

Vuex 是 Vue 的状态管理库,它提供了一些辅助函数来简化状态的管理和访问。常用的辅助函数包括 mapStatemapGettersmapMutationsmapActions。下面将分别介绍这些辅助函数的使用方法。

1. mapState

mapState 辅助函数用于将 store 中的 state 映射到局部计算属性。

import { mapState } from 'vuex';
export default {
  computed: {
    // 使用对象展开运算符将映射后的状态混合到局部计算属性中
    ...mapState({
      // 映射 this.count 到 store.state.count
      count: state => state.count,
      // 也可以使用字符串模板
      countAlias: 'count',
      // 为了能够使用 `this` 获取局部状态,可以传递一个字符串方法名
      countPlusLocalState (state) {
        return state.count + this.localCount;
      }
    })
  }
};

2. mapGetters

mapGetters 辅助函数用于将 store 中的 getters 映射到局部计算属性。

import { mapGetters } from 'vuex';
export default {
  computed: {
    // 使用对象展开运算符将映射后的 getters 混合到局部计算属性中
    ...mapGetters([
      'doneTodosCount', // 映射 this.doneTodosCount 到 store.getters.doneTodosCount
      'anotherGetter'
    ])
  }
};

3. mapMutations

mapMutations 辅助函数用于将组件中的 methods 映射为 store.commit 调用。

import { mapMutations } from 'vuex';
export default {
  methods: {
    // 使用对象展开运算符将映射后的 mutations 混合到局部 methods 中
    ...mapMutations({
      increment: 'INCREMENT', // 映射 this.increment() 到 this.$store.commit('INCREMENT')
      incrementBy: 'INCREMENT_BY' // 映射 this.incrementBy(amount) 到 this.$store.commit('INCREMENT_BY', amount)
    })
  }
};

4. mapActions

mapActions 辅助函数用于将组件中的 methods 映射为 store.dispatch 调用。

import { mapActions } from 'vuex';
export default {
  methods: {
    // 使用对象展开运算符将映射后的 actions 混合到局部 methods 中
    ...mapActions({
      incrementAsync: 'incrementAsync', // 映射 this.incrementAsync() 到 this.$store.dispatch('incrementAsync')
      decrementAsync: 'decrementAsync' // 映射 this.decrementAsync() 到 this.$store.dispatch('decrementAsync')
    })
  }
};

使用注意事项:

  • 展开运算符:在上述示例中,我们使用了 ES6 的对象展开运算符 ... 来将映射后的函数混合到组件的 computedmethods 中。
  • 命名冲突:如果映射的函数名与组件中已有的函数名冲突,可以在映射时提供不同的本地名称。
  • 模块化存储:如果使用模块化的存储,需要指定命名空间。 例如,对于模块化的存储:
import { mapState } from 'vuex';
export default {
  computed: {
    ...mapState('moduleName', {
      count: state => state.count
    })
  }
};

在上述示例中,moduleName 是模块的命名空间。 通过使用这些辅助函数,可以更简洁地在组件中访问和操作 Vuex store 中的状态。

29. Vuex有几种属性,它们存在的意义分别是什么?

Vuex 是 Vue 的状态管理库,它有五种主要的属性:stategettersmutationsactionsmodules。每种属性都有其特定的意义和用途:

1. state

  • 意义:存储应用的状态数据。
  • 用途state 是 Vuex store 的核心,它包含了应用中需要共享的数据。组件可以通过 store.state 来访问这些数据。

2. getters

  • 意义:对 state 进行派生,类似于 Vue 组件中的计算属性。
  • 用途getters 用于获取 state 的派生状态,例如筛选、计数或合并数据。它们可以接受其他 getters 作为第二个参数,用于更复杂的派生。

3. mutations

  • 意义:更改 state 的唯一方法。
  • 用途mutations 是同步函数,用于直接修改 state。每个 mutation 都有一个字符串的名称和一个处理函数,处理函数接受 state 作为第一个参数,以及一个可选的载荷(payload)作为第二个参数。

4. actions

  • 意义:提交 mutations,可以包含异步操作。
  • 用途actions 用于处理异步操作,例如从服务器获取数据。它们不能直接修改 state,而是通过提交 mutations 来间接更改 stateactions 可以通过 context 对象访问 stategetters 和其他 actions

5. modules

  • 意义:将 store 分割成模块,每个模块拥有自己的 statemutationsactionsgetters
  • 用途modules 用于大型应用,可以帮助将 store 分割成更小的、可管理的部分。每个模块可以独立命名,以避免命名冲突,并且可以嵌套使用。

各属性存在的意义总结:

  • state:提供了一个集中存储所有组件状态的地方,使得状态易于管理和维护。
  • getters:允许组件以声明式的方式获取派生状态,增加了代码的可读性和可维护性。
  • mutations:确保了状态变更的可追踪性和可调试性,因为所有的状态变更都必须通过 mutations 来实现。
  • actions:使得异步操作变得更加容易管理,并且可以组合多个 mutations
  • modules:帮助组织大型应用的状态,使得状态管理更加模块化和可扩展。 通过这些属性,Vuex 提供了一种集中和可预测的状态管理方式,使得大型应用的状态管理变得更加简单和高效。

30. Vuex 是什么?

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式 + 库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。 Vuex 的核心概念:

  • State:驱动应用的数据源;
  • View:以声明方式将 state 映射到视图;
  • Actions:响应在 view 上的用户输入导致的状态变化。 Vuex 的特点:
  • 集中式存储:所有组件的状态都存储在一个集中的 store 中,易于管理和维护。
  • 可预测的状态变化:通过 mutations 和 actions 来管理状态的变化,确保状态变化的可追踪性和可调试性。
  • 模块化:可以将 store 分割成模块,每个模块拥有自己的 state、mutations、actions 和 getters,使得大型应用的状态管理更加模块化和可扩展。 Vuex 的适用场景:
  • 中大型单页应用
  • 多个视图依赖于同一状态
  • 来自不同视图的行为需要变更同一状态 Vuex 的优势:
  • 使得组件间的状态共享变得简单
  • 提高了应用的可维护性和可扩展性
  • 使得状态变化更加可预测和可追踪 Vuex 的劣势:
  • 引入了额外的复杂度
  • 需要学习新的概念和 API 总的来说,Vuex 是一个强大的状态管理库,适用于中大型 Vue.js 应用程序。它提供了一种集中和可预测的状态管理方式,使得状态管理变得更加简单和高效。然而,对于小型或简单的应用程序,使用 Vuex 可能会引入不必要的复杂度。