前端面试八股文是指前端面试过程中经常被问到的一系列问题,基本也是面试必问的基础问题,被大家戏称为前端八股文,在国内互联网公司前端面试里,考八股文和手写代码是逃不了的两部分,据我今年面试情况基本上一面问八股文的概率已经超过 80 %。
这里汇总了最近面试被问到频率较高的一些基础题及我的简要回答,主要包括 HTML 、JavaScript、前端框架三部分内容,如有错误或遗漏欢迎反馈斧正,后续有新问题会持续更新,希望大家在 2023 年都能找到合适满意的工作。
更新:补充了面试新文章 -> 前端面试系列 - 手写代码
HTML 语义化就是让页面的内容结构化,便于对浏览器、搜索引擎解析;在没有样式 CCS 情况下也以一种文档格式显示,并且是容易阅读的。搜索引擎的爬虫依赖于标记来确定上下文和各个关键字的权重,利于 SEO。
前端加载 html,html 解析器运行于主线程中,如果遇到<script> 标签后会阻塞,直到脚本从网络中下载并被执行,也就是说<script>标签的脚本会阻塞浏览器的渲染。这里还涉及到页面生命周期:
当浏览器处理一个 HTML 文档,并在文档中遇到 <script> 标签时,就会在继续构建 DOM 之前运行它。这是一种防范措施,因为脚本可能想要修改 DOM,甚至对其执行 document.write 操作,所以 DOMContentLoaded 必须等待脚本执行结束。如何解决这个问题,可以使用 script 标签的两个属性,defer 和 async。
cookie 的属性:
要讲明白这两个概念,需要先说一下浏览器的页面生成过程,当我们通过 url 链接访问一个页面时,在加载完 html、css 、script资源后会有一个解析布局绘制页面的过程,对应的三个操作解析、布局、绘制,网页生成的时候,至少需要排列及绘制一次,随着用户的操作后续有可能而后面会触发重排和重绘。
过程:
重排(reflow):当 dom 的变化影响了元素的几何信息(例如它的位置,尺寸等),这个时候浏览器会重新计算它的属性值,并且把它放在正确位置上,这个叫重排(重新生成布局,重新排列)
常见的触发属性或方法:width,height、font-size、display、scrollTo等
重绘(repaints):当一个元素的外观发生了变化,但没有改变布局,浏览器会把元素外观重新绘制出来。常见的触发属性: color,border-style、background-position 等
重绘不一定会导致重排,但是重排一定会导致重绘。
一些方法减少不必要的重排:
js
1div.style.left = div.offsetLeft + 1 + 'px'; 2 3div.style.top = div.offsetTop + 1 + 'px'; 4 5// 上述会进行两次重排,而通过变量存储后,讲进行一次重排 6 7var curLeft = div.offsetLeft; 8 9var curTop = div.offsetTop; 10 11div.style.left = curLeft + 1 + 'px'; 12 13div.style.top = curTop + 1 + 'px'; 14 15这也相当于是分离读写操作了优化为1次重排
每一个元素在浏览器中都可以理解成一个盒子,它包含对应的四个属性值:宽高、boder 边框、padding 内边距、外边距 margin
分为了 W3C 盒子模型(标准盒模型)和 IE 盒子模型(怪异盒模型)
标准盒子模型的内容空间的宽度是由 width 属性设置的,例如宽度 = width + border + 内边距的值,也就是说 width 只是里面的内容的宽度,实际大小会加上 border + padding的值。
它的实际区域内容宽高值分别是:
宽:width + padding + border
高:height + padding + border
但在ie也就是怪异盒子模型里,会包含 border 和 padding 的值,也就是说,如果你将一个元素的 width 设为 100px,那么这 100px 会包含它的 border 和 padding,内容区的实际宽度是 width 减 去(border + padding) 的值。
它的实际区域内容宽高值分别是:
三个参数分别对应的是 flex-grow, flex-shrink 和 flex-basis,默认值为0 1 auto。
BFC(Block Formatting Context),即块级格式化上下文,它是页面中的一块渲染区域,容器页面里的子元素不会影响到外部的元素,并且有一套属于自己的渲染规则:
触发条件:
(创建BFC的方式:根元素、浮动元素和绝对定位元素,非块级盒子的块级容器,overflow 值不为 visiable 的块级盒子)
应用场景:
通过控制边宽及 transparent 来实现
css
1#demo{ 2 width:100px; 3 height:100px; 4 border:3px red solid; 5} 6 7#demo{ 8 width:100px; 9 height:100px; 10 border: 20px solid; 11 border-color: red blue red blue; 12}
css
1#demo{ 2 width:0px; 3 height:0px; 4 border: 40px solid; 5 border-color: red blue red blue; 6}
css
1#demo{ 2 width:0px; 3 height:0px; 4 border:40px solid transparent; 5 border-bottom:80px solid red; 6}
块级元素,顾名思义,该元素呈现“块”状,所以它有自己的宽度和高度,块级元素比较霸道,它独自占据一行高度(float浮动除外),一般可以作为其他容器使用,可容纳块级元素和行内元素。
行内元素不可以设置宽(width)和高(height),但可以与其他行内元素位于同一行,行内元素内一般不可以包含块级元素。行内元素的高度一般由元素内部的字体大小决定,宽度由内容的长度控制。
input、img就是行内块级元素,
css
1.con1_l{ 2 float:left; 3} 4.con1_r{ 5 overflow: hidden; 6}
css
1.con2_l{ 2 float:left; 3} 4.con2_r{ 5 margin-left:200px; 6}
css
1/* 5 calc */ 2.con5_l{ 3 float:left; 4} 5.con5_r{ 6 width:calc(100% - 200px); 7 float:left; 8} 9
内存空间分为栈 Stack 和 堆 heap 两种,其中栈存放变量及基础类型,堆存放复杂对象、也叫引用数据类型,栈自动分配相对固定大小的内存空间。。
在 JS 里内存是由系统自动分配管理的,内存管理机制是内存基元会在变量创建时分配,然后在它们不再被使用时“自动”释放,后面这个过程也叫垃圾回收机制,
而栈就像管道里放箱子,后放的在上边,所以后进先出。
队列是先进先出:就像一条路,有一个入口和一个出口,先进去的就可以先出去。
栈只能从头部取数据,也就最先放入的需要遍历整个栈 最后才能取出来,而且在遍历数据的时候还得为数据开辟临时空间,队列则不同,它基于地址指针进行遍历,而且可以从头或尾部开始遍历,速度快很多
IIFE(立即调用函数表达式)是一个在定义时就会立即执行的 JavaScript 函数。好处:表达式中的变量不能从外部访问。
js
1(function () { 2 statements 3})();
use strict 现在用得不是特别多,其实根据字面意思也能知道它其实是要去我们用严格的行为模式来写代码执行代码。"use strict" 是在ECMAScript5 中新增的一个声明,但它不是一条语句,是一个字面量表达式。可以在脚本或函数的头部添加 use strict 来声明。
例子:
作用:
length是函数对象的一个属性值,指该函数期望传入的参数数量,即形参的个数console.log(((...args) => {}).length);原型及原型链主要涉及到这三个名词 proto、prototype、 constructor:
每个 js 复杂数据类型(Object Function Array)等都会自带一个 prototype 对象,这个对象就是我们说的原型。
proto 访问器属性,它指向原型对象,所以不管你是 Function 还是 Object 都会有 proto 属性,这些最终都指向了 Object.protoype 原型对象,它也是对象,它也有 proto ,它的原型对象指向了 null。
在 JavaScript 中原型是一个 prototype 对象,用于表示类型之间的关系。
原型对象的用途是为每个实例对象存储共享的方法和属性,它仅仅是一个普通对象而已。并且所有的实例是共享同一个原型对象,因此有别于实例方法或属性,原型对象仅有一份。
JavaScript 万物都是对象,对象和对象之间也有关系,并不是孤立存在的。对象之间的继承关系,在 JavaScript 中是通过 prototype 对象指向父类对象,直到指向 Object 对象为止,这样就形成了一个原型指向的链条,专业术语称之为原型链。
js
1function Fn() {} // Fn为构造函数 2 3var f1 = new Fn(); //f1是Fn构造函数创建出来的对象
构造函数的 prototype 属性值就是对象原型。(Fn.prototype就是对象的原型)
构造函数的 prototype 属性值的类型就是对象 typeof Fn.prototype===object.
对象原型中的 constructor 属性指向构造函数 (Fn.prototype.constructor===Fn)
对象的 proto 属性值就是对象的原型。(f1.__proto__就是对象原型)
常见的面试题,实现一个 curry add 函数,将 add(a + b) 转换为 add(a)(b),既然需要返回继续可执行的函数,第一个想到的是闭包,把当前函数当结果返回就能继续执行了
js
1function add(a, b) { 2 return a + b; 3} 4 5// curry 化 6function curryAdd(a) { 7 return function(b) { 8 return a + b 9 }; 10}; 11 12// 如果用箭头函数实现 13const curryAdd = a => b => a + b 14add(1,2) 15curryAdd(1)(2) // 3 16 17const curry = function (fn, ...a) { 18 // 实参数量大于等于形参数量吗? 19 return a.length >= fn.length ? 20 // 如果大于返回执行结果 21 fn(...a) : 22 // 反之继续柯里化,递归,并将上一次的参数以及下次的参数继续传递下去 23 (...b) => curry(fn, ...a, ...b); 24}; 25const add = (a, b, c) => a + b + c; 26// 将add加工成柯里化函数 27const addCurry = curry(add); 28console.log(addCurry(1, 2, 3));// 6 29console.log(addCurry(1)(2)(3));// 6 30console.log(addCurry(1, 2)(3));// 6 31console.log(addCurry(1)(2, 3));// 6
js
1function Parent () { 2 this.name = 'kevin'; 3} 4 5Parent.prototype.getName = function () { 6 console.log(this.name); 7} 8 9function Child () { 10 11} 12 13Child.prototype = new Parent(); 14 15var child1 = new Child(); 16 17console.log(child1.getName())
js
1Object.defineProperty(Object.prototype, 2 "extend", 3 { 4 writable: true, 5 enumerable: false, 6 configurable: true, 7 value: function(o){ 8 // 获取所有的自有属性 9 var names = Object.getOwnPropertyNames(o); 10 for(var i =0;i< names.length;i++){ 11 // 如果属性已存在则跳过 12 if(names[i] in this) continue; 13 // 获取该属性的属性描述符 14 var desc = Object.getOwnPropertyDescriptor(o,names[i]); 15 Object.defineProperty(this,names[i],desc) 16 } 17 } 18 } 19) 20 21var a = { item: 1} 22Object.defineProperty(a,"item2",{ 23 writable: true, 24 enumerable: false, 25 configurable: true, 26 value: 2 27}) 28var b = {} 29b.extend(a)
常见的几种数据类型,按存储类型来分的话有 6 种:
基础类型
引用类型:
Set 对象是值的集合,Set 中的元素只会出现一次,即 Set 中的元素是唯一的。
jsx
1let mySet = new Set(); 2mySet.add(1) // Set [ 1 ] 3mySet.has(1); // true 4mySet.size // 1
与 Object 的区别:
toString() 方法转换为字符串的结果。[object, type] type 其实拿到的是 Symbol.toStringTag,是一个内置 symbol,它通常作为对象的属性键使用。其实 toString 是对象上的方法,每一个对象上都有这个方法,那就意味着数字、字符串和布尔值这些基本数据类型不能使用 toString()方法,但上例中的基本数据类型却是可以使用,这要归功于javascript中的包装类,即 Number、String 和 Boolean。原始值不能有属性和方法,当要使用 toString() 方法时,会先将原始值包装成对象再使用。
所有类都继承自 Object,按理来说输出结果应该都类似于 [object Object] 这样。不一样的原因是所有类在继承 Object的时候,改写了 toString() 方法。所以当我们想要判断数据类型时,必须使用 Object 上的**toString()**方法。
这里涉及到作用域、变量提升的问题,作用域本质上是指变量/函数可供访问的范围。
当在最外层函数的外部声明 var 变量时,作用域是全局的。这意味着在最外层函数的外部用 var 声明的任何变量都可以访问到。但当你在函数范围内申明变量时,外部的代码是无法访问到这个变量的。
var 的一个问题是变量被修改覆盖,举个简单的例子:
js
1var greeter = 'hello' 2if (true) { 3 var greeter = 'world' 4} 5console.log(greeter) // 这里会打印 world 替换成 let 后能解决这个问题 6
let 是块级作用域,块是由 {} 界定的代码块,大括号中有一个块。大括号内的任何内容都包含在一个块级作用域中,因为变量仅在其块级作用域内存在。
区别:
var声明的变量会被提升到其作用域的顶部,并使用 undefined 值对其进行初始化。let声明的变量有暂时性死区,没有变量提升,不会对值进行初始化。访问的时候是直接 Reference Error两个都是优化高频重复操作代码的一个方式,例如我们在使用浏览器的 resize scroll 等事件时,当用户短时间内触发这些事件时,绑定的回调函数会不断地被调用,就有两种优化策略 throttle 节流 和 debounce 防抖
用发车的一个例子来描述这两种情况,如果每个乘客上车发一次车比较耗时:
js
1// throttle 节流 2const throttle = (fn, delay) => { 3 let timer = null; 4 return function (... args) { 5 if (!timer) { 6 timer = setTimeout(() => { 7 fn.apply(this, args); 8 timer = null; 9 }, delay) 10 } 11 } 12}; 13 14// debounce 防抖 15function debounce(fn, delay) { 16 let timer; 17 return function() { 18 let args = arguments; 19 if (timer) clearTimeout(timer); 20 timer = setTimeout(() => { 21 fn.apply(this, args); 22 }, delay) 23 } 24}
window.requestIdleCallback() 方法使用插入一个函数,这个函数将在浏览器空闲时期被调用。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。函数一般会按先进先调用的顺序执行,然而,如果回调函数指定了执行超时时间 timeout,则有可能为了在超时前执行函数而打乱执行顺序。
浅拷贝:拷贝了赋值的引用地址,修改值会影响到原始值,拷贝的是对象的引用地址。指向的还是同一片空间。
深拷贝:拷贝所有的属性,地址也与原先的不同,修改不会影响到原始值
实现一个简单的深拷贝:
js
1function deepclone (obj) { 2 let cloneObj = [].isArray(obj) ? [] : {}; 3 if (obj && typeof obj === 'object') { 4 for (key in obj) { 5 if (obj.hasOwnProperty(key)) { 6 if (obj[key] && typeof obj[key] === 'object') { 7 cloneObj[key] = deepclone(obj[key]) 8 } else { 9 cloneObj[key] = obj[key] 10 } 11 } 12 } 13 } 14 return cloneObj; 15}
使用 new 来新建一个构造函数或类得到对应实例,是非常普遍的操作了,ES5 中使用 function 构造函数,到 ES6 使用 class 都可以使用 new 来新建实例。
js
1//定义的new方法 2let newMethod = function (Parent, ...rest) { 3 // 1.以构造器的 prototype 属性为原型,创建新对象; 4 let child = Object.create(Parent.prototype); 5 // 2.将 this 和调用参数传给构造器执行 6 let result = Parent.apply(child, rest); 7 // 3.如果构造器没有手动返回对象,则返回第一步的对象 8 return typeof result === 'object' ? result : child; 9};
分严格模式和非严格模式及在指定的场景:
js 的作用域分为:全局作用域、块级作用域、函数作用域。
全局作用域是指函数或者 {} 外面的执行环境,叫全局作用域,根据 js 执行环境不同,全局作用域不同。
在函数内部定义的变量,就是局部作用域。在函数作用域内,对外是封闭的,从外层作用域无法直接访问内部作用域,但是函数内部作用域可以访问外部的作用域。
作用域是分层的,子作用域可以访问父作用域,不能从父作用域引用子作用域中的变量;如果一个变量或者其他表达式不在当前的作用域,那么js 机制会继续沿着作用域链向上查找直到全局作用域( Node中 的 global 或浏览器中的 window),如果找不到则表示变量不可用
变量提升:
异步任务队列又分为微任务(micro task)队列和宏任务(macro task)队列;事件循环的过程中,执行栈在同步代码执行完成后。
常见的宏任务:
微任务:
为什么会存在这样的两个方法,主要是 js 存在定义时上下文、执行上下文以及上下文 context 可以改变的,换句话说其实上为了改变 this 的指向。
当一个对象没有某个方法的时候,但是其它对象有,我们就可以借助 call 或 apply 来实现用它对象上方法的调用。
js
1function Friut {} 2 3Friut.prototype = { 4 color: 'red', 5 say: function () { 6 console.log('color is:' + this.color) 7 } 8} 9 10var apple = new Friut; 11apple.say(); 12 13// 重新定义一个水果对象 banana,它没有say方法,可以使用apple的的say 14 15var banana = { 16 color: 'yellow' 17} 18apple.say.call(banana) // color is: yellow
bind,call 和 apply 的区别:
手动实现 call apply bind 参考链接
js
1// call 2Function.prototype.myCall = function(context) { 3 if(typeof this !== 'function') { 4 console.log('type error') 5 } 6 // 获取参数 7 let args = [...arguments].slice(1); 8 result = null 9 context = context || window 10 context.fn = this 11 // 调用函数 12 result = context.fn(...args); 13 // 删除属性 14 delete context.fn 15 return result 16} 17 18// apply 19Function.prototype.myApply = function (context) { 20 if(typeof this !== 'function') { 21 throw new TypeError('error') 22 } 23 let result = null 24 context = context || window 25 context.fn = this 26 if (arguments[1]) { 27 result = context.fn([...arguments[1]]) // 调用赋值的函数 28 } 29 delete context.fn 30 return result 31} 32 33// bind 34Function.prototype.myBind = function(context) { 35 if (typeof this !== 'function') { 36 throw new TypeError('error') 37 } 38 // 获取参数 39 let args = [...arguments].slice(1) 40 context.fn = this 41 return function Fn() { 42 return fn.apply(this instanceof Fn ? this : context, args.concat(...arguments)) 43 } 44}
一句话解释闭包:闭包允许函数访问并操作函数外部的变量。要理解闭包,先理解 js 的变量作用域。
js 的变量作用域有两种,一种是全局变量,另一种是局部变量,函数内部可以读取到外部的全局变量,但函数外部无法读取内部的局部变量。那有没有什么办法可以访问到这个变量呢,有的,使用闭包。
js
1let sum = (num1, num2) => num1 + num2; 2// 等同于 3let sum = function(num1, num2) { 4 return num1 + num2; 5};
箭头函数是 es6 新语法,跟传统普通函数比有以下一些优势:
js
1var id = 'id1'; 2var obj = { 3 id: 'id2', 4 a: function(){ 5 console.log(this.id); 6 }, 7 b: () => { 8 console.log(this.id); 9 } 10}; 11 12obj.a(); // 'id2' 13obj.b(); // 'id1'
JS 从诞生起就是一门单线程的非阻塞脚本语言
单线程意味着不管执行什么任务,当前都只有一个主线程在执行任务
但是要做到非阻塞的话,意味着例如在处理异步请求的时候不能卡在当前代码,代码继续往下执行,这时会 pending 这个异步任务,当它返回结果后再去执行注册的相应回调。
一般是在 栈里来执行方法,而堆用来存放一些较大的数据,像对象等。javascript 执行的时候会存放在内存的不同地方,使用栈 stack 和 堆 heap 来加于区分。
一个方法执行会向执行栈中加入这个方法的执行环境,在这个环境中也可以调用自己,当这个执行环境中的代码 执行完毕并返回结果后,js会退出这个执行环境并把这个执行环境销。
上面是同步代码,异步代码有一点不一样,因为异步代码不会立即返回结果,而是将这个事件挂起,这个时候就需要用到这个事件队列的机制,异步返回结果后,js 会把这个事件存放在与当前执行栈不一样的一个队列中,我们称之为事件队列,但被放入到队列的事件并不会立刻执行这个回调。而是等待当前执行栈所有任务执行完,主线程处于闲置状态时,再去查找队列里是否还有任务没有执行,有的话再把它拿出来放在执行栈中去执行,如此反复就形成了事件循环。
为什么 js 是单线程?一开始是主要在浏览器端来使用 js,需要保证程序执行的一致性。
worker 线程的使用有一些注意点:
场景:
Mutation Observer API 用来监视 DOM 变动。DOM 的任何变动,比如节点的增减、属性的变动、文本内容的变动,这个 API 都可以得到通知。
可以理解为 DOM 发生变动就会触发 Mutation Observer 事件。但是,它与事件有一个本质不同:事件是同步触发,也就是说,DOM 的变动立刻会触发相应的事件;Mutation Observer 则是异步触发,DOM 的变动并不会马上触发,而是要等到当前所有 DOM 操作都结束才触发。
通过传入一个函数来创建一个 MutationObserver 实例,每当有变化发生,这个函数将会被调用。函数的第一个参数是变动数组,每个变化都会提供它的类型和已经发生的变化的信息
这个被创建的对象有三个方法:
在前端领域中,跨域是指浏览器允许向服务器发送跨域请求,为什么有这个跨域,主要是同源策略。同源策略是浏览器的一个核心的安全策略,主要指"协议+域名+端口”三种需要相同,不同的话会被限制发请求、cookie 、localstorage 、dom等也无法读取
常见的跨域方式
浏览器将CORS跨域请求分为简单请求和非简单请求。
简单请求里会新增加一个 Origin 字段。需要传递你接口的源的信息,后端判断是否同意这次请求,对应的相应头里也会返回 Access-Control 开头的字段,Access-Control-Allow-Origin。Allow-Credentials,默认是不带 cookie,如果需要带设置成 true
不是简单请求,会多发一个预请求,预检"请求用的请求方法是 OPTIONS,表示这个请求是用来询问的。请求头信息里面,关键字段是 Origin,表示请求来自哪个源。除了 Origin 字段,预检请求的头信息包括两个特殊字段:
两种实现,例如当中一个 div 元素当中有一个 p 子元素,如果两个元素都有一个 click 的处理函数,那么我们怎么才能知道哪一个函数会首先被触发呢?微软提出了名为事件冒泡 (event bubbling) 的事件流。事件冒泡可以形象地比喻为把一颗石头投入水中,泡泡会一直从水底冒出水面。也就是说,事件会从最内层的元素开始发生,一直向上传播,直到 document 对象。
我们知道如果需要执行代码,操作系统的运行时就需要开辟内存空间来运行,当运行完及时释放内存,如果有不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)。
轻则影响程序的运行速率,重则容易造成程序崩溃。
说一下场景的容易引起内存泄露的场景
js
1var fn = (function(){ 2 var a = '***';// 被闭包所引用,不会被回收 3 return function(){ 4 console.log(a); 5 } 6})()
Web 端和 Native 可以类比于 Client/Server 模式,Web 端调用原生接口时就如同 Client 向 Server 端发送一个请求类似,JSB 在此充当类似于 HTTP 协议的角色,将 Native 与 JavaScript 的每次互相调用看做一次 RPC 调用。前端相当于 rpc 里的客户端,Native 相当于 rpc 里的服务端
实现 JSBridge 主要有两种:
主要原因是转换成了二进制进行运算
js
1/** 2 * 精确加法 3 */ 4function add(num1, num2) { 5 const num1Digits = (num1.toString().split('.')[1] || '').length; 6 const num2Digits = (num2.toString().split('.')[1] || '').length; 7 const baseNum = Math.pow(10, Math.max(num1Digits, num2Digits)); 8 return (num1 * baseNum + num2 * baseNum) / baseNum; 9}
JavaScript 垃圾回收机制的原理说白了也就是定期找出那些不再用到的内存(变量),然后释放其内存。这里主要讲一下 v8 的 gc 回收算法,主要是两种:
目前在 JavaScript引擎 里这种算法是最常用的,到目前为止的大多数浏览器的 JavaScript 引擎都在采用标记清除算法。此算法分为标记和清除两个阶段,这使得一位二进制位(0和1)就可以为其标记,非常简单。
标记清除算法有一个很大的缺点,就是在清除之后,剩余的对象内存位置是不变的,也会导致空闲内存空间是不连续的,出现了内存碎片并且由于剩余空闲内存不是一整块,它是由不同大小内存组成的内存列表,这牵扯出了内存分配的问题
这其实是早先的一种垃圾回收算法,它把对象是否不再需要简化定义为对象有没有其他对象引用它。例如定义一个变量赋值时1,当它被另一个变量覆盖时,-1,当这个值的引用次数变为 0 的时候,说明没有变量在使用,清理掉内存
js
1<script> 2 // your code 3</script>
js
1<script src="./a.js"> 2<script src="./b.js">
js
1<script src="./a.js"> 2window.moduleA = { 3 data: 'a', 4 funA: () => {} 5}
js
1(function () { 2 let data = ‘a’ 3})()
前端路由的核心:
hash:
在 url 中的 # 之后对应的是 hash 值, 其原理是通过 hashChange() 事件监听hash值的变化, 根据路由表对应的 hash 值来判断加载对应的路由加载对应的组件。
前端配置路由表, 不需要后端的参与,因为 hash 值变化不会向后端发请求,所以属于前端路由,兼容性好, 浏览器都能支持。
history 是 url 地址规范, 不需要#,包括 back,forward,go 三个方法,这种模式充分利用 history.pushState replaceStateAPI 来完成 URL 跳转而无须重新加载页面。
区别:hash 的传参是基于 url 的,如果要传递复杂的数据,会有体积的限制
2、nodejs 所谓的单线程,只是主线程是单线程,所有的网络请求或者异步任务都交给了内部的线程池去实现,本身只负责不断的往返调度,由事件循环不断驱动事件执行。
3、Nodejs 之所以单线程可以处理高并发的原因,得益于 libuv 层的事件循环机制,和底层线程池实现。
型是静态类型语言的基本特征,允许将类型作为参数传递给另一个类型、函数、或者其他结构。TypeScript 支持泛型作为将类型安全引入组件的一种方式。这些组件接受参数和返回值,其类型将是不确定的,直到它在代码中被使用。
泛型的语法为
例如当我们想实现一个传入什么类型就得到什么类型的函数时,就可以用到泛型,有没有一种方法在调用的时候再指定类型呢。
将泛型与函数一起使用的最常见场景之一就是,当有一些不容易为所有的用例定义类型时,为了使该函数适用于更多情况,就可以使用泛型来定义
js
1function identity<T>(value: T): T { 2 return value; 3}
https://nodelover.gitbook.io/typescript/fan-xing#le-jie-object.create
二者都是可以赋值给任意类型的, any 会绕过类型检查,直接可用,而 unkonwn 则必须要在判断完它是什么类型之后才能继续用
常用的一些关键字
Exclude<T, U> 从Y中排出可以分配给U的元素.
Omit<T, K> 忽略T中的某些属性.
Merge<O1, O2> 将两个对象的属性合并.
Compute<A & B> 将交叉类型合并.
Intersection<T, U> 取T的属性,此属性同样哦存在于U.
Overwrite<T, U> 用U的属性覆盖T的相同属性.
Covariant 协变,ts 对象兼容性是协变,父类<=子类是可以的,子类 <= 父类错误。 Contravariant 逆变,禁用 strictFunctionTypes 编译,函数参数类型都是逆变的,父类 <= 子类,是错误。子类 <= 父类,是可以的。 Bivariant 双向协变,函数参数的类型默认是双向协变的。父类 <= 子类,是可以的。子类 <= 父类,是可以的。
相同点:
不同点:
implements 一般是实现接口。extends 是继承类。
共同点:
不同点:
implements 实现,一个新的类,从父类或者接口实现所有的属性和方法,同时可以重写属性和方法,包含一些新的功能 extends 继承,一个新的接口或者类,从父类或者接口继承所有的属性和方法,不可以重写属性,但可以重写方法
js
1type Types = number | string; 2// 重载签名 3function add(x: number, y: number): number; 4function add(x: string, y: string): string; 5function add(x: string, y: number): string; 6function add(x: number, y: string): string; 7function add(x: Types, y: Types) { 8 if (typeof x === 'string' || typeof y === 'string') { 9 return x.toString() + y.toString(); 10 } 11 return x + y; 12} 13const result = add('hearts', ' spades'); 14result.split(' ');
常量枚举只能使用常量枚举表达式并且不同于常规的枚举,他们在编译阶段会被删除。常量枚举成员在使用的地方会被内联起来,之所以真可以这么做是因为,常量枚举不允许包含计算成员。
https 多了一个sercurty 安全,唯一的区别就是使用了 TSL(ssl) 来加密普通的http 请求和响应,并且对请求和响应进行数字前面,所以更安全。
OSI是一个开放性的通信系统互连参考模型
应用层:为程序提供网络服务,例如 HTTP,FTP,SMTP
表示层:这一层主要是数据格式化和加密解密
会话层:建立维护管理会话连接(在tcp/ip协议组合并一个应用层)
传输层: 建立管理端到端的连接 TCP、UDP
网络层:IP 寻址和路由选择
数据链路:控制网络层和物理层通信的
物理层:比特流传输 例如光缆和电缆等设备组网
TCP 是一种面向连接的单播协议,在发送数据前,通信双方必须在彼此间建立一条连接。所谓的“连接”,其实是客户端和服务器的内存里保存的一份关于对方的信息,如ip地址、端口号等。
这是由于 TCP 的半关闭(half-close)特性造成的,TCP 提供了连接的一端在结束它的发送后还能接收来自另一端数据的能力。
第一次挥手:客户端发送一个 FIN 报文(请求连接终止:FIN = 1),报文中会指定一个序列号 seq = u。并停止再发送数据,主动关闭 TCP 连接。
第二次挥手 服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序号值 +1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT 状态。
**第三次挥手 :**如果服务端也想断开连接了(没有要向客户端发出的数据),和客户端的第一次挥手一样,发送 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 的状态,等待客户端的确认
第四次挥手客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答(ack = w+1),且把服务端的序列值 +1 作为自己 ACK 报文的序号值
通俗的来说,两次握手就可以释放一端到另一端的 TCP 连接,完全释放连接一共需要四次握手
资源的优先级被分为 5 级: Highest 、 Medium 、 Low 、 Lowest 、 Idle ;
浏览器首先会按照资源默认的优先级确定加载顺序:
是一种网络传输协议,位于 OSI 模型的应用层。可在单个 TCP 连接上进行全双工通信,能更好的节省服务器资源和带宽并达到实时通迅,客户端和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。使用场景如弹幕、协同编辑等
xss:跨域脚本攻击,原来就叫 CSS,主要是把恶意的脚本代码HTMLJS代码植入到网页中,当用户浏览时执行代码,可能获取到cookie、会话劫持、钓鱼网站等。
主要分两种:反射型,将恶意脚本附加到 url 参数中,这种危害性较小
持久性:恶意代码上传或存储至漏洞服务器中,用户浏览时就执行,例如获取cookie信息
防范:
CSRF:跨站请求伪造。原理或本质就是让用户在已登录的站点上执行非本意的操作,
攻击者首先盗用了你的身份,然后以你的名义进行某些非法操作,web 中用户身份认证验证的一个漏洞:简单的身份验证仅仅能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。
两个必要条件:
常见的例如:img src 属性能够以 get 方式发送请求第三方资源,浏览器会带上你的 cookie
响应式更新流程图


created 方法
mounted 方法
virtual DOM 是一种编程理念(数据驱动视图),将 ui 虚拟 dom 保持到内存中,并且通过某些库渲染成真实的 dom,这个过程又叫做协调。
包括:
实际上它只是一层对真实 DOM 的抽象,以 JavaScript 对象 (VNode 节点) 作为基础的树,用对象的属性来描述节点,最终可以通过一系列操作使这棵树映射到真实环境上
在 Javascript 对象中,虚拟 DOM 表现为一个 Object 对象。并且最少包含标签名 (tag)、属性 (attrs) 和子元素对象 (children) 三个属性,不同框架对这三个属性的名命可能会有差别
react 函数式组件思想 当你 setstate 就会遍历 diff 当前组件所有的子节点子组件, 这种方式开销是很大的, 所以 react 16 采用了 fiber 链表代替之前的树,可以中断的,分片的在浏览器空闲时候执行
vue 组件响应式思想 采用代理监听数据,我在某个组件里修改数据,就会明确知道那个组件产生了变化,只用 diff 这个组件就可以了
在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM,所以放在 Vue.nextTick() 回调函数中的执行。nextTick 本质是一种优化策略
我们可以理解成,Vue 在更新 DOM 时是异步执行的。当数据发生变化,Vue 将开启一个异步更新队列,视图需要等队列中所有数据变化完成之后,再统一进行更新。
Vue 是异步执行 dom 更新的,一旦观察到数据变化,Vue 就会开启一个队列,然后把在同一个事件循环 (event loop) 当中观察到数据变化的 watcher 推送进这个队列。如果这个 watcher 被触发多次,只会被推送到队列一次。这种缓冲行为可以有效的去掉重复数据造成的不必要的计算和 DOm 操作。
控制手段不同编译过程不同编译条件不同:
控制手段:v-show 隐藏则是为该元素添加 css display:none,dom 元素依旧还在。v-if 显示隐藏是将 dom 元素整个添加或删除
编译过程:切换过程中合适地销毁和重建内部的事件监听和子组件;v-show 只是简单的基于 css 切换
原理:设置 display 属性,另一个是根据条件值判断生成 DOM
表现的话就是组件样式互相隔离,样式私有化,不污染全局的作用;
Scope CSS 的本质是基于 HTML 和 CSS 属性选择器,即分别给 HTML 标签和 CSS 选择器添加 data-v-xxx;
具体来说,它是 通过 vue-loader 实现 的,实现过程大致分 3 步:
keepalive 可以接收3个属性做为参数进行匹配对应的组件进行缓存:
v2 核心:Object.defineProperty() 对属性的读取、修改进行拦截(数据劫持)
问题:例如遇到数组对象时,直接通过下标修改数组,界面会不会自动更新?这部分可以看下面 v2 和 v3 版不同的实现原理:
用于定义基本操作的自定义行为,说白了 Proxy 用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等),用到了Reflect 也就是反射对象
对比:
因为 Hook 的每一次渲染都按照同样的顺序被调用,主要是为保证在多次的 useState 和 useEffect 调用之间保持 Hook 状态的正确。详见 https://overreacted.io/zh-hans/why-do-hooks-rely-on-call-order/
用 jsx 写 react 组件,render() 输出虚拟 dom(通过 babel 插件),虚拟 dom 转为 DOM,再在 DOM 上注册事件,事件触发 setState()修改数据,在每次调用 setState 方法时,React 会自动执行 render 方法来更新虚拟 dom,如果组件已经被渲染,那么还会更新到 DOM 中去。
js执行会占据主线程时间较长,会导致页面响应度变差,使得动画、手势交互等事件产生卡顿。
Fiber 之前的架构是同步更新,遍历,从根组件开始到子节点
React 在 V16 之前会面临的主要性能问题是:当组件树很庞大时,更新状态可能造成页面卡顿,根本原因在于——更新流程是 【同步、不可中断的】
原因是 useEffect 是在浏览器绘制之后执行的,所以方块一开始就在最左边,于是我们看到了方块移动的动画。然而 useLayoutEffect 是在绘制之前执行的,会阻塞页面的绘制,所以页面会在 useLayoutEffect 里面的代码执行结束后才去继续绘制,于是方块就直接出现在了右边
memo 默认情况下对负责对象做浅层比较,如果想控制对比过程,可以在第二个参数位置传入方法自己对比 props(prevProps, nextProps)
useCallback:
通过 useCallback 获得一个记忆后的函数,避免函数组件在每次渲染的时候如果有传递函数的话,重新渲染子组件。用来做性能优化。
useMemo:
记忆组件,和 useCallback 类似,不同的是:useCallback 不会执行第一个参数函数,而是将它返回给你,而 useMemo 会执行第一个函数并且将函数执行结果返回给你
AST 是对源代码的抽象语法结构的树状表现形式。
分词:将整个的代码字符串,分割成语法单元数组(token)。 JS中的语法单元(token)指标识符(function,return),运算符,括号,数字,字符串等能解析的最小单元。
语法分析:语义分析的目的是将分词得到的语法单元进行一个整体的组合,分析确定语法单元之间的关系。简单来说,语义分析可以理解成对语句(statement)和表达式(expression)的识别。

原子性(Atomicity):
事务是数据库的逻辑工作单位,它对数据库的修改要么全部执行,要么全部不执行。
一致性(Consistemcy):
事务前后,数据库的状态都满足所有的完整性约束。
隔离性(Isolation):
并发执行的事务是隔离的,一个不影响一个。如果有两个事务,运行在相同的时间内,执行相同的功能,事务的隔离性将确保每一事务在系统中认为只有该事务在使用系统。这种属性有时称为串行化,为了防止事务操作间的混淆,必须串行化或序列化请求,使得在同一时间仅有一个请求用于同一数据。通过设置数据库的隔离级别,可以达到不同的隔离效果。
持久性(Durability):
在事务完成以后,该事务所对数据库所作的更改便持久的保存在数据库之中,并不会被回滚。
4个级别:
序列化,系统中所有的事务以串行地方式逐个执行,所以能避免所有数据不一致情况。
Repeatable read(可重复读)一个事务一旦开始,事务过程中所读取的所有数据不允许被其他事务修改。
Read Committed(已提交读)一个事务能读取到其他事务提交过(Committed)的数据。
Read Uncommitted(未提交读)一个事务能读取到其他事务修改过,但是还没有提交的(Uncommitted)的数据。
前端面试八股文是指前端面试过程中经常被问到的一系列问题,基本也是面试必问的基础问题,被大家戏称为前端八股文,在国内互联网公司前端面试里,考八股文和手写代码是逃不了的两部分,据我今年面试情况基本上一面问八股文的概率已经超过 80 %。
这里汇总了最近面试被问到频率较高的一些基础题及我的简要回答,主要包括 HTML 、JavaScript、前端框架三部分内容,如有错误或遗漏欢迎反馈斧正,后续有新问题会持续更新,希望大家在 2023 年都能找到合适满意的工作。
更新:补充了面试新文章 -> 前端面试系列 - 手写代码
HTML 语义化就是让页面的内容结构化,便于对浏览器、搜索引擎解析;在没有样式 CCS 情况下也以一种文档格式显示,并且是容易阅读的。搜索引擎的爬虫依赖于标记来确定上下文和各个关键字的权重,利于 SEO。
前端加载 html,html 解析器运行于主线程中,如果遇到<script> 标签后会阻塞,直到脚本从网络中下载并被执行,也就是说<script>标签的脚本会阻塞浏览器的渲染。这里还涉及到页面生命周期:
当浏览器处理一个 HTML 文档,并在文档中遇到 <script> 标签时,就会在继续构建 DOM 之前运行它。这是一种防范措施,因为脚本可能想要修改 DOM,甚至对其执行 document.write 操作,所以 DOMContentLoaded 必须等待脚本执行结束。如何解决这个问题,可以使用 script 标签的两个属性,defer 和 async。
cookie 的属性:
要讲明白这两个概念,需要先说一下浏览器的页面生成过程,当我们通过 url 链接访问一个页面时,在加载完 html、css 、script资源后会有一个解析布局绘制页面的过程,对应的三个操作解析、布局、绘制,网页生成的时候,至少需要排列及绘制一次,随着用户的操作后续有可能而后面会触发重排和重绘。
过程:
重排(reflow):当 dom 的变化影响了元素的几何信息(例如它的位置,尺寸等),这个时候浏览器会重新计算它的属性值,并且把它放在正确位置上,这个叫重排(重新生成布局,重新排列)
常见的触发属性或方法:width,height、font-size、display、scrollTo等
重绘(repaints):当一个元素的外观发生了变化,但没有改变布局,浏览器会把元素外观重新绘制出来。常见的触发属性: color,border-style、background-position 等
重绘不一定会导致重排,但是重排一定会导致重绘。
一些方法减少不必要的重排:
js
1div.style.left = div.offsetLeft + 1 + 'px'; 2 3div.style.top = div.offsetTop + 1 + 'px'; 4 5// 上述会进行两次重排,而通过变量存储后,讲进行一次重排 6 7var curLeft = div.offsetLeft; 8 9var curTop = div.offsetTop; 10 11div.style.left = curLeft + 1 + 'px'; 12 13div.style.top = curTop + 1 + 'px'; 14 15这也相当于是分离读写操作了优化为1次重排
每一个元素在浏览器中都可以理解成一个盒子,它包含对应的四个属性值:宽高、boder 边框、padding 内边距、外边距 margin
分为了 W3C 盒子模型(标准盒模型)和 IE 盒子模型(怪异盒模型)
标准盒子模型的内容空间的宽度是由 width 属性设置的,例如宽度 = width + border + 内边距的值,也就是说 width 只是里面的内容的宽度,实际大小会加上 border + padding的值。
它的实际区域内容宽高值分别是:
宽:width + padding + border
高:height + padding + border
但在ie也就是怪异盒子模型里,会包含 border 和 padding 的值,也就是说,如果你将一个元素的 width 设为 100px,那么这 100px 会包含它的 border 和 padding,内容区的实际宽度是 width 减 去(border + padding) 的值。
它的实际区域内容宽高值分别是:
三个参数分别对应的是 flex-grow, flex-shrink 和 flex-basis,默认值为0 1 auto。
BFC(Block Formatting Context),即块级格式化上下文,它是页面中的一块渲染区域,容器页面里的子元素不会影响到外部的元素,并且有一套属于自己的渲染规则:
触发条件:
(创建BFC的方式:根元素、浮动元素和绝对定位元素,非块级盒子的块级容器,overflow 值不为 visiable 的块级盒子)
应用场景:
通过控制边宽及 transparent 来实现
css
1#demo{ 2 width:100px; 3 height:100px; 4 border:3px red solid; 5} 6 7#demo{ 8 width:100px; 9 height:100px; 10 border: 20px solid; 11 border-color: red blue red blue; 12}
css
1#demo{ 2 width:0px; 3 height:0px; 4 border: 40px solid; 5 border-color: red blue red blue; 6}
css
1#demo{ 2 width:0px; 3 height:0px; 4 border:40px solid transparent; 5 border-bottom:80px solid red; 6}
块级元素,顾名思义,该元素呈现“块”状,所以它有自己的宽度和高度,块级元素比较霸道,它独自占据一行高度(float浮动除外),一般可以作为其他容器使用,可容纳块级元素和行内元素。
行内元素不可以设置宽(width)和高(height),但可以与其他行内元素位于同一行,行内元素内一般不可以包含块级元素。行内元素的高度一般由元素内部的字体大小决定,宽度由内容的长度控制。
input、img就是行内块级元素,
css
1.con1_l{ 2 float:left; 3} 4.con1_r{ 5 overflow: hidden; 6}
css
1.con2_l{ 2 float:left; 3} 4.con2_r{ 5 margin-left:200px; 6}
css
1/* 5 calc */ 2.con5_l{ 3 float:left; 4} 5.con5_r{ 6 width:calc(100% - 200px); 7 float:left; 8} 9
内存空间分为栈 Stack 和 堆 heap 两种,其中栈存放变量及基础类型,堆存放复杂对象、也叫引用数据类型,栈自动分配相对固定大小的内存空间。。
在 JS 里内存是由系统自动分配管理的,内存管理机制是内存基元会在变量创建时分配,然后在它们不再被使用时“自动”释放,后面这个过程也叫垃圾回收机制,
而栈就像管道里放箱子,后放的在上边,所以后进先出。
队列是先进先出:就像一条路,有一个入口和一个出口,先进去的就可以先出去。
栈只能从头部取数据,也就最先放入的需要遍历整个栈 最后才能取出来,而且在遍历数据的时候还得为数据开辟临时空间,队列则不同,它基于地址指针进行遍历,而且可以从头或尾部开始遍历,速度快很多
IIFE(立即调用函数表达式)是一个在定义时就会立即执行的 JavaScript 函数。好处:表达式中的变量不能从外部访问。
js
1(function () { 2 statements 3})();
use strict 现在用得不是特别多,其实根据字面意思也能知道它其实是要去我们用严格的行为模式来写代码执行代码。"use strict" 是在ECMAScript5 中新增的一个声明,但它不是一条语句,是一个字面量表达式。可以在脚本或函数的头部添加 use strict 来声明。
例子:
作用:
length是函数对象的一个属性值,指该函数期望传入的参数数量,即形参的个数console.log(((...args) => {}).length);原型及原型链主要涉及到这三个名词 proto、prototype、 constructor:
每个 js 复杂数据类型(Object Function Array)等都会自带一个 prototype 对象,这个对象就是我们说的原型。
proto 访问器属性,它指向原型对象,所以不管你是 Function 还是 Object 都会有 proto 属性,这些最终都指向了 Object.protoype 原型对象,它也是对象,它也有 proto ,它的原型对象指向了 null。
在 JavaScript 中原型是一个 prototype 对象,用于表示类型之间的关系。
原型对象的用途是为每个实例对象存储共享的方法和属性,它仅仅是一个普通对象而已。并且所有的实例是共享同一个原型对象,因此有别于实例方法或属性,原型对象仅有一份。
JavaScript 万物都是对象,对象和对象之间也有关系,并不是孤立存在的。对象之间的继承关系,在 JavaScript 中是通过 prototype 对象指向父类对象,直到指向 Object 对象为止,这样就形成了一个原型指向的链条,专业术语称之为原型链。
js
1function Fn() {} // Fn为构造函数 2 3var f1 = new Fn(); //f1是Fn构造函数创建出来的对象
构造函数的 prototype 属性值就是对象原型。(Fn.prototype就是对象的原型)
构造函数的 prototype 属性值的类型就是对象 typeof Fn.prototype===object.
对象原型中的 constructor 属性指向构造函数 (Fn.prototype.constructor===Fn)
对象的 proto 属性值就是对象的原型。(f1.__proto__就是对象原型)
常见的面试题,实现一个 curry add 函数,将 add(a + b) 转换为 add(a)(b),既然需要返回继续可执行的函数,第一个想到的是闭包,把当前函数当结果返回就能继续执行了
js
1function add(a, b) { 2 return a + b; 3} 4 5// curry 化 6function curryAdd(a) { 7 return function(b) { 8 return a + b 9 }; 10}; 11 12// 如果用箭头函数实现 13const curryAdd = a => b => a + b 14add(1,2) 15curryAdd(1)(2) // 3 16 17const curry = function (fn, ...a) { 18 // 实参数量大于等于形参数量吗? 19 return a.length >= fn.length ? 20 // 如果大于返回执行结果 21 fn(...a) : 22 // 反之继续柯里化,递归,并将上一次的参数以及下次的参数继续传递下去 23 (...b) => curry(fn, ...a, ...b); 24}; 25const add = (a, b, c) => a + b + c; 26// 将add加工成柯里化函数 27const addCurry = curry(add); 28console.log(addCurry(1, 2, 3));// 6 29console.log(addCurry(1)(2)(3));// 6 30console.log(addCurry(1, 2)(3));// 6 31console.log(addCurry(1)(2, 3));// 6
js
1function Parent () { 2 this.name = 'kevin'; 3} 4 5Parent.prototype.getName = function () { 6 console.log(this.name); 7} 8 9function Child () { 10 11} 12 13Child.prototype = new Parent(); 14 15var child1 = new Child(); 16 17console.log(child1.getName())
js
1Object.defineProperty(Object.prototype, 2 "extend", 3 { 4 writable: true, 5 enumerable: false, 6 configurable: true, 7 value: function(o){ 8 // 获取所有的自有属性 9 var names = Object.getOwnPropertyNames(o); 10 for(var i =0;i< names.length;i++){ 11 // 如果属性已存在则跳过 12 if(names[i] in this) continue; 13 // 获取该属性的属性描述符 14 var desc = Object.getOwnPropertyDescriptor(o,names[i]); 15 Object.defineProperty(this,names[i],desc) 16 } 17 } 18 } 19) 20 21var a = { item: 1} 22Object.defineProperty(a,"item2",{ 23 writable: true, 24 enumerable: false, 25 configurable: true, 26 value: 2 27}) 28var b = {} 29b.extend(a)
常见的几种数据类型,按存储类型来分的话有 6 种:
基础类型
引用类型:
Set 对象是值的集合,Set 中的元素只会出现一次,即 Set 中的元素是唯一的。
jsx
1let mySet = new Set(); 2mySet.add(1) // Set [ 1 ] 3mySet.has(1); // true 4mySet.size // 1
与 Object 的区别:
toString() 方法转换为字符串的结果。[object, type] type 其实拿到的是 Symbol.toStringTag,是一个内置 symbol,它通常作为对象的属性键使用。其实 toString 是对象上的方法,每一个对象上都有这个方法,那就意味着数字、字符串和布尔值这些基本数据类型不能使用 toString()方法,但上例中的基本数据类型却是可以使用,这要归功于javascript中的包装类,即 Number、String 和 Boolean。原始值不能有属性和方法,当要使用 toString() 方法时,会先将原始值包装成对象再使用。
所有类都继承自 Object,按理来说输出结果应该都类似于 [object Object] 这样。不一样的原因是所有类在继承 Object的时候,改写了 toString() 方法。所以当我们想要判断数据类型时,必须使用 Object 上的**toString()**方法。
这里涉及到作用域、变量提升的问题,作用域本质上是指变量/函数可供访问的范围。
当在最外层函数的外部声明 var 变量时,作用域是全局的。这意味着在最外层函数的外部用 var 声明的任何变量都可以访问到。但当你在函数范围内申明变量时,外部的代码是无法访问到这个变量的。
var 的一个问题是变量被修改覆盖,举个简单的例子:
js
1var greeter = 'hello' 2if (true) { 3 var greeter = 'world' 4} 5console.log(greeter) // 这里会打印 world 替换成 let 后能解决这个问题 6
let 是块级作用域,块是由 {} 界定的代码块,大括号中有一个块。大括号内的任何内容都包含在一个块级作用域中,因为变量仅在其块级作用域内存在。
区别:
var声明的变量会被提升到其作用域的顶部,并使用 undefined 值对其进行初始化。let声明的变量有暂时性死区,没有变量提升,不会对值进行初始化。访问的时候是直接 Reference Error两个都是优化高频重复操作代码的一个方式,例如我们在使用浏览器的 resize scroll 等事件时,当用户短时间内触发这些事件时,绑定的回调函数会不断地被调用,就有两种优化策略 throttle 节流 和 debounce 防抖
用发车的一个例子来描述这两种情况,如果每个乘客上车发一次车比较耗时:
js
1// throttle 节流 2const throttle = (fn, delay) => { 3 let timer = null; 4 return function (... args) { 5 if (!timer) { 6 timer = setTimeout(() => { 7 fn.apply(this, args); 8 timer = null; 9 }, delay) 10 } 11 } 12}; 13 14// debounce 防抖 15function debounce(fn, delay) { 16 let timer; 17 return function() { 18 let args = arguments; 19 if (timer) clearTimeout(timer); 20 timer = setTimeout(() => { 21 fn.apply(this, args); 22 }, delay) 23 } 24}
window.requestIdleCallback() 方法使用插入一个函数,这个函数将在浏览器空闲时期被调用。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。函数一般会按先进先调用的顺序执行,然而,如果回调函数指定了执行超时时间 timeout,则有可能为了在超时前执行函数而打乱执行顺序。
浅拷贝:拷贝了赋值的引用地址,修改值会影响到原始值,拷贝的是对象的引用地址。指向的还是同一片空间。
深拷贝:拷贝所有的属性,地址也与原先的不同,修改不会影响到原始值
实现一个简单的深拷贝:
js
1function deepclone (obj) { 2 let cloneObj = [].isArray(obj) ? [] : {}; 3 if (obj && typeof obj === 'object') { 4 for (key in obj) { 5 if (obj.hasOwnProperty(key)) { 6 if (obj[key] && typeof obj[key] === 'object') { 7 cloneObj[key] = deepclone(obj[key]) 8 } else { 9 cloneObj[key] = obj[key] 10 } 11 } 12 } 13 } 14 return cloneObj; 15}
使用 new 来新建一个构造函数或类得到对应实例,是非常普遍的操作了,ES5 中使用 function 构造函数,到 ES6 使用 class 都可以使用 new 来新建实例。
js
1//定义的new方法 2let newMethod = function (Parent, ...rest) { 3 // 1.以构造器的 prototype 属性为原型,创建新对象; 4 let child = Object.create(Parent.prototype); 5 // 2.将 this 和调用参数传给构造器执行 6 let result = Parent.apply(child, rest); 7 // 3.如果构造器没有手动返回对象,则返回第一步的对象 8 return typeof result === 'object' ? result : child; 9};
分严格模式和非严格模式及在指定的场景:
js 的作用域分为:全局作用域、块级作用域、函数作用域。
全局作用域是指函数或者 {} 外面的执行环境,叫全局作用域,根据 js 执行环境不同,全局作用域不同。
在函数内部定义的变量,就是局部作用域。在函数作用域内,对外是封闭的,从外层作用域无法直接访问内部作用域,但是函数内部作用域可以访问外部的作用域。
作用域是分层的,子作用域可以访问父作用域,不能从父作用域引用子作用域中的变量;如果一个变量或者其他表达式不在当前的作用域,那么js 机制会继续沿着作用域链向上查找直到全局作用域( Node中 的 global 或浏览器中的 window),如果找不到则表示变量不可用
变量提升:
异步任务队列又分为微任务(micro task)队列和宏任务(macro task)队列;事件循环的过程中,执行栈在同步代码执行完成后。
常见的宏任务:
微任务:
为什么会存在这样的两个方法,主要是 js 存在定义时上下文、执行上下文以及上下文 context 可以改变的,换句话说其实上为了改变 this 的指向。
当一个对象没有某个方法的时候,但是其它对象有,我们就可以借助 call 或 apply 来实现用它对象上方法的调用。
js
1function Friut {} 2 3Friut.prototype = { 4 color: 'red', 5 say: function () { 6 console.log('color is:' + this.color) 7 } 8} 9 10var apple = new Friut; 11apple.say(); 12 13// 重新定义一个水果对象 banana,它没有say方法,可以使用apple的的say 14 15var banana = { 16 color: 'yellow' 17} 18apple.say.call(banana) // color is: yellow
bind,call 和 apply 的区别:
手动实现 call apply bind 参考链接
js
1// call 2Function.prototype.myCall = function(context) { 3 if(typeof this !== 'function') { 4 console.log('type error') 5 } 6 // 获取参数 7 let args = [...arguments].slice(1); 8 result = null 9 context = context || window 10 context.fn = this 11 // 调用函数 12 result = context.fn(...args); 13 // 删除属性 14 delete context.fn 15 return result 16} 17 18// apply 19Function.prototype.myApply = function (context) { 20 if(typeof this !== 'function') { 21 throw new TypeError('error') 22 } 23 let result = null 24 context = context || window 25 context.fn = this 26 if (arguments[1]) { 27 result = context.fn([...arguments[1]]) // 调用赋值的函数 28 } 29 delete context.fn 30 return result 31} 32 33// bind 34Function.prototype.myBind = function(context) { 35 if (typeof this !== 'function') { 36 throw new TypeError('error') 37 } 38 // 获取参数 39 let args = [...arguments].slice(1) 40 context.fn = this 41 return function Fn() { 42 return fn.apply(this instanceof Fn ? this : context, args.concat(...arguments)) 43 } 44}
一句话解释闭包:闭包允许函数访问并操作函数外部的变量。要理解闭包,先理解 js 的变量作用域。
js 的变量作用域有两种,一种是全局变量,另一种是局部变量,函数内部可以读取到外部的全局变量,但函数外部无法读取内部的局部变量。那有没有什么办法可以访问到这个变量呢,有的,使用闭包。
js
1let sum = (num1, num2) => num1 + num2; 2// 等同于 3let sum = function(num1, num2) { 4 return num1 + num2; 5};
箭头函数是 es6 新语法,跟传统普通函数比有以下一些优势:
js
1var id = 'id1'; 2var obj = { 3 id: 'id2', 4 a: function(){ 5 console.log(this.id); 6 }, 7 b: () => { 8 console.log(this.id); 9 } 10}; 11 12obj.a(); // 'id2' 13obj.b(); // 'id1'
JS 从诞生起就是一门单线程的非阻塞脚本语言
单线程意味着不管执行什么任务,当前都只有一个主线程在执行任务
但是要做到非阻塞的话,意味着例如在处理异步请求的时候不能卡在当前代码,代码继续往下执行,这时会 pending 这个异步任务,当它返回结果后再去执行注册的相应回调。
一般是在 栈里来执行方法,而堆用来存放一些较大的数据,像对象等。javascript 执行的时候会存放在内存的不同地方,使用栈 stack 和 堆 heap 来加于区分。
一个方法执行会向执行栈中加入这个方法的执行环境,在这个环境中也可以调用自己,当这个执行环境中的代码 执行完毕并返回结果后,js会退出这个执行环境并把这个执行环境销。
上面是同步代码,异步代码有一点不一样,因为异步代码不会立即返回结果,而是将这个事件挂起,这个时候就需要用到这个事件队列的机制,异步返回结果后,js 会把这个事件存放在与当前执行栈不一样的一个队列中,我们称之为事件队列,但被放入到队列的事件并不会立刻执行这个回调。而是等待当前执行栈所有任务执行完,主线程处于闲置状态时,再去查找队列里是否还有任务没有执行,有的话再把它拿出来放在执行栈中去执行,如此反复就形成了事件循环。
为什么 js 是单线程?一开始是主要在浏览器端来使用 js,需要保证程序执行的一致性。
worker 线程的使用有一些注意点:
场景:
Mutation Observer API 用来监视 DOM 变动。DOM 的任何变动,比如节点的增减、属性的变动、文本内容的变动,这个 API 都可以得到通知。
可以理解为 DOM 发生变动就会触发 Mutation Observer 事件。但是,它与事件有一个本质不同:事件是同步触发,也就是说,DOM 的变动立刻会触发相应的事件;Mutation Observer 则是异步触发,DOM 的变动并不会马上触发,而是要等到当前所有 DOM 操作都结束才触发。
通过传入一个函数来创建一个 MutationObserver 实例,每当有变化发生,这个函数将会被调用。函数的第一个参数是变动数组,每个变化都会提供它的类型和已经发生的变化的信息
这个被创建的对象有三个方法:
在前端领域中,跨域是指浏览器允许向服务器发送跨域请求,为什么有这个跨域,主要是同源策略。同源策略是浏览器的一个核心的安全策略,主要指"协议+域名+端口”三种需要相同,不同的话会被限制发请求、cookie 、localstorage 、dom等也无法读取
常见的跨域方式
浏览器将CORS跨域请求分为简单请求和非简单请求。
简单请求里会新增加一个 Origin 字段。需要传递你接口的源的信息,后端判断是否同意这次请求,对应的相应头里也会返回 Access-Control 开头的字段,Access-Control-Allow-Origin。Allow-Credentials,默认是不带 cookie,如果需要带设置成 true
不是简单请求,会多发一个预请求,预检"请求用的请求方法是 OPTIONS,表示这个请求是用来询问的。请求头信息里面,关键字段是 Origin,表示请求来自哪个源。除了 Origin 字段,预检请求的头信息包括两个特殊字段:
两种实现,例如当中一个 div 元素当中有一个 p 子元素,如果两个元素都有一个 click 的处理函数,那么我们怎么才能知道哪一个函数会首先被触发呢?微软提出了名为事件冒泡 (event bubbling) 的事件流。事件冒泡可以形象地比喻为把一颗石头投入水中,泡泡会一直从水底冒出水面。也就是说,事件会从最内层的元素开始发生,一直向上传播,直到 document 对象。
我们知道如果需要执行代码,操作系统的运行时就需要开辟内存空间来运行,当运行完及时释放内存,如果有不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)。
轻则影响程序的运行速率,重则容易造成程序崩溃。
说一下场景的容易引起内存泄露的场景
js
1var fn = (function(){ 2 var a = '***';// 被闭包所引用,不会被回收 3 return function(){ 4 console.log(a); 5 } 6})()
Web 端和 Native 可以类比于 Client/Server 模式,Web 端调用原生接口时就如同 Client 向 Server 端发送一个请求类似,JSB 在此充当类似于 HTTP 协议的角色,将 Native 与 JavaScript 的每次互相调用看做一次 RPC 调用。前端相当于 rpc 里的客户端,Native 相当于 rpc 里的服务端
实现 JSBridge 主要有两种:
主要原因是转换成了二进制进行运算
js
1/** 2 * 精确加法 3 */ 4function add(num1, num2) { 5 const num1Digits = (num1.toString().split('.')[1] || '').length; 6 const num2Digits = (num2.toString().split('.')[1] || '').length; 7 const baseNum = Math.pow(10, Math.max(num1Digits, num2Digits)); 8 return (num1 * baseNum + num2 * baseNum) / baseNum; 9}
JavaScript 垃圾回收机制的原理说白了也就是定期找出那些不再用到的内存(变量),然后释放其内存。这里主要讲一下 v8 的 gc 回收算法,主要是两种:
目前在 JavaScript引擎 里这种算法是最常用的,到目前为止的大多数浏览器的 JavaScript 引擎都在采用标记清除算法。此算法分为标记和清除两个阶段,这使得一位二进制位(0和1)就可以为其标记,非常简单。
标记清除算法有一个很大的缺点,就是在清除之后,剩余的对象内存位置是不变的,也会导致空闲内存空间是不连续的,出现了内存碎片并且由于剩余空闲内存不是一整块,它是由不同大小内存组成的内存列表,这牵扯出了内存分配的问题
这其实是早先的一种垃圾回收算法,它把对象是否不再需要简化定义为对象有没有其他对象引用它。例如定义一个变量赋值时1,当它被另一个变量覆盖时,-1,当这个值的引用次数变为 0 的时候,说明没有变量在使用,清理掉内存
js
1<script> 2 // your code 3</script>
js
1<script src="./a.js"> 2<script src="./b.js">
js
1<script src="./a.js"> 2window.moduleA = { 3 data: 'a', 4 funA: () => {} 5}
js
1(function () { 2 let data = ‘a’ 3})()
前端路由的核心:
hash:
在 url 中的 # 之后对应的是 hash 值, 其原理是通过 hashChange() 事件监听hash值的变化, 根据路由表对应的 hash 值来判断加载对应的路由加载对应的组件。
前端配置路由表, 不需要后端的参与,因为 hash 值变化不会向后端发请求,所以属于前端路由,兼容性好, 浏览器都能支持。
history 是 url 地址规范, 不需要#,包括 back,forward,go 三个方法,这种模式充分利用 history.pushState replaceStateAPI 来完成 URL 跳转而无须重新加载页面。
区别:hash 的传参是基于 url 的,如果要传递复杂的数据,会有体积的限制
2、nodejs 所谓的单线程,只是主线程是单线程,所有的网络请求或者异步任务都交给了内部的线程池去实现,本身只负责不断的往返调度,由事件循环不断驱动事件执行。
3、Nodejs 之所以单线程可以处理高并发的原因,得益于 libuv 层的事件循环机制,和底层线程池实现。
型是静态类型语言的基本特征,允许将类型作为参数传递给另一个类型、函数、或者其他结构。TypeScript 支持泛型作为将类型安全引入组件的一种方式。这些组件接受参数和返回值,其类型将是不确定的,直到它在代码中被使用。
泛型的语法为
例如当我们想实现一个传入什么类型就得到什么类型的函数时,就可以用到泛型,有没有一种方法在调用的时候再指定类型呢。
将泛型与函数一起使用的最常见场景之一就是,当有一些不容易为所有的用例定义类型时,为了使该函数适用于更多情况,就可以使用泛型来定义
js
1function identity<T>(value: T): T { 2 return value; 3}
https://nodelover.gitbook.io/typescript/fan-xing#le-jie-object.create
二者都是可以赋值给任意类型的, any 会绕过类型检查,直接可用,而 unkonwn 则必须要在判断完它是什么类型之后才能继续用
常用的一些关键字
Exclude<T, U> 从Y中排出可以分配给U的元素.
Omit<T, K> 忽略T中的某些属性.
Merge<O1, O2> 将两个对象的属性合并.
Compute<A & B> 将交叉类型合并.
Intersection<T, U> 取T的属性,此属性同样哦存在于U.
Overwrite<T, U> 用U的属性覆盖T的相同属性.
Covariant 协变,ts 对象兼容性是协变,父类<=子类是可以的,子类 <= 父类错误。 Contravariant 逆变,禁用 strictFunctionTypes 编译,函数参数类型都是逆变的,父类 <= 子类,是错误。子类 <= 父类,是可以的。 Bivariant 双向协变,函数参数的类型默认是双向协变的。父类 <= 子类,是可以的。子类 <= 父类,是可以的。
相同点:
不同点:
implements 一般是实现接口。extends 是继承类。
共同点:
不同点:
implements 实现,一个新的类,从父类或者接口实现所有的属性和方法,同时可以重写属性和方法,包含一些新的功能 extends 继承,一个新的接口或者类,从父类或者接口继承所有的属性和方法,不可以重写属性,但可以重写方法
js
1type Types = number | string; 2// 重载签名 3function add(x: number, y: number): number; 4function add(x: string, y: string): string; 5function add(x: string, y: number): string; 6function add(x: number, y: string): string; 7function add(x: Types, y: Types) { 8 if (typeof x === 'string' || typeof y === 'string') { 9 return x.toString() + y.toString(); 10 } 11 return x + y; 12} 13const result = add('hearts', ' spades'); 14result.split(' ');
常量枚举只能使用常量枚举表达式并且不同于常规的枚举,他们在编译阶段会被删除。常量枚举成员在使用的地方会被内联起来,之所以真可以这么做是因为,常量枚举不允许包含计算成员。
https 多了一个sercurty 安全,唯一的区别就是使用了 TSL(ssl) 来加密普通的http 请求和响应,并且对请求和响应进行数字前面,所以更安全。
OSI是一个开放性的通信系统互连参考模型
应用层:为程序提供网络服务,例如 HTTP,FTP,SMTP
表示层:这一层主要是数据格式化和加密解密
会话层:建立维护管理会话连接(在tcp/ip协议组合并一个应用层)
传输层: 建立管理端到端的连接 TCP、UDP
网络层:IP 寻址和路由选择
数据链路:控制网络层和物理层通信的
物理层:比特流传输 例如光缆和电缆等设备组网
TCP 是一种面向连接的单播协议,在发送数据前,通信双方必须在彼此间建立一条连接。所谓的“连接”,其实是客户端和服务器的内存里保存的一份关于对方的信息,如ip地址、端口号等。
这是由于 TCP 的半关闭(half-close)特性造成的,TCP 提供了连接的一端在结束它的发送后还能接收来自另一端数据的能力。
第一次挥手:客户端发送一个 FIN 报文(请求连接终止:FIN = 1),报文中会指定一个序列号 seq = u。并停止再发送数据,主动关闭 TCP 连接。
第二次挥手 服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序号值 +1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT 状态。
**第三次挥手 :**如果服务端也想断开连接了(没有要向客户端发出的数据),和客户端的第一次挥手一样,发送 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 的状态,等待客户端的确认
第四次挥手客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答(ack = w+1),且把服务端的序列值 +1 作为自己 ACK 报文的序号值
通俗的来说,两次握手就可以释放一端到另一端的 TCP 连接,完全释放连接一共需要四次握手
资源的优先级被分为 5 级: Highest 、 Medium 、 Low 、 Lowest 、 Idle ;
浏览器首先会按照资源默认的优先级确定加载顺序:
是一种网络传输协议,位于 OSI 模型的应用层。可在单个 TCP 连接上进行全双工通信,能更好的节省服务器资源和带宽并达到实时通迅,客户端和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。使用场景如弹幕、协同编辑等
xss:跨域脚本攻击,原来就叫 CSS,主要是把恶意的脚本代码HTMLJS代码植入到网页中,当用户浏览时执行代码,可能获取到cookie、会话劫持、钓鱼网站等。
主要分两种:反射型,将恶意脚本附加到 url 参数中,这种危害性较小
持久性:恶意代码上传或存储至漏洞服务器中,用户浏览时就执行,例如获取cookie信息
防范:
CSRF:跨站请求伪造。原理或本质就是让用户在已登录的站点上执行非本意的操作,
攻击者首先盗用了你的身份,然后以你的名义进行某些非法操作,web 中用户身份认证验证的一个漏洞:简单的身份验证仅仅能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。
两个必要条件:
常见的例如:img src 属性能够以 get 方式发送请求第三方资源,浏览器会带上你的 cookie
响应式更新流程图


created 方法
mounted 方法
virtual DOM 是一种编程理念(数据驱动视图),将 ui 虚拟 dom 保持到内存中,并且通过某些库渲染成真实的 dom,这个过程又叫做协调。
包括:
实际上它只是一层对真实 DOM 的抽象,以 JavaScript 对象 (VNode 节点) 作为基础的树,用对象的属性来描述节点,最终可以通过一系列操作使这棵树映射到真实环境上
在 Javascript 对象中,虚拟 DOM 表现为一个 Object 对象。并且最少包含标签名 (tag)、属性 (attrs) 和子元素对象 (children) 三个属性,不同框架对这三个属性的名命可能会有差别
react 函数式组件思想 当你 setstate 就会遍历 diff 当前组件所有的子节点子组件, 这种方式开销是很大的, 所以 react 16 采用了 fiber 链表代替之前的树,可以中断的,分片的在浏览器空闲时候执行
vue 组件响应式思想 采用代理监听数据,我在某个组件里修改数据,就会明确知道那个组件产生了变化,只用 diff 这个组件就可以了
在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM,所以放在 Vue.nextTick() 回调函数中的执行。nextTick 本质是一种优化策略
我们可以理解成,Vue 在更新 DOM 时是异步执行的。当数据发生变化,Vue 将开启一个异步更新队列,视图需要等队列中所有数据变化完成之后,再统一进行更新。
Vue 是异步执行 dom 更新的,一旦观察到数据变化,Vue 就会开启一个队列,然后把在同一个事件循环 (event loop) 当中观察到数据变化的 watcher 推送进这个队列。如果这个 watcher 被触发多次,只会被推送到队列一次。这种缓冲行为可以有效的去掉重复数据造成的不必要的计算和 DOm 操作。
控制手段不同编译过程不同编译条件不同:
控制手段:v-show 隐藏则是为该元素添加 css display:none,dom 元素依旧还在。v-if 显示隐藏是将 dom 元素整个添加或删除
编译过程:切换过程中合适地销毁和重建内部的事件监听和子组件;v-show 只是简单的基于 css 切换
原理:设置 display 属性,另一个是根据条件值判断生成 DOM
表现的话就是组件样式互相隔离,样式私有化,不污染全局的作用;
Scope CSS 的本质是基于 HTML 和 CSS 属性选择器,即分别给 HTML 标签和 CSS 选择器添加 data-v-xxx;
具体来说,它是 通过 vue-loader 实现 的,实现过程大致分 3 步:
keepalive 可以接收3个属性做为参数进行匹配对应的组件进行缓存:
v2 核心:Object.defineProperty() 对属性的读取、修改进行拦截(数据劫持)
问题:例如遇到数组对象时,直接通过下标修改数组,界面会不会自动更新?这部分可以看下面 v2 和 v3 版不同的实现原理:
用于定义基本操作的自定义行为,说白了 Proxy 用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等),用到了Reflect 也就是反射对象
对比:
因为 Hook 的每一次渲染都按照同样的顺序被调用,主要是为保证在多次的 useState 和 useEffect 调用之间保持 Hook 状态的正确。详见 https://overreacted.io/zh-hans/why-do-hooks-rely-on-call-order/
用 jsx 写 react 组件,render() 输出虚拟 dom(通过 babel 插件),虚拟 dom 转为 DOM,再在 DOM 上注册事件,事件触发 setState()修改数据,在每次调用 setState 方法时,React 会自动执行 render 方法来更新虚拟 dom,如果组件已经被渲染,那么还会更新到 DOM 中去。
js执行会占据主线程时间较长,会导致页面响应度变差,使得动画、手势交互等事件产生卡顿。
Fiber 之前的架构是同步更新,遍历,从根组件开始到子节点
React 在 V16 之前会面临的主要性能问题是:当组件树很庞大时,更新状态可能造成页面卡顿,根本原因在于——更新流程是 【同步、不可中断的】
原因是 useEffect 是在浏览器绘制之后执行的,所以方块一开始就在最左边,于是我们看到了方块移动的动画。然而 useLayoutEffect 是在绘制之前执行的,会阻塞页面的绘制,所以页面会在 useLayoutEffect 里面的代码执行结束后才去继续绘制,于是方块就直接出现在了右边
memo 默认情况下对负责对象做浅层比较,如果想控制对比过程,可以在第二个参数位置传入方法自己对比 props(prevProps, nextProps)
useCallback:
通过 useCallback 获得一个记忆后的函数,避免函数组件在每次渲染的时候如果有传递函数的话,重新渲染子组件。用来做性能优化。
useMemo:
记忆组件,和 useCallback 类似,不同的是:useCallback 不会执行第一个参数函数,而是将它返回给你,而 useMemo 会执行第一个函数并且将函数执行结果返回给你
AST 是对源代码的抽象语法结构的树状表现形式。
分词:将整个的代码字符串,分割成语法单元数组(token)。 JS中的语法单元(token)指标识符(function,return),运算符,括号,数字,字符串等能解析的最小单元。
语法分析:语义分析的目的是将分词得到的语法单元进行一个整体的组合,分析确定语法单元之间的关系。简单来说,语义分析可以理解成对语句(statement)和表达式(expression)的识别。

原子性(Atomicity):
事务是数据库的逻辑工作单位,它对数据库的修改要么全部执行,要么全部不执行。
一致性(Consistemcy):
事务前后,数据库的状态都满足所有的完整性约束。
隔离性(Isolation):
并发执行的事务是隔离的,一个不影响一个。如果有两个事务,运行在相同的时间内,执行相同的功能,事务的隔离性将确保每一事务在系统中认为只有该事务在使用系统。这种属性有时称为串行化,为了防止事务操作间的混淆,必须串行化或序列化请求,使得在同一时间仅有一个请求用于同一数据。通过设置数据库的隔离级别,可以达到不同的隔离效果。
持久性(Durability):
在事务完成以后,该事务所对数据库所作的更改便持久的保存在数据库之中,并不会被回滚。
4个级别:
序列化,系统中所有的事务以串行地方式逐个执行,所以能避免所有数据不一致情况。
Repeatable read(可重复读)一个事务一旦开始,事务过程中所读取的所有数据不允许被其他事务修改。
Read Committed(已提交读)一个事务能读取到其他事务提交过(Committed)的数据。
Read Uncommitted(未提交读)一个事务能读取到其他事务修改过,但是还没有提交的(Uncommitted)的数据。