前端面试题记录(一)

CSS_水平垂直居中

  • 绝对定位方式(注意子绝父相)

  • 不确定div的宽高的时候

    • left:50% top:50% 和 transfrom:translate(-50%,50%)的方式实现
      • 如果确定了当前div的宽度,利用margin负值的方法实现
      • 在绝对定位下给top left right bottom 都设置为0 配合margin:auto实现
      • calc() 函数动态计算实现水平垂直居中
  • flex布局

    • 父级 设置display:flex

      • 主轴 justify-content:center
      • 侧轴 align-content:center
      • display:table-cell方法 文字
    • table-cell实现水平垂直居中:

      • 父级 display:table-cell vertical-align:middle text -align:center
      • 子div display:inline-block

CSS_BFC

  • BFC是块级格式上下文 有BFC特性的元素可以看作一个不会在布局上影响外部的独立容器
  • 触发BFC 有body根元素 浮动 绝对定位元素 display为行内块 table-cells flex的元素 overflow除了visible(默认值)以外的值
  • 比如会应用在解决外边距折叠问题 清除浮动 阻止元素被浮动元素覆盖

CSS_displaynone

display:none;

  1. DOM 结构:浏览器不会渲染 display 属性为 none 的元素,不占据空间;
  2. 事件监听:无法进行 DOM 事件监听;
  3. 性能:动态改变此属性时会引起重排,性能较差;
  4. 继承:不会被子元素继承,毕竟子类也不 会被渲染;
  5. transition:transition 不支持 display。

visibility: hidden;

  1. DOM 结构:元素被隐藏,但是会被渲染不会消失,占据空间;
  2. 事件监听:无法进行 DOM 事件监听;
  3. 性 能:动态改变此属性时会引起重绘,性能较高;
  4. 继 承:会被子元素继承,子元素可以通过设置 visibility: visible; 来取消隐藏;
  5. transition:visibility 会立即显示,隐藏时会延时

opacity: 0;

  1. DOM 结构:透明度为 100%,元素隐藏,占据空间;
  2. 事件监听:可以进行 DOM 事件监听;
  3. 性 能:提升为合成层,不会触发重绘,性能较高;
  4. 继 承:会被子元素继承,且,子元素并不能通过 opacity: 1 来取消隐藏;
  5. transition:opacity 可以延时显示和隐藏

JS_数组扁平化

  1. es6提供的API flat(depth)
1
2
3
let a = [1,[2,3]];  
a.flat(); // [1,2,3]
a.flat(1); //[1,2,3]

2.for 循环的方式

1
2
3
4
5
6
7
8
9
10
11
12
 function flatten(arr){
var res = []
for(let i = 0;i<arr.length;i++){
if(Array.isArray(arr[i])){
res = res.concat(flatten(arr[i]))
//res.push(...flatten(arr[i])); //扩展运算符
} else {
res.push(arr[i])
}
}
return res
}

3.while 循环的方式

1
2
3
4
5
6
function flatten(arr){
while(arr.some(item=>Array.isArray(item))){
arr = [].concat(...arr)
}
return arr
}
  1. 如果数组项都为数字 那么可以使用join() toString()等方式
1
2
3
4
5
function flatten(arr){
//需要map是因为split转成的数组中元素为字符串 所以要重新转为数字
return arr.toString().split(",").map(item=>Number(item))
//return arr.join().split(",").map(item=>Number(item))
}

5.使用reduce方法

1
2
3
4
5
6
function flatten(arr){
return arr.reduce((res,next)=>{
return res.concat(Array.isArray(next)?flatten(next):next)
},[])
//第二个参数是一个空数组,也是作为遍历的开始。(res)
}

JS_判断元素是否是数组

  • 首先排除typeof typeof返回的是Object

  • 使用instanceof判断

    • instanceof 运算符可以用来判断某个构造函数的prototype属性指向的对象是否存在于另外一个要检测对象的原型链上 object instanceof constructor
    • 实际上用instanceof判断就可以理解成判断一个Object 是否是一个数组 (实际上在js中 数组也是一种对象)如果在这个Object的原型链上能够找到Array构造函数 ,那么这个Object应该就是一个数组,如果这个Object的原型链上只能找到Object构造函数的话,那么它就不是一个数组。
    1
    2
    3
    4
    5
    const arr = [];
    const obj = {};
    console.log(arr instanceof Array);//true
    console.log(arr instanceof Object);//true,在数组的原型链上也能找到Object构造函数
    console.log(obj instanceof Array);//false
    • 这种方法可以来判断该元素是否是数组 然而存在弊端
      • 使用 instanceof 的问题是假定只有一个全局执行上下文。如果网页里有多个框架,则可能涉及两
        个不同的全局执行上下文,因此就会有两个不同版本的 Array 构造函数。如果要把数组从一个框架传
        给另一个框架,则这个数组的构造函数将有别于在第二个框架内本地创建的数组。
  • 为了解决instanceof的问题 ECMAScript 提供了**Array.isArray()**的方法

1
2
let a = []
Array.isArray(a) // true

如果考虑兼容性 那么我们可以加一层判断

1
2
3
4
5
if (!Array.isArray) {
Array.isArray = function(arg) {
return Object.prototype.toString.call(arg) === '[object Array]';
};
}
  • 使用Object的toString方法判断

    • 如果一个对象的toString方法没有被重写过的话,那么toString方法将会返回”[object type]”
    • 不直接调用数组或者字符串自己的toString方法的原因是
    1
    2
    3
    4
    5
    6
    const a = ['Hello','Howard'];
    const b = {0:'Hello',1:'Howard'};
    const c = 'Hello Howard';
    a.toString();//"Hello,Howard"
    b.toString();//"[object Object]"
    c.toString();//"Hello,Howard"

    如上代码 只有对象才会返回对象的类型 所以我们需要使用对象的toString方法 通过call或者apply方法来改变toString

    方法的执行上下文

    1
    2
    3
    4
    5
    6
    const a = ['Hello','Howard'];
    const b = {0:'Hello',1:'Howard'};
    const c = 'Hello Howard';
    Object.prototype.toString.call(a);//"[object Array]"
    Object.prototype.toString.call(b);//"[object Object]"
    Object.prototype.toString.call(c);//"[object String]"

实现判断代码

1
2
3
function flatten(array){
return (Object.prototype.toString.call(array)==="[object Array]")
}

JS_数组方法

Array.prototype.fill()

  • Fill()方法用一个固定值填充一个数组中从起始索引到终止索引的全部元素 (不包括终止索引)
  • fill()中有提供三个参数 分别是value,start,end
    • value用来填充数组元素的值
    • start 起始索引 默认是0
    • end 终止索引 默认是this.length
  • fill方法的返回值是修改后的数组

Array.prototype.slice()

  • Slice()方法返回一个新的数组对象 这个对象是一个由 beginend 决定的原数组的浅拷贝(包括begin 不包括end) 原数组不会被改变
  • Slice方法有两个参数 begin 和 end 这两个参数都是可选
    • begin参数是提取起始处的索引(从0开始)的数组的元素 如果这个参数为负数表示从原数组倒数第几个参数开始提取(包含最后一个元素) 如果省略begin 那么索引从0开始 如果begin超出原数组的索引范围返回空数组
    • end参数 slice 会提取原数组中索引从begin到end的所有元素(包含begin 不包含end)
      • 如果该参数为负数, 则它表示在原数组中的倒数第几个元素结束抽取。 slice(-2,-1) 表示抽取了原数组中的倒数第二个元素到最后一个元素
      • 如果 end 被省略,则 slice 会一直提取到原数组末尾
  • 使用slice()可以直接浅拷贝一个数组

Array.prototype.splice()

  • Splice()方法通过删除或者替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内容 该方法改变原数组
  • Splice 的参数
    • start
      • 指定修改的开始位置(从0开始计数) start参数是第几位的意思 如果超出数组长度从数组末尾开始添加内容 如果是负数表示从数组末尾开始的第几位(从-1开始计数 意味着-n是倒数第n个元素并且等价于array.length-n) 如果负值绝对值大于数组的长度 那么从第0个开始
    • deleteCount() (可选参数)
      • 参数为整数 表示要删除的个数
      • 如果deleteCount大于Start之后元素的总数 从start后面的数组元素都将被删除(包含start位)
      • 如果省略deleteCount 或者它的值大于等于array.length-start(实际上就是start后面所有元素) 那么start后面的元素都会被删除
      • 如果deleteCount 是0或者负数那么就不移除元素 这种情况只要要添加一个元素
    • item 要添加进数组的元素,从start 位置开始 如果不指定,则 splice() 将只删除数组元素。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const arr = ['rick','owens','raf','simons','oamc']
//删除从第一位开始的一个元素owens
arr.splice(1,1)
console.log(arr) //arr ["rick", "raf", "simons", "oamc"]

//把owens再添加到原来的位置
arr.splice(1,0,'owens')
console.log(arr) // ["rick", "owens", "raf", "simons", "oamc"]
//删除oamc
arr.splice(-1,1)
console.log(arr) //["rick", "owens", "raf", "simons"]
//只留下rick
arr.splice(1)
console.log(arr) //["rick"]

具体示例MDN:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/splice

Array.from()

从一个类似数组或可迭代对象创建一个新的浅拷贝的数组实例 返回值是该数组实例

参数:

  • arrayLike
    • 想要转换成数组的伪数组或者可迭代对象
  • mapFn(可选)
    • 如果指定了这个参数 新数组中的每个元素会执行该回调函数
  • thisArg(可选)
    • 执行回调函数mapFn时的this对象

MDN示例:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/from

JS_节流防抖

节流

  • 节流的原理

    • 如果事件持续的被触发,每隔一段时间,只执行一次事件
  • 实现节流

    • 函数的节流通过闭包保存一个标记,在函数开头判断该标记是否为true,为true执行函数 不为true 就直接返回,当判断完该标记,将该标记设置为false,然后把外部传入的函数放在一个setTimeout中,函数执行之后将标记设置为true 可以执行下一次循环
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function throttle(fn, interval = 300) {
    let canRun = true;
    return function () {
    if (!canRun) return;
    canRun = false;
    setTimeout(() => {
    fn.apply(this, arguments);
    canRun = true;
    }, interval);
    };
    }

防抖

  • 防抖的原理

    • 当事件触发时,相应的函数并不会立即触发,而是会等待一定的时间;
    • 当事件密集触发时,函数的触发会被频繁的推迟;
    • 只有等待了一段时间也没有事件触发,才会真正的执行响应函数;
  • 实现防抖

    • 通过闭包来保存setTimeout返回的值,当用户输入的时候将之前的setTimeout给clear掉,创建一个新的setTimeout
    1
    2
    3
    4
    5
    6
    7
    8
    9
    function debounce(fn, interval = 300) {
    let timeout = null;
    return function () {
    clearTimeout(timeout);
    timeout = setTimeout(() => {
    fn.apply(this, arguments);
    }, interval);
    };
    }

JS_跨域

跨域产生的原因是浏览器同源策略导致, 当请求的url和当前的url 协议 域名 端口有任意一个不相同 那么就会产生跨域 跨域的解决方法 可以通过jsonp跨域 iframe标签 跨域资源共享 代理

vue中的proxyTable原理

  • 浏览器是禁止跨域的,但是服务端不禁止,在本地运行npm run dev等命令时实际上是用node运行了一个服务器,因此proxyTable实际上是将请求发给自己的服务器,再由服务器转发给后台服务器,做了一层代理,因此不会出现跨域问题。

JS_this的指向

JS_隐式转换

valueOf()和toString()

toString

  • toString 的返回值是一个表示该对象的字符串
  • 每个对象都有一个toString()方法 当该对象被表示为一个文本值时,或者一个对象以预期的字符串方式引用时会自动调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Dog(name) {
this.name = name;
}

const dog1 = new Dog('Gabby');

Dog.prototype.toString = function dogToString() {
console.log("toString调用")
return `${this.name}`;
};

console.log(dog1);
//输出结果是 "toString调用"
// Gabby

JS_函数柯里化

函数柯里化在维基百科中的定义是

在数学和计算科学中 柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的的函数的技术

1
2
3
4
5
6
7
function add(a,b){
return a + b
}
//执行 add(1,2) 一次传入两个参数
//如果有一个curry函数可以做到如下
var addCurry = curry(add)
addCurry(1)(2) //结果和add(1,2)相同 那么该addCurry 实现函数的柯里化

柯里化的作用?
柯里化实际用途可以理解为 参数复用,本质上是降低通用性,提高适用性

函数柯里化 经典面试题

1

JS_深拷贝&浅拷贝

浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。

深拷贝递归方式代码简单实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//该方法是返回一个深拷贝之后的对象
const deepClone = (obj)=>{
if(obj === null) return null
//assign可以合并对象这一步是实现了浅拷贝
let clone = Object.assign({},obj)
//keys方法返回对象key集合数组
Object.keys(clone).forEach(
key =>
(clone[key]=typeof obj[key] === 'object'?deepClone(obj[key]):obj[key]))
//处理是array的问题
if(Array.isArray(obj)){
clone.length = obj.length
return Array.from(clone)
}
return clone
}
//const a = { foo: 'bar', obj: { a: 1, b: 2 },arr:[1,2,3] };
//const b = deepClone(a)
//a !== b

JS_继承

原型链继承

盗用构造函数继承

组合继承

原型式继承

寄生式继承

寄生式组合继承

Vue_nextTick

Vue_响应式原理

vue是采用数据劫持配合发布者订阅者模式的方式 通过Object.defineProperty()来劫持各个属性的setter和getter 当数据变动时 发布消息给依赖收集器 去通知Watcher做出对应的回调函数 然后去更新视图

在new Vue() 的时候作为绑定入口 内部整合Observer Compile 和 Watcher三者 通过Observer来监听model数据变化,通过compile来解析编译模板指令,最终利用Watcher搭起了Observer和Compile之间的通信桥梁 达到数据变化影响视图更新, 当视图交互变化时候 数据model发生变更的双向绑定效果

Vue-图片懒加载原理

  • 图片懒加载的实现原理

    • 一张图片就是一个标签 浏览器是否发出请求图片就是根据的src属性
    • 所以实现懒加载的关键就是在于当图片没有进入可视化区域的时候先不给img的src赋值 这样浏览器就不会发送请求了 等到图片进入可视窗口在给src赋值
  • 实现方法:

    • 判断元素是否在可视范围内
    1
    2
    3
    4
    5
    function isVisible(ele){
    let windowHeight = window.innerHeight
    let position = ele.getBoundingClientRect()
    // 当元素的top偏移量小于页面大小并且大于高度的负数
    if(position.top<windowHeight && position.top>-position.height) {return true}return false}
    • 对图片实现懒加载
    1
    2
    3
    4
    5
    6
    function lazyLoad(img, src){
    if(img && src && isVisible(img)){ // 元素存在,元素未被加载,元素可见
    setTimeout(function(){
    img.setAttribute('src', src)
    }, 1000) // 模拟网络请求
    }}
  • 添加滚动监听

    1
    2
    3
    window.addEventListener('scroll', function(){
    lazyLoad(img, src)
    })

网络_浏览器缓存

  • 浏览器每次发起请求(一般是get请求),都会先在浏览器缓存中查找该请求的结果以及缓存标识
  • 浏览器每次拿到返回的请求结果都会将该结果和缓存标识存入浏览器缓存
  • 根据是否需要向服务器重新发起HTTP请求将缓存过程分为两个部分,分别是强制缓存协商缓存

请求头

  • If-none-match
    • 如果上一次的响应头中有ETag,就会将ETag的值作为请求头的值
    • 如果服务器发现资源的最新摘要值跟f-None- Match不匹配,就会返回新的资源(200OK)
    • 否则,就不会返回资源的具体数据(304 Not Modified)
  • If-modified-since
    • 如果上一次的响应头中没有ETag,有Last- Modified,就会将 Last-modifiedi的值作为请求头的值
    • 如果服务器发现资源的最后一次修改时间晚于last- Modified- Since,就会返回新的资源(200 OK)

强缓存

强制缓存就是向浏览器缓存查找该请求结果,并根据该结果的缓存规则来决定是否使用该缓存结果的过程

  • 响应头内的字段用于控制缓存机制
  • Pragma:作用类似Cache-Control HTTP/1.0的产物
  • Expires:缓存过期时间 HTTP/1.0的产物
  • Cache-Control
    • no- storage:不缓存数据到本地
    • public:允许用户、代理服务器缓存数据到本地 private:只允许用户缓存数据到本地
    • max-age:缓存的有效时间(多长时间不过期),单位秒
    • no- cache:每次需要发请求给服务器询问缓存是否有变化,再来決定如何使用缓存
  • 获取缓存检测缓存是否过期,如果没过期取缓存,优先从内存,其次硬盘,如果过期,则与服务器协商缓存是否仍然可用,如果不可用则获取,可用取缓存

协商缓存

  • 协商缓存就是强制缓存失效或者cache-control是no-cache后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程,服务器看缓存是否有变化 主要有以下两种情况:
    • 协商缓存生效,返回304
    • 协商缓存失效,返回200和请求结果结果

网络_从URL输入到页面展现发生了什么

大致过程

  • 解析URL

  • 检查缓存

    • DNS检查是否有缓存
  • 域名解析(DNS):将域名解析为IP地址

  • TCP链接: TCP三次握手

    • 如图
    • 为什么三次握手不是两次握手
      • 目的是防止server端一直等待 造成资源浪费
      • 如果客户端要和服务器建立连接 然后第一次握手发送SYN和Seq之后 因为网络延迟的原因在连接已经释放后的一段时间才会到达server 这时server再次收到了这个连接请求 server依然会发送确认字段建立连接 如果是只有两次握手 那么这时server会认为建立成功了 而客户端并没有建立连接意愿 server就会一直等待 这样就会造成资源的浪费
  • 发送HTTP请求

  • 服务器处理请求并返回HTTP报文

  • 浏览器解析渲染界面

    • 根据HTML解析出DOM树
    • 根据CSS解析生成CSS规则树
    • 结合DOM和CSS规则树 生成渲染树
    • 根据渲染树计算每个节点的信息(布局)
      • 布局:通过渲染树中渲染对象的信息,计算出每一个渲染对象的位置和尺寸
      • 回流:在布局完成后,发现了某个部分发生了变化影响了布局,那就需要倒回去重新渲染。
    • 根据计算好的信息绘制出界面
      • 重绘:某个元素的背景颜色,文字颜色等,不影响元素周围或内部布局的属性,将只会引起浏览器的重绘。
      • 回流:某个元素的尺寸发生了变化,则需重新计算渲染树,重新渲染。
  • 断开连接: TCP四次挥手

    • 当数据传送完毕,需要断开 tcp 连接,此时发起 tcp 四次挥手。
      • 发起方向被动方发送报文,Fin、Ack、Seq,表示已经没有数据传输了。并进入 FIN_WAIT_1 状态(第一次挥手:由浏览器发起的,发送给服务器,我请求报文发送完了,你准备关闭吧)
      • 被动方发送报文,Ack、Seq,表示同意关闭请求。此时主机发起方进入 FIN_WAIT_2 状态。(第二次挥手:由服务器发起的,告诉浏览器,我请求报文接受完了,我准备关闭了,你也准备吧)
      • 被动方向发起方发送报文段,Fin、Ack、Seq,请求关闭连接。并进入 LAST_ACK 状态。(第三次挥手:由服务器发起,告诉浏览器,我响应报文发送完了,你准备关闭吧) 第二次和第三次挥手可以合并直接进入TIME_WAIT状态
      • 发起方发送报文 ACK Seq 给被动方 被动方收到后进入CLOSED状态 发送方发送之后会进入TIME_WAIT状态。经过2MSL之后发起方进入CLOSED状态(因为这可以防止本次连接中产生的数据包误传到下一次的连接中(因为本次连接中的数据包都会在2MSL时间内消失了 这样就可以保证不会误传))

前端理解_前后端渲染 前后端路由

有的时候面试官会对我们为什么要使用router提出问题 我们可以从前后端渲染开始讲起

后端路由阶段:

  • 早起的网页开发是服务器直接生产渲染好对应的HTML页面, 返回给客户端进行展示 一般使用jsp来实现
  • 但是我们的网址是多页面的 后端路由时期是通过每个页面有自己对应的网址 然后服务器通过正则匹配URL的方式通过后端的Controller处理生成HTML页面返回给前端
  • 在这个阶段是后端处理URL和页面之间的映射关系
  • 缺点显而易见就是难以编写和维护

前后端分离阶段:

  • ajax出现后出现前后端分离开发模式
  • 该模式就是后端只负责提供数据而不负责任何阶段的内容
  • 前端的内容是由前端通过js实现的页面
  • 这样的优点就是前后端责任清晰 前端可以专注于可视化和交互上

单页面富应用(SPA)阶段:

整个网页只有一个html界面 SPA的最主要的特点是在前后端分离的基础上加上了一层前端路由

也就是我们的路由规则是前端来维护的 就是我们可以通过路由跳转的方式切换组件 并且改变URL页面不会进行整体的刷新

如何实现不刷新跳转呢?

我们可以通过两种方式

  • URL 的 hash 本质上是改变window.location 的href属性
    • 可以通过直接赋值location.hash来改变href 但是页面不会刷新
  • HTML5中的history
    • pushState replaceState go 模式