前端面试题

一些面试题

css 部分

css 盒模型

MDN 文档

对一个文档进行布局时候, 浏览器渲染引擎会根据css 基础框盒模型 将所有元素都表示为一个矩形盒子, css 决定了盒子的大小位置以及其他一些属性.

盒子由四个部分或称区域组成 内容边界 内边距边界 边框边界 外边框边界

通过下面的 css 来改变应该如何计算一个元素的总宽度和总高度

1
2
box-sizing: content-box;  默认值
box-sizing: border-box;

绝对定位和相对定位的区别

  • 绝对定位相对于元素最近的已定位的祖先元素进行定位
  • 相对定位是根据元素在文档中的初始位置进行定位

水平垂直居中

  • flex
1
2
3
display: flex;
justify-content: center;
align-item: center;

0.5px 的线

1
2
3
height: 1px;
background: #333;
transform: scaleX(0.5);

flex

是一个简写属性, 规定了 flex-grow flex-shrink flex-basis

  • flex-grow 他指定了 flex 容器中剩余的多少空间应该分配给项目
  • flex-shrink 制定了 flex 元素的收缩规则, flex 元素仅在默认宽度和大于容器的是才会发生收缩,其收缩的大小一句是 flex-shrink
  • flex-basis 指定了 flex 元素在主轴方向上的初始大小, 如果不是用 box-sizing 改变盒模型的话,那么这个属性就决定了 flex 元素的内容盒的尺寸

javascript

call apply bind

三个函数都是重新指定函数内部的 this

  • call 方法接收的是一个包含等多个参数的列表
  • apply 方法接收的是一个包含税多个参数的 数组
  • bind 方法和 call 类似,但是 bind 是返回一个新的函数

闭包

MDN

函数和对其周围状态的引用捆绑在一起构成的闭包, 从而实现让你在可以从内部函数访问外部函数作用域.

作用域 MDN

this

MDN this

  1. 无论严格模式还是非严格模式,在全局环境中 this 都指向全局对象
1
2
3
4
5
// 在控制台输入

this.a = 100;
console.log(global.a);
console.log(this === global);
  1. 在函数内执行, 严格模式和非严格模式会有差别
1
2
3
4
5
6
7
8
9
10
function t1() {
return this;
}
console.log(t1() === global);

function t2() {
"use strict";
return this;
}
console.log(t2() === undefined);

在 ES 模块中所有代码运行于严格模式之下

  1. this 的值取决于函数/对象被调用的方式
1
2
3
4
5
6
7
8
9
10
11
12
13
const a = {
b() {
return this;
},
};

console.log(a.b() === a);

const d = {
e: a.b,
};

console.log(d.e() === d);
  1. bind this

一般可以使用 函数原型上的 bind 方法来为一个方法指定其中的 this

1
2
3
4
5
6
7
8
9
10
11
function a() {
return this;
}

const b = { b: 1 };
const d = { d: 1 };

console.log(a() === global);

console.log(a.bind(b)() === b);
console.log(a.bind(d)() === d);

简易实现 bind 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function myBind(self, fn, ...args) {
return function () {
return fn.apply(self, args);
};
}

function a(arg) {
console.log(arg);
return this;
}

const b = { b: 1 };

console.log(myBind(b, a, "arg")() === b);

var let const

var 会有变量提升
let const 会有临时性死区

loop 里面 var 和 let 的差异, var 始终是一个变量, let 每次循环的变量都是新的

JavaScript 原型, 原型链

JavaScript 只有一种结构, 对象, 每个实例对象又有一个私有属性 __proto__, 它指向他的构造函数的原型对象 prototype, 而该原型对象也有一个自己的原型对象 __proto__, 一直层层向上,直到找到 null, 而根据定义 null 没有原型,到此为止.

而所有的 JavaScript 对象都是位于顶端的 Object 的实例.

1
2
3
4
5
6
7
8
9
10
11
12
13
class A {
constructor() {
this.state = 1;
}
}

const b = new A();

// __proto__ 是非标准的,但是浏览器都实现了它,如果有需要请使用 Object.getPrototypeOf 来获取 prototype
console.log(b.__proto__);
console.log(Object.getPrototypeOf(b) === b.__proto__); // true
console.log(b instanceof A); // true
console.log(b instanceof Object); // true

事件委托 冒泡 捕获

对于比较多的元素进行绑定事件,可以不用一个一个的为元素进行绑定,将原本的事件委托绑定给父元素, 让父元素利用事件冒泡的特性从而可以处理, 在事件内可以通过 event.target 可以知道实际发生事件的位置和元素.

冒泡和捕获

事件冒泡是由微软提出的, 主张事件应该从目标元素一直向上触发,一直传播到 document 之上 例如

div -> body -> html -> document

而事件捕获是由王景公司提出的,主张事件从最外层一直向最内侧进行传播 例如

document -> html -> body -> div

之后 w3c 委员会进行了折中处理, 先捕获然后再冒泡

1
target.addEventListener(type, listener, useCapture);

第三个参数 useCapture 如果是 true 那么就会在捕获阶段触发, 默认是 false

JavaScript 异步处理方法 async/await 的实现原理

异步处理方法:

  1. 使用回调函数
  2. 事件监听, 通过创建一个自定义事件, 来监听指定事件,监听到之后就运行代码
  3. Promise

async await 是 generator 的语法糖. 内置了执行器.不需要在手动 next()

简单的说 async/await === Promise + generator + yield

防抖和节流的区别是什么

防抖是将多次执行变为指定事件内的最后一次执行, 而节流是变成固定时间只执行一次.

React

React 的 key 的作用

React 更新主要基于下面三种策略:

  1. 如果层级相同的节点 DOM 结构发生了变化 例如从 div 变为了 p 那么直接卸载重新创建
  2. 如果层级相同的节点 DOM 结构没有发生变化, 但是属性变了 例如
    1
    变成了
    1
    那么 React 只会更新这个属性
  3. 所有同一级的子节点,他们都会通过 key 来区分. 如果 key 相同那么他就会更新属性,如果 key 不同, 那么他就 卸载重新创建.

Vue React 的差别

首先 Vue 是一个渐进式的框架, 你可以只用 Vue 做 View 也可以使用 Vue 全家桶, 而 React 只是一个 View 的库.

React 基于 jsx 语法, Vue 基于 html 模板. 两者都是 虚拟 dom

Vue 通过代理 data 可以检测出哪些数据发生了变化从而进行更新, 并且更新时同步的
React 通过基于时间的调度器, 对 Fiber 对象链表遍历对比更新.

执行栈

执行栈可以理解为储存函数调用的栈结构,单向的(只能从一侧进栈或者出栈)遵循先进后出的原则.
运行一个函数就会将函数和相关的变量储存在栈内, 当函数运行完毕之后会从栈中弹出. 栈的大小是有限制的,根据浏览器不同限制也不尽相同.如果超过最大栈深度,那么就会抛出错误.

Event loop

JavaScript 在执行的过程中会产生执行环境,这些执行环境会被顺序价值入到执行栈中, 如果遇到异步代码,会被挂起并加入到执行队列中, 一旦执行栈为空, Event Loop 就会从 Task 队列中拿出要执行的代码并放入执行栈中进行执行. 所以从本质上来讲 异步也是同步行为.

不同的任务会被放在不同的队列中, 任务可以分为微任务和宏任务, 在 ES6 规范中, 微任务被称为 jobs, 宏任务被称为 task

如果当前宏任务执行完毕, 那么会查询当前是否有微任务需要执行, 如果有就执行没有就开始下一轮 Event loop

举例说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
console.log(1); // 执行1
setTimeout(() => console.log(6), 0); // 非同步宏任务放在 task 队列
new Promise((resolve) => {
// 输出2 并且 jobs 队列添加了两个微任务
console.log(2);
resolve();
})
.then(() => console.log(4))
.then(() => console.log(5));
console.log(3); // 执行3

// 3 执行完成之后执行队列就空了,线程就会去看看有没有需要执行的微任务
// 发现微任务 执行 4, 执行 5,
// 微任务也执行完毕了
// 开启下一轮事件循环拿出 setTimeout 看看到期没有,没到期就放回去继续等,到期了就执行

常见微任务:

  • process.nextTick
  • Promise
  • Object.observe
  • MutaionObserver

宏任务:

  • JavaScript 同步代码
  • setTimeout
  • setTnterval
  • setImmediate
  • I/O
  • UI render

浏览器相关

浏览器渲染流程

  1. 浏览器收到 document 响应之后,会对文本进行解析, 首先解析成 AST ,之后再将语法树解析成实际 Node ,从而构建出一颗 DOM 树

  2. 浏览器在解析的同时也会一边处理资源,对资源进行加载和运行,遇到 css 就开始请求 css 文件,调用布局引擎开始解析 css,类似也是解析语法,生成树,树生成实际 css Node 树.

在这两者解析的过程也会开始处理布局 js 等. 这也就是为什么 js 会堵塞渲染, 和 js 放在文档末尾比较好.

重绘 和 重排

重绘是指当前节点需要更改外观,而不需要更改布局, 比如字体颜色 div 背景颜色之类的.

重排就是当布局或者几何属性发生变化的时候就称为重排

重排必定会发生重绘, 重绘则不一定会引发重排, 重排所需要的城北比重绘高很多, 改变父节点内的子节点很可能回调熬制父节点一系列重排.

所以以下操作会影响性能:

  • 改变窗口大小
  • 改变字体
  • 添加或者删除样式
  • 改变布局

JavaScript 引起的重绘重排

  • JavaScript 在事件循环的时候执行完微任务列表之后,就会判断 document 是否需要更新, 因为浏览器是 60Hz 刷新率,所以每 16.6ms 才会更新一次

  • 判断有没有 resize 或者 scroll 事件, 有就执行, 所以这两个事件也是 16.6 ms 以上才会执行一次

  • 更新动画

  • 判断是否有全屏操作事件

  • 执行 requsetAnimationFram 回调

  • 执行 IntersectionObserver 回调

  • 如果还有剩余时间 执行 requestIdleCallback 回调

同时在 JavaScript 使用一些获取大小位置信息的 api 都会引起强制重绘,以获得最新值 说明文档, 举例说明

1
2
3
4
elementA.className = "a";
const aHeight = element.offsetHeight; // 此时就会强制重排
elementB.className = "b";
const heightB = elementB.offsetHeight; // 此时再次强制重排获得最新值

那么可以通过如下修改来避免重复重绘

1
2
3
4
elementA.className = "a";
elementB.className = "b";
const aHeight = element.offsetHeight; // 强制重排获得最新值
const heightB = elementB.offsetHeight; // 不会强制重排因为已经是最新值

HTTP

强缓存和协议缓存

  • 浏览器会在加载资源时,根据请求头的过期时间以及 cache-control 判断是否命中缓存, 是则直接从缓存读取资源
  • 如果没有命中缓存,浏览器会发送一个请求到服务器,通过 last-modified 和 etag 验证资源是否命中缓存. 如果命中.服务器将返回这个请求,但是不会反悔资源的数据.
  • 如果两者都没严明中,则直接从服务器加载资源

强缓存

  • expires
    expores 是一个 HTTP 响应头.标识资源过期时间, 但是 expires 受限于本地时间,如果修改了时间,则可能造成缓存失效

  • cache-control

cache-control 优先级高于 expires 表示的是相对时间.

协商缓存

如果某个资源没有命中强缓存, 就会发送一个请求到服务器,验证协商请求是否命中,如果命中协商缓存,请求响应返回的 HTTP 状态为 304,并显示一个 not modified

  • last-modified if-modified-since

last-modified 标识本地文件最后修改日期, 浏览器会在请求头上加上 if-modified-sice (返回上次的 last-modified) 访问服务器在该日期后资源是否有更新,有更新的话就会将新资源发送回来

  • Etag if-none-match

Etag 就像一个指纹,每次资源变化都会导致 etag 变化,和最后修改时间没关系, etag 可以保证每个资源都是唯一的
if-none-match 和头会将上次返回的 etag 发送给服务器, 询问该资源的 etag 是否有更新,有变动就发回新的资源回来

etag 优先级比 last-modified 高

  • 一些文件也许会周期性的改变,但是他的内容官兵不改变,这个时候并不希望客户端认为这个文件被修改了,而工薪 get
  • 某些文件修改非常频繁, if modifled since 能检查到的是秒级的而 etag 可以
  • 某些服务器不能精确的得到文件的最后修改时间

几种状态码的区别

  • 200 强缓存 expires cache-control 失效,返回新的资源文件
  • 200 from cache 强缓存都在,未过期,从本地读取
  • 304 last modified 和 etag 没有过期,服务端返回 304

SCRF 和 XSS

  • XSS 指攻击者对客户端网页注入一些恶意脚本的方式进行攻击.通常是获取用户隐私数据. 防范方式也很简单.转义用户输入,不相信任何用户输入. 设置 cookie 的 HTTPOnly 禁止 JavaScript 访问 cookie

  • SCRF 是指跨站请求伪造, cookie 有个 donmain 属性指定, 如果 cookie 的域和页面的域相同,那么就是第一方 cookie, 如果 cookie 的域和页面不同,则称为第三方域, 页面上包含图片或者其他域上的资源时, 第一方 cookie 也会发送给他们的服务器

http referer 头

http 会有一个 Referer 头用来表明这个请求是从哪来的.但是网站可以选择不发送任何 referer

添加 token 头
可以要求请求添加一个单独的 token 来防止攻击

跨域

跨域是指一个域的脚本请求另一个域的资源或者请求.同源就是 协议 + 域名 + 端口必须一致,否则非同域

同源限制一下行为:

  • cookie loacalstorage 和 indexDB 无法读取
  • DOM 和 Js 无法获得
  • 请求无法发送或者被拦截

跨域解决方法:

  • 跨域资源共享 CORS 跨域
  • nginx 代理
  • JSONP 跨域