1. 说一下css盒模型
内容区域(Content) 、内边距(Padding) 、边框(Border) 、外边距(Margin)
标准盒模型:默认情况下,CSS使用标准盒模型,这意味着 width 和 height 仅包括内容区域,不包括内 边距和边框。 box-sizing: content-box;
IE盒模型:IE盒模型(或称为怪异盒模型)包括内容、内边距和边框。 box-sizing: border-box;
2. Css选择器的优先级
内联样式:内联样式的优先级最高,它直接写在HTML元素的style属性中。
ID选择器:ID选择器的优先级次高。
类选择器、伪类选择器、属性选择器:这些选择器的优先级比ID选择器低。
元素选择器、伪元素选择器:这些选择器的优先级最低。
通配符选择器、组合器、子选择器:这些通常有最低的优先级。
CSS优先级由四个部分组成(从左到右):内联样式、ID选择器、类选择器、元素选择器。优先级以四个级别的数字表示,例如0,1,0,0。越左边的数字越大,优先级越高。
3. 隐藏元素的方法
display: none;
元素被完全隐藏,不占据任何空间。
适用于完全移除元素,用户看不到它,浏览器也不为其分配空间。
visibility: hidden;
元素被隐藏,但仍然占据空间。
适用于保持页面布局不变,只隐藏元素内容。
opacity: 0;
元素完全透明,但仍然占据空间并响应事件。
适用于需要隐藏元素但仍保持其交互功能,例如悬停效果或动画。
position: absolute; 和 left: -9999px;
将元素移出可视区域,仍然保留在DOM中。
适用于需要将元素移出视线但保留其存在,例如无障碍功能。
transform: scale(0);
通过缩放隐藏元素,使其不可见,但仍然占据空间。
适用于动画效果,使元素在隐藏和显示之间平滑过渡。
height: 0; width: 0; overflow: hidden;
通过设置高度和宽度为0隐藏元素,并隐藏溢出的内容。
适用于隐藏元素但需要保持其在DOM中的存在,例如无障碍功能
4. px、em、rem的区别是什么
px、em、rem的区别是什么
在CSS中,px、em和rem是三种常用的单位,用于定义元素的尺寸和间距。它们各有特点和应用场景。以下是它们的区别:
px(像素)绝对单位:px 是一个绝对单位,它表示屏幕上固定的像素数。这意味着无论页面的字体大小或其他设置如何,px 的值总是固定的。
应用场景:px 常用于需要精确控制元素尺寸的场合,如边框、图片大小等。
优点:易于理解和使用,适用于固定布局。
缺点:在响应式设计中灵活性较差。
em****相对单位:em 是一个相对单位,它相对于当前元素的字体大小。例如,如果当前元素的字体大小为16px,那么1em 就等于16px。
应用场景:em 常用于设置字体大小、内边距、外边距等,能够实现相对尺寸的调整。
优点:在嵌套元素中,em 可以相对于父元素的字体大小进行调整,具有一定的灵活性。
缺点:在深层次嵌套时,计算可能会变得复杂。
rem****根相对单位:rem 是一个相对单位,它相对于根元素(<html>)的字体大小。例如,如果根元素的字体大小为16px,那么1rem 就等于16px。
应用场景:rem 常用于全局设置的场合,如全局的字体大小、内边距、外边距等,能够实现相对尺寸的统一调整。
优点:在整个文档中,rem 相对于根元素的字体大小,不会受其他元素的影响,计算简单且一致性高。
缺点:需要在根元素上进行统一设置,初始配置稍微复杂。
5. 重绘和重排
重绘发生在元素的外观改变但不影响布局的情况下。比如颜色、背景、边框的变化,这些变化只会影响元素的外观,不会改变元素在页面上的位置和尺寸。
触发条件:
改变元素的颜色(例如 color、background-color)。
改变元素的透明度(例如 opacity)。
改变边框的颜色(例如 border-color)。
重排(又称回流)发生在元素的布局改变时,浏览器需要重新计算元素在页面上的位置和大小。这些变化会影响页面布局的其他部分,因此浏览器需要重新计算整个页面的布局。
触发条件:
添加或删除DOM元素。
改变元素的尺寸(例如 width、height)。
改变元素的位置(例如 position、top、left)。
改变元素的字体大小(例如 font-size)。
改变元素的边距(例如 margin、padding)。
重排的代价要比重绘高,因为重排不仅要重新绘制元素,还需要重新计算布局,甚至可能会影响整个页面的布局。因此,在性能优化中,应该尽量减少重排操作。
6. 元素水平垂直居中的方法
将元素水平和垂直居中在网页上可以通过多种方法实现,以下是几种常见的方法:
- 使用Flexbox:
.container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh; /* 使容器高度占满整个视窗 */
}
- 使用Grid布局:
.container {
display: grid;
place-items: center;
height: 100vh; /* 使容器高度占满整个视窗 */
}
- 使用定位方法(positioning):
.container {
position: relative;
height: 100vh; /* 使容器高度占满整个视窗 */
}
.element {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
- 使用表格方法:
.container {
display: table;
width: 100%;
height: 100vh; /* 使容器高度占满整个视窗 */
}
.element {
display: table-cell;
vertical-align: middle;
text-align: center;
7. 如何实现自适应布局
实现自适应布局(响应式设计)意味着网页能够根据不同的设备和屏幕大小自动调整布局。以下是几种实现自适应布局的方法:
- 使用媒体查询 (Media Queries): 媒体查询允许你根据不同的屏幕尺寸应用不同的CSS规则。
/* 小于600px的屏幕 */
@media (max-width: 600px) {
.container {
flex-direction: column;
}
}
/* 大于600px的屏幕 */
@media (min-width: 601px) {
.container {
flex-direction: row;
}
}
- 使用百分比和相对单位: 使用百分比、
em和rem等相对单位来代替固定单位(如px),可以让元素更好地适应不同的屏幕尺寸。
.container {
width: 80%; /* 使用百分比 */
padding: 2em; /* 使用em */
}
- Flexbox 和 Grid 布局: Flexbox和Grid布局都非常适合创建响应式布局。
/* 使用Flexbox */
.container {
display: flex;
flex-wrap: wrap;
}
.item {
flex: 1 1 200px; /* 元素最小宽度为200px */
}
/* 使用Grid */
.container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
}
- 响应式图片: 使用
srcset属性来指定不同分辨率下的图片。
<img src="small.jpg" srcset="small.jpg 600w, medium.jpg 1200w, large.jpg 1800w" alt="Responsive Image">
- 使用CSS框架: 使用Bootstrap、Tailwind CSS等CSS框架,它们内置了许多响应式设计的类和功能,可以简化自适应布局的实现。
<div class="container">
<div class="row">
<div class="col-md-6">Column 1</div>
<div class="col-md-6">Column 2</div>
</div>
8. 清除浮动的方法有哪些?伪元素清除的原理是什么?
1. 使用清除元素(Clearing Element)
在浮动元素后添加一个清除浮动的元素,如带有clear: both;样式的<div>。
<div class="clearfix"></div>
<style>
.clearfix {
clear: both;
}
</style>
2. 使用父元素设置overflow属性
为包含浮动元素的父元素设置overflow属性(如hidden、auto等),强制父元素包含其内部的浮动元素。
<style>
.container {
overflow: hidden; /* 或 overflow: auto; */
}
</style>
3. 使用伪元素清除浮动
通过CSS伪元素::after在父元素的末尾添加一个隐藏的清除浮动元素,这是最常用且最优雅的方法。
<div class="container">
<div class="float-child"></div>
<div class="float-child"></div>
</div>
<style>
.container::after {
content: "";
display: table;
clear: both;
}
.float-child {
float: left;
width: 50%;
}
</style>
伪元素清除浮动的原理:
- content: "":这是伪元素的内容,空字符串表示没有实际内容。
- display: table:伪元素被设置为
table显示模式,这使其成为一个块级元素,可以正确处理清除浮动。 - clear: both:伪元素被设置为清除左右两侧的浮动,从而确保父元素能够包含其内部的浮动子元素。
9. BFC是什么?可以解决什么问题?
BFC(Block Formatting Context)是前端开发中的一个概念,它是一种CSS布局机制。当元素进入BFC时,它会创建一个新的布局块,这个布局块独立于外部的布局。这意味着BFC内的元素不会影响外部元素的布局,反之亦然。
BFC可以解决一些常见的布局问题,例如:
-
清除浮动元素:通过将浮动元素包裹在BFC内,可以清除浮动元素对外部布局的影响。
-
避免父元素高度塌陷:当父元素中的子元素使用了BFC时,父元素会包含所有BFC内的内容,从而避免高度塌陷问题。
-
控制垂直方向的间距:通过使用BFC,可以更好地控制垂直方向的间距,使元素之间的间隔更为一致。
10. css哪些属性可以继承
文字相关属性、列表相关属性 、表格相关属性
11. 有没有用过预处理器
Sass (Syntactically Awesome Stylesheets):
- 变量:允许定义变量,简化重复使用的值。
- 嵌套:支持嵌套规则,使CSS结构更加清晰。
- Mixin:通过定义混入(Mixin),重用样式规则。
- 继承:使用继承(@extend)简化类选择器的重复。
LESS (Leaner Style Sheets):
- 变量:与Sass类似,可以定义变量。
- 嵌套:支持嵌套,使CSS更具层次感。
- 混入:支持参数化和非参数化的混入。
- 运算:在样式中进行数学运算(如加、减、乘、除)。
12. HTML5有哪些新特性
语义化标签
- :表示一篇独立的内容。
- :表示文档中的一个部分或章节。
- :表示内容或章节的头部。
- :表示内容或章节的尾部。
- :表示导航链接的部分。
多媒体支持
- :用于嵌入音频内容。
- :用于嵌入视频内容,支持多种格式。
- :用于指定不同的媒体文件来源。
图形和效果
- :用于绘制图形,通过JavaScript来绘制2D图像。
- :支持可缩放的矢量图形。
表单增强
- 新的输入类型:如
<input type="date">、<input type="email">、<input type="url">等。 - 属性和元素:如
placeholder、required、autocomplete等。
本地存储
- localStorage:用于在客户端存储数据,数据不会随浏览器关闭而删除。
- sessionStorage:用于在客户端存储数据,数据会在浏览器会话结束时删除。
离线和存储
- :支持创建离线Web应用。
- :用于定义Web应用的离线资源列表。
拖放(Drag and Drop)
- 支持原生的拖放操作,使得拖放功能的实现更为简单。
新的API
- Geolocation API:获取用户的地理位置信息。
- Web Workers:用于在后台线程运行JavaScript,提高页面性能。
- WebSockets:实现全双工通信,适用于实时数据应用。
13. Css3有哪些新特性
选择器增强
- 属性选择器:根据元素的属性来选择元素。
- 伪类:如
:nth-child()、:nth-of-type()、:not()等,选择更复杂的元素。
布局模块
- Flexbox:用于创建灵活的布局,简化了复杂布局的实现。
- Grid:强大的二维布局系统,使得布局设计更加精细和结构化。
边框和背景
- border-radius:创建圆角效果。
- box-shadow:添加阴影效果。
- background-clip、background-origin:增强背景图像的控制。
- multiple backgrounds:支持多个背景图像叠加。
文本效果
- text-shadow:添加文本阴影效果。
- word-wrap:处理长单词的换行问题。
- text-overflow:控制文本溢出时的显示方式。
变形和动画
- Transforms:如
transform属性,支持旋转、缩放、倾斜等变形效果。 - Transitions:如
transition属性,用于实现平滑的过渡效果。 - Animations:如
@keyframes规则,定义关键帧动画。
多列布局
- columns:如
column-count、column-gap等属性,支持多列布局。
媒体查询
- Media Queries:根据设备特性(如屏幕宽度)应用不同的CSS规则,实现响应式设计。
Web字体
- @font-face:允许引入自定义字体,使用网络字体。
用户界面
- resize:允许用户调整元素的大小。
- box-sizing:控制盒模型的计算方式。
14. JS的内置对象
基础对象
- Object:JavaScript中的基本对象,所有其他对象都继承自它。
- Function:表示一个函数。
- Boolean:表示布尔值(true或false)。
- Symbol:表示独特且不可变的数据类型,通常用作对象属性的键。
数值与日期对象
- Number:表示数值(整数或浮点数)。
- BigInt:表示任意精度的整数。
- Math:提供数学常数和函数(如
Math.PI、Math.random())。 - Date:表示日期和时间,允许进行日期时间的操作。
字符串与正则表达式对象
- String:表示字符串。
- RegExp:表示正则表达式,用于模式匹配。
数据结构对象
- Array:表示数组,允许存储有序的集合。
- Map:表示键值对集合,允许任何类型的键。
- Set:表示值的集合,所有值都是唯一的。
- WeakMap:表示弱键值对集合,键必须是对象,键的引用不会阻止垃圾回收。
- WeakSet:表示弱集合,只能包含对象,值的引用不会阻止垃圾回收。
错误对象
- Error:表示一个错误对象,其他错误对象都继承自它。
- EvalError:表示与
eval()函数有关的错误。 - RangeError:表示数值超出其合法范围的错误。
- ReferenceError:表示无效引用的错误。
- SyntaxError:表示语法错误。
- TypeError:表示变量或参数不是预期类型的错误。
- URIError:表示URI处理函数中的错误。
- EvalError:表示与
全局对象
- Global对象:全局对象,例如在浏览器环境中是
window,在Node.js中是global。
15. 说说操作数组的方法,哪些会改变原数组?
1. 会改变原数组的方法
-
push():在数组末尾添加一个或多个元素,并返回新的长度。
let arr = [1, 2, 3]; arr.push(4); // arr变为 [1, 2, 3, 4] -
pop():删除数组末尾的一个元素,并返回该元素。
javascript
let arr = [1, 2, 3]; arr.pop(); // arr变为 [1, 2] -
shift():删除数组开头的一个元素,并返回该元素。
javascript
let arr = [1, 2, 3]; arr.shift(); // arr变为 [2, 3] -
unshift():在数组开头添加一个或多个元素,并返回新的长度。
javascript
let arr = [1, 2, 3]; arr.unshift(0); // arr变为 [0, 1, 2, 3] -
splice():从数组中添加或删除元素。
javascript
let arr = [1, 2, 3, 4]; arr.splice(1, 2); // 删除索引1开始的两个元素,arr变为 [1, 4] arr.splice(1, 0, 2, 3); // 在索引1处添加元素,arr变为 [1, 2, 3, 4] -
sort():对数组元素进行排序,并返回排序后的数组。
javascript
let arr = [3, 1, 2]; arr.sort(); // arr变为 [1, 2, 3] -
reverse():颠倒数组中元素的顺序。
javascript
let arr = [1, 2, 3]; arr.reverse(); // arr变为 [3, 2, 1]
2. 不会改变原数组的方法
-
concat():合并两个或多个数组,并返回新的数组。
javascript
let arr1 = [1, 2]; let arr2 = [3, 4]; let newArr = arr1.concat(arr2); // arr1保持不变,新数组为 [1, 2, 3, 4] -
slice():返回数组的一个子数组,不修改原数组。
javascript
let arr = [1, 2, 3, 4]; let newArr = arr.slice(1, 3); // arr保持不变,新数组为 [2, 3] -
map():返回一个新数组,数组中的每个元素是原数组元素调用提供的函数后的结果。
javascript
let arr = [1, 2, 3]; let newArr = arr.map(x => x * 2); // arr保持不变,新数组为 [2, 4, 6] -
filter():返回一个新数组,包含所有通过提供函数测试的元素。
javascript
let arr = [1, 2, 3, 4]; let newArr = arr.filter(x => x > 2); // arr保持不变,新数组为 [3, 4] -
reduce():对数组中的每个元素执行提供的函数,并返回单个累积值,不修改原数组。
let arr = [1, 2, 3, 4]; let sum = arr.reduce((acc, x) => acc + x, 0);
16. JS对数据类的检查方法
使用typeof操作符
typeof操作符可以返回一个表示操作数数据类型的字符串。常见的数据类型包括:undefined、boolean、number、string、object、function和symbol。
console.log(typeof undefined); // "undefined"
console.log(typeof true); // "boolean"
console.log(typeof 42); // "number"
console.log(typeof "Hello"); // "string"
console.log(typeof {}); // "object"
console.log(typeof function(){}); // "function"
console.log(typeof Symbol()); // "symbol"
使用instanceof操作符
instanceof操作符可以检测对象是否是某个构造函数的实例。
let arr = [];
console.log(arr instanceof Array); // true
console.log(arr instanceof Object); // true
使用Array.isArray()方法
Array.isArray()方法专门用于检测一个值是否为数组。
console.log(Array.isArray([])); // true
console.log(Array.isArray({})); // false
使用Object.prototype.toString.call()
这种方法可以更准确地检测各种对象的具体类型。返回值格式为"[object Type]"。
console.log(Object.prototype.toString.call([])); // "[object Array]"
console.log(Object.prototype.toString.call({})); // "[object Object]"
console.log(Object.prototype.toString.call(null)); // "[object Null]"
console.log(Object.prototype.toString.call(undefined)); // "[object Undefined]"
console.log(Object.prototype.toString.call(new Date())); // "[object Date]"
使用constructor属性
对象的constructor属性指向它的构造函数。
let arr = [];
console.log(arr.constructor === Array); // true
console.log({}.constructor === Object); // true
ES6中的其他方法
在ES6中,还引入了许多其他方法和类型检测工具:
-
Symbol:用于创建唯一标识符。
let sym = Symbol(); console.log(typeof sym); // "symbol" -
Set和Map:用于存储唯一值的集合和键值对的集合。
let set = new Set(); console.log(set instanceof Set); // true let map = new Map(); console.log(map instanceof Map); // true
17. 如何理解作用域和作用域链
作用域是指在代码中定义的变量和函数的可访问范围。 作用域链是指在当前作用域中查找变量时,JavaScript引擎按照从内到外的顺序,依次查找各个作用域,直到找到变量或到达全局作用域为止。如果在所有作用域中都没有找到变量,就会抛出ReferenceError错误。
18. 谈谈闭包,有什么特点,有什么用
特点
- 捕获外部变量:闭包可以捕获并保持其所在作用域中的变量,即使在该作用域结束后,变量仍然可以通过闭包访问。
- 私有变量和方法:闭包常用于创建私有变量和方法,保护这些变量和方法不被外部访问和修改。
- 持久化状态:闭包可以持久化状态,即使函数执行结束,闭包内部的变量和状态仍然存在。
用途
-
数据封装和隐藏:闭包可以用于封装数据,将变量和方法私有化,防止外部代码直接访问和修改。
function createCounter() { let count = 0; return { increment: function() { count++; console.log(count); }, getCount: function() { return count; } }; } const counter = createCounter(); counter.increment(); // 输出: 1 counter.increment(); // 输出: 2 console.log(counter.getCount()); // 输出: 2 -
模拟私有方法:通过闭包,可以创建私有方法,使得这些方法只能在闭包内访问,避免了全局命名空间的污染。
function createPerson(name) { let age = 30; function getAge() { return age; } return { getName: function() { return name; }, getAge: getAge }; } const person = createPerson("Alice"); console.log(person.getName()); // 输出: Alice console.log(person.getAge()); // 输出: 30 -
回调函数和事件处理:闭包在回调函数和事件处理器中非常有用,可以在回调函数中访问外部变量。
function setupClickHandler(element, message) { element.addEventListener('click', function() { console.log(message); }); } const button = document.querySelector('button'); setupClickHandler(button, "Button clicked!"); -
记忆化(Memoization):闭包可以用于记忆化,缓存函数的计算结果,提高性能。
function memoize(fn) { const cache = {}; return function(...args) { const key = JSON.stringify(args); if (cache[key]) { return cache[key]; } else { const result = fn(...args); cache[key] = result; return result; } }; } const fibonacci = memoize(function(n) { if (n <= 1) return n; return fibonacci(n - 1) + fibonacci(n - 2); }); console.log(fibonacci(10));
19. 内存泄露怎么理解
内存泄漏(Memory Leak)是指计算机程序在运行过程中,动态分配的内存无法释放或未正确释放,导致系统内存逐渐减少,最终可能导致程序崩溃或系统性能下降。内存泄漏的主要原因是程序没有正确管理内存资源,导致某些不再需要的内存块无法被回收。
内存泄漏的常见原因
-
全局变量:使用全局变量会导致内存无法及时释放,因为它们在程序运行期间始终存在。
var globalVar = []; function addItem(item) { globalVar.push(item); } -
闭包:闭包可以导致未使用的变量一直保存在内存中,导致内存泄漏。
function createClosure() { let bigData = new Array(1000).fill('big data'); return function() { console.log(bigData.length); }; } -
事件监听器:忘记移除不再需要的事件监听器,会导致内存泄漏。
function setupListener() { let element = document.getElementById('myElement'); element.addEventListener('click', function() { console.log('clicked'); }); } -
定时器和回调:未清除的不再使用的定时器或回调函数,也会导致内存泄漏。
let intervalId = setInterval(function() { console.log('interval'); }, 1000); // 忘记清除定时器 // clearInterval(intervalId);
如何避免内存泄漏
-
使用局部变量:尽量使用局部变量,避免不必要的全局变量。
-
合理使用闭包:谨慎使用闭包,确保在不再需要时释放闭包中的引用。
-
移除事件监听器:在不再需要时,及时移除事件监听器。
function setupListener() { let element = document.getElementById('myElement'); let handleClick = function() { console.log('clicked'); }; element.addEventListener('click', handleClick); // 移除事件监听器 element.removeEventListener('click', handleClick); } -
清除定时器和回调:在不再需要时,及时清除定时器和回调函数。
let intervalId = setInterval(function() { console.log('interval'); }, 1000); // 清除定时器 clearInterval(intervalId); -
工具和监测:使用内存分析工具(如Chrome开发者工具中的Performance和Memory工具)来监测和分析内存使用情况,识别和修复内存泄漏。
20. 什么是事件冒泡和事件捕获?
事件冒泡(Event Bubbling)和事件捕获(Event Capturing)是JavaScript事件模型中的两个阶段,它们决定了事件在DOM(文档对象模型)中传播的顺序。
事件冒泡(Event Bubbling)
在事件冒泡阶段,事件从目标元素开始,逐层向上传播到最外层的祖先元素(通常是document对象)。换句话说,事件首先在最内层的元素上触发,然后在父元素上触发,再到父元素的父元素,依此类推,直到到达document对象。
示例:
<!DOCTYPE html>
<html>
<head>
<title>Event Bubbling</title>
</head>
<body>
<div id="parent">
<button id="child">Click Me</button>
</div>
<script>
document.getElementById('parent').addEventListener('click', function() {
console.log('Parent element clicked');
});
document.getElementById('child').addEventListener('click', function() {
console.log('Child element clicked');
});
</script>
</body>
</html>
在上面的示例中,当点击按钮时,事件首先在child元素上触发,然后在parent元素上触发,控制台将输出:
Child element clicked
Parent element clicked
事件捕获(Event Capturing)
在事件捕获阶段,事件从最外层的祖先元素(通常是document对象)开始,逐层向下传播到目标元素。换句话说,事件首先在最外层的元素上触发,然后在子元素上触发,再到子元素的子元素,依此类推,直到到达目标元素。
示例:
<!DOCTYPE html>
<html>
<head>
<title>Event Capturing</title>
</head>
<body>
<div id="parent">
<button id="child">Click Me</button>
</div>
<script>
document.getElementById('parent').addEventListener('click', function() {
console.log('Parent element clicked');
}, true); // 事件捕获阶段
document.getElementById('child').addEventListener('click', function() {
console.log('Child element clicked');
}, true); // 事件捕获阶段
</script>
</body>
</html>
在上面的示例中,当点击按钮时,事件首先在parent元素上触发,然后在child元素上触发,控制台将输出:
Parent element clicked
Child element clicked
阶段顺序
在一个事件从触发到完成的过程中,它会经历三个阶段:
- 事件捕获阶段(Capturing Phase):事件从最外层元素开始向下传播。
- 目标阶段(Target Phase):事件在目标元素上触发。
- 事件冒泡阶段(Bubbling Phase):事件从目标元素向上传播到最外层元素。
21. 事件委托是什么
事件委托是什么
事件委托(Event Delegation)是一种处理事件的常见技术,通过利用事件冒泡机制,将事件处理程序添加到父级元素上,而不是为每个子元素单独添加处理程序。这样可以提高性能,简化代码,特别是在处理大量动态生成的元素时尤为有用。
原理
事件委托的原理是利用事件冒泡机制,当一个事件在某个元素上触发时,它会向上传播到父级元素,从而可以在父级元素上捕获到该事件。通过在父级元素上添加一个事件处理程序,可以监听并处理其子元素上触发的事件。
优点
- 减少内存消耗:只需要在父级元素上添加一个事件处理程序,而不是为每个子元素都添加处理程序,可以减少内存消耗。
- 动态元素处理:可以方便地处理动态添加或删除的子元素,而无需重新绑定事件处理程序。
- 简化代码:代码更简洁,易于维护和管理。
示例
假设有一个包含多个按钮的列表,每个按钮点击时都会触发一个事件。传统方式是为每个按钮单独添加事件处理程序:
<!DOCTYPE html>
<html>
<head>
<title>Event Delegation</title>
</head>
<body>
<ul id="list">
<li><button class="item">Button 1</button></li>
<li><button class="item">Button 2</button></li>
<li><button class="item">Button 3</button></li>
</ul>
<script>
const buttons = document.querySelectorAll('.item');
buttons.forEach(button => {
button.addEventListener('click', function() {
console.log(this.textContent);
});
});
</script>
</body>
</html>
使用事件委托的方式,只需在父级元素<ul>上添加一个事件处理程序:
<!DOCTYPE html>
<html>
<head>
<title>Event Delegation</title>
</head>
<body>
<ul id="list">
<li><button class="item">Button 1</button></li>
<li><button class="item">Button 2</button></li>
<li><button class="item">Button 3</button></li>
</ul>
<script>
const list = document.getElementById('list');
list.addEventListener('click', function(event) {
if (event.target && event.target.matches('button.item')) {
console.log(event.target.textContent);
}
});
</script>
</body>
</html>
在上述代码中,我们将事件处理程序绑定到父级元素<ul>上,通过event.target判断事件的目标元素是否是按钮,从而实现了事件委托。
应用场景
- 动态生成的元素:例如通过AJAX请求生成的新内容,使用事件委托可以避免为每个新元素重新绑定事件处理程序。
- 列表和表格中的操作:如编辑、删除等操作,可以通过事件委托简化事件处理。
22. 基本数据类型和引用数据类型的区别
基本数据类型
基本数据类型是按值存储的,这意味着变量直接存储值本身。这些类型在内存中分配固定大小的空间。
基本数据类型包括:
Number:表示数字类型(整数和浮点数)。String:表示字符串类型。Boolean:表示布尔值(true或false)。Undefined:表示未定义的值。Null:表示空值或无效值。Symbol:表示独一无二的符号,通常用于对象的属性键(ES6引入)。BigInt:表示任意精度的整数(ES11引入)。
示例:
let num = 42; // Number
let str = "Hello, world!"; // String
let bool = true; // Boolean
let undef; // Undefined
let n = null; // Null
let sym = Symbol('unique'); // Symbol
let bigInt = BigInt(12345678901234567890n); // BigInt
引用数据类型
引用数据类型是按引用存储的,这意味着变量存储的是引用地址,而不是值本身。引用数据类型的实例在内存中通常会动态分配空间,其大小不固定。
引用数据类型包括:
- 对象(Object):包含对象字面量、数组、函数、日期等。
- 数组(Array):表示有序集合。
- 函数(Function):表示可调用的代码块。
示例:
let obj = { name: "Alice", age: 25 }; // Object
let arr = [1, 2, 3]; // Array
let func = function() { console.log("Hello!"); }; // Function
主要区别
-
存储方式:
- 基本数据类型:直接存储值本身。
- 引用数据类型:存储的是引用地址,指向堆内存中的实际数据。
-
复制行为:
-
基本数据类型:复制时会创建值的副本,修改副本不会影响原始变量。
let a = 10; let b = a; b = 20; console.log(a); // 输出: 10 console.log(b); // 输出: 20 -
引用数据类型:复制时复制的是引用地址,修改副本会影响原始对象。
let obj1 = { name: "Alice" }; let obj2 = obj1; obj2.name = "Bob"; console.log(obj1.name); // 输出: Bob console.log(obj2.name); // 输出: Bob
-
-
比较方式:
-
基本数据类型:按值比较。
let x = 5; let y = 5; console.log(x === y); // 输出: true -
引用数据类型:按引用地址比较。
let arr1 = [1, 2, 3]; let arr2 = [1, 2, 3]; let arr3 = arr1; console.log(arr1 === arr2); // 输出: false console.log(arr1 === arr3); // 输出: true
-
23. new关键词做了什么
在JavaScript中,new关键词用于创建一个新的对象实例,并执行构造函数。它的主要功能和操作步骤如下:
new关键词的操作步骤
- 创建一个新对象:
new首先创建一个空的JavaScript对象,并将其原型(prototype)设置为构造函数的原型。 - 绑定this:构造函数内部的
this被绑定到这个新创建的对象。 - 执行构造函数:调用构造函数,并将参数传递给它。
- 返回新对象:如果构造函数没有显式返回对象,则默认返回新创建的对象。
示例
function Person(name, age) {
this.name = name;
this.age = age;
}
const person1 = new Person('Alice', 30);
const person2 = new Person('Bob', 25);
console.log(person1.name); // 输出: Alice
console.log(person2.age); // 输出: 25
在上面的示例中,new Person('Alice', 30)执行了以下操作:
- 创建一个新对象
{}。 - 将新对象的原型(
__proto__)设置为Person.prototype。 - 将构造函数内部的
this绑定到新对象。 - 调用
Person构造函数,并将name和age作为参数传递。 - 如果构造函数没有显式返回对象,则返回新对象。
等效代码
上述示例中new关键词的操作可以通过以下等效代码来理解:
function Person(name, age) {
this.name = name;
this.age = age;
}
function createNew(constructor, ...args) {
// 创建一个新对象,并将其原型设置为构造函数的原型
const newObj = Object.create(constructor.prototype);
// 调用构造函数,并将this绑定到新对象
const result = constructor.apply(newObj, args);
// 如果构造函数返回一个对象,则使用该对象,否则返回新对象
return typeof result === 'object' && result !== null ? result : newObj;
}
const person1 = createNew(Person, 'Alice', 30);
const person2 = createNew(Person, 'Bob', 25);
console.log(person1.name); // 输出: Alice
console.log(person2.age); // 输出: 25
24. 说一下原型和原型链
原型(Prototype)和原型链(Prototype Chain)是JavaScript中的两个重要概念,它们构成了JavaScript的继承机制和对象系统。理解原型和原型链有助于更好地理解JavaScript的面向对象编程。
原型(Prototype)
每一个JavaScript对象(除了null)都具有一个内部属性,称为__proto__(有时也称为隐式原型)。这个属性指向对象的原型,即另一个对象。通过原型,JavaScript对象可以继承其他对象的属性和方法。
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
console.log(`Hello, my name is ${this.name}`);
};
const alice = new Person('Alice');
alice.sayHello(); // 输出: Hello, my name is Alice
在这个例子中,Person构造函数的原型对象具有一个sayHello方法。通过原型链,alice实例对象可以访问并调用这个方法。
原型链(Prototype Chain)
原型链是由多个对象通过原型连接起来的一条链。当访问一个对象的属性或方法时,JavaScript引擎会首先检查该对象本身是否具有这个属性或方法。如果没有找到,它会沿着原型链向上查找,直到找到目标属性或方法,或者到达原型链的末端(即null)。
const obj = {
a: 1,
b: 2
};
const protoObj = {
b: 3,
c: 4
};
Object.setPrototypeOf(obj, protoObj);
console.log(obj.a); // 输出: 1(来自 obj)
console.log(obj.b); // 输出: 2(来自 obj)
console.log(obj.c); // 输出: 4(来自 protoObj)
在上面的例子中,通过Object.setPrototypeOf方法,我们将protoObj设置为obj的原型。当访问obj.c时,JavaScript引擎在obj中找不到属性c,于是沿着原型链向上查找,最终在protoObj中找到了属性c。
构造函数与原型链
当使用构造函数创建对象时,构造函数的prototype属性指向一个原型对象。新创建的对象的__proto__属性指向这个原型对象。
function Animal(name) {
this.name = name;
}
Animal.prototype.eat = function() {
console.log(`${this.name} is eating`);
};
const dog = new Animal('Dog');
dog.eat(); // 输出: Dog is eating
console.log(dog.__proto__ === Animal.prototype); // 输出: true
console.log(Animal.prototype.constructor === Animal); // 输出: true
总结
原型和原型链是JavaScript实现继承和代码重用的重要机制。通过原型链,对象可以继承其他对象的属性和方法,使得代码更加模块化和可复用。理解它们的工作原理,可以帮助我们更好地编写和优化JavaScript代码。
25. JS如何实现继承的
在JavaScript中,继承通常通过原型链和ES6的class语法来实现。以下是两种主要的实现方式:
1. 原型链继承
在ES6之前,JavaScript的继承是通过原型链实现的。我们可以将一个对象的原型设置为另一个对象,从而实现继承。
示例:
function Animal(name) {
this.name = name;
}
Animal.prototype.eat = function() {
console.log(`${this.name} is eating`);
};
function Dog(name, breed) {
Animal.call(this, name); // 调用父类构造函数,绑定this
this.breed = breed;
}
// 将Dog的原型设置为Animal的实例
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function() {
console.log(`${this.name} is barking`);
};
const myDog = new Dog('Buddy', 'Golden Retriever');
myDog.eat(); // 输出: Buddy is eating
myDog.bark(); // 输出: Buddy is barking
在这个例子中,我们通过以下步骤实现继承:
- 在
Dog构造函数中调用Animal构造函数,绑定this,从而继承Animal的属性。 - 使用
Object.create方法将Dog.prototype设置为Animal.prototype的一个实例,从而继承Animal的原型方法。 - 将
Dog.prototype.constructor重新指向Dog构造函数。
2. ES6 class 语法
ES6引入了class语法,使得继承变得更加简洁和直观。通过extends关键字,可以实现子类继承父类。
示例:
class Animal {
constructor(name) {
this.name = name;
}
eat() {
console.log(`${this.name} is eating`);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // 调用父类构造函数
this.breed = breed;
}
bark() {
console.log(`${this.name} is barking`);
}
}
const myDog = new Dog('Buddy', 'Golden Retriever');
myDog.eat(); // 输出: Buddy is eating
myDog.bark(); // 输出: Buddy is barking
在这个例子中,我们通过以下步骤实现继承:
- 使用
class关键字定义Animal和Dog类。 - 使用
extends关键字让Dog继承Animal。 - 在子类
Dog的构造函数中使用super调用父类Animal的构造函数,从而继承父类的属性和方法。
总结
JavaScript的继承机制主要通过原型链实现,ES6引入的class语法使得继承变得更加简洁和易读。通过这两种方式,可以实现对象之间的属性和方法的继承,从而更好地组织和复用代码。
26. 谈谈this的指向
1. 全局上下文
在全局上下文中,this指向全局对象。在浏览器中,全局对象是window。
console.log(this === window); // 输出: true
2. 函数上下文
在普通函数中,this指向调用该函数的对象。如果函数是作为对象的方法调用的,this指向该对象。
function showThis() {
console.log(this);
}
const obj = {
name: 'Alice',
showThis: showThis
};
obj.showThis(); // 输出: obj 对象
showThis(); // 输出: window 对象(在严格模式下为 undefined)
3. 构造函数
在构造函数中,this指向新创建的对象。
function Person(name) {
this.name = name;
}
const alice = new Person('Alice');
console.log(alice.name); // 输出: Alice
4. 箭头函数
箭头函数不绑定this,它会捕获其所在上下文(即定义时的上下文)的this值。
const obj = {
name: 'Alice',
showThis: function() {
const arrow = () => {
console.log(this);
};
arrow();
}
};
obj.showThis(); // 输出: obj 对象
5. call、apply和bind方法
可以使用call、apply和bind方法显式地设置this的指向。
function showThis() {
console.log(this.name);
}
const obj1 = { name: 'Alice' };
const obj2 = { name: 'Bob' };
showThis.call(obj1); // 输出: Alice
showThis.apply(obj2); // 输出: Bob
const boundShowThis = showThis.bind(obj1);
boundShowThis(); // 输出: Alice
6. 事件处理函数
在事件处理函数中,this通常指向触发事件的元素。
document.getElementById('myButton').addEventListener('click', function() {
console.log(this); // 输出: 触发事件的按钮元素
});
7. ES6类方法
在ES6类的方法中,this指向类的实例。
class Person {
constructor(name) {
this.name = name;
}
showThis() {
console.log(this);
}
}
const alice = new Person('Alice');
alice.showThis(); // 输出: alice 对象
8. 内联事件处理器
在HTML内联事件处理器中,this指向触发事件的元素。
<button onclick="console.log(this)">Click me</button>
27. 改变this指向
1. 使用call方法
call方法可以立即调用函数,并传递一个参数作为this的值。
function showThis() {
console.log(this.name);
}
const obj = { name: 'Alice' };
showThis.call(obj); // 输出: Alice
2. 使用apply方法
apply方法与call方法类似,不同的是它接受一个参数数组。
function showThis() {
console.log(this.name);
}
const obj = { name: 'Bob' };
showThis.apply(obj); // 输出: Bob
3. 使用bind方法
bind方法创建一个新函数,并绑定this到指定的对象。
function showThis() {
console.log(this.name);
}
const obj = { name: 'Charlie' };
const boundShowThis = showThis.bind(obj);
boundShowThis();
28. 如何实现浅拷贝和深拷贝
在JavaScript中,浅拷贝和深拷贝是两种不同的对象复制方法。它们的区别在于:浅拷贝只复制对象的第一层属性,而深拷贝会递归复制所有层级的属性。下面是实现浅拷贝和深拷贝的几种方法:
浅拷贝
-
Object.assign()方法:const obj1 = { a: 1, b: { c: 2 } }; const obj2 = Object.assign({}, obj1); console.log(obj2); // 输出: { a: 1, b: { c: 2 } } console.log(obj2.b === obj1.b); // 输出: true (浅拷贝) -
展开运算符(Spread Operator):
const obj1 = { a: 1, b: { c: 2 } }; const obj2 = { ...obj1 }; console.log(obj2); // 输出: { a: 1, b: { c: 2 } } console.log(obj2.b === obj1.b); // 输出: true (浅拷贝)
深拷贝
-
递归拷贝:手动实现递归拷贝,复制每一层属性。
function deepClone(obj) { if (obj === null || typeof obj !== 'object') { return obj; } if (Array.isArray(obj)) { const arrCopy = []; obj.forEach((item, index) => { arrCopy[index] = deepClone(item); }); return arrCopy; } const objCopy = {}; Object.keys(obj).forEach(key => { objCopy[key] = deepClone(obj[key]); }); return objCopy; } const obj1 = { a: 1, b: { c: 2 } }; const obj2 = deepClone(obj1); console.log(obj2); // 输出: { a: 1, b: { c: 2 } } console.log(obj2.b === obj1.b); // 输出: false (深拷贝) -
JSON序列化和反序列化:利用
JSON.stringify和JSON.parse方法进行深拷贝,但无法处理函数和循环引用。const obj1 = { a: 1, b: { c: 2 } }; const obj2 = JSON.parse(JSON.stringify(obj1)); console.log(obj2); // 输出: { a: 1, b: { c: 2 } } console.log(obj2.b === obj1.b); // 输出: false (深拷贝) -
使用第三方库:例如
lodash库中的_.cloneDeep方法。const _ = require('lodash'); const obj1 = { a: 1, b: { c: 2 } }; const obj2 = _.cloneDeep(obj1); console.log(obj2); // 输出: { a: 1, b: { c: 2 } } console.log(obj2.b === obj1.b); // 输出: false (深拷贝)
小结
- 浅拷贝:适用于复制简单对象,如果对象包含嵌套的引用类型,浅拷贝可能不够。
- 深拷贝:适用于复制复杂对象,确保所有层级的属性都被独立复制。
29. 什么是高阶函数?有什么使用场景
高阶函数(Higher-Order Function)是指接受一个或多个函数作为参数,或返回一个函数作为结果的函数。在JavaScript中,高阶函数是一种强大且灵活的编程模式,广泛应用于函数式编程和异步编程。
高阶函数的特点
- 接受函数作为参数:高阶函数可以接受其他函数作为参数,从而可以动态地改变函数的行为。
- 返回函数作为结果:高阶函数可以返回一个新的函数,延迟执行或创建更复杂的功能。
示例
示例1:接受函数作为参数:
function forEach(array, callback) {
for (let i = 0; i < array.length; i++) {
callback(array[i], i, array);
}
}
const arr = [1, 2, 3, 4];
forEach(arr, function(item, index) {
console.log(`Index ${index}: ${item}`);
});
示例2:返回函数作为结果:
function createMultiplier(multiplier) {
return function(value) {
return value * multiplier;
};
}
const double = createMultiplier(2);
console.log(double(5)); // 输出: 10
const triple = createMultiplier(3);
console.log(triple(5)); // 输出: 15
常见的高阶函数
-
map:用于对数组中的每个元素进行转换,返回一个新的数组。const numbers = [1, 2, 3, 4]; const doubled = numbers.map(num => num * 2); console.log(doubled); // 输出: [2, 4, 6, 8] -
filter:用于过滤数组中的元素,返回满足条件的元素组成的新数组。const numbers = [1, 2, 3, 4]; const evenNumbers = numbers.filter(num => num % 2 === 0); console.log(evenNumbers); // 输出: [2, 4] -
reduce:用于对数组中的元素进行累积计算,返回一个单一值。const numbers = [1, 2, 3, 4]; const sum = numbers.reduce((accumulator, currentValue) => accumulator + currentValue, 0); console.log(sum); // 输出: 10 -
setTimeout和setInterval:这些内置的高阶函数接受回调函数,并在指定的时间后执行。setTimeout(() => { console.log('Delayed message'); }, 1000); setInterval(() => { console.log('Repeating message'); }, 1000);
使用场景
- 函数式编程:高阶函数是函数式编程的核心,可以实现更高阶的抽象和代码复用。
- 事件处理:在事件处理和回调函数中,高阶函数用于动态绑定和处理事件。
- 异步编程:在异步操作(如AJAX请求、定时器等)中,高阶函数用于处理异步回调。
- 数据处理:使用高阶函数处理数组和集合,可以编写简洁、优雅的代码,如
map、filter、reduce等。
30. ES6新特性有哪些?
1. let 和 const 关键字
let用于声明块级作用域的变量。const用于声明块级作用域的常量。
let a = 10;
const b = 20;
2. 箭头函数
箭头函数提供了一种更简洁的函数定义方式,并且不绑定自己的 this。
const add = (x, y) => x + y;
3. 模板字符串
模板字符串使用反引号(``)包裹,可以插入变量和表达式,更加简洁。
const name = "Alice";
const greeting = `Hello, ${name}!`;
4. 默认参数
函数可以为参数指定默认值。
function greet(name = 'Guest') {
return `Hello, ${name}!`;
}
5. 解构赋值
解构赋值允许从数组或对象中提取值并赋给变量。
const [x, y] = [1, 2];
const { name, age } = { name: "Alice", age: 30 };
6. 扩展运算符和剩余参数
扩展运算符(...)用于展开数组和对象,剩余参数用于收集函数的参数。
const arr1 = [1, 2];
const arr2 = [...arr1, 3, 4];
const sum = (...args) => args.reduce((a, b) => a + b, 0);
7. 类(Class)
ES6 引入了类的语法糖,提供了一种更简洁的面向对象编程方式。
class Person {
constructor(name) {
this.name = name;
}
greet() {
return `Hello, ${this.name}`;
}
}
const alice = new Person('Alice');
console.log(alice.greet());
8. 模块(Modules)
ES6 引入了模块系统,用于导入和导出模块。
// export.js
export const pi = 3.14;
export function square(x) {
return x * x;
}
// import.js
import { pi, square } from './export';
console.log(pi); // 3.14
console.log(square(2)); // 4
9. Promise
Promise 提供了一种更简单的处理异步操作的方法。
const promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("Success!"), 1000);
});
promise.then(result => {
console.log(result); // 输出: Success!
});
10. 迭代器和生成器
迭代器是一种新的遍历机制,生成器是使用 function* 声明的函数,可以生成多个值。
function* generator() {
yield 1;
yield 2;
yield 3;
}
const gen = generator();
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
console.log(gen.next().value);
31. 如何从数组和对象中解构赋值
从数组中解构赋值
-
基本示例:
const numbers = [1, 2, 3, 4]; const [first, second] = numbers; console.log(first); // 输出: 1 console.log(second); // 输出: 2 -
跳过元素:
const numbers = [1, 2, 3, 4]; const [first, , third] = numbers; console.log(first); // 输出: 1 console.log(third); // 输出: 3 -
使用剩余参数:
const numbers = [1, 2, 3, 4]; const [first, ...rest] = numbers; console.log(first); // 输出: 1 console.log(rest); // 输出: [2, 3, 4]
从对象中解构赋值
-
基本示例:
const person = { name: 'Alice', age: 30 }; const { name, age } = person; console.log(name); // 输出: Alice console.log(age); // 输出: 30 -
重命名变量:
const person = { name: 'Bob', age: 25 }; const { name: personName, age: personAge } = person; console.log(personName); // 输出: Bob console.log(personAge); // 输出: 25 -
使用默认值:
const person = { name: 'Charlie' }; const { name, age = 40 } = person; console.log(name); // 输出: Charlie console.log(age); // 输出: 40 -
嵌套解构:
const person = { name: 'David', address: { city: 'New York', zip: '10001' } }; const { name, address: { city, zip } } = person; console.log(name); // 输出: David console.log(city); // 输出: New York console.log(zip); // 输出: 10001
32. 如何使用扩展运算符复制或合并对象
扩展运算符(Spread Operator)在JavaScript中使用三个点(...)表示,可以用于复制或合并对象和数组。下面是一些示例,展示如何使用扩展运算符进行这些操作:
复制对象
使用扩展运算符可以创建一个对象的浅拷贝。
const original = { a: 1, b: 2 };
const copy = { ...original };
console.log(copy); // 输出: { a: 1, b: 2 }
console.log(copy === original); // 输出: false (不同的对象)
合并对象
使用扩展运算符可以合并多个对象。
const obj1 = { a: 1, b: 2 };
const obj2 = { b: 3, c: 4 };
const merged = { ...obj1, ...obj2 };
console.log(merged); // 输出: { a: 1, b: 3, c: 4 }
在这个示例中,obj2中的属性b会覆盖obj1中的同名属性。
添加属性到对象
使用扩展运算符可以方便地向对象添加新的属性。
const original = { a: 1, b: 2 };
const withAdditionalProperty = { ...original, c: 3 };
console.log(withAdditionalProperty); // 输出: { a: 1, b: 2, c: 3 }
深拷贝注意事项
扩展运算符进行的是浅拷贝,如果对象的属性是引用类型(如对象或数组),则复制的是引用地址。
const original = { a: 1, b: { c: 2 } };
const copy = { ...original };
console.log(copy.b === original.b); // 输出: true (同一个引用)
如果需要深拷贝,可以结合其他方法,如递归或使用JSON.parse和JSON.stringify方法。
const original = { a: 1, b: { c: 2 } };
const deepCopy = JSON.parse(JSON.stringify(original));
console.log(deepCopy.b === original.b);
33. Es6有什么遍历数组的方法
1. forEach()
forEach 方法对数组的每个元素执行一次提供的函数。
const numbers = [1, 2, 3, 4];
numbers.forEach(num => {
console.log(num); // 依次输出: 1, 2, 3, 4
});
2. map()
map 方法创建一个新数组,数组中的每个元素是原数组元素调用提供的函数后的返回值。
const numbers = [1, 2, 3, 4];
const squared = numbers.map(num => num * 2);
console.log(squared); // 输出: [2, 4, 6, 8]
3. filter()
filter 方法创建一个新数组,其中包含所有通过提供函数测试的元素。
const numbers = [1, 2, 3, 4];
const even = numbers.filter(num => num % 2 === 0);
console.log(even); // 输出: [2, 4]
4. reduce()
reduce 方法对数组中的每个元素执行提供的函数,将其结果汇总为单个值。
const numbers = [1, 2, 3, 4];
const sum = numbers.reduce((acc, num) => acc + num, 0);
console.log(sum); // 输出: 10
5. some()
some 方法测试数组中的某些元素是否通过了提供的函数测试。它返回一个布尔值。
const numbers = [1, 2, 3, 4];
const hasEven = numbers.some(num => num % 2 === 0);
console.log(hasEven); // 输出: true
6. every()
every 方法测试数组中的所有元素是否都通过了提供的函数测试。它返回一个布尔值。
const numbers = [1, 2, 3, 4];
const allEven = numbers.every(num => num % 2 === 0);
console.log(allEven); // 输出: false
7. find()
find 方法返回数组中满足提供的函数的第一个元素。如果没有元素满足条件,则返回 undefined。
const numbers = [1, 2, 3, 4];
const firstEven = numbers.find(num => num % 2 === 0);
console.log(firstEven); // 输出: 2
8. findIndex()
findIndex 方法返回数组中满足提供的函数的第一个元素的索引。如果没有元素满足条件,则返回 -1。
javascript
const numbers = [1, 2, 3, 4];
const firstEvenIndex = numbers.findIndex(num => num % 2 === 0);
console.log(firstEvenIndex); // 输出: 1
34. Map和Set各自有什么特性
Map 和 Set 是ES6引入的两种新的数据结构,它们各自有独特的特性和用途。
Map 的特性
-
键值对存储:
Map对象用于存储键值对。任何值(对象或原始值)都可以作为键或值。let map = new Map(); map.set('name', 'Alice'); map.set('age', 30); console.log(map.get('name')); // 输出: Alice console.log(map.size); // 输出: 2 -
有序性:
Map中的键值对是按照插入顺序保存的,因此可以进行有序的迭代。map.forEach((value, key) => { console.log(`${key}: ${value}`); }); -
键的唯一性:
Map中的每个键都是唯一的。如果多次使用相同的键,后面的值会覆盖前面的值。map.set('name', 'Bob'); console.log(map.get('name')); // 输出: Bob -
与普通对象的区别:
Map对象与普通对象的主要区别在于,Map的键可以是任何类型(包括对象),而普通对象的键只能是字符串或符号。let obj = {}; let objKey = { key: 'value' }; map.set(objKey, 'Some value'); console.log(map.get(objKey)); // 输出: Some value
Set 的特性
-
值集合:
Set对象允许你存储任何类型的唯一值(无重复值)。let set = new Set(); set.add(1); set.add(5); set.add(1); // 重复值不会被添加 console.log(set.size); // 输出: 2 -
值的唯一性:
Set中的每个值都是唯一的,重复的值不会被添加。set.add('hello'); set.add('world'); set.add('hello'); // 重复值不会被添加 console.log(set); // 输出: Set { 1, 5, 'hello', 'world' } -
有序性:
Set中的值是按照插入顺序保存的,因此可以进行有序的迭代。set.forEach(value => { console.log(value); }); -
与数组的区别:
Set对象与数组的主要区别在于,Set只存储唯一值,而数组可以存储重复值。let array = [1, 2, 3, 3, 4]; let uniqueArray = [...new Set(array)]; console.log(uniqueArray); // 输出: [1, 2, 3, 4]
主要用途
Map:适用于需要使用复杂键(例如对象)或需要按插入顺序迭代的场景。Set:适用于需要存储唯一值集合、去重和需要按插入顺序迭代的场景。
35. let var有什么区别
let 和 var 都是用来声明变量的,但它们在行为和作用域上有一些重要的区别。以下是它们的主要区别:
1. 作用域(Scope)
-
var:var声明的变量具有函数作用域(function scope),也就是说,var声明的变量在其所在的函数内都可以访问。如果在函数外声明,则具有全局作用域。function exampleVar() { if (true) { var x = 10; } console.log(x); // 输出: 10 } exampleVar(); -
let:let声明的变量具有块作用域(block scope),即变量只在其所在的代码块内(如{}中)可以访问。function exampleLet() { if (true) { let y = 20; } console.log(y); // 会报错,因为y在if块外不可访问 } exampleLet();
2. 重复声明
-
var:同一个作用域内,var可以重复声明变量。var a = 1; var a = 2; // 不会报错 console.log(a); // 输出: 2 -
let:同一个作用域内,let不能重复声明变量。let b = 1; let b = 2; // 会报错,SyntaxError: Identifier 'b' has already been declared
3. 变量提升(Hoisting)
-
var:var声明的变量会被提升到作用域的顶部,但提升后变量的值为undefined。console.log(c); // 输出: undefined var c = 3; -
let:let声明的变量也会被提升到作用域的顶部,但在变量声明之前不能访问,会导致ReferenceError。console.log(d); // 会报错,ReferenceError: Cannot access 'd' before initialization let d = 4;
4. 全局对象属性
-
var:在全局作用域中用var声明的变量,会成为全局对象(如window)的属性。var e = 5; console.log(window.e); // 输出: 5 -
let:在全局作用域中用let声明的变量,不会成为全局对象的属性。let f = 6; console.log(window.f); // 输出: undefined
5. 暂时性死区(Temporal Dead Zone)
-
let:在变量声明之前,暂时性死区(TDZ)内访问let声明的变量会导致ReferenceError。{ // TDZ 开始 console.log(g); // 会报错,ReferenceError: Cannot access 'g' before initialization let g = 7; // TDZ 结束 }
36. 箭头函数和普通函数有什么区别
1. this 绑定
-
普通函数:
this的值取决于函数的调用方式。如果函数作为对象的方法调用,this指向调用该方法的对象;如果作为普通函数调用,this通常指向全局对象(在严格模式下为undefined)。const obj = { name: 'Alice', sayHello: function() { console.log(this.name); } }; obj.sayHello(); // 输出: Alice -
箭头函数:箭头函数不会创建自己的
this,它会捕获其定义时的上下文的this,即指向外层作用域的this。const obj = { name: 'Bob', sayHello: function() { const inner = () => { console.log(this.name); }; inner(); } }; obj.sayHello(); // 输出: Bob
2. 语法
-
普通函数:需要使用
function关键字定义。function add(a, b) { return a + b; } -
箭头函数:使用箭头
=>定义,语法更加简洁。const add = (a, b) => a + b;
3. 参数
-
普通函数:普通函数有自己的
arguments对象,包含了所有传递给函数的参数。function showArgs() { console.log(arguments); } showArgs(1, 2, 3); // 输出: [1, 2, 3] -
箭头函数:箭头函数没有
arguments对象,可以使用剩余参数语法...args来获取参数。const showArgs = (...args) => { console.log(args); }; showArgs(1, 2, 3); // 输出: [1, 2, 3]
4. 作为方法
-
普通函数:可以作为对象的方法,并且可以动态地改变
this的指向。const obj1 = { value: 10, getValue: function() { return this.value; } }; const obj2 = { value: 20 }; console.log(obj1.getValue.call(obj2)); // 输出: 20 -
箭头函数:不能作为构造函数,不能使用
new关键字实例化对象。const ArrowFunction = () => {}; // const instance = new ArrowFunction(); // 会报错,ArrowFunction is not a constructor
5. 构造函数
-
普通函数:可以作为构造函数使用,使用
new关键字实例化对象。function Person(name) { this.name = name; } const alice = new Person('Alice'); console.log(alice.name); // 输出: Alice -
箭头函数:不能作为构造函数,不能使用
new关键字。const Person = (name) => { this.name = name; }; // const bob = new Person('Bob'); // 会报错,Person is not a constructor
6. 绑定 this
-
普通函数:可以显式地使用
bind方法绑定this。function greet() { console.log(`Hello, ${this.name}`); } const person = { name: 'Charlie' }; const boundGreet = greet.bind(person); boundGreet(); // 输出: Hello, Charlie -
箭头函数:没有自己的
this,无法使用bind绑定this。const greet = () => { console.log(`Hello, ${this.name}`); }; const person = { name: 'Dave' }; const boundGreet = greet.bind(person); boundGreet(); // 输出: Hello, undefined
37. Es6如何改进了类的定义和继承机制
ES6引入了类(Class)的语法糖,使得定义和继承类变得更加简洁和直观。这些改进主要体现在以下几个方面:
1. 类的定义
在ES6之前,JavaScript中定义类和实现继承需要使用构造函数和原型链。ES6引入的class关键字提供了一种更简洁的方式来定义类。
ES5 方式:
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHello = function() {
console.log(`Hello, my name is ${this.name}`);
};
const alice = new Person('Alice', 30);
alice.sayHello(); // 输出: Hello, my name is Alice
ES6 方式:
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
sayHello() {
console.log(`Hello, my name is ${this.name}`);
}
}
const alice = new Person('Alice', 30);
alice.sayHello(); // 输出: Hello, my name is Alice
2. 类的继承
ES6引入了extends关键字和super关键字,使得类的继承变得更加直观和易读。
ES5 方式:
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function() {
console.log(`${this.name} makes a noise`);
};
function Dog(name, breed) {
Animal.call(this, name);
this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function() {
console.log(`${this.name} barks`);
};
const dog = new Dog('Buddy', 'Golden Retriever');
dog.speak(); // 输出: Buddy makes a noise
dog.bark(); // 输出: Buddy barks
ES6 方式:
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a noise`);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
bark() {
console.log(`${this.name} barks`);
}
}
const dog = new Dog('Buddy', 'Golden Retriever');
dog.speak(); // 输出: Buddy makes a noise
dog.bark(); // 输出: Buddy barks
3. 静态方法
ES6中的类还支持静态方法(static methods),这些方法可以直接在类上调用,而不是在类的实例上调用。
class MathUtil {
static add(a, b) {
return a + b;
}
}
console.log(MathUtil.add(2, 3)); // 输出: 5
4. 访问器属性
ES6中的类可以定义getter和setter方法,用于访问和修改对象的属性。
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
get area() {
return this.width * this.height;
}
set area(value) {
this.width = Math.sqrt(value);
this.height = Math.sqrt(value);
}
}
const rect = new Rectangle(4, 5);
console.log(rect.area); // 输出: 20
rect.area = 36;
console.log(rect.width); // 输出: 6
console.log(rect.height); // 输出: 6
总结
ES6引入的类语法糖使得类的定义和继承变得更加简洁、直观和可读。通过class、extends、super、静态方法以及访问器属性,开发者可以更方便地实现面向对象编程。
38. 事件循环机制?
事件循环(Event Loop)是JavaScript中的一种机制,用于处理异步编程。它使得JavaScript能够非阻塞地执行代码,同时管理多个任务和事件。为了更好地理解事件循环,以下是其工作原理和相关概念的概述:
单线程模型
JavaScript 是一种单线程语言,即同一时间只能执行一个任务。这意味着,只有一个主线程负责执行所有的JavaScript代码。
异步编程
为了避免长时间的阻塞操作(如网络请求、文件I/O等)阻碍主线程执行,JavaScript引入了异步编程。异步任务不会立即执行,而是将其放入任务队列(Task Queue)中,等待事件循环处理。
事件循环的工作原理
事件循环负责不断地检查任务队列和调用栈(Call Stack),并将任务从任务队列中取出,放入调用栈中执行。事件循环的工作流程如下:
- 执行同步代码:主线程首先执行所有同步代码,这些代码会被放入调用栈中逐一执行。
- 任务进入任务队列:当异步任务(如定时器、网络请求等)完成时,回调函数会被放入任务队列中。
- 事件循环检查任务队列:事件循环不断地检查调用栈是否为空。如果调用栈为空,则会从任务队列中取出一个任务,并将其放入调用栈中执行。
- 重复过程:事件循环重复上述过程,直到所有任务都执行完毕。
微任务队列(Microtask Queue)
除了任务队列(宏任务队列),JavaScript还有一个微任务队列,用于存放微任务(Microtasks)。微任务包括Promise的回调函数、MutationObserver等。微任务的优先级高于宏任务,在每个宏任务执行完成后,事件循环会先检查并执行所有的微任务,然后再继续处理下一个宏任务。
示例
console.log('Start');
setTimeout(() => {
console.log('Timeout');
}, 0);
Promise.resolve().then(() => {
console.log('Promise');
});
console.log('End');
输出结果:
Start
End
Promise
Timeout
解释:
- 首先执行同步代码,输出
Start和End。 setTimeout将回调函数放入任务队列。Promise.resolve().then将回调函数放入微任务队列。- 执行所有微任务,输出
Promise。 - 处理任务队列中的任务,输出
Timeout。
39. ajax是什么?怎么实现的
AJAX(Asynchronous JavaScript and XML)是一种用于在不重新加载整个网页的情况下与服务器进行异步通信的技术。通过AJAX,网页可以在后台与服务器交换数据,并动态更新网页内容,而无需刷新页面。这使得用户体验更加流畅和响应迅速。
AJAX的特点
- 异步通信:AJAX允许在后台与服务器进行异步通信,不会阻塞用户的操作。
- 动态更新:使用AJAX可以动态更新网页的部分内容,而不需要重新加载整个页面。
- 广泛支持:AJAX可以使用多种数据格式进行通信,如XML、JSON、HTML或纯文本。
实现AJAX的几种方法
1. 使用XMLHttpRequest对象
这是传统的实现AJAX的方法,使用JavaScript中的XMLHttpRequest对象。
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/data', true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
console.log(xhr.responseText);
}
};
xhr.send();
2. 使用Fetch API
Fetch API是现代浏览器中引入的一种更简洁的实现AJAX的方法,返回的是一个Promise对象。
fetch('https://api.example.com/data')
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
console.log(data);
})
.catch(error => {
console.error('There was a problem with the fetch operation:', error);
});
3. 使用JQuery
JQuery提供了一种简洁的实现AJAX的方法。
$.ajax({
url: 'https://api.example.com/data',
method: 'GET',
success: function(data) {
console.log(data);
},
error: function(error) {
console.error('There was an error:', error);
}
});
总结
AJAX是一种非常强大和常用的技术,能够在不刷新页面的情况下与服务器进行通信,动态更新网页内容。现代浏览器中推荐使用Fetch API来实现AJAX,因为它的语法更简洁,使用Promise进行异步操作更加方便。
40. get和post区别
GET 请求
-
数据传输方式:GET请求通过URL中的查询字符串传输数据。
GET /api/example?name=Alice&age=30 HTTP/1.1 -
安全性:因为数据在URL中明文传输,所以不适合传输敏感信息。
-
请求长度:GET请求的长度受到URL长度的限制,不同浏览器和服务器对URL长度有不同的限制。
-
缓存:GET请求默认是可以被缓存的,因此适用于获取不经常变化的数据。
-
幂等性:GET请求是幂等的,即多次请求相同的资源不会改变服务器的状态。
-
书签:GET请求可以方便地创建书签,因为参数包含在URL中。
POST 请求
-
数据传输方式:POST请求通过请求体(body)传输数据。
POST /api/example HTTP/1.1 Content-Type: application/x-www-form-urlencoded name=Alice&age=30 -
安全性:数据在请求体中传输,相对来说更安全,但仍然需要使用HTTPS加密传输敏感信息。
-
请求长度:POST请求的数据长度没有限制,可以传输大量数据。
-
缓存:POST请求默认是不被缓存的,因此适用于需要立即处理的数据提交,如表单提交。
-
幂等性:POST请求不是幂等的,即多次请求相同的资源可能会改变服务器的状态。
-
书签:POST请求不能方便地创建书签,因为参数不在URL中。
使用场景
- GET:适用于获取数据,如检索信息、加载页面内容、获取搜索结果等。
- POST:适用于提交数据,如表单提交、上传文件、添加新记录等。
示例代码
GET请求示例:
fetch('https://api.example.com/data?name=Alice&age=30')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
POST请求示例:
fetch('https://api.example.com/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ name: 'Alice', age: 30 })
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
41. promise如何处理异步操作
Promise是JavaScript中处理异步操作的一个强大工具。它允许你在异步操作完成之后执行代码,而不会阻塞主线程。下面是Promise的基本用法以及如何处理异步操作:
- 创建Promise对象:Promise对象接受一个函数作为参数,该函数包含两个参数:
resolve和reject。resolve在操作成功时被调用,reject在操作失败时被调用。
let myPromise = new Promise((resolve, reject) => {
// 异步操作
let success = true; // 这是一个示例,通常是异步操作的结果
if (success) {
resolve("操作成功!");
} else {
reject("操作失败!");
}
});
- 处理Promise:使用
then()方法处理操作成功的情况,使用catch()方法处理操作失败的情况。
myPromise.then((message) => {
console.log(message); // 输出: 操作成功!
}).catch((errorMessage) => {
console.log(errorMessage); // 输出: 操作失败!
});
- 链式调用:你可以将多个
then()方法链式调用,以处理一系列的异步操作。
myPromise.then((message) => {
console.log(message); // 输出: 操作成功!
return anotherAsyncFunction(); // 返回另一个Promise
}).then((result) => {
console.log(result); // 处理另一个异步操作的结果
}).catch((errorMessage) => {
console.log(errorMessage); // 输出: 操作失败!
});
- 使用
finally():finally()方法在Promise操作完成或失败后都会执行,无论结果如何。
myPromise.finally(() => {
console.log("操作结束!");
});
42. Promise的内部原理是什么?优缺点是什么?
内部原理:
- 状态管理:Promise有三种状态:
pending(等待),fulfilled(完成)和rejected(失败)。一旦Promise的状态变成fulfilled或rejected,它就不能再改变。 - 状态转换:
- 当Promise被创建时,它处于
pending状态。 - 调用
resolve函数会将状态变为fulfilled,并将结果传递给相应的处理函数。 - 调用
reject函数会将状态变为rejected,并将错误信息传递给相应的处理函数。
- 当Promise被创建时,它处于
- 回调函数:在Promise内部,有两个函数被定义:
resolve和reject。这些函数用于触发Promise状态的转换。 - 异步处理:Promise通过事件循环和微任务队列来实现异步操作。一旦Promise的状态从
pending转换为fulfilled或rejected,相应的回调函数将被排队,并在事件循环的下一次迭代中执行。
优点:
- 提高代码可读性:相比于嵌套回调,Promise可以让异步代码更为清晰和可读。
- 错误处理:Promise提供了
catch方法,可以集中处理异步操作中的错误。 - 链式调用:Promise支持链式调用,使得可以连续处理多个异步操作。
- 灵活性:Promise可以与其他异步操作(如
async/await)结合使用,提供更多的灵活性。
缺点:
- 学习曲线:对于初学者来说,理解Promise的概念和用法可能需要一些时间。
- 调试困难:在某些情况下,调试Promise链中的错误可能会比较困难。
- 过度使用:如果过度使用Promise,可能会导致代码变得复杂和难以维护。
43. promise和async await的区别
Promise
-
基础概念:
Promise对象表示一个将来可能完成或失败的异步操作及其结果值。 -
语法:
let myPromise = new Promise((resolve, reject) => { // 异步操作 if (成功) { resolve("成功"); } else { reject("失败"); } }); myPromise.then((value) => { console.log(value); // 操作成功时的处理 }).catch((error) => { console.error(error); // 操作失败时的处理 }); -
链式调用:通过
then()和catch()方法可以链式处理多个异步操作。
async/await
-
基础概念:
async/await是基于 Promise 的语法糖,使得异步代码看起来像同步代码,进一步简化了异步操作的处理。 -
语法:
async function myAsyncFunction() { try { let value = await myPromise; console.log(value); // 操作成功时的处理 } catch (error) { console.error(error); // 操作失败时的处理 } } myAsyncFunction(); -
关键字:
async:用于定义一个异步函数,该函数会自动返回一个 Promise。await:用于等待一个 Promise 完成,并获取其结果。只能在async函数内使用。
区别总结
- 可读性:
async/await使得异步代码更加直观和易读,类似于同步代码的编写方式。 - 错误处理:使用
try...catch块来处理async/await中的错误,更加统一和简洁。 - 语法复杂度:相比于嵌套和链式调用的
then()和catch()方法,async/await语法更加简洁。 - 返回值:
async函数总是返回一个 Promise,而不是一个直接值。
例子对比
使用 Promise
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("数据加载成功!");
}, 1000);
});
}
fetchData().then((data) => {
console.log(data);
}).catch((error) => {
console.error(error);
});
使用 async/await
async function fetchData() {
try {
let data = await new Promise((resolve, reject) => {
setTimeout(() => {
resolve("数据加载成功!");
}, 1000);
});
console.log(data);
} catch (error) {
console.error(error);
}
}
fetchData();
44. 为什么微任务比宏任务早
宏任务(Macro tasks)
宏任务包括如 setTimeout、setInterval、I/O 操作等。每次事件循环时,主线程从宏任务队列中取出一个任务执行,执行完毕后再去执行微任务队列中的任务。
微任务(Micro tasks)
微任务包括 Promise 的回调、process.nextTick(Node.js 环境中)等。微任务是在宏任务执行完毕后立即执行的,且在同一个事件循环中可以执行多个微任务。
执行顺序
- 事件循环开始。
- 执行一个宏任务,例如从任务队列中取出一个
setTimeout的回调函数并执行。 - 执行所有的微任务。这些微任务会在宏任务之后,但在下一个宏任务之前执行。如果在执行微任务过程中产生了新的微任务,则会将这些新的微任务添加到当前的微任务队列中并继续执行,直到微任务队列为空。
- 进行页面渲染(如果有的话)。
- 回到步骤 1,继续下一个事件循环。
为什么微任务比宏任务早
- 更高优先级:微任务的优先级比宏任务高。微任务的设计初衷就是为了快速响应异步操作并且处理后续的逻辑,而不必等待下一个事件循环的宏任务执行。
- 及时性:微任务在当前宏任务执行结束后,立即执行。在某些情况下,这样可以保证代码的执行顺序和一致性,避免因为宏任务的延迟导致的一些问题。
- 性能优化:在执行宏任务之间处理所有的微任务,可以减少主线程空闲时间,优化性能。
示例
以下是一个简单的代码示例,展示了微任务和宏任务的执行顺序:
console.log("开始");
setTimeout(() => {
console.log("宏任务:setTimeout");
}, 0);
Promise.resolve().then(() => {
console.log("微任务:Promise");
});
console.log("结束");
输出顺序:
- 开始
- 结束
- 微任务:Promise
- 宏任务:setTimeout
45. 如何解决回调地狱
回调地狱(Callback Hell)是指在使用嵌套回调函数处理异步操作时,代码变得难以阅读和维护的情况。为了避免回调地狱,你可以使用以下几种方法:
1. 使用Promise
Promise提供了一种更清晰的方式来处理异步操作,并且可以避免嵌套回调。
function asyncFunction() {
return new Promise((resolve, reject) => {
// 异步操作
resolve("成功");
});
}
asyncFunction().then((result) => {
console.log(result); // 输出: 成功
}).catch((error) => {
console.error(error);
});
2. 使用async/await
async/await是Promise的语法糖,使得异步代码看起来像同步代码,提高了可读性。
async function exampleFunction() {
try {
let result = await asyncFunction();
console.log(result); // 输出: 成功
} catch (error) {
console.error(error);
}
}
exampleFunction();
3. 模块化代码
将异步操作拆分为多个模块或函数,使代码更加结构化和易于维护。
function firstAsyncFunction() {
return new Promise((resolve, reject) => {
// 第一个异步操作
resolve("第一个操作成功");
});
}
function secondAsyncFunction() {
return new Promise((resolve, reject) => {
// 第二个异步操作
resolve("第二个操作成功");
});
}
async function executeAsyncFunctions() {
try {
let firstResult = await firstAsyncFunction();
console.log(firstResult);
let secondResult = await secondAsyncFunction();
console.log(secondResult);
} catch (error) {
console.error(error);
}
}
executeAsyncFunctions();
4. 使用控制流库
有一些库专门用于管理异步操作,如async.js。这些库提供了很多强大的控制流工具,可以简化异步代码的管理。
const async = require('async');
async.series([
function(callback) {
// 第一个异步操作
callback(null, '第一个操作成功');
},
function(callback) {
// 第二个异步操作
callback(null, '第二个操作成功');
}
], function(error, results) {
if (error) {
console.error(error);
} else {
console.log(results); // 输出: ['第一个操作成功', '第二个操作成功']
}
});
46. 浏览器的存储方式有哪些?
1. Cookies
- 描述:Cookies 是一种小型数据存储方式,通常用于保存用户登录状态、偏好设置等信息。
- 容量:每个域名最多能存储 4KB 左右的数据。
- 过期时间:可以设置过期时间,默认为会话结束时失效。
- 特点:Cookies 会随着每次 HTTP 请求发送给服务器,因此不适合存储大量数据。
2. LocalStorage
- 描述:LocalStorage 是 HTML5 提供的本地存储方式,可以保存键值对,数据持久性较高。
- 容量:每个域名可以存储约 5-10MB 的数据。
- 过期时间:没有过期时间,数据会一直存在,直到被手动清除。
- 特点:只能在客户端访问,不能与服务器通信。
3. SessionStorage
- 描述:SessionStorage 与 LocalStorage 类似,但它的数据仅在当前会话中有效,浏览器关闭后数据会被清除。
- 容量:每个域名可以存储约 5-10MB 的数据。
- 过期时间:会话结束时清除。
- 特点:适用于在一次浏览会话中临时存储数据。
4. IndexedDB
- 描述:IndexedDB 是一种低级 API,用于在客户端存储大量结构化数据。它支持更复杂的数据查询和事务处理。
- 容量:存储容量较大,通常在几百MB甚至更多。
- 过期时间:没有过期时间,数据会一直存在,直到被手动清除。
- 特点:适用于存储大量和复杂的数据,如离线应用、PWA 等。
5. WebSQL
- 描述:WebSQL 是一种基于 SQL 的数据库存储方式,已被弃用,不推荐使用。
- 容量:存储容量较大,通常在几百MB甚至更多。
- 过期时间:没有过期时间,数据会一直存在,直到被手动清除。
- 特点:尽管功能强大,但由于被弃用,不再推荐使用。
示例代码
下面是使用 LocalStorage 存储和获取数据的示例:
// 存储数据
localStorage.setItem('key', 'value');
// 获取数据
let data = localStorage.getItem('key');
console.log(data); // 输出: value
// 删除数据
localStorage.removeItem('key');
// 清除所有数据
localStorage.clear();
47. token存在哪里?
- LocalStorage
- 优点:易于使用,持久化存储,不会随着页面刷新或关闭而丢失。
- 缺点:相对不安全,容易受到 XSS(跨站脚本攻击)的威胁。
// 存储token
localStorage.setItem('token', 'your-token-value');
// 获取token
let token = localStorage.getItem('token');
2. SessionStorage
- 优点:与 LocalStorage 类似,但数据仅在当前会话中有效,浏览器关闭后数据会被清除。
- 缺点:相对不安全,容易受到 XSS 攻击。
// 存储token
sessionStorage.setItem('token', 'your-token-value');
// 获取token
let token = sessionStorage.getItem('token');
3. Cookies
- 优点:可以设置 HttpOnly 标志,防止 XSS 攻击,并且可以设置 Secure 标志,仅在 HTTPS 连接时传输。
- 缺点:通常每次请求都会发送到服务器,增加流量。
// 设置token
document.cookie = "token=your-token-value; Secure; HttpOnly";
// 获取token
function getCookie(name) {
let match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
if (match) return match[2];
return null;
}
let token = getCookie('token');
4. 内存
- 优点:最安全的方式,token 存储在内存中,页面刷新后会丢失,防止持久化存储带来的安全风险。
- 缺点:token 在页面刷新或关闭后丢失。
let token = 'your-token-value';
// 获取token
console.log(token);
总结
- LocalStorage 和 SessionStorage 适用于简单的客户端存储,但要注意安全问题。
- Cookies 是更为安全的选择,特别是使用
HttpOnly和Secure标志。 - 内存 存储是最安全的方式,但需要考虑页面刷新后的处理逻辑。
48. token的登路流程
Token登录流程通常用于基于令牌的身份验证系统,如使用JWT(JSON Web Token)或OAuth。以下是一个典型的Token登录流程:
1. 用户登录
- 用户在登录页面输入用户名和密码。
- 客户端将用户名和密码发送到服务器。
fetch('https://api.example.com/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ username: 'user', password: 'pass' }),
})
.then(response => response.json())
.then(data => {
// 获取Token
const token = data.token;
// 存储Token
localStorage.setItem('token', token);
})
.catch(error => console.error('Error:', error));
2. 服务器验证
- 服务器接收到用户名和密码,验证其有效性。
- 如果验证通过,服务器生成一个Token(如JWT)。
const jwt = require('jsonwebtoken');
const secret = 'your-secret-key';
app.post('/login', (req, res) => {
const { username, password } = req.body;
// 验证用户名和密码
if (username === 'user' && password === 'pass') {
// 生成Token
const token = jwt.sign({ username }, secret, { expiresIn: '1h' });
res.json({ token });
} else {
res.status(401).json({ error: 'Invalid credentials' });
}
});
3. 客户端存储Token
- 客户端收到服务器返回的Token,将其存储在
localStorage或sessionStorage中,以便在后续请求中使用。
localStorage.setItem('token', token);
4. 发送带Token的请求
- 在后续的请求中,客户端将Token包含在请求头中,发送到服务器进行身份验证。
fetch('https://api.example.com/protected', {
method: 'GET',
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token'),
},
})
.then(response => response.json())
.then(data => {
console.log('Protected data:', data);
})
.catch(error => console.error('Error:', error));
5. 服务器验证Token
- 服务器接收到请求后,从请求头中提取Token,并验证其有效性。
app.get('/protected', (req, res) => {
const token = req.headers['authorization'].split(' ')[1];
if (!token) {
return res.status(403).send('A token is required for authentication');
}
try {
const decoded = jwt.verify(token, secret);
res.json({ data: 'Protected data' });
} catch (err) {
return res.status(401).send('Invalid token');
}
});
6. 响应请求
- 如果Token有效,服务器响应请求,返回所需的数据或执行所需的操作。
- 如果Token无效或已过期,服务器返回401或403状态码,并提示用户重新登录。
49. 页面渲染过程
1. 解析HTML
浏览器解析HTML文档,构建DOM(文档对象模型)树。HTML标签被转换为DOM节点,形成树形结构。
2. 解析CSS
浏览器解析CSS文件和内嵌的样式,构建CSSOM(CSS对象模型)树。CSS规则被转换为CSSOM节点,形成树形结构。
3. 构建渲染树
浏览器将DOM树和CSSOM树结合起来,生成渲染树。渲染树包含了每个节点的可视信息,如样式和位置,但不包含非可视的HTML标签(例如script、meta标签)。
4. 布局
浏览器根据渲染树计算每个节点的布局(位置和大小)。这个过程称为布局或回流。浏览器会从渲染树的根节点开始,逐个计算每个节点的位置和尺寸。
5. 绘制
浏览器将渲染树中的每个节点绘制到屏幕上。绘制过程可能涉及多个绘制阶段(例如背景颜色、边框、文本等),并且可能使用图层来优化渲染性能。
6. JavaScript执行
浏览器执行JavaScript代码,可能会修改DOM树和CSSOM树。这会导致重新计算布局和重新绘制页面。
7. 页面重绘和回流
当页面内容(例如元素的大小、位置)发生变化时,浏览器可能需要重新计算布局和重新绘制页面。这种情况称为回流(reflow)和重绘(repaint)。回流是影响性能的,因为它涉及布局计算,而重绘只涉及视觉更新。
渲染过程示意图
HTML -> DOM树
CSS -> CSSOM树
DOM树 + CSSOM树 -> 渲染树
渲染树 -> 布局(回流)
布局 -> 绘制(重绘)
50. Dom树和渲染树有什么区别
区别总结
- 构建目的:DOM树用于表示文档结构,使JavaScript可以操作;渲染树用于绘制页面,使浏览器能够显示内容。
- 包含内容:DOM树包含所有的HTML节点,渲染树只包含可见节点。
- 构建来源:DOM树仅来源于HTML,渲染树由DOM树和CSSOM树合并而成。
SVG格式是什么
SVG(Scalable Vector Graphics)是一种基于XML的矢量图形格式。与传统的位图图像(如JPEG和PNG)不同,SVG使用几何图形元素(如点、线、曲线和多边形)来描述图像,因此它可以在不同分辨率下无损缩放,非常适合用于需要高质量图像的场景,如网页设计、图标和图形。
了解过JWT么
JWT(JSON Web Token)是一种用于身份验证和信息交换的紧凑型、安全标准。JWT特别适合在Web应用中使用,以下是关于JWT的一些关键点:
JWT的结构
JWT由三部分组成,每部分使用点(.)分隔:
- Header(头部):通常包含签名的算法(如HMAC SHA256或RSA)和令牌类型(JWT)。
- Payload(有效载荷):包含声明(claims),例如用户身份信息、令牌过期时间等。声明可以是标准的,也可以是自定义的。
- Signature(签名):用于验证令牌的真实性,防止数据被篡改。签名是通过对头部和载荷进行编码,然后使用指定的算法和密钥进行签名生成的。
举例如下:
plaintext
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
JWT的使用
- 生成JWT:服务器在用户登录时生成JWT,并将其返回给客户端。
- 存储JWT:客户端通常将JWT存储在LocalStorage、SessionStorage或Cookies中。
- 发送JWT:在后续请求中,客户端将JWT包含在HTTP请求头中(通常是
Authorization: Bearer <token>)。 - 验证JWT:服务器在接收到请求后验证JWT的签名和有效性,确认用户身份。
JWT的优点
- 自包含:JWT包含所有必要的信息,无需查询数据库,因此可以减少服务器负载。
- 灵活性:JWT可以在不同的编程语言和框架中使用,易于集成。
- 安全性:JWT可以使用签名算法进行加密,确保数据的完整性和安全性。
JWT的缺点
- 大小问题:由于包含了所有必要信息,JWT的大小可能较大,可能影响性能。
- 存储问题:将JWT存储在客户端需要注意安全性,避免XSS攻击和泄露风险。
- 无状态:由于JWT是无状态的,如果需要在服务器端撤销令牌,需要额外的机制。
51. npm底层环境是什么
npm(Node Package Manager)的底层环境主要是基于Node.js的运行环境。Node.js是一个开源的、跨平台的JavaScript运行环境,它允许开发人员在服务器端运行JavaScript代码。npm是Node.js的默认包管理器,用于安装、更新和管理Node.js模块。
npm的底层环境包括:
- Node.js核心模块:提供基本的运行环境和系统API。
- npm本身:负责包管理,包括安装、卸载和更新包。
- 包存储库:npm默认使用的包存储库是npm官方注册表,但开发人员也可以配置自己的私人存储库。
- 依赖关系管理:npm可以解析和安装包的依赖关系,确保所有需要的包都被正确安装。
这些组件共同构成了npm的底层环境
52. HTTP协议规定和请求头有什么
HTTP(HyperText Transfer Protocol)是一种用于传输超媒体文档(如HTML)的应用层协议。它是万维网的基础,定义了客户端和服务器之间的通信方式。HTTP协议包括很多规定和请求头字段,下面是一些关键点:
HTTP协议规定
- 请求方法:
GET:请求指定资源,使用URL传递参数。POST:提交数据到指定资源进行处理,如表单提交。PUT:上传指定资源的最新内容。DELETE:请求服务器删除指定的资源。HEAD:请求资源的头部信息,类似于GET,但不返回具体内容。OPTIONS:请求与资源的通信选项。PATCH:对资源进行部分修改。
- 状态码:
1xx:信息性响应,如100 Continue。2xx:成功响应,如200 OK。3xx:重定向响应,如301 Moved Permanently。4xx:客户端错误,如404 Not Found。5xx:服务器错误,如500 Internal Server Error。
HTTP请求头
HTTP请求头是HTTP请求的一部分,包含了有关请求和客户端的元数据。以下是一些常见的HTTP请求头字段:
- 通用请求头:
Host:指定服务器的域名和端口号。User-Agent:包含发出请求的客户端浏览器的信息。Accept:指定客户端接受的媒体类型。
- 请求内容相关头:
Content-Type:指定请求体的媒体类型,如application/json。Content-Length:指定请求体的字节长度。
- 缓存相关头:
Cache-Control:指定请求和响应的缓存机制,如no-cache。
- 身份认证相关头:
Authorization:包含客户端发送的身份验证信息,如Bearer <token>。
- 条件请求头:
If-Modified-Since:仅在资源自给定日期以来修改过时返回资源。If-None-Match:仅在资源的ETag不匹配时返回资源。
示例代码
以下是一个包含HTTP请求头的示例:
fetch('https://api.example.com/data', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer <token>',
'Accept': 'application/json',
'User-Agent': 'Mozilla/5.0'
}
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:',
53. 浏览器缓存策略
1. HTTP 缓存控制头
HTTP 缓存控制头是服务器和浏览器之间用于控制缓存行为的头部字段。常见的缓存控制头包括:
Cache-Control:指定缓存机制,如no-cache、no-store、must-revalidate、max-age等。Expires:指定资源的过期时间,日期格式为 HTTP 日期。ETag:实体标签,用于验证资源是否已修改。Last-Modified:资源的最后修改时间,用于验证资源是否已修改。
2. 强缓存
强缓存策略告诉浏览器直接从缓存中读取资源,而不需要向服务器发送请求。常见的头部字段有:
Cache-Control: max-age=<seconds>:指定资源的缓存时间。Expires: <date>:指定资源的过期日期。
当缓存未过期时,浏览器会直接使用缓存的资源,而不会发送请求到服务器。
3. 协商缓存
协商缓存策略允许浏览器与服务器验证缓存资源的有效性。如果资源未修改,则使用缓存;否则,重新下载。常见的头部字段有:
ETag和If-None-Match:使用 ETag 验证资源是否修改。Last-Modified和If-Modified-Since:使用最后修改时间验证资源是否修改。
浏览器发送请求时,会携带 If-None-Match 或 If-Modified-Since 头部,服务器根据这些头部返回 304 状态码(未修改)或 200 状态码(新资源)。
4. Service Worker 缓存
Service Worker 是一种独立的 JavaScript 工作线程,可以拦截和处理网络请求。通过 Service Worker,可以自定义缓存策略,提高离线和性能体验。
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request).then((fetchResponse) => {
return caches.open('my-cache').then((cache) => {
cache.put(event.request, fetchResponse.clone());
return fetchResponse;
});
});
})
);
});
5. 浏览器缓存机制
浏览器使用多种机制来缓存资源,例如:
- 内存缓存:在内存中存储资源,适用于短期缓存,如页面切换。
- 磁盘缓存:在磁盘上存储资源,适用于长期缓存,如跨会话缓存。
- 持久缓存:使用 LocalStorage、IndexedDB 等持久存储来缓存数据。
54. 什么是同源策略
同源的定义
一个资源的“源”由三个部分组成:
- 协议(如http、https)
- 域名(如example.com)
- 端口(如80、443)
如果两个URL在这三个方面完全相同,它们就被认为是同源的。
55. 防抖和节流?如何实现?
防抖的原理是:某个操作在一定时间内没有再触发时才执行处理函数。如果在这段时间内再次触发,则重新计时。这样可以避免函数在高频率触发时频繁执行。
function debounce(func, wait) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => {
func.apply(this, args);
}, wait);
};
}
// 使用示例
const handleResize = debounce(() => {
console.log('窗口大小改变');
}, 500);
window.addEventListener('resize', handleResize);
节流的原理是:规定在一定时间内只执行一次操作,也就是说无论事件如何频繁触发,都会按照设定的频率来执行。
function throttle(func, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
// 使用示例
const handleScroll = throttle(() => {
console.log('页面滚动');
}, 200);
window.addEventListener('scroll', handleScroll);
总结:
- 防抖:在某个时间段内没有触发操作时才执行一次函数(等待结束后执行)。
- 节流:在某个时间段内只执行一次函数(间隔时间内只执行一次)。
56. 解释一下什么是JSON
它是基于JavaScript编程语言的一个子集,专为数据传输而设计,非常容易被人类和机器读取和编写。
57. 当数据没有请求过来的时候怎么做
1. 显示加载状态
在页面上显示加载状态(例如“加载中...”的提示),告知用户数据正在请求中,避免用户以为页面卡住。
<div id="loading">加载中...</div>
2. 重试机制
实现重试机制,尝试多次请求数据。如果请求失败,可以自动重试。
function fetchDataWithRetry(url, retries = 3) {
return fetch(url)
.then(response => {
if (!response.ok) throw new Error('Network response was not ok');
return response.json();
})
.catch(error => {
if (retries > 0) {
console.log(`Retrying... (${retries})`);
return fetchDataWithRetry(url, retries - 1);
} else {
throw error;
}
});
}
fetchDataWithRetry('https://api.example.com/data')
.then(data => {
console.log(data);
})
.catch(error => {
console.error('Failed to fetch data:', error);
});
3. 错误处理
提供友好的错误处理和提示,告知用户数据加载失败,并提供重试按钮。
<div id="error" style="display: none;">
数据加载失败,请重试。
<button onclick="reloadData()">重试</button>
</div>
<script>
function reloadData() {
document.getElementById('error').style.display = 'none';
// 重新请求数据的逻辑
}
</script>
4. 缓存机制
使用缓存机制保存上一次成功请求的数据,在请求失败时使用缓存的数据,提供较好的用户体验。
function fetchDataWithCache(url) {
const cachedData = localStorage.getItem('data');
if (cachedData) {
return Promise.resolve(JSON.parse(cachedData));
}
return fetch(url)
.then(response => response.json())
.then(data => {
localStorage.setItem('data', JSON.stringify(data));
return data;
})
.catch(error => {
console.error('Failed to fetch data:', error);
});
}
fetchDataWithCache('https://api.example.com/data')
.then(data => {
console.log(data);
});
5. 降级处理
在请求失败时提供简化的数据或占位内容,保证页面功能的基本可用性。
function handleDataRequest(url) {
fetch(url)
.then(response => response.json())
.then(data => {
renderData(data);
})
.catch(error => {
console.error('Failed to fetch data:', error);
renderPlaceholderData();
});
}
function renderPlaceholderData() {
// 渲染占位内容的逻辑
}
58. rem怎么做适配
在Web开发中,使用rem(root em)单位进行适配是一种常见的方法。rem相对于根元素(通常是<html>)的字体大小来计算,从而实现响应式设计和不同屏幕大小的适配。以下是实现rem适配的一些步骤:
1. 设置根元素的字体大小
在CSS中,通过设置<html>元素的字体大小来定义一个基准单位。常见的做法是基于视口宽度(viewport width)来动态计算字体大小。
/* 设置基准字体大小,假设设计稿宽度为375px */
html {
font-size: 100%; /* 默认16px */
}
/* 使用媒体查询进行动态调整 */
@media (min-width: 320px) {
html {
font-size: calc(100% * (100vw / 375));
}
}
2. 使用rem单位
在其他CSS规则中使用rem单位来定义元素的大小、间距等属性。
/* 使用rem单位定义元素样式 */
.container {
width: 20rem; /* 相当于 20 * 根元素字体大小 */
padding: 1rem;
}
h1 {
font-size: 2rem;
}
p {
font-size: 1rem;
}
3. 处理不同的设计稿宽度
根据设计稿的不同宽度,调整根元素的字体大小以实现适配。以下是一个例子,其中设计稿宽度为375px:
/* 根元素字体大小根据视口宽度进行调整 */
html {
font-size: calc(100% * (100vw / 375));
}
4. 使用JavaScript动态调整字体大小
可以通过JavaScript动态调整根元素的字体大小,以更精确地实现适配。
(function() {
function setRemUnit() {
const baseWidth = 375; // 设计稿宽度
const rootElement = document.documentElement;
rootElement.style.fontSize = (rootElement.clientWidth / baseWidth) * 100 + '%';
}
window.addEventListener('resize', setRemUnit);
document.addEventListener('DOMContentLoaded', setRemUnit);
})();
优点
- 响应式设计:使用
rem单位可以轻松实现响应式设计,适配不同屏幕大小。 - 一致性:所有元素的大小相对于根元素的字体大小,使得设计更加一致。
注意事项
- 媒体查询:根据不同的屏幕宽度使用媒体查询来调整根元素的字体大小,以实现更精确的适配。
- 性能:避免过多的计算和重绘,确保页面性能。
59. 如何解决移动端兼容问题
1. 使用媒体查询
媒体查询允许你根据设备的屏幕大小和分辨率应用不同的CSS样式。通过这种方式,你可以为不同设备调整布局和样式。
/* 示例媒体查询 */
@media (max-width: 767px) {
.container {
width: 100%;
padding: 10px;
}
}
@media (min-width: 768px) and (max-width: 1024px) {
.container {
width: 80%;
padding: 20px;
}
}
2. 响应式布局
使用响应式布局技术,如灵活的网格系统(例如Bootstrap、CSS Grid)和弹性盒模型(Flexbox),可以帮助你创建适应各种屏幕尺寸的布局。
/* Flexbox 示例 */
.container {
display: flex;
flex-wrap: wrap;
}
.item {
flex: 1 1 200px; /* 每个项目至少200px宽,且根据剩余空间进行调整 */
}
3. 可伸缩的字体和单位
使用相对单位(如em、rem、百分比)代替绝对单位(如px),以便字体大小和间距能够根据屏幕大小进行调整。
/* 使用rem单位 */
body {
font-size: 1rem;
}
.container {
padding: 2rem;
}
4. 视口元标签
在HTML中添加视口元标签,确保页面在不同设备上正确缩放和渲染。
<meta name="viewport" content="width=device-width, initial-scale=1.0">
5. 测试和调试
通过模拟器和实际设备进行测试,确保你的网页在各种设备和浏览器上都能正常运行。使用开发者工具(如Chrome DevTools)来调试和优化。
6. CSS重置或标准化
使用CSS重置(如Normalize.css)来消除浏览器默认样式的差异,确保在所有浏览器中呈现一致的外观。
7. 图片优化
使用响应式图片(<picture>和<img>标签中的srcset属性)和图像压缩技术,以适应不同分辨率和网络条件。
<!-- 响应式图片示例 -->
<picture>
<source media="(min-width: 800px)" srcset="large.jpg">
<source media="(min-width: 400px)" srcset="medium.jpg">
<img src="small.jpg" alt="Responsive image">
</picture>
8. 外部库和框架
使用外部库和框架,如Bootstrap、Foundation等,它们内置了许多处理移动端兼容性的样式和组件。
9. 优化性能
确保你的网页在移动设备上的加载速度和性能。减少不必要的资源请求、使用延迟加载、优化代码等。