2025面试大全(21)

190 阅读55分钟

1. 说说 vue3 中的响应式设计原理

Vue 3 中的响应式设计原理主要基于 Proxy 对象,这是与 Vue 2 中基于 Object.defineProperty 的实现方式不同的地方。以下是 Vue 3 中响应式设计原理的详细解释:

1. Proxy 对象

Vue 3 使用了 JavaScript 的 Proxy 对象来拦截对对象属性的访问和修改。Proxy 对象可以自定义对象的基本操作,如属性读取、属性设置、属性删除、函数调用等。

2. 响应式追踪

当组件渲染函数被调用时,Vue 3 会遍历组件的模板,收集依赖(即哪些响应式属性被使用了)。这个过程中,Vue 会利用 Proxy 拦截属性的读取操作,把当前正在执行的副作用函数(如组件的渲染函数)收集到对应的依赖列表中。

3. 依赖触发

当响应式对象的属性被修改时,Proxy 会拦截这个设置操作,并通知 Vue 重新执行那些依赖于被修改属性的副作用函数。这就是所谓的“依赖触发”。

4. 嵌套对象的处理

Vue 3 的响应式系统可以递归地处理嵌套对象,确保深层次的对象属性也是响应式的。当访问或修改嵌套对象的属性时,Proxy 仍然可以拦截这些操作,并进行依赖收集或触发更新。

5. 性能优化

与 Vue 2 相比,Vue 3 的响应式系统在性能上有所优化:

  • 懒代理:Vue 3 中的响应式对象是按需创建的,只有当对象真正被使用时,才会为其创建 Proxy。
  • 缓存:Vue 3 会缓存已经创建的 Proxy,避免重复创建。
  • 批量更新:Vue 3 中的响应式系统可以批量处理依赖触发,减少渲染次数,提高性能。

6. Composition API

Vue 3 引入了 Composition API,使得响应式状态的管理更加灵活和可组合。通过 refreactive 函数,开发者可以更容易地创建和管理响应式状态。

示例

以下是一个简单的示例,展示了如何在 Vue 3 中创建响应式状态:

import { reactive } from 'vue';
const state = reactive({
  count: 0
});
function increment() {
  state.count++;
}
// 当 state.count 被修改时,依赖于 state.count 的组件会自动重新渲染

在这个示例中,reactive 函数将一个普通对象转换为一个响应式对象。当 increment 函数被调用时,state.count 的修改会触发依赖于 state.count 的组件的重新渲染。 总之,Vue 3 的响应式设计原理基于 Proxy 对象,提供了更高效、更灵活的响应式状态管理方式。

2. 实现斐波那契数列

在 JavaScript 中实现斐波那契数列有多种方法,包括递归、迭代和利用矩阵快速幂等。以下是几种常见的实现方式:

1. 递归实现

递归是实现斐波那契数列的最直接方法,但效率较低,因为存在大量的重复计算。

function fibonacciRecursive(n) {
  if (n <= 1) {
    return n;
  }
  return fibonacciRecursive(n - 1) + fibonacciRecursive(n - 2);
}

2. 迭代实现

迭代方法使用循环来计算斐波那契数列,效率比递归高。

function fibonacciIterative(n) {
  let a = 0, b = 1, sum;
  for (let i = 0; i < n; i++) {
    sum = a + b;
    a = b;
    b = sum;
  }
  return a;
}

3. 使用动态规划

动态规划是一种优化递归的方法,通过存储已经计算过的值来避免重复计算。

function fibonacciDynamic(n) {
  const fib = [0, 1];
  for (let i = 2; i <= n; i++) {
    fib[i] = fib[i - 1] + fib[i - 2];
  }
  return fib[n];
}

4. 使用矩阵快速幂

矩阵快速幂是一种非常高效的方法,可以将时间复杂度降低到 O(log n)。

function matrixMultiply(a, b) {
  return [
    a[0] * b[0] + a[1] * b[2], a[0] * b[1] + a[1] * b[3],
    a[2] * b[0] + a[3] * b[2], a[2] * b[1] + a[3] * b[3]
  ];
}
function matrixPower(matrix, n) {
  if (n === 1) {
    return matrix;
  }
  if (n % 2 === 0) {
    const halfPower = matrixPower(matrix, n / 2);
    return matrixMultiply(halfPower, halfPower);
  } else {
    return matrixMultiply(matrix, matrixPower(matrix, n - 1));
  }
}
function fibonacciMatrix(n) {
  if (n <= 1) {
    return n;
  }
  const baseMatrix = [1, 1, 1, 0];
  const resultMatrix = matrixPower(baseMatrix, n - 1);
  return resultMatrix[0];
}

5. 使用闭包和生成器

还可以使用闭包或生成器来创建一个斐波那契数列生成器。

function fibonacciGenerator() {
  let a = 0, b = 1;
  return function() {
    [a, b] = [b, a + b];
    return a;
  };
}
const fibGen = fibonacciGenerator();
for (let i = 0; i < 10; i++) {
  console.log(fibGen()); // 生成前10个斐波那契数
}

选择哪种实现方式取决于你的具体需求,例如对性能的要求、代码的可读性等。对于小规模的计算,迭代或动态规划方法通常足够高效。如果需要计算非常大的斐波那契数,可以考虑使用矩阵快速幂方法。

3. vue路由中,history和hash两种模式有什么区别?

在 Vue 路由(Vue Router)中,history 模式和 hash 模式是两种不同的路由管理方式,它们的主要区别在于 URL 的表现和路由的实现机制:

Hash 模式

  1. URL 表现
    • URL 中会包含一个 # 符号,例如:http://example.com/#/home
    • # 后面的内容表示路由路径。
  2. 工作原理
    • 利用浏览器中的 hashchange 事件来监听 URL 的变化。
    • 当 URL 的 hash 部分发生变化时,不会向服务器发送请求,而是通过 JavaScript 来控制页面内容的更新。
  3. 优点
    • 兼容性好,不需要服务器配置。
    • 在刷新页面时,不会向服务器发送请求,因为浏览器会忽略 # 后面的内容。
  4. 缺点
    • URL 中包含 #,可能不符合某些人的审美或规范。
    • 对于 SEO(搜索引擎优化)不太友好,因为搜索引擎可能不会索引 # 后面的内容。

History 模式

  1. URL 表现
    • URL 看起来像普通的页面跳转,例如:http://example.com/home
    • 不包含 # 符号。
  2. 工作原理
    • 利用 HTML5 的 history API(如 pushStatereplaceState)来管理路由历史。
    • 当用户点击链接或触发路由变化时,通过 JavaScript 来动态更新页面内容,同时改变浏览器的历史记录。
  3. 优点
    • URL 更加美观,符合常规的页面跳转习惯。
    • 对 SEO 更友好,因为搜索引擎可以正常索引页面。
  4. 缺点
    • 需要服务器配置,以正确处理页面刷新时的请求。如果服务器没有正确配置,刷新页面时可能会得到 404 错误。
    • 在不支持 HTML5 history API 的旧版浏览器中,需要降级处理或使用 polyfill。

选择哪种模式?

  • 如果你的应用不需要考虑 SEO,并且希望快速搭建而不需要服务器配置,可以选择 hash 模式。
  • 如果你的应用需要更好的 SEO 支持,并且可以配置服务器来处理页面刷新,那么 history 模式是更好的选择。 在 Vue Router 中,你可以通过设置 mode 属性来选择路由模式:
const router = new VueRouter({
  mode: 'history', // 或 'hash'
  routes: [...]
});

总的来说,两种模式各有优缺点,选择哪种模式取决于你的应用需求和环境。

4. 给一个dom同时绑定两个点击事件,一个用捕获,一个用冒泡,说下会执行几次事件,然后会先执行冒泡还是捕获?

在 JavaScript 中,事件传播可以分为三个阶段:捕获阶段、目标阶段和冒泡阶段。当你给一个 DOM 元素同时绑定两个点击事件,一个在捕获阶段触发,另一个在冒泡阶段触发时,事件的执行顺序和次数如下:

  1. 捕获阶段:事件从顶层窗口向下传播到目标元素,依次经过各个父元素。
  2. 目标阶段:事件到达目标元素。
  3. 冒泡阶段:事件从目标元素向上传播到顶层窗口,依次经过各个父元素。

绑定事件

假设我们有以下代码:

const element = document.getElementById('myElement');
element.addEventListener('click', function() {
  console.log('捕获阶段事件');
}, true); // 第三个参数为 true,表示在捕获阶段触发
element.addEventListener('click', function() {
  console.log('冒泡阶段事件');
}, false); // 第三个参数为 false 或省略,表示在冒泡阶段触发

执行次数

当点击 element 时,两个事件都会被触发,所以总共会执行 两次 事件。

执行顺序

根据事件传播的机制:

  1. 首先执行捕获阶段的事件。
  2. 然后执行冒泡阶段的事件。 所以,输出顺序将是:
捕获阶段事件
冒泡阶段事件

总结

  • 执行次数:两次
  • 执行顺序:先执行捕获阶段的事件,然后执行冒泡阶段的事件。 这个行为是由浏览器的事件传播机制决定的,并且在不同浏览器中都是一致的。

5. 说说你对“三次握手”、“四次挥手”的理解

“三次握手”和“四次挥手”是TCP(传输控制协议)中用于建立和终止网络连接的两个重要过程。它们确保了数据传输的可靠性和有序性。

三次握手(建立连接)

三次握手的目的是在两个TCP端点之间建立可靠的连接。这个过程分为三个步骤:

  1. SYN:客户端发送一个SYN(同步序列编号)报文到服务器,并进入SYN_SENT状态,等待服务器确认。
  2. SYN + ACK:服务器收到SYN报文后,会发送一个SYN+ACK(同步和确认应答)报文作为应答,并将连接状态设置为SYN_RECEIVED。这个报文中既包含SYN也包含ACK(确认字符),表示服务器已经收到了客户端的SYN报文。
  3. ACK:客户端收到服务器的SYN+ACK报文后,会向服务器发送一个ACK报文,确认连接的建立。此报文发送完毕后,客户端和服务器都进入ESTABLISHED状态,完成连接的建立。 三次握手的目的是为了确认双方的接收和发送能力都是正常的,并且同步初始序列号,以便可靠地传输数据。

四次挥手(终止连接)

四次挥手的目的是在两个TCP端点之间终止一个已经建立的连接。这个过程分为四个步骤:

  1. FIN:客户端发送一个FIN(结束)报文,表示客户端已经完成发送数据,并进入FIN_WAIT_1状态,等待服务器确认。
  2. ACK:服务器收到这个FIN报文后,会发送一个ACK报文作为应答,并将连接状态设置为CLOSE_WAIT。客户端收到这个ACK后,进入FIN_WAIT_2状态。
  3. FIN:服务器发送自己的FIN报文,表示服务器也已经完成发送数据,并进入LAST_ACK状态,等待客户端的确认。
  4. ACK:客户端收到服务器的FIN报文后,发送一个ACK报文作为应答,然后进入TIME_WAIT状态。经过一段时间(称为2MSL,即最大报文生存时间的两倍)后,确保服务器收到了最后的ACK报文,客户端关闭连接。服务器收到这个ACK报文后,立即关闭连接。 四次挥手的目的是确保双方都能够正常关闭连接,并且所有的数据都已经正确传输和接收。

总结

  • 三次握手用于建立连接,确保双方能够正常通信。
  • 四次挥手用于终止连接,确保双方都能够正常关闭连接,并且所有数据都已经传输完成。 这两个过程是TCP协议保证数据传输可靠性的核心机制之一。

6. promise.catch后面的.then还会执行吗?

是的promise.catch()后面的.then()还会执行。 catch()方法实际上是.then(null, rejectionCallback)的别名,它用于指定发生错误时的回调函数。无论前面的Promise是否被拒绝(即出现错误),catch()后面的.then()都会被执行,因为catch()已经处理了错误。 举个例子:

new Promise((resolve, reject) => {
  reject('error'); // 模拟一个错误
})
.catch(error => {
  console.log('caught', error); // 这里会捕获到错误
  return 'recovered'; // 返回一个新的值
})
.then(value => {
  console.log(value); // 这里会输出'recovered'
});

在这个例子中,虽然Promise被拒绝了,但是catch()方法捕获了错误并返回了一个新的值'recovered'。随后,.then()方法被调用并接收到了这个值,因此它也会执行。 需要注意的是,如果catch()方法中发生了错误并且没有进一步的处理(比如没有返回一个新的Promise或者没有再次使用catch()),那么这个错误将会被传递到后续的.then()方法中,并且可以被另一个catch()捕获。例如:

new Promise((resolve, reject) => {
  reject('error'); // 模拟一个错误
})
.catch(error => {
  console.log('caught', error); // 这里会捕获到错误
  throw new Error('another error'); // 抛出另一个错误
})
.then(value => {
  console.log('this will not be executed'); // 这里的代码不会执行
})
.catch(error => {
  console.log('caught again', error); // 这里会捕获到另一个错误
});

在这个例子中,第一个catch()方法抛出了一个新的错误,这个错误被后续的.then()方法忽略(因为它没有提供错误处理的回调),但是被最后一个catch()方法捕获。

7. 怎么实现样式隔离?

样式隔离是前端开发中的一个重要概念,尤其是在开发组件库、微前端或多租户应用时。样式隔离的目的是确保不同组件或应用的样式不会相互干扰,从而保持各自的独立性和可维护性。以下是一些实现样式隔离的常见方法:

1. CSS Modules

CSS Modules 是一种将 CSS 类名局部化的技术,通过编译工具(如 Webpack)将 CSS 类名转换为唯一的标识符,从而避免类名冲突。

/* styles.module.css */
.title {
  color: red;
}
import styles from './styles.module.css';
const title = <h1 className={styles.title}>Hello, World!</h1>;

2. BEM(Block Element Modifier)

BEM 是一种命名约定,通过特定的命名规则来避免样式冲突。

/* BEM Naming */
.block__element--modifier {
  /* styles */
}

3. CSS-in-JS

CSS-in-JS 是一种将 CSS 样式写入 JavaScript 的技术,可以在运行时动态生成样式,从而实现样式隔离。

import styled from 'styled-components';
const Title = styled.h1`
  color: red;
`;

4. Shadow DOM

Web Components 的 Shadow DOM 提供了一种将样式封装在组件内部的方法,从而实现样式隔离。

class MyComponent extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.shadowRoot.innerHTML = `
      <style>
        .title {
          color: red;
        }
      </style>
      <h1 class="title">Hello, World!</h1>
    `;
  }
}

5. Namespaces

为样式类名添加命名空间前缀,以减少冲突的可能性。

/* Namespace Prefix */
.ns-title {
  color: red;
}

6. Scoped CSS

在 Vue.js 等框架中,可以使用 scoped 属性来限制 CSS 只作用于当前组件。

<template>
  <h1 class="title">Hello, World!</h1>
</template>
<style scoped>
.title {
  color: red;
}
</style>

7. CSS Variables

使用 CSS 变量来封装和隔离样式。

:root {
  --title-color: red;
}
.title {
  color: var(--title-color);
}

8. Iframes

对于完全隔离的需求,可以使用 iframes 来实现样式和 DOM 的完全隔离。

<iframe src="component.html" frameborder="0"></iframe>

9. JavaScript 动态样式

通过 JavaScript 动态添加样式,可以为每个组件生成唯一的类名。

const uniqueClass = `title-${Math.random().toString(36).substring(7)}`;
document.head.appendChild(document.createElement('style')).textContent = `
  .${uniqueClass} {
    color: red;
  }
`;

10. 微前端架构

在微前端架构中,每个微前端可以有自己的样式隔离策略,例如使用不同的链接标签引入样式或者使用上述任何一种方法。 选择哪种方法取决于具体的项目需求、技术栈和团队偏好。在实际开发中,可能会结合多种方法来实现更有效的样式隔离。

8. 前端性能优化指标有哪些?怎么进行性能检测?

前端性能优化是提高网页加载速度和用户体验的关键环节。以下是一些常见的前端性能优化指标以及如何进行性能检测的方法:

前端性能优化指标

  1. 首次内容绘制(FCP):测量从页面开始加载到主要内容在屏幕上呈现的时间。
  2. 首次输入延迟(FID):测量用户首次与页面交互(如点击按钮)到浏览器响应的时间。
  3. 最大内容绘制(LCP):测量页面主要内容加载完成的时间。
  4. 累积布局偏移(CLS):测量页面布局在加载过程中的稳定性,即元素位置偏移的程度。
  5. 可交互时间(TTI):测量页面完全可交互的时间。
  6. 速度指数(Speed Index):衡量页面内容可见的速度。
  7. 页面完全加载时间:从请求开始到页面完全加载完成的时间。
  8. 请求次数和大小:页面加载过程中发出的网络请求次数和总数据量。
  9. 渲染阻塞资源:分析CSS、JavaScript等资源是否阻塞页面渲染。

性能检测方法

  1. 浏览器开发者工具
    • Network Panel:查看网络请求的时间线、大小和状态。
    • Performance Panel:录制页面加载和运行时的性能表现,分析帧率、CPU和内存使用情况。
    • Lighthouse:集成在Chrome浏览器中的性能审计工具,提供详细的性能指标和建议。
  2. Web Vitals
    • Google推出的核心网页性能指标,可以通过浏览器扩展或API进行检测。
  3. Performance API
    • 使用JavaScript的Performance API(如Performance.mark和Performance.measure)来手动检测性能指标。
  4. Real User Monitoring (RUM)
    • 通过收集真实用户的数据来分析页面性能。
  5. Synthetic Monitoring
    • 使用工具模拟用户行为,定期检测页面性能。
  6. WebPageTest
    • 一个在线工具,提供详细的性能测试报告,包括加载时间、速度指数等。
  7. GTmetrix
    • 另一个在线性能测试工具,结合了Google PageSpeed Insights和WebPageTest的结果。
  8. Pingdom Tools
    • 提供网站速度测试,分析加载时间和性能瓶颈。
  9. Chrome User Experience Report (CrUX)
    • Google提供的大规模真实用户性能数据集。
  10. Custom Scripts
    • 编写自定义脚本来检测特定的性能指标,例如使用Navigation Timing API或Resource Timing API。

性能优化实践

  • 压缩资源:压缩CSS、JavaScript和图片文件。
  • 异步加载:使用async或defer属性异步加载JavaScript文件。
  • 懒加载:对图片和视频等非关键资源进行懒加载。
  • 缓存策略:利用浏览器缓存和HTTP缓存头减少重复请求。
  • 减少HTTP请求:合并文件,减少请求次数。
  • 使用CDN:分发内容,减少服务器响应时间。
  • 优化CSS和JavaScript:避免复杂的CSS选择器,减少JavaScript执行时间。
  • 服务器端渲染(SSR):提高首次内容绘制的速度。
  • 预加载和预渲染:提前加载关键资源或渲染页面。 进行性能检测后,根据报告中的建议进行相应的优化,然后重新测试以验证优化效果。性能优化是一个持续的过程,需要不断地监控和调整。

9. computed怎么实现的缓存

在Vue中,computed属性是一种依赖跟踪的响应式属性,其缓存机制是基于其依赖的响应式属性是否发生变化来实现的。当依赖的响应式属性没有变化时,computed属性会返回之前的计算结果,从而实现缓存。以下是computed实现缓存的基本原理:

1. 依赖收集

  • 当定义一个computed属性时,Vue会为这个属性创建一个watcher对象。
  • watcher对象会订阅这个computed属性所依赖的响应式属性(即data中的属性或其他computed属性)。
  • 当这些依赖的响应式属性被访问时,它们会将当前的watcher对象添加到自己的依赖列表中。

2. 缓存机制

  • computed属性的watcher对象会有一个dirty标志,用来表示计算属性是否需要重新计算。
  • 初始时,dirty标志为true,表示需要计算。
  • computed属性被访问时,如果dirtytrue,则会执行定义的计算函数,计算结果会被缓存,并且将dirty标志设置为false
  • 如果dirtyfalse,则直接返回缓存的结果,不会重新执行计算函数。

3. 依赖变化与更新

  • computed属性的依赖发生变化时,Vue的响应式系统会通知相关的watcher对象。
  • watcher对象接收到通知后,会将自己的dirty标志设置为true,表示下次访问时需要重新计算。
  • 这样,只有当依赖真正发生变化时,computed属性才会重新计算,否则一直使用缓存的结果。

4. 示例代码

以下是一个简化的示例,展示了computed属性是如何实现缓存的:

function defineReactive(obj, key, val) {
  // 简化的响应式系统
  let dep = new Dep();
  Object.defineProperty(obj, key, {
    get() {
      if (Dep.target) {
        dep.depend();
      }
      return val;
    },
    set(newVal) {
      val = newVal;
      dep.notify();
    }
  });
}
function Dep() {
  this.subs = [];
}
Dep.prototype.depend = function() {
  if (Dep.target) {
    this.subs.push(Dep.target);
  }
};
Dep.prototype.notify = function() {
  this.subs.forEach(sub => sub.update());
};
function Watcher(vm, expOrFn, cb) {
  this.vm = vm;
  this.getter = typeof expOrFn === 'function' ? expOrFn : () => vm[expOrFn];
  this.value = this.get();
}
Watcher.prototype.get = function() {
  Dep.target = this;
  let value = this.getter.call(this.vm);
  Dep.target = null;
  return value;
};
Watcher.prototype.update = function() {
  this.value = this.get();
};
function computed(vm, key, fn) {
  let watcher = new Watcher(vm, fn, () => {});
  Object.defineProperty(vm, key, {
    get() {
      if (watcher.dirty) {
        watcher.evaluate();
      }
      return watcher.value;
    }
  });
}
Watcher.prototype.evaluate = function() {
  this.value = this.get();
  this.dirty = false;
};
// 使用示例
let vm = {};
defineReactive(vm, 'a', 1);
computed(vm, 'b', function() {
  return this.a * 2;
});
console.log(vm.b); // 2
vm.a = 2;
console.log(vm.b); // 4

在这个示例中,computed属性b依赖于响应式属性a。当a的值发生变化时,b会重新计算。如果a没有变化,b会返回缓存的结果。 这就是Vue中computed属性实现缓存的基本原理。通过依赖收集和dirty标志,Vue能够高效地管理computed属性的更新,只有在必要时才进行重新计算,从而提高性能。

10. 如何确保你的构造函数只能被new调用,而不能被普通调用?

为了确保构造函数只能被 new 调用,而不能被普通调用,你可以在构造函数内部检查 this 是否是构造函数的实例。如果不是,那么就说明构造函数没有被正确地使用 new 来调用。在这种情况下,你可以抛出一个错误来阻止构造函数的执行。以下是一个示例:

function MyConstructor() {
  // 检查this是否是MyConstructor的实例
  if (!(this instanceof MyConstructor)) {
    // 如果不是,抛出错误
    throw new Error("MyConstructor must be called with the new operator.");
  }
  // 正常的构造函数代码
  this.property = "some value";
}
// 正确的使用方式
var instance = new MyConstructor();
console.log(instance.property); // "some value"
// 错误的使用方式,将会抛出错误
try {
  var wrongInstance = MyConstructor(); // 没有使用new
} catch (e) {
  console.error(e.message); // "MyConstructor must be called with the new operator."
}

在这个示例中,MyConstructor 函数内部通过检查 this instanceof MyConstructor 来确定是否使用了 new。如果没有使用 newthis 将不会是 MyConstructor 的实例,而是指向全局对象(在非严格模式下)或 undefined(在严格模式下),这时会抛出一个错误。 另外,ES6 类语法自动保证了构造函数只能被 new 调用,如果你使用类来定义构造函数,就不需要手动进行这样的检查:

class MyClass {
  constructor() {
    this.property = "some value";
  }
}
// 正确的使用方式
var instance = new MyClass();
console.log(instance.property); // "some value"
// 错误的使用方式,将会抛出错误
try {
  var wrongInstance = MyClass(); // 没有使用new
} catch (e) {
  console.error(e.message); // "Class constructor MyClass cannot be invoked without 'new'"
}

在ES6类中,如果你尝试不使用 new 来调用类构造函数,JavaScript会自动抛出一个错误,因此你不需要手动添加检查。

11. 如何获取到一个实例对象的原型对象?

在JavaScript中,获取一个实例对象的原型对象有多种方法,以下是几种常见的方式:

1. 使用 Object.getPrototypeOf() 方法

Object.getPrototypeOf() 方法可以直接获取一个对象的原型对象。

function MyConstructor() {}
var instance = new MyConstructor();
var prototype = Object.getPrototypeOf(instance);
console.log(prototype === MyConstructor.prototype); // true

2. 使用对象的 __proto__ 属性

大多数现代浏览器提供了一个非标准的 __proto__ 属性,可以直接访问对象的原型。

function MyConstructor() {}
var instance = new MyConstructor();
var prototype = instance.__proto__;
console.log(prototype === MyConstructor.prototype); // true

需要注意的是,__proto__ 属性是非标准的,不推荐在生产环境中使用。

3. 使用构造函数的 prototype 属性

每个函数都有一个 prototype 属性,它指向该函数创建的实例的原型对象。

function MyConstructor() {}
var instance = new MyConstructor();
var prototype = MyConstructor.prototype;
console.log(Object.getPrototypeOf(instance) === prototype); // true

4. 使用 Object.setPrototypeOf() 方法的逆操作

虽然不常用,但你可以通过 Object.setPrototypeOf() 方法的逆操作来获取原型对象。

function MyConstructor() {}
var instance = new MyConstructor();
var prototype = Object.setPrototypeOf(instance, null); // 这将移除原型链
console.log(prototype === MyConstructor.prototype); // true,但在实际操作中,这会破坏原型链

注意事项

  • Object.getPrototypeOf() 是ECMAScript标准的一部分,是获取原型对象的推荐方法。
  • __proto__ 属性虽然方便,但因为它不是标准的一部分,所以可能会在未来的JavaScript版本中移除或更改。
  • 构造函数的 prototype 属性指向的是该构造函数创建的实例的原型对象,而不是实例本身的原型对象。但是,通过 Object.getPrototypeOf(instance) 获取的原型对象与构造函数的 prototype 属性指向的是同一个对象。 在实际开发中,推荐使用 Object.getPrototypeOf() 方法来获取实例对象的原型对象。

12. koa和express有哪些不同?

Koa和Express都是Node.js的Web框架,但它们在设计理念、架构和功能上有所不同。以下是Koa和Express之间的一些主要区别:

1. 设计理念

  • Express:更倾向于提供一个完整的、功能丰富的框架,包括路由、中间件、模板引擎等,使得开发者可以快速开始构建Web应用。
  • Koa:更注重简洁和模块化,提供了一个更轻量级的核心,鼓励开发者只引入他们需要的中间件和功能。

2. 中间件机制

  • Express:使用传统的回调函数作为中间件,中间件的执行顺序是线性的,通过next()函数来控制流程。
  • Koa:使用ES6的生成器(在Koa 2.x中是异步函数)作为中间件,提供了更强大的异步流控制能力,通过yield(在Koa 2.x中是await)来暂停和恢复执行。

3. 错误处理

  • Express:错误处理通常通过在中间件中添加一个错误处理函数作为第一个参数来实现。
  • Koa:提供了更先进的错误处理机制,可以通过try...catch捕获中间件中的错误,或者使用ctx.onerror事件处理错误。

4. 上下文对象

  • Express:请求对象(req)和响应对象(res)是分开的,开发者需要分别处理它们。
  • Koa:提供了一个统一的上下文对象(ctx),它封装了请求和响应,使得开发者可以更方便地操作它们。

5. 路由

  • Express:内置了路由功能,可以直接使用app.get(), app.post()等方法来定义路由。
  • Koa:核心库不包含路由功能,需要通过引入第三方中间件(如koa-router)来实现路由。

6. 性能

  • Koa:由于使用了更现代的JavaScript特性(如异步函数),Koa在某些情况下可能具有更好的性能。
  • Express:虽然性能也很不错,但相对于Koa,可能在处理大量异步操作时稍显不足。

7. 社区和生态系统

  • Express:拥有更庞大的社区和更成熟的生态系统,有大量的插件和中间件可供选择。
  • Koa:虽然社区相对较小,但也在不断增长,并且有很多高质量的中间件。

8. 学习曲线

  • Express:由于功能丰富且文档完善,学习曲线相对平缓。
  • Koa:由于采用了更现代的JavaScript特性,对于不熟悉这些特性的开发者来说,学习曲线可能稍陡。

总结

选择Koa还是Express取决于项目的具体需求和开发者的偏好。如果需要一个轻量级、模块化的框架,并且希望利用现代JavaScript特性,Koa可能是一个更好的选择。如果需要一个功能丰富、易于上手的框架,并且希望利用庞大的社区和生态系统,Express可能更适合。

13. react中,父子组件的生命周期执行顺序是怎么样的?

在React中,父子组件的生命周期执行顺序是一个常见的话题,尤其是在React 16.3之前的版本中。随着React 16.3引入了新的生命周期方法,以及React 16.4和更高版本的进一步更新,生命周期的执行顺序也有所变化。以下是基于React 16.3及以后版本的父子组件生命周期执行顺序的概述:

挂载阶段(Mounting)

  1. 父组件
    • constructor
    • static getDerivedStateFromProps
    • render
  2. 子组件
    • constructor
    • static getDerivedStateFromProps
    • render
  3. 子组件
    • componentDidMount
  4. 父组件
    • componentDidMount

更新阶段(Updating)

  1. 父组件
    • static getDerivedStateFromProps
    • shouldComponentUpdate
    • render
  2. 子组件
    • static getDerivedStateFromProps
    • shouldComponentUpdate
    • render
  3. 子组件
    • getSnapshotBeforeUpdate
    • componentDidUpdate
  4. 父组件
    • getSnapshotBeforeUpdate
    • componentDidUpdate

卸载阶段(Unmounting)

  1. 子组件
    • componentWillUnmount
  2. 父组件
    • componentWillUnmount

注意事项

  • constructorcomponentDidMountcomponentDidUpdatecomponentWillUnmount这些方法在父子组件中的执行顺序是先子后父。
  • static getDerivedStateFromPropsshouldComponentUpdaterender这些方法在父子组件中的执行顺序是先父后子。
  • getSnapshotBeforeUpdate是在render之后,componentDidUpdate之前调用的,用于获取更新前的快照。

React 16.3+ 新生命周期方法

  • static getDerivedStateFromProps:用于代替componentWillReceiveProps,用于根据新的props计算state。
  • getSnapshotBeforeUpdate:在更新前获取快照,用于componentDidUpdate中比较。

React 17+ 生命周期变化

React 17及以后版本中,UNSAFE_componentWillMountUNSAFE_componentWillReceivePropsUNSAFE_componentWillUpdate这些生命周期方法被标记为不安全,并建议使用新的生命周期方法替代。

总结

理解父子组件的生命周期执行顺序对于优化组件性能和避免潜在的问题非常重要。随着React版本的更新,生命周期方法可能会有所变化,因此建议随时关注官方文档以获取最新信息。

14. 为什么推荐将静态资源放到cdn上?

将静态资源放到CDN(内容分发网络)上有多个好处,以下是主要原因:

  1. 加快加载速度
    • CDN分布在全球多个地理位置,用户可以从距离最近的CDN节点获取资源,减少了数据传输的距离,从而加快了加载速度。
  2. 减轻服务器压力
    • 通过CDN分发静态资源,可以减少主服务器的请求量,从而减轻服务器的负载,提高服务器的响应速度和处理能力。
  3. 提高可靠性
    • CDN具有高可用性和冗余性,即使某个节点出现故障,用户仍然可以从其他节点获取资源,提高了网站的可靠性。
  4. 缓存优化
    • CDN通常会缓存静态资源,用户在访问时可以直接从CDN缓存中获取,减少了源站的请求次数,进一步提高了加载速度。
  5. 分布式部署
    • CDN的分布式特性使得它能够更好地应对大规模的并发请求,特别是在高流量事件(如直播、大型活动)中,能够有效避免单点故障。
  6. 降低带宽成本
    • 由于CDN分担了部分流量,主服务器的带宽使用量会减少,从而可以降低带宽成本。
  7. 改善用户体验
    • 更快的加载速度和更高的可靠性直接提升了用户的访问体验,减少了页面加载时间和失败率。
  8. SEO优化
    • 快速的页面加载速度是搜索引擎排名的一个重要因素,使用CDN可以间接提高网站的SEO排名。
  9. 安全防护
    • 许多CDN提供商提供了额外的安全功能,如DDoS防护、SSL加密等,可以增强网站的安全性。
  10. 易于扩展
    • CDN架构易于扩展,可以随着用户量的增长而轻松扩展,满足不断增长的需求。 总之,将静态资源放到CDN上可以显著提高网站的性能、可靠性和安全性,同时降低成本,提升用户体验。因此,对于需要高性能、高可用性的网站,推荐使用CDN来分发静态资源。

15. position:absolute绝对定位,是相对于谁的定位?

position: absolute 绝对定位是相对于最近的已定位祖先元素(即其position属性值为relativeabsolutefixedsticky的祖先元素)的定位。如果不存在已定位的祖先元素,那么它是相对于初始包含块(通常是浏览器窗口)进行定位的。 具体来说:

  1. 已定位的祖先元素
    • 如果一个元素设置了position: absolute,并且其祖先元素中有一个设置了position: relativeabsolutefixedsticky,那么这个绝对定位元素是相对于这个已定位的祖先元素进行定位的。
  2. 初始包含块
    • 如果没有已定位的祖先元素,绝对定位元素是相对于初始包含块进行定位的。在大多数情况下,初始包含块是浏览器窗口,即视口(viewport)。
  3. 特殊情况
    • 对于非根元素,当其祖先元素都没有设置定位时,绝对定位是相对于根元素(<html>)的定位。
    • 对于根元素自身,绝对定位是相对于视口进行定位的。 示例:
<div style="position: relative;">
  <div style="position: absolute; top: 10px; left: 10px;">
    这个元素是相对于其父元素定位的。
  </div>
</div>

在这个示例中,内部<div>是相对于其父元素(设置了position: relative<div>)进行绝对定位的。 需要注意的是,绝对定位会使得元素脱离正常文档流,不占据空间,其他元素会忽略它的位置进行布局。

16. 说说React事件和原生事件的执行顺序

在React中,事件处理与原生事件处理在执行顺序上存在一些差异,主要是因为React实现了一个 SyntheticEvent(合成事件)系统,这个系统对原生事件进行了封装,以提供更一致的跨浏览器行为。

执行顺序概述:

  1. 原生事件:首先触发的是原生事件,这些事件是由浏览器直接触发的,比如clickmousedown等。
  2. React合成事件:紧接着,React会捕获并处理这些原生事件,然后触发相应的合成事件。React的合成事件是在原生事件之后触发的。
  3. 事件传播:在React中,事件传播分为两个阶段:捕获阶段和冒泡阶段。默认情况下,React事件处理器是在冒泡阶段被调用的,但你可以通过e.stopPropagation()来阻止事件的进一步传播。

详细说明:

  • 原生事件:当你点击一个按钮时,浏览器会首先触发原生click事件。
  • React合成事件:React会在原生事件触发后,捕获这个事件,并创建一个合成事件。然后,React会根据事件类型和绑定的事件处理器,在合适的时机(通常是冒泡阶段)触发这个合成事件。
  • 执行顺序:如果你在一个React组件上同时绑定了原生事件处理器和React事件处理器,那么首先会执行原生事件处理器,然后执行React事件处理器。

示例:

class MyComponent extends React.Component {
  handleNativeClick = (e) => {
    console.log('Native click');
  };
  handleReactClick = (e) => {
    console.log('React click');
  };
  render() {
    return (
      <button onClick={this.handleReactClick} onMouseDown={this.handleNativeClick}>
        Click me
      </button>
    );
  }
}

在这个示例中,当你点击按钮时,输出顺序将是:

Native click
React click

这是因为onMouseDown是一个原生事件,而onClick是一个React合成事件。

注意事项:

  • 阻止默认行为:在使用React合成事件时,如果你想要阻止默认行为,应该使用e.preventDefault(),而不是在原生事件处理器中阻止。
  • 事件传播:React中的事件传播与原生事件传播类似,但React的事件系统是独立于浏览器的,因此有一些特定的行为和限制。
  • 性能考虑:React的合成事件系统是为了提高性能而设计的,它减少了对原生事件的直接依赖,从而减少了内存使用和提高了解耦性。 总的来说,React事件和原生事件的执行顺序是:先执行原生事件,然后执行React合成事件。在实际开发中,通常建议使用React的合成事件系统,因为它提供了更好的跨浏览器兼容性和一致性。

17. VNode 有哪些属性?

在Vue中,VNode(虚拟节点)是一个轻量级的JavaScript对象,它代表了DOM节点。VNode是Vue的虚拟DOM实现的基础,它使得Vue能够高效地更新视图。VNode对象包含了一系列属性,以下是一些常见的VNode属性:

  1. tag:字符串,表示VNode的标签名,例如"div""span"等。
  2. data:对象,包含了与VNode相关的数据,例如属性、事件监听器、样式等。
  3. children:数组,包含了VNode的子节点。
  4. text:字符串,表示VNode的文本内容。
  5. elm:DOM元素,表示VNode对应的真实DOM节点。
  6. ns:字符串,表示VNode的命名空间,用于处理SVG等特殊元素。
  7. context:组件实例,表示VNode所属的组件上下文。
  8. key:字符串或数字,用于列表渲染时的节点复用和排序。
  9. componentOptions:对象,包含了组件的选项,如组件定义、props等。
  10. .componentInstance:组件实例,表示VNode对应的组件实例。
  11. parent:VNode,表示VNode的父节点。
  12. raw:布尔值,表示VNode是否是原生HTML内容。
  13. isStatic:布尔值,表示VNode是否是静态节点。
  14. isRootInsert:布尔值,表示VNode是否作为根节点插入。
  15. isComment:布尔值,表示VNode是否是注释节点。
  16. isCloned:布尔值,表示VNode是否是克隆的。
  17. isOnce:布尔值,表示VNode是否只渲染一次。
  18. asyncFactory:函数,表示VNode的异步工厂函数。
  19. asyncMeta:对象,包含了与异步组件相关的元数据。
  20. isAsyncPlaceholder:布尔值,表示VNode是否是异步组件的占位符。 这些属性在不同的场景和版本中可能会有所不同,但它们涵盖了VNode的主要功能。VNode的这些属性使得Vue能够高效地创建、更新和复用DOM节点,从而实现响应式和组件化的视图更新。

18. Vue2.0为什么不能检查数组的变化,该怎么解决?

Vue 2.0 不能检查数组变化的原因: 在Vue 2.0中,响应式系统是基于Object.defineProperty()实现的。这个方法可以拦截对象属性的读取和设置操作,从而实现数据的响应式。然而,对于数组来说,Vue 2.0并没有对数组的索引进行拦截,也没有对数组的长度属性进行拦截。因此,直接通过索引修改数组元素或通过修改数组长度来改变数组时,Vue无法检测到这些变化。 解决方法: 为了解决数组变化无法检测的问题,Vue 2.0提供了一些方法来触发视图更新:

  1. 使用Vue.set方法: Vue.set方法可以向数组中添加元素,并确保新添加的元素是响应式的。同时,它也会触发视图更新。
    Vue.set(array, index, newValue);
    
  2. 使用数组的变异方法: Vue 2.0重写了数组的某些方法,如push、pop、shift、unshift、splice、sort和reverse等。这些方法被称为“变异方法”,因为它们会改变数组本身,并且可以触发视图更新。
    array.push(newValue);
    
  3. 替换数组: 如果需要基于原数组创建一个新数组,可以使用filter、concat或slice等方法。然后,将新数组赋值给原数组,这样可以触发视图更新。
    array = array.filter(function (item) {
      return item !== valueToBeRemoved;
    });
    
  4. **使用forceUpdate方法:在某些特殊情况下,如果以上方法都无法解决问题,可以使用forceUpdate方法**: 在某些特殊情况下,如果以上方法都无法解决问题,可以使用forceUpdate方法强制更新组件。但这种方法应尽量避免使用,因为它会跳过正常的响应式更新过程。
    this.$forceUpdate();
    

注意事项

  • 尽量避免直接通过索引修改数组元素,如array[0] = newValue,因为这种方式无法触发视图更新。
  • 在使用对象数组时,确保对象是响应式的,即使用Vue.set或扩展运算符等方式添加属性。 通过以上方法,可以有效地解决Vue 2.0中数组变化无法检测的问题。

19. 说说Vue 页面渲染流程

Vue页面的渲染流程是一个复杂但高效的过程,它涉及到了Vue的响应式系统、虚拟DOM以及组件的生命周期等多个方面。以下是Vue页面渲染的基本流程:

  1. 初始化阶段
    • 创建Vue实例:通过new Vue()创建一个Vue实例。
    • 初始化生命周期:初始化生命周期相关的属性和事件。
    • 初始化事件:初始化事件监听器。
    • 初始化渲染:确定模板编译方式(运行时编译或编译器编译)。
    • 调用beforeCreate钩子:此时数据观测和事件/侦听器的配置尚未完成。
    • 初始化注入和响应式:将数据转换为响应式对象,以便数据变化时能够触发视图更新。
    • 调用created钩子:此时已完成数据观测、属性和方法的运算,但尚未开始渲染。
  2. 模板编译阶段(如果使用编译器版本):
    • 解析模板:将模板字符串解析成抽象语法树(AST)。
    • 优化AST:标记静态节点,以便在后续的渲染过程中跳过这些节点。
    • 生成渲染函数:将AST转换成渲染函数,渲染函数会返回虚拟DOM。
  3. 挂载阶段
    • 调用beforeMount钩子:此时已经创建了虚拟DOM,但尚未将其渲染到页面上。
    • 创建虚拟DOM:根据渲染函数生成虚拟DOM。
    • 将虚拟DOM渲染成真实DOM:通过mount方法将虚拟DOM渲染成真实DOM,并插入到指定的元素中。
    • 调用mounted钩子:此时已经将组件渲染到了页面上。
  4. 更新阶段(响应数据变化):
    • 数据变化:当组件的响应式数据发生变化时,触发响应式系统的更新。
    • 调用beforeUpdate钩子:此时数据已更新,但尚未重新渲染。
    • 重新生成虚拟DOM:根据新的数据重新生成虚拟DOM。
    • 对比新旧虚拟DOM:通过Diff算法对比新旧虚拟DOM,计算出需要变更的最小差异。
    • 更新真实DOM:根据Diff结果更新真实DOM。
    • 调用updated钩子:此时已经完成了数据的更新和DOM的重新渲染。
  5. 卸载阶段
    • 调用beforeDestroy钩子:此时实例仍然完全可用。
    • 销毁实例:卸载组件,解绑事件监听器,取消订阅等。
    • 调用destroyed钩子:此时实例已经被完全销毁。 在整个渲染流程中,Vue利用了虚拟DOM和Diff算法来优化渲染性能,减少不必要的真实DOM操作。同时,通过生命周期钩子函数,开发者可以在特定的阶段执行自定义逻辑。

20. react 和 vue 有什么区别?

React和Vue都是现代前端开发中流行的JavaScript框架,它们都用于构建用户界面,但它们在设计理念、语法、性能和生态系统等方面存在一些区别。以下是React和Vue之间的一些主要区别:

设计理念

  • React
    • 由Facebook开发,遵循“学习一次,到处使用”的理念。
    • 更倾向于作为一个库,而不是一个完整的框架,它主要关注于视图层。
    • 强调组件的不可变性,使用虚拟DOM来优化性能。
  • Vue
    • 由尤雨溪开发,是一个渐进式框架,可以逐步集成到项目中。
    • 提供了更完整的解决方案,包括路由、状态管理等。
    • 强调简洁和易于上手,同时提供了响应式系统和模板语法。

语法和模板

  • React
    • 使用JSX(JavaScript XML)来编写组件,允许在JavaScript中编写HTML结构。
    • 组件通常是一个纯函数,接收props并返回JSX。
  • Vue
    • 使用基于HTML的模板语法,允许在模板中直接使用指令(如v-if、v-for等)。
    • 组件可以定义模板、脚本和样式,更加结构化。

响应式系统

  • React
    • 使用状态管理和不可变数据来处理响应式更新。
    • 状态变化时,通过比较新旧虚拟DOM来更新视图。
  • Vue
    • 使用基于Proxy的响应式系统,自动追踪依赖并在数据变化时更新视图。
    • 提供了更直观的响应式数据绑定。

组件通信

  • React
    • 主要通过props和context进行父子组件通信。
    • 状态管理通常使用外部库,如Redux或MobX。
  • Vue
    • 提供了props、events、slots等多种组件通信方式。
    • 状态管理可以使用Vuex,它是Vue的官方状态管理库。

性能

  • React
    • 使用虚拟DOM和Diff算法来优化重渲染性能。
    • Fiber架构提供了更流畅的更新和更好的调度。
  • Vue
    • 也使用虚拟DOM,但Vue的更新策略更倾向于细粒度的依赖追踪。
    • 通常在小型到中型应用中表现更优,但在大型应用中两者性能差异不大。

生态系统

  • React
    • 拥有庞大的生态系统和社区支持。
    • 有大量的第三方库和工具,如React Router、Create React App等。
  • Vue
    • 生态系统同样丰富,包括Vue Router、Vuex、Vue CLI等官方工具。
    • 社区活跃,但相对于React来说规模较小。

学习曲线

  • React
    • 学习曲线相对较陡,需要理解JSX、组件生命周期、状态管理等概念。
    • 需要结合其他库来构建完整的应用。
  • Vue
    • 学习曲线较平缓,易于上手,特别是对于熟悉HTML和CSS的开发者。
    • 提供了更丰富的官方文档和指南。

总结

React和Vue都是强大的前端框架,选择哪一个取决于项目需求、团队熟悉度和个人偏好。React更适合大型应用和需要高度定制化的项目,而Vue则因其简洁和易用性而受到许多开发者的喜爱。两者都在不断发展和改进,为前端开发提供了丰富的可能性。

21. RESTful 接口规范是什么?

RESTful接口规范是一种设计风格,用于创建网络API,特别是Web服务。REST(Representational State Transfer)是一种架构风格,而RESTful则是遵循REST原则的Web服务。以下是RESTful接口规范的一些关键点:

1. 资源(Resources)

  • 定义:资源是RESTful架构中的核心概念,可以是任何事物,如用户、订单、产品等。
  • URI:每个资源都应该有一个唯一的URI(统一资源标识符)来标识,例如 /users/orders

2. HTTP方法(Verbs)

  • GET:获取资源信息。
  • POST:创建新资源。
  • PUT:更新现有资源。
  • DELETE:删除资源。
  • PATCH:部分更新资源。
  • HEAD:获取资源头部信息。
  • OPTIONS:获取资源支持的HTTP方法。

3. 状态码(Status Codes)

  • 2xx:成功(如200 OK、201 Created)。
  • 3xx:重定向(如301 Moved Permanently、302 Found)。
  • 4xx:客户端错误(如400 Bad Request、404 Not Found)。
  • 5xx:服务器错误(如500 Internal Server Error、503 Service Unavailable)。

4. 无状态(Stateless)

  • 每个请求都必须包含所有必要的信息,以便服务器理解请求,而不依赖于之前的请求状态。

5. 表现层(Representation)

  • 资源可以有多种表现形式,如JSON、XML、HTML等。
  • 客户端可以通过Accept头部请求特定格式的响应。

6. 分页、过滤和排序

  • 分页:对于大量数据,应支持分页,如使用查询参数?page=2&limit=10
  • 过滤:允许通过查询参数过滤资源,如?status=active
  • 排序:允许通过查询参数排序资源,如?sort_by=name

7. 安全性

  • HTTPS:推荐使用HTTPS来保证数据传输的安全性。
  • 认证:使用OAuth、JWT等认证机制保护资源。

8. 版本控制

  • URI版本化:如 /v1/users/v2/users
  • 媒体类型版本化:如 application/vnd.myapi.v1+json

9. HATEOAS(Hypermedia as the Engine of Application State)

  • 响应中应包含链接,指导客户端如何进行下一步操作,如 { "next": "/orders?page=2" }

10. 一致性

  • 接口设计应保持一致性,便于理解和维护。

示例

GET /users - 获取用户列表
POST /users - 创建新用户
GET /users/123 - 获取用户ID为123的用户信息
PUT /users/123 - 更新用户ID为123的用户信息
DELETE /users/123 - 删除用户ID为123的用户

遵循RESTful接口规范可以使得API更加易于理解、使用和维护,同时也有利于不同系统之间的集成。

22. ES5怎么实现继承

在ES5(ECMAScript 5th edition)中,实现继承通常是通过构造函数和原型链来完成的。以下是一种常见的继承实现方式:

1. 原型链继承

function Parent() {
  this.parentProperty = 'I am a parent property';
}
Parent.prototype.getParentProperty = function() {
  return this.parentProperty;
};
function Child() {
  this.childProperty = 'I am a child property';
}
// 继承Parent
Child.prototype = new Parent();
Child.prototype.constructor = Child; // 重写constructor指向
Child.prototype.getChildProperty = function() {
  return this.childProperty;
};
// 使用
var child = new Child();
console.log(child.getParentProperty()); // 输出: I am a parent property
console.log(child.getChildProperty()); // 输出: I am a child property

2. 借用构造函数继承

function Parent(name) {
  this.name = name;
}
Parent.prototype.sayName = function() {
  console.log(this.name);
};
function Child(name, age) {
  Parent.call(this, name); // 借用Parent构造函数
  this.age = age;
}
// 使用
var child = new Child('Alice', 30);
child.sayName(); // 错误,sayName不在Child的原型上
console.log(child.name); // 输出: Alice
console.log(child.age); // 输出: 30

3. 组合继承(原型链 + 借用构造函数)

function Parent(name) {
  this.name = name;
}
Parent.prototype.sayName = function() {
  console.log(this.name);
};
function Child(name, age) {
  Parent.call(this, name); // 借用构造函数继承属性
  this.age = age;
}
// 原型链继承方法
Child.prototype = new Parent();
Child.prototype.constructor = Child;
Child.prototype.sayAge = function() {
  console.log(this.age);
};
// 使用
var child = new Child('Alice', 30);
child.sayName(); // 输出: Alice
child.sayAge(); // 输出: 30

4. 寄生组合继承

function Parent(name) {
  this.name = name;
}
Parent.prototype.sayName = function() {
  console.log(this.name);
};
function Child(name, age) {
  Parent.call(this, name);
  this.age = age;
}
// 寄生组合继承
(function() {
  var Super = function() {};
  Super.prototype = Parent.prototype;
  Child.prototype = new Super();
  Child.prototype.constructor = Child;
})();
Child.prototype.sayAge = function() {
  console.log(this.age);
};
// 使用
var child = new Child('Alice', 30);
child.sayName(); // 输出: Alice
child.sayAge(); // 输出: 30

在ES5中,没有内置的继承关键字,如ES6中的classextends,因此需要通过上述方式手动实现继承。其中,寄生组合继承是较为理想的方式,因为它避免了原型链继承和借用构造函数继承的一些问题,同时保持了函数的原型链不变。

23. 请简述 == 的机制

在JavaScript中,==(等于运算符)用于比较两个值是否相等,但它会在比较前对操作数进行类型转换,这被称为类型强制转换(coercion)。==的机制可以概括为以下步骤:

  1. 类型相同:如果两个值类型相同,直接比较它们的值是否相等。
  2. 类型不同:如果两个值类型不同,JavaScript会尝试将它们转换为同一类型,然后进行比较。
    • null和undefinednull == undefinedtrue,它们之间相互等于,但它们不等于任何其他值。
    • 数字和字符串:字符串会尝试转换为数字,然后进行比较。
    • 布尔值:布尔值会转换为数字,true转换为1false转换为0,然后进行比较。
    • 对象和非对象:对象会调用其valueOf()方法,如果返回的不是原始值,则会调用toString()方法,将结果转换为原始类型后再进行比较。
  3. 特殊规则
    • NaN不等于任何值,包括它自己。
    • +0-0相等。
    • 对于对象,比较的是引用而不是值,只有当它们引用同一个对象时才相等。

示例

console.log(1 == '1'); // true,字符串'1'被转换为数字1
console.log(true == 1); // true,布尔值true被转换为数字1
console.log(null == undefined); // true,特殊规则
console.log('0' == false); // true,字符串'0'被转换为数字0,布尔值false被转换为数字0
console.log({} == '{}'); // false,对象和字符串比较,对象被转换为字符串'[object Object]',与'{}'不相等

注意

由于==会进行类型转换,可能会导致一些意外的结果,因此在编程实践中,很多开发者倾向于使用===(严格等于运算符),它不会进行类型转换,只有当两个值的类型和值都相等时才返回true

console.log(1 === '1'); // false,类型不同
console.log(true === 1); // false,类型不同

了解==的机制对于避免JavaScript中的常见陷阱和编写更可靠的代码非常重要。

24. 介绍下304过程

304过程通常指的是HTTP协议中的一种响应状态码——304 Not Modified。这个过程主要涉及到客户端缓存和服务器验证缓存有效性的机制。以下是304过程的详细步骤:

1. 客户端发起请求

客户端(如浏览器)在发起请求时,会检查本地缓存中是否有所请求资源的副本。如果存在,客户端会在请求头中添加以下字段:

  • If-Modified-Since:上次从服务器获取资源时,服务器返回的Last-Modified时间。
  • If-None-Match:上次从服务器获取资源时,服务器返回的ETag(实体标签,用于标识资源版本)。

2. 服务器验证

服务器接收到请求后,会根据请求头中的If-Modified-SinceIf-None-Match字段来验证客户端缓存的资源是否仍然有效:

  • 如果资源的修改时间晚于If-Modified-Since指定的时间,或者ETag与If-None-Match不匹配,说明资源已被修改,服务器会返回正常的响应(状态码200 OK)以及最新的资源内容。
  • 如果资源的修改时间早于或等于If-Modified-Since指定的时间,且ETag与If-None-Match匹配,说明资源未被修改,服务器会返回状态码304 Not Modified。

3. 客户端处理响应

  • 如果收到状态码200 OK,客户端会丢弃本地缓存,并使用服务器返回的新资源。
  • 如果收到状态码304 Not Modified,客户端会继续使用本地缓存的资源,而不会从服务器下载新资源。

优点

  • 减少带宽使用:只有当资源确实发生变化时,才需要下载新资源。
  • 提高加载速度:客户端可以使用本地缓存,减少了等待服务器响应的时间。
  • 减轻服务器负担:减少了服务器的请求处理和资源传输负担。

示例

// 客户端请求头
GET /example.html HTTP/1.1
Host: www.example.com
If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT
If-None-Match: "123456789"
// 服务器响应头(资源未修改)
HTTP/1.1 304 Not Modified
Date: Tue, 22 Oct 2019 12:34:56 GMT
// 服务器响应头(资源已修改)
HTTP/1.1 200 OK
Date: Tue, 22 Oct 2019 12:34:56 GMT
Last-Modified: Wed, 22 Oct 2019 10:00:00 GMT
ETag: "abcdef123456"
Content-Length: 12345
Content-Type: text/html
<!DOCTYPE html>
<html>
<head>
    <title>Example</title>
</head>
<body>
    <p>New content here.</p>
</body>
</html>

在这个示例中,如果资源未修改,服务器返回304 Not Modified,客户端使用本地缓存;如果资源已修改,服务器返回200 OK和新资源内容,客户端更新本地缓存。 304过程是HTTP缓存机制的重要组成部分,对于提升Web应用的性能和用户体验具有重要意义。

25. 下面这段代码中,class为content的元素,实际高度是100px吗?

要确定HTML元素的实际高度,我们需要考虑其CSS样式、内容、边框、内边距、外边距等因素。然而,您没有提供具体的HTML和CSS代码,只提到了元素的类名为content。假设我们有以下简单的HTML和CSS代码:

<div class="content">
  <!-- 内容可能影响高度 -->
</div>
.content {
  height: 100px; /* 指定高度为100px */
}

在这个假设的例子中,.content类的元素被明确设置了height: 100px。但是,实际高度是否真的是100px还取决于其他因素:

  1. 内容溢出:如果.content内部的内容超过了100px的高度,并且没有设置overflow属性来隐藏或滚动内容,那么实际高度可能会大于100px。
  2. 内边距(Padding):如果.content设置了内边距(padding),那么实际高度将会是100px + padding-top + padding-bottom
  3. 边框(Border):如果.content设置了边框,边框的宽度也会影响总高度,但通常我们讨论内容区域的高度时不会包括边框。
  4. 外边距(Margin):外边距不会影响元素本身的高度,但会影响到元素在页面上的布局位置。
  5. 盒模型:CSS的盒模型计算方式(内容框、内边距、边框、外边距)也会影响最终的高度。
  6. 其他CSS属性:例如box-shadowtransform等属性可能会视觉上影响元素的高度,但不会改变其布局高度。 如果没有其他CSS属性影响,并且内容没有溢出,那么.content的实际内容区域高度应该是100px。但是,如果存在上述提到的其他因素,实际高度可能会不同。 为了准确回答您的问题,需要查看具体的HTML结构和相关的CSS样式。如果您能提供更多代码细节,我可以给出更准确的答案。

26. 怎么做移动端的样式适配?

移动端样式适配是确保网页在不同尺寸的移动设备上都能良好显示的关键。以下是一些常用的移动端样式适配方法:

1. 响应式设计(Responsive Design)

媒体查询(Media Queries): 使用CSS媒体查询根据不同屏幕尺寸应用不同的样式。

@media (max-width: 768px) {
  /* 小屏设备样式 */
  .content {
    width: 100%;
  }
}

百分比宽度: 使用百分比而不是固定像素值来设置元素的宽度,使布局更灵活。

.content {
  width: 80%;
}

弹性布局(Flexbox): 利用Flexbox布局模型实现灵活的布局设计。

.flex-container {
  display: flex;
  flex-direction: column;
}

网格布局(Grid): 使用CSS Grid布局创建复杂的响应式设计。

.grid-container {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
}

2. 移动端优先

移动端优先策略: 首先为移动设备设计样式,然后通过媒体查询逐步为更大屏幕的设备添加样式。

/* 默认为移动端样式 */
.content {
  width: 100%;
}
@media (min-width: 768px) {
  /* 平板及以上设备样式 */
  .content {
    width: 50%;
  }
}

3. 视口单位(Viewport Units)

使用视口单位: 使用vw、vh、vmin、vmax等视口单位来设置元素的大小,使其与视口大小相关联。

.content {
  width: 50vw; /* 视口宽度的50% */
  height: 50vh; /* 视口高度的50% */
}

4. 图片和媒体的响应式

响应式图片: 使用<img>标签的srcset属性或picture元素来提供不同分辨率的图片。

<img src="image.jpg" srcset="image-2x.jpg 2x, image-3x.jpg 3x" alt="描述">

5. 字体大小和行高

相对单位: 使用em、rem等相对单位来设置字体大小和行高,以便在不同设备上保持可读性。

body {
  font-size: 16px;
}
h1 {
  font-size: 2rem; /* 32px */
}

6. 工具和框架

使用CSS框架: 如Bootstrap、Foundation等,它们提供了预定义的响应式网格系统和组件。 使用PostCSS: 通过PostCSS插件(如autoprefixer、postcss-media-minmax)来简化CSS编写和兼容性处理。

7. 测试和调试

设备测试: 使用真实设备或浏览器模拟器测试网页在不同设备上的显示效果。 调试工具: 利用Chrome DevTools等浏览器开发者工具进行调试和优化。

8. 性能优化

压缩资源: 压缩CSS、JavaScript和图片文件,减少加载时间。 懒加载: 对非视口内容进行懒加载,提高页面加载速度。 优化触摸体验: 确保按钮和可点击元素的大小适合触摸操作,避免误触。 通过结合以上方法,可以有效地实现移动端样式适配,确保网页在不同设备上都能提供良好的用户体验。

27. 说说sourcemap的原理?

SourceMap是一种提供源代码到编译后代码之间映射的技术,它允许开发者调试压缩或编译后的代码时,能够定位到原始源代码的位置。SourceMap的原理主要包括以下几个方面:

1. 映射关系

SourceMap通过一个映射文件(通常以.map为扩展名)来存储源代码与编译后代码之间的映射关系。这个映射文件是一个JSON格式的文件,包含了源文件的位置信息、行列号以及与编译后代码的对应关系。

2. 生成SourceMap

在编译或压缩源代码的过程中(如使用Webpack、Babel、UglifyJS等工具),可以生成SourceMap文件。生成SourceMap时,工具会记录源代码中每个字符在编译后代码中的位置信息。

3. SourceMap文件结构

SourceMap文件主要包括以下字段:

  • version:SourceMap的版本号。
  • sources:源文件列表。
  • names:源代码中所有变量和函数名称的列表。
  • mappings:映射关系,是一个字符串,表示源代码与编译后代码之间的位置对应关系。
  • file:编译后的文件名。
  • sourceRoot:源文件根路径。

4. 映射解析

mappings字段是SourceMap的核心,它使用一种Base64 VLQ(Variable-Length Quantity)编码格式来表示源代码与编译后代码之间的行列号映射。每个映射段包含四个信息:

  • 编译后代码的列号。
  • 源文件索引。
  • 源代码的行号。
  • 源代码的列号。 通过这些信息,开发者工具可以准确地定位到源代码中的位置。

5. 使用SourceMap

在浏览器中,开发者可以通过开发者工具启用SourceMap功能。当调试编译后的代码时,浏览器会根据SourceMap文件解析出原始源代码的位置,并在开发者工具中显示,从而方便调试。

6. 安全性和隐私

由于SourceMap可以映射到源代码,因此可能会暴露源代码的结构和内容。在生产环境中,通常需要对SourceMap文件进行安全控制,避免泄露敏感信息。

7. 性能考虑

SourceMap文件可能会较大,特别是对于大型项目。这可能会影响页面加载性能,因此有时需要权衡是否在生产环境中包含SourceMap。

示例

假设有一个简单的源代码文件source.js

function add(a, b) {
  return a + b;
}

经过压缩和编译后,可能变成compiled.js

function add(a,b){return a+b}

对应的SourceMap文件compiled.js.map可能包含以下内容:

{
  "version": 3,
  "file": "compiled.js",
  "sourceRoot": "",
  "sources": ["source.js"],
  "names": ["add", "a", "b"],
  "mappings": "AAAA,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC"
}

在调试时,浏览器开发者工具会使用这个SourceMap文件来将compiled.js中的代码映射回source.js中的原始代码。 通过这种方式,SourceMap使得开发者能够在不影响生产环境性能的前提下,调试压缩或编译后的代码。

28. 时钟指针的夹角

要计算时钟上两个指针之间的夹角,我们需要考虑时钟的布局和指针的运动规律。时钟是一个圆形,共有12个小时标记,因此每个小时标记之间的夹角是360度除以12,即30度。分钟指针每分钟移动6度(360度除以60分钟)。 以下是计算两个指针之间夹角的步骤:

  1. 计算小时指针的角度
    • 小时指针不仅随小时变化,还随分钟变化。每过一分钟,小时指针会稍微移动一点。
    • 小时指针的角度 = (小时 % 12)* 30 + (分钟 / 60)* 30
  2. 计算分钟指针的角度
    • 分钟指针的角度 = 分钟 * 6
  3. 计算两个指针之间的夹角
    • 两个指针之间的夹角 = |小时指针的角度 - 分钟指针的角度|
    • 如果计算出的角度大于180度,我们需要用360度减去这个角度,因为时钟是圆形的,我们需要最小的那个夹角。 下面是一个JavaScript函数,用于计算时钟上小时和分钟指针之间的夹角:
function calculateAngle(hour, minute) {
  // 将小时转换为12小时制
  hour = hour % 12;
  // 计算小时指针的角度
  let hourAngle = (hour * 30) + (minute / 60) * 30;
  // 计算分钟指针的角度
  let minuteAngle = minute * 6;
  // 计算两个指针之间的夹角
  let angle = Math.abs(hourAngle - minuteAngle);
  // 确保得到的是最小的夹角
  angle = Math.min(angle, 360 - angle);
  return angle;
}
// 示例:计算3点15分时,时钟指针的夹角
console.log(calculateAngle(3, 15)); // 输出夹角

这个函数接受小时和分钟作为参数,并返回两个指针之间的夹角。你可以调用这个函数并传入特定的时间来获取相应的夹角。