# 前端面经
[TOC]
# HTML 篇
# HTML 语义化
用正确的标签做正确的事情。
html 语义化让页面的内容结构化,结构更清晰,便于对浏览器、搜索引擎解析;即使在没有样式 CSS 情况下也以一种文档格式显示,并且是容易阅读的;
搜索引擎的爬虫也依赖于 HTML 标记来确定上下文和各个关键字的权重,利于 SEO;
使阅读源代码的人对网站更容易将网站分块,便于阅读维护理解。
初探・HTML5 语义化 - 知乎 (zhihu.com)
# <img> 标签上 title 与 alt 属性的区别
alt 是给搜索引擎识别,在图像无法显示时的替代文本;
title 是关于元素的注释信息,主要是给用户解读。
当鼠标放到文字或是图片上时有 title 文字显示。
# href 与 src 的区别
- href:指定资源的位置,用于建立当前页面与引用资源之间的关系(链接),
- src:(source 的缩写),指向外部资源的位置,指向的内容将会应用到文档中当前标签所在位置。
- 遇到 href,页面会并行加载后续内容;而 src 则不同,浏览器需要加载完毕 src 的内容才会继续往下走。
# 很多网站不常用 table iframe 这两个元素的原因
因为浏览器页面渲染的时候是从上至下的,而 table 和 iframe 这两种元素会改变这样渲染规则,他们是要等待自己元素内的内容加载完才整体渲染。用户体验会很不友好。
# HTML5 新增特性
html5 新特性总结 - 斌果 - 博客园 (cnblogs.com)
新的语义标签
标签 描述 <header> 定义了文档的头部区域 <footer> 定义了文档的尾部区域 <nav> 定义文档的导航 <section> 定义文档中的节 <article> 定义文章 <aside> 定义页面以外的内容 <details> 定义用户可以看到或者隐藏的额外细节 <summary> 标签包含 details 元素的标题 <dialog> 定义对话框 <figure> 定义自包含内容,如图表 <main> 定义文档主内容 <mark> 定义文档的主内容 <time> 定义日期 / 时间 画布 (Canvas) API
地理定位 (Geolocation) API
本地离线存储 localStorage 长期存储数据,浏览器关闭后数据不丢失;
sessionStorage 的数据在浏览器关闭后自动删除新的技术 webworker, websocket, Geolocation
拖拽释放 API
音频、视频 API (audio,video)
表单控件,calendar、date、time、email、url、searc
# 块级标签,行内标签,行内块标签
块级元素:独占一行,对宽高的属性值生效;如果不给宽度,块级元素就默认为浏览器的宽度,即就是 100% 宽;
块标签:包含 div、p、ul、ol、li、dl、h1~h6、form、table;
行内元素:可以多个标签存在一行,对宽高属性值不生效,完全靠内容撑开宽高!
行内标签:包含 a、button、span、em、i、strong、b、img、input、label、br;
行内块元素:结合的行内和块级的优点(不仅可以对宽高属性值生效,还可以多个标签存在一行显示);
行内块标签:img,input,textarea
各种标签之间的转换
- 块级标签转换为行内标签:display:inline;
- 行内标签转换为块级标签:display:block;
- 转换为行内块标签:display:inline-block;
# XHTML 和 HTML 的区别
- html 元素必须正确嵌套,不能乱;
- 属性必须是小写的;
- 属性值必须加引号;
- 标签必须有结束,单标签也应该用 “/” 来结束掉;
# 每个 HTML 文件里开头都有个很重要的东西,Doctype,知道这是干什么的吗?
声明位于文档中的最前面的位置,处于 标签之前。此标签可告知浏览器文档使用哪种 HTML 或 XHTML 规范。(重点:告诉浏览器按照何种规范解析页面)
# 渐进增强和优雅降级之间的不同
- 渐进增强 progressive enhancement:针对低版本浏览器进行构建页面,保证最基本的功能,然后再针对高级浏览器进行效果、交互等改进和追加功能达到更好的用户体验。
- 优雅降级 graceful degradation:一开始就构建完整的功能,然后再针对低版本浏览器进行兼容
# CSS 篇
# 盒子模型的介绍
盒模型: 内容 (content)、填充 (padding)、 边框 (border)、边界 (margin);
类型:IE 盒子模型,标准 W3C 盒子模型
区 别: IE 的 content 的 width 和 height 把 border 和 padding 计算了进去;
# css 选择器优先级
单个选择器:
!important > 行内样式(比重 1000)> ID 选择器(比重 100) > 类选择器(比重 10) > 标签(比重 1) > 通配符 > 继承 > 浏览器默认属性
复合选择器:
# 垂直居中的几种方式
- 单行文本: line-height = height
- 图片: vertical-align: middle;
- flex: justify-content:center
- transform + absolute : top: 50%;left: 50%;transform: translate(-50%, -50%);
- absolute+margin 负值:top: 50%;left: 50%;margin-top: -50px;margin-left: -100px;
- 暂略
- CSS 垂直居中,你会多少种写法? - 知乎 (zhihu.com)
# 简明说一下 CSS link 与 @import 的区别和用法?
- ** 加载顺序:**link 是先将 css 文件加载到网页,然后再进行编译。@import 是先加载完 html 结构再加载 css 文件,如果网速较慢则会影响视觉效果。
- ** 兼容性:**link 是 xhtml 标签无兼容问题,@import 是 css2.1 提出的所以不支持 IE5 以前的浏览器。
- **DOM 支持:**link 支持 DOM 改变样式,@import 不支持。
# rgba 和 opacity 的透明效果有什么不同?
- opacity :子元素会继承父元素的 opacity 属性;
- RGBA :设置的元素的后代元素不会继承不透明属性。
# display:none 和 visibility:hidden 的区别?
- display:none :隐藏对应的元素,在文档布局中不再给它分配空间,它各边的元素会合拢,就当他从来不存在;
- visibility:hidden :隐藏对应的元素,但是在文档布局中仍保留原来的空间。
# 有哪些方式可以对一个 DOM 设置它的 CSS 样式?
- 外部样式表,引入一个外部 css 文件
- 内部样式表,将 css 代码放在 标签内部
- 内联样式,将 css 样式直接定义在 HTML 元素内部
# 文档流是什么?
文档流也称为普通流,即网页在解析时,遵循从上向下,从左向右的顺序。
css 中的定位机制,共三种:
- 正常的文档流
- float
- postion 的 absolute
- flex
# position 的值, relative 和 absolute 分别是相对于谁进行定位的?
- relative: 相对定位,相对于自己本身在正常文档流中的位置进行定位。
- absolute: 生成绝对定位,相对于最近一级定位不为 static 的父元素进行定位。
- fixed: (老版本 IE 不支持)生成绝对定位,相对于浏览器窗口或者 frame 进行定
- static: 默认值,没有定位,元素出现在正常的文档流中。
- sticky: 生成粘性定位的元素,容器的位置根据正常文档流计算得出。
# div+css 的布局较 table 布局有什么优点?
- 改版的时候更方便 只要改 css 文件。
- 页面加载速度更快、结构化清晰、页面显示简洁。
- 表现与结构相分离。
- 易于优化(seo)搜索引擎更友好,排名更容易靠前。
# 如何创建块级格式化上下文 (block formatting context),BFC 有什么用?
什么是 BFC?
BFC 格式化上下文,它是一个独立的渲染区域,让处于 BFC 内部的元素和外部的元素相互隔离,使内外元素的定位不会相互影响
如何产生 BFC?
display: inline-block
position: absolute/fixed
BFC 作用
BFC 最大的一个作用就是:在页面上有一个独立隔离容器,容器内的元素和容器外的元素布局不会相互影响。
# CSS3 有哪些新特性?
弹性盒模型 display: flex;
颜色透明度 color: rgba(255, 0, 0, 0.75);
圆角 border-radius: 5px;
阴影 box-shadow:3px 3px 3px rgba(0, 64, 128, 0.3);
2d,3d 变换;
平滑过渡 transition: all .3s ease-in .1s;
动画 @keyframes anim-1 {50% {border-radius: 50%;}} animation: anim-1 1s;
新增伪类选择器::checked、:enabled、:disabled
暂略
# CSS3 动画(简单动画的实现,如旋转等)
依靠 CSS3 中提出的三个属性:transition、transform、animation
- transition:定义了元素在变化过程中是怎么样的,包含 transition-property、transition-duration、transition-timing-function、transition-delay。
- transform:定义元素的变化结果,包含 rotate、scale、skew、translate。
- animation:动画定义了动作的每一帧(@keyframes)有什么效果,包括 animation-name,animation-duration、animation-timing-function、animation-delay、animation-iteration-count、animation-direction
# 如何清除浮动?
# 常见兼容性问题?
- 浏览器默认的 margin 和 padding 不同。解决方案是加一个全局的 *{margin:0;padding:0;} 来统一。
- Chrome 中文界面下默认会将小于 12px 的文本强制按照 12px 显示,
可通过加入 CSS 属性 -webkit-text-size-adjust: none; 解决.
# canvas 与 svg 的区别?
canvas 是 H5 时期才有绘图方法,而 svg 已经有了十多年的历史(XML);
canvas 绘图是基于像素点,是位图,如果进行放大或缩小会失真;svg 基于图形,是矢图用 html 标签描绘形状,放大缩小不会失真;
canvas 需要在 js 中绘制,svg 在 html 绘制;
canvas 支持颜色比 svg 多;
canvas 无法对已经绘制的图像进行修改、操作,svg 可以获取到标签进行操作。
矢量图 表现的图像颜色比较单一,所以所占用的空间会很小。
位图 表现的色彩比较丰富,所以占用的空间会很大,颜色信息越多,占用空间越大,图像越清晰,占用空间越大。矢量图 色彩不丰富,无法表现逼真的实物,矢量图常常用来表示标识、图标、Logo 等简单直接的图像。
位图 表现的色彩比较丰富,可以表现出色彩丰富的图象,可逼真表现自然界各类实物。
# 画三角形
#box { | |
width: 0px; | |
height: 0px; | |
border-top: 20px solid red; | |
border-right: 20px solid transparent; | |
border-bottom: 20px solid transparent; | |
border-left: 20px solid transparent; | |
} |
# JS 篇
# JS 数据类型
- 基本数据类型: Undefined、Null、Boolean、Number、String、Symbol
- 引用数据类型: Object (包括 Object 、Array 、Function、Date、Set、Map、RegExp)
- 声明变量时不同的内存地址分配:
- 简单类型的值存放在栈中,在栈中存放的是对应的值
- 引用类型对应的值存储在堆中,在栈中存放的是指向堆内存的地址
- 不同的类型数据导致赋值变量时的不同:
- 简单类型赋值,是生成相同的值,两个对象对应不同的地址
- 复杂类型赋值,是将保存对象的内存地址赋值给另一个变量。也就是两个变量指向堆内存中同一个对象
# == 和 === 的区别
等于操作符
操作符(==)在比较中会先进行类型转换,再确定操作数是否相等
遵循以下规则:
如果任一操作数是布尔值,则将其转换为数值再比较是否相等;
let result1 = (true == 1); // true
如果一个操作数是字符串,另一个操作数是数值,则尝试将字符串转换为数值,再比较是否相等;
let result1 = ("55" == 55); // true
如果一个操作数是对象,另一个操作数不是,则调用对象的
valueOf()
方法取得其原始值,再根据前面的规则进行比较;let obj = {valueOf:function(){return 1}}
let result1 = (obj == 1); // true
null
和undefined
相等;let result1 = (null == undefined ); // true
如果有任一操作数是
NaN
,则相等操作符返回false
;let result1 = (NaN == NaN ); // false
如果两个操作数都是对象,则比较它们是不是同一个对象。如果两个操作数都指向同一个对象,则相等操作符返回
true
;let obj1 = {name:"xxx"}
let obj2 = {name:"xxx"}
let result1 = (obj1 == obj2 ); // false
简单小结:
- 两个都为简单类型,字符串和布尔值都会转换成数值,再比较
- 简单类型与引用类型比较,对象转化成其原始类型的值,再比较
- 两个都为引用类型,则比较它们是否指向同一个对象
- null 和 undefined 相等
- 存在 NaN 则返回 false
全等操作符
全等操作符由 3 个等于号( === )表示,只有两个操作数在不转换的前提下相等才返回
true
。即类型相同,值也需相同;undefined
和null
与自身严格相等小结
除了在比较对象属性为
null
或者undefined
的情况下,我们可以使用相等操作符(==),其他情况建议一律使用全等操作符(===);相等运算符隐藏的类型转换,会带来一些违反直觉的结果
'' == '0' // false
0 == '' // true
0 == '0' // true
false == 'false' // false
false == '0' // true
false == undefined // false
false == null // false
null == undefined // true
' \t\r\n' == 0 // true
# null 和 undefined 的区别?
- null 表示一个对象被定义了,值为 “空值”;
- undefined 表示不存在这个值。
- 变量被声明了,但没有赋值时,就等于 undefined
- 调用函数时,应该提供的参数没有提供,该参数等于 undefined
- 对象没有赋值的属性,该属性的值为 undefined
- 函数没有返回值时,默认返回 undefined。
# eval 是做什么的?
它的功能是把对应的字符串解析成 JS 代码并运行;
应该避免使用 eval,不安全,非常耗性能(2 次,一次解析成 js 语句,一次执行);
# 箭头函数的特点
- 不需要 function 关键字来创建函数;
- 省略 return 关键字;
- 改变 this 指向;
# var、let、const 区别?
- var 存在变量提升。
- let 只能在块级作用域内访问。
- const 用来定义常量,必须初始化,不能修改(对象特殊)。
# DOM 的常见操作
文档对象模型 (DOM) 是 HTML
和 XML
文档的编程接口
它提供了对文档的结构化的表述,并定义了一种方式可以使从程序中对该结构进行访问,从而改变文档的结构,样式和内容
创建节点
createElement
创建新元素,接受一个参数,即要创建元素的标签名
const divEl = document.createElement("div"); |
createTextNode
创建一个文本节点
const textEl=document.createTextNode('content') |
createDocumentFragment
用来创建一个文档碎片,它表示一种轻量级的文档,主要是用来存储临时节点,然后把文档碎片的内容一次性添加到 DOM
中(当请求把一个 DocumentFragment
节点插入文档树时,插入的不是 DocumentFragment
自身,而是它的所有子孙节点)
const fragment = document.createDocumentFragment() |
createAttribute
创建属性节点,可以是自定义属性
const attribute = document.createAttribute('custom'); |
获取节点
querySelector
querySelector
传入任何有效的 css
选择器,即可选中单个 DOM
元素(首个):
document.querySelector('.element') | |
document.querySelector('#element') | |
document.querySelector('div') | |
document.querySelector('[name="username"]') | |
document.querySelector('div + p > span') |
querySelectorAll
返回一个包含节点子树内所有与之相匹配的 Element
节点列表,如果没有相匹配的,则返回一个空节点列表
const notLive = document.querySelectorAll("p"); |
关于获取 DOM
元素的方法还有如下,就不一一述说
document.getElementById('id属性值');返回拥有指定id的对象的引用 | |
document.getElementsByClassName('class属性值');返回拥有指定class的对象集合 | |
document.getElementsByTagName('标签名');返回拥有指定标签名的对象集合 | |
document.getElementsByName('name属性值'); 返回拥有指定名称的对象结合 | |
document/element.querySelector('CSS选择器'); 仅返回第一个匹配的元素 | |
document/element.querySelectorAll('CSS选择器'); 返回所有匹配的元素 | |
document.documentElement; 获取页面中的HTML标签 | |
document.body; 获取页面中的BODY标签 | |
document.all['']; 获取页面中的所有元素节点的对象集合型 |
更新节点
innerHTML
不但可以修改一个 DOM
节点的文本内容,还可以直接通过 HTML
片段修改 DOM
节点内部的子树
// 获取 & lt;p id="p">...</p > | |
var p = document.getElementById('p'); | |
// 设置文本为 abc: | |
p.innerHTML = 'ABC'; // <p id="p">ABC</p > | |
// 设置 HTML: | |
p.innerHTML = 'ABC <span style="color:red">RED</span> XYZ'; | |
// <p>...</p > 的内部结构已修改 |
style
DOM
节点的 style
属性对应所有的 CSS
,可以直接获取或设置。遇到 -
需要转化为驼峰命名
// 获取 & lt;p id="p-id">...</p > | |
const p = document.getElementById('p-id'); | |
// 设置 CSS: | |
p.style.color = '#ff0000'; | |
p.style.fontSize = '20px'; // 驼峰命名 | |
p.style.paddingTop = '2em'; |
添加节点
appendChild
把一个子节点添加到父节点的最后一个子节点
insertBefore
把子节点插入到指定的位置,使用方法如下
setAttribute
在指定元素中添加一个属性节点,如果元素中已有该属性改变属性值
删除节点
删除一个节点,首先要获得该节点本身以及它的父节点,然后,调用父节点的 removeChild
把自己删掉
// 拿到待删除节点: | |
const self = document.getElementById('to-be-removed'); | |
// 拿到父节点: | |
const parent = self.parentElement; | |
// 删除: | |
const removed = parent.removeChild(self); | |
removed === self; // true |
删除后的节点虽然不在文档树中了,但其实它还在内存中,可以随时再次被添加到别的位置.
# 对 BOM 的理解
BOM
(Browser Object Model),浏览器对象模型,提供了独立于内容与浏览器窗口进行交互的对象
其作用就是跟浏览器做一些交互效果,比如如何进行页面的后退,前进,刷新,浏览器的窗口发生变化,滚动条的滚动,以及获取客户的一些信息如:浏览器品牌版本,屏幕分辨率。
window
Bom
的核心对象是 window
,它表示浏览器的一个实例。在浏览器中, window
对象有双重角色,即是浏览器窗口的一个接口,又是全局对象。因此所有在全局作用域中声明的变量、函数都会变成 window
对象的属性和方法
location
url
地址如下:
http://foouser:barpassword@www.wrox.com:80/WileyCDA/?q=javascript#contents |
location
属性描述如下:
属性名 | 例子 | 说明 |
---|---|---|
hash | "#contents" | utl 中 #后面的字符,没有则返回空串 |
host | www.wrox.com:80 | 服务器名称和端口号 |
hostname | www.wrox.com | 域名,不带端口号 |
href | http://www.wrox.com:80/WileyCDA/?q=javascript#contents | 完整 url |
pathname | "/WileyCDA/" | 服务器下面的文件路径 |
port | 80 | url 的端口号,没有则为空 |
protocol | http: | 使用的协议 |
search | ?q=javascript | url 的查询字符串,通常为?后面的内容 |
除了 hash
之外,只要修改 location
的一个属性,就会导致页面重新加载新 URL
location.reload()
,此方法可以重新刷新当前页面。这个方法会根据最有效的方式刷新页面,如果页面自上一次请求以来没有改变过,页面就会从浏览器缓存中重新加载
navigator
navigator
对象主要用来获取浏览器的属性,区分浏览器类型。
history
history
对象主要用来操作浏览器 URL
的历史记录,可以通过参数向前,向后,或者向指定 URL
跳转
常用的属性如下:
history.go()
接收一个整数数字或者字符串参数:向最近的一个记录中包含指定字符串的页面跳转,
history.go('maixaofei.com') |
当参数为整数数字的时候,正数表示向前跳转指定的页面,负数为向后跳转指定的页面
history.go(3) // 向前跳转三个记录 | |
history.go(-1) // 向后跳转一个记录 |
history.forward()
:向前跳转一个页面history.back()
:向后跳转一个页面history.length
:获取历史记录数
screen
保存的纯粹是客户端能力信息,也就是浏览器窗口外面的客户端显示器的信息,比如像素宽度和像素高度
面试官:说说你对 BOM 的理解,常见的 BOM 对象你了解哪些? | web 前端面试 - 面试官系列 (vue3js.cn)
# 宏任务与微任务
- JS 分为同步任务和异步任务
- 同步任务都在主线程上执行,形成一个执行栈
- 主线程之外,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放置一个事件。
- 一旦执行栈中的所有同步任务执行完毕(此时 JS 引擎空闲),系统就会读取任务队列,将可运行的异步任务添加到可执行栈中,开始执行。
宏任务
(macro)task,可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)。浏览器为了能够使得 JS 内部 (macro) task 与 DOM 任务能够有序的执行,会在一个 (macro) task 执行结束后,在下一个 (macro) task 执行开始前,对页面进行重新渲染;
宏任务包含:
- script (整体代码)
- setTimeout
- setInterval
- I/O
- UI 交互事件
- postMessage
- MessageChannel
- setImmediate (Node.js 环境)
微任务
microtask, 可以理解是在当前 task 执行结束后立即执行的任务。也就是说,在当前 task 任务后,下一个 task 之前,在渲染之前。
微任务报含:
- Promise.then、
- Object.observe
- MutationObserver
- process.nextTick (Node.js 环境)
运行机制
在事件循环中,每进行一次循环操作称为 tick,每一次 tick 的任务处理模型是比较复杂的,但关键步骤如下:
- 执行一个宏任务(栈中没有就从事件队列中获取)
- 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
- 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
- 当前宏任务执行完毕,开始检查渲染,然后 GUI 线程接管渲染
- 渲染完毕后,JS 线程继续接管,开始下一个宏任务(从事件队列中获取)
# new 操作符具体干了什么呢?
创建一个新的对象
obj
;将新对象与构建函数
Mother
的原型通过原型链连接起来;obj.__proto__=Mother.prototype
将构建函数中的
this
绑定到新建的对象obj
上;根据构建函数返回类型作判断,如果是原始值则被忽略,如果是返回对象,需要正常处理
手写 new
function mynew(Func, ...args) { | |
// 1. 创建一个新对象 | |
const obj = {} | |
// 2. 新对象原型指向构造函数原型对象 | |
obj.__proto__ = Func.prototype | |
// 3. 将构建函数的 this 指向新对象 | |
let result = Func.apply(obj, args) | |
// 4. 根据返回值判断 | |
return result instanceof Object ? result : obj | |
} |
# JSON 是什么?
JSON 的全称是”JavaScript Object Notation”,意思是 JavaScript 对象表示法,它是一种基于文本,独立于语言的轻量级数据交换格式。
# Ajax 是什么?如何创建一个 Ajax?
AJAX 是一种用于创建快速动态网页的技术。通过在后台与服务器进行少量数据交换,AJAX 可以使网页实现异步更新。这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更新。而传统的网页(不使用 AJAX)如果需要更新内容,必需重载整个网页面。
Ajax
的原理简单来说通过 XmlHttpRequest
对象来向服务器发异步请求,从服务器获得数据,然后用 JavaScript
来操作 DOM
而更新页面
- 创建 XMLHttpRequest 对象,也就是创建一个异步调用对象;
- 创建一个新的 HTTP 请求,并指定该 HTTP 请求的方法、URL 及验证信息;
- 设置响应 HTTP 请求状态变化的函数;
- 发送 HTTP 请求;
- 获取异步调用返回的数据;
- 使用 JavaScript 和 DOM 实现局部刷新;
面试官:ajax 原理是什么?如何实现? | web 前端面试 - 面试官系列 (vue3js.cn)
# call、apply、bind 方法的区别
call
和 apply
可以用来重新定义函数的执行环境,也就是 this
的指向; call
和 apply
都是为了改变某个函数运行时的 context
,即上下文而存在的,换句话说,就是为了改变函数体内部 this
的指向。
从定义中也可以看出来, call()
和 apply()
的不同点就是接收参数的方式不同。
People.call(this, name, age); | |
People.apply(this, [name, age]); |
- apply () 方法接收两个参数,一个是函数运行的作用域(
this
),另一个是参数数组。 - call () 方法不一定接受两个参数,第一个参数也是函数运行的作用域(
this
),但是传递给函数的参数必须列举出来。 apply
和call
是一次性传入参数,而bind
可以分为多次传入,且bind
是返回绑定 this 之后的函数,apply
、call
则是立即执行。
# innerHTML、innerText、outerHTML 的区别?
innerHTML:对象的起始位置到终止位置的全部内容,包括 Html 标签。
innerText :从起始位置到终止位置的内容,但它去除 Html 标签。
outerHTML:除了包含 innerHTML 的全部内容外,还包含对象标签本身。
# documen.write 和 innerHTML 的区别?
- document.write 只能重绘整个页面;
- innerHTML 可以重绘页面的一部分;
# typeof 与 instanceof 区别
typeof
操作符返回一个字符串,表示未经计算的操作数的类型;
typeof 1 // 'number' | |
typeof '1' // 'string' | |
typeof undefined // 'undefined' | |
typeof true // 'boolean' | |
typeof Symbol() // 'symbol' | |
typeof console.log // 'function' | |
typeof null // 'object' | |
typeof [] // 'object' | |
typeof {} // 'object' | |
typeof console // 'object' |
如果我们想要判断一个变量是否存在,可以使用 typeof
:
if(typeof a != 'undefined'){ | |
// 变量存在 | |
} |
instanceof
运算符用于检测构造函数的 prototype
属性是否出现在某个实例对象的原型链上,即构造函数通过 new
可以实例对象, instanceof
能判断这个对象是否是之前那个构造函数生成的对象;
实现原理:
function myInstanceof(left, right) { | |
// 这里先用 typeof 来判断基础数据类型,如果是,直接返回 false | |
if(typeof left !== 'object' || left === null) return false; | |
//getProtypeOf 是 Object 对象自带的 API,能够拿到参数的原型对象 | |
let proto = Object.getPrototypeOf(left); | |
while(true) { | |
if(proto === null) return false; | |
if(proto === right.prototype) return true;// 找到相同原型对象,返回 true | |
proto = Object.getPrototypeof(proto); | |
} | |
} |
typeof
与 instanceof
都是判断数据类型的方法,区别如下:
typeof
会返回一个变量的基本类型,instanceof
返回的是一个布尔值instanceof
可以准确地判断复杂引用数据类型,但是不能正确判断基础数据类型- 而
typeof
也存在弊端,它虽然可以判断基础数据类型(null
除外),但是引用数据类型中,除了function
类型以外,其他的也无法判断
如果需要通用检测数据类型,可以采用 Object.prototype.toString
,调用该方法,统一返回格式 “[object Xxx]”
的字符串
# 事件代理
适合事件委托的事件有: click
, mousedown
, mouseup
, keydown
, keyup
, keypress
从上面应用场景中,我们就可以看到使用事件委托存在两大优点:
- 减少整个页面所需的内存,提升整体性能
- 动态绑定,减少重复工作
但是使用事件委托也是存在局限性:
focus
、blur
这些事件没有事件冒泡机制,所以无法进行委托绑定事件mousemove
、mouseout
这样的事件,虽然有事件冒泡,但是只能不断通过位置去计算定位,对性能消耗高,因此也是不适合于事件委托的
# JS 的事件模型及其事件流
javascript
中的事件,可以理解就是在 HTML
文档或者浏览器中发生的一种交互操作,使得网页具备互动性, 常见的有加载事件、鼠标事件、自定义事件等
由于 DOM
是一个树结构,如果在父子节点绑定事件时候,当触发子节点的时候,就存在一个顺序问题,这就涉及到了事件流的概念
事件流都会经历三个阶段:
- 事件捕获阶段 (capture phase)
- 处于目标阶段 (target phase)
- 事件冒泡阶段 (bubbling phase)
事件模型:
事件模型可以分为三种:
- 原始事件模型(DOM0 级)
- 标准事件模型(DOM2 级)
- IE 事件模型(基本不用)
原始事件模型
事件绑定监听函数比较简单,有两种方式:
- HTML 代码中直接绑定
<input type="button" onclick="fun()"> |
- 通过
JS
代码绑定
var btn = document.getElementById('.btn'); | |
btn.onclick = fun; |
特性:
绑定速度快
DOM0
级事件具有很好的跨浏览器优势,会以最快的速度绑定,但由于绑定速度太快,可能页面还未完全加载出来,以至于事件可能无法正常运行只支持冒泡,不支持捕获
同一个类型的事件只能绑定一次
标准事件模型
在该事件模型中,一次事件共有三个过程:
- 事件捕获阶段:事件从
document
一直向下传播到目标元素,依次检查经过的节点是否绑定了事件监听函数,如果有则执行 - 事件处理阶段:事件到达目标元素,触发目标元素的监听函数
- 事件冒泡阶段:事件从目标元素冒泡到
document
, 依次检查经过的节点是否绑定了事件监听函数,如果有则执行
btn.addEventListener('eventType', handler, useCapture) |
参数如下:
eventType
指定事件类型 (不要加 on)handler
是事件处理函数useCapture
是一个boolean
用于指定是否在捕获阶段进行处理,一般设置为false
与 IE 浏览器保持一致
特性:
- 可以在一个
DOM
元素上绑定多个事件处理器,各自并不会冲突; - 执行时机可能选择。当第三个参数 (
useCapture
) 设置为true
就在捕获过程中执行,反之在冒泡过程中执行处理函数;
btn.addEventListener('click',function(){ | |
console.log('btn') | |
},true) | |
box.addEventListener('click',function(){ | |
console.log('box') | |
},false) | |
// 设置为 true,则事件在捕获阶段执行,为 false 则在冒泡阶段执行。 |
IE 事件模型
IE 事件模型共有两个过程:
- 事件处理阶段:事件到达目标元素,触发目标元素的监听函数。
- 事件冒泡阶段:事件从目标元素冒泡到
document
, 依次检查经过的节点是否绑定了事件监听函数,如果有则执行
# 请解释一下 JavaScript 的同源策略?
- 概念:同源策略是客户端脚本(尤其是 Netscape Navigator2.0,其目的是防止某个文档或脚本从多个不同源装载。
- 所谓 “同源” 指的是” 三个相同 “。相同的域名、端口和协议,这三个相同的话就视为同一个域,本域下的 JS 脚本只能读写本域下的数据资源,无法访问其它域的资源。
- 同源策略是一种安全协议,指一段脚本只能读取来自同一来源的窗口和文档的属性。
同源策略的作用
- 无法用 js 读取非同源的 Cookie、LocalStorage 和 IndexDB:这个主要是为了防止恶意网站通过 js 获取用户其他网站的 cookie 等用户信息。
- 无法用 js 获取非同源的 DOM:防止恶意网站通过 iframe 获取页面 dom,从而窃取页面的信息。
- 无法用 js 发送非同源的 AJAX 请求:防止恶意的请求攻击服务器窃取数据信息。
# 跨域问题的解决方案
JSONP
创建一个 script 标签,将 src 改成我们要请求的接口,并将 script 添加在 body 中,那么当浏览器解析到这个 script 时,会想 src 对应的服务器发送一个 get 请求,并将参数带过去。
然后当浏览器接收到服务端返回的数据,就会触发参数中 callbak 对应的回调函数 cb,从而完成整个 get 请求。
var script = document.createElement('script'); | |
script.src = 'http://localhost:3000/api/test.do?a=1&b=2&callback=cb'; | |
$('body').append(script); | |
function cb(res){ | |
// do something | |
console.log(res) | |
} |
- 只支持 get 请求
- 需要后台配合,将返回结果包装成 callback (res) 的形式
cors
cors 是一个 W3C 标准,全称是 "跨域资源共享"(Cross-origin resource sharing),它允许浏览器向跨源服务器发送 XMLHttpRequest 请求,从而克服了 AJAX 只能同源使用的限制
cors 需要浏览器和服务器同时支持,整个 CORS 通信过程,都是浏览器自动完成不需要用户参与,对于开发者来说,cors 的代码和正常的 ajax 没有什么差别,浏览器一旦发现跨域请求,就会添加一些附加的头信息。
- 请求方式只能是:GET、POST、HEAD
- HTTP 请求头限制这几种字段:Accept、Accept-Language、Content-Language、Content-Type、Last-Event-ID
- Content-type 只能取:application/x-www-form-urlencoded、multipart/form-data、text/plain
服务器反向代理
通过服务器的方向代理,将前端访问域名跟后端服务域名映射到同源的地址下,从而实现前端服务和后端服务的同源
跨域问题
# 什么是执行上下文?
简而言之,执行上下文就是当前 JavaScript 代码被解析和执行时所在环境的抽象概念, JavaScript 中运行任何的代码都是在执行上下文中运行。
执行上下文的类型
执行上下文总共有三种类型:
- 全局执行上下文: 这是默认的、最基础的执行上下文。不在任何函数中的代码都位于全局执行上下文中。它做了两件事:1. 创建一个全局对象,在浏览器中这个全局对象就是 window 对象。2. 将 this 指针指向这个全局对象。一个程序中只能存在一个全局执行上下文。
- 函数执行上下文: 每次调用函数时,都会为该函数创建一个新的执行上下文。每个函数都拥有自己的执行上下文,但是只有在函数被调用的时候才会被创建。
- Eval 函数执行上下文: 运行在 eval 函数中的代码也获得了自己的执行上下文。
执行上下文的生命周期
执行上下文的生命周期包括三个阶段:创建阶段 → 执行阶段 → 回收阶段
创建阶段
创建阶段即当函数被调用,但未执行任何其内部代码之前
创建阶段做了三件事:
- 确定 this 的值,也被称为
This Binding
- LexicalEnvironment(词法环境) 组件被创建
- VariableEnvironment(变量环境) 组件被创建
- 确定 this 的值,也被称为
执行阶段
在这阶段,执行变量赋值、代码执行
回收阶段
执行上下文出栈等待虚拟机回收执行上下文
# 执行栈
执行栈,也叫调用栈,具有 LIFO(后进先出)结构,用于存储在代码执行期间创建的所有执行上下文。
当 Javascript
引擎开始执行你第一行脚本代码的时候,它就会创建一个全局执行上下文然后将它压到执行栈中;
每当引擎碰到一个函数的时候,它就会创建一个函数执行上下文,然后将这个执行上下文压到执行栈中;
引擎会执行位于执行栈栈顶的执行上下文 (一般是函数执行上下文),当该函数执行结束后,对应的执行上下文就会被弹出,然后控制流程到达执行栈的下一个执行上下文。
# 解决 Javascript 数字精度丢失的问题
function strip(num, precision = 12) { | |
return +parseFloat(num.toPrecision(precision)); | |
} |
# 如何判断一个元素是否在可视区域中
面试官:如何判断一个元素是否在可视区域中? | web 前端面试 - 面试官系列 (vue3js.cn)
# this 的理解
this 永远指向一个对象;
this 的指向完全取决于函数调用的位置
根据不同的使用场合,
this
有不同的值,主要分为下面几种情况:默认绑定
隐式绑定
new 绑定
显示绑定
其中:new 绑定优先级 > 显示绑定优先级 > 隐式绑定优先级 > 默认绑定优先级
面试官:谈谈 this 对象的理解 | web 前端面试 - 面试官系列 (vue3js.cn)
# Promise 解决的问题
Promise 到底解决了哪些问题? - 知乎 (zhihu.com)
# 介绍一下闭包和闭包的常用场景?
- 闭包是指有权访问另一个函数作用域中的变量的函数,创建闭包常见方式,就是在一个函数的内部创建另一个函数。
- 使用闭包主要为了设计私有的方法和变量,闭包的优点是可以避免变量的污染,缺点是闭包会常驻内存,会增大内存使用量,使用不当很容易造成内存泄露。
在 js 中,函数即闭包,只有函数才会产生作用域的概念。
闭包有三个特性:
- 函数嵌套函数
- 函数内部可以引用外部的参数和变量
- 参数和变量不会被垃圾回收机制回收
应用场景:
创建私有变量
function makeSizer(size) {
return function() {
document.body.style.fontSize = size + 'px';
};
}
var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);
document.getElementById('size-12').onclick = size12;
document.getElementById('size-14').onclick = size14;
document.getElementById('size-16').onclick = size16;
延长变量的生命周期
不适用场景:返回闭包的函数是个非常大的函数
闭包的缺点就是常驻内存,会增大内存使用量,使用不当会造成内存泄漏
# javascript 的内存 (垃圾) 回收机制?
- 垃圾回收器会每隔一段时间找出那些不再使用的内存,然后为其释放内存
- 一般使用标记清除方法 (mark and sweep), 当变量进入环境标记为进入环境,离开环境标记为离开环境
垃圾回收器会在运行的时候给存储在内存中的所有变量加上标记,然后去掉环境中的变量以及被环境中变量所引用的变量(闭包),在这些完成之后仍存在标记的就是要删除的变量了 - 还有引用计数方法 (reference counting), 在低版本 IE 中经常会出现内存泄露,很多时候就是因为其采用引用计数方式进行垃圾回收。引用计数的策略是跟踪记录每个值被使用的次数,当声明了一个 变量并将一个引用类型赋值给该变量的时候这个值的引用次数就加 1,如果该变量的值变成了另外一个,则这个值得引用次数减 1,当这个值的引用次数变为 0 的时 候,说明没有变量在使用,这个值没法被访问了,因此可以将其占用的空间回收,这样垃圾回收器会在运行的时候清理掉引用次数为 0 的值占用的空间。
[JavaScript 垃圾回收机制 - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/60336501#:~:text= JS 的垃圾回收机制是为了以防内存泄漏,内存泄漏的含义就是当已经不需要某块内存时这块内存还存在着,垃圾回收机制就是间歇的不定期的寻找到不再使用的变量,并释放掉它们所指向的内存。,C%23、Java、JavaScript 有自动垃圾回收机制,但 c%2B%2B 和 c 就没有垃圾回收机制,也许是因为垃圾回收机制必须由一种平台来实现。 在 JS 中,JS 的执行环境会负责管理代码执行过程中使用的内存。)
# 作用域链的理解?
我们一般将作用域分成:
全局作用域
任何不在函数中或是大括号中声明的变量,都是在全局作用域下,全局作用域下声明的变量可以在程序的任意位置访问
函数作用域
函数作用域也叫局部作用域,如果一个变量是在函数内部声明的它就在一个函数作用域下面。这些变量只能在函数内部访问,不能在函数以外去访问
块级作用域
ES6 引入了
let
和const
关键字,和var
关键字不同,在大括号中使用let
和const
声明的变量存在于块级作用域中。在大括号之外不能访问这些变量
# JavaScript 原型,原型链?有什么特点?
JavaScript
常被描述为一种基于原型的语言 —— 每个对象拥有一个原型对象,即每个对象都会在其内部初始化一个属性,就是 proto;- 当我们访问一个对象的属性时,如果这个对象内部不存在这个属性,那么他就会去 prototype 里找这个属性,这个 prototype 又会有自己的 prototype,于是就这样一直找下去,也就是我们平时所说的原型链的概念;
- 特点:
JavaScript 对象是通过引用来传递的,我们创建的每个新对象实体中并没有一份属于自己的原型副本。当我们修改原型时,与之相关的对象也会继承这一改变。
- 对象有属性__proto__,指向该对象的构造函数的原型对象;
- 方法除了有属性__proto__,还有属性 prototype,prototype 指向该方法的原型对象;而它的__proto__通常指向 Function.prototype;
总结:
__proto__
作为不同对象之间的桥梁,用来指向创建它的构造函数的原型对象的:
举个例子:
function Person(name) { | |
this.name = name; | |
this.age = 18; | |
this.sayName = function() { | |
console.log(this.name); | |
} | |
} | |
// 第二步 创建实例 | |
var person = new Person('person') |
- 一切对象都是继承自
Object
对象,Object
对象直接继承根源对象null
- 一切的函数对象(包括
Object
对象),都是继承自Function
对象
# JS 如何实现继承的?
面试官:Javascript 如何实现继承? | web 前端面试 - 面试官系列 (vue3js.cn)
# JavaScript 和 CSS 是应该放在外部文件中呢还是把它们放在页面本身之内呢?
放在外部文件中:浏览器会缓存这些文件,单页面也更小,用户访问多个网页的话外部文件可以复用;如果是访问频率不高且 JS 文件与 CSS 文件不太大的情况也可以考虑将其放入页面本身;
# querySelector 与 getElementBy 等的区别
querySelector 与 getElementBy 等的区别 - 简书 (jianshu.com)
# Vue 篇
# 谈谈你对 MVVM 开发模式的理解?
- MVVM 分为 Model、View、ViewModel 三者;
- Model 代表数据模型,数据和业务逻辑都在 Model 层中定义;
- View 代表 UI 视图,负责数据的展示;
- ViewModel 负责监听 Model 中数据的改变并且控制视图的更新,处理用户交互操作;
- Model 和 View 并无直接关联,而是通过 ViewModel 的双向数据绑定原理来进行联系的。因此当 Model 中的数据改变时会触发 View 层的刷新,View 中由于用户交互操作而改变的数据也会在 Model 中同步。
- 这种模式实现了 Model 和 View 的数据自动同步,因此开发者只需要专注对数据的维护操作即可,而不需要自己亲自操作 dom。
# VUE 的响应式原理
** 定义:** 响应式指的是组件 data 的数据一旦变化,立刻触发视图的更新。它是实现数据驱动视图的第一步,也是 Vue 的双向绑定中从 data 到 view 的过程。
** 监听 data 变化的核心 API:**Vue 实现响应式的一个核心 API 是 Object.defineProperty
。该方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
如何监听 data 变化:
共定义了三个函数:
- updateView:模拟 Vue 更新视图的入口函数。
- defineReactive:对数据进行监听的具体实现。
- observer:调用该函数后,可对目标对象进行监听,将目标对象编程响应式的。
简而言之:利用的是 Object.defineProperty 进行的数据劫持,再结合与发布订阅者模式,通过监听器 Observer
监听数据的变化,在数据变动时通知订阅者 watcher
进行的数据更新;
** 详细点说:** 当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data
选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty
把这些 property 全部转为 getter/setter。 Object.defineProperty
是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。
这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在 property 被访问和修改时通知变更。
每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把 “接触” 过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。
由于 JavaScript 的限制,Vue 不能检测数组和对象的变化,所以:
对于对象:
Vue 无法检测 property 的添加或移除。由于 Vue 会在初始化实例时对 property 执行 getter/setter 转化,所以 property 必须在 data
对象上存在才能让 Vue 将它转换为响应式的。例如:
var vm = new Vue({ | |
data:{ | |
a:1 | |
} | |
}) | |
// `vm.a` 是响应式的 | |
vm.b = 2 | |
// `vm.b` 是非响应式的 |
对于已经创建的实例,Vue 不允许动态添加根级别的响应式 property。但是,可以使用 Vue.set(object, propertyName, value)
方法向嵌套对象添加响应式 property。例如:
Vue.set(vm.someObject, 'b', 2)
this.$set(this.someObject,'b',2)
有时你可能需要为已有对象赋值多个新 property,比如使用 Object.assign()
或 _.extend()
。但是,这样添加到对象上的新 property 不会触发更新。在这种情况下,你应该用原对象与要混合进去的对象的 property 一起创建一个新的对象。
// 代替 `Object.assign (this.someObject, { a: 1, b: 2})` | |
this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 }) |
对于数组:
Vue 不能检测以下数组的变动:
- 当你利用索引直接设置一个数组项时,例如:
vm.items[indexOfItem] = newValue
- 当你修改数组的长度时,例如:
vm.items.length = newLength
两种解决方法:
// Vue.set | |
Vue.set(vm.items, indexOfItem, newValue) | |
// Array.prototype.splice | |
vm.items.splice(indexOfItem, 1, newValue) |
#
# 你对 SPA 单页面的理解,它的优缺点分别是什么?如何实现 SPA 应用呢?
SPA(single-page application),翻译过来就是单页应用 SPA
是一种网络应用程序或网站的模型,它通过动态重写当前页面来与用户交互,这种方法避免了页面之间切换打断用户体验在单页应用中,所有必要的代码( HTML
、 JavaScript
和 CSS
)都通过单个页面的加载而检索,或者根据需要(通常是为响应用户操作)动态装载适当的资源并添加到页面,页面在任何时间点都不会重新加载。熟知的 JS 框架如 react
, vue
, angular
, ember
都属于 SPA
。
SPA 主要是由一个主页面和多个页面片段构成,只需要通过局部刷新来跳转页面
单页应用优缺点
优点:
- 具有桌面应用的即时性、网站的可移植性和可访问性
- 用户体验好、快,内容的改变不需要重新加载整个页面
- 良好的前后端分离,分工更明确
- 更容易传输数据
- 成本更低
缺点:
- 不利于搜索引擎的抓取
- 首次渲染速度相对较慢
# 生命周期有哪些?
Vue 生命周期总共可以分为 8 个阶段:创建前后,载入前后,更新前后,销毁前销毁后,以及一些特殊场景的生命周期
生命周期 | 描述 |
---|---|
beforeCreate | 组件实例被创建之初,组件的属性生效之前 |
created | 组件实例已经完全创建,属性也绑定,但真实 dom 还没有生成,$el 还不可用 |
beforeMount | 在挂载开始之前被调用:相关的 render 函数首次被调用 |
mounted | el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子 |
beforeUpdate | 组件数据发生变化,发生在虚拟 DOM 打补丁之前 |
updated | 组件数据更新之后 |
beforeDestroy | 组件实例销毁之前 |
destroyed | 组件实例销毁之后 |
activated | keep-alive 专属,组件被激活时调用 |
deactivated | keep-alive 专属,组件被销毁时调用 |
errorCaptured | 捕获一个来自子孙组件的错误时被调用 |
# v-if 和 v-show 有什么区别?
- v-if 是真正的条件渲染,会控制这个 DOM 节点的存在与否。因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不做 —— 直到条件第一次变为真时,才会开始渲染条件块。
- v-show 就简单得多 —— 不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 的 “display” 属性进行切换。
# 你使用过 Vuex 吗?
Vue 中实现集中式状态(数据)管理的一个 Vue 插件,对 vue 应用中多个组件的共享状态进行集中式的管理(读 / 写数据或方法),也是一种组件间通信的方式,且适用于任意组件间通信。
# computed 和 watch 的区别和运用的场景?
- computed: 是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;
- watch: 更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作;
- 运用场景:
- 当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算;
- 当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许我们执行异步操作 (访问一个 API),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。
# 直接给一个数组项赋值,Vue 能检测到变化吗?
由于 JavaScript 的限制,Vue 不能检测到以下数组的变动:
- 当你利用索引直接设置一个数组项时,例如:
vm.items[indexOfItem] = newValue
- 当你修改数组的长度时,例如:
vm.items.length = newLength
为了解决第一个问题,Vue 提供了以下操作方法:
// Vue.set Vue.set(vm.items, indexOfItem, newValue) // vm.$set,Vue.set的一个别名 vm.$set(vm.items, indexOfItem, newValue) // Array.prototype.splice vm.items.splice(indexOfItem, 1, newValue)
为了解决第二个问题,Vue 提供了以下操作方法:
// Array.prototype.splice vm.items.splice(newLength)
# Vue 的父组件和子组件生命周期钩子函数执行顺序?
- 加载渲染过程 :
父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted - 子组件更新过程 :
父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated - 父组件更新过程 :
父 beforeUpdate -> 父 updated - 销毁过程 :
父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed
# 组件中 data 为什么是一个函数?
- 因为组件是用来复用的,且 JS 里对象是引用关系,如果组件中 data 是一个对象,那么这样作用域没有隔离,子组件中的 data 属性值会相互影响,
- 如果组件中 data 选项是一个函数,那么每个实例可以维护一份被返回对象的独立的拷贝,组件实例之间的 data 属性值不会互相影响;而 new Vue 的实例,是不会被复用的,因此不存在引用对象的问题。
# v-model 的原理?
我们在 vue 项目中主要使用 v-model 指令在表单 input、textarea、select 等元素上创建双向数据绑定,我们知道 v-model 本质上不过是语法糖,v-model 在内部为不同的输入元素使用不同的属性并抛出不同的事件:
- text 和 textarea 元素使用 value 属性和 input 事件;
- checkbox 和 radio 使用 checked 属性和 change 事件;
- select 字段将 value 作为 prop 并将 change 作为事件。
# Vue 组件间通信有哪几种方式?
props
/$emit
(父传子 / 子传父)$emit
/$on
(全局事件总线)vuex
(实现集中式状态(数据)管理的插件)
# Vue 中组件和插件有什么区别?
组件: 组件就是把图形、非图形的各种逻辑均抽象为一个统一的概念(组件)来实现开发的模式,在 Vue
中每一个 .vue
文件都可以视为一个组件。
优势:
- 降低整个系统的耦合度(系统各部分的相互依赖程度);
- 调试方便,方便快速定位问题所在;
- 提高可维护性;
插件:
- 添加全局方法或者属性。如:
vue-custom-element
- 添加全局资源:指令 / 过滤器 / 过渡等。如
vue-touch
- 通过全局混入来添加一些组件选项。如
vue-router
- 添加
Vue
实例方法,通过把它们添加到Vue.prototype
上实现。 - 一个库,提供自己的
API
,同时提供上面提到的一个或多个功能。如vue-router
总结:
组件 (Component)
是用来构成你的 App
的业务模块,它的目标是 App.vue
;
插件 (Plugin)
是用来增强你的技术栈的功能模块,它的目标是 Vue
本身,简单来说,插件就是指对 Vue
的功能的增强或补充;
# 路由懒加载
懒加载本质是延迟加载或按需加载,即在需要的时候的时候进行加载。首页不用设置懒加载,一个页面加载过后再次访问不会重复加载。
为什么要进行路由懒加载?
- 当进行打包构建应用时,打包后的代码逻辑实现包可能会非常大。当用户要去使用的时候,那么就会把所有的资源都请求下来才可以。
- 当我们把不同的路由对应的组件分别打包,在路由被访问时再进行加载,就会更加高效。
路由懒加载所做的事情:
- 将路由对应的组件加载成一个个对应的 js 包 。
- 在路由被访问时才将对应的组件加载。
实现方法:
const foo = ()=>{ | |
return import('xxx/pages/Home') // 返回一个 Promise | |
} | |
{ | |
path:'xxx', | |
component:foo, | |
} | |
// 简洁写法 | |
{ | |
path:'xxx', | |
component:()=>import('xxx/pages/Home'), | |
} |
# 对于 vue3.0 特性你有什么了解的吗?
# 打包工具
# webpack
# 构建流程
# 理解
webpack
是一个打包模块化 js 的工具,在 webpack
里一切文件皆模块,通过 loader
转换文件,通过 plugin
注入钩子,最后输出由多个模块组合成的文件, webpack
专注构建模块化项目。
webPack
可以看做是模块的打包机器:它做的事情是,分析你的项目结构,找到 js 模块以及其它的一些浏览器不能直接运行的拓展语言,例如:Scss,TS 等,并将其打包为合适的格式以供浏览器使用。
(2 条消息) webpack 的面试题(吐血整理)_行走的前端小白菜的博客 - CSDN 博客_前端 webpack 面试题
(2 条消息) 面试官:说说 webpack 的构建流程?_动感超人,的博客 - CSDN 博客
# Flutter
# Flutter 的优点
Flutter 是一个跨平台的框架,而且跨平台的难度不高,且相比于其它众多基于 html 的跨平台框架相比,Flutter 的性能与构建思路几乎最接近原生开发;
性能强大,流畅:
Flutter 对比 weex 和 react native 相比,性能的强大是有目共睹的。基于 dom 树渲染原生组件,很难与直接在原生视图上绘图比肩性能,Google 作为一个轮子大厂,直接在两个平台上重写了各自的 UIKit,对接到平台底层,减少 UI 层的多层转换,UI 性能可以比肩原生,这个优势在滑动和播放动画时尤为明显。
路由设计优秀:
Flutter 的路由传值非常方便,push 一个路由,会返回一个 Future 对象(也就是 Promise 对象),使用 await 或者.then 就可以在目标路由 pop,回到当前页面时收到返回值。这个反向传值的设计基本是甩了微信小程序一条街了。弹出 dialog 等一些操作也是使用的路由方法,几乎不用担心出现传值困难。
# 网络协议篇
# TCP 传输的三次握手、四次挥手策略
三次握手:
为了准确无误地吧数据送达目标处,TCP 协议采用了三次握手策略。用 TCP 协议把数据包送出去后,TCP 不会对传送后的情况置之不理,他一定会向对方确认是否送达,握手过程中使用 TCP 的标志:SYN 和 ACK
- 发送端首先发送一个带 SYN 的标志的数据包给对方
- 接收端收到后,回传一个带有 SYN/ACK 标志的数据包以示传达确认信息
- 最后,发送端再回传一个带 ACK 的标志的数据包,代表 “握手” 结束
如在握手过程中某个阶段莫明中断,TCP 协议会再次以相同的顺序发送相同的数据包
- 断开一个 TCP 连接需要 “四次挥手”
- 第一次挥手:主动关闭方发送一个 FIN,用来关注主动方到被动关闭方的数据传送,也即是主动关闭方告诫被动关闭方:我已经不会再给你发数据了(在 FIN 包之前发送的数据,如果没有收到对应的 ACK 确认报文,主动关闭方依然会重发这些数据)。但是,此时主动关闭方还可以接受数据
- 第二次挥手:被动关闭方收到 FIN 包后,发送一个 ACK 给对方,确认序号收到序号 +1(与 SYN 相同,一个 FIN 占用一个序号)
- 第三次挥手:被动关闭方发送一个 FIN。用来关闭被动关闭方到主动关闭方的数据传送,也就是告诉主动关闭方,我的数据也发送完了,不会给你发送数据了
- 第四次挥手:主动关闭方收到 FIN 后,发送一个 ACK 给被动关闭方,确认序号为收到序号 + 1,至此,完成四次挥手
为什么是三次握手而不是两次?
有这样一种情况,当 A 发送一个消息给 B,但是由于网络原因,消息被阻塞在了某个节点,然后阻塞的时间超出设定的时间,A 会认为这个消息丢失了,然后重新发送消息。当 A 和 B 通信完成后,这个被 A 认为失效的消息,到达了 B。对于 B 而言,以为这是一个新的请求链接消息,就向 A 发送确认,但对于 A 而言,它认为没有给 B 再次发送消息(因为上次的通话已经结束)所有 A 不会理睬 B 的这个确认,但是 B 则会一直等待 A 的消息。所以有了三次握手的修订,第三次握手看似多余其实不然,这主要是为了防止已失效的请求报文段突然又传送到了服务端而产生连接的误判。
为什么不是三次挥手而是四次?
当收到对方的 FIN 报文时,仅表示对方不再发送数据但还能接收收据,我们也未必把全部数据都发给了对方,所以我们可以立即 close,也可以发送一些数据给对方后,再发送 FIN 报文给对方表示同意关闭连接。因此我们的 ACK 和 FIN 一般会分开发送,因此多了一次。
# HTTP 协议的理解
深入理解 HTTP 协议 - 知乎 (zhihu.com)
# HTTP 常见的状态码?
- 1xx (临时响应)
- 100 Continue 继续,一般在发送 post 请求时,已发送了 http header 之后服务端将返回此信息,表示确认,之后发送具体参数信息。
- 2xx (成功)
- 200 OK 正常返回信息
- 201 Created 请求成功并且服务器创建了新的资源
- 202 Accepted 服务器已接受请求,但尚未处理。
- 3xx (重定向) 要完成请求,需要进一步操作。
- 301 Moved Permanently 请求的网页已永久移动到新位置。
- 302 Found 临时性重定向,请求的网页暂时被移动到了新位置。
- 303 See Other 临时性重定向,请求的资源在其它的 URl 上,且总是使用 GET 请求新的 URI。
- 304 Not Modified 自从上次请求后,请求的网页未修改过。
- 4xx (请求错误)
- 400 Bad Request 服务器无法理解请求的格式。
- 401 Unauthorized 应该用来表示缺失或错误的认证。
- 403 Forbidden 权限不够而禁止访问。
- 404 Not Found 找不到如何与 URI 相匹配的资源。
- 406 Not Acceptable 表示客户端无法解析服务端返回的内容.
- 5xx (服务器错误)
- 500 Internal Server Error 最常见的服务器端错误。
- 503 Service Unavailable 服务器端暂时无法处理请求(可能是过载或维护)。
- 504 网关超时
# HTTP 和 HTTPS,为什么 HTTPS 安全?
- HTTP 协议通常承载与 TCP 协议之上,在 HTTP 和 TCP 之间添加一个安全协议层(SSL 或 TSL),这个时候,就成了我们常说的 HTTPS
- 默认 HTTP 的端口号为 80,HTTPS 的端口号为 443
- 因为网络请求需要中间有很多的服务器路由的转发,中间的节点都可能篡改信息,而如果使用 HTTPS,密钥在你和终点站才有,https 之所有说比 http 安全,是因为他利用 ssl/tls 协议传输。包含证书,流量转发,负载均衡,页面适配,浏览器适配,refer 传递等,保障了传输过程的安全性
# HTTP 协议下常用的 7 种请求方法
- GET:发出请求从服务器获取一份文档;
- HEAD:同 GET 但只从服务器获取文档的首部;
- POST:向服务器发送带要处理的数据,并可以接收处理过后的数据;
- PUT:将请求的主体部分存储在服务器上;
- TRACE:对可能经过代理服务器传送到服务器上去的报文进行追踪;
- OPTIONS:请求 Web 服务器告知其支持的各种功能;
- DELETE:从服务器上删除一份文档;
# TCP 与 UDP 的区别?
TCP (Transmission Control Protocol) 和 UDP (User Datagram Protocol) 协议属于传输层协议,它们之间的区别包括:
- TCP 是面向连接的,UDP 是无连接的;
- TCP 是可靠的,UDP 是不可靠的;
- TCP 只支持点对点通信,UDP 支持一对一、一对多、多对一、多对多的通信模式;
- TCP 是面向字节流的,UDP 是面向报文的;
- TCP 有拥塞控制机制;UDP 没有拥塞控制,适合媒体通信;
- TCP 首部开销 (20 个字节) 比 UDP 的首部开销 (8 个字节) 要大;
# 什么是 HTTP 协议无状态协议?怎么解决 Http 协议无状态协议?
HTTP 是一个无状态的协议,也就是没有记忆力,这意味着每一次的请求都是独立的,缺少状态意味着如果后续处理需要前面的信息,则它必须要重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就很快。
HTTP 的这种特性有优点也有缺点:
- 优点:解放了服务器,每一次的请求 “点到为止”,不会造成不必要的连接占用
- 缺点:每次请求会传输大量重复的内容信息,并且,在请求之间无法实现数据的共享
解决方案:使用 Cookie 技术
# OSI 网络体系结构与 TCP/IP 协议模型
OSI 是一个理论上的网络通信模型,而 TCP/IP 则是实际上的网络通信标准。
# 前端安全
# XSS
跨站脚本攻击,缩写为 XSS (Cross Site Scripting),是利用网页的漏洞,通过某种方式给网页注入恶意代码,使用户加载网页时执行注入的恶意代码。
三种攻击方式:
- 非持久性跨站(反射型)
- 持久性跨站(存储型)
- DOM 跨站
非持久跨站
攻击步骤:
- 攻击者构造出特殊的
URL
,其中包含恶意代码。 - 用户打开带有恶意代码的
URL
时,网站服务端将恶意代码从URL
中取出,拼接在HTML
中返回给浏览器。 - 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。
- 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
持久性跨站
攻击步骤:
- 攻击者将恶意代码提交到目标网站的数据库中。
- 用户打开目标网站时,网站服务端将恶意代码从数据库取出,拼接在
HTML
中返回给浏览器。 - 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。
- 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
DOM 跨站
攻击步骤:
- 攻击者构造出特殊的
URL
,其中包含恶意代码。 - 用户打开带有恶意代码的
URL
。 - 用户浏览器接收到响应后解析执行,前端
JavaScript
取出URL
中的恶意代码并执行。 - 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
区别:
反射型跟存储型的区别是:
存储型 XSS 的恶意代码存在数据库里,反射型 XSS 的恶意代码存在 URL 里。
DOM 型跟前两种区别是:
DOM 型 XSS 攻击中,取出和执行恶意代码由浏览器端完成,属于前端 JavaScript 自身的安全漏洞,而其他两种 XSS 都属于服务端的安全漏洞。
防御 XSS:
- 设置
HttpOnly
,让客户端脚本无法读取到 cookie 信息; - 转义字符串;
# SQL 注入
SQL 注入就是通过把 SQL 命令插入到 Web 表单提交或输入域名或页面请求的查询字符串,最终达到欺骗服务器执行恶意的 SQL 命令。
# Git 篇
Git 使用说明 - 杂学 | Sakura = Course Of Growth (nc3021.github.io)
# 什么是 Git?
Git 是分布式版本控制系统(DVCS)。它可以跟踪文件的更改,并允许你恢复到任何特定版本的更改。
# Git 是用什么语言编写的?
Git 使用 C 语言编写。 GIT 很快,C 语言通过减少运行时的开销来做到这一点。
# git pull 和 git fetch 有什么区别?
git pull 命令从中央存储库中提取特定分支的新更改或提交,并更新本地存储库中的目标分支。
git fetch 也用于相同的目的,但它的工作方式略有不同。当你执行 git fetch 时,它会从所需的分支中提取所有新提交,并将其存储在本地存储库中的新分支中。如果要在目标分支中反映这些更改,必须在 git fetch 之后执行 git merge。只有在对目标分支和获取的分支进行合并后才会更新目标分支。
# 什么是 git stash?
git stash 会将你的工作目录,即修改后的跟踪文件和暂存的更改保存在一堆未完成的更改中,你可以随时重新应用这些更改。
# 什么时候使用 git rebase 代替 git merge?
使用变基时,意味着使用另一个分支作为集成修改的新基础。一般只有在完全自信且为了使历史分支记录更为清晰的时候使用。
# 提交时发生冲突,你能解释冲突是如何产生的吗?你是如何解决的?
开发过程中,我们都有自己的特性分支,所以冲突发生的并不多,但也碰到过。诸如公共类的公共方法,我和别人同时修改同一个文件,他提交后我再提交就会报冲突的错误。
发生冲突,在 IDE 里面一般都是对比本地文件和远程分支的文件,然后把远程分支上文件的内容手工修改到本地文件,然后再提交冲突的文件使其保证与远程分支的文件一致,这样才会消除冲突,然后再提交自己修改的部分。
发生冲突,也可以使用命令:
- 通过 git stash 命令,把工作区的修改提交到栈区,目的是保存工作区的修改;
- 通过 git pull 命令,拉取远程分支上的代码并合并到本地分支,目的是消除冲突;
- 通过 git stash pop 命令,把保存在栈区的修改部分合并到最新的工作空间中;
# git 如何撤销 commit、git commit 提交之后如何取消本次提交?
可以先用 git reflog 查看历史提交记录
软撤销 --soft
本地代码不会变化,只是 git 转改会恢复为 commit 之前的状态
不删除工作空间改动代码,撤销 commit,不撤销 git add .
git reset --soft HEAD~1
表示撤销最后一次的 commit ,1 可以换成其他更早的数字
硬撤销
本地代码会直接变更为指定的提交版本,慎用
删除工作空间改动代码,撤销 commit,撤销 git add .
注意完成这个操作后,就恢复到了上一次的 commit 状态。
git reset --hard HEAD~1
# 使用过 git cherry-pick,有什么作用?
命令 git cherry-pick 可以把 branch A 的 commit 复制到 branch B 上。
在 branch B 上进行命令操作:
git cherry-pick commitId
# Git 与 SVN 的区别?
(2 条消息) SVN 和 Git 介绍,区别,优缺点以及适用范围_想养一只!的博客 - CSDN 博客_svn 和 git
# 前端杂项篇
# 常见的浏览器内核有哪些
- 浏览器内核主要分成两部分:渲染引擎 (layout engineer 或 Rendering Engine) 和 JS 引擎。
- 渲染引擎:负责取得网页的内容(HTML、XML、图像等等)、整理讯息(例如加入 CSS 等),以及计算网页的显示方式,然后会输出至显示器或打印机。浏览器的内核的不同对于网页的语法解释会有不同,所以渲染的效果也不相同。所有网页浏览器、电子邮件客户端以及其它需要编辑、显示网络内容的应用程序都需要内核。
- JS 引擎则:解析和执行 javascript 来实现网页的动态效果。
- 常见内核:
- Trident 内核:IE, MaxThon, TT, The World, 360, 搜狗浏览器等。[又称 MSHTML]
- Gecko 内核:FireFox
- Webkit 内核:Safari, Chrome 等。 [Chrome 的:Blink(WebKit 的分支)]
# 网页前端性能优化的方式有哪些?
WEB 前端性能优化的方向有两个:一是从用户角度,优化能够让页面加载得更快,对用户的操作响应更快;二是从服务商角度,优化能够减少页面请求数或者减少请求所占带宽,节省资源
尽可能合并 css、js 文件,减少 http 请求次数 ;
压缩 css, js 文件;
尽可能将 JS 文件与 CSS 文件放在 HTML 文档外部,这样浏览器会缓存相应文件,从而减小请求次数,使单页面更小,也能加快使用相应文件的页面加载速度,用户访问多个网页的话外部文件可以复用;如果是访问频率不高且 JS 文件与 CSS 文件不太大的情况也可以考虑将其放入页面本身;
如果 JS 文件与 CSS 文件在文档内部的话,需要将样式表置顶、脚本置低(在 header 中通过 link 引入 css 文件)
- 加载并发数是有上限的,js 和 css 混合放置,会导致 css 的延迟,会导致页面闪动,所以 js 要置底
- css 放在 header 中,阻塞页面的渲染,css 加载完,再加载 dom,放置页面样式跳变,从而保证渲染一步到位
- css 不会阻塞后面 js 并发加载,但会阻塞 js 的执行。如果 js 放在 header 中,会阻塞 html 的渲染。
减少 cookie 体积,减少 HTTP 请求头的大小;
浏览器缓存,减少 HTTP 请求;
图片懒加载;
IntersectionObserver
优化图片大小,尽可能使用压缩图片;
合并图片(雪碧图 / 精灵图);
减少 dom 元素数量;
预加载和延迟加载内容;
使用 CDN
内容分发网络(CDN)是位于不同地理位置的服务器组成的网络。每个服务器都拥有所有网站的文件副本。当用户请求文件和网页时,就可以直接从就近的网站服务器获取相应资源(也可以是从负载最小的服务器)。你可以使用 Amazon cloud front 或者 MaxCDN 为网站开启 CDN 加速。
避免使用 CSS 表达式;
网站性能优化 - webNick - 博客园 (cnblogs.com)
# sessionStorage 、localStorage 和 cookie 之间的区别?
- 共同点:都是保存在浏览器端,且同源的。
- 区别:
- cookie 数据始终在同源的 http 请求中携带(即使不需要),即 cookie 在浏览器和服务器间来回传递。而 sessionStorage 和 localStorage 不会自动把数据发给服务器,仅在本地保存。
- 存储大小限制也不同,cookie 数据不能超过 4k;sessionStorage 和 localStorage 虽然也有存储大小的限制,但比 cookie 大得多,可以达到 5M 或更大。
- 数据有效期不同,sessionStorage:仅在当前浏览器窗口关闭前有效,自然也就不可能持久保持;localStorage:始终有效,窗口或浏览器关闭也一直保存,因此用作持久数据;cookie 只在设置的 cookie 过期时间之前一直有效,即使窗口或浏览器关闭。
- 作用域不同,sessionStorage 不在不同的浏览器窗口中共享,即使是同一个页面;localStorage 在所有同源窗口中都是共享的;cookie 也是在所有同源窗口中都是共享的。
# 在 css/js 代码上线之后开发人员经常会优化性能,从用户刷新网页开始,一次 js 请求一般情况下有哪些地方会有缓存处理?
DNS 缓存
全称 Domain Name System , 即域名系统
CDN 缓存
全称 Content Delivery Network, 即内容分发网络(缓存服务器)
浏览器缓存
服务器缓存
# 网页从输入网址到渲染完成经历了哪些过程与协议?
大致可以分为如下 7 步:
- 输入网址;
- 发送到 DNS 服务器,并获取域名对应的 web 服务器对应的真实 ip 地址;
- 与 web 服务器建立 TCP 连接;
- 浏览器向 web 服务器发送 http 请求;
- web 服务器响应请求,并返回指定 url 的数据(或错误信息,或重定向的新的 url 地址);
- 浏览器下载 web 服务器返回的数据及解析 html 源文件;
- 生成 DOM 树,解析 css 和 js,渲染页面,直至显示完成;
- DNS 域名解析协议;
- ip 地址与 TCP 连接使用了 IP 协议(IP 地址,数据切片成 IP 数据包,广播规则);
- 发送请求一般使用 HTTP 或 HTTPS 协议;
- ARP
# 浏览器的渲染原理和过程
浏览器渲染的过程主要包括以下五步:
- 浏览器将获取的 HTML 文档解析成 DOM 树;
- 处理 CSS 标记,构成层叠样式表模型 CSSOM (CSS Object Model);
- 将 DOM 和 CSSOM 合并为渲染树 (
rendering tree
),代表一系列将被渲染的对象; - 渲染树的每个元素包含的内容都是计算过的,它被称之为布局
layout
。浏览器使用一种流式处理的方法,只需要一次绘制操作就可以布局所有的元素; - 将渲染树的各个节点绘制到屏幕上,这一步被称为绘制
painting
。
具体流程:
构建 DOM 树:
当浏览器接收到服务器响应来的 HTML 文档后,会遍历文档节点,生成 DOM 树。
备注:
- DOM 树在构建的过程中可能会被 CSS 和 JS 的加载而执行阻塞
display:none
的元素也会在 DOM 树中- 注释也会在 DOM 树中
script
标签会在 DOM 树中
构建 CSSOM 规则树:
浏览器解析 CSS 文件并生成 CSSOM,每个 CSS 文件都被分析成一个 StyleSheet 对象,每个对象都包含 CSS 规则。CSS 规则对象包含对应于 CSS 语法的选择器和声明对象以及其他对象。
- CSS 解析可以与 DOM 解析同时进行。
- CSS 解析与
script
的执行互斥 。- 在 Webkit 内核中进行了
script
执行优化,只有在 JS 访问 CSS 时才会发生互斥。
构建渲染树(Render Tree):
通过 DOM 树和 CSS 规则树,浏览器就可以通过它两构建渲染树了。浏览器会先从 DOM 树的根节点开始遍历每个可见节点,然后对每个可见节点找到适配的 CSS 样式规则并应用。
备注:
- Render Tree 和 DOM Tree 不完全对应
display: none
的元素不在 Render Tree 中visibility: hidden
的元素在 Render Tree 中
渲染树布局 (layout of the render tree):
布局阶段会从渲染树的根节点开始遍历,由于渲染树的每个节点都是一个 Render Object 对象,包含宽高,位置,背景色等样式信息。所以浏览器就可以通过这些样式信息来确定每个节点对象在页面上的确切大小和位置,布局阶段的输出就是我们常说的盒子模型,它会精确地捕获每个元素在屏幕内的确切位置与大小。
备注:
float
元素,absoulte
元素,fixed
元素会发生位置偏移。- 我们常说的脱离文档流,其实就是脱离 Render Tree。
更多请见:浏览器渲染原理与过程 - 简书 (jianshu.com)
# Cookie 的设置以及在响应报文的位置
cookie 信息位于 headers 的 set-cookie 字段。有多少个 cookie 信息加入就有多少个 set-cookie 字段。客户端接收到 cookie 后就将 cookie 存储在本地固定位置。
# 浏览器缓存机制
浏览器缓存机制有四个方面,它们按照获取资源时请求的优先级依次排列如下:
- Memory Cache(Disk Cache)
- Service Worker Cache
- HTTP Cache
- Push Cache
Memory Cache
MemoryCache,指的是存在内存中的缓存。从优先级上看,它是浏览器最先尝试去命中的一种缓存。从效率上看,它是响应速度最快的一种缓存。内存缓存是快的,也是 “短命” 的。它和渲染进程相关的,当进程结束后,也就是 tab 关闭以后,内存里的数据也将不复存在。
Base64 格式的图片,几乎永远可以被塞进 memory cache,这可以视作浏览器为节省渲染开销的 “自保行为”;此外,体积不大的 JS、CSS 文件,也有较大地被写入内存几率 —— 相比之下,较大的 JS、CSS 文件就没有这个待遇了,内存资源是有限的,它们往往被直接甩进磁盘(Disk Cache)。
Service Worker Cache
Service Worker 是一种独立于主线程之外的 JavaScript 线程。它脱离于浏览器窗体,因此无法直接访问 DOM。这样独立的个性使得 Service Worker 的 “个人行为” 无法干扰页面的性能,这个 “幕后工作者” 可以帮我们实现离线缓存、消息推送和网络代理等功能。我们借助 Service worker 实现的离线缓存就称为 Service Worker Cache。
Service Worker 的生命周期包括 install、active、working 三个阶段。一旦 Service Worker 被 install,它将始终存在,只会在 active 与 working 之间切换,除非我们主动终止它。这是它可以用来实现离线存储的重要先决条件。
Push Cache
Push Cache 是指 HTTP2 在 server push 阶段存在的缓存。
- Push Cache 是缓存的最后一道防线。浏览器只有在 Memory Cache、HTTP Cache 和 Service Worker Cache 均为命中的情况下才会去询问 Push Cache。
- Push Cache 是一种存在于会话阶段的缓存,当 session 终止时,缓存也随之释放。
- 不同的页面只要共享了同一个 HTTP2 连接,那么它们就可以共享同一个 Push Cache。
HTTP Cache
浏览器缓存分为强缓存和协商缓存。
强制缓存:
代表在已缓存的资源未失效(未过期)的情况下,不会向服务器发送请求,会直接从资源池中读取资源。
缓存规则:
对于强制缓存而言,响应头中有两个字段标明了它的缓存规则,一个是 Expires,另一个是 Cache - Control。
Expires
:Expires 的值包含具体的日期和时间,是服务器返回的资源到期时间,即在此时间之前,可以直接从资源池中读取资源,无需再次请求服务器。
Expires: Wed, 31 Jul 2019 02:58:24 GMT |
Cache-Control
:Cache-Control 可以被用于请求和响应头中,组合使用多种指令来指定缓存机制。下面列举了比较常用的指令:
- private:资源只可以被客户端缓存,Cache-Control 的默认值。
- public:资源可以被客户端和代理服务器缓存。
- max-age=t:客户端缓存的资源将在 t 秒后失效,但是会走协商缓存。
- no-cache:跳过强缓存,需要使用协商缓存来验证资源
- no-store:不缓存任何资源。
- s-maxAge:覆盖
max-age
或者Expires
头,但是仅适用于共享缓存 (比如各个代理),私有缓存会忽略它。
强制缓存的弊端
强制缓存的判定标准,主要依据来自于是否超出某个时间或某个时间段,而不关心服务器端资源是否已经更新,这可能会导致加载的资源早已不是服务器最新的内容。
协商缓存
针对强制缓存的弊端,协商缓存需要进行资源对比判断是否可以使用缓存。客户端第一次请求资源时,服务器会将缓存标识与资源一起返回给客户端,客户端将二者备份至资源池中。当再次请求相同资源时(此时,强制缓存过期,缓存资源还在),客户端将备份的缓存标识发送给服务器,服务器根据缓存标识进行验证,如果验证结果为未更新,服务器会返回 304,通知客户端可以继续使用缓存资源。
协商缓存的优先级低于强制缓存,因此只有在强制缓存失效后,客户端才会携带缓存标识向服务器发起请求。
缓存规则:
Last-Modified/If-Modified-Since
:当我们第一次发出请求时,Last-Modified 由服务器返回,通知客户端,该资源的最后修改时间。当我们再次请求该资源时,If-Modified-Since 由客户端发送,其保存了 Last-Modified 的值。服务器收到请求后,将 If-Modified-Since 的值与被请求资源的最后修改时间进行比对。若资源的最后修改时间大于 If-Modified-Since 的值,说明资源被修改过,则返回状态码 200 以及最新资源;若资源的最后修改时间小于或等于 If-Modified-Since 的值,说明资源无修改,则返回状态码 304,通知客户端继续使用缓存资源。
ETag/If-None-Match
:当我们第一次发出请求时,ETag 由服务器返回,其值为该资源的标签。当我们再次请求该资源时,If-None-Match 由客户端发送,其保存了 ETag 的值。服务器收到请求后,将 If-None-Match 的值与被请求资源的标签进行比对。若资源的标签不等于 If-None-Match 的值,说明资源被修改过,则返回状态码 200 以及最新资源;若资源的标签等于 If-None-Match 的值,说明资源无修改,则返回状态码 304,通知客户端继续使用缓存资源。
ETag / If-None-Match 组合的优先级高于 Last-Modified / If-Modified-Since 组合。
ETag 会出现的原因:
因为 Last-Modified 标注的最后修改只能精确到秒级,如果某些文件在 1 秒钟以内被多次修改的话,它将不能准确标注文件的修改时间。也要考虑到,一些文件也许会周期性的更改,但是它的内容并不改变,仅仅改变的修改时间,这时候用 Last-Modified/If-Modified-Since 就不是很合适了。** 但是注意,大厂一般都不怎么用 Etag。** 因为大厂多使用负载分担的方式来调度 HTTP 请求。因此,同一个客户端对同一个页面的多次请求,很可能被分配到不同的服务器来响应,而根据 ETag 的计算原理,不同的服务器,有可能在资源内容没有变化的情况下,计算出不一样的 Etag,而使得缓存失效。
备注:
当用户 Ctrl + F5 强制刷新网页时,浏览器直接从服务器加载,跳过强缓存和协商缓存
当用户仅仅敲击 F5 刷新网页时,跳过强缓存,但是仍然会进行协商缓存过程
如果该资源文件被强制缓存起来了,并且缓存时间还未过期时,如何判断该文件是否已经被修改了呢?也就是此时的文件是否是最新的我们不得而知。在 HTTP 规范里面并没有解决这个问题的方案。
答:通过不缓存 html,为静态文件添加 MD5 或者 hash 标识,解决浏览器无法跳过缓存过期时间主动感知文件变化的问题。因为加上了 hash 标识,使得每次请求时的路径名不完全相等,也就不能够直接拿本地缓存了,进而去找协商缓存,从而访问服务器。当然加 hash 标识这一步,现在可以用打包工具来实现。
# 其它杂项
# 线程与进程的联系与区别?
- 进程:程序在执行过程中分配和管理资源的基本单位,是资源分配的最小单位,并拥有自己的独立地址空间;
- 线程:线程是进程中执行运算的最小单位,是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行;
- 进程和线程的关系:
- 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。
- 资源分配给进程,同一进程的所有线程共享该进程的所有资源。
- 处理机分给线程,即真正在处理机上运行的是线程。
- 线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。线程是指进程内的一个执行单元,也是进程内的可调度实体.
- 线程作为调度和分配的基本单位,进程作为拥有资源的基本单位
# 什么是关系型数据库?
- 关系型数据库是依据关系模型来创建的数据库;
- 所谓关系模型就是 “一对一、一对多、多对多” 等关系模型,关系模型就是指二维表格模型,因而一个关系型数据库就是由二维表及其之间的联系组成的一个数据组织;
- 关系型数据可以很好地存储一些关系模型的数据,比如多个老师对应多个学生的数据(“多对多”),一本书对应多个作者(“一对多”),一本书对应一个出版日期(“一对一”);
- 关系模型包括数据结构(数据存储的问题,二维表)、操作指令集合(SQL 语句)、完整性约束 (表内数据约束、表与表之间的约束);
- 容易理解(建立在关系模型上),但不节省空间(因为建立在关系模型上,就要遵循某些规则,好比数据中某字段值即使为空仍要分配空间);
# 什么是事务?事务四大特性?
事务是对数据库中一系列操作进行统一的回滚或者提交的操作,主要用来保证数据的完整性和一致性。
- 原子性(Atomicity):
原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚,因此事务的操作如果成功就必须要完全应用到数据库,如果操作失败则不能对数据库有任何影响。 - 一致性(Consistency):
事务开始前和结束后,数据库的完整性约束没有被破坏。比如 A 向 B 转账,不可能 A 扣了钱,B 却没收到。 - 隔离性(Isolation):
隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。比如 A 正在从一张银行卡中取钱,在 A 取钱的过程结束前,B 不能向这张卡转账。 - 持久性(Durability):
持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。
# 事务的并发问题?
# 数据库语言
SQL 语言共分为四大类:
- 数据查询语言 DQL
- 数据操纵语言 DML
- 数据定义语言 DDL
- 数据控制语言 DCL。
1. 数据查询语言 DQL
数据查询语言 DQL 基本结构是由 SELECT 子句,FROM 子句,WHERE 子句组成的查询块:
SELECT
FROM
WHERE
2 . 数据操纵语言 DML
数据操纵语言 DML 主要有三种形式:
- 插入:INSERT
- 更新:UPDATE
- 删除:DELETE
3. 数据定义语言 DDL
数据定义语言 DDL 用来创建数据库中的各种对象 ----- 表、视图、索引、同义词、聚簇等如:
CREATE TABLE/VIEW/INDEX/SYN/CLUSTER
表 视图 索引 同义词 簇
DDL 操作是隐性提交的!不能 rollback
4. 数据控制语言 DCL
数据控制语言 DCL 用来授予或回收访问数据库的某种特权,并控制数据库操纵事务发生的时间及效果,对数据库实行监视等。如:
- GRANT:授权。
- ROLLBACK [WORK] TO [SAVEPOINT]:回退到某一点。回滚 ---ROLLBACK;回滚命令使数据库状态回到上次最后提交的状态。其格式为:
SQL>ROLLBACK; - COMMIT [WORK]:提交。
# 事务的隔离级别
第一种隔离级别:Read uncommitted (读未提交)
如果一个事务已经开始写数据,则另外一个事务不允许同时进行写操作,但允许其他事务读此行数据。解决了更新丢失,但还是可能会出现脏读
第二种隔离级别:Read committed (读提交)
如果是一个读事务 (线程),则允许其他事务读写,如果是写事务将会禁止其他事务访问该行数据。
解决了更新丢失和脏读问题
第三种隔离级别:Repeatable read (可重复读取)
可重复读取是指在一个事务内,多次读同一个数据,在这个事务还没结束时,其他事务不能访问该数据 (包括了读写),这样就可以在同一个事务内两次读到的数据是一样的,因此称为是可重复读隔离级别,读取数据的事务将会禁止写事务 (但允许读事务),写事务则禁止任何其他事务 (包括了读写)。
解决了更新丢失、脏读、不可重复读、但是还会出现幻读
第四种隔离级别:Serializable (可序化)
提供严格的事务隔离,它要求事务序列化执行,事务只能一个接着一个地执行,但不能并发执行。
解决了更新丢失、脏读、不可重复读、幻读 (虚读)
事务的并发问题
**1、脏读:** 事务 A 读取了事务 B 更新的数据,然后 B 回滚操作,那么 A 读取到的数据是脏数据
**2、不可重复读:** 事务 A 多次读取同一数据,事务 B 在事务 A 多次读取的过程中,对数据作了更新并提交,导致事务 A 多次读取同一数据时,结果因此本事务先后两次读到的数据结果会不一致。
**3、幻读:** 幻读解决了不重复读,保证了同一个事务里,查询的结果都是事务开始时的状态(一致性)。
例如:事务 T1 对一个表中所有的行的某个数据项做了从 “1” 修改为 “2” 的操作 这时事务 T2 又对这个表中插入了一行数据项,而这个数据项的数值还是为 “1” 并且提交给数据库。 而操作事务 T1 的用户如果再查看刚刚修改的数据,会发现还有跟没有修改一样,其实这行是从事务 T2 中添加的,就好像产生幻觉一样,这就是发生了幻读。
# 什么是死锁?
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
产生死锁的四个必要条件:
- 互斥条件:一个资源每次只能被一个进程使用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
# 手写题
# 如何从 Url 中获取参数?
url = 'http://www.jianshu.com/search?q=js&page=1&type=note'; | |
//split 拆分法 | |
function getUrl(url) { | |
let n = url.indexOf('?') | |
if (n === -1) return ('没有参数') | |
let str = url.slice(n + 1) | |
let ary = str.split('&') | |
let arg = {} | |
ary.forEach(item => { | |
let arr = item.split('=') | |
arg[arr[0]] = arr[1] | |
}) | |
return arg | |
} | |
// 正则表达式法 | |
function regUrl(url) { | |
if (url.indexOf('?') === -1) return ('没有参数') | |
let regArg = /\w+=\w+/g | |
let aryValue = url.match(regArg) | |
aryValue = aryValue.map(item => item.split('=')) | |
let result = {} | |
for (let i = 0; i < aryValue.length; i++) { | |
result[aryValue[i][0]] = aryValue[i][1] | |
} | |
return result | |
} | |
console.log(getUrl(url)) | |
console.log(regUrl(url)) |
# 手写 call、apply、bind
function person(a, b, c, d) { | |
console.log(this.name) | |
console.log(a, b, c, d) | |
} | |
let egg = { name: 'sakura' } | |
//call | |
Function.prototype.newCall = function (obj, ...arr) { | |
var obj = obj || window | |
obj.p = this | |
let result = obj.p(...arr) | |
delete obj.p | |
return result | |
} | |
person.newCall(egg, 'mu', 'zi', 'chuang', 'ye') | |
//apply | |
Function.prototype.newApply = function (obj, ary) { | |
var obj = obj || window | |
obj.p = this | |
let result | |
if (!ary) { | |
result = obj.p() | |
} | |
else if (typeof ary != 'Object') { | |
result = obj.p(ary) | |
} else { | |
result = obj.p(...ary) | |
} | |
delete obj.p | |
return result | |
} | |
person.newApply(egg, ['mu', 'zi', 'chuang', 'ye']) | |
//bind (待完成) | |
// Function.prototype.newBind = function (obj) { } | |
// person.newBind(egg, 'mu', 'zi', 'chuang', 'ye') |
# 数组去重
let ary = [12, 23, 12, 15, 25, 23, 25, 14, 16] | |
// SET(es6) | |
let arr1 = [...new Set(ary)] | |
console.log(arr1) | |
//filter | |
let arr2 = ary.filter((element, index, self) => (self.indexOf(element) === index)) | |
console.log(arr2) | |
//object | |
let arr3 = [...ary] | |
let obj = {} | |
for (let i = 0; i < arr3.length; i++) { | |
let item = arr3[i] | |
if (typeof (obj[item]) !== 'undefined') { | |
arr3[i] = arr3[arr3.length - 1] | |
i-- | |
arr3.length-- | |
} else { | |
obj[item] = item | |
} | |
} | |
console.log(arr3) | |
// 正则 | |
let arr4 = [...ary] | |
arr4.sort((a, b) => (a - b)) | |
arr4 = arr4.join(',') + ',' | |
let reg = /(\d+,)\1*/g, arr44 = [] | |
arr4.replace(reg, (val, group) => { | |
arr44.push(parseFloat(group)) | |
}) | |
console.log(arr44) |
# 深拷贝与浅拷贝
- 浅拷贝是拷贝一层,属性为对象时,浅拷贝是复制,两个对象指向同一个地址
- 深拷贝是递归拷贝深层次,属性为对象时,深拷贝是新开栈,两个对象指向不同的地址
// 浅拷贝 | |
console.log('浅拷贝') | |
let obj1 = { | |
a: 1, | |
b: [1, 2, 3], | |
c: { d: 4 }, | |
} | |
//way1 | |
let obj2 = {}; | |
for (let key in obj1) { | |
if (!obj1.hasOwnProperty(key)) break | |
obj2[key] = obj1[key] | |
} | |
//way2 (推荐) | |
let obj3 = { ...obj1 } | |
//way3 | |
let obj33 = Object.assign({}, obj1) | |
obj1['b'].push(4) | |
console.log(obj2) | |
console.log(obj3) | |
console.log(obj33) | |
//slice 与 concat 方法也可以浅拷贝字符串和数组 | |
// 深拷贝 | |
console.log('深拷贝') | |
//way1 (不能复制方法) | |
let obj4 = JSON.parse(JSON.stringify(obj2)) | |
obj2['b'].push(5) | |
console.log(obj4) | |
//way2 | |
let obj5 = { | |
a: 1, | |
b: [1, 2], | |
c: { | |
c1: 3, | |
c2: 3, | |
}, | |
d: function () { return this.a }, | |
e: /^\d+$/, | |
f: null | |
} | |
function deepClone(obj) { | |
if (obj === null) return null | |
if (obj instanceof Date) { | |
return new Date(obj) | |
} | |
if (obj instanceof RegExp) { | |
return new RegExp(obj) | |
} | |
if (typeof obj !== 'object') return obj | |
// 不直接创建空对象的目的:克隆的结果和之前保持相同的所有类 | |
let newObj = new obj.constructor | |
for (let key in obj) { | |
if (obj.hasOwnProperty(key)) { | |
newObj[key] = deepClone(obj[key]) | |
} | |
} | |
return newObj; | |
} | |
let obj6 = deepClone(obj5) | |
console.log(obj5.b === obj6.b) | |
console.log(obj5.c === obj6.c) | |
console.log(obj5.d === obj6.d) | |
console.log(obj5.e === obj6.e) | |
console.log(obj5.f === obj6.f) |
# 防抖与节流
<!DOCTYPE html> | |
<html lang="zh-CN"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta http-equiv="X-UA-Compatible" content="IE=edge"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Document</title> | |
</head> | |
<body> | |
<input type="button" value="剁手" id="button"> | |
</body> | |
<script> | |
const button = document.getElementById('button') | |
function payMoney() { | |
console.log('已剁') | |
console.log(this) | |
} | |
// 防抖 | |
function debounce(func, delay) { | |
let timer | |
return function () { | |
let context = this | |
let args = arguments | |
clearTimeout(timer) | |
timer = setTimeout(function () { | |
func.call(context, arguments) | |
}, delay) | |
} | |
} | |
// 节流 | |
function throttle(func, delay) { | |
let pre = 0; | |
return function () { | |
let context = this | |
let args = arguments | |
let now = new Date() | |
if (now - pre > delay) { | |
func.apply(context, args) | |
pre = now | |
} | |
} | |
} | |
// button.addEventListener('click', debounce(payMoney, 1000)) | |
button.addEventListener('click', throttle(payMoney, 1000)) | |
// button.addEventListener('click', payMoney) | |
</script> | |
</html> |
# 函数柯里化
计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数 (最初函数的第一个参数) 的函数,并且返回接受余下的参数且返回结果的新函数的技术。这个技术以逻辑学家 Haskell Curry 命名的。
柯里化的目的在于避免频繁调用具有相同参数函数的同时,又能够轻松的重用。
// 函数柯里化例子 | |
const curryingAdd = function(x) { | |
return function(y) { | |
return function(z) { | |
return x + y + z | |
} | |
} | |
} | |
// 调用 | |
curryingAdd(1)(2)(3) | |
// 即 | |
const fn = curryingAdd(1) | |
const fn1 = fn(2) | |
fn1(3) | |
// 假设我们有一个求长方形面积的函数 | |
function getArea(width, height) { | |
return width * height | |
} | |
// 如果我们碰到的长方形的宽老是 10 | |
const area1 = getArea(10, 20) | |
const area2 = getArea(10, 30) | |
const area3 = getArea(10, 40) | |
// 我们可以使用闭包柯里化这个计算面积的函数 | |
function getArea(width) { | |
return height => { | |
return width * height | |
} | |
} | |
const getTenWidthArea = getArea(10) | |
// 之后碰到宽度为 10 的长方形就可以这样计算面积 | |
const area1 = getTenWidthArea(20) | |
// 而且如果遇到宽度偶尔变化也可以轻松复用 | |
const getTwentyWidthArea = getArea(20) |
# 七大排序
排序方法 | 平均时间复杂度 | 最坏时间复杂度 | 最好时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
直接插入排序 | O(n^2) | O(n^2) | O(n) | O(1) | 稳定 |
希尔排序 | O(n^1.3) | O(n^2) | O(n) | O(1) | 不稳定 |
冒泡排序 | O(n^2) | O(n^2) | O(n) | O(1) | 稳定 |
快速排序 | O(n) | O(n^2) | O(n) | O() | 不稳定 |
直接选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不稳定 |
堆排序 | O(n) | O(n) | O(n) | O(1) | 不稳定 |
归并排序 | O(n) | O(n) | O(n) | O(n) | 稳定 |
# 选择排序
** 思路分析:** 第一趟从 n 个元素的数据序列中选出关键字最小 / 大的元素并放在最前 / 后位置,下一趟从 n-1 个元素中选出最小 / 大的元素并放在最前 / 后位置。以此类推,经过 n-1 趟完成排序。
import *as aryMake from "./aryMake.js" | |
// 选择排序 | |
function selectSort(ary) { | |
for (let i = 0; i < ary.length; i++) { | |
let min = i | |
for (j = i + 1; j < ary.length; j++) { | |
if (ary[j] < ary[min]) { | |
min = j | |
} | |
} | |
[ary[i], ary[min]] = [ary[min], ary[i]] | |
} | |
return ary | |
} | |
let arr = ary(30) | |
console.log(selectSort(arr)) | |
//O (n^2) 不稳定 |
直接选择排序的最好时间复杂度号最坏时间复杂度都是 O (n^2),因为即使数组一开始就是正序的,也需要将两重循环进行完,平均时间复杂度也是 O (n^2),最好的时间复杂度为 O (n^2),空间复杂度为 O (1),因为不占用多余的空间。直接选择排序是一种原地排序并且不稳定的排序算法.
# 冒泡排序
思路分析:
①比较相邻的元素。如果第一个比第二个大,就交换他们两个。
②对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
③针对所有的元素重复以上的步骤,除了最后一个。
④持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
import { ary } from "./aryMake.js" | |
// 冒泡排序 | |
function bubbleSort(ary) { | |
for (let i = 0; i < ary.length; i++) { | |
var flag = true | |
for (let ii = 0; ii < ary.length - i - 1; ii++) { | |
if (ary[ii] > ary[ii + 1]) { | |
[ary[ii], ary[ii + 1]] = [ary[ii + 1], ary[ii]] | |
flag = false | |
} | |
} | |
// 本次排序都没有发生交换,说明已经有序了 | |
if (flag) { | |
break | |
} | |
} | |
return ary | |
} | |
let ary1 = ary(30) | |
console.log(bubbleSort(ary1).join()) | |
//O (n^2) 稳定 |
总结:冒泡排序算法最坏的情况和平均复杂度是 O (n^2),最好时间复杂度为 O (n),空间复杂度为 O (1),排序算法稳定。
# 插入排序
思路分析:
①在长度为 N 的数组,将数组中第 i [1~(N-1) ] 个元素,插入到数组 [0~i] 适当的位置上。
②在排序的过程中当前元素之前的数组元素已经是有序的了。
③在插入的过程中,有序的数组元素,需要向右移动为更小的元素腾出空间,直到为当前元素找到合适的位置。
import *as aryMake from "./aryMake.js" | |
// 插入排序 | |
export function insertSort(ary) { | |
for (let i = 1; i < ary.length; i++) { | |
let j = i, insert = ary[j] | |
while ((j > 0) && (insert < ary[j - 1])) { | |
ary[j] = ary[j - 1] | |
j -= 1 | |
} | |
ary[j] = insert | |
} | |
return ary | |
} | |
let arr = aryMake.ary(30) | |
console.log(insertSort(arr)) | |
//O (n^2) 稳定 |
总结:时间复杂度平均为 O (n^2),最坏为 O (n^2),最好为 O (n),空间复杂度为 O (1),排序算法稳定。
# 希尔排序
希尔排序是对插入排序最坏的情况的改进,主要是减少数据移动次数,增加算法的效率。
思路分析:
先比较距离远的元素,而不是像简单交换排序算法那样先比较相邻的元素,这样可以快速减少大量的无序情况,从而减轻后续的工作。被比较的元素之间的距离逐步减少,直到减少为 1,这时的排序变成了相邻元素的互换。
import *as aryMake from "./aryMake.js" | |
// 希尔排序 | |
function shellSort(ary) { | |
let gap = 1 | |
while (gap < ary.length / 3) { | |
gap = gap * 3 + 1 | |
} | |
for (gap; gap > 0; gap = Math.floor(gap / 3)) { | |
for (let i = gap; i < ary.length; i++) { | |
for (let j = i - gap; ary[j] > ary[j + gap] && j >= 0; j -= gap) { | |
[ary[j], ary[j + gap]] = [ary[j + gap], ary[j]] | |
} | |
} | |
} | |
return ary | |
} | |
let arr = aryMake.ary(30) | |
console.log(arr) | |
console.log(shellSort(arr)) |
总结:希尔排序通过将比较的全部元素分为几个区域来提升插入排序的性能。这样让一个元素可以一次性的朝最终的位置前进一大步。然后算法再取越来越小的步长进行排序,算法最后一步就是普通的插入排序,但是到了这步,需要排序的数据几乎接近有序了(此时插入排序较快),时间复杂度平均为 O (nlogn),最坏为 O (nlogn),空间复杂度为 O (1),排序算法不稳定。
# 归并排序
思路分析:
当一个数组左边有序,右边也有序,那合并这两个有序数组就完成了排序。如何让左右两边有序了?用递归!这样递归下去,合并上来就是归并排序。
import *as aryMake from "./aryMake.js" | |
// 归并排序 | |
function mergeSort(ary) { | |
let len = ary.length | |
if (len < 2) { return ary } | |
let lenL = Math.floor(len / 2) | |
return ary = merge(mergeSort(ary.slice(0, lenL)), mergeSort(ary.slice(lenL))) | |
} | |
function merge(aryLeft, aryRight) { | |
let aryMerge = [] | |
while (aryLeft.length > 0 && aryRight.length > 0) { | |
aryMerge.push((aryLeft[0] < aryRight[0]) ? aryLeft.shift() : aryRight.shift()) | |
} | |
return aryMerge.concat(aryLeft.concat(aryRight)) | |
} | |
let arr = ary(30) | |
console.log(mergeSort(arr)) | |
//O (n*log2n) 稳定 |
总结:平均和最坏、最好时间复杂度为 O (nlogn),空间复杂度 O (n),排序算法稳定。
# 快速排序
本质上快速排序把数据划分成几份,所以快速排序通过选取一个关键数据,再根据它的大小,把原数组分成两个子数组:第一个数组里的数都比这个主元数据小或等于,而另一个数组里的数都比这个主元数据要大或等于。
import *as aryMake from "./sort/aryMake.js" | |
import { insertSort } from "./sort/insertSort.js" | |
function quickSort(ary) { | |
let len = ary.length | |
// 小区间优化 | |
if (len < 5) { | |
return insertSort(ary) | |
} | |
// 三数取中法 | |
let mid = Math.floor(len / 2) | |
if (ary[0] > ary[mid]) { [ary[0], ary[mid]] = [ary[mid], ary[0]] } | |
if (ary[mid] > ary[len - 1]) { [ary[mid], ary[len - 1]] = [ary[len - 1], ary[mid]] } | |
// 左右指针 | |
let left = 0, right = len - 1, key = ary[mid] | |
while (left < right) { | |
while (ary[left] <= key && left < right) { left++ } | |
while (ary[right] >= key && left < right) { right-- } | |
if (left < right) { | |
[ary[left], ary[right]] = [ary[right], ary[left]] | |
} | |
} | |
if (ary[left] < key) { left++ } | |
return quickSort(ary.slice(0, left)).concat(quickSort(ary.slice(left))) | |
} | |
let ary0 = aryMake.ary(40) | |
console.log(quickSort(ary0)) |
总结:平均时间复杂度为 O (n),最坏时间复杂度为 O (n^2),空间复杂度为 O (n),排序算法不稳定。
# 堆排序
思路分析:
①将长度为 n 的待排序的数组进行堆有序化 (从最后一个具有子节点的父节点开始 Math.floor(ary.length / 2) - 1
) 构造成一个大顶堆;
②将根节点与尾节点交换并输出此时的尾节点;
③将剩余的 n -1 个节点重新进行堆有序化;
④重复步骤 2 与将根节点进行堆有序化直至构造成一个有序序列;
import *as aryMake from "./aryMake.js" | |
// 堆排序 | |
function heapSort(ary) { | |
for (let i = Math.floor(ary.length / 2) - 1; i >= 0; i--) { | |
// console.log(ary) | |
heap(ary, i, ary.length) | |
} | |
for (let j = ary.length - 1; j > 0; j--) { | |
[ary[0], ary[j]] = [ary[j], ary[0]] | |
heap(ary, 0, j) | |
} | |
return ary | |
} | |
function heap(ary, i, len) { | |
for (let k = 2 * i + 1; k < len; k = 2 * k + 1) { | |
if (k + 1 < len && ary[k + 1] > ary[k]) { k += 1 } | |
if (ary[k] > ary[i]) { | |
[ary[i], ary[k]] = [ary[k], ary[i]] | |
i = k | |
} else { break } | |
} | |
} | |
let arr = aryMake.ary(30) | |
console.log(heapSort(arr)) |
堆排序的最好和最差情况时间复杂度都为 O (n),平均时间复杂度为 O (n),空间复杂度为 O (1),排序算法不稳定,无需使用多余的空间帮助排序。优点是占用空间较小,时间复杂度较低,缺点是实现较为复杂,并且当待排序序列发生改动时,哪怕是特别小的改动,都需要调整整个堆来维护堆的性质,维护开销较大。