├── .DS_Store ├── .idea ├── .gitignore ├── front-end-interview.iml ├── modules.xml ├── prettier.xml └── vcs.xml ├── 1 前端面试准备.md ├── 10 offer收割机之手写代码篇.md ├── 11 offer收割机之代码输出篇.md ├── 12LeetCode面试常考题目.md ├── 13 offer收割机之Vue篇.md ├── 14 vue项目的性能优化.md ├── 2 程序员面试软技能.md ├── 3 offer收割机之HTML篇.md ├── 4 offer收割机之CSS篇.md ├── 5 offer收割机之JavaScript篇.md ├── 6 offer收割机之性能优化篇.md ├── 7 offer收割机之前端工程化篇.md ├── 8 offer收割机之计算机网络篇.md ├── 9 offer收割机之浏览器原理篇.md ├── README.md └── erweima ├── .DS_Store ├── gong.jpg └── we.jpg /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kjd1000000/front-end-interview/a166c7d0119588ff4f3dd89ea5877adedc683f7e/.DS_Store -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # 默认忽略的文件 2 | /shelf/ 3 | /workspace.xml 4 | # 基于编辑器的 HTTP 客户端请求 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /.idea/front-end-interview.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/prettier.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /1 前端面试准备.md: -------------------------------------------------------------------------------- 1 | 2 | ## 一、面试准备 3 | 4 | ### 1. 利用脑图来梳理知识点 5 | 6 | 对于统一校招类的面试,要重点梳理前端的所有知识点,校招面试一般是为了做人才储备,所以看的是候选人的可塑性和学习能力;对于社招类面试,则看重的是业务能力和 JD 匹配程度,所以要针对性地整理前端知识点,针对性的内容包括:项目用到的技术细节、个人技能部分需要加强或提升的常考知识点。 7 | 8 | 9 | 10 | 所以,不仅仅简历要针对性地来写,知识点也要根据自己的经历、准备的简历、公司和职位描述来针对性地梳理。 11 | 12 | 13 | 14 | 基础知识来自于自己平时的储备,一般对着一本系统的书籍或者自己平时的笔记过一遍即可,但是提到自己做到的项目是没有固定的复习套路的,而且围绕项目可以衍生出来各种问题,都需要了解,项目讲清楚对于候选人也特别重要。基础是固定的,任何人经过一段时间都可以学完的,但是项目经历是实打实的经验。 15 | 16 | 17 | 18 | 对于项目的复习和准备,建议**列思维导图(脑图)**,针对自己重点需要讲的项目,列出用到的技术点(知识点),介绍背景、项目上线后的收益以及后续优化点。这是第一层,第二层就是针对技术点(知识点)做各种发散的问题。 19 | 20 | 21 | 22 | 注:JD(job description),是指职位描述,是其缩写。一般在招聘中,最常用到的意思就是岗位介绍和工作职责描述的意思。 23 | 24 | ### 2. 程序员应该具备哪些软技能? 25 | 26 | 程序员除了业务技能外,应该具有下面的软技能: 27 | 28 | - 韧性:抗压能力,在一定项目压力下能够迎难而上,比如勇于主动承担和解决技术难题 29 | - 责任心:对于自己做过的项目,能够出现 bug 之类主动解决 30 | - 持续学习能力:IT 行业是个需要不断充电的行业,尤其 Web 前端这些年一直在巨变,所以持续学习能力很重要 31 | - 团队合作能力:做项目不能个人英雄主义,应该融入团队,跟团队一起打仗 32 | - 交流沟通能力:经常会遇见沟通需求和交互设计的工作,应该乐于沟通分享 33 | 34 | ### 3. 准备合适的技术型简历 35 | 36 | **(1)技术型简历的重要组成部分** 37 | 38 | 一份合适的技术型简历最重要的三部分是: 39 | 40 | - 个人掌握的技能,是否有岗位需要用到的技能,及其技能掌握的熟练程度:熟悉、了解还是精通 41 | - 项目经历,项目经历是否对现在岗位有用或者有重叠,是否能够驾驭大型项目 42 | - 实习经历,对于没有经验的应届生来说,实习经历是很重要的部分,是否有大公司或者具体项目的实习经历是筛选简历的重要参考 43 | 44 | 45 | 46 | 技术型简历一般不要太花俏,关键要语言表达通顺清楚,让语言准确和容易理解,在 HR 筛选简历的时候,可以瞬间抓住他的眼球。另外如果有一些特殊奖项,也可以在简历中突出出来,比如:季度之星、最佳个人之类的奖项,应届生会有优秀毕业生、全额奖学金等。 47 | 48 | 49 | 50 | **(2)推荐使用 PDF 版本的简历** 51 | 52 | 一般来说简历会有 Word、Markdown、PDF 等版本,这里推荐使用 PDF 版本的简历,主要原因如下: 53 | 54 | - 内容丰富,布局调整方便 55 | - 字体等格式有保障,你不知道收到你简历的人用的是什么环境,PDF 版本不会因为不同操作系统等原因而受限 56 | - 便于携带和传播,始终存一份简历在手机或者邮箱内,随时发送 57 | - 不容易被涂改 58 | 59 | 60 | 61 | 一般 Windows 系统的 Word、Mac 系统的 Pages 都支持导出 PDF 格式的文件,原稿可以保存到云端或者 iCloud,方便以后修改。 62 | 63 | 64 | 65 | 虽然我们是 Web 前端工程师,但是不推荐使用 HTML 格式的简历,HTML 版本的简历容易受浏览器等环境因素影响,而且接收方不一定是技术人员,炫酷的效果对方不一定能被看到。 66 | 67 | 68 | 69 | **(3)简历最好要有针对性地来写** 70 | 71 | 简历是「**敲门砖**」,建议根据想要找的公司、岗位和职位描述来有针对性地写简历。尤其是个人技能和项目(实习)经验部分,要根据岗位要求来写,这样才能增加受邀面试的机会。 72 | 73 | > 举个例子:好友给你推荐了百度地图部门的一个 Web 前端工程师工作,并且把职位描述(JD)发给你了,里面有要求哪些技能,用到哪些技术,还有加分项。那么你写简历就应该思考自己有没有这些技能。如果没有 JD,那么至少你应该知道:地图部门肯定做一些跟地图相关的工作,如果恰巧你之前研究过地图定位,了解 HTML5 Geolocation 定位接口,那么你可以在简历里提一下。 74 | 75 | 很多时候我们并不知道简历会被谁看到,也不知道简历会被朋友/猎头投递到什么公司或者职位,那么这样的简历应该是一种「通用简历」。所谓通用简历,应该是与我们找的职位和期望的级别相匹配的简历,比如想找大概 T4 水平的 Web 前端工作,那么就应该在简历体现出来自己的技能能够达到 T4 的水平。不要拿着一两年前的简历去找工作,前端这两年发展速度很快,只靠一两年前简历上面「精通、熟悉」的库和框架,可能已经找不到工作了。 76 | 77 | 78 | 79 | 所以,写简历也是个技术活,而且是一个辛苦活!不要用千篇一律的模板! 80 | 81 | 82 | 83 | **(4)简历是面试时「点菜」用的菜单** 84 | 85 | 简历除了是「敲门砖」之外,还是供面试官提问用的「菜单」。面试官会从简历上面写的技能、项目进行提问。所以简历是候选人「反客为主」的重要工具,这也是我们一直提到的:**不要造假或者描述太出格**,而应该实事求是地写简历。简历中的技能和项目都要做好知识点梳理,尽量多地梳理出面试官可能问到的问题,并且想出怎么回答应对,**千万不要在简历上自己给自己挖坑**。 86 | 87 | 88 | 89 | 另外简历中不要出现错误的单词拼写,注意单词的大小写,比如`jQuery`之类。并且,作为一个前端工程师,简历的布局一定要合理,美观。 90 | 91 | ### 4. 收到面试邀请之后的准备 92 | 93 | 当有公司邀请我们去面试的时候,应该针对性地做一些功课。 94 | 95 | **(1)了解部门和团队** 96 | 97 | 了解部门做的事情,团队用的技术栈,前文提到这部分信息一般从 JD 当中就可以看到,如果 JD 并没有这些信息,那么可以根据面试的部门搜索下,总会找到一些零星的信息,如果实在没有任何信息,就准备岗位需要的通用技术。 98 | 99 | **(2)了解面试官** 100 | 101 | 通过邀请电话或者面试邀请邮件,可以找到面试官信息。通过这些信息查找面试官技术博客、GitHub 等,了解面试官最近关注的技术和擅长的技术,因为面试官往往会在面试的过程中问自己擅长的技术。 102 | 103 | ## 二、面试过程: 104 | 105 | ### 1. 面试过程中要注意社交礼仪 106 | 107 | - **注意社交礼仪:**虽然说 IT 行业不怎么注重工作环境,上下级也没有繁文缛节,但是在面试中还是应该注意一些社交礼仪的。像进门敲门、出门关门、站着迎人这类基本礼仪还是要做的。 108 | - **舒适但不随意的着装:**首先着装方面,不要太随意,也不要太正式,太正式的衣服可能会使人紧张,所以建议穿自己平时喜欢的衣服,关键是干净整洁。 109 | - **约个双方都舒服的面试时间:**如果 HR 打电话预约面试时间,记得一定要约个双方都舒服的时间,宁愿请假也要安排好面试时间。面试时间很重要,**提前十分钟到面试地点**,熟悉下环境,做个登记之类的,留下个守时的好印象。如果因为堵车之类的原因不能按时到达,则要在约定时间之前电话通知对方。 110 | 111 | ### 2 面试中出现的常规问题 112 | 113 | 对于面试中出现的常规问题要做好准备,比如:介绍下自己,为什么跳槽,面试最后一般会问有什么要问的。 114 | 115 | **(1)介绍自己** 116 | 117 | 介绍自己时,切忌从自己大学实习一直到最新公司全部毫无侧重地介绍,这些在简历当中都有,最好的方式是在介绍中铺垫自己的技术特长、做的项目,引导面试官问自己准备好的问题。 118 | 119 | **(2) 为什么跳槽** 120 | 121 | 这个问题一定要慎重和认真思考,诚实回答。一般这个问题是想评估你入职能够待多长时间,是否能够融入团队。每个人跳槽前肯定想了很多原因,最终才走出这一步,不管现在工作怎样,**切忌抱怨,不要吐槽,更不要说和现在领导不和睦之类的话**。 多从自身发展找原因,可以表达寻找自己心目中的好的技术团队氛围和平台机会,比如:个人遇见了天花板,希望找个更好的发展机会。 122 | 123 | ### 3. 如何介绍自己的项目经历 124 | 125 | **(1)介绍项目背景** 126 | 127 | 这个项目为什么做,当初大的环境背景是什么?还是为了解决一个什么问题而设立的项目?背景是很重要的,如果不了解背景,一上来就听一个结论性的项目,面试官可能对于项目的技术选型、技术难度会有理解偏差,甚至怀疑是否真的有过这样的项目。 128 | 129 | > 比如一上来就说:我们的项目采用了「backbone」来做框架,然后。。。而「backbone」已经是三四年前比较新鲜的技术,现在会有更好的选择方案,如果不介绍项目的时间背景,面试官肯定一脸懵逼。 130 | 131 | **(2) 承担角色** 132 | 133 | 项目涉及的人员角色有哪些,自己在其中扮演的角色是什么? 134 | 135 | 136 | 137 | 这里候选往往人会自己给自己挖坑,比如把自己在项目中起到的作用夸大等。一般来说,面试官细节追问的时候,如果候选人能够把细节或者技术方案等讲明白、讲清楚,不管他是真的做过还是跟别人做过,或者自己认真思考过,都能体现候选人的技术水平和技术视野。前提还是在你能够兜得住的可控范围之内做适当的「美化」。 138 | 139 | **(3)最终的结果和收益** 140 | 141 | 项目介绍过程中,应该介绍项目最终的结果和收益,比如项目最后经过多久的开发上线了,上线后的数据是怎样的,是否达到预期,还是带来了新的问题,遇见了问题自己后续又是怎样补救的。 142 | 143 | **(4)有始有终:项目总结和反思** 144 | 145 | 有总结和反思,才会有进步。 项目做完了往往会有一些心得和体会,这时候应该跟面试官说出来。在梳理项目的总结和反思时,可以按照下面的列表来梳理: 146 | 147 | - 收获有哪些? 148 | - 是否有做得不足的地方,怎么改进? 149 | - 是否具有可迁移性? 150 | 151 | 152 | 153 | 比如,之前详细介绍了某个项目,这个项目当时看来没有什么问题,但是现在有更好的解决方案了,候选人就应该在这里提出来:现在看来,这个项目还有 xx 的问题,我可以通过 xx 的方式来解决。再比如:做这个项目的时候,你做得比较出彩的地方,可以迁移到其他项目中直接使用,小到代码片段,大到解决方案,总会有你值得总结和梳理的地方。 154 | 155 | 156 | 157 | 介绍完项目总结这部分,也可以引导面试官往自己擅长的领域思考。比如上面提到项目中的问题,可以往你擅长的方面引导,即使面试官没有问到,你也介绍到了。 158 | 159 | 160 | 161 | 按照上面的四段体介绍项目,会让面试官感觉候选人有清晰的思路,对整个项目也有理解和想法,还能够总结反思项目的收益和问题,可谓「一箭三雕」。 162 | 163 | ### 4. 项目细节和技术点的追问 164 | 165 | 介绍项目的过程中,面试官可能会追问技术细节,所以在准备面试的时候,应该尽量把技术细节梳理清楚,技术细节包括: 166 | 167 | - 技术选型方案:当时做技术选型所面临的状况 168 | - 技术解决方案:最终确定某种技术方案的原因,比如:选择用 Vue 而没有用 React 是为什么? 169 | - 项目数据和收益 170 | - 项目中最难的地方 171 | - 遇见的坑:如使用某种框架遇见哪些坑 172 | 173 | 174 | 175 | 一般来说,做技术选型的时候需要考虑下面几个因素: 176 | 177 | - 时代:现在比较火的技术是什么,为什么火起来,解决了什么问题,能否用到我的项目中? 178 | - 团队:个人或者团队对某种技术的熟悉程度是怎样的,学习成本又是怎样的? 179 | - 业务需求:需求是怎样的,能否套用现在的成熟解决方案/库来快速解决? 180 | - 维护成本:一个解决方案的是否再能够 cover 住的范围之内? 181 | 182 | 183 | 184 | 在项目中遇见的数据和收益应该做好跟踪,保证数据的真实性和可信性。另外,遇见的坑可能是面试官问得比较多的,尤其现在比较火的一些技术(Vue、React、webpack),一般团队都在使用,所以一定要提前准备 185 | 186 | 下。 187 | 188 | ### 5. 没有做过大型项目怎么办 189 | 190 | 对于刚刚找工作的应届生,或者面试官让你进行一个大型项目的设计,候选人可能没有类似的经验。这时候不要用「我不会、没做过」一句话就带过。如果是实在没有项目可以说,那么可以提自己日常做的练手项目,或者看到一个解决方案的文章/书,提到的某个项目,抒发下自己的想法。 191 | 192 | 193 | 194 | 如果是对于面试官提出来需要你设计的项目/系统,可以按照下面几步思考: 195 | 196 | 1. 有没有遇见过类似的项目 197 | 2. 有没有读过类似解决方案的文章 198 | 3. 项目能不能拆解,拆解过程中能不能发现自己做过的项目可以用 199 | 4. 项目解决的问题是什么,这类问题有没有更好的解决方案 200 | 201 | 202 | 203 | 总之,切记不要一句「不知道、没做过」就放弃,每一次提问都是自己表现的机会。 204 | 205 | ### 6. 当被分配一个几乎不可能完成的任务时,会怎么做 206 | 207 | 这种情况下,一般通过下面方式来解决: 208 | 209 | 1. 自己先查找资料,寻找解决方案,评估自己需要怎样的资源来完成,需要多长时间 210 | 2. 能不能借助周围同事来解决问题 211 | 3. 拿着分析结果跟上级反馈,寻求帮助或者资源 212 | 213 | 突出的软技能:分析和解决问题,沟通寻求帮助。 214 | 215 | ### 7. 提问环节 216 | 217 | 面试是一个双向选择的事情,所以面试后一般会有提问环节。在提问环节,候选人最好不要什么都不问,更不要只问薪水待遇、是否加班之类的问题。 218 | 219 | 220 | 221 | 其实这个时候可以反问面试官了解团队情况、团队做的业务、本职位具体做的工作、工作的规划,甚至一些数据(可能有些问题不会直面回答)。 222 | 223 | 224 | 225 | 还可以问一些关于公司培训机会和晋升机会之类的问题。如果是一些高端职位,则可以问一下:自己的 leader 想把这个职位安排给什么样的人,希望多久的时间内可以达到怎样的水平。 226 | 227 | ## 三、HR面试: 228 | 229 | ### 1. 谈薪资——准确定位和自我估值 230 | 231 | 在准备跳槽时,每个人肯定会对自己有一个预估,做好足够的心理准备。下面谈下怎么对自己的薪酬做个评估。一般来说跳槽的薪水是根据现在薪酬的基础上浮 15~30%,具体看个人面试的情况。对于应届毕业生,大公司基本都有标准薪水,同期的应届生差别不会特别大。 232 | 233 | 234 | 235 | 除了上面的方法,还应该按照公司的技术职级进行估值。每个公司都有对应的技术职级,不同的技术职级薪酬范围是固定的,如果是小公司,则可以参考大公司的职级范围来确定薪资范围。根据职级薪资范围和自己现在薪酬基础上浮后的薪酬,做个比较,取其较高的结果。 236 | 237 | 238 | 239 | 除此之外,我们可以在**微信小程序****offershow****、牛客网**等平台看看网友分享的各个公司的薪酬体系。 240 | 241 | 242 | 243 | 当然如果面试结果很好,可以适当地提高下薪酬预期。除了这种情况,应该针对不同的性质来对 offer先做好不同的估值。这里的预期估值只是心理预期,也就是自己的「底牌」。 244 | 245 | 所谓不同性质的 offer 指的是: 246 | 247 | - 是否是自己真心喜欢的工作岗位: 如果是自己真心喜欢的工作岗位,比如对于个人成长有利,或者希望进入某个公司部门,从事某个专业方向的工作,而你自己对于薪酬又不是特别在意,这时候可以适当调低薪酬预期,以拿到这个工作机会为主。 248 | - 是否只是做 backup 的岗位:面试可能不止面试一家,对于不是特别喜欢的公司部门,那么可以把这个 offer 做为 backup,后面遇见喜欢的公司可以以此基础来谈薪水。 249 | 250 | 251 | 252 | 这时候分两种情况:如果面试结果不是很好,这种情况应该优先拿到 offer,所以可以适当降低期望薪酬;如果面试结果很好,这种情况应该多要一些薪酬,增加的薪酬可以让你加入这家公司也心里很舒服。 253 | 254 | 对于自己真正的目标职位,面试之前应该先找 backup 岗位练练手,一是为了找出面试的感觉,二是为了拿到几个 offer 做好 backup。 255 | 256 | ### 2. 跟 HR 沟通的技巧 257 | 258 | 跟 HR 沟通的时候,不要夸大现在的薪酬,HR 知道的信息往往会超出你的认知,尤其大公司还会有背景调查,所以不要撒谎,实事求是。跟 HR 沟通的技巧有以下几点: 259 | 260 | - **不要急于出价** 261 | 262 | 不要急于亮出自己的底牌,一旦你说出一个薪酬范围,自己就不能增加薪酬了,还给了对方砍价的空间。而且一个不合理的价格反而会让对方直接放弃。所以不要着急出价,先让对方出价。 263 | 264 | 同时,对于公司级别也是,不要一开始就奔着某个目标去面试,这样会加大面试的难度,比如: 265 | 266 | > 目标是拿到阿里 P7 的职位,不要说不给 P7 我就不去面试之类的,这样的要求会让对方一开始就拿 P7 的标准来面试,可能会找 P8+ 的面试官来面试你,这样会大大提升面试难度。 267 | 268 | - **要有底气足够自信** 269 | 270 | 要有底气,自信,自己按照上面的估值盘算好了想要的薪酬,那么应该有底气地说出来,并且给出具体的原因,比如: 271 | 272 | > 1. 我已经对贵公司的薪酬范围和级别有了大概的了解,我现在的水平大概范围是多少 273 | > 2. 现在公司很快就有调薪机会,自己已经很久没有调薪,年前跳槽会损失年终奖等情况 274 | > 3. 现在我已经有某个公司多少 K 的 offer 275 | 276 | 如果 HR 表示你想要的薪酬不能满足,这时候你应该给出自己评估的依据,是根据行业职级标准还是自己现有薪酬范围,这样做到有理有据。 277 | 278 | - **谈好 offer 就要尽快落实** 279 | 280 | 对于已经谈拢的薪酬待遇,一定要 HR 以发邮件 offer 的形式来确认。 281 | 282 | ## 四、其他 283 | 284 | ### 1. 总结和思考 285 | 286 | - 面试完了多总结自己哪里做得不好,哪里做得好,都记录下来,后续扬长避短 287 | - 通过面试肯定亲身体会到了公司团队文化、面试官体现出来的技术能力、专业性以及职位将来所做的事情,跟自己预期是否有差距,多个 offer 的话多做对比 288 | 289 | 290 | 291 | 每次面试应该都有所收获,毕竟花费了时间和精力。即使面不上也可以知道自己哪方面做得不好,继续加强。 292 | 293 | ### 2. 面试注意点 294 | 295 | 在面试过程中,我们经常会被问及各种问题,在回答的过程中,这里简单列举了一些“坑”。 296 | 297 | - “对不起,我真的很紧张”,即使紧张也不要说出来; 298 | - “我想知道这个职位的具体收入有多少”一开始就谈钱,你的理想、价值观、使命、目标呢? 299 | - “我的缺点是斤斤计较,不能加班,承受不了工作压力太大”不要主动告诉别人你的缺点,你来是展示你的优势的; 300 | - “我真的很想要这份工作”不要太过于表现你的欲望或绝望,这是你软弱的表现; 301 | - “我现在(之前)的老板太不好了……”向 HR 说你老板的话会变成对方对你的看法; 302 | - “我需要……能实现工作目标”招聘是为了满足公司需求,不是为你搭建舞台; 303 | - “请问面试什么时候结束”不要表现赶时间,你不尊重公司,自然不会录用你; 304 | - “我喜欢贵公司的福利待遇”你是来工作的,不是因为福利待遇才来的;“无可奉告”不违法不涉及隐私,如实告知,有准备的话不会“无可奉告”; 305 | - “工作第一年的福利待遇及带薪年假和病假等情况是什么样的”这是入职时才可以问的规定,先问只会被误会; 306 | - “我在离婚或者怀孕期间经历了非常艰难的时期”不要主动告诉对方隐私情况,你个人的事情处理不好会容易联想到工作表现; 307 | - “我没有什么问题要问”最后被问到时这样回答等同于“再也不见”。 -------------------------------------------------------------------------------- /11 offer收割机之代码输出篇.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### 前言: 4 | 5 | **代码输出结果**也是面试中常考的题目,一段代码中可能涉及到很多的知识点,这就考察到了应聘者的基础能力。在前端面试中,常考的代码输出问题主要涉及到以下知识点:**异步编程、事件循环、this指向、作用域、变量提升、闭包、原型、继承**等,这些知识点往往不是单独出现的,而是在同一段代码中包含多个知识点。所以,笔者将这些问题大致分为四类进行讨论。这里不会系统的阐述基础知识,而是通过面试例题的形式,来讲述每个题目的知识点以及代码的执行过程。如果会了这些例题,在前端面试中多数代码输出问题就可以轻而易举的解决了。 6 | 7 | 8 | 9 | **注:**本文中所有例题收集自牛客网面经、网络博文等,如果侵权,请联系删除! 10 | 11 | ## 一、异步&事件循环 12 | 13 | ### 1. 代码输出结果 14 | 15 | ``` 16 | const promise = new Promise((resolve, reject) => { 17 | console.log(1); 18 | console.log(2); 19 | }); 20 | promise.then(() => { 21 | console.log(3); 22 | }); 23 | console.log(4); 24 | ``` 25 | 26 | 输出结果如下: 27 | 28 | ``` 29 | 1 30 | 2 31 | 4 32 | ``` 33 | 34 | promise.then 是微任务,它会在所有的宏任务执行完之后才会执行,同时需要promise内部的状态发生变化,因为这里内部没有发生变化,一直处于pending状态,所以不输出3。 35 | 36 | ### 2. 代码输出结果 37 | 38 | ``` 39 | const promise1 = new Promise((resolve, reject) => { 40 | console.log('promise1') 41 | resolve('resolve1') 42 | }) 43 | const promise2 = promise1.then(res => { 44 | console.log(res) 45 | }) 46 | console.log('1', promise1); 47 | console.log('2', promise2); 48 | ``` 49 | 50 | 输出结果如下: 51 | 52 | ``` 53 | promise1 54 | 1 Promise{: resolve1} 55 | 2 Promise{} 56 | resolve1 57 | ``` 58 | 59 | 需要注意的是,直接打印promise1,会打印出它的状态值和参数。 60 | 61 | 62 | 63 | 代码执行过程如下: 64 | 65 | 1. script是一个宏任务,按照顺序执行这些代码; 66 | 2. 首先进入Promise,执行该构造函数中的代码,打印`promise1`; 67 | 3. 碰到`resolve`函数, 将`promise1`的状态改变为`resolved`, 并将结果保存下来; 68 | 4. 碰到`promise1.then`这个微任务,将它放入微任务队列; 69 | 5. `promise2`是一个新的状态为`pending`的`Promise`; 70 | 6. 执行同步代码1, 同时打印出`promise1`的状态是`resolved`; 71 | 7. 执行同步代码2,同时打印出`promise2`的状态是`pending`; 72 | 8. 宏任务执行完毕,查找微任务队列,发现`promise1.then`这个微任务且状态为`resolved`,执行它。 73 | 74 | ### 3. 代码输出结果 75 | 76 | ``` 77 | const promise = new Promise((resolve, reject) => { 78 | console.log(1); 79 | setTimeout(() => { 80 | console.log("timerStart"); 81 | resolve("success"); 82 | console.log("timerEnd"); 83 | }, 0); 84 | console.log(2); 85 | }); 86 | promise.then((res) => { 87 | console.log(res); 88 | }); 89 | console.log(4); 90 | ``` 91 | 92 | 输出结果如下: 93 | 94 | ``` 95 | 1 96 | 2 97 | 4 98 | timerStart 99 | timerEnd 100 | success 101 | ``` 102 | 103 | 代码执行过程如下: 104 | 105 | - 首先遇到Promise构造函数,会先执行里面的内容,打印`1`; 106 | - 遇到定时器`steTimeout`,它是一个宏任务,放入宏任务队列; 107 | - 继续向下执行,打印出2; 108 | - 由于`Promise`的状态此时还是`pending`,所以`promise.then`先不执行; 109 | - 继续执行下面的同步任务,打印出4; 110 | - 此时微任务队列没有任务,继续执行下一轮宏任务,执行`steTimeout`; 111 | - 首先执行`timerStart`,然后遇到了`resolve`,将`promise`的状态改为`resolved`且保存结果并将之前的`promise.then`推入微任务队列,再执行`timerEnd`; 112 | - 执行完这个宏任务,就去执行微任务`promise.then`,打印出`resolve`的结果。 113 | 114 | ### 4. 代码输出结果 115 | 116 | ``` 117 | Promise.resolve().then(() => { 118 | console.log('promise1'); 119 | const timer2 = setTimeout(() => { 120 | console.log('timer2') 121 | }, 0) 122 | }); 123 | const timer1 = setTimeout(() => { 124 | console.log('timer1') 125 | Promise.resolve().then(() => { 126 | console.log('promise2') 127 | }) 128 | }, 0) 129 | console.log('start'); 130 | ``` 131 | 132 | 输出结果如下: 133 | 134 | ``` 135 | start 136 | promise1 137 | timer1 138 | promise2 139 | timer2 140 | ``` 141 | 142 | 代码执行过程如下: 143 | 144 | 1. 首先,`Promise.resolve().then`是一个微任务,加入微任务队列 145 | 2. 执行timer1,它是一个宏任务,加入宏任务队列 146 | 3. 继续执行下面的同步代码,打印出`start` 147 | 4. 这样第一轮宏任务就执行完了,开始执行微任务`Promise.resolve().then`,打印出`promise1` 148 | 5. 遇到`timer2`,它是一个宏任务,将其加入宏任务队列,此时宏任务队列有两个任务,分别是`timer1`、`timer2`; 149 | 6. 这样第一轮微任务就执行完了,开始执行第二轮宏任务,首先执行定时器`timer1`,打印`timer1`; 150 | 7. 遇到`Promise.resolve().then`,它是一个微任务,加入微任务队列 151 | 8. 开始执行微任务队列中的任务,打印`promise2`; 152 | 9. 最后执行宏任务`timer2`定时器,打印出`timer2`; 153 | 154 | ### 5. 代码输出结果 155 | 156 | ``` 157 | const promise = new Promise((resolve, reject) => { 158 | resolve('success1'); 159 | reject('error'); 160 | resolve('success2'); 161 | }); 162 | promise.then((res) => { 163 | console.log('then:', res); 164 | }).catch((err) => { 165 | console.log('catch:', err); 166 | }) 167 | ``` 168 | 169 | 输出结果如下: 170 | 171 | ``` 172 | then:success1 173 | ``` 174 | 175 | 这个题目考察的就是**Promise的状态在发生变化之后,就不会再发生变化**。开始状态由`pending`变为`resolve`,说明已经变为已完成状态,下面的两个状态的就不会再执行,同时下面的catch也不会捕获到错误。 176 | 177 | ### 6. 代码输出结果 178 | 179 | ``` 180 | Promise.resolve(1) 181 | .then(2) 182 | .then(Promise.resolve(3)) 183 | .then(console.log) 184 | ``` 185 | 186 | 输出结果如下: 187 | 188 | ``` 189 | 1 190 | Promise {: undefined} 191 | ``` 192 | 193 | Promise.resolve方法的参数如果是一个原始值,或者是一个不具有then方法的对象,则Promise.resolve方法返回一个新的Promise对象,状态为resolved,Promise.resolve方法的参数,会同时传给回调函数。 194 | 195 | 196 | 197 | then方法接受的参数是函数,而如果传递的并非是一个函数,它实际上会将其解释为then(null),这就会导致前一个Promise的结果会传递下面。 198 | 199 | ### 7. 代码输出结果 200 | 201 | ``` 202 | const promise1 = new Promise((resolve, reject) => { 203 | setTimeout(() => { 204 | resolve('success') 205 | }, 1000) 206 | }) 207 | const promise2 = promise1.then(() => { 208 | throw new Error('error!!!') 209 | }) 210 | console.log('promise1', promise1) 211 | console.log('promise2', promise2) 212 | setTimeout(() => { 213 | console.log('promise1', promise1) 214 | console.log('promise2', promise2) 215 | }, 2000) 216 | ``` 217 | 218 | 输出结果如下: 219 | 220 | ``` 221 | promise1 Promise {} 222 | promise2 Promise {} 223 | 224 | Uncaught (in promise) Error: error!!! 225 | promise1 Promise {: "success"} 226 | promise2 Promise {: Error: error!!} 227 | ``` 228 | 229 | ### 8. 代码输出结果 230 | 231 | ``` 232 | Promise.resolve(1) 233 | .then(res => { 234 | console.log(res); 235 | return 2; 236 | }) 237 | .catch(err => { 238 | return 3; 239 | }) 240 | .then(res => { 241 | console.log(res); 242 | }); 243 | ``` 244 | 245 | 输出结果如下: 246 | 247 | ``` 248 | 1 249 | 2 250 | ``` 251 | 252 | Promise是可以链式调用的,由于每次调用 `.then` 或者 `.catch` 都会返回一个新的 promise,从而实现了链式调用, 它并不像一般任务的链式调用一样return this。 253 | 254 | 255 | 256 | 上面的输出结果之所以依次打印出1和2,是因为`resolve(1)`之后走的是第一个then方法,并没有进catch里,所以第二个then中的res得到的实际上是第一个then的返回值。并且return 2会被包装成`resolve(2)`,被最后的then打印输出2。 257 | 258 | ### 9. 代码输出结果 259 | 260 | ``` 261 | Promise.resolve().then(() => { 262 | return new Error('error!!!') 263 | }).then(res => { 264 | console.log("then: ", res) 265 | }).catch(err => { 266 | console.log("catch: ", err) 267 | }) 268 | ``` 269 | 270 | 输出结果如下: 271 | 272 | ``` 273 | "then: " "Error: error!!!" 274 | ``` 275 | 276 | 返回任意一个非 promise 的值都会被包裹成 promise 对象,因此这里的`return new Error('error!!!')`也被包裹成了`return Promise.resolve(new Error('error!!!'))`,因此它会被then捕获而不是catch。 277 | 278 | ### 10. 代码输出结果 279 | 280 | ``` 281 | const promise = Promise.resolve().then(() => { 282 | return promise; 283 | }) 284 | promise.catch(console.err) 285 | ``` 286 | 287 | 输出结果如下: 288 | 289 | ``` 290 | Uncaught (in promise) TypeError: Chaining cycle detected for promise # 291 | ``` 292 | 293 | 这里其实是一个坑,`.then` 或 `.catch` 返回的值不能是 promise 本身,否则会造成死循环。 294 | 295 | ### 11. 代码输出结果 296 | 297 | ``` 298 | Promise.resolve(1) 299 | .then(2) 300 | .then(Promise.resolve(3)) 301 | .then(console.log) 302 | ``` 303 | 304 | 输出结果如下: 305 | 306 | ``` 307 | 1 308 | ``` 309 | 310 | 看到这个题目,好多的then,实际上只需要记住一个原则:`.then` 或`.catch` 的参数期望是函数,传入非函数则会发生**值透传**。 311 | 312 | 313 | 314 | 第一个then和第二个then中传入的都不是函数,一个是数字,一个是对象,因此发生了透传,将`resolve(1)` 的值直接传到最后一个then里,直接打印出1。 315 | 316 | ### 12. 代码输出结果 317 | 318 | ``` 319 | Promise.reject('err!!!') 320 | .then((res) => { 321 | console.log('success', res) 322 | }, (err) => { 323 | console.log('error', err) 324 | }).catch(err => { 325 | console.log('catch', err) 326 | }) 327 | ``` 328 | 329 | 输出结果如下: 330 | 331 | ``` 332 | error err!!! 333 | ``` 334 | 335 | 我们知道,`.then`函数中的两个参数: 336 | 337 | - 第一个参数是用来处理Promise成功的函数 338 | - 第二个则是处理失败的函数 339 | 340 | 也就是说`Promise.resolve('1')`的值会进入成功的函数,`Promise.reject('2')`的值会进入失败的函数。 341 | 342 | 343 | 344 | 在这道题中,错误直接被`then`的第二个参数捕获了,所以就不会被`catch`捕获了,输出结果为:`error err!!!'` 345 | 346 | 347 | 348 | 但是,如果是像下面这样: 349 | 350 | ``` 351 | Promise.resolve() 352 | .then(function success (res) { 353 | throw new Error('error!!!') 354 | }, function fail1 (err) { 355 | console.log('fail1', err) 356 | }).catch(function fail2 (err) { 357 | console.log('fail2', err) 358 | }) 359 | ``` 360 | 361 | 在`then`的第一参数中抛出了错误,那么他就不会被第二个参数不活了,而是被后面的`catch`捕获到。 362 | 363 | ### 13. 代码输出结果 364 | 365 | ``` 366 | Promise.resolve('1') 367 | .then(res => { 368 | console.log(res) 369 | }) 370 | .finally(() => { 371 | console.log('finally') 372 | }) 373 | Promise.resolve('2') 374 | .finally(() => { 375 | console.log('finally2') 376 | return '我是finally2返回的值' 377 | }) 378 | .then(res => { 379 | console.log('finally2后面的then函数', res) 380 | }) 381 | ``` 382 | 383 | 输出结果如下: 384 | 385 | ``` 386 | 1 387 | finally2 388 | finally 389 | finally2后面的then函数 2 390 | ``` 391 | 392 | `.finally()`一般用的很少,只要记住以下几点就可以了: 393 | 394 | - `.finally()`方法不管Promise对象最后的状态如何都会执行 395 | - `.finally()`方法的回调函数不接受任何的参数,也就是说你在`.finally()`函数中是无法知道Promise最终的状态是`resolved`还是`rejected`的 396 | - 它最终返回的默认会是一个上一次的Promise对象值,不过如果抛出的是一个异常则返回异常的Promise对象。 397 | - finally本质上是then方法的特例 398 | 399 | 400 | 401 | `.finally()`的错误捕获: 402 | 403 | ``` 404 | Promise.resolve('1') 405 | .finally(() => { 406 | console.log('finally1') 407 | throw new Error('我是finally中抛出的异常') 408 | }) 409 | .then(res => { 410 | console.log('finally后面的then函数', res) 411 | }) 412 | .catch(err => { 413 | console.log('捕获错误', err) 414 | }) 415 | ``` 416 | 417 | 输出结果为: 418 | 419 | ``` 420 | 'finally1' 421 | '捕获错误' Error: 我是finally中抛出的异常 422 | ``` 423 | 424 | ### 14. 代码输出结果 425 | 426 | ``` 427 | function runAsync (x) { 428 | const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000)) 429 | return p 430 | } 431 | 432 | Promise.all([runAsync(1), runAsync(2), runAsync(3)]).then(res => console.log(res)) 433 | ``` 434 | 435 | 输出结果如下: 436 | 437 | ``` 438 | 1 439 | 2 440 | 3 441 | [1, 2, 3] 442 | ``` 443 | 444 | 首先,定义了一个Promise,来异步执行函数runAsync,该函数传入一个值x,然后间隔一秒后打印出这个x。 445 | 446 | 447 | 448 | 之后再使用`Promise.all`来执行这个函数,执行的时候,看到一秒之后输出了1,2,3,同时输出了数组[1, 2, 3],三个函数是同步执行的,并且在一个回调函数中返回了所有的结果。并且结果和函数的执行顺序是一致的。 449 | 450 | ### 15. 代码输出结果 451 | 452 | ``` 453 | function runAsync (x) { 454 | const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000)) 455 | return p 456 | } 457 | function runReject (x) { 458 | const p = new Promise((res, rej) => setTimeout(() => rej(`Error: ${x}`, console.log(x)), 1000 * x)) 459 | return p 460 | } 461 | Promise.all([runAsync(1), runReject(4), runAsync(3), runReject(2)]) 462 | .then(res => console.log(res)) 463 | .catch(err => console.log(err)) 464 | ``` 465 | 466 | 输出结果如下: 467 | 468 | ``` 469 | // 1s后输出 470 | 1 471 | 3 472 | // 2s后输出 473 | 2 474 | Error: 2 475 | // 4s后输出 476 | 4 477 | ``` 478 | 479 | 可以看到。catch捕获到了第一个错误,在这道题目中最先的错误就是`runReject(2)`的结果。如果一组异步操作中有一个异常都不会进入`.then()`的第一个回调函数参数中。会被`.then()`的第二个回调函数捕获。 480 | 481 | ### 16. 代码输出结果 482 | 483 | ``` 484 | function runAsync (x) { 485 | const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000)) 486 | return p 487 | } 488 | Promise.race([runAsync(1), runAsync(2), runAsync(3)]) 489 | .then(res => console.log('result: ', res)) 490 | .catch(err => console.log(err)) 491 | ``` 492 | 493 | 输出结果如下: 494 | 495 | ``` 496 | 1 497 | 'result: ' 1 498 | 2 499 | 3 500 | ``` 501 | 502 | then只会捕获第一个成功的方法,其他的函数虽然还会继续执行,但是不是被then捕获了。 503 | 504 | ### 17. 代码输出结果 505 | 506 | ``` 507 | function runAsync(x) { 508 | const p = new Promise(r => 509 | setTimeout(() => r(x, console.log(x)), 1000) 510 | ); 511 | return p; 512 | } 513 | function runReject(x) { 514 | const p = new Promise((res, rej) => 515 | setTimeout(() => rej(`Error: ${x}`, console.log(x)), 1000 * x) 516 | ); 517 | return p; 518 | } 519 | Promise.race([runReject(0), runAsync(1), runAsync(2), runAsync(3)]) 520 | .then(res => console.log("result: ", res)) 521 | .catch(err => console.log(err)); 522 | ``` 523 | 524 | 输出结果如下: 525 | 526 | ``` 527 | 0 528 | Error: 0 529 | 1 530 | 2 531 | 3 532 | ``` 533 | 534 | 可以看到在catch捕获到第一个错误之后,后面的代码还不执行,不过不会再被捕获了。 535 | 536 | 537 | 538 | 注意:`all`和`race`传入的数组中如果有会抛出异常的异步任务,那么只有最先抛出的错误会被捕获,并且是被then的第二个参数或者后面的catch捕获;但并不会影响数组中其它的异步任务的执行。 539 | 540 | ### 18. 代码输出结果 541 | 542 | ``` 543 | async function async1() { 544 | console.log("async1 start"); 545 | await async2(); 546 | console.log("async1 end"); 547 | } 548 | async function async2() { 549 | console.log("async2"); 550 | } 551 | async1(); 552 | console.log('start') 553 | ``` 554 | 555 | 输出结果如下: 556 | 557 | ``` 558 | async1 start 559 | async2 560 | start 561 | async1 end 562 | ``` 563 | 564 | 代码的执行过程如下: 565 | 566 | 1. 首先执行函数中的同步代码`async1 start`,之后遇到了`await`,它会阻塞`async1`后面代码的执行,因此会先去执行`async2`中的同步代码`async2`,然后跳出`async1`; 567 | 2. 跳出`async1`函数后,执行同步代码`start`; 568 | 3. 在一轮宏任务全部执行完之后,再来执行`await`后面的内容`async1 end`。 569 | 570 | 571 | 572 | 这里可以理解为await后面的语句相当于放到了new Promise中,下一行及之后的语句相当于放在Promise.then中。 573 | 574 | ### 19. 代码输出结果 575 | 576 | ``` 577 | async function async1() { 578 | console.log("async1 start"); 579 | await async2(); 580 | console.log("async1 end"); 581 | setTimeout(() => { 582 | console.log('timer1') 583 | }, 0) 584 | } 585 | async function async2() { 586 | setTimeout(() => { 587 | console.log('timer2') 588 | }, 0) 589 | console.log("async2"); 590 | } 591 | async1(); 592 | setTimeout(() => { 593 | console.log('timer3') 594 | }, 0) 595 | console.log("start") 596 | ``` 597 | 598 | 输出结果如下: 599 | 600 | ``` 601 | async1 start 602 | async2 603 | start 604 | async1 end 605 | timer2 606 | timer3 607 | timer1 608 | ``` 609 | 610 | 代码的执行过程如下: 611 | 612 | 1. 首先进入`async1`,打印出`async1 start`; 613 | 2. 之后遇到`async2`,进入`async2`,遇到定时器`timer2`,加入宏任务队列,之后打印`async2`; 614 | 3. 由于`async2`阻塞了后面代码的执行,所以执行后面的定时器`timer3`,将其加入宏任务队列,之后打印`start`; 615 | 4. 然后执行async2后面的代码,打印出`async1 end`,遇到定时器timer1,将其加入宏任务队列; 616 | 5. 最后,宏任务队列有三个任务,先后顺序为`timer2`,`timer3`,`timer1`,没有微任务,所以直接所有的宏任务按照先进先出的原则执行。 617 | 618 | ### 20. 代码输出结果 619 | 620 | ``` 621 | async function async1 () { 622 | console.log('async1 start'); 623 | await new Promise(resolve => { 624 | console.log('promise1') 625 | }) 626 | console.log('async1 success'); 627 | return 'async1 end' 628 | } 629 | console.log('srcipt start') 630 | async1().then(res => console.log(res)) 631 | console.log('srcipt end') 632 | ``` 633 | 634 | 输出结果如下: 635 | 636 | ``` 637 | script start 638 | async1 start 639 | promise1 640 | script end 641 | ``` 642 | 643 | 这里需要注意的是在`async1`中`await`后面的Promise是没有返回值的,也就是它的状态始终是`pending`状态,所以在`await`之后的内容是不会执行的,包括`async1`后面的 `.then`。 644 | 645 | ### 21. 代码输出结果 646 | 647 | ``` 648 | async function async1 () { 649 | console.log('async1 start'); 650 | await new Promise(resolve => { 651 | console.log('promise1') 652 | resolve('promise1 resolve') 653 | }).then(res => console.log(res)) 654 | console.log('async1 success'); 655 | return 'async1 end' 656 | } 657 | console.log('srcipt start') 658 | async1().then(res => console.log(res)) 659 | console.log('srcipt end') 660 | ``` 661 | 662 | 这里是对上面一题进行了改造,加上了resolve。 663 | 664 | 665 | 666 | 输出结果如下: 667 | 668 | ``` 669 | script start 670 | async1 start 671 | promise1 672 | script end 673 | promise1 resolve 674 | async1 success 675 | async1 end 676 | ``` 677 | 678 | ### 22. 代码输出结果 679 | 680 | ``` 681 | async function async1() { 682 | console.log("async1 start"); 683 | await async2(); 684 | console.log("async1 end"); 685 | } 686 | 687 | async function async2() { 688 | console.log("async2"); 689 | } 690 | 691 | console.log("script start"); 692 | 693 | setTimeout(function() { 694 | console.log("setTimeout"); 695 | }, 0); 696 | 697 | async1(); 698 | 699 | new Promise(resolve => { 700 | console.log("promise1"); 701 | resolve(); 702 | }).then(function() { 703 | console.log("promise2"); 704 | }); 705 | console.log('script end') 706 | ``` 707 | 708 | 输出结果如下: 709 | 710 | ``` 711 | script start 712 | async1 start 713 | async2 714 | promise1 715 | script end 716 | async1 end 717 | promise2 718 | setTimeout 719 | ``` 720 | 721 | 代码执行过程如下: 722 | 723 | 1. 开头定义了async1和async2两个函数,但是并未执行,执行script中的代码,所以打印出script start; 724 | 2. 遇到定时器Settimeout,它是一个宏任务,将其加入到宏任务队列; 725 | 3. 之后执行函数async1,首先打印出async1 start; 726 | 4. 遇到await,执行async2,打印出async2,并阻断后面代码的执行,将后面的代码加入到微任务队列; 727 | 5. 然后跳出async1和async2,遇到Promise,打印出promise1; 728 | 6. 遇到resolve,将其加入到微任务队列,然后执行后面的script代码,打印出script end; 729 | 7. 之后就该执行微任务队列了,首先打印出async1 end,然后打印出promise2; 730 | 8. 执行完微任务队列,就开始执行宏任务队列中的定时器,打印出setTimeout。 731 | 732 | ### 23. 代码输出结果 733 | 734 | ``` 735 | async function async1 () { 736 | await async2(); 737 | console.log('async1'); 738 | return 'async1 success' 739 | } 740 | async function async2 () { 741 | return new Promise((resolve, reject) => { 742 | console.log('async2') 743 | reject('error') 744 | }) 745 | } 746 | async1().then(res => console.log(res)) 747 | ``` 748 | 749 | 输出结果如下: 750 | 751 | ``` 752 | async2 753 | Uncaught (in promise) error 754 | ``` 755 | 756 | 可以看到,如果async函数中抛出了错误,就会终止错误结果,不会继续向下执行。 757 | 758 | 759 | 760 | 如果想要让错误不足之处后面的代码执行,可以使用catch来捕获: 761 | 762 | ``` 763 | async function async1 () { 764 | await Promise.reject('error!!!').catch(e => console.log(e)) 765 | console.log('async1'); 766 | return Promise.resolve('async1 success') 767 | } 768 | async1().then(res => console.log(res)) 769 | console.log('script start') 770 | ``` 771 | 772 | 这样的输出结果就是: 773 | 774 | ``` 775 | script start 776 | error!!! 777 | async1 778 | async1 success 779 | ``` 780 | 781 | ### 24. 代码输出结果 782 | 783 | ``` 784 | const first = () => (new Promise((resolve, reject) => { 785 | console.log(3); 786 | let p = new Promise((resolve, reject) => { 787 | console.log(7); 788 | setTimeout(() => { 789 | console.log(5); 790 | resolve(6); 791 | console.log(p) 792 | }, 0) 793 | resolve(1); 794 | }); 795 | resolve(2); 796 | p.then((arg) => { 797 | console.log(arg); 798 | }); 799 | })); 800 | first().then((arg) => { 801 | console.log(arg); 802 | }); 803 | console.log(4); 804 | ``` 805 | 806 | 输出结果如下: 807 | 808 | ``` 809 | 3 810 | 7 811 | 4 812 | 1 813 | 2 814 | 5 815 | Promise{: 1} 816 | ``` 817 | 818 | 代码的执行过程如下: 819 | 820 | 1. 首先会进入Promise,打印出3,之后进入下面的Promise,打印出7; 821 | 2. 遇到了定时器,将其加入宏任务队列; 822 | 3. 执行Promise p中的resolve,状态变为resolved,返回值为1; 823 | 4. 执行Promise first中的resolve,状态变为resolved,返回值为2; 824 | 5. 遇到p.then,将其加入微任务队列,遇到first().then,将其加入任务队列; 825 | 6. 执行外面的代码,打印出4; 826 | 7. 这样第一轮宏任务就执行完了,开始执行微任务队列中的任务,先后打印出1和2; 827 | 8. 这样微任务就执行完了,开始执行下一轮宏任务,宏任务队列中有一个定时器,执行它,打印出5,由于执行已经变为resolved状态,所以`resolve(6)`不会再执行; 828 | 9. 最后`console.log(p)`打印出`Promise{: 1}`; 829 | 830 | ### 25. 代码输出结果 831 | 832 | ``` 833 | const async1 = async () => { 834 | console.log('async1'); 835 | setTimeout(() => { 836 | console.log('timer1') 837 | }, 2000) 838 | await new Promise(resolve => { 839 | console.log('promise1') 840 | }) 841 | console.log('async1 end') 842 | return 'async1 success' 843 | } 844 | console.log('script start'); 845 | async1().then(res => console.log(res)); 846 | console.log('script end'); 847 | Promise.resolve(1) 848 | .then(2) 849 | .then(Promise.resolve(3)) 850 | .catch(4) 851 | .then(res => console.log(res)) 852 | setTimeout(() => { 853 | console.log('timer2') 854 | }, 1000) 855 | ``` 856 | 857 | 输出结果如下: 858 | 859 | ``` 860 | script start 861 | async1 862 | promise1 863 | script end 864 | 1 865 | timer2 866 | timer1 867 | ``` 868 | 869 | 代码的执行过程如下: 870 | 871 | 1. 首先执行同步带吗,打印出script start; 872 | 2. 遇到定时器timer1将其加入宏任务队列; 873 | 3. 之后是执行Promise,打印出promise1,由于Promise没有返回值,所以后面的代码不会执行; 874 | 4. 然后执行同步代码,打印出script end; 875 | 5. 继续执行下面的Promise,.then和.catch期望参数是一个函数,这里传入的是一个数字,因此就会发生值渗透,将resolve(1)的值传到最后一个then,直接打印出1; 876 | 6. 遇到第二个定时器,将其加入到微任务队列,执行微任务队列,按顺序依次执行两个定时器,但是由于定时器时间的原因,会在两秒后先打印出timer2,在四秒后打印出timer1。 877 | 878 | ### 26. 代码输出结果 879 | 880 | ``` 881 | const p1 = new Promise((resolve) => { 882 | setTimeout(() => { 883 | resolve('resolve3'); 884 | console.log('timer1') 885 | }, 0) 886 | resolve('resovle1'); 887 | resolve('resolve2'); 888 | }).then(res => { 889 | console.log(res) // resolve1 890 | setTimeout(() => { 891 | console.log(p1) 892 | }, 1000) 893 | }).finally(res => { 894 | console.log('finally', res) 895 | }) 896 | ``` 897 | 898 | 执行结果为如下: 899 | 900 | ``` 901 | resolve1 902 | finally undefined 903 | timer1 904 | Promise{: undefined} 905 | ``` 906 | 907 | ### 27. 代码输出结果 908 | 909 | ``` 910 | console.log('1'); 911 | 912 | setTimeout(function() { 913 | console.log('2'); 914 | process.nextTick(function() { 915 | console.log('3'); 916 | }) 917 | new Promise(function(resolve) { 918 | console.log('4'); 919 | resolve(); 920 | }).then(function() { 921 | console.log('5') 922 | }) 923 | }) 924 | process.nextTick(function() { 925 | console.log('6'); 926 | }) 927 | new Promise(function(resolve) { 928 | console.log('7'); 929 | resolve(); 930 | }).then(function() { 931 | console.log('8') 932 | }) 933 | 934 | setTimeout(function() { 935 | console.log('9'); 936 | process.nextTick(function() { 937 | console.log('10'); 938 | }) 939 | new Promise(function(resolve) { 940 | console.log('11'); 941 | resolve(); 942 | }).then(function() { 943 | console.log('12') 944 | }) 945 | }) 946 | ``` 947 | 948 | 输出结果如下: 949 | 950 | ``` 951 | 1 952 | 7 953 | 6 954 | 8 955 | 2 956 | 4 957 | 3 958 | 5 959 | 9 960 | 11 961 | 10 962 | 12 963 | ``` 964 | 965 | **(1)第一轮事件循环流程分析如下:** 966 | 967 | - 整体script作为第一个宏任务进入主线程,遇到`console.log`,输出1。 968 | - 遇到`setTimeout`,其回调函数被分发到宏任务Event Queue中。暂且记为`setTimeout1`。 969 | - 遇到`process.nextTick()`,其回调函数被分发到微任务Event Queue中。记为`process1`。 970 | - 遇到`Promise`,`new Promise`直接执行,输出7。`then`被分发到微任务Event Queue中。记为`then1`。 971 | - 又遇到了`setTimeout`,其回调函数被分发到宏任务Event Queue中,记为`setTimeout2`。 972 | 973 | | 宏任务Event Queue | 微任务Event Queue | 974 | | ----------------- | ----------------- | 975 | | setTimeout1 | process1 | 976 | | setTimeout2 | then1 | 977 | 978 | 上表是第一轮事件循环宏任务结束时各Event Queue的情况,此时已经输出了1和7。发现了`process1`和`then1`两个微任务: 979 | 980 | - 执行`process1`,输出6。 981 | - 执行`then1`,输出8。 982 | 983 | 第一轮事件循环正式结束,这一轮的结果是输出1,7,6,8。 984 | 985 | 986 | 987 | **(2)第二轮时间循环从**`**setTimeout1**`**宏任务开始:** 988 | 989 | - 首先输出2。接下来遇到了`process.nextTick()`,同样将其分发到微任务Event Queue中,记为`process2`。 990 | - `new Promise`立即执行输出4,`then`也分发到微任务Event Queue中,记为`then2`。 991 | 992 | | 宏任务Event Queue | 微任务Event Queue | 993 | | ----------------- | ----------------- | 994 | | setTimeout2 | process2 | 995 | | | then2 | 996 | 997 | 第二轮事件循环宏任务结束,发现有`process2`和`then2`两个微任务可以执行: 998 | 999 | - 输出3。 1000 | - 输出5。 1001 | 1002 | 第二轮事件循环结束,第二轮输出2,4,3,5。 1003 | 1004 | 1005 | 1006 | **(3)第三轮事件循环开始,此时只剩setTimeout2了,执行。** 1007 | 1008 | - 直接输出9。 1009 | - 将`process.nextTick()`分发到微任务Event Queue中。记为`process3`。 1010 | - 直接执行`new Promise`,输出11。 1011 | - 将`then`分发到微任务Event Queue中,记为`then3`。 1012 | 1013 | | 宏任务Event Queue | 微任务Event Queue | 1014 | | ----------------- | ----------------- | 1015 | | | process3 | 1016 | | | then3 | 1017 | 1018 | 第三轮事件循环宏任务执行结束,执行两个微任务`process3`和`then3`: 1019 | 1020 | - 输出10。 1021 | - 输出12。 1022 | 1023 | 第三轮事件循环结束,第三轮输出9,11,10,12。 1024 | 1025 | 1026 | 1027 | 整段代码,共进行了三次事件循环,完整的输出为1,7,6,8,2,4,3,5,9,11,10,12。 1028 | 1029 | ### 28. 代码输出结果 1030 | 1031 | ``` 1032 | console.log(1) 1033 | 1034 | setTimeout(() => { 1035 | console.log(2) 1036 | }) 1037 | 1038 | new Promise(resolve => { 1039 | console.log(3) 1040 | resolve(4) 1041 | }).then(d => console.log(d)) 1042 | 1043 | setTimeout(() => { 1044 | console.log(5) 1045 | new Promise(resolve => { 1046 | resolve(6) 1047 | }).then(d => console.log(d)) 1048 | }) 1049 | 1050 | setTimeout(() => { 1051 | console.log(7) 1052 | }) 1053 | 1054 | console.log(8) 1055 | ``` 1056 | 1057 | 输出结果如下: 1058 | 1059 | ``` 1060 | 1 1061 | 3 1062 | 8 1063 | 4 1064 | 2 1065 | 5 1066 | 6 1067 | 7 1068 | ``` 1069 | 1070 | 代码执行过程如下: 1071 | 1072 | 1. 首先执行script代码,打印出1; 1073 | 2. 遇到第一个定时器,加入到宏任务队列; 1074 | 3. 遇到Promise,执行代码,打印出3,遇到resolve,将其加入到微任务队列; 1075 | 4. 遇到第二个定时器,加入到宏任务队列; 1076 | 5. 遇到第三个定时器,加入到宏任务队列; 1077 | 6. 继续执行script代码,打印出8,第一轮执行结束; 1078 | 7. 执行微任务队列,打印出第一个Promise的resolve结果:4; 1079 | 8. 开始执行宏任务队列,执行第一个定时器,打印出2; 1080 | 9. 此时没有微任务,继续执行宏任务中的第二个定时器,首先打印出5,遇到Promise,首选打印出6,遇到resolve,将其加入到微任务队列; 1081 | 10. 执行微任务队列,打印出6; 1082 | 11. 执行宏任务队列中的最后一个定时器,打印出7。 1083 | 1084 | ### 29. 代码输出结果 1085 | 1086 | ``` 1087 | console.log(1); 1088 | 1089 | setTimeout(() => { 1090 | console.log(2); 1091 | Promise.resolve().then(() => { 1092 | console.log(3) 1093 | }); 1094 | }); 1095 | 1096 | new Promise((resolve, reject) => { 1097 | console.log(4) 1098 | resolve(5) 1099 | }).then((data) => { 1100 | console.log(data); 1101 | }) 1102 | 1103 | setTimeout(() => { 1104 | console.log(6); 1105 | }) 1106 | 1107 | console.log(7); 1108 | ``` 1109 | 1110 | 代码输出结果如下: 1111 | 1112 | ``` 1113 | 1 1114 | 4 1115 | 7 1116 | 5 1117 | 2 1118 | 3 1119 | 6 1120 | ``` 1121 | 1122 | 代码执行过程如下: 1123 | 1124 | 1. 首先执行scrip代码,打印出1; 1125 | 2. 遇到第一个定时器setTimeout,将其加入到宏任务队列; 1126 | 3. 遇到Promise,执行里面的同步代码,打印出4,遇到resolve,将其加入到微任务队列; 1127 | 4. 遇到第二个定时器setTimeout,将其加入到红任务队列; 1128 | 5. 执行script代码,打印出7,至此第一轮执行完成; 1129 | 6. 指定微任务队列中的代码,打印出resolve的结果:5; 1130 | 7. 执行宏任务中的第一个定时器setTimeout,首先打印出2,然后遇到 Promise.resolve().then(),将其加入到微任务队列; 1131 | 8. 执行完这个宏任务,就开始执行微任务队列,打印出3; 1132 | 9. 继续执行宏任务队列中的第二个定时器,打印出6。 1133 | 1134 | ### 30. 代码输出结果 1135 | 1136 | ``` 1137 | Promise.resolve().then(() => { 1138 | console.log('1'); 1139 | throw 'Error'; 1140 | }).then(() => { 1141 | console.log('2'); 1142 | }).catch(() => { 1143 | console.log('3'); 1144 | throw 'Error'; 1145 | }).then(() => { 1146 | console.log('4'); 1147 | }).catch(() => { 1148 | console.log('5'); 1149 | }).then(() => { 1150 | console.log('6'); 1151 | }); 1152 | ``` 1153 | 1154 | 执行结果如下: 1155 | 1156 | ``` 1157 | 1 1158 | 3 1159 | 5 1160 | 6 1161 | ``` 1162 | 1163 | 在这道题目中,我们需要知道,无论是thne还是catch中,只要throw 抛出了错误,就会被catch捕获,如果没有throw出错误,就被继续执行后面的then。 1164 | 1165 | ### 31. 代码输出结果 1166 | 1167 | ``` 1168 | setTimeout(function () { 1169 | console.log(1); 1170 | }, 100); 1171 | 1172 | new Promise(function (resolve) { 1173 | console.log(2); 1174 | resolve(); 1175 | console.log(3); 1176 | }).then(function () { 1177 | console.log(4); 1178 | new Promise((resove, reject) => { 1179 | console.log(5); 1180 | setTimeout(() => { 1181 | console.log(6); 1182 | }, 10); 1183 | }) 1184 | }); 1185 | console.log(7); 1186 | console.log(8); 1187 | ``` 1188 | 1189 | 输出结果为: 1190 | 1191 | ``` 1192 | 2 1193 | 3 1194 | 7 1195 | 8 1196 | 4 1197 | 5 1198 | 6 1199 | 1 1200 | ``` 1201 | 1202 | 代码执行过程如下: 1203 | 1204 | 1. 首先遇到定时器,将其加入到宏任务队列; 1205 | 2. 遇到Promise,首先执行里面的同步代码,打印出2,遇到resolve,将其加入到微任务队列,执行后面同步代码,打印出3; 1206 | 3. 继续执行script中的代码,打印出7和8,至此第一轮代码执行完成; 1207 | 4. 执行微任务队列中的代码,首先打印出4,如遇到Promise,执行其中的同步代码,打印出5,遇到定时器,将其加入到宏任务队列中,此时宏任务队列中有两个定时器; 1208 | 5. 执行宏任务队列中的代码,这里我们需要注意是的第一个定时器的时间为100ms,第二个定时器的时间为10ms,所以先执行第二个定时器,打印出6; 1209 | 6. 此时微任务队列为空,继续执行宏任务队列,打印出1。 1210 | 1211 | 1212 | 1213 | 做完这道题目,我们就需要格外注意,每个定时器的时间,并不是所有定时器的时间都为0哦。 1214 | 1215 | ## 二、this 1216 | 1217 | ### 1. 代码输出结果 1218 | 1219 | ``` 1220 | function foo() { 1221 | console.log( this.a ); 1222 | } 1223 | 1224 | function doFoo() { 1225 | foo(); 1226 | } 1227 | 1228 | var obj = { 1229 | a: 1, 1230 | doFoo: doFoo 1231 | }; 1232 | 1233 | var a = 2; 1234 | obj.doFoo() 1235 | ``` 1236 | 1237 | 输出结果:2 1238 | 1239 | 1240 | 1241 | 在Javascript中,this指向函数执行时的当前对象。在执行foo的时候,执行环境就是doFoo函数,执行环境为全局。所以,foo中的this是指向window的,所以会打印出2。 1242 | 1243 | ### 2. 代码输出结果 1244 | 1245 | ``` 1246 | var a = 10 1247 | var obj = { 1248 | a: 20, 1249 | say: () => { 1250 | console.log(this.a) 1251 | } 1252 | } 1253 | obj.say() 1254 | 1255 | var anotherObj = { a: 30 } 1256 | obj.say.apply(anotherObj) 1257 | ``` 1258 | 1259 | 输出结果:10 10 1260 | 1261 | 1262 | 1263 | 我么知道,箭头函数时不绑定this的,它的this来自原其父级所处的上下文,所以首先会打印全局中的 a 的值10。后面虽然让say方法指向了另外一个对象,但是仍不能改变箭头函数的特性,它的this仍然是指向全局的,所以依旧会输出10。 1264 | 1265 | 1266 | 1267 | 但是,如果是普通函数,那么就会有完全不一样的结果: 1268 | 1269 | ``` 1270 | var a = 10 1271 | var obj = { 1272 | a: 20, 1273 | say(){ 1274 | console.log(this.a) 1275 | } 1276 | } 1277 | obj.say() 1278 | var anotherObj={a:30} 1279 | obj.say.apply(anotherObj) 1280 | ``` 1281 | 1282 | 输出结果:20 30 1283 | 1284 | 1285 | 1286 | 这时,say方法中的this就会指向他所在的对象,输出其中的a的值。 1287 | 1288 | ### 3. 代码输出结果 1289 | 1290 | ``` 1291 | function a() { 1292 | console.log(this); 1293 | } 1294 | a.call(null); 1295 | ``` 1296 | 1297 | 打印结果:window对象 1298 | 1299 | 1300 | 1301 | 根据ECMAScript262规范规定:如果第一个参数传入的对象调用者是null或者undefined,call方法将把全局对象(浏览器上是window对象)作为this的值。所以,不管传入null 还是 undefined,其this都是全局对象window。所以,在浏览器上答案是输出 window 对象。 1302 | 1303 | 1304 | 1305 | 要注意的是,在严格模式中,null 就是 null,undefined 就是 undefined: 1306 | 1307 | ``` 1308 | 'use strict'; 1309 | 1310 | function a() { 1311 | console.log(this); 1312 | } 1313 | a.call(null); // null 1314 | a.call(undefined); // undefined 1315 | ``` 1316 | 1317 | ### 4. 代码输出结果 1318 | 1319 | ``` 1320 | var obj = { 1321 | name: 'cuggz', 1322 | fun: function(){ 1323 | console.log(this.name); 1324 | } 1325 | } 1326 | obj.fun() // cuggz 1327 | new obj.fun() // undefined 1328 | ``` 1329 | 1330 | ### 6. 代码输出结果 1331 | 1332 | ``` 1333 | var obj = { 1334 | say: function() { 1335 | var f1 = () => { 1336 | console.log("1111", this); 1337 | } 1338 | f1(); 1339 | }, 1340 | pro: { 1341 | getPro:() => { 1342 | console.log(this); 1343 | } 1344 | } 1345 | } 1346 | var o = obj.say; 1347 | o(); 1348 | obj.say(); 1349 | obj.pro.getPro(); 1350 | ``` 1351 | 1352 | ``` 1353 | var obj = { 1354 | say: function() { 1355 | var f1 = () => { 1356 | console.log("1111", this); 1357 | } 1358 | f1(); 1359 | }, 1360 | pro: { 1361 | getPro:() => { 1362 | console.log(this); 1363 | } 1364 | } 1365 | } 1366 | var o = obj.say; 1367 | o(); 1368 | obj.say(); 1369 | obj.pro.getPro(); 1370 | ``` 1371 | 1372 | 输出结果: 1373 | 1374 | ``` 1375 | 1111 window对象 1376 | 1111 obj对象 1377 | window对象 1378 | ``` 1379 | 1380 | **解析:** 1381 | 1382 | 1. o(),o是在全局执行的,而f1是箭头函数,它是没有绑定this的,它的this指向其父级的this,其父级say方法的this指向的是全局作用域,所以会打印出window; 1383 | 2. obj.say(),谁调用say,say 的this就指向谁,所以此时this指向的是obj对象; 1384 | 3. obj.pro.getPro(),我们知道,箭头函数时不绑定this的,getPro处于pro中,而对象不构成单独的作用域,所以箭头的函数的this就指向了全局作用域window。 1385 | 1386 | ### 7. 代码输出结果 1387 | 1388 | ``` 1389 | var myObject = { 1390 | foo: "bar", 1391 | func: function() { 1392 | var self = this; 1393 | console.log(this.foo); 1394 | console.log(self.foo); 1395 | (function() { 1396 | console.log(this.foo); 1397 | console.log(self.foo); 1398 | }()); 1399 | } 1400 | }; 1401 | myObject.func(); 1402 | ``` 1403 | 1404 | 输出结果:bar bar undefined bar 1405 | 1406 | 1407 | 1408 | **解析:** 1409 | 1410 | 1. 首先func是由myObject调用的,this指向myObject。又因为var self = this;所以self指向myObject。 1411 | 2. 这个立即执行匿名函数表达式是由window调用的,this指向window 。立即执行匿名函数的作用域处于myObject.func的作用域中,在这个作用域找不到self变量,沿着作用域链向上查找self变量,找到了指向 myObject对象的self。 1412 | 1413 | ### 8. 代码输出问题 1414 | 1415 | ``` 1416 | window.number = 2; 1417 | var obj = { 1418 | number: 3, 1419 | db1: (function(){ 1420 | console.log(this); 1421 | this.number *= 4; 1422 | return function(){ 1423 | console.log(this); 1424 | this.number *= 5; 1425 | } 1426 | })() 1427 | } 1428 | var db1 = obj.db1; 1429 | db1(); 1430 | obj.db1(); 1431 | console.log(obj.number); // 15 1432 | console.log(window.number); // 40 1433 | ``` 1434 | 1435 | 这道题目看清起来有点乱,但是实际上是考察this指向的: 1436 | 1437 | 1. 执行db1()时,this指向全局作用域,所以window.number * 4 = 8,然后执行匿名函数, 所以window.number * 5 = 40; 1438 | 2. 执行obj.db1();时,this指向obj对象,执行匿名函数,所以obj.numer * 5 = 15。 1439 | 1440 | ### 9. 代码输出结果 1441 | 1442 | ``` 1443 | var length = 10; 1444 | function fn() { 1445 | console.log(this.length); 1446 | } 1447 | 1448 | var obj = { 1449 | length: 5, 1450 | method: function(fn) { 1451 | fn(); 1452 | arguments[0](); 1453 | } 1454 | }; 1455 | 1456 | obj.method(fn, 1); 1457 | ``` 1458 | 1459 | 输出结果: 10 2 1460 | 1461 | 1462 | 1463 | **解析:** 1464 | 1465 | 1. 第一次执行fn(),this指向window对象,输出10。 1466 | 2. 第二次执行arguments[0](),相当于arguments调用方法,this指向arguments,而这里传了两个参数,故输出arguments长度为2。 1467 | 1468 | ### 10. 代码输出结果 1469 | 1470 | ``` 1471 | var a = 1; 1472 | function printA(){ 1473 | console.log(this.a); 1474 | } 1475 | var obj={ 1476 | a:2, 1477 | foo:printA, 1478 | bar:function(){ 1479 | printA(); 1480 | } 1481 | } 1482 | 1483 | obj.foo(); // 2 1484 | obj.bar(); // 1 1485 | var foo = obj.foo; 1486 | foo(); // 1 1487 | ``` 1488 | 1489 | 输出结果: 2 1 1 1490 | 1491 | 1492 | 1493 | **解析:** 1494 | 1495 | 1. obj.foo(),foo 的this指向obj对象,所以a会输出2; 1496 | 2. obj.bar(),printA在bar方法中执行,所以此时printA的this指向的是window,所以会输出1; 1497 | 3. foo(),foo是在全局对象中执行的,所以其this指向的是window,所以会输出1; 1498 | 1499 | ### 11. 代码输出结果 1500 | 1501 | ``` 1502 | var x = 3; 1503 | var y = 4; 1504 | var obj = { 1505 | x: 1, 1506 | y: 6, 1507 | getX: function() { 1508 | var x = 5; 1509 | return function() { 1510 | return this.x; 1511 | }(); 1512 | }, 1513 | getY: function() { 1514 | var y = 7; 1515 | return this.y; 1516 | } 1517 | } 1518 | console.log(obj.getX()) // 3 1519 | console.log(obj.getY()) // 6 1520 | ``` 1521 | 1522 | 输出结果:3 6 1523 | 1524 | 1525 | 1526 | **解析:** 1527 | 1528 | 1. 我们知道,匿名函数的this是指向全局对象的,所以this指向window,会打印出3; 1529 | 2. getY是由obj调用的,所以其this指向的是obj对象,会打印出6。 1530 | 1531 | ### 12. 代码输出结果 1532 | 1533 | ``` 1534 | var a = 10; 1535 | var obt = { 1536 | a: 20, 1537 | fn: function(){ 1538 | var a = 30; 1539 | console.log(this.a) 1540 | } 1541 | } 1542 | obt.fn(); // 20 1543 | obt.fn.call(); // 10 1544 | (obt.fn)(); // 20 1545 | ``` 1546 | 1547 | 输出结果: 20 10 20 1548 | 1549 | 1550 | 1551 | **解析:** 1552 | 1553 | 1. obt.fn(),fn是由obt调用的,所以其this指向obt对象,会打印出20; 1554 | 2. obt.fn.call(),这里call的参数啥都没写,就表示null,我们知道如果call的参数为undefined或null,那么this就会指向全局对象this,所以会打印出 10; 1555 | 3. (obt.fn)(), 这里给表达式加了括号,而括号的作用是改变表达式的运算顺序,而在这里加与不加括号并无影响;相当于 obt.fn(),所以会打印出 20; 1556 | 1557 | ### 13. 代码输出结果 1558 | 1559 | ``` 1560 | function a(xx){ 1561 | this.x = xx; 1562 | return this 1563 | }; 1564 | var x = a(5); 1565 | var y = a(6); 1566 | 1567 | console.log(x.x) // undefined 1568 | console.log(y.x) // 6 1569 | ``` 1570 | 1571 | 输出结果: undefined 6 1572 | 1573 | 1574 | 1575 | **解析:** 1576 | 1577 | 1. 最关键的就是var x = a(5),函数a是在全局作用域调用,所以函数内部的this指向window对象。**所以 this.x = 5 就相当于:window.x = 5。**之后 return this,也就是说 var x = a(5) 中的x变量的值是window,这里的x将函数内部的x的值覆盖了。然后执行console.log(x.x), 也就是console.log(window.x),而window对象中没有x属性,所以会输出undefined。 1578 | 2. 当指向y.x时,会给全局变量中的x赋值为6,所以会打印出6。 1579 | 1580 | ### 14. 代码输出结果 1581 | 1582 | ``` 1583 | function foo(something){ 1584 | this.a = something 1585 | } 1586 | 1587 | var obj1 = { 1588 | foo: foo 1589 | } 1590 | 1591 | var obj2 = {} 1592 | 1593 | obj1.foo(2); 1594 | console.log(obj1.a); // 2 1595 | 1596 | obj1.foo.call(obj2, 3); 1597 | console.log(obj2.a); // 3 1598 | 1599 | var bar = new obj1.foo(4) 1600 | console.log(obj1.a); // 2 1601 | console.log(bar.a); // 4 1602 | ``` 1603 | 1604 | 输出结果: 2 3 2 4 1605 | 1606 | 1607 | 1608 | **解析:** 1609 | 1610 | 1. 首先执行obj1.foo(2); 会在obj中添加a属性,其值为2。之后执行obj1.a,a是右obj1调用的,所以this指向obj,打印出2; 1611 | 2. 执行 obj1.foo.call(obj2, 3) 时,会将foo的this指向obj2,后面就和上面一样了,所以会打印出3; 1612 | 3. obj1.a会打印出2; 1613 | 4. 最后就是考察this绑定的优先级了,new 绑定是比隐式绑定优先级高,所以会输出4。 1614 | 1615 | ### 15. 代码输出结果 1616 | 1617 | ``` 1618 | function foo(something){ 1619 | this.a = something 1620 | } 1621 | 1622 | var obj1 = {} 1623 | 1624 | var bar = foo.bind(obj1); 1625 | bar(2); 1626 | console.log(obj1.a); // 2 1627 | 1628 | var baz = new bar(3); 1629 | console.log(obj1.a); // 2 1630 | console.log(baz.a); // 3 1631 | ``` 1632 | 1633 | 输出结果: 2 2 3 1634 | 1635 | 1636 | 1637 | 这道题目和上面题目差不多,主要都是考察this绑定的优先级。记住以下结论即可:**this绑定的优先级:****new绑定 > 显式绑定 > 隐式绑定 > 默认绑定。** 1638 | 1639 | ## 三、作用域&变量提升&闭包 1640 | 1641 | ### 1. 代码输出结果 1642 | 1643 | ``` 1644 | (function(){ 1645 | var x = y = 1; 1646 | })(); 1647 | var z; 1648 | 1649 | console.log(y); // 1 1650 | console.log(z); // undefined 1651 | console.log(x); // Uncaught ReferenceError: x is not defined 1652 | ``` 1653 | 1654 | 这段代码的关键在于:var x = y = 1; 实际上这里是从右往左执行的,首先执行y = 1, 因为y没有使用var声明,所以它是一个全局变量,然后第二步是将y赋值给x,讲一个全局变量赋值给了一个局部变量,最终,x是一个局部变量,y是一个全局变量,所以打印x是报错。 1655 | 1656 | ### 2. 代码输出结果 1657 | 1658 | ``` 1659 | var a, b 1660 | (function () { 1661 | console.log(a); 1662 | console.log(b); 1663 | var a = (b = 3); 1664 | console.log(a); 1665 | console.log(b); 1666 | })() 1667 | console.log(a); 1668 | console.log(b); 1669 | ``` 1670 | 1671 | 输出结果: 1672 | 1673 | ``` 1674 | undefined 1675 | undefined 1676 | 3 1677 | 3 1678 | undefined 1679 | 3 1680 | ``` 1681 | 1682 | ``` 1683 | undefined 1684 | undefined 1685 | 3 1686 | 3 1687 | undefined 1688 | 3 1689 | ``` 1690 | 1691 | 这个题目和上面题目考察的知识点类似,b赋值为3,b此时是一个全局变量,而将3赋值给a,a是一个局部变量,所以最后打印的时候,a仍旧是undefined。 1692 | 1693 | ### 3. 代码输出结果 1694 | 1695 | ``` 1696 | var friendName = 'World'; 1697 | (function() { 1698 | if (typeof friendName === 'undefined') { 1699 | var friendName = 'Jack'; 1700 | console.log('Goodbye ' + friendName); 1701 | } else { 1702 | console.log('Hello ' + friendName); 1703 | } 1704 | })(); 1705 | ``` 1706 | 1707 | 输出结果:Goodbye Jack 1708 | 1709 | 1710 | 1711 | 我们知道,在 JavaScript中, Function 和 var 都会被提升(变量提升),所以上面的代码就相当于: 1712 | 1713 | ``` 1714 | var name = 'World!'; 1715 | (function () { 1716 | var name; 1717 | if (typeof name === 'undefined') { 1718 | name = 'Jack'; 1719 | console.log('Goodbye ' + name); 1720 | } else { 1721 | console.log('Hello ' + name); 1722 | } 1723 | })(); 1724 | ``` 1725 | 1726 | 这样,答案就一目了然了。 1727 | 1728 | ### 4. 代码输出结果 1729 | 1730 | ``` 1731 | function fn1(){ 1732 | console.log('fn1') 1733 | } 1734 | var fn2 1735 | 1736 | fn1() 1737 | fn2() 1738 | 1739 | fn2 = function() { 1740 | console.log('fn2') 1741 | } 1742 | 1743 | fn2() 1744 | ``` 1745 | 1746 | 输出结果: 1747 | 1748 | ``` 1749 | fn1 1750 | Uncaught TypeError: fn2 is not a function 1751 | fn2 1752 | ``` 1753 | 1754 | 这里也是在考察变量提升,关键在于第一个fn2(),这时fn2仍是一个undefined的变量,所以会报错fn2不是一个函数。 1755 | 1756 | ### 5. 代码输出结果 1757 | 1758 | ``` 1759 | function a() { 1760 | var temp = 10; 1761 | function b() { 1762 | console.log(temp); // 10 1763 | } 1764 | b(); 1765 | } 1766 | a(); 1767 | 1768 | function a() { 1769 | var temp = 10; 1770 | b(); 1771 | } 1772 | function b() { 1773 | console.log(temp); // 报错 Uncaught ReferenceError: temp is not defined 1774 | } 1775 | a(); 1776 | ``` 1777 | 1778 | 在上面的两段代码中,第一段是可以正常输出,这个应该没啥问题,关键在于第二段代码,它会报错Uncaught ReferenceError: temp is not defined。这时因为在b方法执行时,temp 的值为undefined。 1779 | 1780 | ### 6. 代码输出结果 1781 | 1782 | ``` 1783 | var a=3; 1784 | function c(){ 1785 | alert(a); 1786 | } 1787 | (function(){ 1788 | var a=4; 1789 | c(); 1790 | })(); 1791 | ``` 1792 | 1793 | js中变量的作用域链与定义时的环境有关,与执行时无关。执行环境只会改变this、传递的参数、全局变量等 1794 | 1795 | ### 7. 代码输出问题 1796 | 1797 | ``` 1798 | function fun(n, o) { 1799 | console.log(o) 1800 | return { 1801 | fun: function(m){ 1802 | return fun(m, n); 1803 | } 1804 | }; 1805 | } 1806 | var a = fun(0); a.fun(1); a.fun(2); a.fun(3); 1807 | var b = fun(0).fun(1).fun(2).fun(3); 1808 | var c = fun(0).fun(1); c.fun(2); c.fun(3); 1809 | ``` 1810 | 1811 | 输出结果: 1812 | 1813 | ``` 1814 | undefined 0 0 0 1815 | undefined 0 1 2 1816 | undefined 0 1 1 1817 | ``` 1818 | 1819 | 这是一道关于闭包的题目,对于fun方法,调用之后返回的是一个对象。我们知道,当调用函数的时候传入的实参比函数声明时指定的形参个数要少,剩下的形参都将设置为undefined值。所以 `console.log(o);` 会输出undefined。而a就是是fun(0)返回的那个对象。也就是说,函数fun中参数 n 的值是0,而返回的那个对象中,需要一个参数n,而这个对象的作用域中没有n,它就继续沿着作用域向上一级的作用域中寻找n,最后在函数fun中找到了n,n的值是0。了解了这一点,其他运算就很简单了,以此类推。 1820 | 1821 | ### 8. 代码输出结果 1822 | 1823 | ``` 1824 | f = function() {return true;}; 1825 | g = function() {return false;}; 1826 | (function() { 1827 | if (g() && [] == ![]) { 1828 | f = function f() {return false;}; 1829 | function g() {return true;} 1830 | } 1831 | })(); 1832 | console.log(f()); 1833 | ``` 1834 | 1835 | 输出结果: false 1836 | 1837 | 1838 | 1839 | 这里首先定义了两个变量f和g,我们知道变量是可以重新赋值的。后面是一个匿名自执行函数,在 if 条件中调用了函数 g(),由于在匿名函数中,又重新定义了函数g,就覆盖了外部定义的变量g,所以,这里调用的是内部函数 g 方法,返回为 true。第一个条件通过,进入第二个条件。 1840 | 1841 | 1842 | 1843 | 第二个条件是[] == ![],先看 ![] ,在 JavaScript 中,当用于布尔运算时,比如在这里,对象的非空引用被视为 true,空引用 null 则被视为 false。由于这里不是一个 null, 而是一个没有元素的数组,所以 [] 被视为 true, 而 ![] 的结果就是 false 了。当一个布尔值参与到条件运算的时候,true 会被看作 1, 而 false 会被看作 0。现在条件变成了 [] == 0 的问题了,当一个对象参与条件比较的时候,它会被求值,求值的结果是数组成为一个字符串,[] 的结果就是 '' ,而 '' 会被当作 0 ,所以,条件成立。 1844 | 1845 | 1846 | 1847 | 两个条件都成立,所以会执行条件中的代码, f 在定义是没有使用var,所以他是一个全局变量。因此,这里会通过闭包访问到外部的变量 f, 重新赋值,现在执行 f 函数返回值已经成为 false 了。而 g 则不会有这个问题,这里是一个函数内定义的 g,不会影响到外部的 g 函数。所以最后的结果就是 false。 1848 | 1849 | ## 四、原型&继承 1850 | 1851 | ### 1. 代码输出结果 1852 | 1853 | ``` 1854 | function Person(name) { 1855 | this.name = name 1856 | } 1857 | var p2 = new Person('king'); 1858 | console.log(p2.__proto__) //Person.prototype 1859 | console.log(p2.__proto__.__proto__) //Object.prototype 1860 | console.log(p2.__proto__.__proto__.__proto__) // null 1861 | console.log(p2.__proto__.__proto__.__proto__.__proto__)//null后面没有了,报错 1862 | console.log(p2.__proto__.__proto__.__proto__.__proto__.__proto__)//null后面没有了,报错 1863 | console.log(p2.constructor)//Person 1864 | console.log(p2.prototype)//undefined p2是实例,没有prototype属性 1865 | console.log(Person.constructor)//Function 一个空函数 1866 | console.log(Person.prototype)//打印出Person.prototype这个对象里所有的方法和属性 1867 | console.log(Person.prototype.constructor)//Person 1868 | console.log(Person.prototype.__proto__)// Object.prototype 1869 | console.log(Person.__proto__) //Function.prototype 1870 | console.log(Function.prototype.__proto__)//Object.prototype 1871 | console.log(Function.__proto__)//Function.prototype 1872 | console.log(Object.__proto__)//Function.prototype 1873 | console.log(Object.prototype.__proto__)//null 1874 | ``` 1875 | 1876 | 这道义题目考察原型、原型链的基础,记住就可以了。 1877 | 1878 | ### 2. 代码输出结果 1879 | 1880 | ``` 1881 | // a 1882 | function Foo () { 1883 | getName = function () { 1884 | console.log(1); 1885 | } 1886 | return this; 1887 | } 1888 | // b 1889 | Foo.getName = function () { 1890 | console.log(2); 1891 | } 1892 | // c 1893 | Foo.prototype.getName = function () { 1894 | console.log(3); 1895 | } 1896 | // d 1897 | var getName = function () { 1898 | console.log(4); 1899 | } 1900 | // e 1901 | function getName () { 1902 | console.log(5); 1903 | } 1904 | 1905 | Foo.getName(); // 2 1906 | getName(); // 4 1907 | Foo().getName(); // 1 1908 | getName(); // 1 1909 | new Foo.getName(); // 2 1910 | new Foo().getName(); // 3 1911 | new new Foo().getName(); // 3 1912 | ``` 1913 | 1914 | 输出结果:2 4 1 1 2 3 3 1915 | 1916 | 1917 | 1918 | **解析:** 1919 | 1920 | 1. **Foo.getName(),**Foo为一个函数对象,对象都可以有属性,b 处定义Foo的getName属性为函数,输出2; 1921 | 2. **getName(),**这里看d、e处,d为函数表达式,e为函数声明,两者区别在于变量提升,函数声明的 5 会被后边函数表达式的 4 覆盖; 1922 | 3. **Foo().getName(),**这里要看a处,在Foo内部将全局的getName重新赋值为 console.log(1) 的函数,执行Foo()返回 this,这个this指向window,Foo().getName() 即为window.getName(),输出 1; 1923 | 4. **getName(),**上面3中,全局的getName已经被重新赋值,所以这里依然输出 1; 1924 | 5. **new Foo.getName(),**这里等价于 new (Foo.getName()),先执行 Foo.getName(),输出 2,然后new一个实例; 1925 | 6. **new Foo().getName(),**这里等价于 (new Foo()).getName(), 先new一个Foo的实例,再执行这个实例的getName方法,但是这个实例本身没有这个方法,所以去原型链__protot__上边找,实例.__protot__ === Foo.prototype,所以输出 3; 1926 | 7. **new new Foo().getName(),**这里等价于new (new Foo().getName()),如上述6,先输出 3,然后new 一个 new Foo().getName() 的实例。 1927 | 1928 | ### 3. 代码输出结果 1929 | 1930 | ``` 1931 | var F = function() {}; 1932 | Object.prototype.a = function() { 1933 | console.log('a'); 1934 | }; 1935 | Function.prototype.b = function() { 1936 | console.log('b'); 1937 | } 1938 | var f = new F(); 1939 | f.a(); 1940 | f.b(); 1941 | F.a(); 1942 | F.b() 1943 | ``` 1944 | 1945 | 输出结果: 1946 | 1947 | ``` 1948 | a 1949 | Uncaught TypeError: f.b is not a function 1950 | a 1951 | b 1952 | ``` 1953 | 1954 | **解析:** 1955 | 1956 | 1. f 并不是 Function 的实例,因为它本来就不是构造函数,调用的是 Function 原型链上的相关属性和方法,只能访问到 Object 原型链。所以 f.a() 输出 a ,而 f.b() 就报错了。 1957 | 2. F 是个构造函数,而 F 是构造函数 Function 的一个实例。因为 F instanceof Object === true,F instanceof Function === true,由此可以得出结论:F 是 Object 和 Function 两个的实例,即 F 能访问到 a, 也能访问到 b。所以 F.a() 输出 a ,F.b() 输出 b。 1958 | 1959 | ### 4. 代码输出结果 1960 | 1961 | ``` 1962 | function Foo(){ 1963 | Foo.a = function(){ 1964 | console.log(1); 1965 | } 1966 | this.a = function(){ 1967 | console.log(2) 1968 | } 1969 | } 1970 | 1971 | Foo.prototype.a = function(){ 1972 | console.log(3); 1973 | } 1974 | 1975 | Foo.a = function(){ 1976 | console.log(4); 1977 | } 1978 | 1979 | Foo.a(); 1980 | let obj = new Foo(); 1981 | obj.a(); 1982 | Foo.a(); 1983 | ``` 1984 | 1985 | 输出结果:4 2 1 1986 | 1987 | 1988 | 1989 | **解析:** 1990 | 1991 | 1. Foo.a() 这个是调用 Foo 函数的静态方法 a,虽然 Foo 中有优先级更高的属性方法 a,但 Foo 此时没有被调用,所以此时输出 Foo 的静态方法 a 的结果:4 1992 | 2. let obj = new Foo(); 使用了 new 方法调用了函数,返回了函数实例对象,此时 Foo 函数内部的属性方法初始化,原型链建立。 1993 | 3. obj.a() ; 调用 obj 实例上的方法 a,该实例上目前有两个 a 方法:一个是内部属性方法,另一个是原型上的方法。当这两者都存在时,首先查找 ownProperty ,如果没有才去原型链上找,所以调用实例上的 a 输出:2 1994 | 4. Foo.a() ; 根据第2步可知 Foo 函数内部的属性方法已初始化,覆盖了同名的静态方法,所以输出:1 1995 | 1996 | ### 5. 代码输出结果 1997 | 1998 | ``` 1999 | function Dog() { 2000 | this.name = 'puppy' 2001 | } 2002 | Dog.prototype.bark = () => { 2003 | console.log('woof!woof!') 2004 | } 2005 | const dog = new Dog() 2006 | console.log(Dog.prototype.constructor === Dog && dog.constructor === Dog && dog instanceof Dog) 2007 | ``` 2008 | 2009 | 输出结果:true 2010 | 2011 | 2012 | 2013 | **解析:** 2014 | 2015 | 因为constructor是prototype上的属性,所以dog.constructor实际上就是指向Dog.prototype.constructor;constructor属性指向构造函数。instanceof而实际检测的是类型是否在实例的原型链上。 2016 | 2017 | 2018 | 2019 | constructor是prototype上的属性,这一点很容易被忽略掉。constructor和instanceof 的作用是不同的,感性地来说,constructor的限制比较严格,它只能严格对比对象的构造函数是不是指定的值;而instanceof比较松散,只要检测的类型在原型链上,就会返回true。 2020 | 2021 | ### 6. 代码输出结果 2022 | 2023 | ``` 2024 | var A = {n: 4399}; 2025 | var B = function(){this.n = 9999}; 2026 | var C = function(){var n = 8888}; 2027 | B.prototype = A; 2028 | C.prototype = A; 2029 | var b = new B(); 2030 | var c = new C(); 2031 | A.n++ 2032 | console.log(b.n); 2033 | console.log(c.n); 2034 | ``` 2035 | 2036 | 输出结果:9999 4400 2037 | 2038 | 2039 | 2040 | **解析:** 2041 | 2042 | 1. console.log(b.n),在查找b.n是首先查找 b 对象自身有没有 n 属性,如果没有会去原型(prototype)上查找,当执行var b = new B()时,函数内部this.n=9999(此时this指向 b) 返回b对象,b对象有自身的n属性,所以返回 9999。 2043 | 2. console.log(c.n),同理,当执行var c = new C()时,c对象没有自身的n属性,向上查找,找到原型 (prototype)上的 n 属性,因为 A.n++(此时对象A中的n为4400), 所以返回4400。 2044 | 2045 | ### 7. 代码输出问题 2046 | 2047 | ``` 2048 | function A(){ 2049 | } 2050 | function B(a){ 2051 |   this.a = a; 2052 | } 2053 | function C(a){ 2054 |   if(a){ 2055 | this.a = a; 2056 |   } 2057 | } 2058 | A.prototype.a = 1; 2059 | B.prototype.a = 1; 2060 | C.prototype.a = 1; 2061 | 2062 | console.log(new A().a); 2063 | console.log(new B().a); 2064 | console.log(new C(2).a); 2065 | ``` 2066 | 2067 | 输出结果:1 undefined 2 2068 | 2069 | 2070 | 2071 | **解析:** 2072 | 2073 | 1. console.log(new A().a),new A()为构造函数创建的对象,本身没有a属性,所以向它的原型去找,发现原型的a属性的属性值为1,故该输出值为1; 2074 | 2. console.log(new B().a),ew B()为构造函数创建的对象,该构造函数有参数a,但该对象没有传参,故该输出值为undefined; 2075 | 3. console.log(new C(2).a),new C()为构造函数创建的对象,该构造函数有参数a,且传的实参为2,执行函数内部,发现if为真,执行this.a = 2,故属性a的值为2。 2076 | 2077 | ### 8 代码输出问题 2078 | 2079 | ``` 2080 | function Parent() { 2081 | this.a = 1; 2082 | this.b = [1, 2, this.a]; 2083 | this.c = { demo: 5 }; 2084 | this.show = function () { 2085 | console.log(this.a , this.b , this.c.demo ); 2086 | } 2087 | } 2088 | 2089 | function Child() { 2090 | this.a = 2; 2091 | this.change = function () { 2092 | this.b.push(this.a); 2093 | this.a = this.b.length; 2094 | this.c.demo = this.a++; 2095 | } 2096 | } 2097 | 2098 | Child.prototype = new Parent(); 2099 | var parent = new Parent(); 2100 | var child1 = new Child(); 2101 | var child2 = new Child(); 2102 | child1.a = 11; 2103 | child2.a = 12; 2104 | parent.show(); 2105 | child1.show(); 2106 | child2.show(); 2107 | child1.change(); 2108 | child2.change(); 2109 | parent.show(); 2110 | child1.show(); 2111 | child2.show(); 2112 | ``` 2113 | 2114 | 输出结果: 2115 | 2116 | ``` 2117 | parent.show(); // 1 [1,2,1] 5 2118 | 2119 | child1.show(); // 11 [1,2,1] 5 2120 | child2.show(); // 12 [1,2,1] 5 2121 | 2122 | parent.show(); // 1 [1,2,1] 5 2123 | 2124 | child1.show(); // 5 [1,2,1,11,12] 5 2125 | 2126 | child2.show(); // 6 [1,2,1,11,12] 5 2127 | ``` 2128 | 2129 | 这道题目值得神帝,他涉及到的知识点很多,例如**this的指向、原型、原型链、类的继承、数据类型**等。 2130 | 2131 | 2132 | 2133 | **解析****:** 2134 | 2135 | 1. parent.show(),可以直接获得所需的值,没啥好说的; 2136 | 2. child1.show(),`Child`的构造函数原本是指向`Child`的,题目显式将`Child`类的原型对象指向了`Parent`类的一个实例,需要注意`Child.prototype`指向的是`Parent`的实例`parent`,而不是指向`Parent`这个类。 2137 | 3. child2.show(),这个也没啥好说的; 2138 | 4. parent.show(),`parent`是一个`Parent`类的实例,`Child.prorotype`指向的是`Parent`类的另一个实例,两者在堆内存中互不影响,所以上述操作不影响`parent`实例,所以输出结果不变; 2139 | 5. child1.show(),`child1`执行了`change()`方法后,发生了怎样的变化呢? 2140 | 2141 | - **this.b.push(this.a),**由于this的动态指向特性,this.b会指向`Child.prototype`上的**b**数组,this.a会指向`child1`的**a**属性,所以`Child.prototype.b`变成了**[1,2,1,11]**; 2142 | - **this.a = this.b.length,**这条语句中`this.a`和`this.b`的指向与上一句一致,故结果为`child1.a`变为**4**; 2143 | - **this.c.demo = this.a++,**由于`child1`自身属性并没有**c**这个属性,所以此处的`this.c`会指向`Child.prototype.c`,`this.a`值为**4**,为原始类型,故赋值操作时会直接赋值,`Child.prototype.c.demo`的结果为**4**,而`this.a`随后自增为**5(4 + 1 = 5)。** 2144 | 2145 | 1. `child2`执行了`change()`方法, 而`child2`和`child1`均是`Child`类的实例,所以他们的原型链指向同一个原型对象`Child.prototype`,也就是同一个`parent`实例,所以`child2.change()`中所有影响到原型对象的语句都会影响`child1`的最终输出结果。 2146 | 2147 | - **this.b.push(this.a),**由于this的动态指向特性,this.b会指向`Child.prototype`上的**b**数组,this.a会指向`child2`的**a**属性,所以`Child.prototype.b`变成了**[1,2,1,11,12]**; 2148 | - **this.a = this.b.length,**这条语句中`this.a`和`this.b`的指向与上一句一致,故结果为`child2.a`变为**5**; 2149 | - **this.c.demo = this.a++,**由于`child2`自身属性并没有**c**这个属性,所以此处的`this.c`会指向`Child.prototype.c`,故执行结果为`Child.prototype.c.demo`的值变为`child2.a`的值**5**,而`child2.a`最终自增为**6(5 + 1 = 6)。** 2150 | 2151 | ### 9. 代码输出结果 2152 | 2153 | ``` 2154 | function SuperType(){ 2155 | this.property = true; 2156 | } 2157 | 2158 | SuperType.prototype.getSuperValue = function(){ 2159 | return this.property; 2160 | }; 2161 | 2162 | function SubType(){ 2163 | this.subproperty = false; 2164 | } 2165 | 2166 | SubType.prototype = new SuperType(); 2167 | SubType.prototype.getSubValue = function (){ 2168 | return this.subproperty; 2169 | }; 2170 | 2171 | var instance = new SubType(); 2172 | console.log(instance.getSuperValue()); 2173 | ``` 2174 | 2175 | 输出结果:true 2176 | 2177 | 2178 | 2179 | 实际上,这段代码就是在实现原型链继承,SubType继承了SuperType,本质是重写了SubType的原型对象,代之以一个新类型的实例。SubType的原型被重写了,所以instance.constructor指向的是SuperType。具体如下: 2180 | 2181 | ![image.png](https://cdn.nlark.com/yuque/0/2021/png/1500604/1620043144097-44798602-b916-406d-bc50-e003f27dece2.png) -------------------------------------------------------------------------------- /14 vue项目的性能优化.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | Vue 框架通过数据双向绑定和虚拟 DOM 技术,帮我们处理了前端开发中最脏最累的 DOM 操作部分, 我们不再需要去考虑如何操作 DOM 以及如何最高效地操作 DOM;但 Vue 项目中仍然存在项目首屏优化、Webpack 编译配置优化等问题,所以我们仍然需要去关注 Vue 项目性能方面的优化,使项目具有更高效的性能、更好的用户体验。本文是作者通过实际项目的优化实践进行总结而来,希望读者读完本文,有一定的启发思考,从而对自己的项目进行优化起到帮助。本文内容分为以下三部分组成: 3 | 4 | Vue 代码层面的优化; 5 | 6 | webpack 配置层面的优化; 7 | 8 | 基础的 Web 技术层面的优化。 9 | 10 | 11 | 12 | ## 一、代码层面的优化 13 | **1.1、v-if 和 v-show 区分使用场景** 14 | 15 | v-if 是 真正 的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。 16 | 17 | v-show 就简单得多, 不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 的 display 属性进行切换。 18 | 19 | 所以,v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景;v-show 则适用于需要非常频繁切换条件的场景。 20 | 21 | **1.2、computed 和 watch 区分使用场景** 22 | 23 | computed: 是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值; 24 | 25 | watch: 更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作; 26 | 27 | 运用场景: 28 | 29 | 当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算; 30 | 31 | 当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。 32 | 33 | **1.3、v-for 遍历必须为 item 添加 key,且避免同时使用 v-if** 34 | 35 | (1)v-for 遍历必须为 item 添加 key 36 | 37 | 在列表数据进行遍历渲染时,需要为每一项 item 设置唯一 key 值,方便 Vue.js 内部机制精准找到该条列表数据。当 state 更新时,新的状态值和旧的状态值对比,较快地定位到 diff 。 38 | 39 | (2)v-for 遍历避免同时使用 v-if 40 | 41 | v-for 比 v-if 优先级高,如果每一次都需要遍历整个数组,将会影响速度,尤其是当之需要渲染很小一部分的时候,必要情况下应该替换成 computed 属性。 42 | 43 | 推荐: 44 | ``` 45 |
    46 |
  • 49 | {{ user.name }} 50 |
  • 51 |
52 | computed: { 53 | activeUsers: function () { 54 | return this.users.filter(function (user) { 55 | return user.isActive 56 | }) 57 | } 58 | } 59 | ``` 60 | 不推荐: 61 | ``` 62 |
    63 |
  • 67 | {{ user.name }} 68 |
  • 69 |
70 | ``` 71 | **1.4、长列表性能优化** 72 | 73 | Vue 会通过 Object.defineProperty 对数据进行劫持,来实现视图响应数据的变化,然而有些时候我们的组件就是纯粹的数据展示,不会有任何改变,我们就不需要 Vue 来劫持我们的数据,在大量数据展示的情况下,这能够很明显的减少组件初始化的时间,那如何禁止 Vue 劫持我们的数据呢?可以通过 Object.freeze 方法来冻结一个对象,一旦被冻结的对象就再也不能被修改了。 74 | ``` 75 | export default { 76 | data: () => ({ 77 | users: {} 78 | }), 79 | async created() { 80 | const users = await axios.get("/api/users"); 81 | this.users = Object.freeze(users); 82 | } 83 | }; 84 | ``` 85 | **1.5、事件的销毁** 86 | 87 | Vue 组件销毁时,会自动清理它与其它实例的连接,解绑它的全部指令及事件监听器,但是仅限于组件本身的事件。如果在 js 内 88 | ``` 89 | created() { 90 | addEventListener('click', this.click, false) 91 | }, 92 | beforeDestroy() { 93 | removeEventListener('click', this.click, false) 94 | } 95 | ``` 96 | **1.6、图片资源懒加载** 97 | 98 | 对于图片过多的页面,为了加速页面加载速度,所以很多时候我们需要将页面内未出现在可视区域内的图片先不做加载, 等到滚动到可视区域后再去加载。这样对于页面加载性能上会有很大的提升,也提高了用户体验。我们在项目中使用 Vue 的 vue-lazyload 插件: 99 | 100 | (1)安装插件 101 | 102 | ``` 103 | npm install vue-lazyload --save-dev 104 | ``` 105 | (2)在入口文件 man.js 中引入并使用 106 | 107 | ``` 108 | import VueLazyload from 'vue-lazyload' 109 | ``` 110 | 然后再 vue 中直接使用 111 | 112 | 113 | ``` 114 | Vue.use(VueLazyload) 115 | ``` 116 | 或者添加自定义选项 117 | ``` 118 | Vue.use(VueLazyload, { 119 | preLoad: 1.3, 120 | error: 'dist/error.png', 121 | loading: 'dist/loading.gif', 122 | attempt: 1 123 | }) 124 | ``` 125 | (3)在 vue 文件中将 img 标签的 src 属性直接改为 v-lazy ,从而将图片显示方式更改为懒加载显示: 126 | 127 | 128 | 129 | 130 | 以上为 vue-lazyload 插件的简单使用,如果要看插件的更多参数选项,可以查看 vue-lazyload 的 github 地址。 131 | 132 | **1.7、路由懒加载** 133 | Vue 是单页面应用,可能会有很多的路由引入 ,这样使用 webpcak 打包后的文件很大,当进入首页时,加载的资源过多,页面会出现白屏的情况,不利于用户体验。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应的组件,这样就更加高效了。这样会大大提高首屏显示的速度,但是可能其他的页面的速度就会降下来。 134 | 135 | 路由懒加载: 136 | ``` 137 | const Foo = () => import('./Foo.vue') 138 | const router = new VueRouter({ 139 | routes: [ 140 | { path: '/foo', component: Foo } 141 | ] 142 | }) 143 | ``` 144 | **1.8、第三方插件的按需引入** 145 | 146 | 我们在项目中经常会需要引入第三方插件,如果我们直接引入整个插件,会导致项目的体积太大,我们可以借助 babel-plugin-component ,然后可以只引入需要的组件,以达到减小项目体积的目的。以下为项目中引入 element-ui 组件库为例: 147 | 148 | (1)首先,安装 babel-plugin-component : 149 | 150 | 151 | ``` 152 | npm install babel-plugin-component -D 153 | ``` 154 | (2)然后,将 .babelrc 修改为: 155 | ``` 156 | { 157 | "presets": [["es2015", { "modules": false }]], 158 | "plugins": [ 159 | [ 160 | "component", 161 | { 162 | "libraryName": "element-ui", 163 | "styleLibraryName": "theme-chalk" 164 | } 165 | ] 166 | ] 167 | } 168 | ``` 169 | (3)在 main.js 中引入部分组件: 170 | ``` 171 | import Vue from 'vue'; 172 | import { Button, Select } from 'element-ui'; 173 | 174 | Vue.use(Button) 175 | Vue.use(Select) 176 | ``` 177 | **1.9、优化无限列表性能** 178 | 179 | 如果你的应用存在非常长或者无限滚动的列表,那么需要采用 窗口化 的技术来优化性能,只需要渲染少部分区域的内容,减少重新渲染组件和创建 dom 节点的时间。你可以参考以下开源项目 vue-virtual-scroll-list 和 vue-virtual-scroller 来优化这种无限列表的场景的。 180 | 181 | **1.10、服务端渲染 SSR or 预渲染** 182 | 183 | 服务端渲染是指 Vue 在客户端将标签渲染成的整个 html 片段的工作在服务端完成,服务端形成的 html 片段直接返回给客户端这个过程就叫做服务端渲染。 184 | 185 | (1)服务端渲染的优点: 186 | 187 | 更好的 SEO:因为 SPA 页面的内容是通过 Ajax 获取,而搜索引擎爬取工具并不会等待 Ajax 异步完成后再抓取页面内容,所以在 SPA 中是抓取不到页面通过 Ajax 获取到的内容;而 SSR 是直接由服务端返回已经渲染好的页面(数据已经包含在页面中),所以搜索引擎爬取工具可以抓取渲染好的页面; 188 | 189 | 更快的内容到达时间(首屏加载更快):SPA 会等待所有 Vue 编译后的 js 文件都下载完成后,才开始进行页面的渲染,文件下载等需要一定的时间等,所以首屏渲染需要一定的时间;SSR 直接由服务端渲染好页面直接返回显示,无需等待下载 js 文件及再去渲染等,所以 SSR 有更快的内容到达时间; 190 | 191 | (2)服务端渲染的缺点: 192 | 193 | 更多的开发条件限制:例如服务端渲染只支持 beforCreate 和 created 两个钩子函数,这会导致一些外部扩展库需要特殊处理,才能在服务端渲染应用程序中运行;并且与可以部署在任何静态文件服务器上的完全静态单页面应用程序 SPA 不同,服务端渲染应用程序,需要处于 Node.js server 运行环境; 194 | 195 | 更多的服务器负载:在 Node.js 中渲染完整的应用程序,显然会比仅仅提供静态文件的 server 更加大量占用CPU 资源,因此如果你预料在高流量环境下使用,请准备相应的服务器负载,并明智地采用缓存策略。 196 | 197 | 如果你的项目的 SEO 和 首屏渲染是评价项目的关键指标,那么你的项目就需要服务端渲染来帮助你实现最佳的初始加载性能和 SEO,具体的 Vue SSR 如何实现,可以参考作者的另一篇文章《Vue SSR 踩坑之旅》。如果你的 Vue 项目只需改善少数营销页面(例如 /, /about, /contact 等)的 SEO,那么你可能需要预渲染,在构建时 (build time) 简单地生成针对特定路由的静态 HTML 文件。优点是设置预渲染更简单,并可以将你的前端作为一个完全静态的站点,具体你可以使用 prerender-spa-plugin 就可以轻松地添加预渲染 。 198 | 199 | 200 | 201 | ## 二、Webpack 层面的优化 202 | 203 | **2.1、Webpack 对图片进行压缩** 204 | 205 | 在 vue 项目中除了可以在 webpack.base.conf.js 中 url-loader 中设置 limit 大小来对图片处理,对小于 limit 的图片转化为 base64 格式,其余的不做操作。所以对有些较大的图片资源,在请求资源的时候,加载会很慢,我们可以用 image-webpack-loader来压缩图片: 206 | 207 | (1)首先,安装 image-webpack-loader : 208 | ``` 209 | npm install image-webpack-loader --save-dev 210 | ``` 211 | (2)然后,在 webpack.base.conf.js 中进行配置: 212 | ``` 213 | { 214 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 215 | use:[ 216 | { 217 | loader: 'url-loader', 218 | options: { 219 | limit: 10000, 220 | name: utils.assetsPath('img/[name].[hash:7].[ext]') 221 | } 222 | }, 223 | { 224 | loader: 'image-webpack-loader', 225 | options: { 226 | bypassOnDebug: true, 227 | } 228 | } 229 | ] 230 | } 231 | ``` 232 | 2.2、减少 ES6 转为 ES5 的冗余代码 233 | Babel 插件会在将 ES6 代码转换成 ES5 代码时会注入一些辅助函数,例如下面的 ES6 代码: 234 | ``` 235 | class HelloWebpack extends Component{...} 236 | ``` 237 | 这段代码再被转换成能正常运行的 ES5 代码时需要以下两个辅助函数: 238 | 239 | 240 | ``` 241 | babel-runtime/helpers/createClass // 用于实现 class 语法 242 | babel-runtime/helpers/inherits // 用于实现 extends 语法 243 | ``` 244 | 在默认情况下, Babel 会在每个输出文件中内嵌这些依赖的辅助函数代码,如果多个源代码文件都依赖这些辅助函数,那么这些辅助函数的代码将会出现很多次,造成代码冗余。为了不让这些辅助函数的代码重复出现,可以在依赖它们时通过 require('babel-runtime/helpers/createClass') 的方式导入,这样就能做到只让它们出现一次。babel-plugin-transform-runtime 插件就是用来实现这个作用的,将相关辅助函数进行替换成导入语句,从而减小 babel 编译出来的代码的文件大小。 245 | 246 | (1)首先,安装 babel-plugin-transform-runtime : 247 | 248 | 249 | ``` 250 | npm install babel-plugin-transform-runtime --save-dev 251 | ``` 252 | (2)然后,修改 .babelrc 配置文件为: 253 | ``` 254 | "plugins": [ 255 | "transform-runtime" 256 | ] 257 | ``` 258 | 如果要看插件的更多详细内容,可以查看babel-plugin-transform-runtime 的 详细介绍。 259 | 260 | **2.3、提取公共代码** 261 | 262 | 如果项目中没有去将每个页面的第三方库和公共模块提取出来,则项目会存在以下问题: 263 | 264 | 相同的资源被重复加载,浪费用户的流量和服务器的成本。 265 | 266 | 每个页面需要加载的资源太大,导致网页首屏加载缓慢,影响用户体验。 267 | 268 | 所以我们需要将多个页面的公共代码抽离成单独的文件,来优化以上问题 。Webpack 内置了专门用于提取多个Chunk 中的公共部分的插件 CommonsChunkPlugin,我们在项目中 CommonsChunkPlugin 的配置如下: 269 | 270 | // 所有在 package.json 里面依赖的包,都会被打包进 vendor.js 这个文件中。 271 | ``` 272 | new webpack.optimize.CommonsChunkPlugin({ 273 | name: 'vendor', 274 | minChunks: function(module, count) { 275 | return ( 276 | module.resource && 277 | /\.js$/.test(module.resource) && 278 | module.resource.indexOf( 279 | path.join(__dirname, '../node_modules') 280 | ) === 0 281 | ); 282 | } 283 | }), 284 | // 抽取出代码模块的映射关系 285 | new webpack.optimize.CommonsChunkPlugin({ 286 | name: 'manifest', 287 | chunks: ['vendor'] 288 | }) 289 | ``` 290 | 如果要看插件的更多详细内容,可以查看 CommonsChunkPlugin 的 详细介绍。 291 | 292 | **2.4、模板预编译** 293 | 294 | 当使用 DOM 内模板或 JavaScript 内的字符串模板时,模板会在运行时被编译为渲染函数。通常情况下这个过程已经足够快了,但对性能敏感的应用还是最好避免这种用法。 295 | 296 | 预编译模板最简单的方式就是使用单文件组件——相关的构建设置会自动把预编译处理好,所以构建好的代码已经包含了编译出来的渲染函数而不是原始的模板字符串。 297 | 298 | 如果你使用 webpack,并且喜欢分离 JavaScript 和模板文件,你可以使用 vue-template-loader,它也可以在构建过程中把模板文件转换成为 JavaScript 渲染函数。 299 | 300 | **2.5、提取组件的 CSS** 301 | 302 | 当使用单文件组件时,组件内的 CSS 会以 style 标签的方式通过 JavaScript 动态注入。这有一些小小的运行时开销,如果你使用服务端渲染,这会导致一段 “无样式内容闪烁 (fouc) ” 。将所有组件的 CSS 提取到同一个文件可以避免这个问题,也会让 CSS 更好地进行压缩和缓存。 303 | 304 | 查阅这个构建工具各自的文档来了解更多: 305 | 306 | webpack + vue-loader ( vue-cli 的 webpack 模板已经预先配置好) 307 | 308 | Browserify + vueify 309 | 310 | Rollup + rollup-plugin-vue 311 | 312 | **2.6、优化 SourceMap** 313 | 314 | 我们在项目进行打包后,会将开发中的多个文件代码打包到一个文件中,并且经过压缩、去掉多余的空格、babel编译化后,最终将编译得到的代码会用于线上环境,那么这样处理后的代码和源代码会有很大的差别,当有 bug的时候,我们只能定位到压缩处理后的代码位置,无法定位到开发环境中的代码,对于开发来说不好调式定位问题,因此 sourceMap 出现了,它就是为了解决不好调式代码问题的。 315 | 316 | SourceMap 的可选值如下(+ 号越多,代表速度越快,- 号越多,代表速度越慢, o 代表中等速度 ) 317 | 318 | 319 | 320 | 开发环境推荐:cheap-module-eval-source-map 321 | 322 | 生产环境推荐:cheap-module-source-map 323 | 324 | 原因如下: 325 | 326 | cheap:源代码中的列信息是没有任何作用,因此我们打包后的文件不希望包含列相关信息,只有行信息能建立打包前后的依赖关系。因此不管是开发环境或生产环境,我们都希望添加 cheap 的基本类型来忽略打包前后的列信息; 327 | 328 | module :不管是开发环境还是正式环境,我们都希望能定位到bug的源代码具体的位置,比如说某个 Vue 文件报错了,我们希望能定位到具体的 Vue 文件,因此我们也需要 module 配置; 329 | 330 | soure-map :source-map 会为每一个打包后的模块生成独立的 soucemap 文件 ,因此我们需要增加source-map 属性; 331 | 332 | eval-source-map:eval 打包代码的速度非常快,因为它不生成 map 文件,但是可以对 eval 组合使用 eval-source-map 使用会将 map 文件以 DataURL 的形式存在打包后的 js 文件中。在正式环境中不要使用 eval-source-map, 因为它会增加文件的大小,但是在开发环境中,可以试用下,因为他们打包的速度很快。 333 | 334 | **2.7、构建结果输出分析** 335 | 336 | Webpack 输出的代码可读性非常差而且文件非常大,让我们非常头疼。为了更简单、直观地分析输出结果,社区中出现了许多可视化分析工具。这些工具以图形的方式将结果更直观地展示出来,让我们快速了解问题所在。接下来讲解我们在 Vue 项目中用到的分析工具:webpack-bundle-analyzer 。 337 | 338 | 我们在项目中 webpack.prod.conf.js 进行配置: 339 | ``` 340 | if (config.build.bundleAnalyzerReport) { 341 | var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; 342 | webpackConfig.plugins.push(new BundleAnalyzerPlugin()); 343 | } 344 | ``` 345 | 执行 $ npm run build --report 后生成分析报告如下: 346 | 347 | 348 | 349 | **2.8、Vue 项目的编译优化** 350 | 351 | 如果你的 Vue 项目使用 Webpack 编译,需要你喝一杯咖啡的时间,那么也许你需要对项目的 Webpack 配置进行优化,提高 Webpack 的构建效率。具体如何进行 Vue 项目的 Webpack 构建优化,可以参考作者的另一篇文章《 Vue 项目 Webpack 优化实践》 352 | 353 | 354 | 355 | ## 三、基础的 Web 技术优化 356 | **3.1、开启 gzip 压缩** 357 | 358 | gzip 是 GNUzip 的缩写,最早用于 UNIX 系统的文件压缩。HTTP 协议上的 gzip 编码是一种用来改进 web 应用程序性能的技术,web 服务器和客户端(浏览器)必须共同支持 gzip。目前主流的浏览器,Chrome,firefox,IE等都支持该协议。常见的服务器如 Apache,Nginx,IIS 同样支持,gzip 压缩效率非常高,通常可以达到 70% 的压缩率,也就是说,如果你的网页有 30K,压缩之后就变成了 9K 左右 359 | 360 | 以下我们以服务端使用我们熟悉的 express 为例,开启 gzip 非常简单,相关步骤如下: 361 | 362 | 安装: 363 | 364 | npm install compression --save 365 | 添加代码逻辑: 366 | ``` 367 | var compression = require('compression'); 368 | var app = express(); 369 | app.use(compression()) 370 | ``` 371 | 重启服务,观察网络面板里面的 response header,如果看到如下红圈里的字段则表明 gzip 开启成功 : 372 | 373 | 374 | **3.2、浏览器缓存** 375 | 376 | 为了提高用户加载页面的速度,对静态资源进行缓存是非常必要的,根据是否需要重新向服务器发起请求来分类,将 HTTP 缓存规则分为两大类(强制缓存,对比缓存),如果对缓存机制还不是了解很清楚的,可以参考作者写的关于 HTTP 缓存的文章《深入理解HTTP缓存机制及原理》,这里不再赘述。 377 | 378 | **3.3、CDN 的使用** 379 | 380 | 浏览器从服务器上下载 CSS、js 和图片等文件时都要和服务器连接,而大部分服务器的带宽有限,如果超过限制,网页就半天反应不过来。而 CDN 可以通过不同的域名来加载文件,从而使下载文件的并发连接数大大增加,且CDN 具有更好的可用性,更低的网络延迟和丢包率 。 381 | 382 | **3.4、使用 Chrome Performance 查找性能瓶颈** 383 | 384 | Chrome 的 Performance 面板可以录制一段时间内的 js 执行细节及时间。使用 Chrome 开发者工具分析页面性能的步骤如下。 385 | 386 | 打开 Chrome 开发者工具,切换到 Performance 面板 387 | 388 | 点击 Record 开始录制 389 | 390 | 刷新页面或展开某个节点 391 | 392 | 点击 Stop 停止录制 393 | 394 | ## 原文地址:https://blog.csdn.net/qq_37939251/article/details/100031285 -------------------------------------------------------------------------------- /2 程序员面试软技能.md: -------------------------------------------------------------------------------- 1 | ## 一、如何通过HR⾯ 2 | 3 | HR通常是程序员⾯试的最后⼀⾯,讲道理刷⼈的⼏率不⼤,但是依然有⼈倒在了这最后⼀关上,我们会从HR的⻆度出发来分析如何应对HR⾯. 4 | 5 | ### 1. HR⾯的⽬的 6 | 7 | HR⾯往往是把控⼈才质量的最后⼀关,与前⾯的技术⾯不同,HR⾯往往侧重员⼯⻛险的评估与基本的员⼯素质。 8 | 9 | - **录⽤⻛险评估****,**这部分是评估候选⼈是否具备稳定性,是否会带来额外的管理⻛险,是否能⻢上胜任⼯作,⽐如频繁的跳槽会带了稳定性的⻛险,HR会慎重考虑这⼀点,⽐如在⾯试中候选⼈体现出了「杠精」潜质,HR会担⼼候选⼈在⼯作中会难以与他⼈协作或者不服从管理,带来管理⻛险,再⽐如,虽然国家明确规定在招聘中不得有性别、年龄等歧视,但是⼀个⼤龄已婚妇⼥会有近期产⼦的可能性,可能会有⻓期的产假,HR也会做出评估。 10 | - **员⼯素质评估****,**这部分评估候选⼈是否具备职场的基本素质,是否有基本的沟通能⼒,是否有团队精神和合作意识等等,⽐如⼀个表现极为内向的候选⼈,HR可能会对其沟通能⼒产⽣怀疑. 11 | 12 | 13 | 14 | 所以在与HR交流中要尽量保持踏实稳重、积极乐观的态度,切忌暴露出夸夸其谈、负能量、浮躁等性格缺陷。 15 | 16 | ### 2. HR⾯的常⻅问题 17 | 18 | #### (1)你对未来3-5年的职业规划 19 | 20 | **⽬的**: 这个问题就是考察候选⼈对未来的规划能⼒,主要想通过候选⼈的规划来嗅出候选⼈对⼯作的态度、稳定性和对技术的追求. 21 | 22 | **分析**: ⼀定要在你的回到中体现对技术的追求、对团队的贡献、对⼯作的态度,不要谈⼀些假⼤空的东⻄,或者薪资、职位这些太过于功利的东⻄,⽽且最好体现出你的稳定性,如果是校招⽣或者⼯作没⼏年的新⼈最好不要涉及创业这种话题,⼀⽅⾯职场新⼈计划没⼏年就创业,这种很不切实际,说明候选⼈没法按实际出发,另⼀⽅⾯说明候选⼈的稳定性不够. 23 | 24 | 25 | 26 | 建议分三部分谈: 27 | 28 | 1. ⾸先表示考虑过这个问题(有规划),如何谈⼀谈⾃⼰的现状(结合实际). 29 | 2. 接着从⼯作本身出发,谈谈⾃⼰会如何出⾊完成本职⼯作,如何对团队贡献、如何帮助带领团队其他成员创造更多的价值、如何帮助团队扩⼤影响⼒. 30 | 3. 最后从学习出发,谈谈⾃⼰会如何精进领域知识、如何通过提升⾃⼰专业能⼒,如何反哺团队. 31 | 32 | 33 | 34 | ⾄于想成为技术leader还是技术专家,就看⾃⼰的喜好了. 35 | 36 | #### (2)如何看待加班(996)? 37 | 38 | **⽬的**: 考察候选⼈的抗压能⼒和责任⼼ 39 | 40 | **分析**: 这个问题⼏乎是必问的,虽然996ICU事件闹得沸沸扬扬,但是官⽅的态度很暧昧,只⼝头批评从没有实际⾏动,基本上是默许企业违反劳动法的,除了个别外企在国内基本没可能找到不加班的公司,所以在这个⾯试题中尽量体现出⾃⼰愿意牺牲⾃我时间来帮助团队和企业的意愿就⾏了,⽽且要强调⾃⼰的责任⼼,如果真的是碰到⽆意义加班,好好学习怎么⽤vscode刷LeetCode划⽔是正道. 41 | 42 | **建议**: 43 | 44 | 1. 把加班分为紧急加班和⻓期加班 45 | 2. 对于紧急加班,表示这是每个公司都会遇到的情况,⾃⼰愿意牺牲时间帮助公司和团队 46 | 3. 对于⻓期加班,如果是⾃⼰⻓期加班那么会磨练⾃⼰的技能,提⾼⾃⼰的效率,如果是团队⻓期加班,⾃⼰会帮助团队找到问题,利⽤⾃动化⼯具或者更⾼效的协作流程来提⾼整个团队的效率,帮助⼤家摆脱加班 47 | 48 | 49 | 50 | 当然了,就算你提⾼了团队效率,还是会被安排更多的任务,加班很多时候仅仅是⽬的,,但是你不能说出来啊,尤其是⼀些候选⼈很强硬得表示⻓期加班不接受,其实可以回答的更委婉,除⾮你是真的对这个公司没兴趣,如果以进⼊这个公司为第⼀⽬的,还是做个⾼姿态⽐较好。 51 | 52 | #### (3)⾯对⼤量超过⾃⼰承受能⼒且时间有限的⼯作时你会怎么办? 53 | 54 | **⽬的**: 考察候选⼈时间管理和处理⼤量任务的能⼒,当然也会涉及⼀定的沟通能⼒ 55 | 56 | **分析**: 程序员的⼯作内容可能⼤部分时间并不在写代码上,⽽是要处理各种会议、需求和沟通,通常都属于⼯作超负荷的状态,⾯对上⾯这种问题不建议以加班的⽅式来解决,因为主要考察的是你的时间管理能⼒和沟通能⼒,这些要素要在回答中体现出来 57 | 58 | 建议: 59 | 60 | 1. 将⼤量任务分解为紧急且重要、重要但不紧急、紧急但不重要、不重要且不紧急,依次完成上述任务,在这⾥体现出时间管理的能⼒ 61 | 2. 与⾃⼰的领导沟通将不重要的任务放缓执⾏或者砍掉,或者派给组内的新⼈处理,在这⾥体现出沟通能⼒ 62 | 63 | #### (4)你之前在上海为什么现在来北京发展? 64 | 65 | **⽬的**: 考察候选⼈的稳定性和职业选择 66 | 67 | 分析: 这个问题⼀般是上份⼯作在异地的情况下⼤概率出现,HR主要担⼼候选⼈异地换⼯作可能会不稳定,有短期内离职⻛险,这个时候不建议说"北京互联⽹公司多,机会多"这种话(合着觉得北京好跳槽?),回答最好要体现出⾃⼰的稳定性,⽐如"⼥朋友在北京,⻓期异地,准备来北京⼀起发展" "家在北京,回北京发展" 等等,潜台词就是以后会在北京发展,不会在多地之间来回摇摆. 68 | 69 | #### (5)为什么从上⼀家公司离职? 70 | 71 | ⽬的: 考察离职原因,候选⼈离职⻛险评估 72 | 73 | 74 | **分析**: 这个问题经常会在跳槽的时候问到,这个时候切忌吐槽上⼀家公司或者⾃⼰的上⼀任⽼板,尽量从职业发展的⻆度来回答,凸显⾃⼰的稳定性和渴望学习上升的决⼼,⾄于⼀些敏感话题,⽐如加班太多、薪资太低这种问题也是可以谈的,毕竟你跳槽的诉求就是解决上家公司碰到的问题,但是不能触碰刚才提到的底线问题,切忌吐槽向. 75 | 76 | **建议**: 77 | 78 | 1. 因为⼯资低、离家远、加班多、技术含量低等等原因离职 79 | 2. 因为离家远花费在路途上的时间过多,不如⽤来充电,因为加班多导致没有时间充电,⽆法提⾼等等 80 | 81 | 82 | 83 | 除了不要有负能量和吐槽向,这个部分可以坦诚得说出来 84 | 85 | #### (6)你还有其他公司的Offer吗? 86 | 87 | ⽬的: 评估候选⼈是否有短时间内⼊职其他公司的可能性 88 | 89 | 分析: 很多时候并不是候选⼈完美符合⼀个岗位的要求,HR当然想要⼀个技术更好、要钱更少、技术更匹配的候选⼈,但是候选⼈⼀般都会有这样或者那样的⼩问题。 90 | 91 | 92 | 93 | ⽐如,你的表现是可以胜任⽬前的岗位的,但是这个岗位不是很紧急,HR可能把你当做备胎,来找⼀个性价⽐更⾼的候选⼈。⽐如,你的表现很好,履历优秀,HR不知道能不能100%拿下你。所以如果你很希望加⼊这个公司,最好要做到「欲擒故纵」,既要体现⾃身的市场竞争⼒,⼜要给到HR⼀定的压⼒。所以,即使你已经拿了全北京城互联⽹公司的offer了,也不要说⾃⼰offer多如⽜⽑,⼀副满不在乎的样⼦,这样会给HR造成他⼊职可能性不⼤的错觉,因为他的选择太多了。当然,也不要跪在地上舔:"加⼊公司是我的梦想,我只等这⼀个offer",放⼼吧,⼀定被hr放到备胎⼈才库中. 94 | 95 | 96 | 97 | 建议: 98 | 99 | 1. 表明⾃⼰有三四个已经确认过的offer了(没有offer也要吹,但是不要透露具体公司) 100 | 2. 但是第⼀意向还是本公司,如果薪资差距不⼤,会优先考虑本公司 101 | 3. 再透露出,有⼀两个offer催得⽐较急,希望这边快点出结果 102 | 103 | #### (7)如何与HR谈薪资? 104 | 105 | HR与你谈论薪资经常有如下套路: 106 | 107 | - HR: 您期望的薪资是多少? 108 | - 你: 25K。 109 | 110 | 111 | 112 | OK,你已经被HR成功套路。这个时候你的最⾼价就是25K了,然后HR会顺着这个价往下砍,所以你最终的薪资⼀般都会低于25K。等你接到offer,你的⼼⾥肯定充满了各种“悔恨”:其实当时报价26、27甚⾄28、29也是可以的。 113 | 114 | 115 | 116 | 正确的回答可以这样,并且还能够反套路⼀下HR: 117 | 118 | - HR: 您期望的薪资是多少? 119 | - 你: 就我的⾯试表现,贵公司最⾼可以给多少薪⽔? 120 | 121 | 122 | 123 | 如果经验不够⽼道的HR可能就真会说出⼀个报价(如25K)来,然后,你就可以很开⼼地顺着这个价慢慢地往上谈了。所以这种情况下,你最终的薪资肯定是⼤于25K的。当然,经验⽼道的HR会给你⼀句很官⽅的套话: 124 | 125 | - HR: 您期望的薪资是多少? 126 | - 你: 就我的⾯试表现,贵公司最⾼可以给多少薪⽔? 127 | - HR: 这个暂且没法确定,要结合您⼏轮⾯试结果和⽤⼈部⻔的意⻅来综合评定。 128 | 129 | 130 | 131 | 虽然薪资很重要,但是我个⼈觉得这不是最重要的。我有以下建议: 132 | 133 | - 如果你觉得你技术⾯试效果很好,可以报⼀个⾼⼀点的薪资,这样如果HR想要你,会找你商量的。 134 | - 如果你觉得技术⾯试效果⼀般,但是你⽐较想进这家公司,可以报⼀个折中的薪资。 135 | - 如果你觉得⾯试效果很好,但是你不想进这家公司,你可以适当“漫天要价”⼀下。 136 | - 如果你觉得⾯试效果不好,但是你想进这家公司,你可以开⼀个稍微低⼀点的⼯资。 137 | 138 | 139 | 140 | 需要注意的是,⾯试求职是⼀个双向选择的过程。⾯试应该做到不卑不亢,千万不要因为⾯试结果不好,就低声下⽓地乞求⼯作,每个⼈的⼯作经历和经验都是不⼀样的,技术⾯试不好,知道⾃⼰的短板针对性地补缺补差就⾏,⽽不是在⼈事关系上动歪脑筋。 141 | 142 | ## 二、回答问题的技巧 143 | 144 | 技术⾯试通常⾄少三轮: 145 | 146 | 1. 基础⾯试: 主要考察对岗位和简历中涉及到基础知识部分的提问,包括⼀部分算法和场景设计的⾯试题,这⼀⾯可能会涉及现场coding. 147 | 2. 项⽬⾯试: 主要考察简历中涉及的项⽬,会涉及你项⽬的相关业务知识、扮演⻆⾊、技术取舍、技术攻坚等等. 148 | 3. HR⾯试: 这⼀⾯通常是HR把关,主要涉及⾏为⾯试,考察候选⼈是否价值观符合公司要求、⼯作稳定性如何、沟通协作能⼒如何等等. 149 | 150 | 151 | 152 | 当然,对于初级岗或者校招⽣会涉及⼀轮笔试,相当多的公司会在现场⾯之前进⾏⼀轮电话⾯试,⽬的是最快速有效地把不符合要求的候选⼈筛除,对于个别需要跨部⻔协作的岗位会涉及交叉⾯试,⽐如前端候选⼈会被后端的⾯试官⾯试,⼀些有管理需求的岗位或者重要岗位可能会涉及总监⾯试或者vp⾯。 153 | 154 | 155 | 156 | ⽽⼀个正常的技术⾯试流程(以项⽬⾯为例)分为⼤致三个部分: 157 | 158 | 1. ⾃我介绍 159 | 2. 项⽬(技术)考察 160 | 3. 向⾯试官提问 161 | 162 | 163 | 164 | 那么该如何准备技术⾯试,如何在⾯试中掌握主动权呢? 165 | 166 | ### 1. ⾃我介绍 167 | 168 | ⼏乎所有的⾯试都是从⾃我介绍这个环节开始的,所以我们得搞清楚为什么⾃我介绍通常作为⼀个⾯试的开头。 169 | 170 | #### (1)为什么需要⾃我介绍 171 | 172 | ⾸先,有⼀个很普遍的问题就是⾯试官很可能才刚拿到你的简历,他需要在你⾃我介绍的时候快速浏览你的简历,因为技术⾯试的⾯试官很多是⼀线的员⼯,⾯试候选⼈只是其⼯作中的⼀⼩部分,很多情况下是没有提前看过你的简历的。 173 | 174 | 175 | 176 | 其次,⾃我介绍其实是⼀个热身,⾯试官和候选⼈其实是陌⽣⼈,⾃我介绍不管是⾯试还是其他情况下,都是两个陌⽣⼈彼此交流的起点,也是缓解候选⼈与⾯试官之间尴尬的⼀种热身⽅式. 177 | 178 | 179 | 180 | 最后,⾃我介绍是展示⾃我、引出接下来技术⾯试的引⼦,是你⾃⼰指定技术⾯试⽅向的⼀次机会。 181 | 182 | 183 | 184 | 知道了以上原因,我们才能进⾏准备更好的⾃我介绍。 185 | 186 | #### (2)⾃我介绍的⼏个必备要素 187 | 188 | ⾃我介绍归根到底是⼀个热身运动,因此切忌占⽤⼤量的篇幅,上来就把⾃⼰从出⽣的经历到⼤学像流⽔账⼀样吐出来的,往往会被没耐⼼的⾯试官打断,⽽这也暴露了候选⼈讲话缺乏重点、沟通能⼒⼀般的缺点。 189 | 190 | 191 | 192 | 但是,⼀些关键信息是必须体现的,就我个⼈⽽⾔,以下信息是必备的: 193 | 194 | - 个⼈信息: ⾄少要体现出⾃⼰的姓名、岗位和⼯作年限,应届⽣则必须要介绍⾃⼰的教育背景,如果⾃⼰的前东家是个⼤⼚(⽐如BAT)最好提及,⾃⼰的学历是亮点(985或者硕博或者类似于北邮这种CS强校)最好提及,其他的什么有没有⼥朋友、是不是独⽣⼦没⼈在意,不要占⽤篇幅。这个部分重点在于「你是谁?」。 195 | - 技术能⼒: 简要地介绍⾃⼰的技术栈,切忌把⾃⼰只是简单使⽤过,写过⼏个Demo或者看了看⽂档的所谓「技术栈」也说出来,⼀旦后⾯问到算是⾃找尴尬。这个部分的重点在于「你会什么?」。 196 | - 技能擅⻓: 重点介绍⾃⼰擅⻓的技术,⽐如性能优化、⾼并发、系统架构设计或者是沟通协调能⼒等等,切忌夸⼤其词,要实事求是,这是之后考察的重点。这个部分重点⾃在于「你擅⻓什么?」。 197 | 198 | #### (3)⾃我介绍要重点匹配当前岗位的技术栈 199 | 200 | 你的⾯试简历可能包含了各种各样的技术栈,但是在⾃我介绍过程中需要匹配当前岗位的技术要求。就⽐如你⽬前⾯试的是移动端H5前端的开发岗位,就重点在⾃我介绍中突出⾃⼰在移动前端的经验,⽽此时⼤篇幅得讲述 ⾃⼰如何⽤Node⽀撑公司的web项⽬就显得很不明智. 201 | 202 | #### (4)要在⾃我介绍中做刻意引导 203 | 204 | 如果你的⾃我介绍跟流⽔账⼀样,没有任何重点,其实⾯试官也很难办,因为他都没法往下接话... 205 | 206 | 207 | 208 | ⽽只要你稍作引导,绝⼤部分⾯试官就会接你的话茬,⽐如「你在⾃我介绍中重点提及了⼀个项⽬,碰到了⼀些难点,然后被你攻克了,效果如何如何好等等」,如果我是⾯试官⼀定会问「你的xx项⽬的xx难点后来是怎么解决的?」。 209 | 210 | 211 | 212 | ⾯试官的⽬的是考察候选⼈的能⼒,对候选⼈做出评估,因此需要知道候选⼈擅⻓什么,是否匹配岗位,⾯试官绝⼤多数情况下很乐意你这种有意⽆意的引导,这样双⽅的沟通和评估会很顺利,⽽不是故意刁难候选⼈。 213 | 214 | #### (5)如何准备⾃我介绍 215 | 216 | 其实最好的⽅法也是最笨的⽅法就是把⾃我介绍写下来,这个⾃我介绍⼀定要体现上⾯提到的⼏⼤必备要素,在⾯试前简单过⼏遍,能把⾃我介绍的内容顺利得表达出来即可,切忌跟背课⽂⼀样。 217 | 218 | 219 | 220 | ⾃我介绍的时间最好控制在1-3分钟之间,这些时间⾜够⾯试官把你的简历过⼀遍了,⾯试官看完简历后正好接着你的⾃我介绍进⾏提问是最舒服的节奏,别上来开始10分钟的演讲,⾯试官等待的时候会很尴尬,这么⻓的篇幅说明你的⾃我介绍⼀定是流⽔账式的。 221 | 222 | ### 2. 技术考察 223 | 224 | ⼀个好的技术考察的开始,必须得有⾃我介绍部分好的铺垫和引导,有⼀种情况我们经常遇⻅: 225 | 226 | > 候选⼈说了⼀⼤堆⾮重点的⾃我介绍,⾯试官⼀时语塞,完全get不到候选⼈的重点,也不知道候选⼈擅⻓什么、有什么亮点项⽬,然后就在他简历的技术栈中选了本公司也在⽤的技术,候选⼈这个时候也开始冒汗,因为这个技术栈并不是他的擅⻓,回答的也磕磕绊绊,⾯试官的引导和深⼊追问也没有达到很好的效果,⾯试就在这种尴尬的⽓氛中展开了,⾯试结束后⾯试官对候选⼈的评价是技术不熟练、没有深⼊理解原理,候选⼈的感受是,⾯试官专挑⾃⼰不会的问。 227 | 228 | 所以在前⾯的部分,⼀定要做好引导,把⾯试官的问题引到我们擅⻓的领域,但是这样还不够,正所谓不打⽆准备之仗,我们依然需要针对可能出现的问题进⾏准备. 229 | 230 | 231 | 232 | 那么如何准备可能的⾯试题? 233 | 234 | 235 | 236 | ⽐如你擅⻓前端的性能优化,在⾃我介绍的部分已经做好了引导,接下来⾯试官⼀定会重点考察你性能优化的能⼒,很可能会涉及很有深度的问题,即使你擅⻓这⽅⾯的技术,但是如果没有准备也可能临场乱了阵脚. 237 | 238 | #### (1)多重提问 239 | 240 | ⾃我多重提问的意思是,当⼀个技术问题抛出的时候,你可能⾯对更深层次的追问。 241 | 242 | 243 | 244 | 依旧以前端性能优化为例,⾯试官可能的提问: 245 | 246 | 1. 你把这个⼿机端的⽩屏时间减少了150%以上,是从哪些⽅⾯⼊⼿优化的?这个问题即使你没做过前端性能优化也能回答个七七⼋⼋,⽆⾮是组件分割、缓存、tree shaking等等,这是第⼀重⽐较浅的问题。 247 | 2. 我看你⽤webpack中SplitChunksPlugin这个插件进⾏分chunk的,你分chunk的取舍是什么?哪些库分在同⼀个chunk,哪些应该分开你是如何考虑的?如果你提到了SplitChunksPlugin插件可能会有类似的追问,如果没有实际操作过的候选⼈这个时候就难以招架了,这个过程⼀定是需要⼀定的试错和取舍的. 248 | 3. 在分chunk的过程中有没有遇到什么坑?怎么解决的?其实SplitChunksPlugin这个插件有⼀个暗坑,那就是chunid⾃增性导致id不固定唯⼀,很可能⼀个新依赖就导致id全部打乱,使得http缓存失效. 249 | 250 | 251 | 252 | 以上只是针对SplitChunksPlugin插件相关的优化提问,当然也可能从你的性能测试⻆度、代码层⾯进⾏考察,但是思路是类似的。因此不能把⾃⼰准备的问题答案停留在⼀个很浅显的层⾯,⼀⽅⾯⽆法展示⾃⼰的技术深度,另⼀⽅⾯在⾯试官的深度体情况下容易丢分,因此在⾃⼰的答案后⾯多进⾏⾃我的追问,看⼀看能不能把问题做的更深⼊。 253 | 254 | #### (2)答题法则 255 | 256 | 很多⾯试相关的宝典都推荐使⽤STAR法则进⾏问题的应答,我们不想引⼊这个额外的概念,基础技术⾯试的部分⽼⽼实实回答⾯试官的问题即可,通常需要问题运⽤到这个法则的是项⽬⾯,⽐如让你介绍⼀下你最得意的项⽬,回答问题的法则有这⼏个要点: 257 | 258 | - 项⽬背景: 简要说⼀下项⽬的背景,让⾯试官知道这个项⽬是做什么的 259 | - 个⼈⻆⾊: 让⾯试官知道你在这个项⽬中扮演的⻆⾊ 260 | - 难点: 让⾯试官知道你在项⽬开发过程中碰到的难点 261 | - 解决⽅案: 针对上⾯的难点你有哪⼀些解决⽅案,是如何结合业务进⾏取舍的 262 | - 总结沉淀: 在攻克上述的难点后有没有沉淀出⼀套通⽤的解决⽅案,有没有将⾃⼰的⽅案在⼤部⻔进⾏推⼴等等 263 | 264 | 265 | 266 | 重点就在于后⾯三条,也是最体现你个⼈综合素质的⼀部分,我是⾯试官的话会⾮常欣赏那种可以发现问题、找到多种⽅ 案、能对多种⽅案进⾏⽐对取舍还可以总结沉淀出通⽤解决⽅案回馈团队的⼈。从上述⼏点可以体现出⼀个⼈的技术热情、解决问题的能⼒和总结提⾼的能⼒。 267 | 268 | #### (3)刻意引导 269 | 270 | 是的,在回答⾯试官提问的时候也可以做到刻意引导。 271 | 272 | 273 | 274 | 举⼏个简单的例⼦: 275 | 276 | - 除了Vue还⽤过Angular吗? 这个时候很多候选⼈就很实诚回答「没有」,其实我们可以回答的更好,把你知道的说出来展示⾃⼰的能⼒才是最重要的,你可以说「我虽然没⽤过,但是在学习双向绑定原理的时候了解了⼀下 Angular脏检查的原理,在学习Nestjs的时候了解了依赖注⼊的原理,跟Angular也是类似的」,⾯试官⼀定会接着问你脏检查和依赖注⼊的问题,虽然你没有⽤过Angular,但是Angular的基本原理你都懂,这是很好的加分项,说明候选⼈有深⼊理解原理的意愿和触类旁通的能⼒ 277 | - Vue如何实现双向绑定的? 很多候选⼈⽼⽼实实答了 object.defineproperty 如何如何操作,然后就没有了,其实你可以在回答完之后加上⼀嘴「Vue 3.0则选择了更好⽤的Proxy来替代object.defineproperty」或者「除了object.defineproperty这种数据劫持的⽅式,观察者模式和脏检查都可以实现双向绑定」,⾯试官⼤概率会问「Proxy好在哪?」或者「聊聊脏检查」等等,这样下⼀个问题就会依然在你的可控范围内 278 | 279 | 280 | 281 | 我们第⼀个例⼦把本来回答不上来的问题,转化为了成功展示⾃⼰能⼒的加分项,第⼆个例⼦让⾃⼰更多的展示了⾃⼰的能⼒,⽽且始终使⾯试官的问题在⾃⼰的可控范围内。 282 | 283 | ### 3. 向⾯试官提问 284 | 285 | 这个部分基本到了⾯试尾声了,属于做好了不影响⼤局,但是可能加分,如果做不好很容易踩雷的区域. 286 | 287 | 288 | 289 | ⾸先我们声明⼏个雷区: 290 | 291 | - 切忌问结果: 问了也⽩问,绝⼤部分公司规定不会透露结果的,你这样让⼤家很尴尬 292 | - 切忌问⼯资: 除了HR跟你谈⼯资的时候,千万别跟技术⾯试官谈⼯资,⼯资是所有公司的⾼压线,没法谈论 293 | - 切忌问技术问题: 别拿⾃⼰不会的技术难题反问⾯试官,完全没意义,⾯试官答也不是不答也不是 294 | 295 | 296 | 297 | 有⼏个⽐较好的提问可供参考: 298 | 299 | - 如果我⼊职这个岗位的话,前三个⽉你希望我能做到些什么? 300 | - 我的这个岗位的前任是为什么离职的,我什么地⽅能做的更好? 301 | - 你对这个职位理想⼈选的要求是什么? 302 | 303 | 304 | 305 | 尽量围绕你的岗位进⾏提问,这可以使得你更快得熟悉你的⼯作内容,也让⾯试官看到你对此岗位的兴趣和热情,重要的是 这些问题对于⾯试官⽽⾔既可以简略回答,也可以详细的给你讲解,如果他很热情得跟你介绍此岗位相关的情况,说明你可能表现得不错,否则的话,你可能不在他的备选名单⾥,这个时候就需要你早做打算了。 306 | 307 | ## 三、⾯试官到底想看什么样的简历? 308 | 309 | ⾯试⼀直是程序员跳槽时期⾮常热⻔的话题,虽然现在已经过了跳槽的旺季,下⼀轮跳槽季需要到年底才会出现,但是当跳槽季的时候你再看这篇⽂章可能已经晚了,过冬的粮⻝永远不是冬天准备的,⽽是秋收的时候。 310 | 311 | 312 | 313 | 简历是你进⼊⾯试的敲⻔砖,也是留给意向公司的第⼀印象,所以这个很重要,必须在这上⾯做⾜了⽂章,⼀份优秀的⾯试简历是整个⾯试成败的重中之重,我们会详细分析如何准备简历才能保证简历不被刷掉。 314 | 315 | 316 | 317 | 简历通常有这⼏部分构成: 318 | 319 | 1. 基本资料 320 | 2. 专业技能 321 | 3. ⼯作经历 322 | 4. 项⽬经历 323 | 5. 教育背景 324 | 325 | 326 | 327 | 我们会逐⼀进⾏分析。 328 | 329 | ### 1. 准备简历模板 330 | 331 | 万事开头难,简历的编写如果从头开始需要浪费很多时间,其实最快速也最聪明的办法就是先找⼀份还不错的简历模板,之后我们只需要填写信息即可。 332 | 333 | 334 | 335 | 简历模板的选择很讲究,有些简历基本不看内容就会被刷掉,这些简历⼀般会对⾯试官进⾏视觉攻击,让简历给⾯试官的第⼀印象就是反感。 336 | 337 | 338 | 339 | 有两种坑爹的简历模板: 340 | 341 | - ⼀种是经典简历模板,真是堪称『经典』,这种简历模板在我上⼩学的时候就有了,以现在的眼光看有点不够看了,配 ⾊也⽐较『魔幻』,加上表格类的简历属于low到底端的简历类型,基本上扫⼀眼就扔了,这种简历只需要3秒钟就能被⾯试官扔到垃圾堆 342 | - 另⼀种是设计感⼗⾜的简历模板,这种简历设计感⼗⾜,这五颜六⾊的配⾊⼀定能亮瞎⾯试官的双眼,这种花⾥胡哨的简历同样也是3秒钟沉到垃圾堆底部的简历。 343 | 344 | 345 | 346 | 以上两类简历模板堪称⾯试官杀⼿,我相信只要你⽤了上述两类模板,绝对连让⾯试官看第⼆眼的兴趣都没有。⾯试官筛简历要的是⾼效、清晰、内容突出,不管是HR还是技术⾯试官都想在最快速的情况下看到有效信息,你眼中所谓的『视觉效果』在别⼈眼⾥就是『视觉噪⾳』或者『视觉垃圾』,严重影响看简历的⼼情和寻找有效信息的速度。 347 | 348 | ### 2. 准备个⼈信息 349 | 350 | 个⼈信息部分主要包括姓名、电话、点⼦邮箱、求职意向,当然这四个是必填的,其它的都是选填,填好了是加分项,否则很可能减分。 351 | 352 | 353 | 354 | 接下来才是重点: 355 | 356 | 1. github:如果准备⼀个基本没有更新的博客或者没有任何贡献的github,那么给⾯试官⼀种为了放上去⽽放上去的感觉,这基本上就是在跟⾯试官说『这个候选⼈平时根本没有总结提炼的习惯』,所以如果有⻓期维护的github或者博客⼀定要放上去,质量好的话会⾮常有⽤,如果没有千万别放。 357 | 2. 学历:如果你的学历是专科、⾼中毕业之类的,还写在简历头部强调⼀遍,这就造成了你是『学渣』的印象,没有公司喜欢学渣的,这⼜增加了简历被刷的⼏率,如果是研究⽣以上学历可以写,突出⼀下学历优势,本科学历在技术⾯试领域基本上敲⻔砖级别的,没必要写。 358 | 3. 年龄:如果你是⼤龄程序员,尤其是你还在求⼀份低端岗位的时候千万别写,⼀个⼤龄程序员在求职⼀个中低端岗位,说明这些年基本原地踏步,还不能加班,到这⾥基本上此简历就凉了⼀半了。 359 | 4. 照⽚:形象优秀的可以贴,尤其是形象优秀的⼥程序媛,其它的最好不要贴,如果要贴的话,最好是贴那种PS过的⾮常职业的证件照,那种平时搞怪的、光着膀⼦的⽣活照,基本就是⾃杀⾏为。 360 | 361 | ### 3. 准备专业技能 362 | 363 | 对于程序员的专业技能其实就是技术栈,对于⾃⼰的技术栈如何描述是个很难的问题,⽐如什么算是精通?什么算是了解?什么是熟悉? 364 | 365 | 366 | 367 | 关于对技术技能的描述有很多种,有五种的也有三种的,⽽且每个⼈对词汇的理解都不⼀样,我结合相关专家的理解和⾃⼰的理解来简单阐述下描述词汇的区别,我们这⾥只讲三种的了解、熟悉、精通。 368 | 369 | - 了解:使⽤过某⼀项技术,能在别⼈指导下完成⼯作,但不能胜任复杂⼯作,也不能独⽴解决问题。 370 | - 熟悉:⼤量运⽤过的某⼀项技术,能独⽴完成⼯作,且能独⽴完成有⼀定复杂度的⼯作,在技术的应⽤层⾯不会有太⼤问题,甚⾄理解⼀点原理。 371 | - 精通:不仅可以运⽤某⼀⻔技术完成复杂项⽬,⽽且理解这项技术背后的原理,可以对此技术进⾏⼆次开发,甚⾄本身就是技术源码的贡献者。 372 | 373 | 374 | 375 | 我们就以Vue这个框架为例,如果你可以⽤vue写⼀些简单的⻚⾯,单独完成某⼏个⻚⾯的开发,但是⽆法脱离公司脚⼿架⼯作,也⽆法独⽴从0完成⼀个有⼀定复杂度的项⽬,只能称之为了解。 376 | 377 | 378 | 379 | 如果你有⼤量运⽤vue的经验,有从0独⽴完成⼀定复杂度项⽬的能⼒,可以完全脱离脚⼿架进⾏开发,且对vue的原理有⼀定的了解,可以称之为熟悉。 380 | 381 | 382 | 383 | 如果你⽤vue完成过复杂度很⾼的项⽬,⽽且⾮常熟悉vue的原理,是vue源码的主要贡献者,亦或者根据vue源码进⾏过魔改(⽐如mpvue),你可以称得上精通。 384 | 385 | 386 | 387 | 那么有两个坑是候选⼈经常犯的,『杂』和『精』,这种两个坑⼤量集中在应届⽣和刚毕业每两年的新⼿身上,其主要特点是『急于表现⾃我』、『对技术深度与⼴度出现⽆知⽽导致的过度⾃信』。 388 | 389 | 390 | 391 | ⾸先说说杂,⽐如你要应聘⼀个Java后端,⽼⽼实实把⾃⼰的java技术栈写好就⾏了,强调⼀下⾃⼰擅⻓什么即可,最好专精某领域⽐如『⾼并发』、『⾼可⽤』等等,这个时候⼀些简历⾮要给⾃⼰加戏,⾃⼰会的不会的⼀股脑往上堆,什么逆向、密码学、图形、驱动、AI都要体现出来,越杂越好,这种简历给⼈的印象就是个什么都不懂的半吊⼦。 392 | 393 | 394 | 395 | 再说说精,⼀个刚毕业的应届⽣,出来简历就各种精通,精通Java、精通Java虚拟机、精通spring全家桶、精通kafka等等,请放⼼,这种简历是不会没头没脑投过来了,这种在⼤学⾥就精通各种的天才早被他的各种学⻓介绍进了⼤⼚或者外企做某某Star重点培养了,往往看到的这种也是半吊⼦。 396 | 397 | ### 4. 准备⼯作经历 398 | 399 | ⼯作经历本身不⽤花太多笔墨去写,⾯试官主要想看的就是每段⼯作经历的持续时间、在不同公司担任的职责如何、是否有⼤⼚的⼯作经验等等。 400 | 401 | 402 | 403 | 那么什么简历在这⾥给⾯试官减分呢? 404 | 405 | - 频繁跳槽:⽐如三年换了四家公司,每个公司呆的时⻓不要超过⼀年 406 | - 常年初级岗:⽐如⼯作五六年之后依然在完成⼀些简单的项⽬开发 407 | - 末流公司经历:在技术招聘届,⼤⼚的优先级最⾼⽐如BAT、TMD甚⾄微软、⾕歌等外企,知名度独⻆兽其次,⽐如商汤、旷视等等,⼀般的互联⽹公司排在第三,就是⼯作中⼩型的互联⽹公司⼀般⼤家叫不上名字,排在最后的就是外包和传统企业的经历 408 | 409 | 410 | 411 | 所以,如果你有频繁跳槽的经历怎么办?在本公司⽼⽼实实等到满⼀年再跳槽。 412 | 413 | 如果常年初级岗怎么办?想办法晋升或者参与⼀些业界知名项⽬,再或者写⼀个有⼀定复杂度的私⼈项⽬。 414 | 415 | 如果有末流公司经历怎么办?如果是很久以前的末流公司经验可以直接不写,也没⼈在乎你很早之前的⼯作经历,如果你现在就在末流公司,赶紧想办法跳槽,去不了⼤⼚,去⾮知名的互联⽹公司也算是胜利⼤逃亡了。 416 | 417 | ### 5. 准备项⽬经历 418 | 419 | 项⽬经历不管对于社招还是校招都是重中之重,很多时候成败就在于项⽬经历这块,⼀个普通本科可以通过优秀的项⽬经历逆袭985,⼀个⼩⼚的员⼯也可以获得⼤⼚的⾯试机会。 420 | 421 | 422 | 423 | 但是必须要说⼀下项⽬经历的编写很讲究,这是为后⾯⾯试部分铺路的绝佳机会,也是直接让你的简历扑街的重点沦陷区域。 424 | 425 | 426 | 427 | 先说容易让简历扑街的⼏个坑位。 428 | 429 | #### (1)切忌流⽔账写法 430 | 431 | 项⽬经历流⽔账写法是绝⼤多数简历的通病,通篇下来就讲了⼀件事『我⼲了啥』。 432 | 433 | 434 | 435 | ⼤部分简历却是这样的: 436 | 437 | > ⽤Vue、Vuex、Vue-router、axios等技术开发电商⽹站的前端部分,主要负责⾸⻚、店铺详情、商品详情、商品列表、订单详情、订单中⼼等相关⻚⾯的开发⼯作,与设计师与后端配合,可要⾼度还原设计稿。 438 | 439 | 这个描述有什么问题? 其实看似也没啥问题,但是这种流⽔账写法太多了,完全⽆法突出⾃⼰的优势展现⾃⼰的能⼒。项⽬经历是考察重点,⾯试官想知道候选⼈在⼀次项⽬经历中扮演的⻆⾊、负责的模块、碰到的问题、解决的思路、达成的效果以及最后的总结与沉淀。 440 | 441 | 442 | 443 | ⽽上⾯的描述只显示了『我⼲了啥』,所以这种项⽬描述⼏乎是没意义的,因为对于⾯试官⽽⾔他看不到有效信息,没有有效信息的项⽬描述基本就没价值了,如果这个时候你还没有⼤⼚经历或者名校背书,基本上也就凉了。 444 | 445 | #### (2)切忌堆积项⽬ 446 | 447 | 堆积项⽬这种现象往往出现在没有什么优秀项⽬经历的简历身上,候选⼈企图以数量优势掩盖质量的劣势,其实往往适得其反,项⽬经历的⼀栏最好放2-3个项⽬,⾮常优秀的项⽬可能放⼀个就⾜够了,举个极端例⼦如果有⼀天尤⾬溪写简历,其实只需要在项⽬经历那些⼀⾏『Vue.js作者』就⾏了,当然,他并不需要投简历。 448 | 449 | 450 | 451 | 有⼀些项⽬切忌放上去: 452 | 453 | - demo级项⽬:很多简历居然还在放⼀些仿xx官⽹的demo,这是⼗⾜的减分项,有⼀些则是东拼⻄凑抄了⼀些框架 的源码搞了个玩具项⽬,也没有任何价值。 454 | - 烂⼤街的项⽬:这种以vue技术栈的为最,由于视频⽹站的某⻔课程流⾏,导致⼤量的仿饿了么、仿qq⾳乐、仿美 团、仿去哪⼉,同样Java的同学也是仿电商⽹站、仿⼤众点评等等,⼗份简历5份⼀模⼀样的项⽬,你是⾯试官会怎么想。 455 | - 低质量的开源项⽬:⼀个⼤原则就是低star的尽量别放(除⾮是⾼质量代码的冷⻔项⽬),⻓期弃坑的也不要放,不要为了凑数量把低质量的项⽬暴露出来,好好藏着。 456 | 457 | 458 | 459 | 如果只放两个项⽬,最好的搭配是⼀个公司内部挑⼤梁的项⽬和⼀个社区内的开源项⽬,后者之所以可以占据⼀席之地,是因为通过你的开源项⽬,⾯试官可以通过commit完整看到你的创造过程,⽐如⼯程化建设、commit规范、代码规范、协作⽅式、代码能⼒、沟通能⼒等等,这甚⾄⽐⾯试都有⽤,没有⽐开源项⽬更能展示你综合素质的东⻄了。 460 | 461 | #### (3)切忌放虚假项⽬ 462 | 463 | ⼀个项⽬做没做过只要是有经验的⾯试官⼀问便知,如果你真的靠假项⽬忽悠过了⾯试,那这个公司⼋成也有问题,⼈才把关不过硬,你可以想象你的队友都是什么⽔平,在这种公司⼤成⻓价值也不⼤。好,如果你说实在没项⽬可写了,我只能造假了,那么你应该想⼀下这多层追问。 464 | 465 | 466 | 467 | ⽐如你说你优化了⼀个前端项⽬的⾸屏性能,降低了⽩屏时间,那么⾯试官对这个性能优化问题会进⾏深挖,来考察候选⼈的实际⽔平: 468 | 469 | 1. 你的性能优化指标是怎么确定的?平均下来时间减短了多少? 470 | 2. 你的性能是如何测试的?有两种主流的性能测试⽅法你是怎么选的? 471 | 3. 你是根据哪些指标进⾏针对性优化的? 472 | 4. 除了你说的这些优化⽅法还有没有想过通过xx来解决? 473 | 5. 你的这个优化⽅法在实际操作中碰到过什么问题吗?有没有进⼀步做过测试? 474 | 6. 我们假设这么⼀种情况,⽐如xxxx,你会这么进⾏优化? 475 | 476 | 477 | 478 | ⾯试官多层追问的逻辑是这样的:**了解背景** **->** **了解⽅案** **->** **深挖⽅案** **->** **模拟场景** 479 | 480 | 481 | 482 | ⾸先得了解你性能优化的指标如何,接着需要了解你是这么测试的指标、再怎么进⾏针对性优化的,再接着提出⼀些其它解决⽅案考察你对优化场景的知识储备和⽅案决策能⼒,最后再模拟⼀个其它的业务场景,来考察你的技能迁移能⼒,看看是否是对某块领域有⼀定的了解,⽽不是只针对某个项⽬。 483 | 484 | 485 | 486 | 如果要真的在⾯试现场对答如流,那么⼀定是在某⼀块领域有⼀定知识储备的⼈,不是随随便便搞个项⽬就能蒙混过关的。 487 | 488 | #### (4)合格的项⽬经历如何写 489 | 490 | 合格的项⽬经历必须要有以下⼏点: 491 | 492 | - 项⽬概述 493 | - 个⼈职责 494 | - 项⽬难点 495 | - ⼯作成果 496 | 497 | 498 | 499 | 如果你不怕字太多,还可以选择性加⼊解决⽅案、选型思路等等,但是由于篇幅限制和为⾯试铺垫就不太建议写得太多。 500 | 501 | 502 | 503 | **项⽬概述**的⽬的是让⾯试官理解项⽬,不是每个⼈⾯试官都做过你的那种项⽬,所以需⼀个简述⽅便⾯试官理解。 504 | 505 | 506 | 507 | **个⼈职责**就是告诉⾯试官你在本项⽬中扮演的⻆⾊,是领导者?主导者?还是跟随者,你负责了哪些模块,承担了多⼤的⼯作量,以此来评估你在团队中的作⽤。 508 | 509 | 510 | 511 | **项⽬难点**的⽬的在于让⾯试官看到你碰到的技术难题,⽅便后续⾯试对项⽬进⾏⼀系列讨论。 512 | 513 | ⼯作成果就很明显了,⾯试官需要看到你在做了上述⼯作到底达成了什么成绩,这个时候最好以数据说话,⽐如访问量、⽩屏时间等等。 514 | 515 | 516 | 517 | 这个时候也切忌展开⻓篇⼤论,把技术细节⼀个个写上去,甚⾄还写了⼼路历程的都是⼤忌,⼀⽅⾯篇幅太⼤会造成视觉混乱,另⼀⽅⾯⾯试官想看到的是『简』历,不是技术总结,⾯试官要⾯对上百份简历没那么时间来看你⻓篇⼤论,⻓篇⼤论⼤可以在⾯试中展开。 518 | 519 | 520 | 521 | 最好的⽅法就是⼀⾏⽂字简单得说清楚即可,反正项⽬⾯的时候⼀定会问到,到时候好好把你准备的内容讲给⾯试官,掌握⾯试的主动权就是从项⽬经历这⼀栏中开始 522 | 523 | ### 6. 其他 524 | 525 | #### (1)教育背景 526 | 527 | 应届⽣可以写得更详细⼀点,⽐如绩点排名怎么样,有没有突出的科⽬,社招就不要写太多了,简单的⼊学时间、学校、专业即可,⽽且写你的最⾼学历即可,没必要从初中就开始写学历流⽔账,没有⼈看的。 528 | 529 | #### (2)注意事项 530 | 531 | - **⾃我评价不建议写**:技术⾯试⼏乎没⼈看你的⾃我评价,连⾯试技术问题都嫌『talk is cheap show me the code』,你的⾃我评价除了占篇幅没啥⽤处,充其量算是⾯试官的⼲扰信息。 532 | - **简历封⾯千万别搞**:这都是⼀些简历制作⽹站骗⽤户付费的伎俩,不仅是互联⽹⾏业,其它⾏业我也没⻅过要简历封⾯这种⽆⽤操作的。 533 | - **证书不建议写**:应届⽣可以酌情考虑弄个六级证书什么的,对于社招⽽⾔,列⼀堆证书甚⾄是减分项,国内的各种证你也懂的,是有多不⾃信才沦落到靠⼀堆证书来证明⾃⼰的价值。 534 | - **千万别⽤技能图表**:⾸先⽤90分、80分来评价⾃⼰的技术本身就没有什么说服⼒,也不可能这么精准,⽽且什么是90分、什么是80根本就没有⼀个公论,所以⽤⼀般的⽐较通⽤的熟悉、精通描述即可,千万别加戏,⾯试官或者HR没那么多闲⼯夫去理解你的图表,⽼⽼实实按最通⽤⾼效的⽅式描述⾃⼰的技术栈。 535 | - **简历最好⼀⻚**:程序员⼜不是设计师有时候需要作品呈现,如果你的简历超过⼀⻚那么⼀定是出问题了,要么项⽬、技术栈描述太多太杂占据⼤量篇幅,要么加了⼀堆图表或者图画来加戏,当然往往是犯前⼀个错误的更多。 -------------------------------------------------------------------------------- /3 offer收割机之HTML篇.md: -------------------------------------------------------------------------------- 1 | ![HTML面试题.png](https://cdn.nlark.com/yuque/0/2021/png/1500604/1621600195788-acab59b1-a654-4ec4-b9c2-0a491a660671.png) 2 | 3 | ### 1. src 和 href 的区别 4 | 5 | src 和 href 都是**用来引用外部的资源**,它们的区别如下: 6 | 7 | - **src:** 表示对资源的引用,它指向的内容会嵌入到当前标签所在的位置。src 会将其指向的资源下载并应⽤到⽂档内,如请求 js 脚本。当浏览器解析到该元素时,会暂停其他资源的下载和处理,直到将该资源加载、编译、执⾏完毕,所以⼀般 js 脚本会放在页面底部。 8 | - **href:** 表示超文本引用,它指向一些网络资源,建立和当前元素或本文档的链接关系。当浏览器识别到它他指向的⽂件时,就会并行下载资源,不会停⽌对当前⽂档的处理。 常用在 a、link 等标签上。 9 | 10 | ### 2. 对 HTML 语义化的理解 11 | 12 | **语义化是指** 根据内容的结构化(内容语义化),选择合适的标签(代码语义化)。通俗来讲就是用正确的标签做正确的事情。 13 | 14 | 语义化的优点如下: 15 | 16 | - 对机器友好,带有语义的文字表现力丰富,更适合搜索引擎的爬虫爬取有效信息,有利于 SEO。除此之外,语义类还支持读屏软件,根据文章可以自动生成目录; 17 | - 对开发者友好,使用语义类标签增强了可读性,结构更加清晰,开发者能清晰的看出网页的结构,便于团队的开发与维护。 18 | 19 | 常见的语义化标签: 20 | 21 | ```html 22 |
头部 23 | 24 | 导航栏 25 | 26 |
区块(有语义化的div) 27 | 28 |
主要区域 29 | 30 |
主要内容 31 | 32 | 侧边栏 33 | 34 |
底部 35 | ``` 36 | 37 | ### 3. DOCTYPE(文档类型) 的作⽤ 38 | 39 | DOCTYPE 是 HTML5 中一种标准通用标记语言的文档类型声明,它的目的是**告诉浏览器(解析器)应该以什么样(html 或 xhtml)的文档类型定义**来解析文档,不同的渲染模式会影响浏览器对 CSS 代码甚⾄ JavaScript 脚本的解析。它必须声明在 HTML ⽂档的第⼀⾏。 40 | 41 | 浏览器渲染页面的两种模式(可通过 `document.compatMode` 获取,比如,语雀官网的文档类型是**CSS1Compat**): 42 | 43 | - **CSS1Compat:标准模式(Strick mode)**,默认模式,浏览器使用 W3C 的标准解析渲染页面。在标准模式中,浏览器以其支持的最高标准呈现页面。 44 | - **BackCompat:怪异模式(混杂模式)(Quick mode)**,浏览器使用自己的怪异模式解析渲染页面。在怪异模式中,页面以一种比较宽松的向后兼容的方式显示。 45 | 46 | ### 4. script 标签中 defer 和 async 的区别 47 | 48 | 如果没有 defer 或 async 属性,浏览器会立即加载并执行相应的脚本。它不会等待后续加载的文档元素,读取到就会开始加载和执行,这样就阻塞了后续文档的加载。 49 | 50 | 下图可以直观的看出三者之间的区别: 51 | 52 | ![image.png](https://cdn.nlark.com/yuque/0/2020/png/1500604/1603547262709-5029c4e4-42f5-4fd4-bcbb-c0e0e3a40f5a.png) 53 | 54 | 其中蓝色代表 js 脚本网络加载时间,红色代表 js 脚本执行时间,绿色代表 html 解析。 55 | 56 | **defer 和 async 属性都是去异步加载外部的 JS 脚本文件,它们都不会阻塞页面的解析**,其区别如下: 57 | 58 | - **执行顺序:**多个带 async 属性的标签,不能保证加载的顺序;多个带 defer 属性的标签,按照加载顺序执行; 59 | - **脚本是否并行执行:**async 属性,表示**后续文档的加载和执行与 js 脚本的加载和执行是并行进行的**,即异步执行;defer 属性,**加载后续文档的过程和 js 脚本的加载**(此时仅加载不执行)**是并行进行的**(异步),js 脚本需要等到文档所有元素解析完成之后才执行,DOMContentLoaded 事件触发执行之前。 60 | 61 | ### 5. 常用的 meta 标签有哪些 62 | 63 | `meta` 标签由 `name` 和 `content` 属性定义,**用来描述网页文档的属性**,比如网页的作者,网页描述,关键词等,除了 HTTP 标准固定了一些`name`作为大家使用的共识,开发者还可以自定义 name。 64 | 65 | 常用的 meta 标签: 66 | 67 | (1)`charset`,用来描述 HTML 文档的编码类型: 68 | 69 | ```html 70 | 71 | ``` 72 | 73 | (2) `keywords`,页面关键词: 74 | 75 | ```html 76 | 77 | ``` 78 | 79 | (3)`description`,页面描述: 80 | 81 | ```html 82 | 83 | ``` 84 | 85 | (4)`refresh`,页面重定向和刷新: 86 | 87 | ```html 88 | 89 | ``` 90 | 91 | (5)`viewport`,适配移动端,可以控制视口的大小和比例: 92 | 93 | ```html 94 | 95 | ``` 96 | 97 | 其中,`content` 参数有以下几种: 98 | 99 | - `width viewport` :宽度(数值/device-width) 100 | - `height viewport` :高度(数值/device-height) 101 | - `initial-scale` :初始缩放比例 102 | - `maximum-scale` :最大缩放比例 103 | - `minimum-scale` :最小缩放比例 104 | - `user-scalable` :是否允许用户缩放(yes/no) 105 | 106 | (6)搜索引擎索引方式: 107 | 108 | ```html 109 | 110 | ``` 111 | 112 | 其中,`content` 参数有以下几种: 113 | 114 | - `all`:文件将被检索,且页面上的链接可以被查询; 115 | - `none`:文件将不被检索,且页面上的链接不可以被查询; 116 | - `index`:文件将被检索; 117 | - `follow`:页面上的链接可以被查询; 118 | - `noindex`:文件将不被检索; 119 | - `nofollow`:页面上的链接不可以被查询。 120 | 121 | ### 6. HTML5 有哪些更新 122 | 123 | #### 1. 语义化标签 124 | 125 | - header:定义文档的页眉(头部); 126 | - nav:定义导航链接的部分; 127 | - footer:定义文档或节的页脚(底部); 128 | - article:定义文章内容; 129 | - section:定义文档中的节(section、区段); 130 | - aside:定义其所处内容之外的内容(侧边); 131 | 132 | #### 2. 媒体标签 133 | 134 | (1) audio:音频 135 | 136 | ```html 137 | 138 | ``` 139 | 140 | 属性: 141 | 142 | - controls 控制面板 143 | - autoplay 自动播放 144 | - loop=‘true’ 循环播放 145 | 146 | (2)video 视频 147 | 148 | ```html 149 | 150 | ``` 151 | 152 | 属性: 153 | 154 | - poster:指定视频还没有完全下载完毕,或者用户还没有点击播放前显示的封面。默认显示当前视频文件的第一针画面,当然通过 poster 也可以自己指定。 155 | - controls 控制面板 156 | - width 157 | - height 158 | 159 | (3)source 标签 160 | 161 | 因为浏览器对视频格式支持程度不一样,为了能够兼容不同的浏览器,可以通过 source 来指定视频源。 162 | 163 | ```html 164 | 168 | ``` 169 | 170 | #### 3. 表单 171 | 172 | **表单类型:** 173 | 174 | - email :能够验证当前输入的邮箱地址是否合法 175 | - url : 验证 URL 176 | - number : 只能输入数字,其他输入不了,而且自带上下增大减小箭头,max 属性可以设置为最大值,min 可以设置为最小值,value 为默认值。 177 | - search : 输入框后面会给提供一个小叉,可以删除输入的内容,更加人性化。 178 | - range : 可以提供给一个范围,其中可以设置 max 和 min 以及 value,其中 value 属性可以设置为默认值 179 | - color : 提供了一个颜色拾取器 180 | - time : 时分秒 181 | - data : 日期选择年月日 182 | - datatime : 时间和日期(目前只有 Safari 支持) 183 | - datatime-local :日期时间控件 184 | - week :周控件 185 | - month:月控件 186 | 187 | **表单属性:** 188 | 189 | - placeholder :提示信息 190 | - autofocus :自动获取焦点 191 | - autocomplete=“on” 或者 autocomplete=“off” 使用这个属性需要有两个前提: 192 | 193 | - - 表单必须提交过 194 | - 必须有 name 属性。 195 | 196 | - required:要求输入框不能为空,必须有值才能够提交。 197 | - pattern=" " 里面写入想要的正则模式,例如手机号 patte="^(+86)?\d{10}$" 198 | - multiple:可以选择多个文件或者多个邮箱 199 | - form=" form 表单的 ID" 200 | 201 | **表单事件:** 202 | 203 | - oninput 每当 input 里的输入框内容发生变化都会触发此事件。 204 | - oninvalid 当验证不通过时触发此事件。 205 | 206 | #### 4. 进度条、度量器 207 | 208 | - progress 标签:用来表示任务的进度(IE、Safari 不支持),max 用来表示任务的进度,value 表示已完成多少 209 | - meter 属性:用来显示剩余容量或剩余库存(IE、Safari 不支持) 210 | 211 | - - high/low:规定被视作高/低的范围 212 | - max/min:规定最大/小值 213 | - value:规定当前度量值 214 | 215 | 设置规则:min < low < high < max 216 | 217 | #### 5.DOM 查询操作 218 | 219 | - `document.querySelector()` 220 | - `document.querySelectorAll()` 221 | 222 | 它们选择的对象可以是标签,可以是类(需要加点),可以是 ID(需要加#) 223 | 224 | #### 6. Web 存储 225 | 226 | HTML5 提供了两种在客户端存储数据的新方法: 227 | 228 | - localStorage - 没有时间限制的数据存储 229 | - sessionStorage - 针对一个 session 的数据存储 230 | 231 | #### 7. 其他 232 | 233 | - 拖放:拖放是一种常见的特性,即抓取对象以后拖到另一个位置。设置元素可拖放: 234 | 235 | ```html 236 | 237 | ``` 238 | 239 | - 画布(canvas ): canvas 元素使用 JavaScript 在网页上绘制图像。画布是一个矩形区域,可以控制其每一像素。canvas 拥有多种绘制路径、矩形、圆形、字符以及添加图像的方法。 240 | 241 | ```html 242 | 243 | ``` 244 | 245 | - SVG:SVG 指可伸缩矢量图形,用于定义用于网络的基于矢量的图形,使用 XML 格式定义图形,图像在放大或改变尺寸的情况下其图形质量不会有损失,它是万维网联盟的标准 246 | - 地理定位:Geolocation(地理定位)用于定位用户的位置。‘ 247 | 248 | **总结:** 249 | 250 | (1)新增语义化标签:nav、header、footer、aside、section、article 251 | 252 | (2)音频、视频标签:audio、video 253 | 254 | (3)数据存储:localStorage、sessionStorage 255 | 256 | (4)canvas(画布)、Geolocation(地理定位)、websocket(通信协议) 257 | 258 | (5)input 标签新增属性:placeholder、autocomplete、autofocus、required 259 | 260 | (6)history API:go、forward、back、pushstate 261 | 262 | **移除的元素有:** 263 | 264 | - 纯表现的元素:basefont,big,center,font, s,strike,tt,u; 265 | - 对可用性产生负面影响的元素:frame,frameset,noframes; 266 | 267 | ### 7. img 的 srcset 属性的作⽤? 268 | 269 | 响应式页面中经常用到根据屏幕密度设置不同的图片。这时就用到了 img 标签的 srcset 属性。srcset 属性用于设置不同屏幕密度下,img 会自动加载不同的图片。用法如下: 270 | 271 | ```html 272 | 273 | ``` 274 | 275 | 使用上面的代码,就能实现在屏幕密度为 1x 的情况下加载 image-128.png, 屏幕密度为 2x 时加载 image-256.png。 276 | 277 | 按照上面的实现,不同的屏幕密度都要设置图片地址,目前的屏幕密度有 1x,2x,3x,4x 四种,如果每一个图片都设置 4 张图片,加载就会很慢。所以就有了新的 srcset 标准。代码如下: 278 | 279 | ```html 280 | 283 | ``` 284 | 285 | 其中 srcset 指定图片的地址和对应的图片质量。sizes 用来设置图片的尺寸零界点。对于 srcset 中的 w 单位,可以理解成图片质量。如果可视区域小于这个质量的值,就可以使用。浏览器会自动选择一个最小的可用图片。 286 | 287 | sizes 语法如下: 288 | 289 | ``` 290 | sizes="[media query] [length], [media query] [length] ... " 291 | ``` 292 | 293 | sizes 就是指默认显示 128px, 如果视区宽度大于 360px, 则显示 340px。 294 | 295 | ### 8. 行内元素有哪些?块级元素有哪些? 空(void)元素有那些? 296 | 297 | - 行内元素有:`a b span img input select strong`; 298 | - 块级元素有:`div ul ol li dl dt dd h1 h2 h3 h4 h5 h6 p`; 299 | 300 | 空元素,即没有内容的 HTML 元素。空元素是在开始标签中关闭的,也就是空元素没有闭合标签: 301 | 302 | - 常见的有:`
`、`
`、``、``、``、``; 303 | - 鲜见的有:``、``、``、``、``、``、``、``、``、``、``。 304 | 305 | ### 9. 说一下 web worker 306 | 307 | 在 HTML 页面中,如果在执行脚本时,页面的状态是不可响应的,直到脚本执行完成后,页面才变成可响应。web worker 是运行在后台的 js,独立于其他脚本,不会影响页面的性能。 并且通过 postMessage 将结果回传到主线程。这样在进行复杂操作的时候,就不会阻塞主线程了。 308 | 309 | 如何创建 web worker: 310 | 311 | 1. 检测浏览器对于 web worker 的支持性 312 | 2. 创建 web worker 文件(js,回传函数等) 313 | 3. 创建 web worker 对象 314 | 315 | ### 10. HTML5 的离线储存怎么使用,它的工作原理是什么 316 | 317 | 离线存储指的是:在用户没有与因特网连接时,可以正常访问站点或应用,在用户与因特网连接时,更新用户机器上的缓存文件。 318 | 319 | **原理:**HTML5 的离线存储是基于一个新建的 `.appcache` 文件的缓存机制(不是存储技术),通过这个文件上的解析清单离线存储资源,这些资源就会像 cookie 一样被存储了下来。之后当网络在处于离线状态下时,浏览器会通过被离线存储的数据进行页面展示 320 | 321 | **使用方法:** 322 | 323 | (1)创建一个和 html 同名的 manifest 文件,然后在页面头部加入 manifest 属性: 324 | 325 | ```html 326 | 327 | ``` 328 | 329 | (2)在 `cache.manifest` 文件中编写需要离线存储的资源: 330 | 331 | ``` 332 | CACHE MANIFEST 333 | #v0.11 334 | CACHE: 335 | js/app.js 336 | css/style.css 337 | NETWORK: 338 | resourse/logo.png 339 | FALLBACK: 340 | / /offline.html 341 | ``` 342 | 343 | - **CACHE**: 表示需要离线存储的资源列表,由于包含 manifest 文件的页面将被自动离线存储,所以不需要把页面自身也列出来。 344 | - **NETWORK**: 表示在它下面列出来的资源只有在在线的情况下才能访问,他们不会被离线存储,所以在离线情况下无法使用这些资源。不过,如果在 CACHE 和 NETWORK 中有一个相同的资源,那么这个资源还是会被离线存储,也就是说 CACHE 的优先级更高。 345 | - **FALLBACK**: 表示如果访问第一个资源失败,那么就使用第二个资源来替换他,比如上面这个文件表示的就是如果访问根目录下任何一个资源失败了,那么就去访问 offline.html 。 346 | 347 | (3)在离线状态时,操作 `window.applicationCache` 进行离线缓存的操作。 348 | 349 | **如何更新缓存:** 350 | 351 | (1)更新 manifest 文件 352 | 353 | (2)通过 javascript 操作 354 | 355 | (3)清除浏览器缓存 356 | 357 | **注意事项:** 358 | 359 | (1)浏览器对缓存数据的容量限制可能不太一样(某些浏览器设置的限制是每个站点 5MB)。 360 | 361 | (2)如果 manifest 文件,或者内部列举的某一个文件不能正常下载,整个更新过程都将失败,浏览器继续全部使用老的缓存。 362 | 363 | (3)引用 manifest 的 html 必须与 manifest 文件同源,在同一个域下。 364 | 365 | (4)FALLBACK 中的资源必须和 manifest 文件同源。 366 | 367 | (5)当一个资源被缓存后,该浏览器直接请求这个绝对路径也会访问缓存中的资源。 368 | 369 | (6)站点中的其他页面即使没有设置 manifest 属性,请求的资源如果在缓存中也从缓存中访问。 370 | 371 | (7)当 manifest 文件发生改变时,资源请求本身也会触发更新。 372 | 373 | ### 11. 浏览器是如何对 HTML5 的离线储存资源进行管理和加载? 374 | 375 | - **在线的情况下**,浏览器发现 html 头部有 manifest 属性,它会请求 manifest 文件,如果是第一次访问页面 ,那么浏览器就会根据 manifest 文件的内容下载相应的资源并且进行离线存储。如果已经访问过页面并且资源已经进行离线存储了,那么浏览器就会使用离线的资源加载页面,然后浏览器会对比新的 manifest 文件与旧的 manifest 文件,如果文件没有发生改变,就不做任何操作,如果文件改变了,就会重新下载文件中的资源并进行离线存储。 376 | - **离线的情况下**,浏览器会直接使用离线存储的资源。 377 | 378 | ### 12. title 与 h1 的区别、b 与 strong 的区别、i 与 em 的区别?---了解即可 379 | 380 | - strong 标签有语义,是起到加重语气的效果,而 b 标签是没有的,b 标签只是一个简单加粗标签。b 标签之间的字符都设为粗体,strong 标签加强字符的语气都是通过粗体来实现的,而搜索引擎更侧重 strong 标签。 381 | - title 属性没有明确意义只表示是个标题,H1 则表示层次明确的标题,对页面信息的抓取有很大的影响 382 | - **i 内容展示为斜体,em 表示强调的文本** 383 | 384 | ### 13. **iframe 有那些优点和缺点?** 385 | 386 | iframe 元素会创建包含另外一个文档的内联框架(即行内框架)。 387 | 388 | **优点:** 389 | 390 | - 用来加载速度较慢的内容(如广告) 391 | - 可以使脚本可以并行下载 392 | - 可以实现跨子域通信 393 | 394 | **缺点:** 395 | 396 | - iframe 会阻塞主页面的 onload 事件 397 | - 无法被一些搜索引擎索识别 398 | - 会产生很多页面,不容易管理 399 | 400 | ### 14. label 的作用是什么?如何使用?---了解即可 401 | 402 | label 标签来定义表单控件的关系:当用户选择 label 标签时,浏览器会自动将焦点转到和 label 标签相关的表单控件上。 403 | 404 | - 使用方法 1: 405 | 406 | ```html 407 | 408 | 409 | ``` 410 | 411 | - 使用方法 2: 412 | 413 | ```html 414 | 415 | ``` 416 | 417 | ### 15. Canvas 和 SVG 的区别 418 | 419 | **(1)SVG:** 420 | 421 | SVG 可缩放矢量图形(Scalable Vector Graphics)是基于可扩展标记语言 XML 描述的 2D 图形的语言,SVG 基于 XML 就意味着 SVG DOM 中的每个元素都是可用的,可以为某个元素附加 Javascript 事件处理器。在 SVG 中,每个被绘制的图形均被视为对象。如果 SVG 对象的属性发生变化,那么浏览器能够自动重现图形。 422 | 423 | 其特点如下: 424 | 425 | - 不依赖分辨率 426 | - 支持事件处理器 427 | - 最适合带有大型渲染区域的应用程序(比如谷歌地图) 428 | - 复杂度高会减慢渲染速度(任何过度使用 DOM 的应用都不快) 429 | - 不适合游戏应用 430 | 431 | **(2)Canvas:** 432 | 433 | Canvas 是画布,通过 Javascript 来绘制 2D 图形,是逐像素进行渲染的。其位置发生改变,就会重新进行绘制。 434 | 435 | 其特点如下: 436 | 437 | - 依赖分辨率 438 | - 不支持事件处理器 439 | - 弱的文本渲染能力 440 | - 能够以 .png 或 .jpg 格式保存结果图像 441 | - 最适合图像密集型的游戏,其中的许多对象会被频繁重绘 442 | 443 | 注:矢量图,也称为面向对象的图像或绘图图像,在数学上定义为一系列由线连接的点。矢量文件中的图形元素称为对象。每个对象都是一个自成一体的实体,它具有颜色、形状、轮廓、大小和屏幕位置等属性。 444 | 445 | ### 16. head 标签有什么作用,其中什么标签必不可少? 446 | 447 | 标签用于定义文档的头部,它是所有头部元素的容器。 中的元素可以引用脚本、指示浏览器在哪里找到样式表、提供元信息等。 448 | 449 | 文档的头部描述了文档的各种属性和信息,包括文档的标题、在 Web 中的位置以及和其他文档的关系等。绝大多数文档头部包含的数据都不会真正作为内容显示给读者。 450 | 451 | 下面这些标签可用在 head 部分:, , , 163 | ``` 164 | 165 | ### 4. 懒加载与预加载的区别 166 | 167 | 这两种方式都是提高网页性能的方式,两者主要区别是一个是提前加载,一个是迟缓甚至不加载。懒加载对服务器前端有一定的缓解压力作用,预加载则会增加服务器前端压力。 168 | 169 | - **懒加载也叫延迟加载,指的是在长网页中延迟加载图片的时机,当用户需要访问时,再去加载**,这样可以提高网站的首屏加载速度,提升用户的体验,并且可以减少服务器的压力。它适用于图片很多,页面很长的电商网站的场景。懒加载的实现原理是,将页面上的图片的 src 属性设置为空字符串,将图片的真实路径保存在一个自定义属性中,当页面滚动的时候,进行判断,如果图片进入页面可视区域内,则从自定义属性中取出真实路径赋值给图片的 src 属性,以此来实现图片的延迟加载。 170 | - **预加载指的是将所需的资源提前请求加载到本地,这样后面在需要用到时就直接从缓存取资源。**通过预加载能够减少用户的等待时间,提高用户的体验。我了解的预加载的最常用的方式是使用 js 中的 image 对象,通过为 image 对象来设置 scr 属性,来实现图片的预加载。 171 | 172 | ## 三、回流与重绘 173 | 174 | ### 1. 回流与重绘的概念及触发条件 175 | 176 | #### (1)回流 177 | 178 | 当渲染树中部分或者全部元素的尺寸、结构或者属性发生变化时,浏览器会重新渲染部分或者全部文档的过程就称为**回流**。 179 | 180 | 181 | 182 | 下面这些操作会导致回流: 183 | 184 | - 页面的首次渲染 185 | - 浏览器的窗口大小发生变化 186 | - 元素的内容发生变化 187 | - 元素的尺寸或者位置发生变化 188 | - 元素的字体大小发生变化 189 | - 激活CSS伪类 190 | - 查询某些属性或者调用某些方法 191 | - 添加或者删除可见的DOM元素 192 | 193 | 194 | 195 | 在触发回流(重排)的时候,由于浏览器渲染页面是基于流式布局的,所以当触发回流时,会导致周围的DOM元素重新排列,它的影响范围有两种: 196 | 197 | - 全局范围:从根节点开始,对整个渲染树进行重新布局 198 | - 局部范围:对渲染树的某部分或者一个渲染对象进行重新布局 199 | 200 | #### (2)重绘 201 | 202 | 当页面中某些元素的样式发生变化,但是不会影响其在文档流中的位置时,浏览器就会对元素进行重新绘制,这个过程就是**重绘**。 203 | 204 | 205 | 206 | 下面这些操作会导致回流: 207 | 208 | - color、background 相关属性:background-color、background-image 等 209 | - outline 相关属性:outline-color、outline-width 、text-decoration 210 | - border-radius、visibility、box-shadow 211 | 212 | 注意: **当触发回流时,一定会触发重绘,但是重绘不一定会引发回流。** 213 | 214 | ### 2. 如何避免回流与重绘? 215 | 216 | **减少回流与重绘的措施:** 217 | 218 | - 操作DOM时,尽量在低层级的DOM节点进行操作 219 | - 不要使用`table`布局, 一个小的改动可能会使整个`table`进行重新布局 220 | - 使用CSS的表达式 221 | - 不要频繁操作元素的样式,对于静态页面,可以修改类名,而不是样式。 222 | - 使用absolute或者fixed,使元素脱离文档流,这样他们发生变化就不会影响其他元素 223 | - 避免频繁操作DOM,可以创建一个文档片段`documentFragment`,在它上面应用所有DOM操作,最后再把它添加到文档中 224 | - 将元素先设置`display: none`,操作结束后再把它显示出来。因为在display属性为none的元素上进行的DOM操作不会引发回流和重绘。 225 | - 将DOM的多个读操作(或者写操作)放在一起,而不是读写操作穿插着写。这得益于**浏览器的渲染队列机制**。 226 | 227 | 228 | 229 | 浏览器针对页面的回流与重绘,进行了自身的优化——**渲染队列** 230 | 231 | 232 | 233 | **浏览器会将所有的回流、重绘的操作放在一个队列中,当队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会对队列进行批处理。这样就会让多次的回流、重绘变成一次回流重绘。** 234 | 235 | 236 | 237 | 上面,将多个读操作(或者写操作)放在一起,就会等所有的读操作进入队列之后执行,这样,原本应该是触发多次回流,变成了只触发一次回流。 238 | 239 | ### 3. 如何优化动画? 240 | 241 | 对于如何优化动画,我们知道,一般情况下,动画需要频繁的操作DOM,就就会导致页面的性能问题,我们可以将动画的`position`属性设置为`absolute`或者`fixed`,将动画脱离文档流,这样他的回流就不会影响到页面了。 242 | 243 | ### 4. documentFragment 是什么?用它跟直接操作 DOM 的区别是什么? 244 | 245 | MDN中对`documentFragment`的解释: 246 | 247 | > DocumentFragment,文档片段接口,一个没有父对象的最小文档对象。它被作为一个轻量版的 Document使用,就像标准的document一样,存储由节点(nodes)组成的文档结构。与document相比,最大的区别是DocumentFragment不是真实 DOM 树的一部分,它的变化不会触发 DOM 树的重新渲染,且不会导致性能等问题。 248 | 249 | 250 | 251 | 当我们把一个 DocumentFragment 节点插入文档树时,插入的不是 DocumentFragment 自身,而是它的所有子孙节点。在频繁的DOM操作时,我们就可以将DOM元素插入DocumentFragment,之后一次性的将所有的子孙节点插入文档中。和直接操作DOM相比,将DocumentFragment 节点插入DOM树时,不会触发页面的重绘,这样就大大提高了页面的性能。 252 | 253 | ## 四、节流与防抖 254 | 255 | ### 1. 对节流与防抖的理解 256 | 257 | - 函数防抖是指在事件被触发 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则重新计时。这可以使用在一些点击请求的事件上,避免因为用户的多次点击向后端发送多次请求。 258 | - 函数节流是指规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。节流可以使用在 scroll 函数的事件监听上,通过事件节流来降低事件调用的频率。 259 | 260 | 261 | 262 | **防抖函数的应用场景:** 263 | 264 | - 按钮提交场景:防⽌多次提交按钮,只执⾏最后提交的⼀次 265 | - 服务端验证场景:表单验证需要服务端配合,只执⾏⼀段连续的输⼊事件的最后⼀次,还有搜索联想词功能类似⽣存环境请⽤lodash.debounce 266 | 267 | 268 | 269 | **节流函数的****适⽤场景:** 270 | 271 | - 拖拽场景:固定时间内只执⾏⼀次,防⽌超⾼频次触发位置变动 272 | - 缩放场景:监控浏览器resize 273 | - 动画场景:避免短时间内多次触发动画引起性能问题 274 | 275 | ### 2. 实现节流函数和防抖函数 276 | 277 | **函数防抖的实现:** 278 | 279 | ``` 280 | function debounce(fn, wait) { 281 | var timer = null; 282 | 283 | return function() { 284 | var context = this, 285 | args = [...arguments]; 286 | 287 | // 如果此时存在定时器的话,则取消之前的定时器重新记时 288 | if (timer) { 289 | clearTimeout(timer); 290 | timer = null; 291 | } 292 | 293 | // 设置定时器,使事件间隔指定事件后执行 294 | timer = setTimeout(() => { 295 | fn.apply(context, args); 296 | }, wait); 297 | }; 298 | } 299 | ``` 300 | 301 | **函数节流的实现:** 302 | 303 | ``` 304 | // 时间戳版 305 | function throttle(fn, delay) { 306 | var preTime = Date.now(); 307 | 308 | return function() { 309 | var context = this, 310 | args = [...arguments], 311 | nowTime = Date.now(); 312 | 313 | // 如果两次时间间隔超过了指定时间,则执行函数。 314 | if (nowTime - preTime >= delay) { 315 | preTime = Date.now(); 316 | return fn.apply(context, args); 317 | } 318 | }; 319 | } 320 | 321 | // 定时器版 322 | function throttle (fun, wait){ 323 | let timeout = null 324 | return function(){ 325 | let context = this 326 | let args = [...arguments] 327 | if(!timeout){ 328 | timeout = setTimeout(() => { 329 | fun.apply(context, args) 330 | timeout = null 331 | }, wait) 332 | } 333 | } 334 | } 335 | ``` 336 | 337 | ## 五、图片优化 338 | 339 | ### 1. 如何对项目中的图片进行优化? 340 | 341 | 1. 不用图片。很多时候会使用到很多修饰类图片,其实这类修饰图片完全可以用 CSS 去代替。 342 | 2. 对于移动端来说,屏幕宽度就那么点,完全没有必要去加载原图浪费带宽。一般图片都用 CDN 加载,可以计算出适配屏幕的宽度,然后去请求相应裁剪好的图片。 343 | 3. 小图使用 base64 格式 344 | 4. 将多个图标文件整合到一张图片中(雪碧图) 345 | 5. 选择正确的图片格式: 346 | 347 | - - 对于能够显示 WebP 格式的浏览器尽量使用 WebP 格式。因为 WebP 格式具有更好的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量,缺点就是兼容性并不好 348 | - 小图使用 PNG,其实对于大部分图标这类图片,完全可以使用 SVG 代替 349 | - 照片使用 JPEG 350 | 351 | ### 2. 常见的图片格式及使用场景 352 | 353 | (1)**BMP**,是无损的、既支持索引色也支持直接色的点阵图。这种图片格式几乎没有对数据进行压缩,所以BMP格式的图片通常是较大的文件。 354 | 355 | 356 | 357 | (2)**GIF**是无损的、采用索引色的点阵图。采用LZW压缩算法进行编码。文件小,是GIF格式的优点,同时,GIF格式还具有支持动画以及透明的优点。但是GIF格式仅支持8bit的索引色,所以GIF格式适用于对色彩要求不高同时需要文件体积较小的场景。 358 | 359 | 360 | 361 | (3)**JPEG**是有损的、采用直接色的点阵图。JPEG的图片的优点是采用了直接色,得益于更丰富的色彩,JPEG非常适合用来存储照片,与GIF相比,JPEG不适合用来存储企业Logo、线框类的图。因为有损压缩会导致图片模糊,而直接色的选用,又会导致图片文件较GIF更大。 362 | 363 | 364 | 365 | (4)**PNG-8**是无损的、使用索引色的点阵图。PNG是一种比较新的图片格式,PNG-8是非常好的GIF格式替代者,在可能的情况下,应该尽可能的使用PNG-8而不是GIF,因为在相同的图片效果下,PNG-8具有更小的文件体积。除此之外,PNG-8还支持透明度的调节,而GIF并不支持。除非需要动画的支持,否则没有理由使用GIF而不是PNG-8。 366 | 367 | 368 | 369 | (5)**PNG-24**是无损的、使用直接色的点阵图。PNG-24的优点在于它压缩了图片的数据,使得同样效果的图片,PNG-24格式的文件大小要比BMP小得多。当然,PNG24的图片还是要比JPEG、GIF、PNG-8大得多。 370 | 371 | 372 | 373 | (6)**SVG**是无损的矢量图。SVG是矢量图意味着SVG图片由直线和曲线以及绘制它们的方法组成。当放大SVG图片时,看到的还是线和曲线,而不会出现像素点。这意味着SVG图片在放大时,不会失真,所以它非常适合用来绘制Logo、Icon等。 374 | 375 | 376 | 377 | (7)**WebP**是谷歌开发的一种新图片格式,WebP是同时支持有损和无损压缩的、使用直接色的点阵图。从名字就可以看出来它是为Web而生的,什么叫为Web而生呢?就是说相同质量的图片,WebP具有更小的文件体积。现在网站上充满了大量的图片,如果能够降低每一个图片的文件大小,那么将大大减少浏览器和服务器之间的数据传输量,进而降低访问延迟,提升访问体验。目前只有Chrome浏览器和Opera浏览器支持WebP格式,兼容性不太好。 378 | 379 | - 在无损压缩的情况下,相同质量的WebP图片,文件大小要比PNG小26%; 380 | - 在有损压缩的情况下,具有相同图片精度的WebP图片,文件大小要比JPEG小25%~34%; 381 | - WebP图片格式支持图片透明度,一个无损压缩的WebP图片,如果要支持透明度只需要22%的格外文件大小。 382 | 383 | ## 六、Webpack优化 384 | 385 | ### 1. 如何提⾼**webpack**的打包速度**?** 386 | 387 | #### (1)优化 Loader 388 | 389 | 对于 Loader 来说,影响打包效率首当其冲必属 Babel 了。因为 Babel 会将代码转为字符串生成 AST,然后对 AST 继续进行转变最后再生成新的代码,项目越大,**转换代码越多,效率就越低**。当然了,这是可以优化的。 390 | 391 | 392 | 393 | 首先我们**优化 Loader 的文件搜索范围** 394 | 395 | ``` 396 | module.exports = { 397 | module: { 398 | rules: [ 399 | { 400 | // js 文件才使用 babel 401 | test: /\.js$/, 402 | loader: 'babel-loader', 403 | // 只在 src 文件夹下查找 404 | include: [resolve('src')], 405 | // 不会去查找的路径 406 | exclude: /node_modules/ 407 | } 408 | ] 409 | } 410 | } 411 | ``` 412 | 413 | 对于 Babel 来说,希望只作用在 JS 代码上的,然后 `node_modules` 中使用的代码都是编译过的,所以完全没有必要再去处理一遍。 414 | 415 | 416 | 417 | 当然这样做还不够,还可以将 Babel 编译过的文件**缓存**起来,下次只需要编译更改过的代码文件即可,这样可以大幅度加快打包时间 418 | 419 | ``` 420 | loader: 'babel-loader?cacheDirectory=true' 421 | ``` 422 | 423 | #### (2)HappyPack 424 | 425 | 受限于 Node 是单线程运行的,所以 Webpack 在打包的过程中也是单线程的,特别是在执行 Loader 的时候,长时间编译的任务很多,这样就会导致等待的情况。 426 | 427 | 428 | 429 | **HappyPack 可以将 Loader 的同步执行转换为并行的**,这样就能充分利用系统资源来加快打包效率了 430 | 431 | ``` 432 | module: { 433 | loaders: [ 434 | { 435 | test: /\.js$/, 436 | include: [resolve('src')], 437 | exclude: /node_modules/, 438 | // id 后面的内容对应下面 439 | loader: 'happypack/loader?id=happybabel' 440 | } 441 | ] 442 | }, 443 | plugins: [ 444 | new HappyPack({ 445 | id: 'happybabel', 446 | loaders: ['babel-loader?cacheDirectory'], 447 | // 开启 4 个线程 448 | threads: 4 449 | }) 450 | ] 451 | ``` 452 | 453 | #### (3)DllPlugin 454 | 455 | **DllPlugin 可以将特定的类库提前打包然后引入**。这种方式可以极大的减少打包类库的次数,只有当类库更新版本才有需要重新打包,并且也实现了将公共代码抽离成单独文件的优化方案。DllPlugin的使用方法如下: 456 | 457 | ``` 458 | // 单独配置在一个文件中 459 | // webpack.dll.conf.js 460 | const path = require('path') 461 | const webpack = require('webpack') 462 | module.exports = { 463 | entry: { 464 | // 想统一打包的类库 465 | vendor: ['react'] 466 | }, 467 | output: { 468 | path: path.join(__dirname, 'dist'), 469 | filename: '[name].dll.js', 470 | library: '[name]-[hash]' 471 | }, 472 | plugins: [ 473 | new webpack.DllPlugin({ 474 | // name 必须和 output.library 一致 475 | name: '[name]-[hash]', 476 | // 该属性需要与 DllReferencePlugin 中一致 477 | context: __dirname, 478 | path: path.join(__dirname, 'dist', '[name]-manifest.json') 479 | }) 480 | ] 481 | } 482 | ``` 483 | 484 | 然后需要执行这个配置文件生成依赖文件,接下来需要使用 `DllReferencePlugin` 将依赖文件引入项目中 485 | 486 | ``` 487 | // webpack.conf.js 488 | module.exports = { 489 | // ...省略其他配置 490 | plugins: [ 491 | new webpack.DllReferencePlugin({ 492 | context: __dirname, 493 | // manifest 就是之前打包出来的 json 文件 494 | manifest: require('./dist/vendor-manifest.json'), 495 | }) 496 | ] 497 | } 498 | ``` 499 | 500 | #### (4)代码压缩 501 | 502 | 在 Webpack3 中,一般使用 `UglifyJS` 来压缩代码,但是这个是单线程运行的,为了加快效率,可以使用 `webpack-parallel-uglify-plugin` 来并行运行 `UglifyJS`,从而提高效率。 503 | 504 | 505 | 506 | 在 Webpack4 中,不需要以上这些操作了,只需要将 `mode` 设置为 `production` 就可以默认开启以上功能。代码压缩也是我们必做的性能优化方案,当然我们不止可以压缩 JS 代码,还可以压缩 HTML、CSS 代码,并且在压缩 JS 代码的过程中,我们还可以通过配置实现比如删除 `console.log` 这类代码的功能。 507 | 508 | #### (5)其他 509 | 510 | 可以通过一些小的优化点来加快打包速度 511 | 512 | - `resolve.extensions`:用来表明文件后缀列表,默认查找顺序是 `['.js', '.json']`,如果你的导入文件没有添加后缀就会按照这个顺序查找文件。我们应该尽可能减少后缀列表长度,然后将出现频率高的后缀排在前面 513 | - `resolve.alias`:可以通过别名的方式来映射一个路径,能让 Webpack 更快找到路径 514 | - `module.noParse`:如果你确定一个文件下没有其他依赖,就可以使用该属性让 Webpack 不扫描该文件,这种方式对于大型的类库很有帮助 515 | 516 | ### 2. 如何减少 Webpack 打包体积 517 | 518 | #### (1)按需加载 519 | 520 | 在开发 SPA 项目的时候,项目中都会存在很多路由页面。如果将这些页面全部打包进一个 JS 文件的话,虽然将多个请求合并了,但是同样也加载了很多并不需要的代码,耗费了更长的时间。那么为了首页能更快地呈现给用户,希望首页能加载的文件体积越小越好,**这时候就可以使用按需加载,将每个路由页面单独打包为一个文件**。当然不仅仅路由可以按需加载,对于 `loadash` 这种大型类库同样可以使用这个功能。 521 | 522 | 523 | 524 | 按需加载的代码实现这里就不详细展开了,因为鉴于用的框架不同,实现起来都是不一样的。当然了,虽然他们的用法可能不同,但是底层的机制都是一样的。都是当使用的时候再去下载对应文件,返回一个 `Promise`,当 `Promise` 成功以后去执行回调。 525 | 526 | #### (2)Scope Hoisting 527 | 528 | **Scope Hoisting 会分析出模块之间的依赖关系,尽可能的把打包出来的模块合并到一个函数中去。** 529 | 530 | 531 | 532 | 比如希望打包两个文件: 533 | 534 | ``` 535 | // test.js 536 | export const a = 1 537 | // index.js 538 | import { a } from './test.js' 539 | ``` 540 | 541 | 对于这种情况,打包出来的代码会类似这样: 542 | 543 | ``` 544 | [ 545 | /* 0 */ 546 | function (module, exports, require) { 547 | //... 548 | }, 549 | /* 1 */ 550 | function (module, exports, require) { 551 | //... 552 | } 553 | ] 554 | ``` 555 | 556 | 但是如果使用 Scope Hoisting ,代码就会尽可能的合并到一个函数中去,也就变成了这样的类似代码: 557 | 558 | ``` 559 | [ 560 | /* 0 */ 561 | function (module, exports, require) { 562 | //... 563 | } 564 | ] 565 | ``` 566 | 567 | 这样的打包方式生成的代码明显比之前的少多了。如果在 Webpack4 中你希望开启这个功能,只需要启用 `optimization.concatenateModules` 就可以了: 568 | 569 | ``` 570 | module.exports = { 571 | optimization: { 572 | concatenateModules: true 573 | } 574 | } 575 | ``` 576 | 577 | #### (3)Tree Shaking 578 | 579 | **Tree Shaking 可以实现删除项目中未被引用的代码**,比如: 580 | 581 | ``` 582 | // test.js 583 | export const a = 1 584 | export const b = 2 585 | // index.js 586 | import { a } from './test.js' 587 | ``` 588 | 589 | 对于以上情况,`test` 文件中的变量 `b` 如果没有在项目中使用到的话,就不会被打包到文件中。 590 | 591 | 592 | 593 | 如果使用 Webpack 4 的话,开启生产环境就会自动启动这个优化功能。 594 | 595 | ### 3. 如何⽤**webpack**来优化前端性能? 596 | 597 | ⽤webpack优化前端性能是指优化webpack的输出结果,让打包的最终结果在浏览器运⾏快速⾼效。 598 | 599 | - **压缩代码**:删除多余的代码、注释、简化代码的写法等等⽅式。可以利⽤webpack的 UglifyJsPlugin 和 ParallelUglifyPlugin 来压缩JS⽂件, 利⽤ cssnano (css-loader?minimize)来压缩css 600 | - **利⽤****CDN****加速**: 在构建过程中,将引⽤的静态资源路径修改为CDN上对应的路径。可以利⽤webpack对于 output 参数和各loader的 publicPath 参数来修改资源路径 601 | - **Tree Shaking**: 将代码中永远不会⾛到的⽚段删除掉。可以通过在启动webpack时追加参数 --optimize-minimize 来实现 602 | - **Code Splitting:** 将代码按路由维度或者组件分块(chunk),这样做到按需加载,同时可以充分利⽤浏览器缓存 603 | - **提取公共第三⽅库**: SplitChunksPlugin插件来进⾏公共模块抽取,利⽤浏览器缓存可以⻓期缓存这些⽆需频繁变动的公共代码 604 | 605 | ### 4. 如何提⾼**webpack**的构建速度? 606 | 607 | 1. 多⼊⼝情况下,使⽤ CommonsChunkPlugin 来提取公共代码 608 | 2. 通过 externals 配置来提取常⽤库 609 | 3. 利⽤ DllPlugin 和 DllReferencePlugin 预编译资源模块 通过 DllPlugin 来对那些我们引⽤但是绝对不会修改的npm包来进⾏预编译,再通过 DllReferencePlugin 将预编译的模块加载进来。 610 | 4. 使⽤ Happypack 实现多线程加速编译 611 | 5. 使⽤ webpack-uglify-parallel 来提升 uglifyPlugin 的压缩速度。 原理上 webpack-uglify-parallel 采⽤了多核并⾏压缩来提升压缩速度 612 | 6. 使⽤ Tree-shaking 和 Scope Hoisting 来剔除多余代码 -------------------------------------------------------------------------------- /7 offer收割机之前端工程化篇.md: -------------------------------------------------------------------------------- 1 | ## 一、Git 2 | 3 | ### 1. git 和 svn 的区别 4 | 5 | - git 和 svn 最大的区别在于 git 是分布式的,而 svn 是集中式的。因此我们不能再离线的情况下使用 svn。如果服务器出现问题,就没有办法使用 svn 来提交代码。 6 | - svn 中的分支是整个版本库的复制的一份完整目录,而 git 的分支是指针指向某次提交,因此 git 的分支创建更加开销更小并且分支上的变化不会影响到其他人。svn 的分支变化会影响到所有的人。 7 | - svn 的指令相对于 git 来说要简单一些,比 git 更容易上手。 8 | - **GIT把内容按元数据方式存储,而SVN是按文件:**因为git目录是处于个人机器上的一个克隆版的版本库,它拥有中心版本库上所有的东西,例如标签,分支,版本记录等。 9 | - **GIT分支和SVN的分支不同:**svn会发生分支遗漏的情况,而git可以同一个工作目录下快速的在几个分支间切换,很容易发现未被合并的分支,简单而快捷的合并这些文件。 10 | - **GIT没有一个全局的版本号,而SVN有** 11 | - **GIT的内容完整性要优于SVN:**GIT的内容存储使用的是SHA-1哈希算法。这能确保代码内容的完整性,确保在遇到磁盘故障和网络问题时降低对版本库的破坏 12 | 13 | ### 2. 经常使用的 git 命令? 14 | 15 | ``` 16 | git init // 新建 git 代码库 17 | git add // 添加指定文件到暂存区 18 | git rm // 删除工作区文件,并且将这次删除放入暂存区 19 | git commit -m [message] // 提交暂存区到仓库区 20 | git branch // 列出所有分支 21 | git checkout -b [branch] // 新建一个分支,并切换到该分支 22 | git status // 显示有变更文件的状态 23 | ``` 24 | 25 | ### 3. git pull 和 git fetch 的区别 26 | 27 | - git fetch 只是将远程仓库的变化下载下来,并没有和本地分支合并。 28 | - git pull 会将远程仓库的变化下载下来,并和当前分支合并。 29 | 30 | ### 4. git rebase 和 git merge 的区别 31 | 32 | git merge 和 git rebase 都是用于分支合并,关键**在** **commit 记录的处理上不同**: 33 | 34 | - git merge 会新建一个新的 commit 对象,然后两个分支以前的 commit 记录都指向这个新 commit 记录。这种方法会保留之前每个分支的 commit 历史。 35 | - git rebase 会先找到两个分支的第一个共同的 commit 祖先记录,然后将提取当前分支这之后的所有 commit 记录,然后将这个 commit 记录添加到目标分支的最新提交后面。经过这个合并后,两个分支合并后的 commit 记录就变为了线性的记录了。 36 | 37 | ## 二、Webpack 38 | 39 | ### 1. **webpack**与**grunt**、**gulp**的不同? 40 | 41 | **Grunt****、Gulp是基于任务运⾏的⼯具**: 它们会⾃动执⾏指定的任务,就像流⽔线,把资源放上去然后通过不同插件进⾏加⼯,它们包含活跃的社区,丰富的插件,能⽅便的打造各种⼯作流。 42 | 43 | 44 | 45 | **Webpack是基于模块化打包的⼯具:** ⾃动化处理模块,webpack把⼀切当成模块,当 webpack 处理应⽤程序时,它会递归地构建⼀个依赖关系图 (dependency graph),其中包含应⽤程序需要的每个模块,然后将所有这些模块打包成⼀个或多个 bundle。 46 | 47 | 48 | 49 | 因此这是完全不同的两类⼯具,⽽现在主流的⽅式是⽤npm script代替Grunt、Gulp,npm script同样可以打造任务流。 50 | 51 | ### 2. **webpack**、**rollup**、**parcel**优劣? 52 | 53 | - webpack适⽤于⼤型复杂的前端站点构建: webpack有强⼤的loader和插件⽣态,打包后的⽂件实际上就是⼀个⽴即执⾏函数,这个⽴即执⾏函数接收⼀个参数,这个参数是模块对象,键为各个模块的路径,值为模块内容。⽴即执⾏函数内部则处理模块之间的引⽤,执⾏模块等,这种情况更适合⽂件依赖复杂的应⽤开发。 54 | - rollup适⽤于基础库的打包,如vue、d3等: Rollup 就是将各个模块打包进⼀个⽂件中,并且通过 Tree-shaking 来删除⽆⽤的代码,可以最⼤程度上降低代码体积,但是rollup没有webpack如此多的的如代码分割、按需加载等⾼级功能,其更聚焦于库的打包,因此更适合库的开发。 55 | - parcel适⽤于简单的实验性项⽬: 他可以满⾜低⻔槛的快速看到效果,但是⽣态差、报错信息不够全⾯都是他的硬伤,除了⼀些玩具项⽬或者实验项⽬不建议使⽤。 56 | 57 | ### 3. 有哪些常⻅的**Loader**? 58 | 59 | - file-loader:把⽂件输出到⼀个⽂件夹中,在代码中通过相对 URL 去引⽤输出的⽂件 60 | - url-loader:和 file-loader 类似,但是能在⽂件很⼩的情况下以 base64 的⽅式把⽂件内容注⼊到代码中去 61 | - source-map-loader:加载额外的 Source Map ⽂件,以⽅便断点调试 62 | - image-loader:加载并且压缩图⽚⽂件 63 | - babel-loader:把 ES6 转换成 ES5 64 | - css-loader:加载 CSS,⽀持模块化、压缩、⽂件导⼊等特性 65 | - style-loader:把 CSS 代码注⼊到 JavaScript 中,通过 DOM 操作去加载 CSS。 66 | - eslint-loader:通过 ESLint 检查 JavaScript 代码 67 | 68 | 69 | 70 | **注意:**在Webpack中,loader的执行顺序是**从右向左**执行的。因为webpack选择了**compose这样的函数式编程方式**,这种方式的表达式执行是从右向左的。 71 | 72 | ### 4. 有哪些常⻅的**Plugin**? 73 | 74 | - define-plugin:定义环境变量 75 | - html-webpack-plugin:简化html⽂件创建 76 | - uglifyjs-webpack-plugin:通过 UglifyES 压缩 ES6 代码 77 | - webpack-parallel-uglify-plugin: 多核压缩,提⾼压缩速度 78 | - webpack-bundle-analyzer: 可视化webpack输出⽂件的体积 79 | - mini-css-extract-plugin: CSS提取到单独的⽂件中,⽀持按需加载 80 | 81 | ### 5. **bundle**,**chunk**,**module**是什么? 82 | 83 | - bundle:是由webpack打包出来的⽂件; 84 | - chunk:代码块,⼀个chunk由多个模块组合⽽成,⽤于代码的合并和分割; 85 | - module:是开发中的单个模块,在webpack的世界,⼀切皆模块,⼀个模块对应⼀个⽂件,webpack会从配置的 entry中递归开始找出所有依赖的模块。 86 | 87 | ### 6. **Loader**和**Plugin**的不同? 88 | 89 | 不同的作⽤: 90 | 91 | - **Loader**直译为"加载器"。Webpack将⼀切⽂件视为模块,但是webpack原⽣是只能解析js⽂件,如果想将其他⽂件也打包的话,就会⽤到 loader 。 所以Loader的作⽤是让webpack拥有了加载和解析⾮JavaScript⽂件的能⼒。 92 | - **Plugin**直译为"插件"。Plugin可以扩展webpack的功能,让webpack具有更多的灵活性。 在 Webpack 运⾏的⽣命周期中会⼴播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。 93 | 94 | 95 | 96 | **不同的⽤法****:** 97 | 98 | - **Loader**在 module.rules 中配置,也就是说他作为模块的解析规则⽽存在。 类型为数组,每⼀项都是⼀个 Object ,⾥⾯描述了对于什么类型的⽂件( test ),使⽤什么加载( loader )和使⽤的参数( options ) 99 | - **Plugin**在 plugins 中单独配置。 类型为数组,每⼀项是⼀个 plugin 的实例,参数都通过构造函数传⼊。 100 | 101 | ### 7. **webpack**的构建流程**?** 102 | 103 | Webpack 的运⾏流程是⼀个串⾏的过程,从启动到结束会依次执⾏以下流程: 104 | 105 | 1. 初始化参数:从配置⽂件和 Shell 语句中读取与合并参数,得出最终的参数; 106 | 2. 开始编译:⽤上⼀步得到的参数初始化 Compiler 对象,加载所有配置的插件,执⾏对象的 run ⽅法开始执⾏编译; 107 | 3. 确定⼊⼝:根据配置中的 entry 找出所有的⼊⼝⽂件; 108 | 4. 编译模块:从⼊⼝⽂件出发,调⽤所有配置的 Loader 对模块进⾏翻译,再找出该模块依赖的模块,再递归本步骤直到所有⼊⼝依赖的⽂件都经过了本步骤的处理; 109 | 5. 完成模块编译:在经过第4步使⽤ Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系; 110 | 6. 输出资源:根据⼊⼝和模块之间的依赖关系,组装成⼀个个包含多个模块的 Chunk,再把每个 Chunk 转换成⼀个单独的⽂件加⼊到输出列表,这步是可以修改输出内容的最后机会; 111 | 7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和⽂件名,把⽂件内容写⼊到⽂件系统。 112 | 113 | 114 | 115 | 在以上过程中,Webpack 会在特定的时间点⼴播出特定的事件,插件在监听到感兴趣的事件后会执⾏特定的逻辑,并且插件可以调⽤ Webpack 提供的 API 改变 Webpack 的运⾏结果。 116 | 117 | ### 8. 编写**loader**或**plugin**的思路? 118 | 119 | Loader像⼀个"翻译官"把读到的源⽂件内容转义成新的⽂件内容,并且每个Loader通过链式操作,将源⽂件⼀步步翻译成想要的样⼦。 120 | 121 | 122 | 123 | 编写Loader时要遵循单⼀原则,每个Loader只做⼀种"转义"⼯作。 每个Loader的拿到的是源⽂件内容(source),可以通过返回值的⽅式将处理后的内容输出,也可以调⽤ this.callback() ⽅法,将内容返回给webpack。 还可以通过this.async() ⽣成⼀个 callback 函数,再⽤这个callback将处理后的内容输出出去。 此外 webpack 还为开发者准备了开发loader的⼯具函数集——loader-utils 。 124 | 125 | 126 | 127 | 相对于Loader⽽⾔,Plugin的编写就灵活了许多。 webpack在运⾏的⽣命周期中会⼴播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。 128 | 129 | ### 9. **webpack**的热更新是如何做到的?说明其原理? 130 | 131 | webpack的热更新⼜称热替换(Hot Module Replacement),缩写为HMR。 这个机制可以做到不⽤刷新浏览器⽽将新变更的模块替换掉旧的模块。 132 | 133 | 134 | 135 | 原理: 136 | 137 | ![image.png](https://cdn.nlark.com/yuque/0/2021/png/1500604/1615910527011-339c57ce-22b2-4660-bcb5-93a7c6ec113b.png?x-oss-process=image%2Fresize%2Cw_1500) 138 | 139 | ⾸先要知道server端和client端都做了处理⼯作: 140 | 141 | 1. 第⼀步,在 webpack 的 watch 模式下,⽂件系统中某⼀个⽂件发⽣修改,webpack 监听到⽂件变化,根据配置⽂ 142 | 143 | 件对模块重新编译打包,并将打包后的代码通过简单的 JavaScript 对象保存在内存中。 144 | 145 | 1. 第⼆步是 webpack-dev-server 和 webpack 之间的接⼝交互,⽽在这⼀步,主要是 dev-server 的中间件 webpack- dev-middleware 和 webpack 之间的交互,webpack-dev-middleware 调⽤ webpack 暴露的 API对代码变化进⾏监 控,并且告诉 webpack,将代码打包到内存中。 146 | 2. 第三步是 webpack-dev-server 对⽂件变化的⼀个监控,这⼀步不同于第⼀步,并不是监控代码变化重新打包。当我们在配置⽂件中配置了devServer.watchContentBase 为 true 的时候,Server 会监听这些配置⽂件夹中静态⽂件的变化,变化后会通知浏览器端对应⽤进⾏ live reload。注意,这⼉是浏览器刷新,和 HMR 是两个概念。 147 | 3. 第四步也是 webpack-dev-server 代码的⼯作,该步骤主要是通过 sockjs(webpack-dev-server 的依赖)在浏览器端和服务端之间建⽴⼀个 websocket ⻓连接,将 webpack 编译打包的各个阶段的状态信息告知浏览器端,同时也包括第三步中 Server 监听静态⽂件变化的信息。浏览器端根据这些 socket 消息进⾏不同的操作。当然服务端传递的最主要信息还是新模块的 hash 值,后⾯的步骤根据这⼀ hash 值来进⾏模块热替换。 148 | 4. webpack-dev-server/client 端并不能够请求更新的代码,也不会执⾏热更模块操作,⽽把这些⼯作⼜交回给了webpack,webpack/hot/dev-server 的⼯作就是根据 webpack-dev-server/client 传给它的信息以及 dev-server 的配置决定是刷新浏览器呢还是进⾏模块热更新。当然如果仅仅是刷新浏览器,也就没有后⾯那些步骤了。 149 | 5. HotModuleReplacement.runtime 是客户端 HMR 的中枢,它接收到上⼀步传递给他的新模块的 hash 值,它通过JsonpMainTemplate.runtime 向 server 端发送 Ajax 请求,服务端返回⼀个 json,该 json 包含了所有要更新的模块的 hash 值,获取到更新列表后,该模块再次通过 jsonp 请求,获取到最新的模块代码。这就是上图中 7、8、9 步骤。 150 | 6. ⽽第 10 步是决定 HMR 成功与否的关键步骤,在该步骤中,HotModulePlugin 将会对新旧模块进⾏对⽐,决定是否更新模块,在决定更新模块后,检查模块之间的依赖关系,更新模块的同时更新模块间的依赖引⽤。 151 | 7. 最后⼀步,当 HMR 失败后,回退到 live reload 操作,也就是进⾏浏览器刷新来获取最新打包代码。 152 | 153 | ### 10. 如何⽤**webpack**来优化前端性能? 154 | 155 | ⽤webpack优化前端性能是指优化webpack的输出结果,让打包的最终结果在浏览器运⾏快速⾼效。 156 | 157 | - **压缩代码**:删除多余的代码、注释、简化代码的写法等等⽅式。可以利⽤webpack的 UglifyJsPlugin 和 ParallelUglifyPlugin 来压缩JS⽂件, 利⽤ cssnano (css-loader?minimize)来压缩css 158 | - **利⽤****CDN****加速**: 在构建过程中,将引⽤的静态资源路径修改为CDN上对应的路径。可以利⽤webpack对于 output 参数和各loader的 publicPath 参数来修改资源路径 159 | - **Tree Shaking**: 将代码中永远不会⾛到的⽚段删除掉。可以通过在启动webpack时追加参数 --optimize-minimize 来实现 160 | - **Code Splitting:** 将代码按路由维度或者组件分块(chunk),这样做到按需加载,同时可以充分利⽤浏览器缓存 161 | - **提取公共第三⽅库**: SplitChunksPlugin插件来进⾏公共模块抽取,利⽤浏览器缓存可以⻓期缓存这些⽆需频繁变动的公共代码 162 | 163 | ### 11. 如何提⾼**webpack**的打包速度**?** 164 | 165 | - happypack: 利⽤进程并⾏编译loader,利⽤缓存来使得 rebuild 更快,遗憾的是作者表示已经不会继续开发此项⽬,类似的替代者是thread-loader 166 | - 外部扩展(externals): 将不怎么需要更新的第三⽅库脱离webpack打包,不被打⼊bundle中,从⽽减少打包时间,⽐如jQuery⽤script标签引⼊ 167 | - dll: 采⽤webpack的 DllPlugin 和 DllReferencePlugin 引⼊dll,让⼀些基本不会改动的代码先打包成静态资源,避免反复编译浪费时间 168 | - 利⽤缓存: webpack.cache 、babel-loader.cacheDirectory、 HappyPack.cache 都可以利⽤缓存提⾼rebuild效率缩⼩⽂件搜索范围: ⽐如babel-loader插件,如果你的⽂件仅存在于src中,那么可以 include: path.resolve(__dirname,'src') ,当然绝⼤多数情况下这种操作的提升有限,除⾮不⼩⼼build了node_modules⽂件 169 | 170 | ### 12. 如何提⾼**webpack**的构建速度? 171 | 172 | 1. 多⼊⼝情况下,使⽤ CommonsChunkPlugin 来提取公共代码 173 | 2. 通过 externals 配置来提取常⽤库 174 | 3. 利⽤ DllPlugin 和 DllReferencePlugin 预编译资源模块 通过 DllPlugin 来对那些我们引⽤但是绝对不会修改的npm包来进⾏预编译,再通过 DllReferencePlugin 将预编译的模块加载进来。 175 | 4. 使⽤ Happypack 实现多线程加速编译 176 | 5. 使⽤ webpack-uglify-parallel 来提升 uglifyPlugin 的压缩速度。 原理上 webpack-uglify-parallel 采⽤了多核并⾏压缩来提升压缩速度 177 | 6. 使⽤ Tree-shaking 和 Scope Hoisting 来剔除多余代码 178 | 179 | ### 13. 怎么配置单⻚应⽤?怎么配置多⻚应⽤? 180 | 181 | 单⻚应⽤可以理解为webpack的标准模式,直接在 entry 中指定单⻚应⽤的⼊⼝即可,这⾥不再赘述多⻚应⽤的话,可以使⽤webpack的 AutoWebPlugin 来完成简单⾃动化的构建,但是前提是项⽬的⽬录结构必须遵守他预设的规范。 多⻚应⽤中要注意的是: 182 | 183 | - 每个⻚⾯都有公共的代码,可以将这些代码抽离出来,避免重复的加载。⽐如,每个⻚⾯都引⽤了同⼀套css样式表 184 | - 随着业务的不断扩展,⻚⾯可能会不断的追加,所以⼀定要让⼊⼝的配置⾜够灵活,避免每次添加新⻚⾯还需要修改构建配置 185 | 186 | ## 三、其他 187 | 188 | ### **1. Babel**的原理是什么**?** 189 | 190 | babel 的转译过程也分为三个阶段,这三步具体是: 191 | 192 | - **解析 Parse**: 将代码解析⽣成抽象语法树(AST),即词法分析与语法分析的过程; 193 | - **转换 Transform**: 对于 AST 进⾏变换⼀系列的操作,babel 接受得到 AST 并通过 babel-traverse 对其进⾏遍历,在此过程中进⾏添加、更新及移除等操作; 194 | - **⽣成 Generate**: 将变换后的 AST 再转换为 JS 代码, 使⽤到的模块是 babel-generator。 195 | 196 | ![image.png](https://cdn.nlark.com/yuque/0/2021/png/1500604/1615908675152-69682ae3-d0b3-4552-a32e-39c2022b1db0.png?x-oss-process=image%2Fresize%2Cw_1500) -------------------------------------------------------------------------------- /8 offer收割机之计算机网络篇.md: -------------------------------------------------------------------------------- 1 | 2 | ![计算机网络面试题.png](https://cdn.nlark.com/yuque/0/2021/png/1500604/1621606395878-ec50f847-ec2c-451b-885a-0baac77777e0.png?x-oss-process=image%2Fresize%2Cw_746) 3 | 4 | ## 一、HTTP协议 5 | 6 | ### 1. GET和POST的请求的区别 7 | 8 | Post 和 Get 是 HTTP 请求的两种方法,其区别如下: 9 | 10 | - **应用场景:**GET 请求是一个幂等的请求,一般 Get 请求用于对服务器资源不会产生影响的场景,比如说请求一个网页的资源。而 Post 不是一个幂等的请求,一般用于对服务器资源会产生影响的情景,比如注册用户这一类的操作。 11 | - **是否缓存:**因为两者应用场景不同,浏览器一般会对 Get 请求缓存,但很少对 Post 请求缓存。 12 | - **发送的报文格式:**Get 请求的报文中实体部分为空,Post 请求的报文中实体部分一般为向服务器发送的数据。 13 | - **安全性:**Get 请求可以将请求的参数放入 url 中向服务器发送,这样的做法相对于 Post 请求来说是不太安全的,因为请求的 url 会被保留在历史记录中。 14 | - **请求长度:**浏览器由于对 url 长度的限制,所以会影响 get 请求发送数据时的长度。这个限制是浏览器规定的,并不是 RFC 规定的。 15 | - **参数类型:**post 的参数传递支持更多的数据类型。 16 | 17 | ### 2. POST和PUT请求的区别 18 | 19 | - PUT请求是向服务器端发送数据,从而修改数据的内容,但是不会增加数据的种类等,也就是说无论进行多少次PUT操作,其结果并没有不同。(可以理解为是**更新数据**) 20 | - POST请求是向服务器端发送数据,该请求会改变数据的种类等资源,它会创建新的内容。(可以理解为是**创建数据**) 21 | 22 | ### 3. 常见的HTTP请求头和响应头 23 | 24 | **HTTP Request Header 常见的请求头:** 25 | 26 | - Accept:浏览器能够处理的内容类型 27 | - Accept-Charset:浏览器能够显示的字符集 28 | - Accept-Encoding:浏览器能够处理的压缩编码 29 | - Accept-Language:浏览器当前设置的语言 30 | - Connection:浏览器与服务器之间连接的类型 31 | - Cookie:当前页面设置的任何Cookie 32 | - Host:发出请求的页面所在的域 33 | - Referer:发出请求的页面的URL 34 | - User-Agent:浏览器的用户代理字符串 35 | 36 | 37 | 38 | **HTTP Responses Header 常见的响应头:** 39 | 40 | - Date:表示消息发送的时间,时间的描述格式由rfc822定义 41 | - server:服务器名称 42 | - Connection:浏览器与服务器之间连接的类型 43 | - Cache-Control:控制HTTP缓存 44 | - content-type:表示后面的文档属于什么MIME类型 45 | 46 | 47 | 48 | 常见的 Content-Type 属性值有以下四种: 49 | 50 | (1)application/x-www-form-urlencoded:浏览器的原生 form 表单,如果不设置 enctype 属性,那么最终就会以 application/x-www-form-urlencoded 方式提交数据。该种方式提交的数据放在 body 里面,数据按照 key1=val1&key2=val2 的方式进行编码,key 和 val 都进行了 URL转码。 51 | 52 | (2)multipart/form-data:该种方式也是一个常见的 POST 提交方式,通常表单上传文件时使用该种方式。 53 | 54 | (3)application/json:服务器消息主体是序列化后的 JSON 字符串。 55 | 56 | (4)text/xml:该种方式主要用来提交 XML 格式的数据。 57 | 58 | ### 4. HTTP状态码304是多好还是少好 59 | 60 | 服务器为了提高网站访问速度,对之前访问的部分页面指定缓存机制,当客户端在此对这些页面进行请求,服务器会根据缓存内容判断页面与之前是否相同,若相同便直接返回304,此时客户端调用缓存内容,不必进行二次下载。 61 | 62 | 63 | 64 | 状态码304不应该认为是一种错误,而是对客户端**有缓存情况下**服务端的一种响应。 65 | 66 | 67 | 68 | 搜索引擎蜘蛛会更加青睐内容源更新频繁的网站。通过特定时间内对网站抓取返回的状态码来调节对该网站的抓取频次。若网站在一定时间内一直处于304的状态,那么蜘蛛可能会降低对网站的抓取次数。相反,若网站变化的频率非常之快,每次抓取都能获取新内容,那么日积月累,的回访率也会提高。 69 | 70 | 71 | 72 | **产生较多304状态码的原因:** 73 | 74 | - 页面更新周期长或不更新 75 | - 纯静态页面或强制生成静态html 76 | 77 | 78 | 79 | **304状态码出现过多会造成以下问题:** 80 | 81 | - 网站快照停止; 82 | - 收录减少; 83 | - 权重下降。 84 | 85 | ### 5. 常见的HTTP请求方法 86 | 87 | - GET: 向服务器获取数据; 88 | - POST:将实体提交到指定的资源,通常会造成服务器资源的修改; 89 | - PUT:上传文件,更新数据; 90 | - DELETE:删除服务器上的对象; 91 | - HEAD:获取报文首部,与GET相比,不返回报文主体部分; 92 | - OPTIONS:询问支持的请求方法,用来跨域请求; 93 | - CONNECT:要求在与代理服务器通信时建立隧道,使用隧道进行TCP通信; 94 | - TRACE: 回显服务器收到的请求,主要⽤于测试或诊断。 95 | 96 | ### 6. OPTIONS请求方法及使用场景 97 | 98 | OPTIONS是除了GET和POST之外的其中一种 HTTP请求方法。 99 | 100 | 101 | 102 | OPTIONS方法是用于请求获得由`Request-URI`标识的资源在请求/响应的通信过程中可以使用的功能选项。通过这个方法,客户端可以**在采取具体资源请求之前,决定对该资源采取何种必要措施,或者了解服务器的性能**。该请求方法的响应不能缓存。 103 | 104 | 105 | 106 | OPTIONS请求方法的**主要用途**有两个: 107 | 108 | - 获取服务器支持的所有HTTP请求方法; 109 | - 用来检查访问权限。例如:在进行 CORS 跨域资源共享时,对于复杂请求,就是使用 OPTIONS 方法发送嗅探请求,以判断是否有对指定资源的访问权限。 110 | 111 | ### 7. HTTP 1.0 和 HTTP 1.1 之间有哪些区别? 112 | 113 | **HTTP 1.0和 HTTP 1.1** **有以下区别**: 114 | 115 | - **连接方面**,http1.0 默认使用非持久连接,而 http1.1 默认使用持久连接。http1.1 通过使用持久连接来使多个 http 请求复用同一个 TCP 连接,以此来避免使用非持久连接时每次需要建立连接的时延。 116 | - **资源请求方面**,在 http1.0 中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能,http1.1 则在请求头引入了 range 头域,它允许只请求资源的某个部分,即返回码是 206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接。 117 | - **缓存方面**,在 http1.0 中主要使用 header 里的 If-Modified-Since、Expires 来做为缓存判断的标准,http1.1 则引入了更多的缓存控制策略,例如 Etag、If-Unmodified-Since、If-Match、If-None-Match 等更多可供选择的缓存头来控制缓存策略。 118 | - http1.1 中**新增了 host 字段**,用来指定服务器的域名。http1.0 中认为每台服务器都绑定一个唯一的 IP 地址,因此,请求消息中的 URL 并没有传递主机名(hostname)。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机,并且它们共享一个IP地址。因此有了 host 字段,这样就可以将请求发往到同一台服务器上的不同网站。 119 | - http1.1 相对于 http1.0 还新增了很多**请求方法**,如 PUT、HEAD、OPTIONS 等。 120 | 121 | ### 8. HTTP 1.1 和 HTTP 2.0 的区别 122 | 123 | - **二进制协议**:HTTP/2 是一个二进制协议。在 HTTP/1.1 版中,报文的头信息必须是文本(ASCII 编码),数据体可以是文本,也可以是二进制。HTTP/2 则是一个彻底的二进制协议,头信息和数据体都是二进制,并且统称为"帧",可以分为头信息帧和数据帧。 帧的概念是它实现多路复用的基础。 124 | - **多路复用:**HTTP/2 实现了多路复用,HTTP/2 仍然复用 TCP 连接,但是在一个连接里,客户端和服务器都可以同时发送多个请求或回应,而且不用按照顺序一一发送,这样就避免了"队头堵塞"【1】的问题。 125 | - **数据流:**HTTP/2 使用了数据流的概念,因为 HTTP/2 的数据包是不按顺序发送的,同一个连接里面连续的数据包,可能属于不同的请求。因此,必须要对数据包做标记,指出它属于哪个请求。HTTP/2 将每个请求或回应的所有数据包,称为一个数据流。每个数据流都有一个独一无二的编号。数据包发送时,都必须标记数据流 ID ,用来区分它属于哪个数据流。 126 | - **头信息压缩:**HTTP/2 实现了头信息压缩,由于 HTTP 1.1 协议不带状态,每次请求都必须附上所有信息。所以,请求的很多字段都是重复的,比如 Cookie 和 User Agent ,一模一样的内容,每次请求都必须附带,这会浪费很多带宽,也影响速度。HTTP/2 对这一点做了优化,引入了头信息压缩机制。一方面,头信息使用 gzip 或 compress 压缩后再发送;另一方面,客户端和服务器同时维护一张头信息表,所有字段都会存入这个表,生成一个索引号,以后就不发送同样字段了,只发送索引号,这样就能提高速度了。 127 | - **服务器推送:**HTTP/2 允许服务器未经请求,主动向客户端发送资源,这叫做服务器推送。使用服务器推送提前给客户端推送必要的资源,这样就可以相对减少一些延迟时间。这里需要注意的是 http2 下服务器主动推送的是静态资源,和 WebSocket 以及使用 SSE 等方式向客户端发送即时数据的推送是不同的。 128 | 129 | 130 | 131 | **【1】队头堵塞:** 132 | 133 | > 队头阻塞是由 HTTP 基本的“请求 - 应答”模型所导致的。HTTP 规定报文必须是“一发一收”,这就形成了一个先进先出的“串行”队列。队列里的请求是没有优先级的,只有入队的先后顺序,排在最前面的请求会被最优先处理。如果队首的请求因为处理的太慢耽误了时间,那么队列里后面的所有请求也不得不跟着一起等待,结果就是其他的请求承担了不应有的时间成本,造成了队头堵塞的现象。 134 | 135 | ### 9. HTTP和HTTPS协议的区别 136 | 137 | HTTP和HTTPS协议的主要区别如下: 138 | 139 | - HTTPS协议需要CA证书,费用较高;而HTTP协议不需要; 140 | - HTTP协议是超文本传输协议,信息是明文传输的,HTTPS则是具有安全性的SSL加密传输协议; 141 | - 使用不同的连接方式,端口也不同,HTTP协议端口是80,HTTPS协议端口是443; 142 | - HTTP协议连接很简单,是无状态的;HTTPS协议是有SSL和HTTP协议构建的可进行加密传输、身份认证的网络协议,比HTTP更加安全。 143 | 144 | ### 10. GET方法URL长度限制的原因 145 | 146 | 实际上HTTP协议规范并没有对get方法请求的url长度进行限制,这个限制是特定的浏览器及服务器对它的限制。 147 | 148 | IE对URL长度的限制是2083字节(2K+35)。由于IE浏览器对URL长度的允许值是最小的,所以开发过程中,只要URL不超过2083字节,那么在所有浏览器中工作都不会有问题。 149 | 150 | ``` 151 | GET的长度值 = URL(2083)- (你的Domain+Path)-2(2是get请求中?=两个字符的长度) 152 | ``` 153 | 154 | 下面看一下主流浏览器对get方法中url的长度限制范围: 155 | 156 | - Microsoft Internet Explorer (Browser):IE浏览器对URL的最大限制为2083个字符,如果超过这个数字,提交按钮没有任何反应。 157 | - Firefox (Browser):对于Firefox浏览器URL的长度限制为 65,536 个字符。 158 | - Safari (Browser):URL最大长度限制为 80,000 个字符。 159 | - Opera (Browser):URL最大长度限制为 190,000 个字符。 160 | - Google (chrome):URL最大长度限制为 8182 个字符。 161 | 162 | 163 | 164 | 主流的服务器对get方法中url的长度限制范围: 165 | 166 | - Apache (Server):能接受最大url长度为8192个字符。 167 | - Microsoft Internet Information Server(IIS):能接受最大url的长度为16384个字符。 168 | 169 | 170 | 171 | 根据上面的数据,可以知道,get方法中的URL长度最长不超过2083个字符,这样所有的浏览器和服务器都可能正常工作。 172 | 173 | ### 11. 当在浏览器中输入 Google.com 并且按下回车之后发生了什么? 174 | 175 | (1)**解析URL:**首先会对 URL 进行解析,分析所需要使用的传输协议和请求的资源的路径。如果输入的 URL 中的协议或者主机名不合法,将会把地址栏中输入的内容传递给搜索引擎。如果没有问题,浏览器会检查 URL 中是否出现了非法字符,如果存在非法字符,则对非法字符进行转义后再进行下一过程。 176 | 177 | (2)**缓存判断:**浏览器会判断所请求的资源是否在缓存里,如果请求的资源在缓存里并且没有失效,那么就直接使用,否则向服务器发起新的请求。 178 | 179 | (3)**DNS解析:**下一步首先需要获取的是输入的 URL 中的域名的 IP 地址,首先会判断本地是否有该域名的 IP 地址的缓存,如果有则使用,如果没有则向本地 DNS 服务器发起请求。本地 DNS 服务器也会先检查是否存在缓存,如果没有就会先向根域名服务器发起请求,获得负责的顶级域名服务器的地址后,再向顶级域名服务器请求,然后获得负责的权威域名服务器的地址后,再向权威域名服务器发起请求,最终获得域名的 IP 地址后,本地 DNS 服务器再将这个 IP 地址返回给请求的用户。用户向本地 DNS 服务器发起请求属于递归请求,本地 DNS 服务器向各级域名服务器发起请求属于迭代请求。 180 | 181 | (4)**获取MAC地址:**当浏览器得到 IP 地址后,数据传输还需要知道目的主机 MAC 地址,因为应用层下发数据给传输层,TCP 协议会指定源端口号和目的端口号,然后下发给网络层。网络层会将本机地址作为源地址,获取的 IP 地址作为目的地址。然后将下发给数据链路层,数据链路层的发送需要加入通信双方的 MAC 地址,本机的 MAC 地址作为源 MAC 地址,目的 MAC 地址需要分情况处理。通过将 IP 地址与本机的子网掩码相与,可以判断是否与请求主机在同一个子网里,如果在同一个子网里,可以使用 APR 协议获取到目的主机的 MAC 地址,如果不在一个子网里,那么请求应该转发给网关,由它代为转发,此时同样可以通过 ARP 协议来获取网关的 MAC 地址,此时目的主机的 MAC 地址应该为网关的地址。 182 | 183 | (5)**TCP三次握手:**下面是 TCP 建立连接的三次握手的过程,首先客户端向服务器发送一个 SYN 连接请求报文段和一个随机序号,服务端接收到请求后向服务器端发送一个 SYN ACK报文段,确认连接请求,并且也向客户端发送一个随机序号。客户端接收服务器的确认应答后,进入连接建立的状态,同时向服务器也发送一个ACK 确认报文段,服务器端接收到确认后,也进入连接建立状态,此时双方的连接就建立起来了。 184 | 185 | (6)**HTTPS握手:**如果使用的是 HTTPS 协议,在通信前还存在 TLS 的一个四次握手的过程。首先由客户端向服务器端发送使用的协议的版本号、一个随机数和可以使用的加密方法。服务器端收到后,确认加密的方法,也向客户端发送一个随机数和自己的数字证书。客户端收到后,首先检查数字证书是否有效,如果有效,则再生成一个随机数,并使用证书中的公钥对随机数加密,然后发送给服务器端,并且还会提供一个前面所有内容的 hash 值供服务器端检验。服务器端接收后,使用自己的私钥对数据解密,同时向客户端发送一个前面所有内容的 hash 值供客户端检验。这个时候双方都有了三个随机数,按照之前所约定的加密方法,使用这三个随机数生成一把秘钥,以后双方通信前,就使用这个秘钥对数据进行加密后再传输。 186 | 187 | (7)**返回数据:**当页面请求发送到服务器端后,服务器端会返回一个 html 文件作为响应,浏览器接收到响应后,开始对 html 文件进行解析,开始页面的渲染过程。 188 | 189 | (8)**页面渲染:**浏览器首先会根据 html 文件构建 DOM 树,根据解析到的 css 文件构建 CSSOM 树,如果遇到 script 标签,则判端是否含有 defer 或者 async 属性,要不然 script 的加载和执行会造成页面的渲染的阻塞。当 DOM 树和 CSSOM 树建立好后,根据它们来构建渲染树。渲染树构建好后,会根据渲染树来进行布局。布局完成后,最后使用浏览器的 UI 接口对页面进行绘制。这个时候整个页面就显示出来了。 190 | 191 | (9)**TCP四次挥手:**最后一步是 TCP 断开连接的四次挥手过程。若客户端认为数据发送完成,则它需要向服务端发送连接释放请求。服务端收到连接释放请求后,会告诉应用层要释放 TCP 链接。然后会发送 ACK 包,并进入 CLOSE_WAIT 状态,此时表明客户端到服务端的连接已经释放,不再接收客户端发的数据了。但是因为 TCP 连接是双向的,所以服务端仍旧可以发送数据给客户端。服务端如果此时还有没发完的数据会继续发送,完毕后会向客户端发送连接释放请求,然后服务端便进入 LAST-ACK 状态。客户端收到释放请求后,向服务端发送确认应答,此时客户端进入 TIME-WAIT 状态。该状态会持续 2MSL(最大段生存期,指报文段在网络中生存的时间,超时会被抛弃) 时间,若该时间段内没有服务端的重发请求的话,就进入 CLOSED 状态。当服务端收到确认应答后,也便进入 CLOSED 状态。 192 | 193 | ### 12. 对keep-alive的理解 194 | 195 | HTTP1.0 中默认是在每次请求/应答,客户端和服务器都要新建一个连接,完成之后立即断开连接,这就是**短连接**。当使用Keep-Alive模式时,Keep-Alive功能使客户端到服务器端的连接持续有效,当出现对服务器的后继请求时,Keep-Alive功能避免了建立或者重新建立连接,这就是**长连接**。其使用方法如下: 196 | 197 | - HTTP1.0版本是默认没有Keep-alive的(也就是默认会发送keep-alive),所以要想连接得到保持,必须手动配置发送`Connection: keep-alive`字段。若想断开keep-alive连接,需发送`Connection:close`字段; 198 | - HTTP1.1规定了默认保持长连接,数据传输完成了保持TCP连接不断开,等待在同域名下继续用这个通道传输数据。如果需要关闭,需要客户端发送`Connection:close`首部字段。 199 | 200 | 201 | 202 | Keep-Alive的**建立过程**: 203 | 204 | - 客户端向服务器在发送请求报文同时在首部添加发送Connection字段 205 | - 服务器收到请求并处理 Connection字段 206 | - 服务器回送Connection:Keep-Alive字段给客户端 207 | - 客户端接收到Connection字段 208 | - Keep-Alive连接建立成功 209 | 210 | 211 | 212 | **服务端自动断开过程(也就是没有keep-alive)**: 213 | 214 | - 客户端向服务器只是发送内容报文(不包含Connection字段) 215 | - 服务器收到请求并处理 216 | - 服务器返回客户端请求的资源并关闭连接 217 | - 客户端接收资源,发现没有Connection字段,断开连接 218 | 219 | 220 | 221 | **客户端请求断开连接过程**: 222 | 223 | - 客户端向服务器发送Connection:close字段 224 | - 服务器收到请求并处理connection字段 225 | - 服务器回送响应资源并断开连接 226 | - 客户端接收资源并断开连接 227 | 228 | 229 | 230 | 开启Keep-Alive的**优点:** 231 | 232 | - 较少的CPU和内存的使⽤(由于同时打开的连接的减少了); 233 | - 允许请求和应答的HTTP管线化; 234 | - 降低拥塞控制 (TCP连接减少了); 235 | - 减少了后续请求的延迟(⽆需再进⾏握⼿); 236 | - 报告错误⽆需关闭TCP连; 237 | 238 | 239 | 240 | 开启Keep-Alive的**缺点**: 241 | 242 | - 长时间的Tcp连接容易导致系统资源无效占用,浪费系统资源。 243 | 244 | ### 13. 页面有多张图片,HTTP是怎样的加载表现? 245 | 246 | - 在`HTTP 1`下,浏览器对一个域名下最大TCP连接数为6,所以会请求多次。可以用**多域名部署**解决。这样可以提高同时请求的数目,加快页面图片的获取速度。 247 | - 在`HTTP 2`下,可以一瞬间加载出来很多资源,因为,HTTP2支持多路复用,可以在一个TCP连接中发送多个HTTP请求。 248 | 249 | ### 14. HTTP2的头部压缩算法是怎样的? 250 | 251 | HTTP2的头部压缩是HPACK算法。在客户端和服务器两端建立“字典”,用索引号表示重复的字符串,采用哈夫曼编码来压缩整数和字符串,可以达到50%~90%的高压缩率。 252 | 253 | 254 | 255 | 具体来说: 256 | 257 | - 在客户端和服务器端使用“首部表”来跟踪和存储之前发送的键值对,对于相同的数据,不再通过每次请求和响应发送; 258 | - 首部表在HTTP/2的连接存续期内始终存在,由客户端和服务器共同渐进地更新; 259 | - 每个新的首部键值对要么被追加到当前表的末尾,要么替换表中之前的值。 260 | 261 | 262 | 263 | 例如下图中的两个请求, 请求一发送了所有的头部字段,第二个请求则只需要发送差异数据,这样可以减少冗余数据,降低开销。 264 | 265 | ![image](https://cdn.nlark.com/yuque/0/2020/png/1500604/1604070508591-32d79893-7e98-40c8-b779-ffb6da42cd1b.png) 266 | 267 | ### 15. HTTP请求报文的是什么样的? 268 | 269 | 请求报⽂有4部分组成: 270 | 271 | - 请求⾏ 272 | - 请求头部 273 | - 空⾏ 274 | - 请求体 275 | 276 | ![image.png](https://cdn.nlark.com/yuque/0/2021/png/1500604/1615907508156-a82d09e4-00bf-4dc7-a3a3-1ae75481754e.png) 277 | 278 | **其中:** 279 | 280 | (1)请求⾏包括:请求⽅法字段、URL字段、HTTP协议版本字段。它们⽤空格分隔。例如,GET /index.html HTTP/1.1。 281 | 282 | (2)请求头部:请求头部由关键字/值对组成,每⾏⼀对,关键字和值⽤英⽂冒号“:”分隔 283 | 284 | - User-Agent:产⽣请求的浏览器类型。 285 | - Accept:客户端可识别的内容类型列表。 286 | - Host:请求的主机名,允许多个域名同处⼀个IP地址,即虚拟主机。 287 | 288 | (3)请求体: post put等请求携带的数据 289 | 290 | ![image.png](https://cdn.nlark.com/yuque/0/2021/png/1500604/1615907573585-6651540d-9dee-4b33-a97f-dee1b31f231c.png) 291 | 292 | ### 16. HTTP响应报文的是什么样的? 293 | 294 | 请求报⽂有4部分组成: 295 | 296 | - 响应⾏ 297 | - 响应头 298 | - 空⾏ 299 | - 响应体 300 | 301 | ![image.png](https://cdn.nlark.com/yuque/0/2021/png/1500604/1615907658281-b3a51c98-db27-45be-9b0c-fb2c7665b015.png) 302 | 303 | - 响应⾏:由网络协议版本,状态码和状态码的原因短语组成,例如 HTTP/1.1 200 OK 。 304 | - 响应头:响应部⾸组成 305 | - 响应体:服务器响应的数据 306 | 307 | ### 17. HTTP协议的优点和缺点 308 | 309 | HTTP 是超文本传输协议,它定义了客户端和服务器之间交换报文的格式和方式,默认使用 80 端口。它使用 TCP 作为传输层协议,保证了数据传输的可靠性。 310 | 311 | 312 | 313 | HTTP协议具有以下**优点**: 314 | 315 | - 支持客户端/服务器模式 316 | - **简单快速**:客户向服务器请求服务时,只需传送请求方法和路径。由于 HTTP 协议简单,使得 HTTP 服务器的程序规模小,因而通信速度很快。 317 | - **无连接**:无连接就是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接,采用这种方式可以节省传输时间。 318 | - **无状态**:HTTP 协议是无状态协议,这里的状态是指通信过程的上下文信息。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能会导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就比较快。 319 | - **灵活**:HTTP 允许传输任意类型的数据对象。正在传输的类型由 Content-Type 加以标记。 320 | 321 | 322 | 323 | HTTP协议具有以下**缺点**: 324 | 325 | - **无状态:**HTTP 是一个无状态的协议,HTTP 服务器不会保存关于客户的任何信息。 326 | - **明文传输:**协议中的报文使用的是文本形式,这就直接暴露给外界,不安全。 327 | - **不安全** 328 | 329 | (1)通信使用明文(不加密),内容可能会被窃听; 330 | 331 | (2)不验证通信方的身份,因此有可能遭遇伪装; 332 | 333 | (3)无法证明报文的完整性,所以有可能已遭篡改; 334 | 335 | ### 18. 说一下HTTP 3.0 336 | 337 | HTTP/3基于UDP协议实现了类似于TCP的多路复用数据流、传输可靠性等功能,这套功能被称为QUIC协议。 338 | 339 | ![image](https://cdn.nlark.com/yuque/0/2020/webp/1500604/1604068246276-9b0f553d-3c6e-43a3-8185-8565f9fa1fb4.webp) 340 | 341 | 1. 流量控制、传输可靠性功能:QUIC在UDP的基础上增加了一层来保证数据传输可靠性,它提供了数据包重传、拥塞控制、以及其他一些TCP中的特性。 342 | 2. 集成TLS加密功能:目前QUIC使用TLS1.3,减少了握手所花费的RTT数。 343 | 3. 多路复用:同一物理连接上可以有多个独立的逻辑数据流,实现了数据流的单独传输,解决了TCP的队头阻塞问题。 344 | 345 | ![image](https://cdn.nlark.com/yuque/0/2020/webp/1500604/1604068246276-5d0a5de2-00db-425e-8b21-0cc4bbb54b24.webp) 346 | 347 | 1. 快速握手:由于基于UDP,可以实现使用0 ~ 1个RTT来建立连接。 348 | 349 | ### 19. HTTP协议的性能怎么样 350 | 351 | HTTP 协议是基于 TCP/IP,并且使用了**请求-应答**的通信模式,所以性能的关键就在这两点里。 352 | 353 | - **长连接** 354 | 355 | HTTP协议有两种连接模式,一种是持续连接,一种非持续连接。 356 | 357 | (1)非持续连接指的是服务器必须为每一个请求的对象建立和维护一个全新的连接。 358 | 359 | (2)持续连接下,TCP 连接默认不关闭,可以被多个请求复用。采用持续连接的好处是可以避免每次建立 TCP 连接三次握手时所花费的时间。 360 | 361 | 362 | 363 | 对于不同版本的采用不同的连接方式: 364 | 365 | - 在HTTP/1.0 每发起一个请求,都要新建一次 TCP 连接(三次握手),而且是串行请求,做了无畏的 TCP 连接建立和断开,增加了通信开销。该版本使用的非持续的连接,但是可以在请求时,加上 Connection: keep-a live 来要求服务器不要关闭 TCP 连接。 366 | - 在HTTP/1.1 提出了**长连接**的通信方式,也叫持久连接。这种方式的好处在于减少了 TCP 连接的重复建立和断开所造成的额外开销,减轻了服务器端的负载。该版本及以后版本默认采用的是持续的连接。目前对于同一个域,大多数浏览器支持同时建立 6 个持久连接。 367 | 368 | ![image](https://cdn.nlark.com/yuque/0/2020/png/1500604/1604065902281-b98a486c-5442-4d0b-b58d-00aab296ce1a.png) 369 | 370 | - **管道网络传输** 371 | 372 | HTTP/1.1 采用了长连接的方式,这使得管道(pipeline)网络传输成为了可能。 373 | 374 | 375 | 376 | 管道(pipeline)网络传输是指:可以在同一个 TCP 连接里面,客户端可以发起多个请求,只要第一个请求发出去了,不必等其回来,就可以发第二个请求出去,可以减少整体的响应时间。但是服务器还是按照顺序回应请求。如果前面的回应特别慢,后面就会有许多请求排队等着。这称为队头堵塞。 377 | 378 | - **队头堵塞** 379 | 380 | HTTP 传输的报文必须是一发一收,但是,里面的任务被放在一个任务队列中串行执行,一旦队首的请求处理太慢,就会阻塞后面请求的处理。这就是HTTP队头阻塞问题。 381 | 382 | 383 | 384 | **队头阻塞的解决方案:** 385 | 386 | (1)并发连接:对于一个域名允许分配多个长连接,那么相当于增加了任务队列,不至于一个队伍的任务阻塞其它所有任务。 387 | 388 | (2)域名分片:将域名分出很多二级域名,它们都指向同样的一台服务器,能够并发的长连接数变多,解决了队头阻塞的问题。 389 | 390 | ### 20. URL有哪些组成部分 391 | 392 | 以下面的URL为例:**http://www.aspxfans.com:8080/news/index.asp?boardID=5&ID=24618&page=1#name** 393 | 394 | 从上面的URL可以看出,一个完整的URL包括以下几部分: 395 | 396 | - **协议部分**:该URL的协议部分为“http:”,这代表网页使用的是HTTP协议。在Internet中可以使用多种协议,如HTTP,FTP等等本例中使用的是HTTP协议。在"HTTP"后面的“//”为分隔符; 397 | - **域名部分**:该URL的域名部分为“www.aspxfans.com”。一个URL中,也可以使用IP地址作为域名使用 398 | - **端口部分**:跟在域名后面的是端口,域名和端口之间使用“:”作为分隔符。端口不是一个URL必须的部分,如果省略端口部分,将采用默认端口(HTTP协议默认端口是80,HTTPS协议默认端口是443); 399 | - **虚拟目录部分**:从域名后的第一个“/”开始到最后一个“/”为止,是虚拟目录部分。虚拟目录也不是一个URL必须的部分。本例中的虚拟目录是“/news/”; 400 | - **文件名部分**:从域名后的最后一个“/”开始到“?”为止,是文件名部分,如果没有“?”,则是从域名后的最后一个“/”开始到“#”为止,是文件部分,如果没有“?”和“#”,那么从域名后的最后一个“/”开始到结束,都是文件名部分。本例中的文件名是“index.asp”。文件名部分也不是一个URL必须的部分,如果省略该部分,则使用默认的文件名; 401 | - **锚部分**:从“#”开始到最后,都是锚部分。本例中的锚部分是“name”。锚部分也不是一个URL必须的部分; 402 | - **参数部分**:从“?”开始到“#”为止之间的部分为参数部分,又称搜索部分、查询部分。本例中的参数部分为“boardID=5&ID=24618&page=1”。参数可以允许有多个参数,参数与参数之间用“&”作为分隔符。 403 | 404 | ### 21. 与缓存相关的HTTP请求头有哪些 405 | 406 | 强缓存: 407 | 408 | - Expires 409 | - Cache-Control 410 | 411 | 412 | 413 | 协商缓存: 414 | 415 | - Etag、If-None-Match 416 | - Last-Modified、If-Modified-Since 417 | 418 | ## 二、HTTPS协议 419 | 420 | ### 1. 什么是HTTPS协议? 421 | 422 | 超文本传输安全协议(Hypertext Transfer Protocol Secure,简称:HTTPS)是一种通过计算机网络进行安全通信的传输协议。HTTPS经由HTTP进行通信,利用SSL/TLS来加密数据包。HTTPS的主要目的是提供对网站服务器的身份认证,保护交换数据的隐私与完整性。 423 | 424 | ![image](https://cdn.nlark.com/yuque/0/2020/png/1500604/1603965685749-8cc21a1b-4277-42b1-aeed-18978c1cdb95.png?x-oss-process=image%2Fresize%2Cw_1500) 425 | 426 | HTTP协议采用**明文传输**信息,存在**信息窃听**、**信息篡改**和**信息劫持**的风险,而协议TLS/SSL具有**身份验证**、**信息加密**和**完整性校验**的功能,可以避免此类问题发生。 427 | 428 | 429 | 430 | 安全层的主要职责就是**对发起的HTTP请求的数据进行加密操作** 和 **对接收到的HTTP的内容进行解密操作**。 431 | 432 | ### 2. TLS/SSL的工作原理 433 | 434 | **TLS/SSL**全称**安全传输层协议**(Transport Layer Security), 是介于TCP和HTTP之间的一层安全协议,不影响原有的TCP协议和HTTP协议,所以使用HTTPS基本上不需要对HTTP页面进行太多的改造。 435 | 436 | 437 | 438 | TLS/SSL的功能实现主要依赖三类基本算法:**散列函数hash**、**对称加密**、**非对称加密**。这三类算法的作用如下: 439 | 440 | - 基于散列函数验证信息的完整性 441 | - 对称加密算法采用协商的秘钥对数据加密 442 | - 非对称加密实现身份认证和秘钥协商 443 | 444 | ![image](https://cdn.nlark.com/yuque/0/2020/png/1500604/1603965685769-63a91dae-936d-42d3-8571-577cefa11e33.png) 445 | 446 | #### (1)散列函数hash 447 | 448 | 常见的散列函数有MD5、SHA1、SHA256。该函数的特点是单向不可逆,对输入数据非常敏感,输出的长度固定,任何数据的修改都会改变散列函数的结果,可以用于防止信息篡改并验证数据的完整性。 449 | 450 | 451 | 452 | **特点:**在信息传输过程中,散列函数不能三都实现信息防篡改,由于传输是明文传输,中间人可以修改信息后重新计算信息的摘要,所以需要对传输的信息和信息摘要进行加密。 453 | 454 | #### (2)对称加密 455 | 456 | 对称加密的方法是,双方使用同一个秘钥对数据进行加密和解密。但是对称加密的存在一个问题,就是如何保证秘钥传输的安全性,因为秘钥还是会通过网络传输的,一旦秘钥被其他人获取到,那么整个加密过程就毫无作用了。 这就要用到非对称加密的方法。 457 | 458 | 459 | 460 | 常见的对称加密算法有AES-CBC、DES、3DES、AES-GCM等。相同的秘钥可以用于信息的加密和解密。掌握秘钥才能获取信息,防止信息窃听,其通讯方式是一对一。 461 | 462 | 463 | 464 | **特点:**对称加密的优势就是信息传输使用一对一,需要共享相同的密码,密码的安全是保证信息安全的基础,服务器和N个客户端通信,需要维持N个密码记录且不能修改密码。 465 | 466 | #### (3)非对称加密 467 | 468 | 非对称加密的方法是,我们拥有两个秘钥,一个是公钥,一个是私钥。公钥是公开的,私钥是保密的。用私钥加密的数据,只有对应的公钥才能解密,用公钥加密的数据,只有对应的私钥才能解密。我们可以将公钥公布出去,任何想和我们通信的客户, 都可以使用我们提供的公钥对数据进行加密,这样我们就可以使用私钥进行解密,这样就能保证数据的安全了。但是非对称加密有一个缺点就是加密的过程很慢,因此如果每次通信都使用非对称加密的方式的话,反而会造成等待时间过长的问题。 469 | 470 | 471 | 472 | 常见的非对称加密算法有RSA、ECC、DH等。秘钥成对出现,一般称为公钥(公开)和私钥(保密)。公钥加密的信息只有私钥可以解开,私钥加密的信息只能公钥解开,因此掌握公钥的不同客户端之间不能相互解密信息,只能和服务器进行加密通信,服务器可以实现一对多的的通信,客户端也可以用来验证掌握私钥的服务器的身份。 473 | 474 | 475 | 476 | **特点:**非对称加密的特点就是信息一对多,服务器只需要维持一个私钥就可以和多个客户端进行通信,但服务器发出的信息能够被所有的客户端解密,且该算法的计算复杂,加密的速度慢。 477 | 478 | 479 | 480 | 综合上述算法特点,TLS/SSL的工作方式就是客户端使用非对称加密与服务器进行通信,实现身份的验证并协商对称加密使用的秘钥。对称加密算法采用协商秘钥对信息以及信息摘要进行加密通信,不同节点之间采用的对称秘钥不同,从而保证信息只能通信双方获取。这样就解决了两个方法各自存在的问题。 481 | 482 | ### 3. 数字证书是什么? 483 | 484 | 现在的方法也不一定是安全的,因为没有办法确定得到的公钥就一定是安全的公钥。可能存在一个中间人,截取了对方发给我们的公钥,然后将他自己的公钥发送给我们,当我们使用他的公钥加密后发送的信息,就可以被他用自己的私钥解密。然后他伪装成我们以同样的方法向对方发送信息,这样我们的信息就被窃取了,然而自己还不知道。为了解决这样的问题,可以使用数字证书。 485 | 486 | 487 | 488 | 首先使用一种 Hash 算法来对公钥和其他信息进行加密,生成一个信息摘要,然后让有公信力的认证中心(简称 CA )用它的私钥对消息摘要加密,形成签名。最后将原始的信息和签名合在一起,称为数字证书。当接收方收到数字证书的时候,先根据原始信息使用同样的 Hash 算法生成一个摘要,然后使用公证处的公钥来对数字证书中的摘要进行解密,最后将解密的摘要和生成的摘要进行对比,就能发现得到的信息是否被更改了。 489 | 490 | 491 | 492 | 这个方法最要的是认证中心的可靠性,一般浏览器里会内置一些顶层的认证中心的证书,相当于我们自动信任了他们,只有这样才能保证数据的安全。 493 | 494 | ![image](https://cdn.nlark.com/yuque/0/2020/png/1500604/1603965685765-ffc9a525-ccad-43f0-bb25-8e17281d68fe.png) 495 | 496 | ### 4. HTTPS通信(握手)过程 497 | 498 | HTTPS的通信过程如下: 499 | 500 | 1. 客户端向服务器发起请求,请求中包含使用的协议版本号、生成的一个随机数、以及客户端支持的加密方法。 501 | 2. 服务器端接收到请求后,确认双方使用的加密方法、并给出服务器的证书、以及一个服务器生成的随机数。 502 | 3. 客户端确认服务器证书有效后,生成一个新的随机数,并使用数字证书中的公钥,加密这个随机数,然后发给服 务器。并且还会提供一个前面所有内容的 hash 的值,用来供服务器检验。 503 | 4. 服务器使用自己的私钥,来解密客户端发送过来的随机数。并提供前面所有内容的 hash 值来供客户端检验。 504 | 5. 客户端和服务器端根据约定的加密方法使用前面的三个随机数,生成对话秘钥,以后的对话过程都使用这个秘钥来加密信息。 505 | 506 | ### 5. HTTPS的特点 507 | 508 | HTTPS的**优点**如下: 509 | 510 | - 使用HTTPS协议可以认证用户和服务器,确保数据发送到正确的客户端和服务器; 511 | - 使用HTTPS协议可以进行加密传输、身份认证,通信更加安全,防止数据在传输过程中被窃取、修改,确保数据安全性; 512 | - HTTPS是现行架构下最安全的解决方案,虽然不是绝对的安全,但是大幅增加了中间人攻击的成本; 513 | 514 | 515 | 516 | HTTPS的**缺点**如下: 517 | 518 | - HTTPS需要做服务器和客户端双方的加密个解密处理,耗费更多服务器资源,过程复杂; 519 | - HTTPS协议握手阶段比较费时,增加页面的加载时间; 520 | - SSL证书是收费的,功能越强大的证书费用越高; 521 | - HTTPS连接服务器端资源占用高很多,支持访客稍多的网站需要投入更大的成本; 522 | - SSL证书需要绑定IP,不能再同一个IP上绑定多个域名。 523 | 524 | ### 6. **HTTPS**是如何保证安全的? 525 | 526 | 先理解两个概念: 527 | 528 | - 对称加密:即通信的双⽅都使⽤同⼀个秘钥进⾏加解密,对称加密虽然很简单性能也好,但是⽆法解决⾸次把秘钥发给对⽅的问题,很容易被⿊客拦截秘钥。 529 | - ⾮对称加密: 530 | 531 | \1. 私钥 + 公钥= 密钥对 532 | 533 | \2. 即⽤私钥加密的数据,只有对应的公钥才能解密,⽤公钥加密的数据,只有对应的私钥才能解密 534 | 535 | \3. 因为通信双⽅的⼿⾥都有⼀套⾃⼰的密钥对,通信之前双⽅会先把⾃⼰的公钥都先发给对⽅ 536 | 537 | \4. 然后对⽅再拿着这个公钥来加密数据响应给对⽅,等到到了对⽅那⾥,对⽅再⽤⾃⼰的私钥进⾏解密 538 | 539 | 540 | 541 | ⾮对称加密虽然安全性更⾼,但是带来的问题就是速度很慢,影响性能。 542 | 543 | 544 | 545 | **解决⽅案:** 546 | 547 | 结合两种加密⽅式,将对称加密的密钥使⽤⾮对称加密的公钥进⾏加密,然后发送出去,接收⽅使⽤私钥进⾏解密得到对称加密的密钥,然后双⽅可以使⽤对称加密来进⾏沟通。 548 | 549 | 550 | 551 | 此时⼜带来⼀个问题,中间⼈问题: 552 | 553 | 如果此时在客户端和服务器之间存在⼀个中间⼈,这个中间⼈只需要把原本双⽅通信互发的公钥,换成⾃⼰的公钥,这样中间⼈就可以轻松解密通信双⽅所发送的所有数据。 554 | 555 | 556 | 557 | 所以这个时候需要⼀个安全的第三⽅颁发证书(CA),证明身份的身份,防⽌被中间⼈攻击。 证书中包括:签发者、证书⽤途、使⽤者公钥、使⽤者私钥、使⽤者的HASH算法、证书到期时间等。 558 | 559 | 560 | 561 | 但是问题来了,如果中间⼈篡改了证书,那么身份证明是不是就⽆效了?这个证明就⽩买了,这个时候需要⼀个新的技术,数字签名。 562 | 563 | 564 | 565 | 数字签名就是⽤CA⾃带的HASH算法对证书的内容进⾏HASH得到⼀个摘要,再⽤CA的私钥加密,最终组成数字签名。当别⼈把他的证书发过来的时候,我再⽤同样的Hash算法,再次⽣成消息摘要,然后⽤CA的公钥对数字签名解密,得到CA创建的消息摘要,两者⼀⽐,就知道中间有没有被⼈篡改了。这个时候就能最⼤程度保证通信的安全了。 566 | 567 | ## 三、HTTP状态码 568 | 569 | 状态码的类别: 570 | 571 | | **类别** | **原因** | **描述** | 572 | | -------- | ------------------------------- | -------------------------- | 573 | | 1xx | Informational(信息性状态码) | 接受的请求正在处理 | 574 | | 2xx | Success(成功状态码) | 请求正常处理完毕 | 575 | | 3xx | Redirection(重定向状态码) | 需要进行附加操作一完成请求 | 576 | | 4xx | Client Error (客户端错误状态码) | 服务器无法处理请求 | 577 | | 5xx | Server Error(服务器错误状态码) | 服务器处理请求出错 | 578 | 579 | ### 1. 2XX (Success 成功状态码) 580 | 581 | 状态码2XX表示请求被正常处理了。 582 | 583 | #### (1)200 OK 584 | 585 | 200 OK表示客户端发来的请求被服务器端正常处理了。 586 | 587 | #### (2)204 No Content 588 | 589 | 该状态码表示客户端发送的请求已经在服务器端正常处理了,但是没有返回的内容,响应报文中不包含实体的主体部分。一般在只需要从客户端往服务器端发送信息,而服务器端不需要往客户端发送内容时使用。 590 | 591 | #### (3)206 Partial Content 592 | 593 | 该状态码表示客户端进行了范围请求,而服务器端执行了这部分的 GET 请求。响应报文中包含由 Content-Range 指定范围的实体内容。 594 | 595 | ### 2. 3XX (Redirection 重定向状态码) 596 | 597 | 3XX 响应结果表明浏览器需要执行某些特殊的处理以正确处理请求。 598 | 599 | #### (1)301 Moved Permanently 600 | 601 | **永久重定向。** 602 | 603 | 该状态码表示请求的资源已经被分配了新的 URI,以后应使用资源指定的 URI。新的 URI 会在 HTTP 响应头中的 Location 首部字段指定。若用户已经把原来的URI保存为书签,此时会按照 Location 中新的URI重新保存该书签。同时,搜索引擎在抓取新内容的同时也将旧的网址替换为重定向之后的网址。 604 | 605 | 606 | 607 | **使用场景:** 608 | 609 | - 当我们想换个域名,旧的域名不再使用时,用户访问旧域名时用301就重定向到新的域名。其实也是告诉搜索引擎收录的域名需要对新的域名进行收录。 610 | - 在搜索引擎的搜索结果中出现了不带www的域名,而带www的域名却没有收录,这个时候可以用301重定向来告诉搜索引擎我们目标的域名是哪一个。 611 | 612 | #### (2)302 Found 613 | 614 | **临时重定向。** 615 | 616 | 该状态码表示请求的资源被分配到了新的 URI,希望用户(本次)能使用新的 URI 访问资源。和 301 Moved Permanently 状态码相似,但是 302 代表的资源不是被永久重定向,只是临时性质的。也就是说已移动的资源对应的 URI 将来还有可能发生改变。若用户把 URI 保存成书签,但不会像 301 状态码出现时那样去更新书签,而是仍旧保留返回 302 状态码的页面对应的 URI。同时,搜索引擎会抓取新的内容而保留旧的网址。因为服务器返回302代码,搜索引擎认为新的网址只是暂时的。 617 | 618 | 619 | 620 | **使用场景:** 621 | 622 | - 当我们在做活动时,登录到首页自动重定向,进入活动页面。 623 | - 未登陆的用户访问用户中心重定向到登录页面。 624 | - 访问404页面重新定向到首页。 625 | 626 | #### (3)303 See Other 627 | 628 | 该状态码表示由于请求对应的资源存在着另一个 URI,应使用 GET 方法定向获取请求的资源。 629 | 630 | 303 状态码和 302 Found 状态码有着相似的功能,但是 303 状态码明确表示客户端应当采用 GET 方法获取资源。 631 | 632 | 633 | 634 | 303 状态码通常作为 PUT 或 POST 操作的返回结果,它表示重定向链接指向的不是新上传的资源,而是另外一个页面,比如消息确认页面或上传进度页面。而请求重定向页面的方法要总是使用 GET。 635 | 636 | 637 | 638 | 注意: 639 | 640 | - 当 301、302、303 响应状态码返回时,几乎所有的浏览器都会把 POST 改成GET,并删除请求报文内的主体,之后请求会再次自动发送。 641 | - 301、302 标准是禁止将 POST 方法变成 GET方法的,但实际大家都会这么做。 642 | 643 | #### (4)304 Not Modified 644 | 645 | **浏览器缓存相关。** 646 | 647 | 该状态码表示客户端发送附带条件的请求时,服务器端允许请求访问资源,但未满足条件的情况。304 状态码返回时,不包含任何响应的主体部分。304 虽然被划分在 3XX 类别中,但是和重定向没有关系。 648 | 649 | 650 | 651 | 带条件的请求(Http 条件请求):使用 Get方法 请求,请求报文中包含(`if-match`、`if-none-match`、`if-modified-since`、`if-unmodified-since`、`if-range`)中任意首部。 652 | 653 | 654 | 655 | 状态码304并不是一种错误,而是告诉客户端有缓存,直接使用缓存中的数据。返回页面的只有头部信息,是没有内容部分的,这样在一定程度上提高了网页的性能。 656 | 657 | #### (5)307 Temporary Redirect 658 | 659 | **307表示临时重定向。**该状态码与 302 Found 有着相同含义,尽管 302 标准禁止 POST 变成 GET,但是实际使用时还是这样做了。 660 | 661 | 662 | 663 | 307 会遵守浏览器标准,**不会从 POST 变成 GET**。但是对于处理请求的行为时,不同浏览器还是会出现不同的情况。规范要求浏览器继续向 Location 的地址 POST 内容。规范要求浏览器继续向 Location 的地址 POST 内容。 664 | 665 | ### 3. 4XX (Client Error 客户端错误状态码) 666 | 667 | 4XX 的响应结果表明客户端是发生错误的原因所在。 668 | 669 | #### (1)400 Bad Request 670 | 671 | 该状态码表示请求报文中存在语法错误。当错误发生时,需修改请求的内容后再次发送请求。另外,浏览器会像 200 OK 一样对待该状态码。 672 | 673 | #### (2)401 Unauthorized 674 | 675 | 该状态码表示发送的请求需要有通过 HTTP 认证(BASIC 认证、DIGEST 认证)的认证信息。若之前已进行过一次请求,则表示用户认证失败 676 | 677 | 678 | 679 | 返回含有 401 的响应必须包含一个适用于被请求资源的 WWW-Authenticate 首部用以质询(challenge)用户信息。当浏览器初次接收到 401 响应,会弹出认证用的对话窗口。 680 | 681 | 682 | 683 | 以下情况会出现401: 684 | 685 | - 401.1 - 登录失败。 686 | - 401.2 - 服务器配置导致登录失败。 687 | - 401.3 - 由于 ACL 对资源的限制而未获得授权。 688 | - 401.4 - 筛选器授权失败。 689 | - 401.5 - ISAPI/CGI 应用程序授权失败。 690 | - 401.7 - 访问被 Web 服务器上的 URL 授权策略拒绝。这个错误代码为 IIS 6.0 所专用。 691 | 692 | #### (3)403 Forbidden 693 | 694 | 该状态码表明请求资源的访问被服务器拒绝了,服务器端没有必要给出详细理由,但是可以在响应报文实体的主体中进行说明。进入该状态后,不能再继续进行验证。该访问是永久禁止的,并且与应用逻辑密切相关。 695 | 696 | 697 | 698 | IIS 定义了许多不同的 403 错误,它们指明更为具体的错误原因: 699 | 700 | - 403.1 - 执行访问被禁止。 701 | - 403.2 - 读访问被禁止。 702 | - 403.3 - 写访问被禁止。 703 | - 403.4 - 要求 SSL。 704 | - 403.5 - 要求 SSL 128。 705 | - 403.6 - IP 地址被拒绝。 706 | - 403.7 - 要求客户端证书。 707 | - 403.8 - 站点访问被拒绝。 708 | - 403.9 - 用户数过多。 709 | - 403.10 - 配置无效。 710 | - 403.11 - 密码更改。 711 | - 403.12 - 拒绝访问映射表。 712 | - 403.13 - 客户端证书被吊销。 713 | - 403.14 - 拒绝目录列表。 714 | - 403.15 - 超出客户端访问许可。 715 | - 403.16 - 客户端证书不受信任或无效。 716 | - 403.17 - 客户端证书已过期或尚未生效 717 | - 403.18 - 在当前的应用程序池中不能执行所请求的 URL。这个错误代码为 IIS 6.0 所专用。 718 | - 403.19 - 不能为这个应用程序池中的客户端执行 CGI。这个错误代码为 IIS 6.0 所专用。 719 | - 403.20 - Passport 登录失败。这个错误代码为 IIS 6.0 所专用。 720 | 721 | #### (4)404 Not Found 722 | 723 | 该状态码表明服务器上无法找到请求的资源。除此之外,也可以在服务器端拒绝请求且不想说明理由时使用。 724 | 725 | 以下情况会出现404: 726 | 727 | - 404.0 -(无) – 没有找到文件或目录。 728 | - 404.1 - 无法在所请求的端口上访问 Web 站点。 729 | - 404.2 - Web 服务扩展锁定策略阻止本请求。 730 | - 404.3 - MIME 映射策略阻止本请求。 731 | 732 | #### (5)405 Method Not Allowed 733 | 734 | 该状态码表示客户端请求的方法虽然能被服务器识别,但是服务器禁止使用该方法。GET 和 HEAD 方法,服务器应该总是允许客户端进行访问。客户端可以通过 OPTIONS 方法(预检)来查看服务器允许的访问方法, 如下 735 | 736 | ``` 737 | Access-Control-Allow-Methods: GET,HEAD,PUT,PATCH,POST,DELETE 738 | ``` 739 | 740 | ### 4. 5XX (Server Error 服务器错误状态码) 741 | 742 | 5XX 的响应结果表明服务器本身发生错误. 743 | 744 | #### (1)500 Internal Server Error 745 | 746 | 该状态码表明服务器端在执行请求时发生了错误。也有可能是 Web 应用存在的 bug 或某些临时的故障。 747 | 748 | #### (2)502 Bad Gateway 749 | 750 | 该状态码表明扮演网关或代理角色的服务器,从上游服务器中接收到的响应是无效的。注意,502 错误通常不是客户端能够修复的,而是需要由途经的 Web 服务器或者代理服务器对其进行修复。以下情况会出现502: 751 | 752 | - 502.1 - CGI (通用网关接口)应用程序超时。 753 | - 502.2 - CGI (通用网关接口)应用程序出错。 754 | 755 | #### (3)503 Service Unavailable 756 | 757 | 该状态码表明服务器暂时处于超负载或正在进行停机维护,现在无法处理请求。如果事先得知解除以上状况需要的时间,最好写入 RetryAfter 首部字段再返回给客户端。 758 | 759 | 760 | 761 | **使用场景:** 762 | 763 | - 服务器停机维护时,主动用503响应请求; 764 | - nginx 设置限速,超过限速,会返回503。 765 | 766 | #### (4)504 Gateway Timeout 767 | 768 | 该状态码表示网关或者代理的服务器无法在规定的时间内获得想要的响应。他是HTTP 1.1中新加入的。 769 | 770 | 771 | 772 | 使用场景:代码执行时间超时,或者发生了死循环。 773 | 774 | ### 5. 总结 775 | 776 | **(1)2XX 成功** 777 | 778 | - 200 OK,表示从客户端发来的请求在服务器端被正确处理 779 | - 204 No content,表示请求成功,但响应报文不含实体的主体部分 780 | - 205 Reset Content,表示请求成功,但响应报文不含实体的主体部分,但是与 204 响应不同在于要求请求方重置内容 781 | - 206 Partial Content,进行范围请求 782 | 783 | **(2)3XX 重定向** 784 | 785 | - 301 moved permanently,永久性重定向,表示资源已被分配了新的 URL 786 | - 302 found,临时性重定向,表示资源临时被分配了新的 URL 787 | - 303 see other,表示资源存在着另一个 URL,应使用 GET 方法获取资源 788 | - 304 not modified,表示服务器允许访问资源,但因发生请求未满足条件的情况 789 | - 307 temporary redirect,临时重定向,和302含义类似,但是期望客户端保持请求方法不变向新的地址发出请求 790 | 791 | **(3)4XX 客户端错误** 792 | 793 | - 400 bad request,请求报文存在语法错误 794 | - 401 unauthorized,表示发送的请求需要有通过 HTTP 认证的认证信息 795 | - 403 forbidden,表示对请求资源的访问被服务器拒绝 796 | - 404 not found,表示在服务器上没有找到请求的资源 797 | 798 | **(4)5XX 服务器错误** 799 | 800 | - 500 internal sever error,表示服务器端在执行请求时发生了错误 801 | - 501 Not Implemented,表示服务器不支持当前请求所需要的某个功能 802 | - 503 service unavailable,表明服务器暂时处于超负载或正在停机维护,无法处理请求 803 | 804 | ### 6. 同样是重定向,**307**,**303**,**302**的区别? 805 | 806 | 302是http1.0的协议状态码,在http1.1版本的时候为了细化302状态码⼜出来了两个303和307。 303明确表示客户端应当采⽤get⽅法获取资源,他会把POST请求变为GET请求进⾏重定向。 307会遵照浏览器标准,不会从post变为get。 807 | 808 | ## 四、DNS协议介绍 809 | 810 | ### 1. DNS 协议是什么 811 | 812 | **概念**: DNS 是域名系统 (Domain Name System) 的缩写,提供的是一种主机名到 IP 地址的转换服务,就是我们常说的域名系统。它是一个由分层的 DNS 服务器组成的分布式数据库,是定义了主机如何查询这个分布式数据库的方式的应用层协议。能够使人更方便的访问互联网,而不用去记住能够被机器直接读取的IP数串。 813 | 814 | 815 | 816 | **作用**: 将域名解析为IP地址,客户端向DNS服务器(DNS服务器有自己的IP地址)发送域名查询请求,DNS服务器告知客户机Web服务器的 IP 地址。 817 | 818 | ### 2. DNS同时使用TCP和UDP协议? 819 | 820 | **DNS占用53号端口,同时使用TCP和UDP协议。** 821 | 822 | (1)在区域传输的时候使用TCP协议 823 | 824 | - 辅域名服务器会定时(一般3小时)向主域名服务器进行查询以便了解数据是否有变动。如有变动,会执行一次区域传送,进行数据同步。区域传送使用TCP而不是UDP,因为数据同步传送的数据量比一个请求应答的数据量要多得多。 825 | - TCP是一种可靠连接,保证了数据的准确性。 826 | 827 | (2)在域名解析的时候使用UDP协议 828 | 829 | - 客户端向DNS服务器查询域名,一般返回的内容都不超过512字节,用UDP传输即可。不用经过三次握手,这样DNS服务器负载更低,响应更快。理论上说,客户端也可以指定向DNS服务器查询时用TCP,但事实上,很多DNS服务器进行配置的时候,仅支持UDP查询包。 830 | 831 | ### 3. DNS完整的查询过程 832 | 833 | DNS服务器解析域名的过程: 834 | 835 | - 首先会在**浏览器的缓存**中查找对应的IP地址,如果查找到直接返回,若找不到继续下一步 836 | - 将请求发送给**本地DNS服务器**,在本地域名服务器缓存中查询,如果查找到,就直接将查找结果返回,若找不到继续下一步 837 | - 本地DNS服务器向**根域名服务器**发送请求,根域名服务器会返回一个所查询域的顶级域名服务器地址 838 | - 本地DNS服务器向**顶级域名服务器**发送请求,接受请求的服务器查询自己的缓存,如果有记录,就返回查询结果,如果没有就返回相关的下一级的权威域名服务器的地址 839 | - 本地DNS服务器向**权威域名服务器**发送请求,域名服务器返回对应的结果 840 | - 本地DNS服务器将返回结果保存在缓存中,便于下次使用 841 | - 本地DNS服务器将返回结果返回给浏览器 842 | 843 | 844 | 845 | 比如要查询 [www.baidu.com](http://www.baidu.com/) 的 IP 地址,首先会在浏览器的缓存中查找是否有该域名的缓存,如果不存在就将请求发送到本地的 DNS 服务器中,本地DNS服务器会判断是否存在该域名的缓存,如果不存在,则向根域名服务器发送一个请求,根域名服务器返回负责 .com 的顶级域名服务器的 IP 地址的列表。然后本地 DNS 服务器再向其中一个负责 .com 的顶级域名服务器发送一个请求,负责 .com 的顶级域名服务器返回负责 .baidu 的权威域名服务器的 IP 地址列表。然后本地 DNS 服务器再向其中一个权威域名服务器发送一个请求,最后权威域名服务器返回一个对应的主机名的 IP 地址列表。 846 | 847 | ### 4. 迭代查询与递归查询 848 | 849 | 实际上,DNS解析是一个包含迭代查询和递归查询的过程。 850 | 851 | - **递归查询**指的是查询请求发出后,域名服务器代为向下一级域名服务器发出请求,最后向用户返回查询的最终结果。使用递归 查询,用户只需要发出一次查询请求。 852 | - **迭代查询**指的是查询请求后,域名服务器返回单次查询的结果。下一级的查询由用户自己请求。使用迭代查询,用户需要发出 多次的查询请求。 853 | 854 | 855 | 856 | 一般我们向本地 DNS 服务器发送请求的方式就是递归查询,因为我们只需要发出一次请求,然后本地 DNS 服务器返回给我 们最终的请求结果。而本地 DNS 服务器向其他域名服务器请求的过程是迭代查询的过程,因为每一次域名服务器只返回单次 查询的结果,下一级的查询由本地 DNS 服务器自己进行。 857 | 858 | ### 5. DNS 记录和报文 859 | 860 | DNS 服务器中以资源记录的形式存储信息,每一个 DNS 响应报文一般包含多条资源记录。一条资源记录的具体的格式为 861 | 862 | ``` 863 | (Name,Value,Type,TTL) 864 | ``` 865 | 866 | 其中 TTL 是资源记录的生存时间,它定义了资源记录能够被其他的 DNS 服务器缓存多长时间。 867 | 868 | 869 | 870 | 常用的一共有四种 Type 的值,分别是 A、NS、CNAME 和 MX ,不同 Type 的值,对应资源记录代表的意义不同: 871 | 872 | - 如果 Type = A,则 Name 是主机名,Value 是主机名对应的 IP 地址。因此一条记录为 A 的资源记录,提供了标 准的主机名到 IP 地址的映射。 873 | - 如果 Type = NS,则 Name 是个域名,Value 是负责该域名的 DNS 服务器的主机名。这个记录主要用于 DNS 链式 查询时,返回下一级需要查询的 DNS 服务器的信息。 874 | - 如果 Type = CNAME,则 Name 为别名,Value 为该主机的规范主机名。该条记录用于向查询的主机返回一个主机名 对应的规范主机名,从而告诉查询主机去查询这个主机名的 IP 地址。主机别名主要是为了通过给一些复杂的主机名提供 一个便于记忆的简单的别名。 875 | - 如果 Type = MX,则 Name 为一个邮件服务器的别名,Value 为邮件服务器的规范主机名。它的作用和 CNAME 是一 样的,都是为了解决规范主机名不利于记忆的缺点。 876 | 877 | ## 五、网络模型 878 | 879 | ### 1. OSI七层模型 880 | 881 | `ISO`为了更好的使网络应用更为普及,推出了`OSI`参考模型。 882 | 883 | ![image](https://cdn.nlark.com/yuque/0/2020/webp/1500604/1604024701811-7ac177bc-d649-45b8-8646-53af8f18a0de.webp) 884 | 885 | #### (1)应用层 886 | 887 | `OSI`参考模型中最靠近用户的一层,是为计算机用户提供应用接口,也为用户直接提供各种网络服务。我们常见应用层的网络服务协议有:`HTTP`,`HTTPS`,`FTP`,`POP3`、`SMTP`等。 888 | 889 | - 在客户端与服务器中经常会有数据的请求,这个时候就是会用到`http(hyper text transfer protocol)(超文本传输协议)`或者`https`.在后端设计数据接口时,我们常常使用到这个协议。 890 | - `FTP`是文件传输协议,在开发过程中,个人并没有涉及到,但是我想,在一些资源网站,比如`百度网盘``迅雷`应该是基于此协议的。 891 | - `SMTP`是`simple mail transfer protocol(简单邮件传输协议)`。在一个项目中,在用户邮箱验证码登录的功能时,使用到了这个协议。 892 | 893 | #### (2)表示层 894 | 895 | 表示层提供各种用于应用层数据的编码和转换功能,确保一个系统的应用层发送的数据能被另一个系统的应用层识别。如果必要,该层可提供一种标准表示形式,用于将计算机内部的多种数据格式转换成通信中采用的标准表示形式。数据压缩和加密也是表示层可提供的转换功能之一。 896 | 897 | 898 | 899 | 在项目开发中,为了方便数据传输,可以使用`base64`对数据进行编解码。如果按功能来划分,`base64`应该是工作在表示层。 900 | 901 | #### (3)会话层 902 | 903 | 会话层就是负责建立、管理和终止表示层实体之间的通信会话。该层的通信由不同设备中的应用程序之间的服务请求和响应组成。 904 | 905 | #### (4)传输层 906 | 907 | 传输层建立了主机端到端的链接,传输层的作用是为上层协议提供端到端的可靠和透明的数据传输服务,包括处理差错控制和流量控制等问题。该层向高层屏蔽了下层数据通信的细节,使高层用户看到的只是在两个传输实体间的一条主机到主机的、可由用户控制和设定的、可靠的数据通路。我们通常说的,`TCP` `UDP`就是在这一层。端口号既是这里的“端”。 908 | 909 | #### (5)网络层 910 | 911 | 本层通过`IP`寻址来建立两个节点之间的连接,为源端的运输层送来的分组,选择合适的路由和交换节点,正确无误地按照地址传送给目的端的运输层。就是通常说的`IP`层。这一层就是我们经常说的`IP`协议层。`IP`协议是`Internet`的基础。我们可以这样理解,网络层规定了数据包的传输路线,而传输层则规定了数据包的传输方式。 912 | 913 | #### (6)数据链路层 914 | 915 | 将比特组合成字节,再将字节组合成帧,使用链路层地址 (以太网使用MAC地址)来访问介质,并进行差错检测。 916 | 917 | 网络层与数据链路层的对比,通过上面的描述,我们或许可以这样理解,网络层是规划了数据包的传输路线,而数据链路层就是传输路线。不过,在数据链路层上还增加了差错控制的功能。 918 | 919 | #### (7)物理层 920 | 921 | 实际最终信号的传输是通过物理层实现的。通过物理介质传输比特流。规定了电平、速度和电缆针脚。常用设备有(各种物理设备)集线器、中继器、调制解调器、网线、双绞线、同轴电缆。这些都是物理层的传输介质。 922 | 923 | 924 | 925 | **OSI七层模型通信特点:对等通信** 926 | 927 | 对等通信,为了使数据分组从源传送到目的地,源端OSI模型的每一层都必须与目的端的对等层进行通信,这种通信方式称为对等层通信。在每一层通信过程中,使用本层自己协议进行通信。 928 | 929 | ### 2. TCP/IP五层协议 930 | 931 | `TCP/IP`五层协议和`OSI`的七层协议对应关系如下: 932 | 933 | ![image](https://cdn.nlark.com/yuque/0/2020/png/1500604/1604025489154-966a96f5-1c8e-4d03-b9f9-f51250f63a22.png) 934 | 935 | ![image](https://cdn.nlark.com/yuque/0/2020/png/1500604/1604025489154-966a96f5-1c8e-4d03-b9f9-f51250f63a22.png) 936 | 937 | - **应用层 (application layer)**:直接为应用进程提供服务。应用层协议定义的是应用进程间通讯和交互的规则,不同的应用有着不同的应用层协议,如 HTTP协议(万维网服务)、FTP协议(文件传输)、SMTP协议(电子邮件)、DNS(域名查询)等。 938 | - **传输层 (transport layer)**:有时也译为运输层,它负责为两台主机中的进程提供通信服务。该层主要有以下两种协议: 939 | 940 | - - 传输控制协议 (Transmission Control Protocol,TCP):提供面向连接的、可靠的数据传输服务,数据传输的基本单位是报文段(segment); 941 | - 用户数据报协议 (User Datagram Protocol,UDP):提供无连接的、尽最大努力的数据传输服务,但不保证数据传输的可靠性,数据传输的基本单位是用户数据报。 942 | 943 | - **网络层 (internet layer)**:有时也译为网际层,它负责为两台主机提供通信服务,并通过选择合适的路由将数据传递到目标主机。 944 | - **数据链路层 (data link layer)**:负责将网络层交下来的 IP 数据报封装成帧,并在链路的两个相邻节点间传送帧,每一帧都包含数据和必要的控制信息(如同步信息、地址信息、差错控制等)。 945 | - **物理层 (physical Layer)**:确保数据可以在各种物理媒介上进行传输,为数据的传输提供可靠的环境。 946 | 947 | 948 | 949 | 从上图中可以看出,`TCP/IP`模型比`OSI`模型更加简洁,它把`应用层/表示层/会话层`全部整合为了`应用层`。 950 | 951 | 952 | 953 | 在每一层都工作着不同的设备,比如我们常用的交换机就工作在数据链路层的,一般的路由器是工作在网络层的。 954 | 955 | ![image](https://cdn.nlark.com/yuque/0/2020/webp/1500604/1604025001463-47398363-0ab1-4bcf-b247-5d3d76ce117a.webp) 956 | 957 | 在每一层实现的协议也各不同,即每一层的服务也不同,下图列出了每层主要的传输协议: 958 | 959 | ![image](https://cdn.nlark.com/yuque/0/2020/webp/1500604/1604025001203-7b104c36-1453-475e-a282-ff2e9cb19e2c.webp) 960 | 961 | 同样,`TCP/IP`五层协议的通信方式也是对等通信: 962 | 963 | ![image.png](https://cdn.nlark.com/yuque/0/2020/png/1500604/1604065635582-1a4e62ba-bdfa-45db-8a56-0302ff9a8020.png) 964 | 965 | ## 六、TCP与UDP 966 | 967 | ### 1. TCP 和 UDP的概念及特点 968 | 969 | TCP 和 UDP都是传输层协议,他们都属于TCP/IP协议族: 970 | 971 | **(1)UDP** 972 | 973 | UDP的全称是**用户数据报协议**,在网络中它与TCP协议一样用于处理数据包,是一种无连接的协议。在OSI模型中,在传输层,处于IP协议的上一层。UDP有不提供数据包分组、组装和不能对数据包进行排序的缺点,也就是说,当报文发送之后,是无法得知其是否安全完整到达的。 974 | 975 | 976 | 977 | 它的特点如下: 978 | 979 | **1)面向无连接** 980 | 981 | 首先 UDP 是不需要和 TCP一样在发送数据前进行三次握手建立连接的,想发数据就可以开始发送了。并且也只是数据报文的搬运工,不会对数据报文进行任何拆分和拼接操作。 982 | 983 | 984 | 985 | 具体来说就是: 986 | 987 | - 在发送端,应用层将数据传递给传输层的 UDP 协议,UDP 只会给数据增加一个 UDP 头标识下是 UDP 协议,然后就传递给网络层了 988 | - 在接收端,网络层将数据传递给传输层,UDP 只去除 IP 报文头就传递给应用层,不会任何拼接操作 989 | 990 | **2)有单播,多播,广播的功能** 991 | 992 | UDP 不止支持一对一的传输方式,同样支持一对多,多对多,多对一的方式,也就是说 UDP 提供了单播,多播,广播的功能。 993 | 994 | **3)面向报文** 995 | 996 | 发送方的UDP对应用程序交下来的报文,在添加首部后就向下交付IP层。UDP对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。因此,应用程序必须选择合适大小的报文 997 | 998 | **4)不可靠性** 999 | 1000 | 首先不可靠性体现在无连接上,通信都不需要建立连接,想发就发,这样的情况肯定不可靠。 1001 | 1002 | 并且收到什么数据就传递什么数据,并且也不会备份数据,发送数据也不会关心对方是否已经正确接收到数据了。 1003 | 1004 | 1005 | 1006 | 再者网络环境时好时坏,但是 UDP 因为没有拥塞控制,一直会以恒定的速度发送数据。即使网络条件不好,也不会对发送速率进行调整。这样实现的弊端就是在网络条件不好的情况下可能会导致丢包,但是优点也很明显,在某些实时性要求高的场景(比如电话会议)就需要使用 UDP 而不是 TCP。 1007 | 1008 | **5)头部开销小,传输数据报文时是很高效的。** 1009 | 1010 | ![image](https://cdn.nlark.com/yuque/0/2020/png/1500604/1604023358917-72134998-53bd-4d9c-b71f-7a762dff31a9.png) 1011 | 1012 | UDP 头部包含了以下几个数据: 1013 | 1014 | - 两个十六位的端口号,分别为源端口(可选字段)和目标端口 1015 | - 整个数据报文的长度 1016 | - 整个数据报文的检验和(IPv4 可选字段),该字段用于发现头部信息和数据中的错误 1017 | 1018 | 1019 | 1020 | 因此 UDP 的头部开销小,只有8字节,相比 TCP 的至少20字节要少得多,在传输数据报文时是很高效的。 1021 | 1022 | 1023 | 1024 | **(2)TCP** 1025 | 1026 | TCP的全称是传输控制协议是一种面向连接的、可靠的、基于字节流的传输层通信协议。TCP 是面向连接的、可靠的流协议(流就是指不间断的数据结构)。 1027 | 1028 | 1029 | 1030 | 它有以下几个特点: 1031 | 1032 | **1)面向连接** 1033 | 1034 | 面向连接,是指发送数据之前必须在两端建立连接。建立连接的方法是“三次握手”,这样能建立可靠的连接。建立连接,是为数据的可靠传输打下了基础。 1035 | 1036 | **2)仅支持单播传输** 1037 | 1038 | 每条TCP传输连接只能有两个端点,只能进行点对点的数据传输,不支持多播和广播传输方式。 1039 | 1040 | **3)面向字节流** 1041 | 1042 | TCP不像UDP一样那样一个个报文独立地传输,而是在不保留报文边界的情况下以字节流方式进行传输。 1043 | 1044 | **4)可靠传输** 1045 | 1046 | 对于可靠传输,判断丢包、误码靠的是TCP的段编号以及确认号。TCP为了保证报文传输的可靠,就给每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的字节发回一个相应的确认(ACK);如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据(假设丢失了)将会被重传。 1047 | 1048 | **5)提供拥塞控制** 1049 | 1050 | 当网络出现拥塞的时候,TCP能够减小向网络注入数据的速率和数量,缓解拥塞。 1051 | 1052 | **6)提供全双工通信** 1053 | 1054 | TCP允许通信双方的应用程序在任何时候都能发送数据,因为TCP连接的两端都设有缓存,用来临时存放双向通信的数据。当然,TCP可以立即发送一个数据段,也可以缓存一段时间以便一次发送更多的数据段(最大的数据段大小取决于MSS) 1055 | 1056 | ### 2. TCP和UDP的区别 1057 | 1058 | | | UDP | TCP | 1059 | | ------------ | ------------------------------------------ | ---------------------------------------------------- | 1060 | | 是否连接 | 无连接 | 面向连接 | 1061 | | 是否可靠 | 不可靠传输,不使用流量控制和拥塞控制 | 可靠传输(数据顺序和正确性),使用流量控制和拥塞控制 | 1062 | | 连接对象个数 | 支持一对一,一对多,多对一和多对多交互通信 | 只能是一对一通信 | 1063 | | 传输方式 | 面向报文 | 面向字节流 | 1064 | | 首部开销 | 首部开销小,仅8字节 | 首部最小20字节,最大60字节 | 1065 | | 适用场景 | 适用于实时应用,例如视频会议、直播 | 适用于要求可靠传输的应用,例如文件传输 | 1066 | 1067 | ### 3. TCP和UDP的使用场景 1068 | 1069 | - **TCP应用场景:** 效率要求相对低,但对准确性要求相对高的场景。因为传输中需要对数据确认、重发、排序等操作,相比之下效率没有UDP高。例如:文件传输(准确高要求高、但是速度可以相对慢)、接受邮件、远程登录。 1070 | - **UDP应用场景:** 效率要求相对高,对准确性要求相对低的场景。例如:QQ聊天、在线视频、网络语音电话(即时通讯,速度要求高,但是出现偶尔断续不是太大问题,并且此处完全不可以使用重发机制)、广播通信(广播、多播)。 1071 | 1072 | ### 4. UDP协议为什么不可靠? 1073 | 1074 | UDP在传输数据之前不需要先建立连接,远地主机的运输层在接收到UDP报文后,不需要确认,提供不可靠交付。总结就以下四点: 1075 | 1076 | - 不保证消息交付:不确认,不重传,无超时 1077 | - 不保证交付顺序:不设置包序号,不重排,不会发生队首阻塞 1078 | - 不跟踪连接状态:不必建立连接或重启状态机 1079 | - 不进行拥塞控制:不内置客户端或网络反馈机制 1080 | 1081 | ### 5. TCP的重传机制 1082 | 1083 | 由于TCP的下层网络(网络层)可能出现**丢失、重复或失序**的情况,TCP协议提供可靠数据传输服务。为保证数据传输的正确性,TCP会重传其认为已丢失(包括报文中的比特错误)的包。TCP使用两套独立的机制来完成重传,一是**基于时间**,二是**基于确认信息**。 1084 | 1085 | 1086 | 1087 | TCP在发送一个数据之后,就开启一个定时器,若是在这个时间内没有收到发送数据的ACK确认报文,则对该报文进行重传,在达到一定次数还没有成功时放弃并发送一个复位信号。 1088 | 1089 | ### 6. TCP的拥塞控制机制 1090 | 1091 | TCP的拥塞控制机制主要是以下四种机制: 1092 | 1093 | - 慢启动(慢开始) 1094 | - 拥塞避免 1095 | - 快速重传 1096 | - 快速恢复 1097 | 1098 | **(1)慢启动(慢开始)** 1099 | 1100 | - 在开始发送的时候设置cwnd = 1(cwnd指的是拥塞窗口) 1101 | - 思路:开始的时候不要发送大量数据,而是先测试一下网络的拥塞程度,由小到大增加拥塞窗口的大小。 1102 | - 为了防止cwnd增长过大引起网络拥塞,设置一个慢开始门限(ssthresh 状态变量) 1103 | 1104 | - - 当cnwd < ssthresh,使用慢开始算法 1105 | - 当cnwd = ssthresh,既可使用慢开始算法,也可以使用拥塞避免算法 1106 | - 当cnwd > ssthresh,使用拥塞避免算法 1107 | 1108 | **(2)拥塞避免** 1109 | 1110 | - 拥塞避免未必能够完全避免拥塞,是说在拥塞避免阶段将拥塞窗口控制为按线性增长,使网络不容易出现阻塞。 1111 | - 思路: 让拥塞窗口cwnd缓慢的增大,即每经过一个返回时间RTT就把发送方的拥塞控制窗口加一 1112 | - 无论是在慢开始阶段还是在拥塞避免阶段,只要发送方判断网络出现拥塞,就把慢开始门限设置为出现拥塞时的发送窗口大小的一半。然后把拥塞窗口设置为1,执行慢开始算法。如图所示: 1113 | ![image](https://cdn.nlark.com/yuque/0/2020/png/1500604/1604022952123-62276cba-d882-46c9-8a1c-70655dc501af.png) 1114 | 1115 | - 其中,判断网络出现拥塞的根据就是没有收到确认,虽然没有收到确认可能是其他原因的分组丢失,但是因为无法判定,所以都当做拥塞来处理。 1116 | 1117 | **(3)快速重传** 1118 | 1119 | - 快重传要求接收方在收到一个失序的报文段后就立即发出重复确认(为的是使发送方及早知道有报文段没有到达对方)。发送方只要连续收到三个重复确认就立即重传对方尚未收到的报文段,而不必继续等待设置的重传计时器时间到期。 1120 | - 由于不需要等待设置的重传计时器到期,能尽早重传未被确认的报文段,能提高整个网络的吞吐量 1121 | 1122 | **(4)快速恢复** 1123 | 1124 | - 当发送方连续收到三个重复确认时,就执行“乘法减小”算法,把ssthresh门限减半。但是接下去并不执行慢开始算法。 1125 | - 考虑到如果网络出现拥塞的话就不会收到好几个重复的确认,所以发送方现在认为网络可能没有出现拥塞。所以此时不执行慢开始算法,而是将cwnd设置为ssthresh的大小,然后执行拥塞避免算法。 1126 | ![image](https://cdn.nlark.com/yuque/0/2020/png/1500604/1604022952153-a7106d22-225d-4081-9b0a-56b0d1876bc2.png) 1127 | 1128 | ### 7. TCP的流量控制机制 1129 | 1130 | 一般来说,流量控制就是为了让发送方发送数据的速度不要太快,要让接收方来得及接收。TCP采用大小可变的**滑动窗口**进行流量控制,窗口大小的单位是字节。这里说的窗口大小其实就是每次传输的数据大小。 1131 | 1132 | - 当一个连接建立时,连接的每一端分配一个缓冲区来保存输入的数据,并将缓冲区的大小发送给另一端。 1133 | - 当数据到达时,接收方发送确认,其中包含了自己剩余的缓冲区大小。(剩余的缓冲区空间的大小被称为窗口,指出窗口大小的通知称为窗口通告 。接收方在发送的每一确认中都含有一个窗口通告。) 1134 | - 如果接收方应用程序读数据的速度能够与数据到达的速度一样快,接收方将在每一确认中发送一个正的窗口通告。 1135 | - 如果发送方操作的速度快于接收方,接收到的数据最终将充满接收方的缓冲区,导致接收方通告一个零窗口 。发送方收到一个零窗口通告时,必须停止发送,直到接收方重新通告一个正的窗口。 1136 | 1137 | ### 8. TCP的可靠传输机制 1138 | 1139 | TCP 的可靠传输机制是基于连续 ARQ 协议和滑动窗口协议的。 1140 | 1141 | 1142 | 1143 | TCP 协议在发送方维持了一个发送窗口,发送窗口以前的报文段是已经发送并确认了的报文段,发送窗口中包含了已经发送但 未确认的报文段和允许发送但还未发送的报文段,发送窗口以后的报文段是缓存中还不允许发送的报文段。当发送方向接收方发 送报文时,会依次发送窗口内的所有报文段,并且设置一个定时器,这个定时器可以理解为是最早发送但未收到确认的报文段。 如果在定时器的时间内收到某一个报文段的确认回答,则滑动窗口,将窗口的首部向后滑动到确认报文段的后一个位置,此时如 果还有已发送但没有确认的报文段,则重新设置定时器,如果没有了则关闭定时器。如果定时器超时,则重新发送所有已经发送 但还未收到确认的报文段,并将超时的间隔设置为以前的两倍。当发送方收到接收方的三个冗余的确认应答后,这是一种指示, 说明该报文段以后的报文段很有可能发生丢失了,那么发送方会启用快速重传的机制,就是当前定时器结束前,发送所有的已发 送但确认的报文段。 1144 | 1145 | 1146 | 1147 | 接收方使用的是累计确认的机制,对于所有按序到达的报文段,接收方返回一个报文段的肯定回答。如果收到了一个乱序的报文 段,那么接方会直接丢弃,并返回一个最近的按序到达的报文段的肯定回答。使用累计确认保证了返回的确认号之前的报文段都 已经按序到达了,所以发送窗口可以移动到已确认报文段的后面。 1148 | 1149 | 1150 | 1151 | 发送窗口的大小是变化的,它是由接收窗口剩余大小和网络中拥塞程度来决定的,TCP 就是通过控制发送窗口的长度来控制报文 段的发送速率。 1152 | 1153 | 1154 | 1155 | 但是 TCP 协议并不完全和滑动窗口协议相同,因为许多的 TCP 实现会将失序的报文段给缓存起来,并且发生重传时,只会重 传一个报文段,因此 TCP 协议的可靠传输机制更像是窗口滑动协议和选择重传协议的一个混合体。 1156 | 1157 | ### 9. TCP的三次握手和四次挥手 1158 | 1159 | #### (1)三次握手 1160 | 1161 | ![image](https://cdn.nlark.com/yuque/0/2020/png/1500604/1604023663256-5eb6dcdf-fdb6-4b67-a3da-da15c1d396fb.png) 1162 | 1163 | 三次握手(Three-way Handshake)其实就是指建立一个TCP连接时,需要客户端和服务器总共发送3个包。进行三次握手的主要作用就是为了确认双方的接收能力和发送能力是否正常、指定自己的初始化序列号为后面的可靠性传送做准备。实质上其实就是连接服务器指定端口,建立TCP连接,并同步连接双方的序列号和确认号,交换TCP窗口大小信息。 1164 | 1165 | 1166 | 1167 | 刚开始客户端处于 Closed 的状态,服务端处于 Listen 状态。 1168 | 1169 | - 第一次握手:客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号 ISN,此时客户端处于 SYN_SEND 状态。 1170 | 1171 | > 首部的同步位SYN=1,初始序号seq=x,SYN=1的报文段不能携带数据,但要消耗掉一个序号。 1172 | 1173 | - 第二次握手:服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号 ISN。同时会把客户端的 ISN + 1 作为ACK 的值,表示自己已经收到了客户端的 SYN,此时服务器处于 SYN_REVD 的状态。 1174 | 1175 | > 在确认报文段中SYN=1,ACK=1,确认号ack=x+1,初始序号seq=y 1176 | 1177 | - 第三次握手:客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 ESTABLISHED 状态。服务器收到 ACK 报文之后,也处于 ESTABLISHED 状态,此时,双方已建立起了连接。 1178 | 1179 | > 确认报文段ACK=1,确认号ack=y+1,序号seq=x+1(初始为seq=x,第二个报文段所以要+1),ACK报文段可以携带数据,不携带数据则不消耗序号。 1180 | 1181 | 1182 | 1183 | **那为什么要三次握手呢?两次不行吗?** 1184 | 1185 | - 为了确认双方的接收能力和发送能力都正常 1186 | - 如果是用两次握手,则会出现下面这种情况: 1187 | 1188 | > 如客户端发出连接请求,但因连接请求报文丢失而未收到确认,于是客户端再重传一次连接请求。后来收到了确认,建立了连接。数据传输完毕后,就释放了连接,客户端共发出了两个连接请求报文段,其中第一个丢失,第二个到达了服务端,但是第一个丢失的报文段只是在某些网络结点长时间滞留了,延误到连接释放以后的某个时间才到达服务端,此时服务端误认为客户端又发出一次新的连接请求,于是就向客户端发出确认报文段,同意建立连接,不采用三次握手,只要服务端发出确认,就建立新的连接了,此时客户端忽略服务端发来的确认,也不发送数据,则服务端一致等待客户端发送数据,浪费资源。 1189 | 1190 | 1191 | 1192 | **简单来说就是以下三步:** 1193 | 1194 | - **第一次握手:**客户端向服务端发送连接请求报文段。该报文段中包含自身的数据通讯初始序号。请求发送后,客户端便进入 SYN-SENT 状态。 1195 | - **第二次握手:**服务端收到连接请求报文段后,如果同意连接,则会发送一个应答,该应答中也会包含自身的数据通讯初始序号,发送完成后便进入 SYN-RECEIVED 状态。 1196 | - **第三次握手:**当客户端收到连接同意的应答后,还要向服务端发送一个确认报文。客户端发完这个报文段后便进入 ESTABLISHED 状态,服务端收到这个应答后也进入 ESTABLISHED 状态,此时连接建立成功。 1197 | 1198 | 1199 | 1200 | TCP 三次握手的建立连接的过程就是相互确认初始序号的过程,告诉对方,什么样序号的报文段能够被正确接收。 第三次握手的作用是客户端对服务器端的初始序号的确认。如果只使用两次握手,那么服务器就没有办法知道自己的序号是否 已被确认。同时这样也是为了防止失效的请求报文段被服务器接收,而出现错误的情况。 1201 | 1202 | #### (2)四次挥手 1203 | 1204 | ![image](https://cdn.nlark.com/yuque/0/2020/png/1500604/1604023663279-0ea063ba-a06b-4f57-9aa9-0e2d1c8d373c.png) 1205 | 1206 | 刚开始双方都处于 ESTABLISHED 状态,假如是客户端先发起关闭请求。四次挥手的过程如下: 1207 | 1208 | - 第一次挥手: 客户端会发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于 FIN_WAIT1 状态。 1209 | 1210 | > 即发出连接释放报文段(FIN=1,序号seq=u),并停止再发送数据,主动关闭TCP连接,进入FIN_WAIT1(终止等待1)状态,等待服务端的确认。 1211 | 1212 | - 第二次挥手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值 +1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT 状态。 1213 | 1214 | > 即服务端收到连接释放报文段后即发出确认报文段(ACK=1,确认号ack=u+1,序号seq=v),服务端进入CLOSE_WAIT(关闭等待)状态,此时的TCP处于半关闭状态,客户端到服务端的连接释放。客户端收到服务端的确认后,进入FIN_WAIT2(终止等待2)状态,等待服务端发出的连接释放报文段。 1215 | 1216 | - 第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 的状态。 1217 | 1218 | > 即服务端没有要向客户端发出的数据,服务端发出连接释放报文段(FIN=1,ACK=1,序号seq=w,确认号ack=u+1),服务端进入LAST_ACK(最后确认)状态,等待客户端的确认。 1219 | 1220 | - 第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值 +1 作为自己 ACK 报文的序列号值,此时客户端处于 TIME_WAIT 状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态,服务端收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED 状态。 1221 | 1222 | > 即客户端收到服务端的连接释放报文段后,对此发出确认报文段(ACK=1,seq=u+1,ack=w+1),客户端进入TIME_WAIT(时间等待)状态。此时TCP未释放掉,需要经过时间等待计时器设置的时间2MSL后,客户端才进入CLOSED状态。 1223 | 1224 | **那为什么需要四次挥手呢?** 1225 | 1226 | > 因为当服务端收到客户端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当服务端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉客户端,“你发的FIN报文我收到了”。只有等到我服务端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送,故需要四次挥手。 1227 | 1228 | 1229 | 1230 | **简单来说就是以下四步:** 1231 | 1232 | - **第一次****挥手****:**若客户端认为数据发送完成,则它需要向服务端发送连接释放请求。 1233 | - **第二次****挥手**:服务端收到连接释放请求后,会告诉应用层要释放 TCP 链接。然后会发送 ACK 包,并进入 CLOSE_WAIT 状态,此时表明客户端到服务端的连接已经释放,不再接收客户端发的数据了。但是因为 TCP 连接是双向的,所以服务端仍旧可以发送数据给客户端。 1234 | - **第三次挥手**:服务端如果此时还有没发完的数据会继续发送,完毕后会向客户端发送连接释放请求,然后服务端便进入 LAST-ACK 状态。 1235 | - **第四次****挥手****:**客户端收到释放请求后,向服务端发送确认应答,此时客户端进入 TIME-WAIT 状态。该状态会持续 2MSL(最大段生存期,指报文段在网络中生存的时间,超时会被抛弃) 时间,若该时间段内没有服务端的重发请求的话,就进入 CLOSED 状态。当服务端收到确认应答后,也便进入 CLOSED 状态。 1236 | 1237 | 1238 | 1239 | TCP 使用四次挥手的原因是因为 TCP 的连接是全双工的,所以需要双方分别释放到对方的连接,单独一方的连接释放,只代 表不能再向对方发送数据,连接处于的是半释放的状态。 1240 | 1241 | 1242 | 1243 | 最后一次挥手中,客户端会等待一段时间再关闭的原因,是为了防止发送给服务器的确认报文段丢失或者出错,从而导致服务器 端不能正常关闭。 1244 | 1245 | ### 10. **TCP**粘包是怎么回事,如何处理**?** 1246 | 1247 | 默认情况下, TCP 连接会启⽤延迟传送算法 (Nagle 算法), 在数据发送之前缓存他们. 如果短时间有多个数据发送, 会缓冲到⼀起作⼀次发送 (缓冲⼤⼩⻅ socket.bufferSize ), 这样可以减少 IO 消耗提⾼性能. 1248 | 1249 | 1250 | 1251 | 如果是传输⽂件的话, 那么根本不⽤处理粘包的问题, 来⼀个包拼⼀个包就好了。但是如果是多条消息, 或者是别的⽤途的数据那么就需要处理粘包. 1252 | 1253 | 1254 | 1255 | 下面看⼀个例⼦, 连续调⽤两次 send 分别发送两段数据 data1 和 data2, 在接收端有以下⼏种常⻅的情况: 1256 | 1257 | A. 先接收到 data1, 然后接收到 data2 . 1258 | 1259 | B. 先接收到 data1 的部分数据, 然后接收到 data1 余下的部分以及 data2 的全部. 1260 | 1261 | C. 先接收到了 data1 的全部数据和 data2 的部分数据, 然后接收到了 data2 的余下的数据. 1262 | 1263 | D. ⼀次性接收到了 data1 和 data2 的全部数据. 1264 | 1265 | 1266 | 1267 | 其中的 BCD 就是我们常⻅的粘包的情况. ⽽对于处理粘包的问题, 常⻅的解决⽅案有: 1268 | 1269 | - **多次发送之前间隔⼀个等待时间**:只需要等上⼀段时间再进⾏下⼀次 send 就好, 适⽤于交互频率特别低的场景. 缺点也很明显, 对于⽐较频繁的场景⽽⾔传输效率实在太低,不过⼏乎不⽤做什么处理. 1270 | - **关闭** **Nagle** **算法**:关闭 Nagle 算法, 在 Node.js 中你可以通过 socket.setNoDelay() ⽅法来关闭 Nagle 算法, 让每⼀次 send 都不缓冲直接发送。该⽅法⽐较适⽤于每次发送的数据都⽐较⼤ (但不是⽂件那么⼤), 并且频率不是特别⾼的场景。如果是每次发送的数据量⽐较⼩, 并且频率特别⾼的, 关闭 Nagle 纯属⾃废武功。另外, 该⽅法不适⽤于⽹络较差的情况, 因为 Nagle 算法是在服务端进⾏的包合并情况, 但是如果短时间内客户端的⽹络情况不好, 或者应⽤层由于某些原因不能及时将 TCP 的数据 recv, 就会造成多个包在客户端缓冲从⽽粘包的情况。 (如果是在稳定的机房内部通信那么这个概率是⽐较⼩可以选择忽略的) 1271 | - **进⾏封包****/拆包:**封包/拆包是⽬前业内常⻅的解决⽅案了。即给每个数据包在发送之前, 于其前/后放⼀些有特征的数据, 然后收到数据的时 候根据特征数据分割出来各个数据包。 1272 | 1273 | ### 11. 为什么**udp**不会粘包? 1274 | 1275 | - TCP协议是⾯向流的协议,UDP是⾯向消息的协议。UDP段都是⼀条消息,应⽤程序必须以消息为单位提取数据,不能⼀次提取任意字节的数据 1276 | - UDP具有保护消息边界,在每个UDP包中就有了消息头(消息来源地址,端⼝等信息),这样对于接收端来说就容易进⾏区分处理了。传输协议把数据当作⼀条独⽴的消息在⽹上传输,接收端只能接收独⽴的消息。接收端⼀次只能接收发送端发出的⼀个数据包,如果⼀次接受数据的⼤⼩⼩于发送端⼀次发送的数据⼤⼩,就会丢失⼀部分数据,即使丢失,接受端也不会分两次去接收。 1277 | 1278 | ## 七、WebSocket 1279 | 1280 | ### 1. 对 WebSocket 的理解 1281 | 1282 | WebSocket是HTML5提供的一种浏览器与服务器进行**全双工通讯**的网络技术,属于应用层协议。它基于TCP传输协议,并复用HTTP的握手通道。浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接, 并进行双向数据传输。 1283 | 1284 | 1285 | 1286 | WebSocket 的出现就解决了半双工通信的弊端。它最大的特点是:**服务器可以向客户端主动推动消息,客户端也可以主动向服务器推送消息。** 1287 | 1288 | 1289 | 1290 | **WebSocket原理**:客户端向 WebSocket 服务器通知(notify)一个带有所有接收者ID(recipients IDs)的事件(event),服务器接收后立即通知所有活跃的(active)客户端,只有ID在接收者ID序列中的客户端才会处理这个事件。 1291 | 1292 | 1293 | 1294 | **WebSocket 特点的如下:** 1295 | 1296 | - 支持双向通信,实时性更强 1297 | - 可以发送文本,也可以发送二进制数据‘’ 1298 | - 建立在TCP协议之上,服务端的实现比较容易 1299 | - 数据格式比较轻量,性能开销小,通信高效 1300 | - 没有同源限制,客户端可以与任意服务器通信 1301 | - 协议标识符是ws(如果加密,则为wss),服务器网址就是 URL 1302 | - 与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。 1303 | 1304 | 1305 | 1306 | **Websocket的使用方法如下:** 1307 | 1308 | 1309 | 1310 | 在客户端中: 1311 | 1312 | ``` 1313 | // 在index.html中直接写WebSocket,设置服务端的端口号为 9999 1314 | let ws = new WebSocket('ws://localhost:9999'); 1315 | // 在客户端与服务端建立连接后触发 1316 | ws.onopen = function() { 1317 | console.log("Connection open."); 1318 | ws.send('hello'); 1319 | }; 1320 | // 在服务端给客户端发来消息的时候触发 1321 | ws.onmessage = function(res) { 1322 | console.log(res); // 打印的是MessageEvent对象 1323 | console.log(res.data); // 打印的是收到的消息 1324 | }; 1325 | // 在客户端与服务端建立关闭后触发 1326 | ws.onclose = function(evt) { 1327 | console.log("Connection closed."); 1328 | }; 1329 | ``` 1330 | 1331 | ### 2. 即时通讯的实现:短轮询、长轮询、SSE 和 WebSocket 间的区别? 1332 | 1333 | 短轮询和长轮询的目的都是用于实现客户端和服务器端的一个即时通讯。 1334 | 1335 | 1336 | 1337 | **短轮询的基本思路:**浏览器每隔一段时间向浏览器发送 http 请求,服务器端在收到请求后,不论是否有数据更新,都直接进行响应。这种方式实现的即时通信,本质上还是浏览器发送请求,服务器接受请求的一个过程,通过让客户端不断的进行请求,使得客户端能够模拟实时地收到服务器端的数据的变化。这种方式的优点是比较简单,易于理解。缺点是这种方式由于需要不断的建立 http 连接,严重浪费了服务器端和客户端的资源。当用户增加时,服务器端的压力就会变大,这是很不合理的。 1338 | 1339 | 1340 | 1341 | **长轮询的基本思路:**首先由客户端向服务器发起请求,当服务器收到客户端发来的请求后,服务器端不会直接进行响应,而是先将这个请求挂起,然后判断服务器端数据是否有更新。如果有更新,则进行响应,如果一直没有数据,则到达一定的时间限制才返回。客户端 JavaScript 响应处理函数会在处理完服务器返回的信息后,再次发出请求,重新建立连接。长轮询和短轮询比起来,它的优点是明显减少了很多不必要的 http 请求次数,相比之下节约了资源。长轮询的缺点在于,连接挂起也会导致资源的浪费。 1342 | 1343 | 1344 | 1345 | **SSE 的基本思想:**服务器使用流信息向服务器推送信息。严格地说,http 协议无法做到服务器主动推送信息。但是,有一种变通方法,就是服务器向客户端声明,接下来要发送的是流信息。也就是说,发送的不是一次性的数据包,而是一个数据流,会连续不断地发送过来。这时,客户端不会关闭连接,会一直等着服务器发过来的新的数据流,视频播放就是这样的例子。SSE 就是利用这种机制,使用流信息向浏览器推送信息。它基于 http 协议,目前除了 IE/Edge,其他浏览器都支持。它相对于前面两种方式来说,不需要建立过多的 http 请求,相比之下节约了资源。 1346 | 1347 | 1348 | 1349 | **WebSocket** 是 HTML5 定义的一个新协议议,与传统的 http 协议不同,该协议允许由服务器主动的向客户端推送信息。使用 WebSocket 协议的缺点是在服务器端的配置比较复杂。WebSocket 是一个全双工的协议,也就是通信双方是平等的,可以相互发送消息,而 SSE 的方式是单向通信的,只能由服务器端向客户端推送信息,如果客户端需要发送信息就是属于下一个 http 请求了。 1350 | 1351 | 1352 | 1353 | **上面的四个通信协议,前三个都是基于HTTP协议的。** 1354 | 1355 | 1356 | 1357 | 对于这四种即使通信协议,从性能的角度来看: 1358 | 1359 | **WebSocket > 长连接(SEE) > 长轮询 > 短轮询** 1360 | 1361 | 但是,我们如果考虑浏览器的兼容性问题,顺序就恰恰相反了: 1362 | 1363 | **短轮询 > 长轮询 > 长连接(SEE) > WebSocket** 1364 | 1365 | 所以,还是要根据具体的使用场景来判断使用哪种方式。 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # front-end-interview 2 | - 最强前端面试资源 3 | - 前端实战资料获取 微信:kjd1000000 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /erweima/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kjd1000000/front-end-interview/a166c7d0119588ff4f3dd89ea5877adedc683f7e/erweima/.DS_Store -------------------------------------------------------------------------------- /erweima/gong.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kjd1000000/front-end-interview/a166c7d0119588ff4f3dd89ea5877adedc683f7e/erweima/gong.jpg -------------------------------------------------------------------------------- /erweima/we.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kjd1000000/front-end-interview/a166c7d0119588ff4f3dd89ea5877adedc683f7e/erweima/we.jpg --------------------------------------------------------------------------------