Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
互联网发展迅猛之余也伴随着互联网寒冬,行业不景气这样的词,等毕业季去各个求职网站投简历,去各个人才市场找机会,才发现四处碰壁,作为应届求职者更需要打好基础,明确发展规划,跟上行业步伐。下面是本人2019年秋招前端面试经历,结合个人博客和牛油们面经中的高频问题以及行业前辈们复习资料的综合整理,包含基础篇、Vue框架篇、HTTP&浏览器、构建工具篇、安全篇、算法篇,欢迎交流斧正。希望大家在毕业季都能一帆风顺,从容斩获OFFER
第一版能看到什么:
90页+PDF
含100+前端高频面试题及其推荐解答
资源分享常用工具优质社区团队个人博客
如果是应届生如何规划自己从毕业到就职之路,通过哪些途径参与求职
这本小册里都能得到解答
🤤 主要面向对象:应届生求职--前端
Vue篇以Vue为主的一些框架问题,后面会考虑加上React
构建工具篇以前端自动化构建中的webpack为核心
面试官一般都会根据回答进行追问,所以小册总结上下几个问题一般具有连贯性
其次也正是因为是面试小册,有些问题都只是总结出较核心概念,比如浏览器解析渲染页面的过程,其实是一个很细的过程,其中滋味还需花时间细品;
Gitbook:
那你说Gitbook国内访问比较慢我也不能科学上网,我是不是不适合学前端 🤣 这两好像没一点关系,我来帮你解决 👇
Github阅读:
那你说我觉得你这点总结的不对或者有更好的建议,该咋办,很简单,你给我发个issue,我会第一时间跟进
那我想下载到本地呢,打开你的git bash:
敲个回车这么简单(这里推荐一个markdown编辑器Typora)
那你说git我也没有装呀,我太难了,我是不是不适合学前端🤣建议还是安装下
那你又说我就不想装,有个性,你遇到了我,我帮你解决👇
为了让有个性的同学也看到这份前端面试小册,我推出了方案C,90页+PDF为你准备
我已经发布了release版本:
v1.0 Jan 9 , 2020
完成近100+前端高频面试题以及推荐解答
添加资源大礼包(优质博客,平台推荐,学习资源等)
🤔 已完善
Q10
🤔 已完善
Q5-10
😀 已完善
Q5-10
😀 已完善
求职准备
🤔 已完善
学习资源
🤔 已完善
篇章
第一版预计完成
状态
Q20
😀 已完善
Q25
😀 已完善
Q15
🤔 已完善
Q15
git clone https://github.com/okaychen/FE-Interview-Brochure.git前端安全方面,应届生遇到的多为常识性问题,一般不具有太大难度,但是必须要了解
前端应用中常遇到的安全问题:
跨站脚本xss
跨站请求伪造 csrf
网络劫持攻击
iframe滥用
恶意第三方库
根据攻击来源的不同,通常分为三种:
反射型
反射性通常发生在URL地址的参数中,常用来窃取客户端的cookie或进行钓鱼欺骗,经常在网站的搜索栏,跳转的地方被注入
存储型
相比反射型 XSS 的恶意代码存在 URL ⾥,存储型 XSS 的恶意代码存在数据库⾥,不需要用户去点击URL进行触发,提前将恶意代码保存在了漏洞服务器或者客户端中,站点取出后会自动解析执行
DOM型
较上面两种,DOM型取出和执行恶意代码都由浏览器端完成,属于前端自身安全漏洞
xss攻击有两大要素:攻击者提交恶意代码,浏览器执行恶意代码
针对cookie劫持,攻击者向漏洞页面写入恶意代码获取cookie,可以通过防止cookie会话劫持来防范,一般要在设置cookie时加HttpOnly,来禁止意外注入站点的恶意js代码操作Cookie造成xss攻击
提高攻击门槛:(XSS Filter)针对用户提交的数据进行有效的验证,只接受我们规定的长度或内容的提交,过滤掉其他的输入内容 或者 将特殊字符输出编码(xss Escape)
xss漏洞检测poc:通过漏洞检测代码,完成自测,最小化被攻击风险
CSRF(Cross-site request forgery)跨站请求伪造:攻击者诱导受害者进⼊第三⽅⽹站,在第三⽅⽹站中,向被攻击⽹站发送跨站请求。利⽤受害者在被攻击⽹站已经获取的注册凭证,绕过后台的⽤户验证,达到冒充⽤户对被攻击的⽹站执⾏某项操作的⽬的,
常规的过程如下:
用户打开浏览器,访问受信任网站A,输入用户名和密码请求登录网站A
在用户信息通过验证后,网站A产生Cookie信息并返回给浏览器
未退出网站A之前,在同一浏览器中,攻击者引诱用户进入网站B
网站B接收到用户请求后,返回一些攻击性代码,并发出一个请求要求访问第三方站点A
CSRF通常从第三⽅⽹站发起,被攻击的⽹站⽆法防⽌攻击发⽣,只能通过增强⾃⼰⽹站针对CSRF的防护能⼒来提升安全性
CSRF有两个特点:
通常发生在第三方域名
攻击者不能获取到cookie等信息,只是使用
针对这两点,我们可以制定专门的防护策略:
阻止不明外域的访问
同源检测
Samesite Cookie
提交时要求附加文本域验证身份才可以获取信息
通过使用Orgin Header确定来源域名:在部分与CSRF有关的请求中,请求的Header中会携带Origin字段,如果Origin存在,那么直接使⽤Origin中的字段确认来源域名就可以
使用Referer Header确定域名来源:根据HTTP协议,HTTP头中有一个字段叫Referer,记录了该HTTP请求的来源地址
Google起草了⼀份草案来改进HTTP协议,那就是为Set-Cookie响应头新增Samesite属性,它⽤来标明这个 Cookie是个“同站 Cookie”,同站Cookie只能作为第⼀⽅Cookie,不能作为第三⽅Cookie,Samesite 有两个属性值:
Samesite=Strict: 这种称为严格模式,表明这个 Cookie 在任何情况下都不可能作为第三⽅ Cookie
Samesite=Lax: 这种称为宽松模式,⽐ Strict 放宽了点限制,假如这个请求是这种请求且同时是个GET请求,则这个Cookie可以作为第三⽅Cookie
通过csrf token验证请求的身份,是够携带正确的token,来区分正常的请求和攻击的请求
CSRF Token的防护策略分为三个步骤:
将CSRF Token输出到⻚⾯中
⻚⾯提交的请求携带这个Token
服务器验证Token是否
利⽤CSRF攻击不能获取到⽤户Cookie的特点,我们可以要求Ajax和表单请求携带⼀个Cookie中的值
双重Cookie采⽤以下流程:
在⽤户访问⽹站⻚⾯时,向请求域名注⼊⼀个Cookie,内容为随机字符串
在前端向后端发起请求时,取出Cookie,并添加到URL的参数中
后端接⼝验证Cookie中的字段与URL参数中的字段是否⼀致,不⼀致则拒
网络劫持一般分为两种:HTTP劫持和DNS劫持
DNS劫持
DNS强制解析:通过修改运营商的本地DNS记录,来引导⽤户流量到缓存服务器
302跳转的⽅式:通过监控⽹络出⼝的流量,分析判断哪些内容是可以进⾏劫持处理的,再对劫持的内存发起跳转的回复,引导⽤户获取内容
HTTP劫持: 由于http明⽂传输,运营商会修改你的http响应内容(即加⼴告)
DNS劫持由于涉嫌违法,已经被监管;针对HTTP劫持,最有效的办法就是全站HTTPS,将HTTP加密,这使得运营商⽆法获取明⽂,就⽆法劫持你的响应内容
中间人(Man-in-the-middle attack, MITM)是指攻击者和通讯的两端分别创建独立的联系,并交换其得到的数据,攻击者可以拦截通信双方的通话并插入新的内容
一般的过程如下:
客户端发送请求到服务端,请求被中间⼈截获
服务器向客户端发送公钥
中间⼈截获公钥,保留在⾃⼰⼿上。然后⾃⼰⽣成⼀个【伪造的】公钥,发给客户端
客户端收到伪造的公钥后,⽣成加密hash值发给服务器
推荐:
浏览器在接收到这些攻击性代码后,根据网站B的请求,在用户不知情的情况下携带Cookie信息,向网站A发出请求。网站A根据用户的Cookie信息以及用户的权限处理该请求,导致来自网站B的恶意代码被执行
CSRF Token
双重cookie验证
中间⼈获得加密hash值,⽤⾃⼰的私钥解密获得真秘钥,同时⽣成假的加密hash值,发给服务器
服务器⽤私钥解密获得假密钥,然后加密数据传输给客户端
webpack是事实上的前端打包标准,使用较广泛,相比其他打包工具webpack更是面试中的热点
webpack4是18年8月25号发布的,相比webpack3主要以下大改动:
node环境升级,不再支持node4.0及之前版本,最低node版本6.11.5
配置增加了mode:production/development/none,必须指定其中一个,在不同的mode下开启了一些默认的优化手段;
不再需要某些plugin,改为在对应生产或者开发模式下默认打开
详细改进:
loader能让webpack处理不同的文件,然后对文件进行一些处理,编译,压缩等,最终一起打包到指定文件中(比如loader可以将sass,less文件写法转换成css,而不需要在使用其他转换工具),loader本身是一个函数,接收源文件作为参数,返回转换的结果
plugins则用于执行广泛的任务,从打包,优化,压缩,一直到重新定义环境中的变量,接口很强大,主要用来扩展webpack的功能,可以实现loader不能实现的更复杂的功能
loader能把源文件经过转化后输出新的结果,一个loader遵循单一职责原则,只完成一种转换,然后链式的顺序去依次经过多个loader转换,直到得到最终结果并返回,所以在写loader时要保持其职责的单一性,同时webpack还提供了一些API供loader调用
开发plugin中最常用的两个对象是Compiler和Compilation,他们是plugin和webpack之间的桥梁
Compiler对象包含了webpack环境的所有配置信息,包含options,loaders,plugins这些信息,这个对象在Webpack启动时候被实例化,它是全局唯一的
Compilation对象包含了当前的模块资源,编译生成资源,变换的文件等。当webpack以开发模式运行时,每当检测到一个文件变化,一个新的Compilation将会被创建
单页应用即为webpack的标准模式,直接在entry中指定单页面应用的入口即可:
多页面应用可以考虑使用webpack的AutoWebPlugin来完成简单的自动化构建,前提是项目目录结构要符合预先设定的规范
webpack热更新(Hot Module Replacement),缩写为HMR,实现了不用刷新浏览器而将新变更的模块替换掉旧的模块,原理如下:
server端和client端都做了处理:
webpack监听到文件变化,重新编译打包,webpack-dev-server和webpack之间接口交互(主要是webpack-dev-middleware调用webpack暴露的API对代码进行监控,并告诉webpack将打包后代码保存到内存中)
通过sockjs(webpack-dev-server的依赖)在浏览器和服务器之间建立 一个websocket长连接,将webpack编译打包各阶段的信息告知浏览器端
webpack根据 webpack-dev-server/client 传给它的信息以及 dev-server的配置决定是刷新浏览器还是进⾏模块热更新,如果是模块热更新继续执行,否者刷新浏览器
参考:
webpack的构建流程是一个串行的过程,从启动到结束依次执行如下:
① 初始化参数:从配置文件和shell语句中读取与合并参数,得出最终的参数
② 开始编译:用上一步得到的参数初始化 Compiler对象,加载所有配置的插件,通过执行对象的run方法开始执行编译
③ 确定入口:根据配置中的entry找出所有的入口文件
④ 编译模块:从入口文件出发,调用所有配置的loader对模块进行"加载",再找出该模块依赖的模块,递归此步骤知道所有入口依赖的文件都经过处理结束
压缩代码:比如利用UglifyJsPlugin来对js文件压缩
CDN加速:将引用的静态资源修改为CDN上的路径。比如可以抽离出静态js,在index利用CDN引入;利⽤webpack对于 output 参数和各loader的publicPath参数来修改资源路径
删除Tree Shaking:将代码中永远不会走到的片段删除。可以通过在启动webpack时追加参数 --optimize-minimize 来实现
<!--比如-->
http://www.test.com/search.php?key="><script>alert("XSS")</script>// node设置httponly
'Set-Cookie' : 'SSID=EqAc1D; Expires=Wed; HttpOnly'<!--比如img图片标记属性跨站攻击代码-->
<img src="javascript:alert(/xss/)"></img><img dynsrc="javascript:alert('xss')">
<!--无需 “<>”,利用 html 标记事件属性跨站-->
<img src="" onerror=alert("xss")>HotModulePlugin 将会对新旧模块进⾏对⽐,决定是否更新模块
当 HMR 失败后,回退到 live reload 操作,也就是进⾏浏览器刷新来获取最新打包代码
⑤ 完成编译模块:处理结束,得到了每个模块被"加载"之后的最终内容以及他们之间的依赖关系
⑥ 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的chunk,再将每个chunk转换成一个单独的文件加入输出列表中
⑦ 输出完成:确定输出内容之后,根据配置确定输出的路径和⽂件名,写⼊到⽂件系统
优化图片,对于小图可以使用 base64 的方式写入文件中
// build/webpack.base.conf.js
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
options: vueLoaderConfig
},
]
},// build/webpack.prod.conf.js
plugins: [
new webpack.DefinePlugin({
'process.env': env
}),
new UglifyJsPlugin({
uglifyOptions: {
compress: {
warnings: false
}
},
sourceMap: config.build.productionSourceMap,
parallel: true
}),
]// 一个简单的loader例子
function replace(source) {
// 使用正则把 @require '../style/index.css' 转换成 require('../style/index.css');
return source.replace(/(\/\/ *@require) +(('|").+('|")).*/, 'require($2);');
}
module.exports = function (content) {
return replace(content);
};// 最基础的plugin代码
class BasicPlugin{
// 在构造函数中获取用户给该插件传入的配置
constructor(options){ }
// Webpack会调用BasicPlugin实例的apply方法给插件实例传入compiler对象
apply(compiler){
conpiler.plugin('compilation',function(compilation){ })
}
}
module.exports = BasicPlugin;module.exports = {
entry: {
app: './src/main.js'
},
}├── pages
│ ├── index
│ │ ├── index.css // 该页面单独需要的 CSS 样式
│ │ └── index.js // 该页面的入口文件
│ └── login
│ ├── index.css
│ └── index.js
├── common.css // 所有页面都需要的公共 CSS 样式
├── google_analytics.js
├── template.html
└── webpack.config.js应届生找一份前端或者互联网其他行业工作我们需要做哪些准备,注意哪些问题
大学或者研究生应尽早设立目标,在相对自由的氛围里找到自己真正热爱的东西
提高自身专业知识度和综合素质,增加人生阅历
去了解一个企业或者一个岗位JD,作为自己的发展目标
用自己的大学或者研究生为自己准备一份简历
简历需要什么不需要什么:
简历是一份一页或者两页pdf,请放弃word,h5等,尽量简介大方,放弃色彩搭配黑白最佳,简洁但是要满,不要稀稀拉拉的,如果是设计岗的同学,请上传现有作品的附件或者链接
HR需要的是一份一眼就能知道你跟这个岗位很吻合的简历
跳出舒适圈,走出学校去一个企业实习
去争取一份工作,平时应该着重培养哪些:
如果应聘技术类的岗位:一定要写点什么,把代码量搞上去,自己发在github上面,有自己代表性的项目,如果刚好你喜欢写作,喜欢经营自己的技术博客那么恭喜你,你会是一个幸运儿,无论你从事什么行业,写作能力真的会伴随你一生
抓住招聘季,春季招聘金三银四,秋季招聘金九银十
分清春季和秋季招聘的区别:
春季招聘多数是招聘本年度暑期实习生,少数是对去年秋季招聘的补招
大三暑假的秋季招聘7月开始是毕业季招聘的核心,是为公司储备应届毕业人才
原文地址:
和岗位的无关的内容一概不要写,尤其不要写自己的生活兴趣爱好等,喜欢运动音乐弹个吉他什么的一类无关紧要的话,hr真的没有时间细看,不小心瞟到了会显得在凑字数
不要造假,不要造假,不要造假,重要的事情说三遍,一旦被证实耽误的真的是你一辈子的就业前途,会被企业永久拉进黑名单,请正视这个问题,现在时间还早,不如多花时间提高,为自己准备一份合格的简历
和岗位JD有关的关键词,多写认真写,在项目中清楚体现
项目经历结构化,按照STAR原则去写
分清暑期实习和日常实习的区别:
暑期实习是为秋季招聘的预备人才,一般三个月以上,需要答辩成功后会有直接转正机会
日常实习也需要到岗实习,实习时间自由一般一周4-5天,看部门缺口很少有转正机会
投递途径:
内推&网申
宣讲会&网申
正视内推
内推只是简历投递的一种途径,你的师兄师姐在你想投递的公司可以帮你内推,也可能直接帮你把简历给部门经理,但是免不免笔试,面试结果如何他还没办法左右
宣讲会:企业会预先安排自己的宣讲会行程,会在哪些学校进行,一般在9月进行,是介绍企业文化,人才培养计划等路演形式,分为一站式和非一站式,一站式现场收简历并且笔试,通过后一般在接下来2-3内在该学校附近就行面试,非一站式可能现场收简历,但是不会笔试面试
招聘流程:
提前批&正式批&补招
提前批是在正式批次的预热,简历一般要求较高,学历,经验,能力,如果提前批你已经应聘上了心仪的公司,恭喜你,你的毕业招聘季在9月份就已经正式结束,也就是大四开学的那段时间
正式批一般是网申批次,走正常招聘流程,不免笔试
补招是企业对秋季招聘人才缺口的补充,在正式批结束后一段时间进行,一般在11月后
笔试(部分提前批内推或者优秀简历免笔试)
面试(分为三种,视频面,现场面和电话面,一般为三面,三面为HR面,技术岗三面可能都为技术面,部分有增加交叉面)
offer流程
意向书和offer流程
意向书,意向书是offer前的接收意向说明,意向书少数情况会被企业"拥抱变化"
offer是用人单位单方愿意接收人才就业,应聘人才有权利拒接,无影响
多offer选择,要综合多方面信息,比如平台大小,发展前景,薪资福利待遇,选择最心仪的
关于录取通知书,两方和三方
三方大四毕业季每个学校都会发,是用人单位,个人和学校三方具有法律效力的合同
录取通知书是应聘人才愿意接收offer,需要签字生效,会写明应届生需要寄交三方,生效后任何一方违约都需要赔偿录取通知里写明的违约金
正式接受后,一般可以选择毕业前去实习,也可以不实习自己安排余下时间等毕业申请入职
HTML&CSS篇(第一个版本该篇预计总结常见问题20个左右),HTML&CSS不少问题都能体现应届生对于前端基础的掌握程度,是应届生求职时不可忽视的重要一环
大致过程:
HTML解析构建DOM->CSS解析构建CSSOM树->根据DOM树和CSSOM树构建render树->根据render树进行布局渲染render layer->根据计算的布局信息进行绘制
不同浏览器的内核不同,所以渲染过程其中有部分细节有不一样,以webkit主流程为例:
一篇很棒的文章(需科学上网):How Browser Work
编者有话说:浏览器解析渲染页面过程是一个复杂的过程,其中有不少的细节和规则,如果把上面分享的文章翻译成译文,至少有3~5页PDF左右,所以这里只能总结大致过程(作为面试回答【很可能让回答的尽可能详细】了解来说已经足够,更深入的了解可以好好读下上面那篇文章)
较详细过程:
HTML解析构建DOM树:其中HTML Parser就起到了将HTML标记解析成DOM Tree的作用,HTML Parser将文本的HTML文档,提炼出关键信息,嵌套层级的树形结构,便于计算拓展;这其中也有很多的规则和操作,比如容错机制,识别特殊标签<br></br>等
CSS解析构建CSSOM树:CSS Parser将很多个CSS文件中的样式合并解析出具有树形结构Style Rules,也叫做CSSOM。
※其中还有一个细节是浏览器解析文档:当遇到
<script>标签的时候会停止解析文档,立即解析脚本,将脚本中改变DOM和CSS的地方分别解析出来,追加到DOM Tree和CSSOM上
根据DOM树和CSSOM树构建Render树:Render Tree的构建其实就是DOM Tree和CSSOM Attach的过程,在webkit中,解析样式和创建呈现器的过程称为"附加",每个DOM节点都有一个"attach"方法,Render Tree其实就相当于一个计算好样式,与HTML对应的Tree
根据Render树进行布局渲染render layer:创建渲染树后,Layout根据根据渲染树中渲染对象的信息,计算好每一个渲染对象的位置和尺寸,将其放在浏览器窗口的正确位置,某些时候会在文档布局完成之后进行DOM修改,重新布局的过程就称为回流
※其中计算(样式计算)一个复杂的过程,因为DOM中的一个元素可以对应样式表中的多个元素,Firefox采用了规则树和样式上下文树来简化样式计算,规则树包含了所有已知规则的匹配路径,样式上下文包含端值,webkit也有样式对象,但它们不保存在类似上下文树这样的结构中,只是由DOM节点指向此类对象的相关样式
根据计算的布局信息进行绘制:绘制阶段则会遍历呈现树,并调用呈现器的paint方法,将呈现器的内容显示在屏幕上,绘制的顺序其实就是元素进入堆栈样式上下文的顺序,例如,块呈现器的堆栈顺序如下:1.背景颜色,2.背景图片,3.边框,4.子代,5.轮廓
区别:
回流指当前窗口发生改变,发生滚动操作,或者元素的位置大小相关属性被更新时会触发布局过程,发生在render树,比如元素的几何尺寸变化,就需要重新验证并计算Render Tree
重绘指当前视觉样式属性被更新时触发的绘制过程,发生在渲染层render layer
所以相比之下,回流的成本要比重绘高得多
减少回流重绘次数的方法:
1)避免一条一条的修改DOM样式,而是修改className或者style.classText
2)对元素进行一个复杂的操作,可以先隐藏它,操作完成后在显示
3)在需要经常获取那些引起浏览器回流的属性值时,要缓存到变量中
4)不使用table布局,一个小的改动可能就会引起整个table重新布局
页面性能优化有一条,用transform代替top,left来实现动画。那么transform的优势在哪里?如何开启GPU加速渲染?开启GPU硬件加速可能会触发的问题,如何解决?
首先相比定位的top&left来说,transform不会引起整个页面的回流和重绘。其次我们可以通过transform开启GPU硬件加速,提高渲染速度,但相应的transform也会占用更多的内存。
可能会导致浏览器频繁闪烁或者抖动,解决方案:
首先需要了解的是,移动端在touch上一共有4个事件,
执行顺序为touchstart -> touchmove -> touchend -> touchcancel
当用户点击屏幕时,会触发touch和click事件,touch事件会优先处理,touch事件经过捕获,目标,冒泡一系列流程处理完成之后,才会触发click,所有我们经常会谈到移动端点击事件300ms延迟的问题
移动端点击事件300ms问题,常见的解决方案:
阻止用户双击缩放,并限制视口大小
设置csstouch-action用于指定某个给定的区域是否允许用户操作,以及如何相应用户操作
fastclick.js来解决,其原理是在检测到touchend事件的时候,会通过自定义事件立即触发模拟一个click事件,并在300ms之后把真正的click事件阻止掉
点透现象:
发生条件:①按钮A和按钮B不是后代继承关系,②A发生touch,A touch后立即消失,B绑定click,③A z-index大于B,即 A 显示在 B 浮层之上
发生原因:当点击屏幕时,系统生成touch和click两个事件,touch先执行,touch执行完之后A消失,然后要执行click的时候,就会发现用户点击的是B,所以就执行了B的click
解决方法:①阻止默认事件,在touch的某个时间段执行event.preventDefault,去取消系统生成的click事件,一半在 touchend 中执行。②要消失的元素延迟300ms后在消失
Doctype是一种DTD文档定义类型,必须声明在HTML文档的第一行,用来规范文档使用哪种方式解析HTML,三种模式分别是怪异模式,标准模式,近乎模式(IE8的一种近乎于前两者之间的一种模式);标准模式按照HTML和CSS定义渲染,怪异模式会模拟更旧的浏览器行为
标准盒模型和IE怪异盒模型,标准盒模型下:盒子总宽度/高度=width/height+padding+border+margin
怪异盒模型,IE5.X 和 6 在怪异模式中使用自己的非标准模型,盒子的总宽度和高度是包含内边距padding和边框border宽度在内的:盒子总宽度/高度=width/height + margin = width/height + margin;
boxsizing属性content-box使用标准盒模型的计算方式,border-box则使用怪异盒模型的计算方式
首先,margin塌陷是相对于父子级关系的两个元素,而margin合并是相对两个兄弟级关系的两个元素
两个兄弟级关系的元素,垂直方向上的margin,其外边距会发生重叠现象,两者两个的外边距取的是两个所设置margin的最大值,就是所说的margin合并问题
两个父子级关系的元素,垂直方向上的margin会粘合在一起,外层和模型的margin-top取两个元素中margin-top的最大值,发生margin塌陷的内层元素相对于整个文档移动
解决方案:两者都可以通过触发BFC来解决
首先需要知道文档流分为定位流,浮动流和普通流三种,而普通流则是BFC中的FC(格式化上下文),它是页面的一块渲染区域,有一套渲染规则,决定了子元素如何布局,以及和其他元素之间的关系和作用,常见的FC有BFC,IFC,还有GFC和FFC
BFC块级格式化上下文,IFC行级格式化上下文,
哪些元素会触发BFC:
根元素
float的属性不为none
position属性为absolute或fixed
display为inline-block,table-cell,table-caption,flex
absolute会使元素位置与文档流无关,不占据空间,absolute 定位的元素和其他元素重叠
relative相对定位时,无论元素是否移动,仍然占据原来的空间
sticky是2017年浏览器才开始支持,会产生动态效果,类似relative和fixed的结合,一个实例是"",生效前提是必须搭配top,left,bottom,right一起使用,不能省略,否则等同于relative定位,不产生"动态固定"的效果
使用Flexbox
百分比布局结合媒体查询
使用rem
rem转换像素大小(根元素的大小乘以rem值),取决与页面根元素的字体大小,即HTML元素的字体大小
em转换像素大小(em值乘以使用em单位的元素的字体大小),比如一个div的字体大小为16px,那么10em就是180px(或者接近它)
① flex实现水平垂直居中
② 进行垂直水平居中(利用transform中translate偏移的百分比值是相对于自身大小的特性)
① 绝对定位+margin:auto
② 使用绝对定位与负边距
三栏布局:两侧定宽中间自适应,在开发中很常见的布局方式,主要有下面六种实现方式:
最简单的方式就是通过float实现
利用绝对定位
利用BFC规则
圣杯布局(通过margin负值调整)
flex意为弹性布局,有两大概念,一是容器(container),二是项目(item),两者都有各自的六个常用属性
用在容器上的六个属性:
flex-direction主轴的方向,
flex-wrap一条轴线装满,如何换行,
flex-flow是前两者的简写,
用在项目上的六个属性:
order排序顺序,默认为0,越小越靠前
flex-grow项目的放大比例,默认为0,不放大
flex-shrink项目的缩小比例,默认为1,空间不足该项目会缩小
清除浮动主要是为了解决父元素因为子元素浮动内部高度为0的问题
额外标签,即在最后一个浮动标签后加一个空div,给其设置clear:both,缺点是增加无意义标签,语义化差
给父级元素添加overflow:不为visible通过触发BFC的方式清除浮动
使用after伪元素,缺点是IE6-7不支持伪元素,zoom:1触发hasLayout
使用before和after双伪元素清除浮动
css sprits又名雪碧图,也叫css精灵,开发人员会将很多小图标合并在一起之后的图片成为雪碧图,使用时通过background-image、background-position和background-size属性,将对应的小图标添加元素中
优点是:
减少加载多张图片的HTTP请求数
可以提前加载资源
缺点是:
维护成本较高,需要改动这张合并的图片
加载优势在http2之后不复存在,http2采用多路复用,多张图片也可以重复使用一个连接
cookie,localstorage,sessionstorage,IndexedDB
(追问)这些方式的区别是什么,分别介绍一下:
cookie:HTML5标准前本地存储主要方式,请求头带着数据,缺点是大小只有4k左右,并且自动请求头加入cookie浪费流量,每个domain限制20个cookie,使用起来也麻烦需要自行封装
localstorage:HTML5标准后加入,以键值对(key-value)方式存储,永久存储,永不失效,除非手动删除,IE8+支持,每个域名限制5M
sessionstorage:和localstorage操作方法一样,区别在于sessionstorage在关闭页面后即被清空,刷新不会清空,但是不能在所有同源窗口中共享,是会话级别的存储方式
IndexedDB:被纳入HTML5标准的数据库存储方案,它是一个NoSql数据库,用键值对进行存储,可以快速读取操作,适合web场景和JavaScript操作
5)在内存中多次操作节点,完成后在添加到文档中
overflow不为visible
双飞翼布局(和双飞翼布局类似,只是处理中间栏被遮挡的方式不同:双飞翼布局在中间栏内部加一个div,设置其margin来避开遮挡)
利用flex布局 👇 Q14
justify-content项目在主轴上的对齐方式
align-items在交叉轴上如何对齐
align-content多跟轴线的对齐方式
flex-basis项目占据主轴空间大小
flex是上面放大,缩小,大小三者的简写,默认为0 1 auto
align-self允许单个项目有不同于其他的对齐方式
.box{
transform:translateZ(0);
//或者
transfor:translate3d(0,0,0);
}.box{
backface-visibility: hidden;
perspective: 1000;
-webkit-backface-visibility: hidden;
-webkit-perspective: 1000;
}<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no"/>* {
touch-action: none;
}box-sizing : content-box || border-box || inherit;static(默认值)
absolute(绝对定位,相对于最近已定位的父元素,如果没有则相对于<html>)
fixed(固定定位,相对于窗口)
relative(相对定位,相对于自身)
sticky(2017年浏览器开始支持,粘性定位).parent {
display: flex;
justify-content: center;
align-items: center;
width: 600px;
height: 600px;
margin: auto;
border: 1px solid yellow;
}
.child {
width: 100px;
height: 100px;
border: 1px solid blue;
}.parent {
position: relative;
width: 600px;
height: 600px;
margin: auto;
border: 1px solid yellow;
}
.child {
position: absolute;
width: 100px;
height: 100px;
border: 1px solid blue;
}
.method3 {
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}.parent {
position: relative;
width: 600px;
height: 600px;
margin: auto;
border: 1px solid red;
}
.child {
position: absolute;
margin: auto;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100px;
height: 100px;
border: 1px solid blue;
}.parent {
position: relative;
width: 600px;
height: 600px;
margin: auto;
border: 1px solid red;
}
.child {
position: absolute;
top: 50%;
left: 50%;
margin: -50px 0 0 -50px;
width: 100px;
height: 100px;
border: 1px solid blue;
}<!--flex实现经典的三栏布局-->
<style>
.flex-container{
display: flex;
height: 200px;
}
.middle {
height: 200px;
background-color: yellowgreen;
flex-grow: 1;
}
.left,.right {
height: 200px;
flex: 0 1 200px;
background-color: green;
}
</style>
<div class="flex-container">
<div class="left">左侧内容</div>
<div class="middle">中间内容</div>
<div class="right">右侧内容</div>
</div>.clearfix:after{
content:"";
display:block;
height:0;
clear:both;
visibility:hidden;
}
.clearfix{
*zoom:1; /*ie6清除浮动的方式 *只有IE6-IE7执行*/
}.clearfix:after,.clearfix:before{
content:'';
display:table;
}
.clearfix:after{
clear:both;
}
.clear{
*zoom:1;
}HTTP/浏览器篇(第一个版本预计总结常见问题15个左右)
简洁来说过程如下:
1.浏览器向DNS服务器请求解析该 URL 中的域名所对应的 IP 地址;
2.建立TCP连接(三次握手);
3.浏览器发出读取文件(URL 中域名后面部分对应的文件)的HTTP 请求,该请求报文作为 TCP 三次握手的第三个报文的数据发送给服务器;
4.服务器对浏览器请求作出响应,并把对应的 html 文本发送给浏览器;
5.浏览器将该 html 文本并显示内容;
6.释放 TCP连接(四次挥手);
浏览器解析渲染过程,见
解析顺序:按照浏览器缓存,系统缓存,路由器缓存,ISP(运营商)DNS缓存,根域名服务器,顶级域名服务器,主域名服务器逐步读取,直到拿到IP地址
DNS递归查询和迭代查询:从客户端到本地DNS服务器是属于递归查询,而DNS服务器之间就是的交互查询就是迭代查询
三次握手:(SYN (同步序列编号)ACK(确认字符))
第一次握手:Client将标志位SYN置为1,随机产生一个值seq=J,并将该数据包发送给Server,Client进入SYN_SENT状态,等 待Server确认。
第二次握手:Server收到数据包后由标志位SYN=1知道Client请求建立连接,Server将标志位SYN和ACK都置为1,ack=J+1,随机产生一个值seq=K,并将该数据包发送给Client以确认连接请求,Server进入SYN_RCVD状态
第三次握手:Client收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给Server,Server检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,Client和Server进入ESTABLISHED状态,完成三次握手,随后Client与Server之间可以开始传输数据了。
四次挥手:
第一次挥手:Client发送一个FIN=M(释放一个连接),用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态。
第二次挥手:Server收到FIN后,发送一个ack=M+1给Client,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),Server进入CLOSE_WAIT状态。
第三次挥手:Server发送一个FIN = N,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态。
第四次挥手:Client收到FIN后,Client进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号ack=N+1,Server进入CLOSED状态,完成四次挥手
追问:释放连接的过程是四次挥手,相比建立连接过程为什么要多一次?
因为服务端收到SYN建连报文后,可以把ACK报文和SYN报文放在一起发送。但是当关闭连接的时候,当对方收到FIN报文通知时,仅仅表示对方已经无数据发送给你了,但未必你已经发送完全部数据,这时就不能马上关闭socket,所以释放连接过程SYN包和ACK包要分开发送。
HTTP1.0定义了三种请求方法,GET,POST和HEAD方法 HTTP1.1新增六种请求方法:OPTIONS,PUT,PATCH,DELETE,TRACH和CONNECT
GET和POST区别主要从三方面讲即可:表现形式,传输数据大小,安全性
首先表现形式上:GET请求的数据会附加在URL后面,以?分割多参数用&连接,由于URL采用ASCII编码,所以非ASCII字符会先编码在传输,可缓存;POST请求会把请求的数据放在请求体中,不可缓存
传输数据大小:对于GET请求,HTTP规范没有对URL长度进行限制,但是不同浏览器对URL长度加以限制,所以GET请求时,传输数据会受到URL长度的限制;POST不是URL传值,理论上无限制,但各个服务器一般会对POST传输数据大小进行限制
安全性:相比URL传值,POST利用请求体传值安全性更高
PS:还有一种GET产生一个数据包,POST产生两个数据包的说法可答可不答,并不是所有浏览器如此,铺展开解释来说就是:对GET请求,浏览器会把请求头和data一起发送,服务器响应200(返回数据),对于POST请求,浏览器会先发送header,服务器响应100后继续,浏览器再发送data,服务器响应200
通常过程如下:
判断是否过期(服务器会通知浏览器一个缓存时间,相关头部信息在Cache-Control和Expires中),如果时间未过期,则直接从缓存中取,即强缓存;
Cache-Control
其中max-age = <seconds>设置缓存存储的最大周期,超过这个时间缓存将会被认为过期,与Expires相反,时间是相对于请求的时间
注:cache-control和expires谁的优先级更高也是校招面试中常问的问题(腾讯一面被问过)
那么如果判断缓存时间已经过期,将会采用协商缓存策略
Last-Modified响应头和If-Modified-Since请求头
Last-Modified 表示资源最后的修改时间,在浏览器第一次发送HTTP请求时,服务器会返回该响应头
HTTP报文结构由报文首部,空行,报文主体三部分组成
而HTTP报文首部则是由首部字段名和字段值键值对形式构成,如Content-Type:text/html
HTTP首部字段通常分为4种类型:通用首部,请求首部,响应首部,实体首部
HTTP请求报文,一个HTTP请求报文由请求行(request line)、请求头部(request header)、空行和请求数据4个部分构成。请求行数据格式由三个部分组成:请求方法、URI、HTTP协议版本,他们之间用空格分隔
HTTP响应报文,一个HTTP响应报文由状态行(HTTP版本、状态码)、响应头部、空行和响应体4个部分构成。状态行主要给出响应HTTP协议的版本号、响应返回状态码、响应描述,同样是单行显示
状态码告知从服务器返回请求的状态,一般由以1~5开头的三位整数组成,
1xx:请求正在处理
2xx:成功
3xx:重定向
301 moved permanently,永久性重定向,表示资源已被分配了新的 URL
302 found,临时性重定向,表示资源临时被分配了新的 URL
303 see other,表示资源存在着另⼀个 URL,应使⽤ GET ⽅法定向获取资源
304 not modified,表示服务器允许访问资源,但因发生请求未满足条件的情况
4xx:客户端错误
400 bad request,请求报文存在语法错误
401 unauthorized,表示发送的请求需要有通过HTTP认证的认证信息
403 forbidden,表示对请求资源的访问被服务器拒绝
404 not found,表示服务器上没找到请求的资源
5xx:服务器错误
500 internal server error,表示服务器端在执行请求时发生了错误
501 Not Implemented,请求超出服务器能力范围
503 service unavailable,表示服务器暂时处于超负载或正在停机维护,无法处理
505 http version not supported,服务器不支持,或者拒绝支持在请求中的使用的HTTP版本
同样是重定向,302是HTTP1.0的状态码,在HTTP1.1版本的时候为了细化302又分出来了303和307,303则明确表示客户端应采用get方法获取资源,他会把post请求变成get请求进行重定向;307则会遵照浏览器标准,不会改变post
HTTP持久连接也称作HTTP keep-alive,使用同一个TCP连接发送和接收多个HTTP请求,是HTTP1.1的新特性,HTTP1.1默认所有连接都是持久连接。在HTTP1.0,使用非持久连接,每个TCP连接只用于传输一个请求和响应,没有官方的keepalive操作,如果浏览器支持通常在请求和响应头中加上Connection: Keep-Alive
那么由于同时打开的TCP连接减少,可以减少内存和CPU的占用;其次之后也无需再次握手,也减少了后续请求的延迟
目前通用标准是HTTP1.1,在1.0的基础上升级加了部分功能,主要从连接方式,缓存,带宽优化(断点传输),host头域,错误提示等方面做了改进和优化
连接方式:HTTP1.0使用短连接(非持久连接);HTTP1.1默认采用带流水线的长连接(持久连接),PS:在持久连接的基础上可选的是管道化的连接
缓存:HTTP1.1新增例如ETag,If-None-Match,Cache-Control等更多的缓存控制策略
带宽优化:HTTP1.0在断连后不得不下载完整的包,存在一些带宽浪费的现象;HTTP1.1则支持断点续传的功能,在请求消息中加入range 头域,允许请求资源的某个部分,在响应消息中Content-Range头域中声明了返回这部分对象的偏移值和长度
host头域:在HTTP1.0中每台服务器都绑定一个唯一的ip地址,所有传递消息中的URL并没有传递主机名;HTTP1.1请求和响应消息都应支持host头域,且请求消息中没有host头域名会抛出一个错误(400 Bad Request)
HTTP2相比HTTP1.x有4大特点,二进制分帧,头部压缩,服务端推送和多路复用
①二进制分帧:HTTP2使用二进制格式传输数据,而HTTP1.x使用文本格式,二进制协议解析更高效
②头部压缩:HTTP1.x每次请求和发送都携带不常改变的,冗杂的头部数据,给网络带来额外负担;而HTTP2在客户端和服务器使用"部首表"来追踪和存储之前发送的键值对,对于相同的数据,不再每次通过每次请求和响应发送
可以简单理解为只发送差异数据,而不是发送全部头部,从而减少头部信息量)
③服务端推送:服务端可以在发送页面HTML时主动推送其他资源,而不用等到浏览器解析到相应位置时,发起请求再响应
④多路复用:在HTTP1.x中如果想并发多个请求,需要多个TCP连接,并且浏览器为了控制资源,一般对单个域名有6-8个TCP连接数量的限制;而在HTTP2中
同个域名所有通信都在单个连接下进行
单个连接可以承载任意数量的双向数据流
数据流以消息的形式发送,而消息又由一个或多个帧组成,多个帧之间可以乱序发送
https相比之下是安全版的http,其实就是HTTP over TSL,因为http都是使用明文传输的,对于敏感信息的传输就很不安全,https正是为了解决http存在的安全隐患而出现的
https的实现也是一个较复杂的过程,将对称加密的密钥使⽤⾮对称加密的公钥进⾏加密,在保证了通信效率的同时防止窃听,同时结合CA证书以及数字签名来最大程度保证安全性:
对称加密:通信双方都使用用一个密钥进行加密解密,比如特工接头的暗号,就属于对称加密;这种方式虽然简单性能好,但是无法解决首次把密钥发给对方的问题,容易被黑客拦截密钥
非对称加密:由私钥+公钥组成的密钥对
即用私钥加密的数据,只要公钥才能解密;用公钥加密的数据,只有私钥才能解密
通信双方都有自己的一套密钥对,通信前将自己的公钥发给对方
非对称加密虽然安全性更高,但是带来的问题就是速度较慢,影响性能
结合两种方式,将对称加密的密钥使用非对称加密的公钥进行加密,然后再发送出去,然后接收方使用自己的私钥进行解密得到对称加密的密钥
但是又带来一个问题,即中间人问题:
如果此时在客户端和服务端之间存在一个中间人,这个中间人只需要把原本双方通信互发的公钥,换成自己的公钥,这样中间人就可以轻松解密通信双方所发送的数据
这时候就需要一个安全的第三方的颁布证书,来证明身份,防止中间攻击:所以就有了CA证书,仔细观察浏览器地址栏,会有一个"小锁"的标志,点开里面有证明身份的CA证书信息
但是仍然存在的一个问题是,如果中间人篡改了CA证书,那么这个证书不就无效了?所以就添加了新的保护方案:数字签名
数字签名就是用证书自带的HASH算法对证书内容进行一个HASH得到一个摘要,再用CA的私钥进行加密,最终组成数字签名
当别人把他的证书发过来时,用同样的HASH算法得到信息摘要,然后再用CA的公钥对数字签名进行解密,得到CA创建的消息摘要,两者一对比就知道中间有没有被篡改了
通过这样的方式,https最大化保证了通信的安全性
Websocket和HTTP都是基于TCP协议两个不同的协议
首先Websocket是一种在单个TCP连接上进行全双工通信的协议,允许服务端主动向客户端推送数据(只需要一次握手,两者之间创建持久化连接,可进行双向数据传输);而HTTP则需要定时轮询向服务器请求,然后服务器再向客户端发送数据
其次,HTTP是一个无状态协议,缺少状态意味着后续处理需要先前数据必须重传;而Websocket需要先创建连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息
应用场景:如果应用是多个用户相互交流,或者展示服务端经常变动的数据都可以考虑websocket;所以体育实况,多媒体聊天等这些都是Websocket不错的应用场景
TCP是面向连接的;UDP则是无连接的,即发送数据之前不需要建立连接
TCP提供的可靠的服务,即TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;而UDP尽最大努力,不保证可靠交付
TCP面向字节流,TCP把数据看成一连串无结构的字节流;UDP是面向报文的
每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信
前端性能优化七大常用的优化手段:减少请求数量,减小资源大小,优化网络连接,优化资源加载,减少回流重绘,使用更好性能的API和构建优化
减少请求数量
文件合并,并按需分配(公共库合并,其他页面组件按需分配)
图片处理:使用雪碧图,将图片转码base64内嵌到HTML中,使用字体图片代替图片
减少重定向:尽量避免使用重定向,当页面发生了重定向,就会延迟整个HTML文档的传输
参考:

public 表示响应可以被任何对象缓存,即使是通常不可缓存的内容
private 表示缓存只能被单个用户缓存,不能作为共享缓存(即代理服务器不可缓存它)
no-cache 告诉浏览器、缓存服务器,不管本地副本是否过期,使用副本前一定要到源服务器进行副本有效校验
no-store 缓存不应该存储有关客户端请求或服务器响应的任何内容
Expires
expires 字段规定了缓存的资源的过期时间,该字段时间格式使用GMT时间标准时间格式, js通过new Date().toUTCString()得到,由于时间期限是由服务器生成,存在着服务端和客户端的时间误差,相比cache-control优先级较低
两种情况:如果资源最后修改时间大于If-Modified-Since,说明资源有被改动过,则响应完整的资源内容,返回状态码200;如果小于或者等于,说明资源未被修改,则响应状态码304,告知浏览器可以继续使用所保存的缓存
Etag响应头和If-None-Match请求头
Etag值为当前资源在服务器的唯一标识
类比上面Last-Modified响应头和If-Modified-Since请求头,请求头中If-None-Match将会和资源的唯一标标识进行对比,如果不同,则说明资源被修改过,响应200;如果相同,则说明资源未改动,响应304
307 temporary redirect,临时重定向,和302含义相同
408 Request timeout,客户端请求超市
409 confict,请求的资源可能引起冲突
错误提示:HTTP1.1新增24个状态响应码,比如409(请求的资源与资源当前状态冲突),410(服务器上某个资源被永久性删除);相比HTTP1.0只定义了16个状态响应码
然后拿着这个公钥来加密数据响应给对方,等对方收到之后,再用自己的私钥进行解密
快
TCP首部开销较大,20字节;UDP只有8字节
TCP的逻辑通信信道是全双工的可靠信道;而UDP则是不可靠信道
使用缓存:即利用浏览器的强缓存和协商缓存
不使用CSS @import:CSS的@import会造成额外的请求
避免使用空的src和href:a标签设置空的href,会重定向到当前的页面地址,form设置空的method,会提交表单到当前的页面地址
减小资源大小
压缩:静态资源删除无效冗余代码并压缩
webp:更小体积
开启gzip:HTTP协议上的GZIP编码是一种用来改进WEB应用程序性能的技术
优化网络连接
使用CDN
使用DNS预解析:DNS Prefetch,即DNS预解析就是根据浏览器定义的规则,提前解析之后可能会用到的域名,使解析结果缓存到系统缓存中,缩短DNS解析时间,来提高网站的访问速度
并行连接:由于在HTTP1.1协议下,chrome每个域名最大并发数是6个。使用多个域名,可以增加并发数
管道化连接:在HTTP2协议中,可以开启管道化连接,即单条连接的多路复用
优化资源加载
减少重绘回流
使用性能更好的API
构建优化:如webpack优化
报文首部
空行
报文主体
TCP
UDP
是否连接
面向连接
面向非连接
传输可靠性
可靠
不可靠
应用场景
少量数据
传输大量数据
速度
慢
// 方法是在 head 标签里面写上几个 link 标签
<link rel="dns-prefecth" href="https://www.google.com">
<link rel="dns-prefecth" href="https://www.google-analytics.com">GET /index.html HTTP/1.1HTTP/1.1 200 OK
框架对于应届生也是较重点,除了基础的知识和部分深度问题,还要看对于技术的广度,是否有一个自己完整的技术栈,以及对于自身技术栈的认知程度
MVC的弊端:MVC即"Model-View-Controller",当视图上发生变化,通过Controller(控件)将响应传入到Model(数据源),由数据源改变View上面的数据,允许view和model直接通信,随着业务不断庞大,会出现向蜘蛛网一样难以处理的依赖关系,违背了开发应该遵循的"开放封闭原则"
MVVM,萌芽于2005年微软推出的基于 Windows 的⽤户界⾯框架WPF ,前端最早的 MVVM 框架 knockout 在2010年发布
即"Model-View-ViewModel",View通过View-Model的DOM Listeners将事件绑定到Model上,而Model则通过Data Bindings来管理View中的数据,View-Model从中起到一个连接桥的作用
vue内部利用Object.defineProperty监听数据变化,使数据具有可观测性,结合发布订阅模式,在数据发生变化时更新视图
利用Proxy或Object.defineProperty生成的Observer针对对象/对象的属性进行"劫持",在属性发生变化后通知订阅者
解析器Compile解析模板中的Directive(指令),收集指令所依赖的方法和数据,等待数据变化然后进行渲染
Watcher属于Observer和Compile桥梁,它将接收到的Observer产生的数据变化,并根据Compile提供的指令进行视图渲染,使得数据变化促使视图变化
Object.defineProperty兼容性较好,但不能直接监听数组的变化,只能监听对象的属性(有时需要深层遍历)
与之相比proxy的优点:
可以直接监听数组的变化
可以直接监听对象而非属性
proxy有多达13种的拦截方法,不限于apply、ownKeys、deleteProperty、has等等
proxy受到各大浏览器厂商的重视
现代前端框架主要有两种监听数据的方式:一种是pull的方式,一种是push的方式
pull,其代表为react,react和vue基于双向数据绑定的依赖收集的订阅式机制不同,react是通过显式的触发函数调用来更新视图,比如setState,然后React会一层层的进行Virtual Dom Diff操作找出差异,通过较暴力diff的方式查找哪里发生变化。另一个代表是angular的脏值检测
push,其代表为vue,当Vue程序初始化的时候就会对数据data进行依赖的收集,一但数据发生变化,响应式系统就会立刻得知;我们知道绑定一个数据通常就需要一个watcher,那么一旦细粒度过高会产生大量的watcher,会给增加内存以及依赖追踪的开销,而细粒度过低会无法精准检测变化,因此vue选择中细粒度方案,在组件级进行push检测的方式(即依赖响应式系统),在组件内部进行Virtual Dom Diff获取更加具体的差异,所以vue采用了push+pull结合的方式
响应式系统简述:
任何⼀个 Vue Component 都有⼀个与之对应的 Watcher 实例
Vue 的 data 上的属性会被添加 getter 和 setter 属性
当Vue Component render函数被执⾏的时候,data上会被触碰(touch), 即被读,getter ⽅法会被调⽤, 此时 Vue 会去记录此 Vue component所依赖的所有data(这⼀过程被称为依赖收集)
data 被改动时(主要是⽤户操作), setter ⽅法会被调⽤, 此时 Vue 会去通知所有依赖于此 data 的组件去调⽤他们的 render 函数进⾏更新
vue 实例有⼀个完整的⽣命周期,也就是从开始创建、初始化数据、编译模版、挂载Dom -> 渲染、更新 -> 渲染、卸载等⼀系列过程,即vue实例从创建到销毁的过程,我们称这是vue的⽣命周期
1.beforeCreate:完成实例初始化,初始化非响应式变量
2.created:实例初始化完成(未挂载DOM)
3.berofeMount:找到对应的template,并编译成render函数
4.mounted:完成创建vm.$el和双向绑定,完成DOM挂载
5.beforeUpdate:数据更新之前(可在更新前访问现有的DOM)
6.updated:完成虚拟DOM的重新渲染和打补丁
7.activated:子组件需要在每次加载时候进行某些操作,可以使用activated钩子触发
8.deactivated:keep-alive 组件被移除时使用
9.beforeDestroy:可做一些删除提示,销毁定时器,解绑全局时间 销毁插件对象
10.destroyed:当前组件已被销毁
当我们要进⾏数值计算,⽽且依赖于其他数据,我们需要使用computed
如果你需要在某个数据变化时做⼀些事情,使⽤watch来观察这个数据
computed:
1.是计算值,
2.应用:就是简化tempalte里面计算和处理props或$emit的传值
3.具有缓存性,页面重新渲染值不变化,计算属性会立即返回之前的计算结果,而不必再次执行函数
watch:
1.是观察的动作,
2.应用:监听props,$emit或本组件的值执行异步操作
3.无缓存性,页面重新渲染时值不变化也会执行
v-html,v-text,v-show,v-for,v-if v-else-if v-else,
v-bind(用来动态的绑定一个或者多个特性)
v-model(创建双向数据绑定)
v-cloak(保持在元素上直到关联实例结束时进行编译)
v-pre(用来跳过这个元素和它的子元素编译过程)
v-if和v-for能不能一起使用(或者问v-for和v-if谁的优先级更高):
v-for指令的优先级要高于v-if,当处于同一节点时候,意味着v-if将分别重复运行于每个 v-for 循环中,所以应该尽量避免v-for和v-if在同一结点
vue采用“就地复用”策略,如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序, 而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素,key 的作用主要是为了高效的更新虚拟DOM。
虚拟dom的diff算法的过程中,先会进⾏新旧节点的⾸尾交叉对⽐,当⽆法匹配的时候会⽤新节点的 key 与旧节点进⾏⽐对,然后超出差异
vue的diff位于patch.js中,这里简单总结一下patchVnode比较的的过程,首先要判断vnode和oldVnode是否都存在,都存在并且vnode和oldVnode是同一节点时,才会进入patchVnode进行比较,结点比较五种情况:
① 引用一致,可以认为没有变化
② 文本节点的比较,如果需要修改:则会调用Node.textContent = vnode.text
③ 两个节点都有子节点,而且它们不一样:则调用updateChildren函数比较子节点
④ 只有新的节点有子节点:则调用addVnodes创建子节点
⑤ 只有老节点有子节点,则调用removeVnodes把这些子节点都删除
updateChildren的过程:updateChildren用指针的方式把新旧节点的子节点的首尾节点标记,即oldStartIndex(1),oldEndIndex(2),newStartIndex(3), oldEndIndex(4)(这里简单用1 2 3 4顺序标记)即依次比较13,14,23,24,有10种左右情况分别做出对应的处理
更新视图但不重新请求页面,是前端路由原理的核心,目前在浏览器环境主要有两种方式:
Hash 模式
hash("#")符号的本来作用是加在URL指示网页中的位置:
#本身以及它后面的字符称之为hash,可通过window.location.hash属性读取,hash虽然在url中,但是却不会被包含在http请求中,也不会重新加载页面,它用来指导浏览器动作
History 模式
History interface是浏览器历史记录栈提供的接口,从HTML5开始,History interface提供了2个新的方法:pushState(),replaceState()使得我们可以对浏览器历史记录栈进行修改;这两个方法有有一个特点,当调用他们修改浏览器历史栈后,虽然当前url改变了,但浏览器不会立即发送请求该url,这就为单页应用前端路由,更新视图但不重新请求页面提供了基础
props/$emit+v-on
父组件通过props的方式向子组件传递数据,而通过$emit 子组件可以向父组件通信
eventBus
通过eventBus向中心事件发送或者接收事件,所有事件都可以共用事件中心
vuex
状态管理模式,采用集中式存储管理应用的所有组件的状态,可以通过vuex管理全局的数据
参考: 只需要熟悉上面三种即可,其他基本不会用到,可以做为了解
懒加载简单来说就是延迟加载或按需加载,即在需要的时候的时候进行加载,常用的懒加载方式有三种:即使用vue异步组件 和 ES6中的import,以及webpack的require.ensure()
vue异步组件
ES6中的import
webpack推出的require.ensure()
// 简单的双向数据绑定
const data = {};
Object.keys(data).forEach(function(key) {
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function() {
console.log('get');
},
set: function(newVal) {
// 当属性值发⽣变化时我们可以进⾏额外操作
},
});
});http://www.example.com/index.html#print// 路由配置,使用vue异步组件
{
path: '/home',
name: 'home',
component: resolve => require(['@/components/home'],resolve)
}// 指定了相同的webpackChunkName,合并打包成一个js文件
// 如果不指定,则分开打包
const Home = () => import(/*webpackChunkName:'ImportFuncDemo'*/ '@/components/home')
const Index = () => import(/*webpackChunkName:'ImportFuncDemo'*/ '@/components/index'){
path: '/home',
name: 'home',
component: r => require.ensure([], () => r(require('@/components/home')), 'demo')
}

算法在前端面试中也有不可忽视的重量,而且不少企业也越来越重视,这一篇只是一些普识性问题,具体到手撕某个问题可能要靠平时空闲多在LeetCode刷题;除了算法问题,可能还会有几个逻辑推理题,具体某个场景根据自己的理解和思路去答即可
当问题规模数据大量增加时,重复执行的次数也必定会增加,那么我们就有必要关心执行次数是以什么样的数量级增加,这也是分析时间复杂度的意义,是一个非常重要衡量算法好快的事前估算的方法
常见的时间复杂度:
O(1):常数阶的复杂度,这种复杂度无论数据规模如何增长,计算时间是不变的
O(n):线性复杂度,线性增长
O(logn):对数复杂度,随着问题规模的增长,计算时间会对数级增长,典型的例子是归并查找
O(nlogn):线性对数复杂度,计算时间随数据规模呈线性对数级增长,典型的例子是归并排序
O(n^2):平方级复杂度,典型就是双层循环的时候,代表应用是冒泡排序算法
常见的排序算法这里总结四种最具代表性的:
冒泡排序
冒泡排序是一种简单的排序算法,它需要重复的走访序列,一次只比较两个数据,如果顺序错误则交换这两个数据,直到没有在需要交换的数据,走访结束,具体算法描述如下:
比较相邻元素,如果第一个比第二个大,就交换他们两个
依次走访执行第一步,那么第一趟后,最后的元素应该是最大的数
重复走访,直到排序完成
冒泡排序改进方案:
方案一:设置一个标记变量pos,用来记录每次走访的最后一次进行交换的位置,那么下次走访之后的序列便可以不再访问
方案二:传统冒泡一趟只能找到一个最大或者最小值,我们可以考虑在利用每趟排序过程中进行正向和反向冒泡,一次可以得到一个最大值和最小值,从而使排序趟数几乎减少一半
以上提供两种改进冒泡的思路,耗时在这只是进行粗略对比,并不完全确定好坏,相比之下改进后的冒泡时间复杂度更低,下图实例展示结果耗时更短
快速排序
快速排序是分治策略的经典实现,也是对冒泡排序的改进,出现频率较高,基本思路是经过一趟排序,把数据分割成独立的两部分,其中一部分数据要比另一部分都小,然后按此方法继续排序,以达到整个序列有序,具体算法描述如下:
从数组中挑出一个元素作为"基准"
分区:所有比基准小的值放前面,而比基准大的值放后面,基准处于数列中间位置
按照此方法依次排序(递归),以达到序列有序
希尔排序
第一个突破O(n^2)的排序算法;是简单插入排序的改进版;与插入排序的不同点在于,它会优先比较距离较远的元素。希尔排序又叫做增量缩小排序,核心在于间隔序列的设定,既可以提前设定好间隔序列,也可以动态的定义间隔序列,后者是《算法(第四版)》中提出的,实现如下:
选择一个增量序列t1,t2...tk,其中ti>tj,tk=1
按增量序列个数k,对序列进行k趟排序
每趟排序根据对应的增量ti,将待排序列分割成长度为m的若干子序列,然后分别对各子表进行直接插入排序。仅当增量因子为1时,整个序列作为一个表来处理,表长度即为整个序列的长度
归并排序
归并排序不受输入数据的影响,时间复杂度始终都是O(nlogn),但代价是需要额外的内存空间。归并排序也是分治法的经典体现,先使子序列有序,再使子序列段间有序,若将两个有序表合并成一个有序表,称为2-路归并。实现如下:
将长度为n的序列,分成两个长度为n/2的子序列
对这两个子序列分别采用归并排序
将两个排序好的子序列合并成最终排序序列
线性查找
线性查找较简单,只需要简单遍历即可
时间复杂度:最佳情况O(n),最差情况O(n),平均情况O(n)
二分查找法
也叫作折半查找,要求查找表的数据实现线性结构存储,还要求查找表中的顺序是有序的
实现思路如下:
首先设两个指针,low表示最低索引,height表示最高索引
然后取中间位置索引,判断middle处的值是否是要查找的数字,是则查找结束;比所求值较小就把low设为middle+1,较大则把height设为middle-1
然后到新分区继续查找,直到找到或者low>height找不到要查找的值结束
时间复杂度分析:最佳情况O(logn),最差情况O(logn),平均情况O(logn)
参考:
二叉树遍历有四种方式:先序遍历,中序遍历,后序遍历,层序遍历
前序遍历:先访问根节点,然后前序遍历左子树,再前序遍历右子树
中序遍历:先中序遍历左子树,然后访问根节点,最后遍历右子树
后序遍历:从左到右,先叶子后结点的方式遍历访问左子树,最后访问根节点
层序遍历:从根结点从上往下逐层遍历,在同一层,按从左到右的顺序对结点逐个访问
有两种通用的遍历树的策略:
深度优先遍历(DFC)
正如名字一样,深度优先遍历采用深度作为优先级,从某个确定的叶子,然后再返回根到另个分支,有细分为先序遍历,中序遍历和后序遍历
广度优先遍历(BFC)
广度优先按照高度顺序一层一层访问整棵树,高层次的结点会比低层的结点先访问到
const increment = n => n++// 最典型的例子就是线性查找
const linearSearch = (arr,target) = {
for (let i = 0;i<arr.length;i++){
if(arr[i] === target) return 1;
}
return -1;
}const bubbleSort = arr => {
console.time('bubbleSort耗时');
let len = arr.length;
for(let i = 0;i<len;i++){
for(let j = 0;j<len-i-1;j++){
if(arr[j]>arr[j+1]){
[arr[j],arr[j+1]] = [arr[j+1],arr[j]]
}
}
}
console.timeEnd('bubbleSort耗时');
return arr
}const bubbleSort_pos = arr => {
console.time('bubbleSort_pos耗时')
let i = arr.length - 1;
while(i > 0){
let pos = 0;
for(var j=0;j<i;j++){
if(arr[j]>arr[j+1]){
pos = j;
[arr[j],arr[j+1]] = [arr[j+1],arr[j]];
}
}
i = pos;
}
console.timeEnd('bubbleSort_pos耗时')
return arr;
}const bubbleSort_ovonic = arr => {
console.time('bubbleSort_ovonic耗时')
let low = 0;
let height = arr.length -1;
let tmp,j;
while(low < height){
for(j=low;j<height;++j){ // 正向冒泡,找到最大值
if(arr[j] > arr[j+1]){
[arr[j],arr[j+1]] = [arr[j+1],arr[j]];
}
}
--height;
for(j=height;j>low;--j){ // 反向冒泡,找到最小值
if(arr[j] < arr[j-1]){
[arr[j-1],arr[j]] = [arr[j],arr[j-1]]
}
}
++low;
}
console.timeEnd('bubbleSort_ovonic耗时')
return arr;
}// 递归方法的其中一种形式
const quickSort = (arr) => {
if(arr.length <= 1){ return arr };
let pivotIndex = Math.floor(arr.length/2);
let pivot = arr.splice(pivotIndex,1)[0]; // 确定基准
let left = [] , right = [];
for(let i = 0;i<arr.length;i++){
if(arr[i]<pivot){
left.push(arr[i]);
}else{
right.push(arr[i]);
}
}
return quickSort(left).concat([pivot],quickSort(right));
}const shellSort = arr => {
console.time('shellSort耗时')
let len = arr.length,
gap = 1,
temp;
while(gap < len/5){ gap = gap*5+1 } // 动态定义间隔序列
for(gap; gap > 0; gap = Math.floor(gap/5)){
for(let i = gap;i<len;i++){
temp = arr[i];
for(var j=i-gap; j>=0&&arr[j]>temp; j-=gap){
arr[j+gap] = arr[j];
}
arr[j+gap] = temp;
}
}
console.timeEnd('shellSort耗时');
return arr;
}const merge = (left,right) => {
let result = [];
while(left.length && right.length){
if(left[0] <= right[0]){
result.push(left.shift());
}else{
result.push(right.shift());
}
}
while(left.length)
result.push(left.shift());
while(right.length)
result.push(right.shift());
return result;
}
const mergeSort = arr => {
let len = arr.length;
if(len < 2) return arr;
let middle = Math.floor(len / 2),
left = arr.slice(0,middle),
right = arr.slice(middle);
return merge(mergeSort(left),mergeSort(right));
}const linearSearch = (arr,target) => {
for(let i =0;i<arr.length;i++){
if(arr[i] === target) return i
}
return -1
}const binarySearch = (arr,target) => {
let height = arr.length - 1;
let low = 0;
while(low <= height){
let middle = Math.floor((low+height)/2)
if(target < arr[middle]){
height = middle - 1
}else if(target > arr[middle]){
low = middle + 1
}else{
return middle
}
}
return -1
}// 通过迭代方式实现
const levelOrder = function(root) {
const res = [];
const stack = [{ index: 0, node: root }];
while (stack.length > 0) {
const { index, node } = stack.pop();
if (!node) continue;
res[index] = res[index] ? [...res[index], node.val] : [node.val];
stack.push({ index: index + 1, node: node.right });
stack.push({ index: index + 1, node: node.left });
}
return res;
};








JavaScript篇(第一个版本预计总结常见问题25个左右),JavaScript是应届求职过程的重中之重,既能体现对于基础的掌握,也会了解到应届生对于一门编程语言的熟练程度,以及是否能够揭开编程语言表层,对底层有一定的了解。
之所以需要跨域,是因为浏览器同源策略的约束,面对不同源的请求,我们无法完成,这时候就需要用到跨域。同源策略拦截的是跨源请求,原因:CORS缺少Access-Control-Allow-Origin头
跨域的方式主要有:JSONP、proxy代理、CORS、XDR
JSONP主要是因为script标签的src属性不被同源策略所约束,同时在没有阻塞的情况下资源加载到页面后会立即执行
缺点:
JSONP只支持get请求而不支持post请求,如果想传给后台一个json格式的数据,此时问题就来了,浏览器会报一个http状态码415错误,请求格式不正确
JSONP本质是一种代码注入,存在安全问题
实际项目中JSONP通常用来获取json格式的数据,这时候前后端通常约定一个参数callback,该参数的值,就是处理返回数据的函数名称
补充:cors是一种现代浏览器支持跨域资源请求的一种方式,它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制
简单列表滚动加载是监听滚动条在满足条件的时候触发回调,然后通过把新的元素加入到页面页尾的方法完成,但是如果用户加载过多列表数据(比如我这一个列表页有一万条数据需要展示),那么用户不断加载,页面不断增加新的元素,很容易就导致页面元素过多而造成卡顿,所以就提出的列表的无限滚动加载,主要是在删除原有元素并且维持高度的基础上,生成并加载新的数据
节流:在一段时间内不管触发了多少次都只认为触发了一次,等计时结束进行响应(假设设置的时间为2000ms,再触发了事件的2000ms之内,你在多少触发该事件,都不会有任何作用,它只为第一个事件等待2000ms。时间一到,它就会执行了。 )
防抖:在某段时间内,不管你触发了多少次事件,都只认最后一次(假设设置时间为2000ms,如果触发事件的2000ms之内,你再次触发该事件,就会给新的事件添加新的计时器,之前的事件统统作废。只执行最后一次触发的事件。)
let,const声明变量
解构赋值
箭头函数
扩展运算符
首先var相对let/const,后者可以使用块级作用域,var声明变量则存在函数作用域内(该域内存在变量提升)。let/const有一个暂时性死区的概念,即进入作用域创建变量到变量可以开始访问的一段时间。
其次let,const主要的区别在于:const一旦被赋值就不再"改变"
但需要理解的是:这并不意味着声明的变量本身不可变,只是说它不可再次被赋值了(const定义引用类型时,只要它的引用地址不发生改变,仍然可以改变它的属性)
JavaScript引擎会先进行预解析,获取所有变量的声明复制undefined,然后逐行执行,这导致所有被声明的变量,都会被提升到代码的头部(被提升的只有变量,值不会被提升),也就是变量提升(hoisting)
预解析阶段会先获的变量a赋值undefined,并将var a = undefined放在最顶端,此时a = 1还在原来的位置,实际就是:
然后会是执行阶段,逐行执行造成了打印出a是undefined
基本数据类型:string、number、Boolean、undefined、null
复杂数据类型:Object
ES6新增数据类型:symbol、Map、Set
其中symbol是基本数据类型,每一个symbol都是一个全局唯一的字符串
set是object里面的一种,set里无论原始值还是引用类型的值,重复的都只会保留一个
Map可以允许任何类型作为对象的键,弥补了object只能使用字符串作为键(注意Map是ES6中的一种数据结构,区别于数组方法map)
值类型: 字符串string,数值number,布尔值boolean,null, undefined
引用类型: 对象 Object,数组Array,函数Function
值类型:
占用空间固定,保存在栈中:当一个方法执行时,每个方法都会建立自己的内存栈,也就是所谓的函数作用域,基础变量的值是存储在栈中的,而引用类型变量存储在栈中的是指向堆中的数组或者对象的"地址"
保存与复制的是值本身
可以用typeof检测值类型
引用类型:
占用空间不固定,保存在堆中:由于对象的创建成本比较大,在程序中创建一个对象时,这个对象将被保存到运行时数据区中,以便反复利用,这个运行时数据区就是堆内存
保存与复制的是指向对象的一个指针
使用instanceof()检测数据类型
数组也是一种数据类型,类比数据类型的学习我们可以从其特性,增删改查,其他方法,支持的运算符七个方面来学习,可以明显提高效率
数组的方法我们除了作用以外,我们还比较关心的就是该数组方法是否改变原数组,下面就按照这个规则来分类:
map方法的调用者一般是数组,参数是一个callback函数,返回值是一个由原数组中每个元素执行callback函数得到的返回值组成的新数组
举个栗子:
reduce方法调用者也一般为数组,参数是callback和一个可选的initialValue,为数组中每个元素执行callback函数,返回一个具体的结果,如果给定initialValue可以作为第一次调用callback的第一个参数,可以控制返回值的格式
举个栗子:
数组方法map模拟实现:
数组方法reduce模拟实现:方法类似核心是数组的遍历,因为reduce有第二个可选参数initialValue做起始值,所以要判断是否有可选参数作为遍历的起始值,并将得到的参数传入回调函数中执行
首先需要知道的是Ajax主要是通过XMLHttpRequest对象向服务器提出请求并处理响应,进行页面的局部更新,XMLHttpRequest对象常用的三大属性:onreadystatechange,readyState,status
XMLHttpRequest历史悠久,因为其API设计其实并不是很好,输入,输出,状态都在同一个接口管理,容易写出非常非常混乱的代码,Fetch API采取了一种新规范,用来取代XMLHttpReques,Fetch更现代化,更接近于未来,内部使用了Promise,用起来也更加简洁
Promise是一种异步编程的解决方法,相比容易陷入回调地狱的回调函数,采用链式调用的方式更合理也更方便,Promise有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败),接受一个作为函数作为参数,该函数有两个参数,分别是resolve和reject两个函数
Promise.all和Promise.race在实际应用中的比较,比如从接口中获取数据,等待所有数据到达后执行某些操作可以用前者,如果从几个接口中获取相同的数据哪个接口先到就用哪个可以使用后者
主要是因为JavaScript同样采用IEEE754标准,在64位中存储一个数字的有效数字形式
第0位表示符号位,0表示整数1表示负数,第1~11位存储指数部分,第12~63位存小数部分;
由于二进制的有效数字总是表示为1.xxx...这样的形式,尾数部分在规约形式下的第一位默认为1,故存储时第一位省略不写,尾数部分存储有效数字小数点后的xxx...,最长52位,因此,JavaScript提供的有效数字最长为53个二进制位(尾数部分52位+被省略的1位)
由于需要对求和结果规格化(用有效数字表示),右规导致低位丢失,此时需对丢失的低位进行舍入操作,遵循IEEE754舍入规则,会有精度损失
this有四种绑定规则,默认绑定、隐式绑定、显示绑定、new 绑定,优先级由低到高
在ECMA内,this 会调用原生方法
ResolveThisBinding()原生方法,该方法使用正在运行的执行上下文的LexicalEnvironment确定关键字this的绑定可以简单总结为:谁直接调用产生这个this指针的函数,this就指向谁
this在一般模式下指向全局对象;严格模式下 this 默认为undefined
箭头函数没有自己的this指针,它的this绑定取决于外层(函数或全局)作用域)
call,apply,bind在非箭头函数下修改this值(箭头函数下只传递参数),不管call , bind, apply多少次,函数的this永远由第一次的决定
闭包是指有权访问另外一个函数作用域中的变量的函数.可以理解为(能够读取其他函数内部变量的函数)
闭包的作用: 正常函数执行完毕后,里面声明的变量被垃圾回收处理掉,但是闭包可以让作用域里的变量,在函数执行完之后依旧保持没有被垃圾回收处理掉
具体有以下几个应用场景:
首先,JavaScript一大特点就是单线程,这样的设计让它在同一时间只做一件事;作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM,避免了复杂性,比如假设JavaScript有两个线程,那么在同一时间进行添加删除节点操作,为浏览器分辨以哪个线程为准带来困难,所以单线程是它作为浏览器脚本语言的优势,也是它的核心特征。
注:虽然为了利用多核CPU的计算能力,HTML5提出了web worker标准,允许JavaScript创建多个线程,但是子线程完全受主线程控制,且不得操作DOM,所以也并没有改变JavaScript单线程的本质
那么,单线程就意味着,所有任务需要排队,前一个任务结束才会执行后一个任务,所以为了提高CPU的利用效率,就把所有任务分成了同步任务(synchronous)和异步任务(asynchronous),同步任务在主线程顺序执行,异步任务则不进入主线程而是进入到任务队列(task queue)中。在主线程上会形成一个执行栈,等执行栈中所有任务执行完毕之后,会去任务队列中查看有哪些事件,此时异步任务结束等待状态,进入执行栈中,开始执行。
主线程从任务队列中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)
异步任务又分为宏任务(macrotask)和微任务(microtask),那么任务队列就有了宏任务队列和微任务队列,微任务总是在宏任务之前执行,也就是说:同步任务>微任务>宏任务,
宏任务包括:
微任务包括:
考察的其实是同一个概念,正因为setTimeout属于宏任务,那么如果当前 执行栈 所花费的时间大于 定时器 时间,那么定时器的回调在 宏任务 里,来不及去调用,所有这个时间会有误差
JavaScript中new一个对象,我们需要了解主要发生了以下四步:
1.创建一个空的对象
let obj = { }
2.设置新对象的constructor属性为构造函数的名称,设置新对象的proto属性指向构造函数的prototype对象
obj.proto = ClassA.prototype
3.使用新对象调用函数,函数中的this被指向新实例对象
ClassA.call(obj); //{}.构造函数()
4.将初始化完毕的新对象地址,保存到等号左边的变量中
JavaScript内存管理有一个主要概念是可达性,“可达性” 值是那些以某种方式可访问或可用的值,它们被保证存储在内存中。有一组基本的固有可达值,由于显而易见的原因无法删除,比如:本地函数的局部变量和参数,全局变量,当前嵌套调用链上的其他函数的变量和参数,这些值被称为"根",如果引用或引用链可以从根访问任何其他值,则认为该值是可访问的
JavaScript在创建变量时自动进行了内存分配,并且在它们不使用时"自动"释放,释放的过程就被称为垃圾回收。
现在各大浏览器通常采用的垃圾回收有两种方法:标记清除、引用计数
引用计数垃圾收集:把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。但是有一个限制是"无法处理循环引用问题"
标记清除:是js中最常用的垃圾回收方式,把“对象是否不再需要”简化定义为“对象是否可以获得”,定期会执行以下的"垃圾回收"步骤(这正是标记清除算法垃圾收集的工作原理):
首先垃圾回收器获取根并“标记”它们
然后它访问并“标记”所有来自它们的引用
柯里化(Currying)是函数式编程的一个很重要的概念,将使用多个参数的一个函数转换成一系列使用一个参数的函数
主要有三个作用:1. 参数复用;2. 提前返回;3. 延迟计算/运行
参数复用
提前返回
延迟执行



数组的新方法 -- map,reduce,filter
promise
基本数据类型是值类型
使用 new() 方法构造出的对象是引用型
✅
requestAnimationFrame
✅
❌
以此类推,直至有未访问的引用,此时进程中不能访问的对象将被认为是不可访问的
除标记的对象外,其余对象将被删除
属性
描述
onreadystatechange
readyState属性的值发生改变,就会触发readystatechange事件
readyState
存有XMLHttpRequest的状态 0:请求未初始化 1:服务器连接已建立 2:请求已接收 3:请求处理中 4:请求已完成,且响应已就绪
status
status属性为只读属性,表示本次请求所得到的HTTP状态码 200, OK,访问正常 301, Moved Permanently,永久移动 302, Move temporarily,暂时移动 304, Not Modified,未修改 307, Temporary Redirect,暂时重定向 401, Unauthorized,未授权 403, Forbidden,禁止访问 404, Not Found,未发现指定网址 500, Internal Server Error,服务器发生错误
#
浏览器
Node
I/O
✅
✅
setTimeout
✅
✅
setInterval
✅
✅
setImmediate
#
浏览器
Node
process.nextTick
❌
✅
MutationObserver
✅
❌
Promise.then catch finally
✅
✅
❌
var f = function(data){
alert(data.name)
}
var _script = document.createElement('script');
_script.type = 'text/javascript'
_script.src = 'http://localhost:8888/jsonp?callback=f'
document.head.appendCild(_script);//node处理
var query = _scr.query;
var params = qs.parse(query);
var f = '';
f = params.callback;
res.writeHead(200,{'Content-Type','text/javascript'})
res.write(f + "({name:'hello world'})")
res.end//php处理(注意输出格式)
$data = array(
'rand' => $_GET['random'],
'msg' => 'Success'
);
echo $_GET['callback'].'('.json_encode($data).')';//node处理
if(req.headers.origin){
res.wirteHead(200,{
'Content-type':'text/html;charset=UTF-8',
'Access-Control-Allow-Origin':'*', // *通配,或者安全考虑替换为指定域名
});
res.write('cors');
res.end();
}//时间戳方式
function throttle(fn,delay){
let pre = Date.now();
return function(){
let context = this;
let args = arguments;
let now = Date.now();
if(now - pre > delay){
fn.apply(context,args);
pre = Date.now();
}
}
}
//定时器方式
function throttle(fn,delay){
let timer = null;
return function(){
let context = this;
let args = arguments;
if(!timer){
timer = setTimeout(function(){
fn.apply(context,args);
timer = null;
},delay)
}
}
}//定时器方式
function debounce(fn,delay){
let timer = null;
return function(){
let context = this;
let args = arguments;
clearTimeout(timer);
timer = setTimeout(function(){
fn.apply(context,args);
},delay)
}
}const name = 'Jack';
name = 'Tom' //TypeError:Assignment to constantconst person = {
name : 'Jack‘
}
person.name = 'Tom'console.log(a) // undefined
var a = 1
function b(){
console.log(a)
}
b() // 1var a = undefined
console.log(a) // undefined
a = 1
function b(){
console.log(a)
}
b() // 1// 在一个块级作用域里使用symbol,创造一个隐藏属性
{
let a = Symbol();
let object = {
name : 'okaychen',
age : 3,
[a] : '隐藏属性'
}
window.object = object
}// 改变原数组的方法:
pop() // 删除数组中的最后一个元素,把数组长度减 1,并且返回它删除的元素的值
push() // 该方法可把它的参数顺序添加到数组的尾部。它直接修改数组,返回后修改数组的长度
reverse() // 将数组元素倒序,改变原数组
unshift() // 可以向数组开头增加一个或多个元素,并返回新的长度
shift() // 数组的第一个元素从其中删除,并返回第一个元素的值,减少数组的长度
sort() // 在原数组上进行排序,不生成副本
splice(start,删除的个数,插入的元素) //可删除从index处开始的零个或多个元素,并且用参数列表中声明的一个或多个值来替换那些被删除的元素
// 不改变原数组:
concat() // 用于连接两个或多个数组,不改变原数组,返回一个新的数组
join() // 将数组中的所有元素都转化为字符串并拼接在一起,默认使用逗号,返回最终生成的字符串
map() // 对数组的每一项运行给定函数,返回每次函数调用的结果组成的数组,不改变原数组
indexof // 返回指定位置的元素值或字符串,通过搜索值与下标寻找
every() // 如果每一项都为true,则返回true
some() // 某一项返回true,则返回true
forEach() // 对数组的每一项运行给定函数,没有返回值// 接口数据映射
let arr = res.map(item=>{
return {
name:item.name,
age:item.age,
sex:item.sex === 1? '男':item.sex === 0?'女':'保密',
avatar: item.img
}
})// 求一个字符串中每个字母出现的次数
let str = 'abcabdaac'
str.split('').reduce((res,cur)=>{
res[cur] ? res[cur]++ : res[cur] = 1
return res
},{})
// {a: 4, b: 2, c: 2, d: 1}Array.prototype._map = fn => {
let newArr = [];
let me = this;
for(let i = 0;i<me.length;i++){
newArr.push(fn(me[i]));
}
return newArr;
}Array.prototype._reduce = (callback, initialValue) => {
let arr = this;
let base = typeof initialValue == 'undefined' ? arr[0] : initialValue;
let startPoint = typeof initialValue === 'undefined' ? 1 : 0;
arr.slice(startPoint).forEach((val, index) => {
base = callback(base, val, index + startPoint, arr)
})
return base
}//Ajax原生简单实现
let xhr = XMLHttpRequest;
xhr.onreadystatechange = () => {
if(xhr.readyState === 4){
if(xhr.status === 200){
console.log(xhr.responseText);
}else{
console.error(xhr.statusText);
}
}
}
xhr.onerror = e => {
console.error(xhr.statusText);
}
xhr.open('GET','/EndPonint',true);
xhr.send(null);fetch('./api/demo.json')
.then((response) => {
response.json().then((data) => {
...
});
});
.catch((err) => {...});// Promise的模拟实现
class _Promise {
constructor(fn) {
let _this = this;
this._queue = [];
this._success = null;
this._error = null;
this.status = '';
fn((...arg) => {
// resolve
if (_this.status != 'error') {
_this.status = 'success';
_this._success = arg;
_this._queue.forEach(json => {
json.fn1(...arg)
})
}
}, (...arg) => {
// reject
if (_this.status != 'success') {
_this.status = 'error';
_this._error = arg;
_this._queue.forEach(json => {
json.fn2(...arg)
})
}
})
}
then(fn1, fn2) {
let _this = this;
return new _Promise((resolve, reject) => {
if (_this.status == 'success') {
resolve(fn1(..._this._success))
} else if (_this.status == 'error') {
fn2(..._this._error)
} else {
_this._queue.push({fn1,fn2});
}
})
}
}//Promise.all的模拟实现(race的实现类似)
Promise.prototype._all = interable => {
let results = [];
let promiseCount = 0;
return new Promise(function (resolve, reject) {
for (let val of iterable) {
Promise.resolve(val).then(res => {
promiseCount++;
results[i] = res;
if (promiseCount === interable.length) {
return resolve(results);
}
}, err => {
return reject(err);
})
}
})
}//经典案例:斐波那契数列 :1, 1, 2, 3, 5, 8, 13, …
let count = 0;
const fib = (()=>{
let arr = [1,1]
return function(n){
count++;
let res = arr[n];
if(res){
return res;
}else{
arr[n] = fib(n-1) + fib(n-2);
return arr[n]
}
}
})();//通过闭包特性,模拟私有变量
const book = (function () {
var page = 100;
return function () {
this.auther = 'okaychen';
this._page = function () {
console.log(page);
}
}
})();
var a = new book();
a.auther // "okaychen"
a._page() // 100
a.page // undefinedfor (var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log(i);
}, i*1000 );
}
// 经典老问题,输出结果:6 6 6 6 6
// js执行的时候首先会先执行主线程,异步相关的(setTimeout)会存到异步队列里,
// 当主线程执行完毕开始执行异步队列,主线程执行完毕后,此时 i 的值为 6,
// 所以在执行异步队列的时候,打印出来的都是 6
// 利用闭包来取正确值
for (var i=1; i<=5; i++) {
setTimeout( (function(i) {
return function() {
console.log(i);
}
})(i), i*1000 );
}
// 方案二:或者使用ES6的let,这里let本质也是形成了一个闭包
for (let i=1; i<=5; i++) {
setTimeout( function timer() {
console.log(i);
}, i*1000 );
}// new构造函数的模拟实现
const _new = function(){
let obj = new Object();
let _constructor = [].shift.call(arguments);
// 使用中间函数来维护原型关系
const F = function(){};
F.prototype = _constructor.prototype;
obj = new F();
let res = _constructor.apply(obj,arguments);
return typeof res === 'object' ? res || obj : obj;
}let element = document.getElementById("some_element");
let myObj = new Object();
myObj.element = element;
element.someObject = myObj;
// 变量myObj有一个element属性执行element,而变量element有一个名为someObject属性指回myObj,因为循环引用,引用计数法将没办法回收该内存
// 我们需要手动切断它们的循环引用,防止内存泄露
myObj.element = null;
element.someObject =null;// 举个栗子:正则验证字符串
// 函数封装后
function check(reg, txt) {
return reg.test(txt)
}
check(/\d+/g, 'test') //false
check(/[a-z]+/g, 'test') //true
// 需要复用第一个reg参数,Currying后,将两个参数分开,可以直接调用hasNumber,hasLetter等函数
function curryingCheck(reg) {
return function(txt) {
return reg.test(txt)
}
}
let hasNumber = curryingCheck(/\d+/g)
let hasLetter = curryingCheck(/[a-z]+/g)
hasNumber('test1') // true
hasLetter('21212') // false// 比如:解决原生方法在现代浏览器和IE之间的兼容问题
// 提前返回: 使用函数立即调用进行了一次兼容判断(部分求值),返回兼容的事件绑定方法
// 延迟执行:返回新函数,在新函数调用兼容的事件方法。等待addEvent新函数调用,延迟执行
const addEvent = (function() {
if(window.addEventListener) {
return function(ele, type, fn, isCapture) {
ele.addEventListener(type, fn, isCapture)
}
} else if(window.attachEvent) {
return function(ele, type, fn) {
ele.attachEvent("on" + type, fn)
}
}
})()// js中bind实现机制正是Currying
Function.prototype.bind = function (context) {
var _this = this
var args = Array.prototype.slice.call(arguments, 1)
return function() {
return _this.apply(context, args)
}
}小册高频面试题题部分到这告别一段尾声了,平时学习过程我们在不断接触一些学习资源,2019年转眼过去,下面是我大学三年前端学习过程收录的一些资源,不能说能面面俱到,但多数都是我有过接触并且得到业界认可的资源,这里做整理和分享,算是20年新年礼物给大家,希望对我和大家都有所帮助,这也是一点小小的愿望
资源教程&官方文档
资源教程
官方文档
开发社区&工具资源
博客推荐
国内以及国外开源团队:
篇幅有限,不能一一列出对我技术成长有帮助的这些团队和大牛的博客,感谢这些技术大牛让我多少改变对写作的看法,更加坚持这件事,我希望我也能带着学习的视角坚持写作(或技术或生活),于后来之人留下哪怕很细微的帮助,到这里这份简简单单的面试小册第一版就结束了,新年新愿望,也祝福大家在新的一年里学业有成,事业顺利
那么,最后为了突出主题呢,还是要写一些对于这份小册的愿景吧:如果你是应届生(当然,大牛除外),正面临找前端开发的工作,或者即将成为毕业生的预备生,我相信这份前端面试小册多多少少会帮到你,在这"不景气"的"寒冬"之季,我们仍要提高自身综合素质,坚持技术生活,通过不断给自己设立小目标并实现来督促自身学习提高,2020愿我们只争朝夕不负韶华

开发社区
工具资源
团队博客
大牛博客
十年踪迹(月影)



