前端面试基础知识整理(持续更新)

46次阅读

共计 12086 个字符,预计需要花费 31 分钟才能阅读完成。

一、CSS
1.css 盒模型

w3c 盒模型:content + padding + border + margin。元素宽高 (css) 等于 content 的宽高。
IE 盒模型:元素的宽高 (css) 等于 content + padding + border 的宽度。

2.BFC 块级格式化上下文

形成条件:根元素;position: absolute/fixed; float;overflow:not visible;display: inline-block / table。
应用:清除浮动

3. 居中布局

水平居中行内元素:text-align: center; 块级元素:margin:auto;
垂直居中 line-height; absolute; flex;
水平垂直居中 absolute; 伪元素形成行内元素居中;flex + justify-content + align-items

4. 清除浮动
设置父元素高度;形成 bfc;伪元素 clear: both;
5.flex 常用 api
引用:Flex 布局教程:语法篇 容器元素 display: flex; 容器属性
flex-direction // 决定主轴的方向(即项目的排列方向)。
flex-wrap // 如果一条轴线排不下,如何换行。
flex-flow // 是 flex-direction 属性和 flex-wrap 属性的简写形式,默认值为 row nowrap。
justify-content // 定义了项目在主轴上的对齐方式。
align-items // 属性定义项目在交叉轴上如何对齐。
align-content // 属性定义了多根轴线的对齐方式。如果项目只有一根轴线,该属性不起作用。
元素属性
order // 属性定义项目的排列顺序。数值越小,排列越靠前,默认为 0。
flex-grow // 属性定义项目的放大比例,默认为 0,即如果存在剩余空间,也不放大。
flex-shrink // 属性定义了项目的缩小比例,默认为 1,即如果空间不足,该项目将缩小
flex-basis // 属性定义了在分配多余空间之前,项目占据的主轴空间(main size)
flex // flex-grow, flex-shrink 和 flex-basis 的简写,默认值为 0 1 auto
align-self // align-self 属性允许单个项目有与其他项目不一样的对齐方式,可覆盖 align-items 属性。
二、JavaScript
1. 原型与继承
原型:prototype
函数的对象属性,包含一个 constructor 属性,指向函数自身:Fun.prototype.constructor === Fun。
原型链
原型链由原型对象构成, 是 js 对象继承的机制。每个对像都有 [[prototype]] 属性,主流浏览器以__proto__暴露给用户。__proto__指向对象的构造函数的原型属性(prototype)。prototype 的__proto__又指向它的构造函数的原型;通过__proto__属性,形成一个引用链。在访问对象的属性时,如果对象本身不存在,就会沿着这条 [[prototype]] 链式结构一层层的访问。
构造函数
可以通过 new 来新建一个对象的函数。
原型与继承
函数拥有原型(prototype);而对象可以通过原型链关系([[prototype]])实现继承。函数是一种特殊对象,也拥有__proto__, 即函数也会继承。函数的原型链是:函数静态方法—> 原生函数类型的原型(拥有函数原生方法)—> 原生对象原型(拥有对象原生方法)—> null class A {}; class B extends A {};
类的继承
B.__proto__ = A.prototype; B.prototype.__proto__ = A.prototype;
new Fun 操作符原理

Object.create(Fun.prototype);
执行 Fun,即类似类中的调用 constructor()(为什么是执行 constructor,因为 constructor 指向函数本身);
如果 constructor 没有明确返回函数或对象,则返回第一步创建的对象。

2. 函数执行相关
堆内存与栈内存

栈内存:由编译器自动分配与释放。我们可以直接操作栈内存中的值。js 的基本类型存放在栈内存中,按值引用;
堆内存:链表结构的类型,可以动态分配大小,js 引用类型占用内存空间的大小不固定,存储在堆内存中。由于 JS 不允许直接访问堆内存中的位置,因此我们不能直接操作 js 的引用类型。而是生成一个指针,并将它放到栈内存中,通过这个指针来操作引用类型。

垃圾回收
类型:标记引用(无法解决循环引用问题);标记清除(现在主流回收算法)。标记清除算法的核型概念是:从根部(在 JS 中就是全局对象)出发定时扫描内存中的对象。凡是能从根部到达的对象,都是还需要使用的。那些无法由根部出发触及到的对象被标记为不再使用,稍后进行回收。
内存泄漏
对于不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)。常见内存泄漏:

不合理的计时器(引用过时的外部变量)
被共享的闭包作用域(函数内部的函数共享一个闭包环境)
脱离 dom 引用(dom 中元素被清除,但是变量还保持这对 dom 元素的引用)
意外的全局变量(未声明或者指向全局的 this.xxx)

事件循环
事件循环是指: 执行一个宏任务,然后执行清空微任务列表,循环再执行宏任务,再清微任务列表

微任务 microtask(jobs): promise / ajax / Object.observe(该方法已废弃)

宏任务 macrotask(task): setTimout / script / IO / UI Rendering

每次事件循环:

执行完主执行线程中的任务。
取出 micro-task 中任务执行直到清空。
取出 macro-task 中一个任务执行。
取出 micro-task 中任务执行直到清空。
重复 3 和 4。

执行环境及作用域
执行环境
执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行环境斗鱼一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。全局执行环境是最外围的执行环境,在 web 浏览器中,全局执行环境被认为是 window 对象,因此所有全局变量和函数都是作为 window 对象的属性和方法被创建的。某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁。(全局执行环境直到应用程序推出时才会被销毁)。
作用域链
当代码在一个环境中执行时,会创建变量对象的一个作用域链,保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端,始终都是当前执行的代码所在环境的变量对象。如果这个环境是函数,则将其活动对象作为变量对象,其在最开始的时候只包含一个变量:arguments。作用域链的下一个变量来自包含环境,而️再下一个变量对象则来自下一个包含环境一直延续到全局执行环境。作用域链在创建的时候就会被生成,保存在内部的 [[scope]] 属性当中。其本质是一个指向变量对象的指针列表,它指引用但不实际包含变量对象。
[[scope]]
函数创建过程中,会创建一个预先包含外部活动对象的作用域链,并始终包含全局环境的变量对象。这个作用域链被保存在内部的 [[scope]] 属性中,当函数执行时,会通过复制该属性中的对象构建起执行环境的作用域链。
执行上下文
定义
执行上下文是指当前 Javascript 代码被解析和执行时所在环境的抽象概念,JavaScript 任何代码的运行都是在执行上下文中。
类型

全局执行上下文
函数执行上下文
eval 函数执行上下文

如何工作
执行上下文分为两个过程:

创建阶段
执行阶段

创建阶段
主要做了三件事

this 绑定(这解释了为什么 this 的值取决于函数的调用方式)
创建词法环境。(创建活动对象,变量和函数声明的位置标记(声明提升过程);根据 [[scope]] 创建作用域链)
创建变量环境(变量环境也是词法法环境,用于存储 var 声明的变量。)

执行阶段
变量赋值,语句执行等。
闭包
闭包是指有权访问另一个函数作用域中的变量的函数。其本质是函数的作用域链中保存着外部函数变量对象的引用。可以通过 [[scope]] 属性查看。因此,即使外部函数被销毁,但是由于外部环境中定义的变量在闭包的 scope 中还保持着引用,所以这些变量暂时不会被垃圾回收机制回收,因此依然可以在闭包函数中正常访问。注意:同一个函数内部的闭包函数共享同一个外部闭包环境。从下图可以看出,即使返回的闭包里面没有引用 c,d 变量,但是由于内部函数 closure1 中引用链,所以即使 closure1 未调用,闭包作用域中依然保存这些变量的引用。

3.js 引用方式

html 静态 <script> 引入
js 动态插入 <script>

<script defer>: 延迟加载,元素解析完成后执行

<script async>: 异步加载,但执行时会阻塞元素渲染

其中蓝色代表 js 脚本网络加载时间,红色代表 js 脚本执行时间,绿色代表 html 解析。更多分析见下文浏览器 -> 页面的加载与渲染
4. 对象的拷贝
浅拷贝
拷贝一层,内部如果是对象,则只是单纯的复制指针地址。

Object.assign()
{…obj}

深拷贝

JSON.parse(JSON.stringify(source)), 缺点层次太深会爆栈,无法解决循环引用问题。
参考深拷贝的终极探索(99% 的人都不知道), 采用循环代替递归实现深层次的拷贝。大致思想如下:

// 保持引用关系
function cloneForce(x) {
// =============
const uniqueList = []; // 用来去重
// =============

let root = {};

// 循环数组
const loopList = [
{
parent: root,
key: undefined,
data: x,
}
];

while(loopList.length) {
// 深度优先
const node = loopList.pop();
const parent = node.parent;
const key = node.key;
const data = node.data;

// 初始化赋值目标,key 为 undefined 则拷贝到父元素,否则拷贝到子元素
let res = parent;
if (typeof key !== ‘undefined’) {
res = parent[key] = {};
}

// =============
// 数据已经存在
let uniqueData = find(uniqueList, data);
if (uniqueData) {
parent[key] = uniqueData.target;
break; // 中断本次循环
}

// 数据不存在
// 保存源数据,在拷贝数据中对应的引用
uniqueList.push({
source: data,
target: res,
});
// =============

for(let k in data) {
if (data.hasOwnProperty(k)) {
if (typeof data[k] === ‘object’) {
// 下一次循环
loopList.push({
parent: res,
key: k,
data: data[k],
});
} else {
res[k] = data[k];
}
}
}
}

return root;
}

function find(arr, item) {
for(let i = 0; i < arr.length; i++) {
if (arr[i].source === item) {
return arr[i];
}
}

return null;
}
5. 模块化

es6 模块
commonJS/AMD 模块

es6 模块与 CommonJS 模块的差异

CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

6. 防抖和节流
防抖 (debounce)
将多次高频操作优化为只在最后一次执行,通常使用的场景是:用户输入,只需再输入完成后做一次输入校验即可
function debounce(fn, wait, immediate) {
let timer = null;

return function(…args) {
if (immediate && !timer) {
fn.apply(this, args);
}

if (timer) clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, args);
}, wait);
};
}
节流(throttle)
每隔一段时间后执行一次,也就是降低频率,将高频操作优化成低频操作,通常使用场景: 滚动条事件 或者 resize 事件,通常每隔 100~500 ms 执行一次即可。
function throttle(fn, wait, immediate) {
let timer = null;
let callNow = immediate;

return function(…args) {
if (callNow) {
fn.apply(this, args);
callNow = false;
}

if (!timer) {
timer = setTimeout(() => {
fn.apply(this, args)
timer = null;
}, wait);
}
}
}
7.ES6/ES7
let/const
let 和 const 可以形成块级作用域,并且不存在声明提升。并且在同一个块级作用域内不可重复声明。
解构赋值
比如 let [a, b, c] = [1, 2, 3]; let {foo} = {foo: ‘bar’};
函数扩展

参数默认值
rest 参数
箭头函数

数组扩展

Array.from() // 将类数组转化为数组
Array.of() // 更语义明确的数组构建函数:用于将一组值,转换为数组。
entries(),keys() 和 values()
实例方法 includes
flat(num?)// 数组扁平化
数组空位,es6 明确将数组空位转化成 undefined

对象扩展

属性简写
属性名表达式

可枚举性

for…in 循环:只遍历对象自身的和继承的可枚举的属性。
Object.keys():返回对象自身的所有可枚举的属性的键名。
JSON.stringify():只串行化对象自身的可枚举的属性。
Object.assign():忽略 enumerable 为 false 的属性,只拷贝对象自身的可枚举

Object.is()
Object.assign()
Object.keys(),Object.values(),Object.entries()

Symbol
let s = Symbol();

typeof s
// “symbol”
Symbol.for(), Symbol.keyFor()
Set,Map
Set
结构类似数组,成员唯一。
const s = new Set();

[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));

for (let i of s) {
console.log(i);
}
// 2 3 5 4
操作方法:

add(value):添加某个值,返回 Set 结构本身。
delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
has(value):返回一个布尔值,表示该值是否为 Set 的成员。
clear():清除所有成员,没有返回值。

WeapSet
WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别。首先,WeakSet 的成员只能是对象,而不能是其他类型的值。
const ws = new WeakSet();
ws.add(1)
// TypeError: Invalid value used in weak set
ws.add(Symbol())
// TypeError: invalid value used in weak set
其次,WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。
Map
类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。
const m = new Map();
const o = {p: ‘Hello World’};

m.set(o, ‘content’)
m.get(o) // “content”

m.has(o) // true
m.delete(o) // true
m.has(o) // false
WeakMap
WeakMap 与 Map 的区别有两点。
首先,WeakMap 只接受对象作为键名(null 除外),不接受其他类型的值作为键名。
其次,WeakMap 的键名所指向的对象,不计入垃圾回收机制。
Proxy
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写 ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。
var proxy = new Proxy(target, handler);
Promise
实现一个简单的 Promise
const FULLFILLED = ‘FULLFILLED’;
const REJECTED = ‘REJECTED’;
const PENDING = ‘PENDING’;
const isFn = val => typeof val === ‘function’;

class Promise {
constructor(handler) {
this._state = PENDING;
this._value = null;
this.FULLFILLED_CBS = [];
this.REJECTED_CBS = [];

try {
handler(this._resolve.bind(this), this._reject.bind(this));
} catch (err) {
this._reject(err);
}
}

_resolve(value) {
const async = () => {
if (this._state !== PENDING) {
return;
}
this._state = FULLFILLED;

const fullfilLed = (val) => {
this._value = val;
this.FULLFILLED_CBS.forEach(cb => cb(val));
};
const rejected = (err) => {
this._value = err;
this.REJECTED_CBS.forEach(cb => cb(err));
};

if (value instanceof Promise) {
value.then(fullfilLed, rejected);
} else {
fullfilLed(value);
}
}

requestAnimationFrame(async);
}

_reject(err) {
const async = () => {
if (this._state !== PENDING) {
return;
}
this._state = REJECTED;
this._value = err;
this.REJECTED_CBS.forEach(cb => cb(err));
}
requestAnimationFrame(async);
}

then(onFullfilled, onRejected) {
return new Promise((resolve, reject) => {
const handerResolve = (value) => {
try {
if (!isFn(onFullfilled)) {
resolve(value);
}
const res = onFullfilled(value);

if (res instanceof Promise) {
res.then(resolve, reject);
} else {
resolve(res);
}
} catch (err) {
reject(err);
}
};
const handlerReject = (err) => {
try {
if (!isFn(onRejected)) {
reject(err);
}
const res = onRejected(err);
if (res instanceof Promise) {
res.then(resolve, reject);
} else {
reject(res);
}
} catch (err) {
reject(err);
}
};
switch(this._state) {
case PENDING:
this.FULLFILLED_CBS.push(handerResolve);
this.REJECTED_CBS.push(handlerReject);
break;
case FULLFILLED:
handerResolve(this._value);
break;
case REJECTED:
handlerReject(this._value);
break;
default: break;
}
});
}

catch(onRejected) {
return this.then(undefined, onRejected);
}

finally(cb) {
const P = this.constructor;
return this.then(
/** 使用 then 保证 promise 的流是正常的,因为 promise 的下一步总是建立在上一步执行完的基础上 */
value => P.resolve(cb()).then(() => value),
reason => P.resolve(cb()).then(() => { throw reason})
);
}

static all(list) {
return new Promise(resolve, reject) {
let count = 0;
const values = [];
const P = this.constructor;

for (let [key, fn] of list) {
P.resolve(fn).then(res => {
values[key] = res;
count++;
if (count === list.length) {
resolve(values);
}
}, err => reject(err));
}
}
}

static race(list) {
return new Promise((resolve, reject) => {
const values = [];
let count = 0;
const P = this.constructor;
for (let fn of list) {
P.resolve(fn).then(res => {
resolve(res);
}, err => {
reject(err);
})
}
})
}

static resolve(value) {
if (value instanceof Promise) {
return value;
}

return new Promise(resolve => resolve(value));
}

static reject(value) {
return new Promise((resolve, reject) => reject(value));
}
}
async/await
async 函数是什么?一句话,它就是 Generator 函数的语法糖。
async 函数返回一个 promise 对象。async 函数内部 return 语句返回的值,会成为 then 方法回调函数的参数。
Class
es6 的类语法是 es5 构造函数的语法糖,但是也有一些不同点

class 的原型属性不可枚举
不使用 new 操作符,class 会报错
class 新增存取函数
不存在声明提升

Class 的继承
抓住两点即可:假设 A 为父类,B 为子类那么
B.__proto__ = A; // 子类的__proto__属性,表示构造函数的继承,总是指向父类。
B.prototype.__proto__ = A.prototype; // 子类 prototype 属性的__proto__属性,表示方法 (实例) 的继承,总是指向父类的 prototype 属性。
Module

es6 模块
commonJS/AMD 模块

es6 模块与 CommonJS 模块的差异

CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

8. 函数柯里化
在一个函数中,首先填充几个参数,然后再返回一个新的函数的技术,称为函数的柯里化。通常可用于在不侵入函数的前提下,为函数 预置通用参数,供多次重复调用。
const add = function add(x) {
return function (y) {
return x + y
}
}

const add1 = add(1)

add1(2) === 3
add1(20) === 21
9. 正则表达式
3. 浏览器
1. 页面的加载与渲染
从输入 url 到展示的过程

DNS 查找相应的 ip 地址 浏览器 DNS 缓存 -> 电脑本地 host 文件查找 -> 运营商 DNS 的解析

三次握手

客户端发送 syn(同步序列编号) 请求,进入 syn_send 状态,等待确认
服务端接收并确认 syn 包后发送 syn+ack 包,进入 syn_recv 状态
客户端接收 syn+ack 包后,发送 ack 包,双方进入 established 状态

发送请求,分析 url,设置请求报文(头,主体)
服务器返回请求的文件 (html)

浏览器渲染

HTML parser –> DOM Tree
CSS parser –> Style Tree
attachment –> Render Tree
layout: 布局

浏览器加载与渲染
浏览器下载 html 文件并进行编译,转化成类似下面的结构
图片来源再谈 load 与 DOMContentLoaded 浏览器会对转化后的数据结构自上而下进行分析:首先开启下载线程,对所有的资源进行优先级排序下载(注意,这里仅仅是下载, 区别于解析过程,解析过程可能被阻塞)。同时主线程会对文档进行解析
css 文件的解析

header 中的 css 的下载不会阻塞 html 的解析, 当 css 文件下载完成,将阻塞 html 解析,优先解析 css,会阻碍渲染。
body 中的 css 下载和解析都不会阻碍 html 的解析,会阻碍渲染。(存在特殊情况,见下文)

js 文件的解析
1. 普通的 script:
遇到 script 标签时,首先阻塞后续内容的解析(但是不会阻塞下载,chrome 对同一个域名支持 6 个 http 同时下载),当下载完成后,便执行 js 文件中的同步代码。然后接着解析 html
2. defer 和 async
如果 script 标签设置了 async 或者 defer 标签,下载过程不会阻塞文档解析。defer 执行将会放到 html 解析完成之后,dcl 事件之前。async 则是下载完就执行,并且阻塞解析。
body 中的首个 script/link
在 body 中第一个 script 资源下载完成之前,浏览器会进行首次渲染。将该 script 标签前面的 DOM 树和 CSSOM 合并成一棵 Render 树,渲染到页面中。这是页面从白屏到首次渲染的时间节点,比较关键。如果是第一个标签是 link,表现有点奇怪,下载和解析会阻塞 html 的解析,并有可能进行首次渲染,也可能不渲染。
渲染过程大致为:计算样式 Recalculate style-> 布局:layout-> 更新布局树 update layer tree -> 绘制 paint
3. 相关概念
dom 构建
DOM 构建的意思是,将文档中的所有 DOM 元素构建成一个树型结构。
css 构建
将文档中的所有 css 资源合并。生成 css rule tree
render 树
将 DOM 树和 CSS 合并成一棵渲染树,render 树在合适的时机会被渲染到页面中。(比如遇到 script 时, 该 script 还没有下载到本地时)。
4. 总结
如下图:

浏览器首先下载该地址所对应的 html 页面。(如果 html 很大,浏览器并不会等待 html 完全下载完,而是采用分段下载,分段解析)
浏览器解析 html 页面的 DOM 结构。
开启下载线程对文档中的所有资源按优先级排序下载。

主线程继续解析文档,到达 head 节点,head 里的外部资源无非是外链样式表和外链 js。
发现有外链 css 或者外链 js,如果是外链 js,则停止解析后续内容,等待该资源下载,下载完后立刻执行。如果是外链 css,继续解析后续内容。

解析到 body

在 body 中第一个 script 资源下载完成之前,浏览器会进行首次渲染。将该 script 标签前面的 DOM 树和 CSSOM 合并成一棵 Render 树,渲染到页面中。这是页面从白屏到首次渲染的时间节点,比较关键。如果是第一个标签是 link,表现有点奇怪,下载和解析会阻塞 html 的解析,并有可能进行首次渲染,也可能不渲染。随后 js 的下载和执行依然会阻塞解析,但是 css 的下载和解析则不会阻塞 html 的解析。

文档解析完毕,页面重新渲染。当页面引用的所有 js 同步代码执行完毕,触发 DOMContentLoaded 事件。然后执行布局:layout-> 更新布局树 update layer tree -> 绘制 paint
html 文档中的图片资源,js 代码中有异步加载的 css、js、图片资源都加载完毕之后,load 事件触发(与 DOMContentLoaded 没有必然的先后顺序关系)。

2. 跨域解决方案
4. 计算机网络基础
5. 前端工程化
6.Vue 源码相关
7.React 源码相关
8. 算法相关
引用参考

ECMAScript 6 入门
中高级前端大厂面试秘籍,为你保驾护航金三银四,直通大厂(上)
JS 事件循环
Flex 布局教程:语法篇
前端笔记 –JS 执行上下文
script 标签中 defer 和 async 属性的区别
[译]页面生命周期:DOMContentLoaded, load, beforeunload, unload 解析
再谈 load 与 DOMContentLoaded
面试带你飞:这是一份全面的 计算机网络基础 总结攻略
深拷贝的终极探索(99% 的人都不知道)

正文完
 0