最后更多分享:前端字节跳动真题解析
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
.outer {
height: 100px;
}
.left {
float: left;
width: 200px;
height: 100%;
background: lightcoral;
}
.right {
overflow: auto;
height: 100%;
background: lightseagreen;
}
复制代码
- 利用 flex 布局,左边元素固定宽度,右边的元素设置 flex: 1 。
.outer {
display: flex;
height: 100px;
}
.left {
width: 200px;
height: 100%;
background: lightcoral;
}
.right {
flex: 1;
height: 100%;
background: lightseagreen;
}
复制代码
- 利用绝对定位,父级元素设为相对定位。左边元素 absolute 定位,宽度固定。右边元素的 margin-left 的值设为左边元素的宽度值。
.outer {
position: relative;
height: 100px;
}
.left {
position: absolute;
width: 200px;
height: 100%;
background: lightcoral;
}
.right {
margin-left: 200px;
height: 100%;
background: lightseagreen;
}
复制代码
- 利用绝对定位,父级元素设为相对定位。左边元素宽度固定,右边元素 absolute 定位, left 为宽度大小,其余方向定位为 0 。
.outer {
position: relative;
height: 100px;
}
.left {
width: 200px;
height: 100%;
background: lightcoral;
}
.right {
position: absolute;
left: 200px;
top: 0;
right: 0;
bottom: 0;
height: 100%;
background: lightseagreen;
}
复制代码
2.6 实现圣杯布局和双飞翼布局(经典三分栏布局)
圣杯布局和双飞翼布局的目的:
-
三栏布局,中间一栏最先加载和渲染(内容最重要,这就是为什么还需要了解这种布局的原因)。
-
两侧内容固定,中间内容随着宽度自适应。
-
一般用于 PC 网页。
圣杯布局和双飞翼布局的技术总结:
-
使用 float 布局。
-
两侧使用 margin 负值,以便和中间内容横向重叠。
-
防止中间内容被两侧覆盖,圣杯布局用 padding ,双飞翼布局用 margin 。
圣杯布局: HTML 结构:
<divid="container"class="clearfix"><pclass="center">我是中间</p><pclass="left">我是左边</p><pclass="right">我是右边</p></div>复制代码
CSS 样式:
#container {
padding-left: 200px;
padding-right: 150px;
overflow: auto;
}
#containerp {
float: left;
}
.center {
width: 100%;
background-color: lightcoral;
}
.left {
width: 200px;
position: relative;
left: -200px;
margin-left: -100%;
background-color: lightcyan;
}
.right {
width: 150px;
margin-right: -150px;
background-color: lightgreen;
}
.clearfix:after {
content: "";
display: table;
clear: both;
}
复制代码
双飞翼布局: HTML 结构:
<divid="main"class="float"><divid="main-wrap">main</div></div><divid="left"class="float">left</div><divid="right"class="float">right</div>复制代码
CSS 样式:
.float {
float: left;
}
#main {
width: 100%;
height: 200px;
background-color: lightpink;
}
#main-wrap {
margin: 0190px0190px;
}
#left {
width: 190px;
height: 200px;
background-color: lightsalmon;
margin-left: -100%;
}
#right {
width: 190px;
height: 200px;
background-color: lightskyblue;
margin-left: -190px;
}
复制代码
tips:上述代码中 margin-left: -100% 相对的是父元素的 content 宽度,即不包含 paddig 、 border 的宽度。
其实以上问题需要掌握 margin 负值问题 即可很好理解。
2.7 水平垂直居中多种实现方式
- 利用绝对定位,设置 left: 50% 和 top: 50% 现将子元素左上角移到父元素中心位置,然后再通过 translate 来调整子元素的中心点到父元素的中心。该方法可以不定宽高。
.father {
position: relative;
}
.son {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
复制代码
- 利用绝对定位,子元素所有方向都为 0 ,将 margin 设置为 auto ,由于宽高固定,对应方向实现平分,该方法必须盒子有宽高。
.father {
position: relative;
}
.son {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0px;
margin: auto;
height: 100px;
width: 100px;
}
复制代码
- 利用绝对定位,设置 left: 50% 和 top: 50% 现将子元素左上角移到父元素中心位置,然后再通过 margin-left 和 margin-top 以子元素自己的一半宽高进行负值赋值。该方法必须定宽高。
.father {
position: relative;
}
.son {
position: absolute;
left: 50%;
top: 50%;
width: 200px;
height: 200px;
margin-left: -100px;
margin-top: -100px;
}
复制代码
- 利用 flex ,最经典最方便的一种了,不用解释,定不定宽高无所谓的。
.father {
display: flex;
justify-content: center;
align-items: center;
}
复制代码
其实还有很多方法,比如 display: grid 或 display: table-cell 来做,有兴趣点击下面这篇文章可以了解下:
面试官:你能实现多少种水平垂直居中的布局(定宽高和不定宽高)。
2.8 flex 布局
这一块内容看 Flex 布局教程 就够了。
这里有个小问题,很多时候我们会用到 flex: 1 ,它具体包含了以下的意思:
-
flex-grow: 1 :该属性默认为 0 ,如果存在剩余空间,元素也不放大。设置为 1 代表会放大。
-
flex-shrink: 1 :该属性默认为 1 ,如果空间不足,元素缩小。
-
flex-basis: 0% :该属性定义在分配多余空间之前,元素占据的主轴空间。浏览器就是根据这个属性来计算是否有多余空间的。默认值为 auto ,即项目本身大小。设置为 0% 之后,因为有 flex-grow 和 flex-shrink 的设置会自动放大或缩小。在做两栏布局时,如果右边的自适应元素 flex-basis 设为 auto 的话,其本身大小将会是 0 。
2.9 line-height 如何继承?
-
父元素的 line-height 写了具体数值,比如 30px,则子元素 line-height 继承该值。
-
父元素的 line-height 写了比例,比如 1.5 或 2,则子元素 line-height 也是继承该比例。
-
父元素的 line-height 写了百分比,比如 200%,则子元素 line-height 继承的是父元素 font-size * 200% 计算出来的值。
三、js 基础
js 的考察其实来回就那些东西,不过就我自己而已学习的时候理解是真的理解了,但是忘也确实会忘(大家都说理解了一定不会忘,但是要答全的话还是需要理解+背)。
1、数据类型
以下是比较重要的几个 js 变量要掌握的点。
1.1 基本的数据类型介绍,及值类型和引用类型的理解
在 JS 中共有 8 种基础的数据类型,分别为: Undefined 、 Null 、 Boolean 、 Number 、 String 、 Object 、 Symbol 、 BigInt 。
其中 Symbol 和 BigInt 是 ES6 新增的数据类型,可能会被单独问:
-
Symbol 代表独一无二的值,最大的用法是用来定义对象的唯一属性名。
-
BigInt 可以表示任意大小的整数。
值类型的赋值变动过程如下:
let a = 100;
let b = a;
a = 200;
console.log(b); // 100复制代码
值类型是直接存储在**栈(stack)**中的简单数据段,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储;
引用类型的赋值变动过程如下:
let a = { age: 20 };
let b = a;
b.age = 30;
console.log(a.age); // 30复制代码
引用类型存储在**堆(heap)**中的对象,占据空间大、大小不固定。如果存储在栈中,将会影响程序运行的性能;
1.2 数据类型的判断
- typeof:能判断所有值类型,函数。不可对 null、对象、数组进行精确判断,因为都返回 object 。
console.log(typeofundefined); // undefinedconsole.log(typeof2); // numberconsole.log(typeoftrue); // booleanconsole.log(typeof"str"); // stringconsole.log(typeofSymbol("foo")); // symbolconsole.log(typeof2172141653n); // bigintconsole.log(typeoffunction () {}); // function// 不能判别console.log(typeof []); // objectconsole.log(typeof {}); // objectconsole.log(typeofnull); // object复制代码
- instanceof:能判断对象类型,不能判断基本数据类型,其内部运行机制是判断在其原型链中能否找到该类型的原型。比如考虑以下代码:
classPeople {}
classStudentextendsPeople {}
const vortesnail = newStudent();
console.log(vortesnail instanceofPeople); // trueconsole.log(vortesnail instanceofStudent); // true复制代码
其实现就是顺着原型链去找,如果能找到对应的 Xxxxx.prototype 即为 true 。比如这里的 vortesnail 作为实例,顺着原型链能找到 Student.prototype 及 People.prototype ,所以都为 true 。
- Object.prototype.toString.call():所有原始数据类型都是能判断的,还有 Error 对象,Date 对象等。
Object.prototype.toString.call(2); // "[object Number]"Object.prototype.toString.call(""); // "[object String]"Object.prototype.toString.call(true); // "[object Boolean]"Object.prototype.toString.call(undefined); // "[object Undefined]"Object.prototype.toString.call(null); // "[object Null]"Object.prototype.toString.call(Math); // "[object Math]"Object.prototype.toString.call({}); // "[object Object]"Object.prototype.toString.call([]); // "[object Array]"Object.prototype.toString.call(function () {}); // "[object Function]"复制代码
在面试中有一个经常被问的问题就是:如何判断变量是否为数组?
Array.isArray(arr); // true
arr.__proto__ === Array.prototype; // true
arr instanceofArray; // trueObject.prototype.toString.call(arr); // "[object Array]"复制代码
大厂面试题分享 面试题库
前端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库
1.3 手写深拷贝
这个题一定要会啊!笔者面试过程中疯狂被问到!
文章推荐:如何写出一个惊艳面试官的深拷贝?
/**
* 深拷贝
* @param {Object} obj 要拷贝的对象
* @param {Map} map 用于存储循环引用对象的地址
*/functiondeepClone(obj = {}, map = newMap()) {
if (typeof obj !== "object") {
return obj;
}
if (map.get(obj)) {
return map.get(obj);
}
let result = {};
// 初始化返回结果if (
obj instanceofArray ||
// 加 || 的原因是为了防止 Array 的 prototype 被重写,Array.isArray 也是如此Object.prototype.toString(obj) === "[object Array]"
) {
result = [];
}
// 防止循环引用
map.set(obj, result);
for (const key in obj) {
// 保证 key 不是原型属性if (obj.hasOwnProperty(key)) {
// 递归调用
result[key] = deepClone(obj[key], map);
}
}
// 返回结果return result;
}
复制代码
1.4 根据 0.1+0.2 ! == 0.3,讲讲 IEEE 754 ,如何让其相等?
建议先阅读这篇文章了解 IEEE 754 :硬核基础二进制篇(一)0.1 + 0.2 != 0.3 和 IEEE-754 标准。 再阅读这篇文章了解如何运算:0.1 + 0.2 不等于 0.3?为什么 JavaScript 有这种“骚”操作?。
原因总结:
-
进制转换 :js 在做数字计算的时候,0.1 和 0.2 都会被转成二进制后无限循环 ,但是 js 采用的 IEEE 754 二进制浮点运算,最大可以存储 53 位有效数字,于是大于 53 位后面的会全部截掉,将导致精度丢失。
-
对阶运算 :由于指数位数不相同,运算时需要对阶运算,阶小的尾数要根据阶差来右移(0舍1入),尾数位移时可能会发生数丢失的情况,影响精度。
解决办法:
- 转为整数(大数)运算。
functionadd(a, b) {
const maxLen = Math.max(
a.toString().split(".")[1].length,
b.toString().split(".")[1].length
);
const base = 10 ** maxLen;
const bigA = BigInt(base * a);
const bigB = BigInt(base * b);
const bigRes = (bigA + bigB) / BigInt(base); // 如果是 (1n + 2n) / 10n 是等于 0n的。。。returnNumber(bigRes);
}
复制代码
这里代码是有问题的,因为最后计算 bigRes 的大数相除(即 /)是会把小数部分截掉的,所以我很疑惑为什么网络上很多文章都说可以通过先转为整数运算再除回去,为了防止转为的整数超出 js 表示范围,还可以运用到 ES6 新增的大数类型,我真的很疑惑,希望有好心人能解答下。
- 使用 Number.EPSILON 误差范围。
functionisEqual(a, b) {
returnMath.abs(a - b) < Number.EPSILON;
}
console.log(isEqual(0.1 + 0.2, 0.3)); // true复制代码
Number.EPSILON 的实质是一个可以接受的最小误差范围,一般来说为 Math.pow(2, -52) 。
- 转成字符串,对字符串做加法运算。
// 字符串数字相加var addStrings = function (num1, num2) {
let i = num1.length - 1;
let j = num2.length - 1;
const res = [];
let carry = 0;
while (i >= 0 || j >= 0) {
const n1 = i >= 0 ? Number(num1[i]) : 0;
const n2 = j >= 0 ? Number(num2[j]) : 0;
const sum = n1 + n2 + carry;
res.unshift(sum % 10);
carry = Math.floor(sum / 10);
i--;
j--;
}
if (carry) {
res.unshift(carry);
}
return res.join("");
};
functionisEqual(a, b, sum) {
const [intStr1, deciStr1] = a.toString().split(".");
const [intStr2, deciStr2] = b.toString().split(".");
const inteSum = addStrings(intStr1, intStr2); // 获取整数相加部分const deciSum = addStrings(deciStr1, deciStr2); // 获取小数相加部分return inteSum + "." + deciSum === String(sum);
}
console.log(isEqual(0.1, 0.2, 0.3)); // true复制代码
这是 leetcode 上一道原题:面试题库。区别在于原题没有考虑小数,但是也是很简单的,我们分为两个部分计算就行。
2、 原型和原型链
可以说这部分每家面试官都会问了。。首先理解的话,其实一张图即可,一段代码即可。
functionFoo() {}
let f1 = newFoo();
let f2 = newFoo();
复制代码
千万别畏惧下面这张图,特别有用,一定要搞懂,熟到提笔就能默画出来。
总结:
-
原型:每一个 JavaScript 对象(null 除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型"继承"属性,其实就是 prototype 对象。
-
原型链:由相互关联的原型组成的链状结构就是原型链。
先说出总结的话,再举例子说明如何顺着原型链找到某个属性。
推荐的阅读:JavaScript 深入之从原型到原型链 掌握基本概念,再阅读这篇文章轻松理解 JS 原型原型链加深上图的印象。
3、 作用域与作用域链
-
作用域:规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。换句话说,作用域决定了代码区块中变量和其他资源的可见性。(全局作用域、函数作用域、块级作用域)
-
作用域链:从当前作用域开始一层层往上找某个变量,如果找到全局作用域还没找到,就放弃寻找 。这种层级关系就是作用域链。(由多个执行上下文的变量对象构成的链表就叫做作用域链,学习下面的内容之后再考虑这句话)
需要注意的是,js 采用的是静态作用域,所以函数的作用域在函数定义时就确定了。
推荐阅读:先阅读JavaScript 深入之词法作用域和动态作用域,再阅读深入理解 JavaScript 作用域和作用域链。
4、 执行上下文
总结:当 JavaScript 代码执行一段可执行代码时,会创建对应的执行上下文。对于每个执行上下文,都有三个重要属性:
-
变量对象(Variable object,VO);
-
作用域链(Scope chain);
-
this。(关于 this 指向问题,在上面推荐的深入系列也有讲从 ES 规范讲的,但是实在是难懂,对于应付面试来说以下这篇阮一峰的文章应该就可以了:JavaScript 的 this 原理)
5、 闭包
根据 MDN 中文的定义,闭包的定义如下:
在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。可以在一个内层函数中访问到其外层函数的作用域。
也可以这样说:
闭包是指那些能够访问自由变量的函数。 自由变量是指在函数中使用的,但既不是
函数参数也不是
函数的局部变量的
变量。 闭包 = 函数 + 函数能够访问的自由变量。
在经过上一小节“执行上下文”的学习,再来阅读这篇文章:JavaScript 深入之闭包,你会对闭包的实质有一定的了解。在回答时,我们这样答:
在某个内部函数的执行上下文创建时,会将父级函数的活动对象加到内部函数的 [[scope]] 中,形成作用域链,所以即使父级函数的执行上下文销毁(即执行上下文栈弹出父级函数的执行上下文),但是因为其活动对象还是实际存储在内存中可被内部函数访问到的,从而实现了闭包。
闭包应用: 函数作为参数被传递:
functionprint(fn) {
const a = 200;
fn();
}
const a = 100;
functionfn() {
console.log(a);
}
print(fn); // 100复制代码
函数作为返回值被返回:
functioncreate() {
const a = 100;
returnfunction () {
console.log(a);
};
}
const fn = create();
const a = 200;
fn(); // 100复制代码
闭包:自由变量的查找,是在函数定义的地方,向上级作用域查找。不是在执行的地方。
应用实例:比如缓存工具,隐藏数据,只提供 API 。
functioncreateCache() {
const data = {}; // 闭包中被隐藏的数据,不被外界访问return {
set: function (key, val) {
data[key] = val;
},
get: function (key) {
return data[key];
},
};
}
const c = createCache();
c.set("a", 100);
console.log(c.get("a")); // 100复制代码
6、 call、apply、bind 实现
这部分实现还是要知道的,就算工作中不会自己手写,但是说不准面试官就是要问,知道点原理也好,可以扩宽我们写代码的思路。
call
call() 方法在使用一个指定的 this 值和若干个指定的参数值的前提下调用某个函数或方法。
举个例子:
var obj = {
value: "vortesnail",
};
functionfn() {
console.log(this.value);
}
fn.call(obj); // vortesnail复制代码
通过 call 方法我们做到了以下两点:
-
call 改变了 this 的指向,指向到 obj 。
-
fn 函数执行了。
那么如果我们自己写 call 方法的话,可以怎么做呢?我们先考虑改造 obj 。
var obj = {
value: "vortesnail",
fn: function () {
console.log(this.value);
},
};
obj.fn(); // vortesnail复制代码
这时候 this 就指向了 obj ,但是这样做我们手动给 obj 增加了一个 fn 属性,这显然是不行的,不用担心,我们执行完再使用对象属性的删除方法(delete)不就行了?
obj.fn = fn;
obj.fn();
delete obj.fn;
复制代码
根据这个思路,我们就可以写出来了:
Function.prototype.myCall = function (context) {
// 判断调用对象if (typeofthis !== "function") {
thrownewError("Type error");
}
// 首先获取参数let args = [...arguments].slice(1);
let result = null;
// 判断 context 是否传入,如果没有传就设置为 window
context = context || window;
// 将被调用的方法设置为 context 的属性// this 即为我们要调用的方法
context.fn = this;
// 执行要被调用的方法
result = context.fn(...args);
// 删除手动增加的属性方法delete context.fn;
// 将执行结果返回return result;
};
复制代码
apply
我们会了 call 的实现之后,apply 就变得很简单了,他们没有任何区别,除了传参方式。
Function.prototype.myApply = function (context) {
if (typeofthis !== "function") {
thrownewError("Type error");
}
let result = null;
context = context || window;
// 与上面代码相比,我们使用 Symbol 来保证属性唯一// 也就是保证不会重写用户自己原来定义在 context 中的同名属性const fnSymbol = Symbol();
context[fnSymbol] = this;
// 执行要被调用的方法if (arguments[1]) {
result = context[fnSymbol](...arguments[1]);
} else {
result = context[fnSymbol]();
}
delete context[fnSymbol];
return result;
};
复制代码
bind
bind 返回的是一个函数,这个地方可以详细阅读这篇文章,讲的非常清楚:解析 bind 原理,并手写 bind 实现。
Function.prototype.myBind = function (context) {
// 判断调用对象是否为函数if (typeofthis !== "function") {
thrownewError("Type error");
}
// 获取参数const args = [...arguments].slice(1),
const fn = this;
returnfunctionFn() {
return fn.apply(
thisinstanceofFn ? this : context,
// 当前的这个 arguments 是指 Fn 的参数
args.concat(...arguments)
);
};
};
复制代码
7、 new 实现
-
首先创一个新的空对象。
-
根据原型链,设置空对象的 __proto__ 为构造函数的 prototype 。
-
构造函数的 this 指向这个对象,执行构造函数的代码(为这个新对象添加属性)。
-
判断函数的返回值类型,如果是引用类型,就返回这个引用类型的对象。
functionmyNew(context) {
const obj = newObject();
obj.__proto__ = context.prototype;
const res = context.apply(obj, [...arguments].slice(1));
returntypeof res === "object" ? res : obj;
}
复制代码
大厂面试题分享 面试题库
前端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库
8、 异步
这部分着重要理解 Promise、async awiat、event loop 等。
8.1 event loop、宏任务和微任务
首先推荐一个可以在线看代码流程的网站:loupe。 然后看下这个视频学习下:到底什么是 Event Loop 呢?
简单的例子:
console.log("Hi");
setTimeout(functioncb() {
console.log("cb"); // cb 即 callback
}, 5000);
console.log("Bye");
复制代码
它的执行过程是这样的:
Web APIs 会创建对应的线程,比如 setTimeout 会创建定时器线程,ajax 请求会创建 http 线程。。。这是由 js 的运行环境决定的,比如浏览器。
看完上面的视频之后,至少大家画 Event Loop 的图讲解不是啥问题了,但是涉及到宏任务和微任务,我们还得拜读一下这篇文章:这一次,彻底弄懂 JavaScript 执行机制。如果意犹未尽,不如再读下这篇非常详细带有大量动图的文章:做一些动图,学习一下 EventLoop。想了解事件循环和页面渲染之间关系的又可以再阅读这篇文章:深入解析你不知道的 EventLoop 和浏览器渲染、帧动画、空闲回调(动图演示)。
注意:1.Call Stack 调用栈空闲 -> 2.尝试 DOM 渲染 -> 触发 Event loop。
-
每次 Call Stack 清空(即每次轮询结束),即同步任务执行完。
-
都是 DOM 重新渲染的机会,DOM 结构有改变则重新渲染。
-
然后再去触发下一次 Event loop。
宏任务:setTimeout,setInterval,Ajax,DOM 事件。 微任务:Promise async/await。
两者区别:
-
宏任务:DOM 渲染后触发,如 setTimeout 、setInterval 、DOM 事件 、script 。
-
微任务:DOM 渲染前触发,如 Promise.then 、MutationObserver 、Node 环境下的 process.nextTick 。
从 event loop 解释,为何微任务执行更早?
-
微任务是 ES6 语法规定的(被压入 micro task queue)。
-
宏任务是由浏览器规定的(通过 Web APIs 压入 Callback queue)。
-
宏任务执行时间一般比较长。
-
每一次宏任务开始之前一定是伴随着一次 event loop 结束的,而微任务是在一次 event loop 结束前执行的。
8.2 Promise
关于这一块儿没什么好说的,最好是实现一遍 Promise A+ 规范,多少有点印象,当然面试官也不会叫你默写一个完整的出来,但是你起码要知道实现原理。
关于 Promise 的所有使用方式,可参照这篇文章:
ECMAScript 6 入门 - Promise 对象。 手写 Promise 源码的解析文章,可阅读此篇文章:
从一道让我失眠的 Promise 面试题开始,深入分析 Promise 实现细节。 关于 Promise 的面试题,可参考这篇文章:
要就来 45 道 Promise 面试题一次爽到底。
实现一个 Promise.all:
Promise.all = function (promises) {
returnnewPromise((resolve, reject) => {
// 参数可以不是数组,但必须具有 Iterator 接口if (typeof promises[Symbol.iterator] !== "function") {
reject("Type error");
}
if (promises.length === 0) {
resolve([]);
} else {
const res = [];
let count = 0;
const len = promises.length;
for (let i = 0; i < len; i++) {
//考虑到 promises[i] 可能是 thenable 对象也可能是普通值Promise.resolve(promises[i])
.then((data) => {
res[i] = data;
if (++count === len) {
resolve(res);
}
})
.catch((err) => {
reject(err);
});
}
}
});
};
复制代码
8.3 async/await 和 Promise 的关系
-
async/await 是消灭异步回调的终极武器。
-
但和 Promise 并不互斥,反而,两者相辅相成。
-
执行 async 函数,返回的一定是 Promise 对象。
-
await 相当于 Promise 的 then。
-
tru…catch 可捕获异常,代替了 Promise 的 catch。
9、 浏览器的垃圾回收机制
这里看这篇文章即可:「硬核 JS」你真的了解垃圾回收机制吗。
总结一下:
有两种垃圾回收策略:
-
标记清除:标记阶段即为所有活动对象做上标记,清除阶段则把没有标记(也就是非活动对象)销毁。
-
引用计数:它把对象是否不再需要简化定义为对象有没有其他对象引用到它。如果没有引用指向该对象(引用计数为 0),对象将被垃圾回收机制回收。
标记清除的缺点:
-
内存碎片化,空闲内存块是不连续的,容易出现很多空闲内存块,还可能会出现分配所需内存过大的对象时找不到合适的块。
-
分配速度慢,因为即便是使用 First-fit 策略,其操作仍是一个 O(n) 的操作,最坏情况是每次都要遍历到最后,同时因为碎片化,大对象的分配效率会更慢。
解决以上的缺点可以使用 **标记整理(Mark-Compact)算法 **,标记结束后,标记整理算法会将活着的对象(即不需要清理的对象)向内存的一端移动,最后清理掉边界的内存(如下图)
引用计数的缺点:
-
需要一个计数器,所占内存空间大,因为我们也不知道被引用数量的上限。
-
解决不了循环引用导致的无法回收问题。
V8 的垃圾回收机制也是基于标记清除算法,不过对其做了一些优化。
-
针对新生区采用并行回收。
-
针对老生区采用增量标记与惰性回收。
10、 实现一个 EventMitter 类
EventMitter 就是发布订阅模式的典型应用:
exportclassEventEmitter {
private_events: Record<string, Array<Function>>;
constructor() {
this._events = Object.create(null);
}
emit(evt: string, ...args: any[]) {
if (!this._events[evt]) returnfalse;
const fns = [...this._events[evt]];
fns.forEach((fn) => {
fn.apply(this, args);
});
returntrue;
}
on(evt: string, fn: Function) {
if (typeof fn !== "function") {
thrownewTypeError("The evet-triggered callback must be a function");
}
if (!this._events[evt]) {
this._events[evt] = [fn];
} else {
this._events[evt].push(fn);
}
}
once(evt: string, fn: Function) {
constexecFn = () => {
fn.apply(this);
this.off(evt, execFn);
};
this.on(evt, execFn);
}
off(evt: string, fn?: Function) {
if (!this._events[evt]) return;
if (!fn) {
this._events[evt] && (this._events[evt].length = 0);
}
let cb;
const cbLen = this._events[evt].length;
for (let i = 0; i < cbLen; i++) {
cb = this._events[evt][i];
if (cb === fn) {
this._events[evt].splice(i, 1);
break;
}
}
}
removeAllListeners(evt?: string) {
if (evt) {
this._events[evt] && (this._events[evt].length = 0);
} else {
this._events = Object.create(null);
}
}
}
复制代码
四、web 存储
要掌握 cookie,localStorage 和 sessionStorage。
1、cookie
-
本身用于浏览器和 server 通讯。
-
被“借用”到本地存储来的。
-
可用 document.cookie = ‘…’ 来修改。
其缺点:
-
存储大小限制为 4KB。
-
http 请求时需要发送到服务端,增加请求数量。
-
只能用 document.cookie = ‘…’ 来修改,太过简陋。
2、localStorage 和 sessionStorage
-
HTML5 专门为存储来设计的,最大可存 5M。
-
API 简单易用, setItem getItem。
-
不会随着 http 请求被发送到服务端。
它们的区别:
-
localStorage 数据会永久存储,除非代码删除或手动删除。
-
sessionStorage 数据只存在于当前会话,浏览器关闭则清空。
-
一般用 localStorage 会多一些。
五、Http
前端工程师做出网页,需要通过网络请求向后端获取数据,因此 http 协议是前端面试的必考内容。
1、http 状态码
1.1 状态码分类
-
1xx - 服务器收到请求。
-
2xx - 请求成功,如 200。
-
3xx - 重定向,如 302。
-
4xx - 客户端错误,如 404。
-
5xx - 服务端错误,如 500。
1.2 常见状态码
-
200 - 成功。
-
301 - 永久重定向(配合 location,浏览器自动处理)。
-
302 - 临时重定向(配合 location,浏览器自动处理)。
-
304 - 资源未被修改。
-
403 - 没权限。
-
404 - 资源未找到。
-
500 - 服务器错误。
-
504 - 网关超时。
1.3 关于协议和规范
-
状态码都是约定出来的。
-
要求大家都跟着执行。
-
不要违反规范,例如 IE 浏览器。
2、http 缓存
-
关于缓存的介绍。
-
http 缓存策略(强制缓存 + 协商缓存)。
-
刷新操作方式,对缓存的影响。
4.1 关于缓存
什么是缓存? 把一些不需要重新获取的内容再重新获取一次
为什么需要缓存? 网络请求相比于 CPU 的计算和页面渲染是非常非常慢的。
哪些资源可以被缓存? 静态资源,比如 js css img。
4.2 强制缓存
Cache-Control:
-
在 Response Headers 中。
-
控制强制缓存的逻辑。
-
例如 Cache-Control: max-age=3153600(单位是秒)
Cache-Control 有哪些值:
-
max-age:缓存最大过期时间。
-
no-cache:可以在客户端存储资源,每次都必须去服务端做新鲜度校验,来决定从服务端获取新的资源(200)还是使用客户端缓存(304)。
-
no-store:永远都不要在客户端存储资源,永远都去原始服务器去获取资源。
4.3 协商缓存(对比缓存)
-
服务端缓存策略。
-
服务端判断客户端资源,是否和服务端资源一样。
-
一致则返回 304,否则返回 200 和最新的资源。
资源标识:
-
在 Response Headers 中,有两种。
-
Last-Modified:资源的最后修改时间。
-
Etag:资源的唯一标识(一个字符串,类似于人类的指纹)。
Last-Modified:
服务端拿到 if-Modified-Since 之后拿这个时间去和服务端资源最后修改时间做比较,如果一致则返回 304 ,不一致(也就是资源已经更新了)就返回 200 和新的资源及新的 Last-Modified。
Etag:
其实 Etag 和 Last-Modified 一样的,只不过 Etag 是服务端对资源按照一定方式(比如 contenthash)计算出来的唯一标识,就像人类指纹一样,传给客户端之后,客户端再传过来时候,服务端会将其与现在的资源计算出来的唯一标识做比较,一致则返回 304,不一致就返回 200 和新的资源及新的 Etag。
两者比较:
-
优先使用 Etag。
-
Last-Modified 只能精确到秒级。
-
如果资源被重复生成,而内容不变,则 Etag 更精确。
4.4 综述
4.4 三种刷新操作对 http 缓存的影响
-
正常操作:地址栏输入 url,跳转链接,前进后退等。
-
手动刷新:f5,点击刷新按钮,右键菜单刷新。
-
强制刷新:ctrl + f5,shift+command+r。
正常操作:强制缓存有效,协商缓存有效。 手动刷新:强制缓存失效,协商缓存有效。 强制刷新:强制缓存失效,协商缓存失效。
3. 面试
对于更多面试中可能出现的问题,我还是建议精读这篇三元的文章:HTTP 灵魂之问,巩固你的 HTTP 知识体系。
比如会被经常问到的: GET 和 POST 的区别。
-
从缓存的角度,GET 请求会被浏览器主动缓存下来,留下历史记录,而 POST 默认不会。
-
从编码的角度,GET 只能进行 URL 编码,只能接收 ASCII 字符,而 POST 没有限制。
-
从参数的角度,GET 一般放在 URL 中,因此不安全,POST 放在请求体中,更适合传输敏感信息。
-
从幂等性的角度,GET 是幂等的,而 POST 不是。(幂等表示执行相同的操作,结果也是相同的)
-
从 TCP 的角度,GET 请求会把请求报文一次性发出去,而 POST 会分为两个 TCP 数据包,首先发 header 部分,如果服务器响应 100(continue), 然后发 body 部分。(火狐浏览器除外,它的 POST 请求只发一个 TCP 包)
HTTP/2 有哪些改进?(很大可能问原理)
-
头部压缩。
-
多路复用。
-
服务器推送。
关于 HTTPS 的一些原理,可以阅读这篇文章:这一次,彻底理解 https 原理。接着你可以观看这个视频进行更进一步的学习:HTTPS 底层原理,面试官直接下跪,唱征服!
关于跨域问题,大部分文章都是理论性比较强,还不如读这篇文章,聊聊跨域的原理与解决方法,讲的非常清晰,我个人觉得对付面试就是先知道使用流程,把这个流程能自己说出来,然后再讲下原理即可。
大厂面试题分享 面试题库
前端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库
六、React
1、 React 事件机制,React 16 和 React 17 事件机制的不同
阅读这篇文章即可:一文吃透 react 事件系统原理。
为什么要自定义事件机制?
-
抹平浏览器差异,实现更好的跨平台。
-
避免垃圾回收,React 引入事件池,在事件池中获取或释放事件对象,避免频繁地去创建和销毁。
-
方便事件统一管理和事务机制。
2、class component
不排除现在还会有面试官问关于 class component 的问题。
2.1 生命周期
- 初始化阶段。
发生在 constructor 中的内容,在 constructor 中进行 state 、props 的初始化,在这个阶段修改 state,不会执行更新阶段的生命周期,可以直接对 state 赋值。
- 挂载阶段。
1. componentWillMount
发生在 render 函数之前,还没有挂载 Dom2. render
3. componentDidMount
发生在 render 函数之后,已经挂载 Dom复制代码
- 更新阶段。
更新阶段分为由 state 更新引起和 props 更新引起。
props 更新时:
1.componentWillReceiveProps(nextProps,nextState)
这个生命周期主要为我们提供对 props 发生改变的监听,如果你需要在 props 发生改变后,相应改变组件的一些 state。在这个方法中改变 state 不会二次渲染,而是直接合并 state。
2.shouldComponentUpdate(nextProps,nextState)
这个生命周期需要返回一个 Boolean 类型的值,判断是否需要更新渲染组件,优化 react 应用的主要手段之一,当返回 false 就不会再向下执行生命周期了,在这个阶段不可以 setState(),会导致循环调用。
3.componentWillUpdate(nextProps,nextState)
这个生命周期主要是给我们一个时机能够处理一些在 Dom 发生更新之前的事情,如获得 Dom 更新前某些元素的坐标、大小等,在这个阶段不可以 setState(),会导致循环调用。
**一直到这里 this.props 和 this.state 都还未发生更新**
4. render
5.componentDidUpdate(prevProps, prevState)
在此时已经完成渲染,Dom 已经发生变化,state 已经发生更新,prevProps、prevState 均为上一个状态的值。
state 更新时(具体同上)
1. shouldComponentUpdate
### 总结
我在成长过程中也是一路摸爬滚打,没有任何人的指点,所以走的很艰难。例如在大三的时候,如果有个学长可以阶段性的指点一二,如果有已经工作的师兄可以告诉我工作上需要什么,我应该前面的三年可以缩短一半;后来去面试bat,失败了有5、6次,每次也不知道具体是什么原因,都是靠面试回忆去猜测可能是哪方面的问题,回来学习和完善,当你真正去招人的时候,你就会知道面试记录是多么重要,面试官可以从面试记录里看到你的成长,总是去面试,总是没有成长,就会被定义为缺乏潜力。
**[开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】](https://bbs.csdn.net/topics/618166371)**
![image](https://img-blog.csdnimg.cn/img_convert/40330847300858cf9fdfc8ac4ac5b772.webp?x-oss-process=image/format,png)
![image](https://img-blog.csdnimg.cn/img_convert/caf5d0e644ed0dbec67cfe2690646519.webp?x-oss-process=image/format,png)
么要自定义事件机制?
* 抹平浏览器差异,实现更好的跨平台。
* 避免垃圾回收,React 引入事件池,在事件池中获取或释放事件对象,避免频繁地去创建和销毁。
* 方便事件统一管理和事务机制。
#### 2、class component
不排除现在还会有面试官问关于 class component 的问题。
##### 2.1 生命周期
* 初始化阶段。
发生在 constructor 中的内容,在 constructor 中进行 state 、props 的初始化,在这个阶段修改 state,不会执行更新阶段的生命周期,可以直接对 state 赋值。
* 挂载阶段。
- componentWillMount
发生在 render 函数之前,还没有挂载 Dom2. render - componentDidMount
发生在 render 函数之后,已经挂载 Dom复制代码
* 更新阶段。
更新阶段分为由 state 更新引起和 props 更新引起。
props 更新时:
1.componentWillReceiveProps(nextProps,nextState)
这个生命周期主要为我们提供对 props 发生改变的监听,如果你需要在 props 发生改变后,相应改变组件的一些 state。在这个方法中改变 state 不会二次渲染,而是直接合并 state。
2.shouldComponentUpdate(nextProps,nextState)
这个生命周期需要返回一个 Boolean 类型的值,判断是否需要更新渲染组件,优化 react 应用的主要手段之一,当返回 false 就不会再向下执行生命周期了,在这个阶段不可以 setState(),会导致循环调用。
3.componentWillUpdate(nextProps,nextState)
这个生命周期主要是给我们一个时机能够处理一些在 Dom 发生更新之前的事情,如获得 Dom 更新前某些元素的坐标、大小等,在这个阶段不可以 setState(),会导致循环调用。
一直到这里 this.props 和 this.state 都还未发生更新
4. render
5.componentDidUpdate(prevProps, prevState)
在此时已经完成渲染,Dom 已经发生变化,state 已经发生更新,prevProps、prevState 均为上一个状态的值。
state 更新时(具体同上)
- shouldComponentUpdate
总结
我在成长过程中也是一路摸爬滚打,没有任何人的指点,所以走的很艰难。例如在大三的时候,如果有个学长可以阶段性的指点一二,如果有已经工作的师兄可以告诉我工作上需要什么,我应该前面的三年可以缩短一半;后来去面试bat,失败了有5、6次,每次也不知道具体是什么原因,都是靠面试回忆去猜测可能是哪方面的问题,回来学习和完善,当你真正去招人的时候,你就会知道面试记录是多么重要,面试官可以从面试记录里看到你的成长,总是去面试,总是没有成长,就会被定义为缺乏潜力。
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
[外链图片转存中…(img-PP2MxTVh-1714975111040)]
[外链图片转存中…(img-4piMHtVb-1714975111041)]