2025面试大全(25)

242 阅读47分钟

1. app中常提到的webview是什么?

WebView 是一种在应用程序中嵌入 Web 内容的技术。它允许开发者在他们的原生应用(如 Android、iOS 应用)中展示网页,而不需要用户打开一个独立的浏览器。WebView 可以看作是一个内置的浏览器,它能够加载和显示网页内容,同时支持与网页交互。 WebView 的主要特点包括:

  1. 集成性:可以直接在应用中展示网页,无需跳转到外部浏览器。
  2. 自定义:开发者可以自定义 WebView 的外观和行为,以适应应用的整体风格。
  3. 交互性:支持 JavaScript 与原生应用的交互,使得网页可以调用原生功能,如摄像头、GPS 等。
  4. 性能:由于 WebView 是内置的,因此加载网页的速度通常比打开外部浏览器更快。 WebView 在各种应用场景中都非常有用,例如:
  • 新闻应用:展示新闻文章。
  • 电商应用:展示商品详情页。
  • 社交应用:展示用户生成的网页内容。
  • 混合应用开发:结合原生代码和 Web 技术开发应用。 然而,WebView 也存在一些局限性,如性能可能不如原生控件、对某些高级 Web 功能的支持有限等。因此,开发者需要根据具体需求权衡是否使用 WebView。

2. 如何查找一篇英文文章中出现频率最高的单词?

要使用JavaScript查找一篇英文文章中出现频率最高的单词,你可以遵循以下步骤:

  1. 预处理文本:将文本转换为小写,以消除大小写差异,并使用正则表达式去除标点符号和特殊字符。
  2. 分词:将预处理后的文本分割成单词数组。
  3. 统计单词频率:遍历单词数组,统计每个单词出现的次数。
  4. 找出频率最高的单词:遍历统计结果,找出出现次数最多的单词。 以下是一个简单的JavaScript代码示例,实现了上述功能:
function findMostFrequentWord(text) {
  // 预处理文本:转换为小写,替换非字母字符为空格
  const processedText = text.toLowerCase().replace(/[^a-z\s]/g, ' ');
  // 分词
  const words = processedText.split(/\s+/);
  // 统计单词频率
  const wordCount = {};
  words.forEach(word => {
    if (word) { // 忽略空字符串
      wordCount[word] = (wordCount[word] || 0) + 1;
    }
  });
  // 找出频率最高的单词
  let mostFrequentWord = '';
  let maxCount = 0;
  for (const [word, count] of Object.entries(wordCount)) {
    if (count > maxCount) {
      mostFrequentWord = word;
      maxCount = count;
    }
  }
  return { mostFrequentWord, maxCount };
}
// 示例使用
const article = "This is a sample text with some words. Some words appear more than once, like 'some'.";
const result = findMostFrequentWord(article);
console.log(`The most frequent word is "${result.mostFrequentWord}" with ${result.maxCount} occurrences.`);

这个函数会返回一个对象,包含出现频率最高的单词及其出现次数。请注意,这个简单的示例没有处理所有可能的边缘情况,例如连字符、撇号等。根据实际需要,你可能需要进一步优化文本预处理和分词逻辑。

3. js 中的倒计时,怎么实现纠正偏差?

在JavaScript中实现倒计时,通常会使用setTimeoutsetInterval函数。然而,由于JavaScript的单线程特性以及浏览器渲染和执行其他任务的时间,这些方法可能会导致倒计时出现偏差。为了纠正这种偏差,可以采取以下策略:

  1. 记录开始时间:在倒计时开始时记录当前时间。
  2. 计算剩余时间:在每次更新显示时,计算从开始到现在已经过去的时间,并从总倒计时时间中减去这个值,以得到剩余时间。
  3. 使用setTimeout进行递归:而不是使用setInterval,因为setTimeout可以在每次执行后重新计算下一次执行的时间,从而纠正偏差。 以下是一个实现倒计时并纠正偏差的示例代码:
function startCountdown(duration, displayElement) {
  let startTime = Date.now(); // 记录开始时间
  let remainingTime = duration; // 初始剩余时间
  function updateDisplay(time) {
    // 更新显示的函数,根据需要格式化时间
    let hours = Math.floor(time / 3600000);
    let minutes = Math.floor((time % 3600000) / 60000);
    let seconds = Math.floor((time % 60000) / 1000);
    displayElement.textContent = `${hours}h ${minutes}m ${seconds}s`;
  }
  function countdown() {
    let currentTime = Date.now();
    let elapsedTime = currentTime - startTime; // 计算已经过去的时间
    remainingTime = duration - elapsedTime; // 计算剩余时间
    if (remainingTime <= 0) {
      // 如果倒计时结束
      remainingTime = 0;
      updateDisplay(remainingTime);
      console.log('Countdown finished!');
    } else {
      // 更新显示并继续倒计时
      updateDisplay(remainingTime);
      setTimeout(countdown, 1000); // 递归调用,设置下一秒的倒计时
    }
  }
  updateDisplay(duration); // 初始显示
  setTimeout(countdown, 1000); // 开始倒计时
}
// 示例使用
let countdownDuration = 3600000; // 1小时的倒计时
let displayElement = document.getElementById('countdown-display'); // 显示倒计时的元素
startCountdown(countdownDuration, displayElement);

在这个示例中,startCountdown函数接受倒计时总时长(以毫秒为单位)和一个用于显示倒计时的DOM元素。函数内部记录开始时间,并在每次递归调用时计算剩余时间,从而纠正由于执行延迟导致的偏差。 这种方法可以有效地减少倒计时偏差,但请注意,如果页面被切换到后台或者浏览器处于不活动状态,某些浏览器可能会降低setTimeout的执行频率,这可能导致倒计时仍然存在一定的偏差。这种情况下,偏差是无法完全避免的。

4. 如果一个列表有 100000 个数据,这个该怎么进行展示?

展示一个包含 100,000 个数据的列表在 Web 应用中是一个挑战,因为直接渲染这么多条目会导致性能问题,如页面卡顿、加载时间过长等。为了有效地展示大量数据,可以采用以下策略:

  1. 分页(Pagination)
    • 将数据分成多个页面,每页显示一定数量的条目(例如,每页 10、20 或 50 条)。
    • 用户可以导航到不同的页面来查看数据。
  2. 无限滚动(Infinite Scrolling)
    • 当用户滚动到页面底部时,自动加载更多数据。
    • 这种方式提供了无缝的浏览体验,但需要注意性能优化。
  3. 虚拟滚动(Virtual Scrolling)
    • 只渲染可视区域内的条目,当用户滚动时,动态加载和卸载条目。
    • 这种方式可以大大减少同时渲染的DOM元素数量,提高性能。
  4. 搜索和过滤(Search and Filtering)
    • 提供搜索框让用户可以搜索特定数据。
    • 提供过滤选项让用户可以根据特定条件筛选数据。
  5. 懒加载(Lazy Loading)
    • 对于图片或其他资源密集型的内容,可以在用户滚动到相应区域时再加载。
  6. 数据分片(Data Sharding)
    • 如果数据存储在服务器端,可以分批次从服务器获取数据,而不是一次性加载所有数据。
  7. Web Workers
    • 使用 Web Workers 在后台线程处理数据,避免阻塞主线程。
  8. 优化数据结构
    • 确保数据结构优化,以便快速检索和渲染。
  9. 前端性能优化
    • 使用高效的DOM操作,避免重绘和回流。
    • 使用CSS硬件加速(如transform和opacity)提高动画性能。
  10. 后端支持
    • 后端应该提供高效的数据查询接口,支持分页、过滤和排序。 实现这些策略通常需要结合前端和后端的优化。以下是一个简单的分页示例:
// 假设有一个函数 fetchPage(pageNumber, pageSize) 从服务器获取数据
function fetchPage(pageNumber, pageSize) {
  // 返回一个Promise,模拟从服务器获取数据
  return new Promise(resolve => {
    const data = []; // 模拟数据
    for (let i = 0; i < pageSize; i++) {
      data.push(`Item ${pageNumber * pageSize + i + 1}`);
    }
    setTimeout(() => resolve(data), 500); // 模拟网络延迟
  });
}
// 分页组件
function Pagination(container, totalCount, pageSize) {
  this.container = container;
  this.totalCount = totalCount;
  this.pageSize = pageSize;
  this.currentPage = 0;
  this.init = function() {
    this.renderControls();
    this.loadPage(0);
  };
  this.renderControls = function() {
    // 渲染分页控制按钮
  };
  this.loadPage = function(pageNumber) {
    fetchPage(pageNumber, this.pageSize).then(data => {
      this.renderPage(data);
      this.currentPage = pageNumber;
    });
  };
  this.renderPage = function(data) {
    // 清空当前内容并渲染新页面数据
    this.container.innerHTML = '';
    data.forEach(item => {
      const element = document.createElement('div');
      element.textContent = item;
      this.container.appendChild(element);
    });
  };
}
// 使用分页组件
const pagination = new Pagination(document.getElementById('data-container'), 100000, 20);
pagination.init();

在这个示例中,我们创建了一个Pagination类来处理分页逻辑。fetchPage函数模拟从服务器获取数据。实际应用中,你需要根据后端API来调整这个函数。 选择哪种策略取决于具体的应用场景和用户需求。在实际开发中,可能需要结合多种策略来达到最佳的性能和用户体验。

5. Math.ceil 和 Math.floor 有什么区别?

Math.ceilMath.floor 都是 JavaScript 的 Math 对象中的方法,用于对数字进行向上或向下取整。它们的区别在于如何处理小数部分:

  1. Math.ceil()
    • ceil 是“天花板”的意思,表示向上取整。
    • 该方法会将数字向上舍入到最接近的整数。
    • 如果数字已经是一个整数,则不会进行任何改变。
    • 例如:Math.ceil(4.3) 会返回 5Math.ceil(-4.3) 会返回 -4
  2. Math.floor()
    • floor 是“地板”的意思,表示向下取整。
    • 该方法会将数字向下舍入到最接近的整数。
    • 如果数字已经是一个整数,则不会进行任何改变。
    • 例如:Math.floor(4.3) 会返回 4Math.floor(-4.3) 会返回 -5。 简单来说,Math.ceil 总是向正无穷大方向取整,而 Math.floor 总是向负无穷大方向取整。 以下是两个方法的一些示例:
console.log(Math.ceil(4.3));  // 输出: 5
console.log(Math.ceil(4.9));  // 输出: 5
console.log(Math.ceil(-4.3)); // 输出: -4
console.log(Math.ceil(-4.9)); // 输出: -4
console.log(Math.floor(4.3));  // 输出: 4
console.log(Math.floor(4.9));  // 输出: 4
console.log(Math.floor(-4.3)); // 输出: -5
console.log(Math.floor(-4.9)); // 输出: -5

在实际应用中,根据需要向上或向下取整的场景选择使用 Math.ceilMath.floor。例如,如果你在计算需要向上取整的页数或者物品数量,可能会使用 Math.ceil。而如果你在处理数组索引或者需要向下取整的场景,可能会使用 Math.floor

6. 如何确定页面的可用性时间,什么是 Performance API?

确定页面的可用性时间 页面的可用性时间通常指的是用户能够与页面进行交互的时间点,这可以通过Performance API中的timing属性来获取。具体来说,可以使用domInteractivedomComplete这两个时间点来大致判断:

  • domInteractive:表示文档解析完成,DOM构建完成,此时用户可以开始与页面进行交互,但样式表、图片和子框架可能还在加载。
  • domComplete:表示页面及其依赖资源完全加载完成,用户可以完全与页面进行交互。 通过这两个时间点,可以大致确定页面的可用性时间。 Performance API Performance API是现代浏览器提供的一套用于测量和分析网页性能的接口。它允许开发者获取到关于页面加载和运行时的详细性能数据,从而帮助开发者优化网页性能。 Performance API主要包括以下几部分:
  1. Performance Timing:提供了与页面加载相关的时间点,如上面提到的domInteractivedomComplete等。通过这些时间点,可以计算出页面的加载时间、白屏时间、可交互时间等关键性能指标。
  2. Performance Navigation:提供了关于页面导航的信息,如导航类型(如链接点击、表单提交、页面刷新等)、重定向次数等。
  3. Performance Resource Timing:允许开发者获取到页面中每个资源(如图片、脚本、样式表等)的加载时间细节。
  4. Performance Mark and Measure:允许开发者在代码中插入标记(mark),并在标记之间进行测量(measure),从而获取到代码执行的时间。
  5. Performance Observer:提供了一个观察者模式,允许开发者订阅特定的性能事件,并在事件发生时接收通知。 使用Performance API,开发者可以更深入地了解网页的性能表现,找出性能瓶颈,并进行相应的优化。 以下是一个简单的示例,展示如何使用Performance Timing获取页面的可用性时间:
if (performance.timing) {
  var timing = performance.timing;
  var interactiveTime = timing.domInteractive - timing.navigationStart;
  var completeTime = timing.domComplete - timing.navigationStart;
  console.log("页面可交互时间:" + interactiveTime + "ms");
  console.log("页面完全加载时间:" + completeTime + "ms");
}

在这个示例中,我们通过performance.timing获取了页面加载的相关时间点,并计算出了页面可交互时间和完全加载时间。这些数据可以帮助我们了解页面的性能表现。

7. 手写一个 jsonp

JSONP(JSON with Padding)是一种跨域获取数据的技术,它利用了<script>标签的跨域特性。下面是一个简单的JSONP实现的示例,包括服务器端和客户端两部分。 服务器端示例(使用Node.js)

const http = require('http');
const server = http.createServer((req, res) => {
  if (req.url === '/jsonp') {
    const callback = req.query.callback || 'callback';
    const data = { message: 'Hello from JSONP!' };
    const jsonpData = `${callback}(${JSON.stringify(data)})`;
    res.writeHead(200, { 'Content-Type': 'application/javascript' });
    res.end(jsonpData);
  } else {
    res.writeHead(404);
    res.end();
  }
});
server.listen(3000, () => {
  console.log('Server is running on http://localhost:3000');
});

在这个服务器端示例中,我们创建了一个简单的HTTP服务器,当请求/jsonp路径时,服务器会返回一个JSONP格式的响应。响应内容是一个函数调用,函数名由客户端通过查询参数callback指定,函数的参数是实际的JSON数据。 客户端示例

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>JSONP Example</title>
</head>
<body>
  <script>
    function handleResponse(data) {
      console.log(data.message); // 输出:Hello from JSONP!
    }
    var script = document.createElement('script');
    script.src = 'http://localhost:3000/jsonp?callback=handleResponse';
    document.body.appendChild(script);
  </script>
</body>
</html>

在客户端示例中,我们创建了一个<script>标签,并将其src属性设置为服务器端的JSONP接口地址,同时传递了一个callback参数,这个参数的值是客户端定义的handleResponse函数名。当服务器响应这个请求时,它会返回一个调用handleResponse函数的JavaScript代码,并将JSON数据作为参数传递给这个函数。 这样,当<script>标签加载完成后,handleResponse函数会被调用,从而实现了跨域数据传输。 请注意,JSONP存在一些安全风险,比如容易受到XSS攻击,因此在使用时需要确保安全性。此外,JSONP只支持GET请求。在现代前端开发中,通常推荐使用CORS(Cross-Origin Resource Sharing)来实现跨域请求,因为它更加安全且功能更强大。

8. 如何判断一个对象是否为空对象?

在JavaScript中,判断一个对象是否为空对象(即没有自己的可枚举属性)可以通过多种方式实现。以下是几种常见的方法:

1. 使用Object.keys()

Object.keys()方法返回一个由对象自身可枚举属性组成的数组。如果这个数组长度为0,那么对象就是空的。

function isEmptyObject(obj) {
  return Object.keys(obj).length === 0;
}
// 示例
const emptyObj = {};
const nonEmptyObj = { a: 1 };
console.log(isEmptyObject(emptyObj)); // true
console.log(isEmptyObject(nonEmptyObj)); // false

2. 使用for...in循环

通过for...in循环遍历对象的所有可枚举属性,如果没有属性,则对象为空。

function isEmptyObject(obj) {
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      return false;
    }
  }
  return true;
}
// 示例
const emptyObj = {};
const nonEmptyObj = { a: 1 };
console.log(isEmptyObject(emptyObj)); // true
console.log(isEmptyObject(nonEmptyObj)); // false

3. 使用JSON.stringify()

将对象转换为JSON字符串,如果结果为"{}",则对象为空。

function isEmptyObject(obj) {
  return JSON.stringify(obj) === '{}';
}
// 示例
const emptyObj = {};
const nonEmptyObj = { a: 1 };
console.log(isEmptyObject(emptyObj)); // true
console.log(isEmptyObject(nonEmptyObj)); // false

4. 使用Object.getOwnPropertyNames()

Object.getOwnPropertyNames()方法返回一个由对象自身所有属性(包括不可枚举属性)组成的数组。如果这个数组长度为0,那么对象就是空的。

function isEmptyObject(obj) {
  return Object.getOwnPropertyNames(obj).length === 0;
}
// 示例
const emptyObj = {};
const nonEmptyObj = { a: 1 };
console.log(isEmptyObject(emptyObj)); // true
console.log(isEmptyObject(nonEmptyObj)); // false

注意事项

  • 这些方法只检查对象自身的属性,不会检查原型链上的属性。
  • Object.keys()for...in循环只考虑可枚举属性,而Object.getOwnPropertyNames()考虑所有属性,包括不可枚举属性。
  • JSON.stringify()方法在对象中包含函数、Symbol属性或循环引用时可能不适用。 根据具体需求选择合适的方法来判断对象是否为空。在大多数情况下,使用Object.keys()方法是最简单和直接的方式。

9. 封装一个 javascript 的类型判断函数

封装一个JavaScript的类型判断函数,可以采用以下几种方式。这里提供一个较为全面的类型判断函数,它能够判断常见的数据类型,如字符串、数字、布尔值、数组、对象、函数、null、undefined等。

类型判断函数

function typeOf(obj) {
  const toString = Object.prototype.toString;
  const map = {
    '[object Boolean]': 'boolean',
    '[object Number]': 'number',
    '[object String]': 'string',
    '[object Function]': 'function',
    '[object Array]': 'array',
    '[object Date]': 'date',
    '[object RegExp]': 'regExp',
    '[object Undefined]': 'undefined',
    '[object Null]': 'null',
    '[object Object]': 'object'
  };
  return map[toString.call(obj)];
}
// 示例
console.log(typeOf({})); // 'object'
console.log(typeOf([])); // 'array'
console.log(typeOf(123)); // 'number'
console.log(typeOf('123')); // 'string'
console.log(typeOf(true)); // 'boolean'
console.log(typeOf(undefined)); // 'undefined'
console.log(typeOf(null)); // 'null'
console.log(typeOf(function () {})); // 'function'
console.log(typeOf(new Date())); // 'date'
console.log(typeOf(/test/i)); // 'regExp'

解释

  1. Object.prototype.toString:这个方法会返回一个表示对象类型的字符串,格式为[object Type],其中Type是对象的类型。
  2. map对象:用于将Object.prototype.toString返回的字符串映射到更简单的类型名称。
  3. toString.call(obj):使用call方法调用toString,这样this指向obj,从而能够获取obj的类型。
  4. 返回类型:根据map中的映射返回对应的类型字符串。

扩展

如果你只需要判断少数几种类型,可以简化这个函数。例如,只判断数组、对象和函数:

function simpleTypeOf(obj) {
  if (Array.isArray(obj)) {
    return 'array';
  } else if (obj === null) {
    return 'null';
  } else {
    return typeof obj;
  }
}
// 示例
console.log(simpleTypeOf({})); // 'object'
console.log(simpleTypeOf([])); // 'array'
console.log(simpleTypeOf(123)); // 'number'
console.log(simpleTypeOf('123')); // 'string'
console.log(simpleTypeOf(true)); // 'boolean'
console.log(simpleTypeOf(undefined)); // 'undefined'
console.log(simpleTypeOf(null)); // 'null'
console.log(simpleTypeOf(function () {})); // 'function'

在这个简化版本中,使用Array.isArray来判断数组,typeof来判断其他类型,并对null进行特殊处理,因为typeof null会返回'object'。 根据你的具体需求,可以选择使用完整版或简化版的类型判断函数。

10. 如何检测浏览器所支持的最小字体大小?

要检测浏览器所支持的最小字体大小,你可以使用CSS和JavaScript来完成这个任务。以下是一种方法:

  1. 使用CSS设置最小字体大小
body {
  font-size: 10px; /* 设置一个较小的字体大小 */
}
  1. 使用JavaScript来检测
function getMinFontSize() {
  const div = document.createElement('div');
  div.style.fontSize = '10px';
  document.body.appendChild(div);
  const computedStyle = window.getComputedStyle(div);
  const minFontSize = parseInt(computedStyle.fontSize);
  document.body.removeChild(div);
  return minFontSize;
}
const minFontSize = getMinFontSize();
console.log(`浏览器支持的最小字体大小为:${minFontSize}px`);

解释

  1. CSS部分:设置bodyfont-size为10px,这是一个较小的字体大小。
  2. JavaScript部分
  • document.createElement('div'):创建一个div元素。
  • div.style.fontSize = '10px':设置这个div的字体大小为10px。
  • document.body.appendChild(div):将这个div添加到文档中。
  • window.getComputedStyle(div):获取这个div的计算样式。
  • parseInt(computedStyle.fontSize):解析计算样式中的字体大小,转换为数字。
  • document.body.removeChild(div):从文档中移除这个div
  • return minFontSize:返回这个最小字体大小。
  1. 输出结果:使用console.log输出浏览器支持的最小字体大小。 这个方法可以准确地检测出浏览器实际支持的最小字体大小,而不仅仅是CSS中设置的大小。因为某些浏览器可能会忽略过小的字体大小,所以实际检测比仅设置CSS更可靠。

11. 怎么使用 setTimeout 实现 setInterval?

setTimeoutsetInterval 都是 JavaScript 中的定时器函数,但它们的行为有所不同。setTimeout 只执行一次,而 setInterval 会每隔一定时间重复执行。如果你想要使用 setTimeout 来模拟 setInterval 的行为,你可以通过在 setTimeout 的回调函数中再次调用 setTimeout 来实现。 以下是一个使用 setTimeout 实现 setInterval 的示例:

function mySetInterval(callback, delay) {
  let intervalId = null;
  function intervalWrapper() {
    callback();
    intervalId = setTimeout(intervalWrapper, delay);
  }
  intervalId = setTimeout(intervalWrapper, delay);
  // 返回一个函数,可以用来停止定时器
  return function stop() {
    clearTimeout(intervalId);
  };
}
// 使用自定义的 mySetInterval
const stopInterval = mySetInterval(() => {
  console.log('这是使用 setTimeout 实现的 setInterval');
}, 1000);
// 如果你想停止这个定时器,可以调用返回的 stop 函数
// stopInterval();

解释:

  1. mySetInterval 函数:这个函数接受两个参数,callback 是要重复执行的函数,delay 是执行间隔的时间(以毫秒为单位)。
  2. intervalWrapper 函数:这是一个内部函数,它首先调用传入的 callback,然后使用 setTimeout 再次调用自身,从而实现重复执行。
  3. 设置定时器:通过 setTimeout(intervalWrapper, delay) 启动定时器。
  4. 停止定时器mySetInterval 函数返回一个 stop 函数,当你调用这个函数时,会使用 clearTimeout 来停止定时器。 这种方法的优势在于提供了更多的控制,例如,你可以更容易地实现一个不会在执行回调时阻塞的“setInterval”,或者根据某些条件动态地调整执行间隔。此外,使用 setTimeout 可以避免 setInterval 在某些情况下可能出现的累积延迟问题。

12. 怎么使用 js 实现拖拽功能?

在 JavaScript 中实现拖拽功能通常涉及到以下步骤:

  1. 选择拖拽元素:确定哪些元素可以被拖拽。
  2. 监听拖拽事件:为拖拽元素添加相应的事件监听器,如 dragstart, drag, dragend 等。
  3. 设置拖拽数据:在 dragstart 事件中设置拖拽数据。
  4. 定义放置区域:确定哪些元素可以作为拖拽目标的放置区域。
  5. 监听放置事件:为放置区域添加事件监听器,如 dragover, drop 等。
  6. 处理放置逻辑:在 drop 事件中处理放置逻辑,如移动元素到新位置。 以下是一个简单的示例,展示了如何使用原生 JavaScript 实现一个基本的拖拽功能:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Drag and Drop Example</title>
<style>
  .draggable {
    width: 100px;
    height: 100px;
    background-color: skyblue;
    margin: 10px;
    padding: 10px;
    cursor: move;
  }
  .drop-zone {
    width: 300px;
    height: 300px;
    background-color: lightgrey;
    position: relative;
  }
</style>
</head>
<body>
<div id="draggable" class="draggable" draggable="true">Drag me!</div>
<div class="drop-zone"></div>
<script>
  const draggable = document.getElementById('draggable');
  const dropZone = document.querySelector('.drop-zone');
  draggable.addEventListener('dragstart', (e) => {
    e.dataTransfer.setData('text/plain', 'Dragged Element');
  });
  dropZone.addEventListener('dragover', (e) => {
    e.preventDefault(); // 必须阻止默认行为以允许放置
  });
  dropZone.addEventListener('drop', (e) => {
    e.preventDefault();
    const data = e.dataTransfer.getData('text/plain');
    // 这里可以执行放置逻辑,例如移动元素
    dropZone.appendChild(draggable);
    console.log(data + ' was dropped');
  });
</script>
</body>
</html>

解释:

  • HTML 结构:一个可拖拽的元素和一个放置区域。
  • CSS 样式:为可拖拽元素和放置区域设置基本的样式。
  • JavaScript 代码
    • 为可拖拽元素添加 dragstart 事件监听器,在拖拽开始时设置拖拽数据。
    • 为放置区域添加 dragover 事件监听器,并在事件处理函数中调用 preventDefault() 以允许放置。
    • 为放置区域添加 drop 事件监听器,在放置时处理逻辑,如将拖拽元素移动到放置区域。 这个示例展示了拖拽功能的基本实现。根据需要,你可以添加更多的功能和样式,例如处理拖拽时的视觉反馈、限制拖拽方向、拖拽多个元素等。

13. get 和 post 请求在缓存方面有什么区别?

GET 和 POST 请求在缓存方面有以下主要区别:

  1. 缓存意图
    • GET 请求:通常用于获取数据,被认为是安全的和幂等的,因此浏览器和中间代理服务器可能会对 GET 请求的结果进行缓存。
    • POST 请求:通常用于提交数据,可能会改变服务器状态,因此通常不会被缓存。
  2. 缓存机制
    • GET 请求:可以通过设置 HTTP 头来控制缓存行为,如 Cache-Control, Expires, ETag 等。浏览器和代理服务器可能会根据这些头来决定是否缓存响应。
    • POST 请求:由于可能包含敏感信息或导致服务器状态变化,通常不会被缓存。即使设置了缓存头,大多数浏览器和代理也不会缓存 POST 请求的响应。
  3. 缓存存储
    • GET 请求:响应可以被存储在浏览器缓存中,也可以被代理服务器缓存,以便于后续的相同请求可以直接使用缓存数据。
    • POST 请求:响应通常不会被存储在缓存中,每个 POST 请求通常都会直接发送到服务器。
  4. 缓存控制
    • GET 请求:开发者可以通过 HTTP 头来明确控制缓存策略,例如使用 Cache-Control: no-cache 来阻止缓存。
    • POST 请求:即使开发者希望缓存 POST 请求的响应,大多数客户端和服务器实现也不会支持这种做法。
  5. 安全性考虑
    • GET 请求:由于参数在 URL 中,可能会被浏览器历史记录、服务器日志等记录,因此不适合传递敏感数据。
    • POST 请求:参数在请求体中,不会直接暴露在 URL 中,更适合传递敏感数据,且由于不缓存,也减少了敏感数据被存储的风险。
  6. Pragma 头
    • GET 请求:可以使用 Pragma: no-cache 来禁止缓存,但这个头主要是为了兼容 HTTP/1.0,在现代的 HTTP/1.1 中建议使用 Cache-Control
    • POST 请求:同样可以使用 Pragma: no-cache,但通常不需要,因为 POST 请求本身就不容易被缓存。 需要注意的是,虽然这些是常见的实践和约定,但具体的缓存行为还取决于浏览器、代理服务器和服务器端的配置。开发者应该根据应用的需求和安全性考虑来选择合适的请求方法,并适当设置缓存控制头。

14. get 请求是否限制了传参长度?

是的,GET 请求在传参长度方面存在限制。这种限制主要来自于两个方面:

  1. 浏览器限制:大多数浏览器对 URL 的长度都有一定的限制。这个限制包括整个 URL 的长度,而不仅仅是查询参数部分。不同浏览器的限制可能不同,但通常这个限制在 2048 字符左右。当 URL 超过这个长度时,浏览器可能无法正确处理请求,或者会截断超出部分的参数。
  2. 服务器限制:除了浏览器的限制外,服务器也可能对请求的 URL 长度有所限制。这个限制取决于服务器的具体配置。如果请求的 URL 超过了服务器允许的最大长度,服务器可能会返回错误响应。 由于这些限制,当需要传递大量数据时,使用 GET 请求可能不是最佳选择。在这种情况下,可以考虑使用 POST 请求。POST 请求将数据放在请求体中,而不是 URL 中,因此不受 URL 长度的限制。这使得 POST 请求更适合用于提交大量数据或敏感信息。 需要注意的是,虽然 POST 请求在传参长度方面更为灵活,但仍然应该避免发送过大的数据量,以保持良好的性能和用户体验。如果需要传输大量数据,可以考虑使用文件上传或其他更高效的数据传输方式。

15. Js 动画与 CSS 动画区别及相应实现

JavaScript动画与CSS动画是前端开发中常用的两种动画实现方式,它们各有特点和使用场景。以下是它们之间的主要区别以及相应的实现方法:

区别:

  1. 性能
    • CSS动画:通常性能更好,因为浏览器可以对CSS动画进行优化,如硬件加速。
    • JavaScript动画:性能取决于代码的优化程度,可能不如CSS动画流畅。
  2. 控制能力
    • CSS动画:控制能力相对较弱,适用于简单的动画效果。
    • JavaScript动画:提供更精细的控制,可以实现复杂的动画逻辑。
  3. 易用性
    • CSS动画:更简单,易于实现,代码量少。
    • JavaScript动画:需要编写更多的代码,复杂度较高。
  4. 兼容性
    • CSS动画:现代浏览器普遍支持,兼容性好。
    • JavaScript动画:也可能具有良好的兼容性,但需要考虑不同浏览器之间的差异。
  5. 动画类型
    • CSS动画:适用于实现过渡效果(transitions)和关键帧动画(keyframes)。
    • JavaScript动画:可以实现任何类型的动画,包括复杂的交互式动画。

CSS动画实现:

过渡效果(Transitions):
/* CSS */
.box {
  width: 100px;
  height: 100px;
  background-color: red;
  transition: width 2s; /* 过渡属性为width,持续时间为2秒 */
}
.box:hover {
  width: 200px; /* 鼠标悬停时宽度变为200px */
}
关键帧动画(Keyframes):
/* CSS */
@keyframes example {
  from {background-color: red;}
  to {background-color: yellow;}
}
.box {
  width: 100px;
  height: 100px;
  animation-name: example;
  animation-duration: 4s;
}

JavaScript动画实现:

// JavaScript
function animateElement(element, property, toValue, duration) {
  let start = performance.now();
  let fromValue = parseInt(window.getComputedStyle(element)[property]);
  requestAnimationFrame(function animate(time) {
    let timeFraction = (time - start) / duration;
    if (timeFraction > 1) timeFraction = 1;
    let value = fromValue + (toValue - fromValue) * timeFraction;
    element.style[property] = value + 'px';
    if (timeFraction < 1) {
      requestAnimationFrame(animate);
    }
  });
}
// 使用示例
let box = document.querySelector('.box');
animateElement(box, 'width', 200, 2000); // 将box的宽度在2秒内变为200px

在实现动画时,应根据动画的复杂度和需求选择合适的方式。对于简单的动画,CSS动画通常是首选,而对于需要复杂逻辑和精细控制的动画,JavaScript动画可能更为合适。

16. 异步编程有哪些实现方式?

异步编程是一种编程范式,允许程序在等待某些操作完成时继续执行其他任务,从而提高程序的效率和响应性。在JavaScript中,异步编程的实现方式主要有以下几种:

1. 回调函数(Callbacks)

回调函数是异步编程的最基本形式。你定义一个函数,将其作为参数传递给另一个函数,然后在某个操作完成后调用它。

function fetchData(callback) {
  // 模拟异步操作
  setTimeout(() => {
    const data = 'some data';
    callback(data);
  }, 1000);
}
fetchData(data => {
  console.log(data); // 'some data'
});

问题:回调地狱(Callback Hell),即多层嵌套的回调函数,可读性差,难以维护。

2. 事件监听(Event Emitters)

通过发布/订阅模式,你可以在某个事件发生时执行回调函数。

const EventEmitter = require('events').EventEmitter;
const emitter = new EventEmitter();
emitter.on('dataReady', data => {
  console.log(data);
});
// 模拟异步操作
setTimeout(() => {
  emitter.emit('dataReady', 'some data');
}, 1000);

3. Promise

Promise对象表示一个异步操作的最终完成(或失败),以及其结果值。

function fetchData() {
  return new Promise((resolve, reject) => {
    // 模拟异步操作
    setTimeout(() => {
      const data = 'some data';
      resolve(data);
    }, 1000);
  });
}
fetchData().then(data => {
  console.log(data); // 'some data'
});

优点:解决了回调地狱问题,代码更清晰。

4. async/await

async/await是ES2017引入的语法,它是基于Promise的语法糖,使得异步代码看起来更像同步代码。

async function fetchData() {
  // 模拟异步操作
  return new Promise(resolve => {
    setTimeout(() => {
      const data = 'some data';
      resolve(data);
    }, 1000);
  });
}
async function main() {
  const data = await fetchData();
  console.log(data); // 'some data'
}
main();

优点:代码更接近同步编程风格,易于理解和维护。

5. 生成器(Generators)和协程(Coroutines)

生成器函数可以暂停执行,然后在需要时恢复执行。结合Promise,可以实现异步流程控制。

function fetchData() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve('some data');
    }, 1000);
  });
}
function* main() {
  const data = yield fetchData();
  console.log(data); // 'some data'
}
const generator = main();
const promise = generator.next().value;
promise.then(data => {
  generator.next(data);
});

注意:生成器主要用于理解异步流程控制,实际开发中较少使用。

6. Observable(可观察对象)

Observable是RxJS库中的一个概念,它表示一个值或事件的序列,可以用来处理复杂的异步数据流。

const { Observable } = require('rxjs');
const dataStream = new Observable(observer => {
  // 模拟异步操作
  setTimeout(() => {
    observer.next('some data');
    observer.complete();
  }, 1000);
});
dataStream.subscribe({
  next: data => console.log(data), // 'some data'
  complete: () => console.log('done')
});

优点:适用于复杂的事件驱动编程,如处理多个异步操作的结果。

选择合适的异步编程方式

  • 对于简单的异步操作,回调函数或Promise可能足够。
  • 对于复杂的异步流程,建议使用async/await或Observable。
  • 在需要处理大量事件或数据流时,考虑使用事件监听或Observable。 每种方式都有其适用场景,选择合适的异步编程方式可以提高代码的可读性、可维护性和效率。

17. offsetWidth/offsetHeight,clientWidth/clientHeight 与 scrollWidth/scrollHeight 的区别?

offsetWidth/offsetHeightclientWidth/clientHeightscrollWidth/scrollHeight 都是 DOM 元素的属性,用于获取元素的大小,但它们各自表示的含义和用途有所不同:

offsetWidth / offsetHeight

  • 定义offsetWidthoffsetHeight 返回元素的布局宽度和高度,包括元素的内容、内边距(padding)和边框(border),但不包括外边距(margin)。
  • 用途:常用于获取元素在页面上的实际占用空间大小。
element.offsetWidth; // 元素的宽度(内容 + 内边距 + 边框)
element.offsetHeight; // 元素的高度(内容 + 内边距 + 边框)

clientWidth / clientHeight

  • 定义clientWidthclientHeight 返回元素的内部宽度和高度,包括内容宽度和内边距(padding),但不包括边框(border)和滚动条(如果有的话)。
  • 用途:常用于获取元素内部可用的空间大小,例如在不考虑边框和滚动条的情况下。
element.clientWidth; // 元素的内部宽度(内容 + 内边距)
element.clientHeight; // 元素的内部高度(内容 + 内边距)

scrollWidth / scrollHeight

  • 定义scrollWidthscrollHeight 返回元素的内容区域的总宽度和总高度,包括由于溢出而无法显示在屏幕上的部分。这些属性包括了内容、内边距(padding)和滚动条不可见部分的大小,但不包括边框(border)。
  • 用途:常用于确定元素是否需要滚动条,以及计算滚动条可滚动的范围。
element.scrollWidth; // 元素的总内容宽度(内容 + 内边距,包括滚动不可见部分)
element.scrollHeight; // 元素的总内容高度(内容 + 内边距,包括滚动不可见部分)

区别总结

  • offsetWidth/offsetHeight 包括内容、内边距和边框,但不包括外边距。
  • clientWidth/clientHeight 包括内容 和内边距,但不包括边框和滚动条。
  • scrollWidth/scrollHeight 包括内容的总大小(无论是否可见),包括内边距,但不包括边框。

示例

假设有一个带有边框、内边距和溢出内容的元素,其 CSS 如下:

.element {
  width: 100px;
  height: 100px;
  padding: 10px;
  border: 5px solid black;
  overflow: auto;
}

如果元素的内容足够多,以至于需要滚动条,那么:

  • offsetWidthoffsetHeight 将返回 120px(内容宽度100px + 内边距20px + 边框10px)。
  • clientWidthclientHeight 将返回 110px(内容宽度100px + 内边距20px,不包括边框和滚动条)。
  • scrollWidthscrollHeight 将返回内容实际需要的总宽度和高度(包括溢出的部分),这通常会大于 clientWidthclientHeight。 了解这些属性的区别有助于在开发中准确获取元素的大小,并进行相应的布局和交互设计。

18. 什么是 MVVM?比之 MVC 有什么区别?什么又是 MVP ?

**MVVM(Model-View-ViewModel)**是一种软件架构模式,它通过将界面(View)与业务逻辑(Model)分离,并通过ViewModel作为中介,来实现数据的双向绑定和自动更新。在MVVM中:

  • Model:代表应用程序的数据和业务逻辑。
  • View:负责界面的展示,不包含业务逻辑。
  • ViewModel:作为Model和View之间的桥梁,它负责将Model的数据转换为View可以展示的形式,同时也处理View的用户交互,将数据变更反馈给Model。 MVVM与MVC的区别
  1. 数据流向
    • MVC:通常是单向数据流,View通过Controller来更新Model,Model更新后再通过Controller来更新View。
    • MVVM:实现了双向数据绑定,ViewModel的变化可以自动更新到View,反之亦然。
  2. 耦合度
    • MVC:Controller与View的耦合度较高,Controller需要知道View的细节。
    • MVVM:ViewModel与View的耦合度较低,ViewModel不依赖于View的具体实现。
  3. 测试性
    • MVC:由于Controller与View的紧密耦合,测试相对复杂。
    • MVVM:ViewModel可以独立于View进行测试,测试性更好。
  4. 开发模式
    • MVC:更倾向于传统的Web开发模式。
    • MVVM:在现代前端框架(如Vue.js、Angular)中更为常见。 **MVP(Model-View-Presenter)**是另一种软件架构模式,它与MVVM有些相似,但也有一些关键区别:
  • Model:与MVVM中的Model相同,代表数据和业务逻辑。
  • View:负责界面的展示,但不包含业务逻辑。
  • Presenter:作为Model和View之间的中介,它负责更新View并响应用户操作,但与MVVM的ViewModel不同,Presenter通常不实现双向数据绑定。 MVP与MVVM的区别
  1. 数据绑定
    • MVP:通常没有双向数据绑定,Presenter需要显式更新View。
    • MVVM:实现了双向数据绑定。
  2. 职责划分
    • MVP:Presenter负责更多的逻辑,包括响应用户操作和更新View。
    • MVVM:ViewModel主要负责数据转换和绑定,View负责响应用户操作。
  3. 灵活性
    • MVP:由于没有双向绑定,Presenter可以更灵活地控制View的更新。
    • MVVM:双向绑定提供了自动更新的便利,但也可能限制了一些灵活性。
  4. 适用场景
    • MVP:在一些需要更细粒度控制View更新的场景中更为适用。
    • MVVM:在需要快速开发、响应式界面更新的场景中更为适用。 总的来说,MVVM、MVC和MVP都是用于分离关注点、提高代码可维护性和可测试性的架构模式,选择哪种模式取决于具体的项目需求和开发团队的偏好。

19. 什么是点击劫持?如何防范点击劫持?

**点击劫持(Clickjacking)**是一种网络攻击手段,在这种攻击中,恶意用户通过在网页上设置透明的或难以察觉的层来诱使用户在不知情的情况下点击某个按钮或链接,从而执行非用户意图的操作。这种攻击通常利用了HTML中的<iframe>标签或者JavaScript来覆盖在目标网页上。

点击劫持的常见形式:

  1. 隐藏式点击劫持:在目标网页上覆盖一个透明的层,当用户点击时,实际上是在点击被覆盖的元素。
  2. 拖拽式点击劫持:诱使用户进行拖拽操作,从而在不知情的情况下执行特定操作。
  3. 模拟点击:使用JavaScript模拟用户的点击行为。

防范点击劫持的方法:

  1. X-Frame-Options头
    • 在服务器响应中添加X-Frame-Options头,可以控制是否允许浏览器中显示页面。有效值包括:
      • DENY:不允许页面被嵌入到任何<iframe>中。
      • SAMEORIGIN:只允许同源的页面嵌入。
      • ALLOW-FROM uri:只允许指定来源的页面嵌入。
    X-Frame-Options: DENY
    
  2. Content Security Policy (CSP)
    • 使用CSP的frame-ancestors指令来指定允许嵌入当前页面的来源。
    Content-Security-Policy: frame-ancestors 'none';
    
  3. JavaScript防御
    • 使用JavaScript检测当前页面是否被嵌入到<iframe>中,并采取相应措施。
    if (top.location != self.location) {
        top.location = self.location;
    }
    
  4. 用户教育
    • 教育用户注意不明链接和可疑的网页行为,不轻易点击不可信的页面元素。
  5. 浏览器插件
    • 使用浏览器插件来阻止点击劫持攻击。
  6. 服务器端验证
    • 在服务器端验证请求的合法性,例如检查HTTP Referer头。
  7. 禁用或限制<iframe>
    • 在网页中禁用或限制<iframe>的使用,特别是来自不同源的<iframe>。 通过结合这些方法,可以有效地防范点击劫持攻击,保护用户不被恶意利用。

20. 什么是 Samesite Cookie 属性?

Samesite Cookie 属性是一种用于增强Cookie安全性的属性,它旨在防止跨站请求伪造(CSRF)攻击。Samesite属性允许开发者指定Cookie是否应与跨站请求一起发送,从而限制Cookie的发送范围。 Samesite属性有三个可能的值:

  1. Strict
    • 如果设置為Strict,Cookie將只在同站请求(即请求的域名与当前页面的域名完全相同)时发送。这可以最大程度地防止CSRF攻击,但可能会影响一些正常的功能,如第三方登录。
    Set-Cookie: sessionId=abc123; SameSite=Strict
    
  2. Lax
    • Lax模式是默认值(如果未显式设置Samesite属性,大多数浏览器会将其视为Lax)。在这种模式下,Cookie会在同站请求时发送,同时也会在安全的跨站请求(如链接点击和GET表单提交)时发送。这平衡了安全性和功能性。
    Set-Cookie: sessionId=abc123; SameSite=Lax
    
  3. None
    • 如果设置为None,Cookie将在所有请求中发送,无论是否跨站。但是,为了安全起见,当设置为None时,必须同时设置Secure属性,确保Cookie只能通过HTTPS传输。
    Set-Cookie: sessionId=abc123; SameSite=None; Secure
    

使用Samesite属性的好处:

  • 防止CSRF攻击:通过限制Cookie的发送范围,可以减少CSRF攻击的风险。
  • 提高用户体验:在Lax模式下,允许在一些安全的跨站场景下保持用户登录状态,而不影响用户体验。
  • 灵活性:开发者可以根据应用的需求选择合适的Samesite值。

注意事项:

  • 在使用Samesite属性时,需要仔细考虑其对应用功能的影响,特别是当涉及到第三方集成时。
  • 对于需要跨站共享Cookie的场景,应确保使用适当的Samesite值(如Lax或None)。
  • 当设置为None时,务必同时设置Secure属性,以确保Cookie的安全传输。 通过合理使用Samesite Cookie属性,可以有效地提高Web应用的安全性。

21. toPrecision 和 toFixed 和 Math.round 有什么区别?

toPrecisiontoFixedMath.round 都是 JavaScript 中用于数字格式化和四舍五入的方法,但它们在功能和使用场景上有所区别:

1. toPrecision()

  • 定义toPrecision() 方法用于将数字转换为指定位数的字符串表示形式。
  • 参数:接受一个参数,表示要保留的有效数字位数。
  • 返回值:返回一个字符串,如果小数点后有多余的零,这些零也会被保留。
  • 用法:当需要控制数字的总有效位数时使用。
var num = 123.456;
console.log(num.toPrecision(3)); // "123"
console.log(num.toPrecision(4)); // "123.5"
console.log(num.toPrecision(5)); // "123.46"

2. toFixed()

  • 定义toFixed() 方法用于将数字转换为指定位数的小数表示形式的字符串。
  • 参数:接受一个参数,表示要保留的小数位数。
  • 返回值:返回一个字符串,如果小数点后有多余的零,这些零也会被保留。
  • 用法:当需要控制小数点后的位数时使用,常用于货币计算。
var num = 123.456;
console.log(num.toFixed(2)); // "123.46"
console.log(num.toFixed(3)); // "123.456"
console.log(num.toFixed(4)); // "123.4560"

3. Math.round()

  • 定义Math.round() 方法用于将数字四舍五入到最接近的整数。
  • 参数:接受一个数字参数。
  • 返回值:返回一个数字,四舍五入后的整数。
  • 用法:当需要将数字四舍五入到最近的整数时使用。
var num = 123.456;
console.log(Math.round(num)); // 123
var num2 = 123.567;
console.log(Math.round(num2)); // 124

区别总结:

  • 目的不同toPrecision 控制总有效位数,toFixed 控制小数位数,Math.round 四舍五入到整数。
  • 返回类型不同toPrecisiontoFixed 返回字符串,Math.round 返回数字。
  • 用法不同toPrecisiontoFixed 用于格式化显示,Math.round 用于数学计算。 在选择使用哪个方法时,应根据具体的需求来决定。如果需要格式化数字以显示特定的小数位数或有效位数,应使用 toFixedtoPrecision。如果需要进行数学上的四舍五入操作,应使用 Math.round

22. 原码、反码和补码分别是什么?

原码、反码和补码是计算机中用于表示有符号整数的三种编码方式。它们的主要目的是为了方便计算机进行算术运算,特别是减法运算。下面分别解释这三种编码方式:

1. 原码(Sign-Magnitude Representation)

  • 定义:原码是最直观的表示有符号整数的方法,最高位表示符号位(0为正,1为负),其余位表示数值的绝对值。
  • 特点:简单直观,但进行加减运算时需要单独处理符号位。 示例
  • 十进制数 +5 的原码(假设用4位二进制表示):0101
  • 十进制数 -5 的原码(假设用4位二进制表示):1101

2. 反码(One's Complement)

  • 定义:反码是为了解决原码加减运算的复杂性而引入的。正数的反码与其原码相同;负数的反码是其原码除符号位外其他位取反(0变1,1变0)。
  • 特点:解决了原码加减运算的部分问题,但仍然存在两个零的表示(正零和负零)。 示例
  • 十进制数 +5 的反码(假设用4位二进制表示):0101(与原码相同)
  • 十进制数 -5 的反码(假设用4位二进制表示):1010(原码1101除符号位外取反)

3. 补码(Two's Complement)

  • 定义:补码是现代计算机中广泛使用的表示有符号整数的方法。正数的补码与其原码和反码相同;负数的补码是其反码加1。
  • 特点:解决了反码中存在的两个零的问题,使得加减运算更加统一和简单,不需要单独处理符号位。 示例
  • 十进制数 +5 的补码(假设用4位二进制表示):0101(与原码和反码相同)
  • 十进制数 -5 的补码(假设用4位二进制表示):1011(反码1010加1)

总结:

  • 原码:直观但运算复杂。
  • 反码:简化了部分运算,但存在两个零的表示。
  • 补码:统一了零的表示,简化了加减运算,是现代计算机中使用的标准表示方法。 在计算机中,补码的使用使得加减运算可以统一处理,不需要考虑数的正负,从而提高了运算效率。例如,减去一个数等同于加上它的补码,这样就可以使用加法器来实现减法运算。

23. Unicode 和 UTF-8 之间有什么关系?

Unicode和UTF-8都是用于文本编码的标准,它们之间的关系可以从以下几个方面来理解:

1. Unicode

  • 定义:Unicode是一种字符集标准,它为世界上大多数语言的每个字符分配了一个唯一的数值,称为代码点(Code Point)。代码点通常用"U+" followed by a hexadecimal number表示,例如,字母"A"的Unicode代码点是U+0041。
  • 目的:统一全世界所有语言的字符编码,解决多语言环境中字符编码的冲突问题。
  • 范围:Unicode可以表示超过110万个字符,包括现代语言、古代语言、符号、表情等。

2. UTF-8

  • 定义:UTF-8是一种针对Unicode的可变长度字符编码,也是一种前缀码。它可以用来表示Unicode标准中的任何字符,且其编码的第一字节仍与ASCII兼容,使得原来处理ASCII字符的软件无须或只进行少部分修改后,便可继续使用。
  • 特点
    • 可变长度:UTF-8使用1到4个字节来表示一个字符,根据字符的Unicode代码点不同,所需的字节数也不同。
    • 兼容ASCII:UTF-8编码的前128个字符(U+0000到U+007F)与ASCII编码完全相同,都是一个字节。
    • 无字节序问题:由于UTF-8的多字节表示中,高位字节总是以特定的位模式开始,因此不会出现字节序(Big-Endian或Little-Endian)的问题。

关系

  • 编码与字符集:Unicode是一个字符集,它定义了字符与数值(代码点)的对应关系;而UTF-8是一种编码方式,它定义了如何将Unicode代码点转换为字节序列。
  • 实现方式:UTF-8是Unicode的一种实现方式,也是最常用的一种。除了UTF-8,还有其他实现Unicode的编码方式,如UTF-16、UTF-32等。
  • 使用场景:UTF-8由于其兼容性和高效性,被广泛应用于网页、电子邮件、文本文件等场合。

总结

Unicode定义了字符的全球唯一编码,而UTF-8是一种将这些编码转换为字节序列的编码方式。UTF-8的设计使得它既兼容ASCII,又能够高效地表示任何Unicode字符,因此在现代计算机系统中得到了广泛的应用。

24. 什么是 Polyfill ?

Polyfill(多填充)是一个术语,用于描述一段代码(或插件),它实现了现代浏览器支持的原生API,但能够在旧版浏览器中运行。Polyfill的目的是为了使旧版浏览器能够支持那些它们原本不支持的新特性,从而实现跨浏览器的兼容性。

Polyfill的工作原理:

  1. 检测特性:Polyfill首先会检测当前浏览器是否支持某个特定的API或特性。
  2. 填充缺失:如果检测到浏览器不支持该特性,Polyfill会“填充”这个缺失,即实现一个类似的API或特性,使得开发者可以像在使用现代浏览器一样使用这些API。

Polyfill的例子:

  • Promise:在现代浏览器中,Promise是原生支持的,但在旧版浏览器中可能不支持。一个Promise的Polyfill会实现Promise的功能,使得开发者可以在旧版浏览器中使用Promise。
  • Fetch API:Fetch API是现代浏览器提供的用于网络请求的API,旧版浏览器可能不支持。一个Fetch API的Polyfill会模拟这个API的行为,使得在旧版浏览器中也能使用Fetch进行网络请求。

Polyfill与 Shim 和 Stub 的区别:

  • Shim:Shim也是一个兼容性代码,但它不仅仅限于API的兼容,还可以包括语法兼容等。Shim可能会改变或扩展原有API的行为。
  • Stub:Stub通常用于测试,它模拟了一个组件或模块的行为,但不会实现完整的功能。

使用Polyfill的注意事项:

  • 性能影响:引入Polyfill可能会增加页面的加载时间和运行时的性能负担,因为需要在旧版浏览器中额外执行代码。
  • 选择合适的Polyfill:根据项目需求和目标浏览器的支持情况,选择合适的Polyfill,避免引入不必要的代码。
  • 更新和维护:随着浏览器的发展,一些Polyfill可能会变得不再必要,需要定期检查和更新项目中使用的Polyfill。 Polyfill是前端开发中实现兼容性的一种重要手段,它帮助开发者在不放弃使用新特性的同时,确保旧版浏览器的用户也能获得良好的体验。

25. 怎么检测浏览器版本?

在JavaScript中,有多种方法可以检测浏览器版本。以下是一些常用的方法:

1. 使用navigator.userAgent

navigator.userAgent是一个字符串,包含了浏览器版本信息。可以通过正则表达式来解析这个字符串以获取浏览器版本。

var userAgent = navigator.userAgent;
var browserName, browserVersion;
if (userAgent.match(/Chrome\/(\d+)/)) {
  browserName = "Chrome";
  browserVersion = userAgent.match(/Chrome\/(\d+)/)[1];
} else if (userAgent.match(/Firefox\/(\d+)/)) {
  browserName = "Firefox";
  browserVersion = userAgent.match(/Firefox\/(\d+)/)[1];
} else if (userAgent.match(/Safari\/(\d+)/)) && !userAgent.match(/Chrome\/(\d+)/)) {
  browserName = "Safari";
  browserVersion = userAgent.match(/Version\/(\d+)/)[1];
} else if (userAgent.match(/MSIE (\d+)/) || userAgent.match(/Trident\/.*rv:(\d+)/)) {
  browserName = "IE";
  browserVersion = userAgent.match(/MSIE (\d+)/) ? userAgent.match(/MSIE (\d+)/)[1] : userAgent.match(/Trident\/.*rv:(\d+)/)[1];
}
console.log("Browser Name: " + browserName);
console.log("Browser Version: " + browserVersion);

2. 使用navigator.appVersion

navigator.appVersion也包含了浏览器版本信息。

var appVersion = navigator.appVersion;
console.log(appVersion);

3. 使用现代API

现代浏览器提供了更标准的API来检测浏览器特性,而不是直接检测版本。

if ('serviceWorker' in navigator) {
  console.log('Service Worker is supported');
} else {
  console.log('Service Worker is not supported');
}

注意事项:

  • 避免过度依赖浏览器检测:尽量使用特性检测而不是浏览器检测,因为特性检测更为可靠和未来 proof。
  • 用户代理字符串可被修改:用户可以修改用户代理字符串,所以这种方法不是100%可靠。
  • 兼容性:旧版浏览器可能不支持某些现代API,所以需要结合使用多种方法。

示例:检测Chrome版本

var isChrome = /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor);
if (isChrome) {
  var chromeVersion = navigator.userAgent.match(/Chrome\/(\d+)/)[1];
  console.log("Chrome Version: " + chromeVersion);
} else {
  console.log("Not Chrome");
}

通过这些方法,可以有效地检测不同浏览器的版本,并根据需要为不同版本的浏览器提供相应的兼容性处理。

26. 什么是“前端路由”?什么时候适合使用“前端路由”?“前端路由”有哪些优点和缺点?

前端路由是一种在单页应用(SPA)中实现页面导航的技术,它允许用户在不重新加载页面的情况下切换视图。前端路由通过监听URL的变化,并映射到相应的组件或视图来实现。

什么时候适合使用前端路由?

  1. 单页应用(SPA):在SPA中,前端路由是核心组成部分,用于实现不同页面之间的导航。
  2. 提升用户体验:希望提供更流畅、更快的导航体验,无需每次都重新加载页面。
  3. 前后端分离:在后端仅提供API,前端负责视图渲染的场景。
  4. 复杂的前端应用:对于具有复杂交互和多个视图的前端应用,前端路由有助于管理这些视图。

前端路由的优点:

  1. 快速导航:无需重新加载页面,导航更加快速和流畅。
  2. 更好的用户体验:提供类似桌面应用的体验,减少页面闪烁和等待时间。
  3. 前端控制:前端完全控制路由,更容易实现复杂的导航逻辑。
  4. 状态保持:可以在不丢失当前状态的情况下切换视图。
  5. SEO优化:通过合理的配置,可以改善单页应用的SEO问题。

前端路由的缺点:

  1. 初次加载时间较长:因为需要加载整个应用的前端资源,初次加载时间可能较长。
  2. SEO挑战:虽然可以进行优化,但单页应用的SEO通常比多页应用更复杂。
  3. 浏览器兼容性:某些旧浏览器可能不支持或不完全支持前端路由所需的技术。
  4. 复杂度增加:前端路由增加了应用的复杂度,需要更多的前端开发和维护工作。
  5. 历史管理:需要自己管理浏览器的历史记录,以确保前进和后退按钮能正常工作。

常见的前端路由实现方式:

  1. Hash模式:使用URL的hash部分(#后面的内容)来作为路由地址,例如http://example.com/#/home
  2. History模式:利用HTML5的History API来改变URL,而不重新加载页面,例如http://example.com/home

示例:

Hash模式

window.addEventListener('hashchange', function() {
  var hash = window.location.hash.substring(1);
  switch (hash) {
    case 'home':
      // 显示首页
      break;
    case 'about':
      // 显示关于页面
      break;
    default:
      // 显示404页面
      break;
  }
});

History模式

window.addEventListener('popstate', function(event) {
  var path = event.state.path;
  switch (path) {
    case '/home':
      // 显示首页
      break;
    case '/about':
      // 显示关于页面
      break;
    default:
      // 显示404页面
      break;
  }
});
// 导航到新页面
history.pushState({ path: '/home' }, '', '/home');

在实际应用中,通常会使用前端框架(如React、Vue、Angular等)提供的路由库(如React Router、Vue Router、Angular Router等)来实现前端路由,这些库提供了更高级的功能和更好的开发体验。

27. 什么是点击穿透,怎么解决?

点击穿透是移动端Web开发中常见的一个问题,通常发生在以下场景:

  1. 页面上有一个上层元素(如弹出层、遮罩层)覆盖在另一个下层元素(如按钮)上。
  2. 用户点击上层元素,触发上层元素的点击事件(如关闭弹出层)。
  3. 上层元素消失后,下层元素的点击事件也被触发,仿佛点击穿透了上层元素。 这种现象主要是因为移动端浏览器的点击事件有延迟(大约300ms),这是为了判断用户是否在进行双击操作。当上层元素消失后,浏览器认为用户的点击操作仍然有效,于是触发了下层元素的点击事件。

解决点击穿透的方法:

  1. 阻止上层元素的默认事件: 在上层元素的点击事件处理函数中,调用event.preventDefault()可以阻止默认事件,从而避免点击穿透。
    document.getElementById('overlay').addEventListener('touchend', function(event) {
      event.preventDefault();
      // 关闭弹出层
    });
    
  2. 使用pointer-events: none: 在上层元素消失后,可以暂时将下层元素的pointer-events属性设置为none,阻止点击事件传递。
    .underlying-element {
      pointer-events: none;
    }
    
    document.getElementById('overlay').addEventListener('touchend', function() {
      // 关闭弹出层
      document.getElementById('underlying-element').style.pointerEvents = 'none';
      setTimeout(function() {
        document.getElementById('underlying-element').style.pointerEvents = 'auto';
      }, 300);
    });
    
  3. 延迟上层元素的消失: 可以在上层元素消失后,延迟一段时间再允许下层元素的点击事件,避开点击延迟。
    document.getElementById('overlay').addEventListener('touchend', function() {
      // 关闭弹出层
      setTimeout(function() {
        // 实际关闭弹出层的代码
      }, 300);
    });
    
  4. 使用fastclickfastclick是一个第三方库,可以消除移动端浏览器的点击延迟,从而解决点击穿透问题。
    FastClick.attach(document.body);
    
  5. 修改下层元素的事件监听: 将下层元素的点击事件改为触摸事件(如touchstarttouchend),这些事件没有延迟。
    document.getElementById('underlying-element').addEventListener('touchend', function() {
      // 处理点击事件
    });
    

选择哪种方法取决于具体的项目需求和场景。在实际开发中,可能需要结合多种方法来彻底解决点击穿透问题。

28. 移动端的点击事件的有延迟,时间是多久,为什么会有? 怎么解决这个延时?

移动端点击事件延迟的时间通常是300毫秒左右。这个延迟的主要原因是为了判断用户是否在进行双击操作。在移动设备上,用户可以通过双击来放大或缩小页面,浏览器需要时间来判断用户的第一次点击是否是双击操作的一部分,因此引入了这个延迟。

为什么会有这个延迟?

  1. 双击检测:如上所述,浏览器需要时间来判断用户的点击操作是否是双击的一部分。
  2. 兼容性:为了保持与桌面浏览器的兼容性,移动端浏览器也引入了类似的延迟。

如何解决这个延迟?

  1. 使用touchstarttouchend事件: 这些触摸事件没有300毫秒的延迟,可以用来替代点击事件。
    element.addEventListener('touchstart', function(event) {
      // 处理触摸开始事件
    });
    element.addEventListener('touchend', function(event) {
      // 处理触摸结束事件
    });
    
  2. 使用fastclickfastclick是一个第三方库,可以消除移动端浏览器的点击延迟。
    // 引入fastclick库
    <script src="path/to/fastclick.js"></script>
    // 初始化fastclick
    FastClick.attach(document.body);
    
  3. 自定义事件处理: 可以通过监听touchend事件,并在事件处理函数中手动触发点击事件,从而绕过浏览器的默认延迟。
    element.addEventListener('touchend', function(event) {
      // 触发点击事件
      event.preventDefault();
      var clickEvent = new MouseEvent('click');
      element.dispatchEvent(clickEvent);
    });
    
  4. CSS媒体查询: 通过CSS媒体查询为移动设备设置特定的样式,以减少点击区域的面积,从而减少误触。
    @media (pointer: coarse) {
      /* 为移动设备设置特定的样式 */
      button {
        padding: 10px 20px;
      }
    }
    
  5. 优化用户体验: 通过设计上的一些调整,如增加点击区域、使用更大的按钮等,来减少用户对点击延迟的感知。 选择哪种方法取决于具体的项目需求和场景。在实际开发中,可能需要结合多种方法来彻底解决点击延迟问题。