先来无事更新了下简历,接到了一个电话面试,没想到一面就是两个半小时 总结下面试内容
CSS
CSS 的两种盒模型分析一下
盒模型分为标准盒模型(content-box)和怪异盒模型(IE 模型)(border-box)
改变方式 box-sizing: ...
content-box: width 与 height 只包括内容的宽和高, 不包括边框(border),内边距(padding),外边距(margin)。 内边距、边框和外边距都在这个盒子的外部 即 width: 内容的宽度
border-box:是当文档处于 Quirks 模式 时 Internet Explorer 使用的盒模型。注意,填充和边框将在盒子内 即 width:border + padding + 内容的宽度
你是怎么实现水平垂直居中的
比较简单:可以通过 flex position gard transform 等实现
你在项目中是怎么使用 px em rem
- px (pixel,像素): 是一个虚拟长度单位,是计算机系统的数字化图像长度单位,如果 px 要换算成物理长度,需要指定精度 DPI(Dots Per Inch,每英寸像素数),在扫描打印时一般都有 DPI 可选。Windows 系统默认是 96dpi,Apple 系统默认是 72dpi。
- em(相对长度单位,相对于当前对象内文本的字体尺寸): 是一个相对长度单位,。现指的是字符宽度的倍数,用法类似百分比,如:0.8em, 1.2em,2em 等。通常 1em=16px。 是一个物理长度单位,指的是 72 分之一英寸。pt=1/72(英寸), px=1/dpi(英寸) 特点:em 指的是一个字体的大小,它会继承父级元素的字体大小,因此并不是一个固定的值。任何浏览器的默认字体大小都是 16px。因此,12px = 0.75em。实际应用中为了方便换算,通常会如下设置样式: html { font-size: 63%; } 这样,1em = 10px。我们常用的 1.2em 理论上就是 12px
- rem(root em,根 em): rem'是'css3'新增的一个相对长度单位,它的出现是为了解决 em 的缺点,em 可以说是相对于父级元素的字体大小,当父级元素字体大小改变时,又得重新计算。rem 出现就可以解决这样的问题,rem 只相对于根目录,即 HTML 元素。有了 rem 这个单位,我们只需要调整根元素 html 的 font-size 就能达到所有元素的动态适配了
flex 怎么用,常用属性有哪些?
flex 的核心的概念就是 容器 和 轴。 父容器 justify-content 项目在主轴上的对齐方式
flex-start | flex-end | center | space-between | space-around space-between 子容器沿主轴均匀分布,位于首尾两端的子容器与父容器相切。 space-around 子容器沿主轴均匀分布,位于首尾两端的子容器到父容器的距离是子容器间距的一半。
align-items 定义项目在侧轴上如何对齐
flex-start | flex-end | center | baseline | stretch; baseline: 项目的第一行文字的基线对齐。 stretch(默认值):如果项目未设置高度或设为 auto,将占满整个容器的高度。
子容器 align-self 单个项目对齐方式
align-self: auto | flex-start | flex-end | center | baseline | stretch;
flex:前面三个属性的简写 是 flex-grow flex-shrink flex-basis 的简写
flex-grow 放大比例 根据所设置的比例分配盒子所剩余的空间 flex-shrink 缩小比例 设置元素的收缩比例 多出盒子的部分,按照比例的大小砍掉相应的大小,即比例越大,被砍的越大,默认值是 1 flex-basis 伸缩基准值 项目占据主轴的空间 flex-basis 该属性设置元素的宽度或高度,当然 width 也可以用来设置元素宽度,如果元素上同时出现了 width 和 flex-basis 那么 flex-basis 会覆盖 width 的值
flex: 0 1 auto; 默认主轴是 row,那么不会去放大比例,如果所有的子元素宽度和大于父元素宽度时,就会按照比例的大小去砍掉相应的大小。 轴 flex-direction 决定主轴的方向 即项目的排列方向 row | row-reverse | column | column-reverse
浏览器
路由规则
可以在不刷新页面的前提下动态改变浏览器地址栏中的 URL 地址,动态修改页面上所显示资源。 window.history 的方法和属性 back() forward() go() HTML5 新方法:添加和替换历史记录的条目
pushState() history.pushState(state, title, url); 添加一条历史记录,不刷新页面
state : 一个于指定网址相关的状态对象,popstate 事件触发时,该对象会传入回调函数中。如果不需要这个对象,此处可以填 null。 title : 新页面的标题,但是所有浏览器目前都忽略这个值,因此这里可以填 null。 url : 新的网址,必须与前页面处在同一个域。浏览器的地址栏将显示这个网址。
replaceState history.replaceState(state, title, url); 替换当前的历史记录,不刷新页面
这两个 API 的相同之处是都会操作浏览器的历史记录,而不会引起页面的刷新。 不同之处在于,pushState 会增加一条新的历史记录,replaceState 则会替换当前的历史记录。 这两个 api,加上 state 改变触发的 popstate 事件,提供了单页应该的另一种路由方式。
popstate 事件:历史记录发生改变时触发
基于 hash(location.hash+hashchange 事件) 我们知道 location.hash 的值就是 url 中#后面的内容,如www.163.com#something。 此网址中,location.hash='#something'。 hash 满足以下几个特性,才使得其可以实现前端路由:
url 中 hash 值的变化并不会重新加载页面,因为 hash 是用来指导浏览器行为的,对服务端是无用的,所以不会包括在 http 请求中。 hash 值的改变,都会在浏览器的访问历史中增加一个记录,也就是能通过浏览器的回退、前进按钮控制 hash 的切换 我们可以通过 hashchange 事件,监听到 hash 值的变化,从而响应不同路径的逻辑处理。
window.addEventListener("hashchange", funcRef, false)
如此一来,我们就可以在 hashchange 事件里,根据 hash 值来更新对应的视图,但不会去重新请求页面,同时呢,也在 history 里增加了一条访问记录,用户也仍然可以通过前进后退键实现 UI 的切换。 触发 hash 值的变化有 2 种方法 👇
一种是通过 a 标签,设置 href 属性,当标签点击之后,地址栏会改变,同时会触发 hashchange 事件
<a href="#TianTianUp">to somewhere</a>
另一种是通过 js 直接赋值给 location.hash,也会改变 url,触发 hashchange 事件。
location.hash="#somewhere"
你认为前端路由和后端路由有什么区别么
后端路由也就是说需要向后台服务器发送请求,然后服务器来决定来 render 那个.html,这也就是最早的 mvc 架构模式,而前端的路由是将这一过程放在浏览器端,也就是前台写 js 代码控制,不在请求服务器,前台一般利用 histroy 和 hash 来控制,达到不刷新页面可以使显示内容发生变化,这样好处是 js 代码不发生变化(浏览器端可以维护一个稳定的 model);一般单页应用就是前台来控制路由,这样速度更快,用户体验更好。
从输入 URL 到页面展示,这中间发生了什么
-
浏览器检查 DNS(域名解析系统)缓存记录,查看域名对应的 IP 地址,为了寻找 DNS 的记录,浏览器会检查以下四种缓存:
- 第一步:检查浏览器缓存,因为浏览器会维护着你在之前某段期间所访问的网站的 DNS 记录
- 第二步:检查操作系统缓存,如果在浏览器缓存中没有找到,浏览器会通过系统调用底层的计算机操作系统的方式来取得记录
- 第三步:检查路由缓存,如果在你的电脑上找不到记录,浏览器会和维护自身 DNS 缓存记录的路由进行通信
- 第四步:检查 ISP 缓存,如果前几步都失败了,浏览器会转向互联网服务提供商,ISP 维护着他自己的 DNS 服务器(这是最后一个寻找你请求 URL 地址缓存的地方)ISP:网络服务提供商】【缓存对于调节网络流量和提高数据传输时间有着重要作用】 如果缓存中没有找到,ISP 的 DNS 服务器就会启动 DNS 查询来寻找主机(如www.baidu.com)服务器的IP地址。DNS解析器一般通过递归查询和迭代查询的方式来找到对应的IP地址,找到后DNS解析器会把IP地址发送给你的浏览器。
-
请求过程 接受到 IP 地址后,浏览器启动与服务器的 TCP 之间连接 浏览器接受到正确的 IP 地址,客户端就会跟服务器通过” TCP/IP 三次握手”建立联系,服务器会匹配到 IP 地址然后传递信息,浏览器使用互联网协议(IP)来建立这种联系,TCP/IP 的三次握手:
-
第一步:客户端通过互联网发送 SYN(同步序列号)给服务器,查询服务器是否对新的连接开发
-
第二步:如果服务有开发的端口,可以接受并启动新的连接,服务器就会通过使用 SYN/ACK 包中的 SYN 包的确定状态响应来响应(SYN/ACK 都会发送给客户端,ACK:确认字符)
-
第三步:客户端会从服务器响应的 SYN/ACK 包,然后通过 ACK 包发送给服务端进行确认已接受服务端发来的数据
-
-
建立 TCP 连接成功后,浏览器就会给 web 服务器发送 HTTP 请求
-
服务器处理请求,并返回 HTTP 响应
-
浏览器展示服务器响应的内容到 HTML,并展示出来 渲染进程对文档进行页面解析和子资源加载,HTML 通过 HTM 解析器转成 DOM Tree(二叉树类似结构的东西),CSS 按照 CSS 规则和 CSS 解释器转成 CSSOM TREE,两个 tree 结合,形成 render tree(不包含 HTML 的具体元素和元素要画的具体位置),通过 Layout 可以计算出每个元素具体的宽高颜色位置,结合起来,开始绘制,最后显示在屏幕中新页面显示出来。
了解 TCP/UDP 么
TCP 和 UDP 协议都是传输层协议。
TCP 是面向连接的协议。在收发数据前,必须和对方建立可靠的连接。 建立链接的过程主要是三次握手 断开链接需要四次挥手 (客户端断开/服务端断开)
UDP 协议:只在 IP 的数据报服务上增加了复用和分用的功能以及差错检测的功能。
只有面向无连接的报文,不可靠传输的特点。
UDP对应用层交下来的数据只添加首部,并进行特别的处理,就交给网络层
对网络层传递上来的用户数据报拆封首部后,原封不动的交给应用层。
区别:1.TCP 是面向连接的协议。 UDP 是无连接的协议。UDP 更加适合消息的多播发布,从单个点向多个点传输消息。 2.TCP 提供交付保证,传输过程中丢失,将会重发。UDP 是不可靠的,不提供任何交付保证。(网游和视频的丢包情况)
简单介绍下浏览器的 localstorage sessionstorage cookie
| 分类 | 生命周期 | 存储容量 | 存储位置 |
| -------- | ----- | ---- | ---- |
| cookie | 默认保存在内存中,随浏览器关闭失效(如果设置过期时间,在到过期时间后失效) | 4KB | 保存在客户端,每次请求时都会带上 |
| localStorage | 论上永久有效的,除非主动清除 | 4.98MB(不同浏览器情况不同,safari 2.49M) | 保存在客户端,不与服务端交互。节省网络流量 |
| sessionStorage | 仅在当前网页会话下有效,关闭页面或浏览器后会被清除。 | 4.98MB(部分浏览器没有限制) | 同上 |
使用
localStorage.setItem("name", "value");
localStorage.getItem("name"); // => 'value'
localStorage.removeItem("name");
localStorage.clear(); // 删除所有数据
sessionStorage.setItem("name", "value");
sessionStorage.setItem("name");
sessionStorage.setItem("name");
sessionStorage.clear();
了解跨域么 跨域的几种方式,你在项目中都是怎么解决跨域的
跨域,是指浏览器不能执行其他网站的脚本。它是由浏览器的同源策略(协议,端口,域名相同)造成的,是浏览器对 JavaScript 实施的安全限制。
常用解决方案 jsonp 跨域 原理是利用 script 标签没有跨域限制 仅支持 get 请求 创建 script 标签设置 script 标签的 src 属性,以问号传递参数,设置好回调函数 callback 名称插入 html 文本中调用回调函数,res 参数就是获取的数据 CORS CORS 需要浏览器和后端同时支持 nginx 代理跨域
了解重排和重绘么 怎么避免
重排: 部分渲染树(或者整个渲染树)需要重新分析并且节点尺寸需要重新计算,表现为重新生成布局,重新排列元素 重绘: 由于节点的几何属性发生改变或者由于样式发生改变,例如改变元素背景色时,屏幕上的部分内容需要更新,表现为某些元素的外观被改变
单单改变元素的外观,肯定不会引起网页重新生成布局,但当浏览器完成重排之后,将会重新绘制受到此次重排影响的部分 重排和重绘代价是高昂的,它们会破坏用户体验,并且让 UI 展示非常迟缓,而相比之下重排的性能影响更大,在两者无法避免的情况下,一般我们宁可选择代价更小的重绘。 『重绘』不一定会出现『重排』,『重排』必然会出现『重绘』。
任何改变用来构建渲染树的信息都会导致一次重排或重绘:比如以下操作
添加、删除、更新 DOM 节点 通过 display: none 隐藏一个 DOM 节点-触发重排和重绘 通过 visibility: hidden 隐藏一个 DOM 节点-只触发重绘,因为没有几何变化 移动或者给页面中的 DOM 节点添加动画 添加一个样式表,调整样式属性 用户行为,例如调整窗口大小,改变字号,或者滚动。
javaScript
js 的基本类型有哪几种,和复杂类型有什么区别
基本类型有: null,undefind,string,number,boolean,symbol,bigInt, typeOf null 返回的是Object
复杂数据有 Object,Array,Function
区别: 1.内存分配不同, 基本数据类型存储在栈内存,存储的是值,复杂数据类型的值存储在堆内存
2. 访问机制不同,基本按值访问,复杂按引用访问,js不允许直接访问保存在堆内存的对象,首先得到的是这个对象在堆内存中的地址 然后根据地址获取到对应的值
数组种你有哪些常用的方法 哪些会改变原数组
修改原数组的有 splice/reverse/fill/copyWithin/sort/push/pop/unshift/shift
不修改原数组的有 slice/map/forEach/every/filter/reduce/entries/find
什么是类数组和数组的区别是什么
类数组:拥有length属性,其他属性为非负整数,不具有数组的方法
区别:类数组是一个普通对象 常见的类数组有 函数的参数 arguments, DOM 对象列表(比如通过 document.querySelectorAll 得到的列表), jQuery 对象 (比如 $("div")). 真是的数组是Array类型
类数组转化为数组的方法
```js
// 第一种
Array.form(fakeArr)
// 第二种 结构赋值
[...fakeArr]
// 第三种
Array.prototype.slice.call(fakeArr)
```
实现数据去重
-
ES6 set 方法
js const newArr = [...new Set(arr)] -
filter 方法
function unique1(arr) { const newArr = arr.filter((item, index, arr) => { return arr.indexOf(item) === index; }); return newArr; }- reduce 方法
function unique2(arr) { const newArr = arr.reduce((pre, cur) => { pre.includes(cur) ? pre : [...pre, cur]; }, []); return newArr; }-
利用对象的键唯一性
function unique3(arr) { let obj = {}; const newArr = arr.filter((item) => { return obj.hasOwnProperty(item) ? false : (item] = true); }); return newArr }
map 和 set 有什么区别
Map 对象是键值对集合,和 JSON 对象类似,但是 key 不仅可以是字符串还可以是其他各种类型的值包括对象都可以成为 Map 的键 Set 对象类似于数组,且成员的值都是唯一的
for in 和 for of 区别
for of 语法遍历的是数组元素的值 for in 遍历的是索引 for of 遍历的只是数组内的元素,而不包括数组的原型属性 method 和索引 name
for..of 适用遍历数/数组对象/字符串/map/set 等拥有迭代器对象的集合,不能遍历对象,因为没有迭代对象,与 forEach()不同的是,它可以正确响应 break、continue 和 return 语句。 for in 可以遍历一个普通的对象,这样也是它的本质工作,for in 会遍历原型以及可枚举属性,最好的情况下,使用 hasOwnProperty 判断是不是实例属性。
说几种数组去重方案
了解变量提升么? 什么是暂时性死区
只有声明本身会被提升,而赋值操作不会被提升。
变量会提升到其所在函数的最上面,而不是整个程序的最上面。
函数声明会被提升,但函数表达式不会被提升。
func(); // 1
var func;
function func() {
console.log(1);
}
func = function () {
console.log(2);
};
//这个 输出1,不会输出2。函数声明和变量声明都会被提升,但是需要注意的是函数会先被提升,然后才是变量。var func;尽管出现在function func()之前,但它是重复的声明,会被忽略,因为函数声明会被提升到普通变量之前。 上面等同于
function func() {
console.log(1);
}
func(); // 1
func = function () {
console.log(2);
};
暂时性死区 即在代码块内,使用 let/const 命令声明变量之前,该变量都是不可用的(会抛出错误)。这在语法上,称为“暂时性死区”。
理解 js 的执行上下文么
执行上下文就是当前 JavaScript 代码被解析和执行时所在环境, JS 执行上下文栈可以认为是一个存储函数调用的栈结构,遵循先进后出的原则。
JavaScript 执行在单线程上,所有的代码都是排队执行。 一开始浏览器执行全局的代码时,首先创建全局的执行上下文,压入执行栈的顶部。 每当进入一个函数的执行就会创建函数的执行上下文,并且把它压入执行栈的顶部。当前函数执行-完成后,当前函数的执行上下文出栈,并等待垃圾回收。 浏览器的 JS 执行引擎总是访问栈顶的执行上下文。 全局上下文只有唯一的一个,它在浏览器关闭时出栈。
理解 js 的时间循环机制么 (eventLoop)
event loop 它最主要是分三部分:主线程、宏队列(macrotask)、微队列(microtask) js 的任务队列分为同步任务和异步任务,所有的同步任务都是在主线程里执行的,异步任务可能会在 macrotask 或者 microtask 里面 主线程 就是访问到的 script 标签里面包含的内容,或者是直接访问某一个 js 文件的时候,里面的可以在当前作用域直接执行的所有内容(执行的方法,new 出来的对象等)
宏队列(macrotask) setTimeout、setInterval、setImmediate、I/O、UI rendering
微队列(microtask) promise.then、process.nextTick
执行顺序 1、先执行主线程
2、遇到宏队列(macrotask)放到宏队列(macrotask)
3、遇到微队列(microtask)放到微队列(microtask)
4、主线程执行完毕
5、执行微队列(microtask),微队列(microtask)执行完毕
6、执行一次宏队列(macrotask)中的一个任务,执行完毕
7、执行微队列(microtask),执行完毕
8、依次循环。。。
上面出来的扩展问题 1.setTimeout(()=>{ console.log(1)},1000) 一定会在一秒后执行么 如果不会请说明原因
setTimeout() 只是将事件插入了“任务队列”,必须等当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码消耗时间很长,也有可能要等很久,所以并没办法保证回调函数一定会在 setTimeout() 指定的时间执行。所以, setTimeout() 的第二个参数表示的是最少时间,并非是确切时间。
什么是闭包,闭包的作用是什么
闭包是指有权访问另一个函数作用域中的变量的函数,创建闭包最常用的方式就是在一个函数内部创建另一个函数。
闭包的作用有:
封装私有变量 模仿块级作用域(ES5 中没有块级作用域) 实现 JS 的模块
谈谈你对原型及原型链的理解
在 JavaScript 中,每当定义一个对象(函数也是对象)时候,对象中都会包含一些预定义的属性。其中每个函数对象都有一个 prototype 属性,这个属性指向函数的原型对象。使用原型对象的好处是所有对象实例共享它所包含的属性和方法。
原型链:每个对象拥有一个原型对象,通过 proto 指针指向其原型对象,并从中继承方法和属性,同时原型对象也可能拥有原型,这样一层一层,最终指向 null(Object.prototype.proto 指向的是 null)。这种关系被称为原型链 (prototype chain),通过原型链一个对象可以拥有定义在其他对象中的属性和方法。
prototype 是构造函数的属性。
proto 是每个实例都有的属性,可以访问 [[prototype]] 属性。
实例的proto 与其构造函数的 prototype 指向的是同一个对象。
graph LR start("构造函数 parent") --protytype--> handler("原型 parent.protytype") handler --constructor--> start("构造函数 parent") start --new--> p('实例对象 p) p --proto--> handler
原型链主要解决了什么问题
原型链主要解决了继承问题 ES5 最佳的继承方式及寄生组合式继承 即通过接用构造函数来继承属性,通过原型链的混成形式来继承方法
主要实现方式
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.getName = function () {
return this.name;
};
function SuberType(name, age) {
SuperType.call(this, name);
this.age = age;
}
//寄生组合式继承
SuberType.prototype = Object.create(SuperType.prototype);
SuberType.prototype.constructor = SuberType;
SuberType.prototype.getAge = function () {
return this.age;
};
let girl = new SuberType("Yvette", 18);
girl.getName();
经常用 ES6 能列举出来 ES6 有哪些新特性么
新增了块级作用域(let,const) 提供了定义类的语法糖(class) 新增了一种基本数据类型(Symbol) 新增了变量的解构赋值 函数参数允许设置默认值,引入了 rest 参数,新增了箭头函数 数组新增了一些 API,如 isArray / from / of 方法;数组实例新增了 entries(),keys() 和 values() 等方法 对象和数组新增了扩展运算符 ES6 新增了模块化(import/export) ES6 新增了 Set 和 Map 数据结构 ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例 ES6 新增了生成器(Generator)和遍历器(Iterator)
了解 Promise 么 promise 有哪几种状态,promise.catch()后还能执行.then()方法么 .then(resolve,reject)接收的两个参数 第二个参数和 catch 有什么区别, 如果让你手动实现一个 promise 你会怎么做
1.promise 有三种状态: fulfilled, rejected, pending 2.可以 catch 也会返回一 Promise() 所以可以继续执行 then() 如果不想执行后续的 then catch 中可以返回 Promise.reject() 3.reject 是用来抛出异常的,catch 是用来处理异常的;reject 是 Promise 的方法,而 then 和 catch 是 Promise 实例的方法(Promise.prototype.then 和 Promise.prototype.catch)。 主要区别就是,如果在 then 的第一个函数里抛出了异常,后面的 catch 能捕获到,而 then 的第二个函数捕获不到。
// 判断变量否为function
const isFunction = (variable) => typeof variable === "function";
// 定义Promise的三种状态常量
const PENDING = "PENDING";
const FULFILLED = "FULFILLED";
const REJECTED = "REJECTED";
// 基本版
class Promise {
constructor(executor) {
this.status = PENDING; // 宏变量, 默认是等待态
this.value = undefined; // then方法要访问到所以放到this上
this.reason = undefined; // then方法要访问到所以放到this上
let resolve = (value) => {
if (this.status === PENDING) {
// 保证只有状态是等待态的时候才能更改状态
this.value = value;
this.status = RESOLVED;
}
};
let reject = (reason) => {
if (this.status === PENDING) {
this.reason = reason;
this.status = REJECTED;
}
};
// 执行executor传入我们定义的成功和失败函数:把内部的resolve和reject传入executor中用户写的resolve, reject
try {
executor(resolve, reject);
} catch (e) {
console.log("catch错误", e);
reject(e); //如果内部出错 直接将error手动调用reject向下传递
}
}
then(onfulfilled, onrejected) {
if (this.status === RESOLVED) {
onfulfilled(this.value);
}
if (this.status === REJECTED) {
onrejected(this.reason);
}
}
}
// 建议升级 加上异步
class Promise {
constructor(executor) {
this.status = PENDING; // 宏变量, 默认是等待态
this.value = undefined; // then方法要访问到所以放到this上
this.reason = undefined; // then方法要访问到所以放到this上
this.onResolvedCallbacks = []; // 专门存放成功的回调函数
this.onRejectedCallbacks = []; // 专门存放成功的回调函数
let resolve = (value) => {
if (this.status === PENDING) {
// 保证只有状态是等待态的时候才能更改状态
this.value = value;
this.status = RESOLVED;
// 需要让成功的方法依次执行
this.onResolvedCallbacks.forEach((fn) => fn());
}
};
let reject = (reason) => {
if (this.status === PENDING) {
this.reason = reason;
this.status = REJECTED;
// 需要让失败的方法依次执行
this.onRejectedCallbacks.forEach((fn) => fn());
}
};
// 执行executor传入我们定义的成功和失败函数:把内部的resolve和reject传入executor中用户写的resolve, reject
try {
executor(resolve, reject);
} catch (e) {
console.log("catch错误", e);
reject(e); //如果内部出错 直接将error手动调用reject向下传递
}
}
then(onfulfilled, onrejected) {
if (this.status === RESOLVED) {
onfulfilled(this.value);
}
if (this.status === REJECTED) {
onrejected(this.reason);
}
// 处理异步的情况
if (this.status === PENDING) {
// this.onResolvedCallbacks.push(onfulfilled); 这种写法可以换成下面的写法,多包了一层,这叫面向切片编程,可以加上自己的逻辑
this.onResolvedCallbacks.push(() => {
// TODO ... 自己的逻辑
onfulfilled(this.value);
});
this.onRejectedCallbacks.push(() => {
// TODO ... 自己的逻辑
onrejected(this.reason);
});
}
}
}
// 完全版
class MyPromise {
constructor(handle) {
if (!isFunction(handle)) {
throw new Error("MyPromise must accept a function as a parameter");
}
// 添加状态
this._status = PENDING;
// 添加状态
this._value = undefined;
// 添加成功回调函数队列
this._fulfilledQueues = [];
// 添加失败回调函数队列
this._rejectedQueues = [];
// 执行handle
try {
handle(this._resolve.bind(this), this._reject.bind(this));
} catch (err) {
this._reject(err);
}
}
// 添加resovle时执行的函数
_resolve(val) {
const run = () => {
if (this._status !== PENDING) return;
// 依次执行成功队列中的函数,并清空队列
const runFulfilled = (value) => {
let cb;
while ((cb = this._fulfilledQueues.shift())) {
cb(value);
}
};
// 依次执行失败队列中的函数,并清空队列
const runRejected = (error) => {
let cb;
while ((cb = this._rejectedQueues.shift())) {
cb(error);
}
};
/* 如果resolve的参数为Promise对象,则必须等待该Promise对象状态改变后,
当前Promsie的状态才会改变,且状态取决于参数Promsie对象的状态
*/
if (val instanceof MyPromise) {
val.then(
(value) => {
this._value = value;
this._status = FULFILLED;
runFulfilled(value);
},
(err) => {
this._value = err;
this._status = REJECTED;
runRejected(err);
}
);
} else {
this._value = val;
this._status = FULFILLED;
runFulfilled(val);
}
};
// 为了支持同步的Promise,这里采用异步调用
setTimeout(run, 0);
}
// 添加reject时执行的函数
_reject(err) {
if (this._status !== PENDING) return;
// 依次执行失败队列中的函数,并清空队列
const run = () => {
this._status = REJECTED;
this._value = err;
let cb;
while ((cb = this._rejectedQueues.shift())) {
cb(err);
}
};
// 为了支持同步的Promise,这里采用异步调用
setTimeout(run, 0);
}
// 添加then方法
then(onFulfilled, onRejected) {
const { _value, _status } = this;
// 返回一个新的Promise对象
return new MyPromise((onFulfilledNext, onRejectedNext) => {
// 封装一个成功时执行的函数
let fulfilled = (value) => {
try {
if (!isFunction(onFulfilled)) {
onFulfilledNext(value);
} else {
let res = onFulfilled(value);
if (res instanceof MyPromise) {
// 如果当前回调函数返回MyPromise对象,必须等待其状态改变后在执行下一个回调
res.then(onFulfilledNext, onRejectedNext);
} else {
//否则会将返回结果直接作为参数,传入下一个then的回调函数,并立即执行下一个then的回调函数
onFulfilledNext(res);
}
}
} catch (err) {
// 如果函数执行出错,新的Promise对象的状态为失败
onRejectedNext(err);
}
};
// 封装一个失败时执行的函数
let rejected = (error) => {
try {
if (!isFunction(onRejected)) {
onRejectedNext(error);
} else {
let res = onRejected(error);
if (res instanceof MyPromise) {
// 如果当前回调函数返回MyPromise对象,必须等待其状态改变后在执行下一个回调
res.then(onFulfilledNext, onRejectedNext);
} else {
//否则会将返回结果直接作为参数,传入下一个then的回调函数,并立即执行下一个then的回调函数
onFulfilledNext(res);
}
}
} catch (err) {
// 如果函数执行出错,新的Promise对象的状态为失败
onRejectedNext(err);
}
};
switch (_status) {
// 当状态为pending时,将then方法回调函数加入执行队列等待执行
case PENDING:
this._fulfilledQueues.push(fulfilled);
this._rejectedQueues.push(rejected);
break;
// 当状态已经改变时,立即执行对应的回调函数
case FULFILLED:
fulfilled(_value);
break;
case REJECTED:
rejected(_value);
break;
}
});
}
// 添加catch方法
catch(onRejected) {
return this.then(undefined, onRejected);
}
// 添加静态resolve方法
static resolve(value) {
// 如果参数是MyPromise实例,直接返回这个实例
if (value instanceof MyPromise) return value;
return new MyPromise((resolve) => resolve(value));
}
// 添加静态reject方法
static reject(value) {
return new MyPromise((resolve, reject) => reject(value));
}
// 添加静态all方法
static all(list) {
return new MyPromise((resolve, reject) => {
/**
* 返回值的集合
*/
let values = [];
let count = 0;
for (let [i, p] of list.entries()) {
// 数组参数如果不是MyPromise实例,先调用MyPromise.resolve
this.resolve(p).then(
(res) => {
values[i] = res;
count++;
// 所有状态都变成fulfilled时返回的MyPromise状态就变成fulfilled
if (count === list.length) resolve(values);
},
(err) => {
// 有一个被rejected时返回的MyPromise状态就变成rejected
reject(err);
}
);
}
});
}
// 添加静态race方法
static race(list) {
return new MyPromise((resolve, reject) => {
for (let p of list) {
// 只要有一个实例率先改变状态,新的MyPromise的状态就跟着改变
this.resolve(p).then(
(res) => {
resolve(res);
},
(err) => {
reject(err);
}
);
}
});
}
finally(cb) {
return this.then(
(value) => MyPromise.resolve(cb()).then(() => value),
(reason) =>
MyPromise.resolve(cb()).then(() => {
throw reason;
})
);
}
}
instanceOf 原理是什么
instanceOf 是通过原型链判断的,比如 A instanceOf B, 在 A 的原型链中层层查找,是否有原型等于 B.prototype 果一直找到 A 的原型链的顶端(null;即 Object.proptotype.proto),仍然不等于 B.prototype,那么返回 false,否则返回 true.
手动实现
function Myinstanceof(leftValue, rightValue) {
let rightValue = rightValue.prototype; // 取得当前类的原型
let proto = Object.getPrototypeOf(leftValue); // 取得当前实例对象的原型链上的属性 等同于 LeftValue.__proto__
while (true) {
if (proto === null) {
// 找到了 Object的基类 Object.prototype.__proto__
return false;
}
if (proto === rightValue) {
// 在当前实例对象的原型链上,找到了当前类
return true;
}
proto = Object.getPrototypeOf(proto); // 沿着原型链__ptoto__一层一层向上查找
}
}
new 的原理是什么
创建一个新对象,这个对象的proto要指向构造函数的原型对象 执行构造函数 返回值为 object 类型则作为 new 方法的返回值返回,否则返回上述全新对象
function Mynew() {
let obj = {}; // 创建一个新的对象
let [constructor, ...args] = [...arguments]; // 获取传递过来的实例 及 fn
obj.__proto__ = constructor.prototype; //把obj的__proto__指向fn的prototype,实现继承
let result = constructor.apply(obj, ...args); // 把Fn当做普通函数执行,并改变this指向
if ((result && typeof result === "function") || typeof result === "object") {
// 分析函数的返回值
return result;
}
return obj;
}
call 和 apply 原理 怎么实现
call 和 apply 的作用就是当执行一个方法的时候希望能够使用另一个对象来作为作用域对象,简单来说就是当我执行 A 方法的时候,希望通过传入参数的形式将一个对象 B 传进去,用以将 A 方法的作用域对象替换为对象 B
改变 this 指向,唯一区别就是传递参数不同
// 实现call
Function.prototype.call = function (context, ...args) {
// null,undefined,和不传时,context为 window
context = context == null ? window : context;
// 必须保证 context 是一个对象类型
let contextType = typeof context;
if (!/^(object|function)$/i.test(contextType)) {
// context = new context.constructor(context); // 不适用于 Symbol/BigInt
context = Object(context);
}
let result;
context["fn"] = this; // 把函数作为对象的某个成员值
result = context["fn"](...args); // 把函数执行,此时函数中的this就是
delete context["fn"]; // 设置完成员属性后,删除
return result;
};
// 实现apply
Function.prototype.apply = function (context, args) {
context = context == null ? window : context;
let contextType = typeof context;
if (!/^(object|function)$/i.test(contextType)) {
context = Object(context);
}
let result;
context["fn"] = this;
result = context["fn"](...args);
delete context["fn"];
return result;
};
防抖和节流的作用是什么 原理是什么, 能不能手动实现
防抖和节流的作用都是防止函数多次调用。区别在于,假设一个用户一直触发这个函数,且每次触发函数的间隔小于设置的时间,防抖的情况下只会调用一次,而节流的情况会每隔一定时间调用一次函数。
防抖原理 定一个 timer timer 内函数只会执行一次
function debounce(fn, delay) {
let timer = null;
return function (...args) {
let context = this;
if (timer) clearTimeout(timer);
timer = setTimeout(function () {
fn.apply(context, args);
}, delay);
};
}
节流原理 再间隔时间内就只会执行一次
function throttle(fn, delay) {
let flag = true,
timer = null;
return function (...args) {
let context = this;
if (!flag) return;
flag = false;
clearTimeout(timer);
timer = setTimeout(function () {
fn.apply(context, args);
flag = true;
}, delay);
};
}
什么是深拷贝 和浅拷贝有什么区别
浅拷贝是指只复制第一层对象,但是当对象的属性是引用类型时,实质复制的是其引用,当引用指向的值改变时也会跟着变化。 深拷贝复制变量值,对于非基本类型的变量,则递归至基本类型变量后,再复制。深拷贝后的对象与原来的对象是完全隔离的,互不影响,对一个对象的修改并不会影响另一个对象。
实现一个深拷贝
function deepClone(obj) {
const cloneObj = obj.constructor === Array ? [] : {}; // 判断要克隆的数据是对象还是数组
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
// 判断key是否为自身的属性(let in 有时候会把原型上的属性也遍历出来,下面会介绍)
if (obj[key] && typeof obj[key] === "object") {
// 判断该属性是否是对象或者数组
cloneObj[key] = obj[key].constructor === Array ? [] : {};
cloneObj[key] = deepClone(obj[key]); // 递归调用 (自己内部调用自己)
} else {
cloneObj[key] = obj[key];
}
}
}
return cloneObj;
}
实现函数函数柯里化
函数柯里化就是把接受「多个参数」的函数变换成接受一个「单一参数」的函数,并且返回接受「余下参数」返回结果的一种应用。 所以我们可以首先判断传递的参数是否达到执行函数的 fn 个数如果没有达到的话 继续返回新的函数 并返回 curry 函数传递剩余参数
function curry(fn, ...args) {
fn.length > args.length
? (...arguments) => curry(fn, ...args, ...arguments)
: fn(...args);
}
TypeScript
ts 中怎么为对象动态分配属性
interface LooseObject {
[key: string]: any;
}
你怎么理解泛型
function identity<T>(value: T): T {
return value;
}
参考上面的方法,当我们调用 identity(1) ,Number 类型就像参数 1 一样,它将在出现 T 的任何位置填充该类型。图中 内部的 T 被称为类型变量,它是我们希望传递给 identity 函数的类型占位符,同时它被分配给 value 参数用来代替它的类型:此时 T 充当的是类型,而不是特定的 Number 类型。 即泛型允许程序员在强类型程序设计语言中编写代码时使用一些以后才指定的类型,在实例化时作为参数指明这些类型。
interface 和 type 有什么区别
interface 和 type 都可以用来描述对象的形状或函数签名:
// interface
interface Point {
x: number;
y: number;
}
interface SetPoint {
(x: number, y: number): void;
}
// type
type Point = {
x: number,
y: number,
};
type SetPoint = (x: number, y: number) => void;
// 类型别名可以用于一些其他类型,比如原始类型、联合类型和元组:
// primitive
type Name = string;
// object
type PartialPointX = { x: number };
type PartialPointY = { y: number };
// union
type PartialPoint = PartialPointX | PartialPointY;
// tuple
type Data = [number, string];
interface 和 type(类型别名) 扩展类型方式不一样
// interface
interface PartialPointX {
x: number;
}
interface Point extends PartialPointX {
y: number;
}
// type
type PartialPointX = { x: number };
type Point = PartialPointX & { y: number };
ts 中有哪些高级类型
- 交叉类型(&) 交叉类型说简单点就是将多个类型合并成一个类型,其语法规则和逻辑 “与” 的符号一致。
- 联合类型(|) 联合类型的语法规则和逻辑 “或” 的符号一致,表示其类型为连接的多个类型中的任意一个。
- 类型别名(type) 类型别名与声明变量的语法类似,只需要把 const、let 换成 type 关键字即可。
- 类型索引(keyof) keyof 类似于 Object.keys ,用于获取一个接口中 Key 的联合类型。
- 类型约束(extends) 泛型内使用的主要作用是对泛型加以约束。
- 类型映射(in) n 关键词的作用主要是做类型的映射,遍历已有接口的 key 或者是遍历联合类型。
- 条件类型(T ? X : Y) 条件类型的语法规则和三元表达式一致,经常用于一些类型不确定的情况。
ts 中你用过哪些工具泛型 怎么使用
TypesScript 中内置了很多工具泛型,除了 Readonly、Extract 这两种,内置的泛型在 TypeScript 内置的 lib.es5.d.ts 中都有定义,所以不需要任何依赖都是可以直接使用的。下面看看一些经常使用的工具泛型吧。
- Partial
type Partial<T> = {
[P in keyof T]?: T[P]
}
Partial 用于将一个接口的所有属性设置为可选状态,首先通过 keyof T,取出类型变量 T 的所有属性,然后通过 in 进行遍历,最后在属性后加上一个 ?。 我们通过 TypeScript 写 React 的组件的时候,如果组件的属性都有默认值的存在,我们就可以通过 Partial 将属性值都变成可选值。
interface ButtonProps {
type: 'button' | 'submit' | 'reset'
text: string
disabled: boolean
onClick: () => void
}
const render = (props: Partial<ButtonProps> = {}) => {return null})
- Required 作用与 Partial 相反
- Record
js type Record<K extends keyof any, T> = { [P in K]: T }Record 接受两个类型变量,Record 生成的类型具有类型 K 中存在的属性,值为类型 T。这里有一个比较疑惑的点就是给类型 K 加一个类型约束,extends keyof any 我们在业务代码中经常会构造某个对象的数组,但是数组不方便索引,所以我们有时候会把对象的某个字段拿出来作为索引,然后构造一个新的对象。假设有个商品列表的数组,要在商品列表中找到商品名为 某某某的商品,我们一般通过遍历数组的方式来查找,比较繁琐,为了方便,我们就会把这个数组改写成对象。
interface Goods {
id: string
name: string
price: string
image: string
}
const goodsMap: Record<string, Goods> = {}
const goodsList: Goods[] = await fetch('server.com/goods/list')
goodsList.forEach(goods => {
goodsMap[goods.name] = goods
})
- Pick
js type Pick<T, K extends keyof T> = { [P in K]: T }Pick 主要用于提取接口的某几个属性。做过 Todo 工具的同学都知道,Todo 工具只有编辑的时候才会填写描述信息,预览的时候只有标题和完成状态,所以我们可以通过 Pick 工具,提取 Todo 接口的两个属性,生成一个新的类型 TodoPreview。
interface Todo {
title: string
completed: boolean
description: string
}
type TodoPreview = Pick<Todo, "title" | "completed">
const todo: TodoPreview = {
title: 'Clean room',
completed: false
}
- Omit
js type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>与 Pick 相反
React
React 中的 keys 的作用是什么 为什么必须加 key
在开发过程中,我们需要保证某个元素的 key 在其同级元素中具有唯一性,在 ReactDiff 算法中 React 会借助元素的 Key 值来判断该元素是新创建的还是被移动而来的元素,React 会保存这个辅助状态,从而减少不必要的元素渲染.此外,React 还需要借助 Key 值来判断元素与本地状态的关联状态
react 利用 key 来识别组件,它是一种身份标识标识,相同的 key react 认为是同一个组件,这样后续相同的 key 对应组件都不会被创建 有了 key 属性后,就可以与组件建立了一种对应关系,react 根据 key 来决定是销毁重新创建组件还是更新组件。 key 相同,若组件属性有所变化,则 react 只更新组件对应的属性;没有变化则不更新。 key 值不同,则 react 先销毁该组件(有状态组件的 componentWillUnmount 会执行),然后重新创建该组件(有状态组件的 constructor 和 componentWillUnmount 都会执行)
我们在调用 setState 后发生了什么,如果多次调用会触发几次 render
在代码中调用 setState 函数之后,React 会将传入的参数对象与组件当前的状态合并,然后触发所谓的调和过程。 经过调和过程,React 会以相对高效的方式根据新的状态构建 React 元素树并且着手重新渲染整个 UI 界面。 在 React 得到元素树之后,React 会自动计算出新的树与老树的节点差异,然后根据差异对界面进行最小化重渲染。 在差异计算算法中,React 能够相对精确地知道哪些位置发生了改变以及应该如何改变,这就保证了按需更新,而不是全部重新渲染。
多次 setState 会合并为一次 render,因为 setState 并不会立即改变 state 的值,而是将其放到一个任务队列里,最终将多个 setState 合并,一次性更新页面。 所以我们可以在代码里多次调用 setState,每次只需要关注当前修改的字段即可。
setState 在什么时候是同步的什么时候是异步的 为什么
setState 在 setTimeout 或者原生事件中是同步的 在 React 方法内是异步的
假如所有的 setState 是同步的 那么我们每次 setState 都会对 dom 进行修改 加重了性能负担 如果是异步则可以将多个 setState 合并 一次性更新页面
在 useEffect 中 setState 我们能能立即拿到更新的值么
不行 理由同上
既然 setState 是异步的 那么我们想立即拿到这个更新的值 该怎么办
可以使用 setState 第二个参数 传入一个 callback 获取到最新的值
react 15 的生命周期知道么
组件在进入和离开 DOM 时要经历一系列生命周期方法,下面是这些生命周期方法。 componentWillMount() 在渲染前调用,在客户端也在服务端,它只发生一次。 componentDidMount() 在第一次渲染后调用,只在客户端。之后组件已经生成了对应的 DOM 结构,可以通过 this.getDOMNode()来进行访问。 如果你想和其他 JavaScript 框架一起使用,可以在这个方法中调用 setTimeout, setInterval 或者发送 AJAX 请求等操作(防止异部操作阻塞 UI)。 componentWillReceiveProps() 在组件接收到一个新的 prop (更新后)时被调用。这个方法在初始化 render 时不会被调用。 shouldComponentUpdate() 返回一个布尔值。在组件接收到新的 props 或者 state 时被调用。在初始化时或者使用 forceUpdate 时不被调用。 可以在你确认不需要更新组件时使用。 componentWillUpdate() 在组件接收到新的 props 或者 state 但还没有 render 时被调用。在初始化时不会被调用。 componentDidUpdate() 在组件完成更新后立即调用。在初始化时不会被调用。 componentWillUnMount() 组件从 DOM 中移除的时候立刻被调用。 getDerivedStateFromError() 这个生命周期方法在 ErrorBoundary 类中使用。实际上,如果使用这个生命周期方法,任何类都会变成 ErrorBoundary。这用于在组件树中出现错误时呈现回退 UI,而不是在屏幕上显示一些奇怪的错误。 componentDidCatch() 这个生命周期方法在 ErrorBoundary 类中使用。实际上,如果使用这个生命周期方法,任何类都会变成 ErrorBoundary。这用于在组件树中出现错误时记录错误。
React 的组件通信方式
父=>子 props 子=>父 propS+回调 跨级组件 props 或者 context
知道 React 合成事件么 他和原生事件有什么区别
React 合成事件和原生事件区别 React 合成事件一套机制:React 并不是将 click 事件直接绑定在 dom 上面,而是采用事件冒泡的形式冒泡到 document 上面,然后 React 将事件封装给正式的函数处理运行和处理。
React 合成事件理解 如果 DOM 上绑定了过多的事件处理函数,整个页面响应以及内存占用可能都会受到影响。React 为了避免这类 DOM 事件滥用,同时屏蔽底层不同浏览器之间的事件系统差异,实现了一个中间层——SyntheticEvent。
当用户在为 onClick 添加函数时,React 并没有将 Click 时间绑定在 DOM 上面。 而是在 document 处监听所有支持的事件,当事件发生并冒泡至 document 处时,React 将事件内容封装交给中间层 SyntheticEvent(负责所有事件合成) 所以当事件触发的时候,对使用统一的分发函数 dispatchEvent 将指定函数执行。
React 做过哪些性能优化
1)对于正常的项目优化,一般都涉及到几个方面,开发过程中、上线之后的首屏、运行过程的状态。
首屏优化一般涉及到几个指标 FP、FCP,FMP;要有一个良好的体验是尽可能的把 FCP 提前,需要做一些工程化的处理,去优化资源的加载
实际开发过程中
- 保证数据的唯一性
- 使用唯一的 key 迭代
- 尽量不在 render 中处理数据
- 大型页面使用 React.memo()包裹优化
- 尽量细分组件