0%

鲨鱼哥的前端面试整理

鲨鱼哥是个牛人,他这份总结是真的高频,原文请戳前端高频面试题整理,其中一些知识点有很多独到的细节,一定要好好把握。

1. CSS基础

1.1 三栏布局问题(左右固定宽度 中间自适应)

1.1.1 float + margin(浮动布局)

.html

1
2
3
4
5
6
<div class="container">
<div class="left">Left</div>
<!-- 右栏部分要写在中间内容之前 -->
<div class="right">Right</div>
<div class="main">Main</div>
</div>

.css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
body, html, .container {
height: 100%;
padding:0;
margin: 0;
}
/* 左边栏左浮动 */
.left{
float: left;
height: 100%;
width: 200px;
background: #333;
}
/* 中间栏自适应 */
.main {
height: 100%;
margin: 0 200px;
background: red;
}
/* 右边栏右浮动 */
.right {
float: right;
height: 100%;
width: 200px;
background: #333;
}

优点:快捷 简单 兼容性较好
缺点:有局限性 脱离文档流 需要清除浮动等

1.1.2 position(绝对布局)

.html

1
2
3
4
5
<div class="container">
<div class="left">Left</div>
<div class="main">Main</div>
<div class="right">Right</div>
</div>

.css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
body, html, .container {
height: 100%;
padding: 0;
margin: 0;
overflow: hidden;
}
/* 左右进行绝对定位 */
.left, .right {
position: absolute;
height: 100%;
top: 0;
background: #333;
}
.left {
left: 0;
width: 200px;
}
.right {
right: 0;
width: 200px;
}
/* 中间用margin空出左右元素所占的空间 */
.main {
height: 100%;
margin: 0 200px;
background: red;
}
/* 或者中间也进行绝对定位 */
.main {
position: absolute;
height: 100%;
left: 200px;
right: 200px;
background: red;
}

优点:简单粗暴
缺点:脱离文档流 高度未知会出现问题 可用性差

1.1.3 flex(弹性盒子布局)

.html

1
2
3
4
5
<div class="container">
<div class="left">Left</div>
<div class="main">Main</div>
<div class="right">Right</div>
</div>

.css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
body, html {
height: 100%;
padding: 0;
margin: 0;
overflow: hidden;
}
.container {
display: flex;
}
.left {
width: 200px;
background: red;
}
.main {
flex: 1;
background: blue;
}
.right {
width:200px;
background: red;
}

优点:比较完美 移动端首选
缺点:不兼容 ie9 及以下

1.1.4 table(表格布局)

.html

1
2
3
4
5
<div class="container">
<div class="left">Left</div>
<div class="main">Main</div>
<div class="right">Right</div>
</div>

.css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
body, html{
height: 100%;
padding: 0;
margin: 0;
overflow: hidden;
}
.container {
display: table;
width: 100%;
}
.container>div {
display: table-cell;
}
.left {
width: 200px;
background: red;
}
.main {
background: blue;
}
.right {
width: 200px;
background: red;
}

优点:兼容性很好(ie8 及以上) 父元素高度会被子元素撑开(不担心高度塌陷)
缺点:seo 不友好 当其中一个单元格高度超出的时候,其他的单元格也是会跟着一起变高的

1.1.5 Grid(网格布局)

.html

1
2
3
4
5
<div class="container">
<div class="left">Left</div>
<div class="main">Main</div>
<div class="right">Right</div>
</div>

.css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
body, html {
height: 100%;
padding: 0;
margin: 0;
overflow: hidden;
}
.container {
display: grid;
width: 100%;
grid-template-rows: 100px; /* 设置行高 */
grid-template-columns: 200px auto 200px; /* 设置列数属性 */
}
.left {
background: red;
}
.main {
background: blue;
}
.right {
background:red;
}

优点:简单强大 解决二维布局问题
缺点:不兼容 ie9 及以下

1.2 CSS 盒模型

img

标准模型和 ie 模型

img

1.3 BFC

BFC(Block Formatting Context)块级格式化上下文,是 Web 页面中盒模型布局的 CSS 渲染模式,指一个独立的渲染区域或者说是一个隔离的独立容器。

1.3.1 BFC 形成条件

1、浮动元素,float 除 none 以外的值;

2、定位元素,position(absolute,fixed);

3、display 为以下其中之一的值 inline-block,table-cell,table-caption;

4、overflow 除了 visible 以外的值(hidden,auto,scroll);

1.3.2 BFC 特性

1.内部的 Box 会在垂直方向上一个接一个的放置;

2.垂直方向上的距离由margin 决定;(解决外边距重叠问题)

3.bfc 的区域不会与 float 的元素区域重叠;(防止浮动文字环绕)

4.计算 bfc 的高度时,浮动元素也参与计算;(清除浮动)

5.bfc 就是页面上的一个独立容器,容器里面的子元素不会影响外面元素;

2. DOM事件

2.1 事件级别

2.1.1 DOM 0级

写法:el.οnclick=function(){}

当希望为同一个元素/标签绑定多个同类型事件的时候(如给上面的这个btn元素绑定3个点击事件),是不被允许的。DOM0事件绑定,给元素的事件行为绑定方法,这些方法都是在当前元素事件行为的冒泡阶段(或者目标阶段)执行的。

2.1.2 DOM 1级

由于DOM 1级中没有事件的相关内容,所以没有DOM 1级事件

2.1.3 DOM 2级

DOM 2级 写法:el.addEventListener(event-name, callback, useCapture)

event-name: 事件名称,可以是标准的DOM事件

callback: 回调函数,当事件触发时,函数会被注入一个参数为当前的事件对象 event

useCapture: 默认是false,代表事件句柄在冒泡阶段执行

2.1.4 DOM 3级

DOM 3级 写法和DOM2级一致 只是在DOM 2级事件的基础上添加了更多的事件类型

UI事件,当用户与页面上的元素交互时触发,如:load、scroll

焦点事件,当元素获得或失去焦点时触发,如:blur、focus

鼠标事件,当用户通过鼠标在页面执行操作时触发如:dblclick、mouseup

滚轮事件,当使用鼠标滚轮或类似设备时触发,如:mousewheel

文本事件,当在文档中输入文本时触发,如:textInput

键盘事件,当用户通过键盘在页面上执行操作时触发,如:keydown、keypress

合成事件,当为IME(输入法编辑器)输入字符时触发,如:compositionstart

变动事件,当底层DOM结构发生变化时触发,如:DOMsubtreeModified

同时DOM3级事件也允许使用者自定义一些事件。

2.2 DOM事件模型 事件流

事件模型分为:捕获和冒泡

事件流:

(1)捕获阶段:事件从window对象自上而下向目标节点传播的阶段;

(2)目标阶段:真正的目标节点正在处理事件的阶段;

(3)冒泡阶段:事件从目标节点自下而上向window对象传播的阶段。

img

2.3 事件委托(代理)

由于事件会在冒泡阶段向上传播到父节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件。这种方法叫做事件的代理(delegation)

优点:

1.减少内存消耗,提高性能(不需要为每一个子元素绑定事件)
2.动态绑定事件

2.4 Event对象使用

2.4.1 阻止默认行为

event. preventDefault()

什么是默认事件呢?例如表单一点击提交按钮(submit)跳转页面、a标签默认页面跳转或是锚点定位等

2.4.2 阻止冒泡

event.stopPropagation() 方法阻止事件冒泡到父元素,阻止任何父事件处理程序被执行

stopImmediatePropagation 既能阻止事件向父元素冒泡,也能阻止元素同事件类型的其它监听器被触发

2.4.3 event.target & event.currentTarget

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<div id="a">
aaaa
<div id="b">
bbbb
<div id="c">
cccc
<div id="d">
dddd
</div>
</div>
</div>
</div>

<script>
document.getElementById("a").addEventListener("click", function (e) {
console.log(
"target:" + e.target.id + "&currentTarget:" + e.currentTarget.id
);
});
document.getElementById("b").addEventListener("click", function (e) {
console.log(
"target:" + e.target.id + "&currentTarget:" + e.currentTarget.id
);
});
document.getElementById("c").addEventListener("click", function (e) {
console.log(
"target:" + e.target.id + "&currentTarget:" + e.currentTarget.id
);
});
document.getElementById("d").addEventListener("click", function (e) {
console.log(
"target:" + e.target.id + "&currentTarget:" + e.currentTarget.id
);
});
</script>

当我们点击最里层的元素d的时候,会依次输出:

1
2
3
4
target:d&currentTarget:d
target:d&currentTarget:c
target:d&currentTarget:b
target:d&currentTarget:a

由上诉例子可知: event.currentTarget始终是监听事件者,而event.target是事件的真正发出者

2.5 自定义事件

创建事件, Event是无法传递参数的

1
var event = new Event('build');

创建事件, CustomEvent是可以传递参数的

1
var event = new CustomEvent('build', { detail: elem.dataset.time });

监听事件Listen for the event.

1
elem.addEventListener('build', function (e) { /* ... */ }, false);

分发/触发事件Dispatch the event.

1
elem.dispatchEvent(event);

2.6 手写EventEmitter(发布订阅模式–简单版)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// 手写发布订阅模式 EventEmitter
class EventEmitter {
constructor() {
this.events = {};
}
// 实现订阅
on(type, callBack) {
if (!this.events) this.events = Object.create(null);
if (!this.events[type]) {
this.events[type] = [callBack];
} else {
this.events[type].push(callBack);
}
}
// 删除订阅
off(type, callBack) {
if (!this.events[type]) return;
this.events[type] = this.events[type].filter(item => {
return item !== callBack;
});
}
// 只执行一次订阅事件
once(type, callBack) {
function fn() {
callBack();
this.off(type, fn);
}
this.on(type, fn);
}
// 触发事件
emit(type, ...rest) {
this.events[type] && this.events[type].forEach(fn => fn.apply(this, rest));
}
}
// 使用如下
const event = new EventEmitter();

const handle = (...rest) => {
console.log(rest);
};

event.on("click", handle);

event.emit("click", 1, 2, 3, 4);

event.off("click", handle);

event.emit("click", 1, 2);

event.once("dbClick", () => {
console.log(123456);
});
event.emit("dbClick");
event.emit("dbClick");

3. JS相关

3.1 创建对象方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 1.对象字面量
let a = {
name: 'xxx'
};

// 2.构造函数
function Person(name){
this.name = name;
}
let b = new Person('xxx');

// 3.Object.create(proto, [propertiesObject])
// Object.create()方法创建的对象时,属性是在原型下面的
let c = Object.create({ name: 'xxx' });

3.2 原型链示意图

img

3.3 instanceof 原理

用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<script>
function Person() {}
function Foo() {}

// 显示改变Foo.prototype指向Person的实例对象(原型继承)
Foo.prototype = new Person();

let a = new Foo();

console.log(a.__proto__ === Foo.prototype); // true

console.log(a instanceof Foo); // true

console.log(Foo.prototype.__proto__ === Person.prototype); // true

console.log(a instanceof Person); // true

console.log(a instanceof Object); // true

// 这个时候改变Foo.prototype的指向

Foo.prototype = {};

// Foo.prototype已经不在a的原型链上面了

console.log(a.__proto__ === Foo.prototype); // false

console.log(a instanceof Foo); // false

// Person.prototype依然在a的原型链上面

console.log(a instanceof Person);// true
</script>

3.4 new运算符原理

1、创建一个空对象

2、让空对象的__proto__(IE没有该属性)成员指向了构造函数的prototype成员对象

3、使用apply调用构造器函数,属性和方法被添加到 this 引用的对象中

4、如果构造函数中没有返回其它对象,那么返回 this,即创建的这个的新对象,否则,返回构造函数中返回的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function _new(func) {
// 第一步 创建新对象
let obj= {};
// 第二步 空对象的_proto_指向了构造函数的prototype成员对象
obj.__proto__ = func.prototype;
// 一二步合并就相当于 let obj = Object.create(func.prototype);

// 第三步 使用apply调用构造器函数,属性和方法被添加到 this 引用的对象中
let result = func.apply(obj);
if (result && (typeof (result) === "object" || typeof (result) === "function")) {
// 如果构造函数执行的结果返回的是一个对象,那么返回这个对象
return result;
}
// 如果构造函数返回的不是一个对象,返回创建的新对象
return obj;
}

3.5 JS实现继承的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 定义一个父类
function Father(name) {
// 属性
this.name = name || "father";
// 实例方法
this.sayName = function() {
console.log(this.name);
};
this.color = ["red", "blue"];
}
// 原型方法
Father.prototype.age = 18;
Father.prototype.sayAge = function() {
console.log(this.age);
};

3.5.1 原型链继承

将父类的实例作为子类的原型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Son(name) {
this.name = name || "son";
}

Son.prototype = new Father();

let s1 = new Son("s1");
let s2 = new Son("s2");

s1.color.push("black");

console.log(s1.name); // s1
console.log(s1.color); // ['red', 'blue', 'black']
console.log(s1.age); // 18
s1.sayAge(); // 18
console.log(s2.name); // s2
console.log(s2.color); // ['red', 'blue', 'black']

优点:

  1. 简单,易于实现
  2. 父类新增原型方法、原型属性,子类都能访问到

缺点:

  1. 无法实现多继承,因为原型一次只能被一个实例更改
  2. 来自原型对象的所有属性被所有实例共享(上诉例子中的color属性)
  3. 创建子类实例时,无法向父构造函数传参

3.5.2 构造继承继承

复制父类的实例属性给子类

1
2
3
4
5
6
7
8
9
10
11
function Son(name) {
Father.call(this, "我是传给父类的参数");
this.name = name || "son";
}
let s = new Son("son");
console.log(s.name); // son
// s.sayAge(); // 抛出错误(无法继承父类原型方法)
s.sayName(); // son
console.log(s.age); // undefined (无法继承父类原型属性)
console.log(s instanceof Father); // false
console.log(s instanceof Son); // true

优点:

  1. 解决了原型链继承中子类实例共享父类引用属性的问题
  2. 创建子类实例时,可以向父类传递参数
  3. 可以实现多继承(call多个父类对象)

缺点:

  1. 实例并不是父类的实例,只是子类的实例
  2. 只能继承父类实例的属性和方法,不能继承其原型上的属性和方法
  3. 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能

3.5.3 组合继承

将原型链和借用构造函数的技术组合到一块。使用原型链实现对原型属性和方法的继承,而通过构造函数来实现对实例属性的继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Son(name) {
// 第一次调用父类构造器 子类实例增加父类实例
Father.call(this, "我是传给父类的参数");
this.name = name || "son";
}
// 经过new运算符 第二次调用父类构造器 子类原型也增加了父类实例
Son.prototype = new Father();

let s = new Son("son");
console.log(s.name); // son
s.sayAge(); // 18
s.sayName(); // son
console.log(s.age); // 18
console.log(s instanceof Father); // true
console.log(s instanceof Son); // true
console.log(s.constructor === Father); // true
console.log(s.constructor === Son); // false

优点:

  1. 弥补了构造继承的缺点,现在既可以继承实例的属性和方法,也可以继承原型的属性和方法
  2. 既是子类的实例,也是父类的实例
  3. 可以向父类传递参数
  4. 函数可以复用

缺点:

  1. 调用了两次父类构造函数,生成了两份实例
  2. constructor指向问题

3.5.4 实例继承

为父类实例添加新特征,作为子类实例返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Son(name) {
let f = new Father('传给父类的参数');
f.name = name || 'son';
return f;
}

let s = new Son("son"); //或者直接调用子类构造函数 let s = Son("son");
console.log(s.name); // son
s.sayAge(); // 18
s.sayName(); // son
console.log(s.age); // 18
console.log(s instanceof Father); // true
console.log(s instanceof Son); // false
console.log(s.constructor === Father); // true
console.log(s.constructor === Son); // false

优点:

  1. 不限制调用方式,不管是new 子类()还是子类(),返回的对象具有相同的效果

缺点:

  1. 实例是父类的实例,不是子类的实例
  2. 不支持多继承

3.5.5 拷贝继承

对父类实例中的的方法与属性拷贝给子类的原型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Son(name) {
let f = new Father("传给父类的参数");
for (let k in f) {
Son.prototype[k] = f[k];
}
Son.prototype.name = name;
}

let s = new Son("son");
console.log(s.name); // son
s.sayAge(); // 18
s.sayName(); // son
console.log(s.age); // 18
console.log(s instanceof Father); // false
console.log(s instanceof Son); // true
console.log(s.constructor === Father); // false
console.log(s.constructor === Son); // true

优点:

  1. 支持多继承

缺点:

  1. 效率低,性能差,占用内存高(因为需要拷贝父类属性)
  2. 无法获取父类不可枚举的方法(不可枚举的方法,不能使用for-in访问到)

3.5.6 寄生组合继承

通过寄生方式,砍掉父类的实例属性,避免了组合继承生成两份实例的缺点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function Son(name) {
Father.call(this);
this.name = name || "son";
}

// 方法一 自己动手创建一个中间类
// (function() {
// let NoneFun = function() {};
// NoneFun.prototype = Father.prototype;
// Son.prototype = new NoneFun();
// Son.prototype.constructor = Son;
// })();

// 方法二 直接借用Object.create()方法
Son.prototype = Object.create(Father.prototype);
// 修复构造函数指向
Son.prototype.constructor = Son;

let s = new Son("son");
console.log(s.name); // son
s.sayAge(); // 18
s.sayName(); // son
console.log(s.age); // 18
console.log(s instanceof Father); // true
console.log(s instanceof Son); // true
console.log(s.constructor === Father); // false
console.log(s.constructor === Son); // true

优点:

  1. 比较完美(js实现继承首选方式)

缺点:

  1. 实现起来较为复杂(可通过Object.create简化)

3.5.7 es6–Class继承

使用extends表明继承自哪个父类,并且在子类构造函数中必须调用super

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Son extends Father {
constructor(name) {
super(name);
this.name = name || "son";
}
}

let s = new Son("son");
console.log(s.name); // son
s.sayAge(); // 18
s.sayName(); // son
console.log(s.age); // 18
console.log(s instanceof Father); // true
console.log(s instanceof Son); // true
console.log(s.constructor === Father); // false
console.log(s.constructor === Son); // true

3.6 JS防抖与节流(性能优化)

防抖:动作停止后的时间超过设定的时间时执行一次函数。注意:这里的动作停止表示你停止了触发这个函数,从这个时间点开始计算,当间隔时间等于你设定时间,才会执行里面的回调函数。如果你一直在触发这个函数并且两次触发间隔小于设定时间,则函数一直不会执行。

简单实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function debance(fn, delay) {
let timer = null;
return () => {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
fn();
}, delay);
};
}

window.addEventListener(
"scroll",
debance(() => {
console.log(111);
}, 1000)
);

防抖应用场景:

  1. search搜索联想,用户在不断输入值时,用防抖来节约请求资源
  2. window触发resize的时候,不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发一次

节流:一定时间内执行的操作只执行一次,也就是说即预先设定一个执行周期,当调用动作的时刻大于等于执行周期则执行该动作,然后进入下一个新周期。

简单实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 方法一:设置一个标志
function throttle(fn, delay) {
let flag = true;
return () => {
if (!flag) return;
flag = false;
timer = setTimeout(() => {
fn();
flag = true;
}, delay);
};
}
// 方法二:使用时间戳
function throttle(fn, delay) {
let startTime = new Date();
return () => {
let endTime = new Date();
if (endTime - startTime >= delay) {
fn();
startTime=endTime;
} else {
return;
}
};
}
window.addEventListener(
"scroll",
throttle(() => {
console.log(111);
}, 1000)
);

节流应用场景:

  1. 鼠标不断点击触发,mousedown(单位时间内只触发一次)
  2. 监听滚动事件,比如是否滑到底部自动加载更多(懒加载),用throttle来判断

3.7 JS运行机制

3.7.1 JS为啥是单线程

js作为浏览器脚本语言,其主要用途是与用户互动,以及操作DOM。这就决定了它只能是单线程,否则会带来很复杂的同步问题。(假设JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?)

3.7.2 JS同步任务和异步任务

事件循环

JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。如果前一个任务耗时很长,后一个任务就不得不一直等着。JavaScript语言的设计者意识到这个问题,将所有任务分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)

同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;

异步任务指的是,不进入主线程、而进入”任务队列”(task queue)的任务,只有”任务队列”通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行

3.7.3 任务队列(消息队列)

任务队列中存着的是异步任务,这些异步任务一定要等到执行栈清空后才会执行。

异步任务,会先到事件列表中注册函数。如果事件列表中的事件触发了,会将这个函数移入到任务队列中(DOM操作对应DOM事件,资源加载操作对应加载事件,定时器操作可以看做对应一个“时间到了”的事件)

3.7.4 宏任务与微任务

宏任务和微任务

macro-task(宏任务):包括整体代码script,setTimeout,setInterval, setImmediate, I/O, UI rendering

micro-task(微任务):Promise,process.nextTick,MutationObserver

微任务意义:

减少更新时的渲染次数 因为根据HTML标准,会在宏任务执行结束之后,在下一个宏任务开始执行之前,UI都会重新渲染。如果在microtask中就完成数据更新,当 macro-task结束就可以得到最新的UI了。如果新建一个 macro-task来做数据更新的话,那么渲染会执行两次

扩展阅读: 全面解析Vue.nextTick实现原理

3.7.5 Event Loop(事件循环)

事件循环

  1. 整体的script(作为第一个宏任务)开始执行的时候,会把所有代码分为两部分:“同步任务”、“异步任务”;
  2. 同步任务会直接进入主线程依次执行;
  3. 异步任务会再分为宏任务和微任务;
  4. 宏任务进入到Event Table中,并在里面注册回调函数,每当指定的事件完成时,Event Table会将这个函数移到Event Queue中;
  5. 微任务也会进入到另一个Event Table中,并在里面注册回调函数,每当指定的事件完成时,Event Table会将这个函数移到Event Queue中;
  6. 当主线程内的任务执行完毕,主线程为空时,会检查微任务的Event Queue,如果有任务,就全部执行,如果没有就执行下一个宏任务;
  7. 上述过程会不断重复,这就是Event Loop事件循环;

3.7.6 一图总结(事件循环、执行栈、任务队列、宏任务、微任务)

js事件循环

3.7.7 经典面试题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
console.log(1);

setTimeout(()=>{
console.log(2);
new Promise((resolve,reject)=>{
console.log(3);
resolve()
}).then(res=>{
console.log(4);
})
})

new Promise((resolve,reject)=>{
resolve()
}).then(res=>{
console.log(5);
}).then(res=>{
console.log(6);

})

new Promise((resolve,reject)=>{
console.log(7);
resolve()
}).then(res=>{
console.log(8);
}).then(res=>{
console.log(9);

})

setTimeout(()=>{
console.log(10);
new Promise((resolve,reject)=>{
console.log(11);
resolve()
}).then(res=>{
console.log(12);
})
})

console.log(13);

依次输出

输出

3.8 如何取消promise

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// 方法一 取消promise方法   promise.race方法
function wrap(p) {
let obj = {};
let p1 = new Promise((resolve, reject) => {
obj.resolve = resolve;
obj.reject = reject;
});
obj.promise = Promise.race([p1, p]);
return obj;
}

let promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(123);
}, 1000);
});
let obj = wrap(promise);
obj.promise.then(res => {
console.log(res);
});
obj.resolve("请求被拦截了");

obj.reject("请求被拒绝了");


// 方法二 取消promise方法 新包装一个可操控的promise

function wrap(p) {
let res = null;
let abort = null;

let p1 = new Promise((resolve, reject) => {
res = resolve;
abort = reject;
});

p1.abort = abort;
p.then(res, abort);

return p1;
}

let promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(123);
}, 1000);
});
let obj = wrap(promise);
obj.then(res => {
console.log(res);
});
obj.abort("请求被拦截");

4. HTTP协议相关

HTTP(Hyper Text Transfer Protocol)<超文本传输协议>的缩写.是用于从WWW服务器传输超文本到本地浏览器的传输协议.HTTP是一个应用层协议,由请求和响应构成,是一个标准的个客户端和服务器模型

4.1 特点

  1. 基于请求/响应模型的协议
  2. 简单快速
  3. 灵活
  4. 无连接
  5. 无状态

4.2 HTTP报文

请求报文:

请求报文

响应报文:

响应报文

4.3 HTTP方法

HTTP请求方法

get和post区别:

  • GET请求会被浏览器主动cache,而POST不会,除非手动设置

  • get把请求的参数放在url上,即HTTP协议头上 post把参数放在HTTP的包体内

  • Get 方式传输的数据量非常小,一般限制在 2 KB 左右,但是执行效率却比 >Post 方法好;而 Post
    方式传递的数据量相对较大,它是等待服务器来读取数>据,不过也有字节限制(实际上IIS4中最大量为80KB,IIS5中为100KB),这是为>了避免对服务器用大量数据进行恶意攻击

  • GET请求只能进行url编码,而POST支持多种编码方式

  • GET产生的URL地址可以加入书签,而POST不可以

  • GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留

  • GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息

4.4 HTTP状态码

状态码:由3位数字组成,第一个数字定义了响应的类别

1xx:指示信息,表示请求已接收,继续处理

2xx:成功,表示请求已被成功接受,处理。

  • 200 OK:客户端请求成功
  • 204 No Content:无内容。服务器成功处理,但未返回内容。一般用在只是客户端向服务器发送信息,而服务器不用向客户端返回什么信息的情况。不会刷新页面。
  • 206 Partial Content:服务器已经完成了部分GET请求(客户端进行了范围请求)。响应报文中包含Content-Range指定范围的实体内容

3xx:重定向

  • 301 Moved Permanently:永久重定向,表示请求的资源已经永久的搬到了其他位置。

  • 302 Found:临时重定向,表示请求的资源临时搬到了其他位置

  • 303 See Other:临时重定向,应使用GET定向获取请求资源。303功能与302一样,区别只是303明确客户端应该使用GET访问

  • 307 Temporary Redirect:临时重定向,和302有着相同含义。POST不会变成GET

  • 304 Not Modified:表示客户端发送附带条件的请求(GET方法请求报文中的IF…)时,条件不满足。返回304时,不包含任何响应主体。虽然304被划分在3XX,但和重定向一毛钱关系都没有

4xx:客户端错误

  • 400 Bad Request:客户端请求有语法错误,服务器无法理解。
  • 401 Unauthorized:请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用。
  • 403 Forbidden:服务器收到请求,但是拒绝提供服务
  • 404 Not Found:请求资源不存在。比如,输入了错误的url
  • 415 Unsupported media type:不支持的媒体类型

5xx:服务器端错误,服务器未能实现合法的请求。

  • 500 Internal Server Error:服务器发生不可预期的错误。
  • 503 Server Unavailable:服务器当前不能处理客户端的请求,一段时间后可能恢复正常。

4.5 HTTP持久化连接与管道化

在HTTP1.0中,默认的是短连接,没有正式规定 Connection:Keep-alive 操作;

HTTP/1.1所有连接都是Keep-alive的,也就是默认都是持续连接的。在事务处理结束之后仍然保持在打开状态的TCP连接称之为持久连接

持久连接详解:

持久连接会在不同事务之间保持打开状态,直到客户端或服务器决定其关闭为止。重用已对目标服务器打开的空闲持久连接,就可以避开缓慢的连接建立阶段。而且,已经打开的连接还可以避免慢启动的拥塞适应阶段,以便更快速地进行数据传输。所以,持久连接降低了时延和连接建立的开销,将连接保持在已调谐状态,而且减少了打开连接的潜在数量

HTTP/1.1允许在持久连接上可选的使用请求管道,是相对于keep-alive连接的又一性能优化。在响应到达之前,可以将多条请求放入队列,当第一条请求通过网络流向服务器时,第二条和第三条请求也可以开始发送了。在高时延网络条件下,这样做可以降低网络的环回时间,提高性能

管道连接注意点:

  1. 如果HTTP客户端无法确认连接是持久的,就不应该使用管道
  2. 必须按照与请求相同的顺序回送HTTP响应。
  3. HTTP客户端必须做好连接会在任意时刻关闭的准备,还要准备好重发所有未完成管道化的请求。
  4. 出错的时候,管道连接会阻碍客户端了解服务器执行的是一些列管道化请求中的哪一些。由于无法安全地重试POST这样的非幂请求,所以出错时,就存在某些方法永远不会被执行的风险。

HTTP连接

5. 前端通信

5.1 同源策略

同源策略限制从一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的关键安全机制。

什么是源:协议、域名与端口。这三者任何一个不一样的话,就算是跨域

什么是限制:不是一个源的文档,没有权限去操作另一个源的文档,包括以下内容

  • Cookie、LocalStorage 和 IndexDB无法读取。
  • DOM无法获得
  • Ajax请求不能发送

5.2 前后端通信方式

  1. Ajax 支持同源通信
  2. WebSocket 不受同源策略影响
  3. CORS 既支持同源通信也支持跨域通信

5.3 如何创建Ajax

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
util.json = function (options) {
var opt = {
url: '',
type: 'get',
data: {},
success: function () {},
error: function () {},
};
util.extend(opt, options);
if (opt.url) {
var xhr = XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP');
var data = opt.data,
url = opt.url,
type = opt.type.toUpperCase(),
dataArr = [];
for (var k in data) {
dataArr.push(k + '=' + data[k]);
}
if (type === 'GET') {
url = url + '?' + dataArr.join('&');
xhr.open(type, url.replace(/\?$/g, ''), true);
xhr.send();
}
if (type === 'POST') {
xhr.open(type, url, true);
xmlhttp.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xhr.send(dataArr.join('&'));
}
xhr.onload = function () {
if (xhr.status === 200 || xhr.status === 304) {
var res;
if (opt.success && opt.success instanceof Function) {
res = xhr.responseText;
if (typeof res === 'string') {
res = JSON.parse(res);
opt.success.call(xhr, res);
}
}
} else {
if (opt.error && opt.error instanceof Function) {
opt.error.call(xhr, res);
}
}
};
}
};

5.4 跨域通信的几种方式

5.4.1 JSONP(只支持GET请求)

通过 script 标签的异步加载来实现的。利用script标签不受同源策略的限制,天然可以跨域的特性。

1
2
3
4
5
6
7
8
9
10
11
12
<script>
var script = document.createElement('script');
script.type = 'text/javascript';

script.src = 'https://api.asilu.com/geo/&callback=jsonp'; // 这个是获取当前经纬度的接口
document.head.appendChild(script); // 创建并添加script标签到<head>下

// 回调执行函数
function jsonp(res) {
console.log(res); // 打印jsonp返回的信息
}
</script>

5.4.2 Hash

url的#后面的内容就叫Hash。Hash的改变,页面不会刷新。

1
2
3
4
5
6
7
8
// 在A中伪代码如下:
var B = document.getElementsByTagName('iframe');
B.src = B.src + '#' + 'data';

// 在B中的伪代码如下
window.onhashchange = function () {
var data = window.location.hash;
};

5.4.3 postMessage

H5中新增的postMessage()方法,可以用来做跨域通信

1
2
3
4
5
6
7
8
9
// 在A窗口中操作如下:向B窗口发送数据
Bwindow.postMessage('data', 'http://B.com'); // 这里强调的是B窗口里的window对象

// 在窗口B中监听 message 事件
Awindow.addEventListener('message', function (event) { // 这里强调的是A窗口里的window对象
console.log(event.origin); // 获取 :A窗口url
console.log(event.source); // 获取:A window对象
console.log(event.data); // 获取传过来的数据
}, false);

5.4.4 WebSocket

WebSocket protocol是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是server push技术的一种很好的实现

1
2
3
4
5
6
7
8
9
10
11
12
var ws = new WebSocket('wss://echo.websocket.org');
ws.onopen = function (evt) {
console.log('Connection open ...');
ws.send('Hello WebSockets!');
};
ws.onmessage = function (evt) {
console.log('Received Message: ', evt.data);
ws.close();
};
ws.onclose = function (evt) {
console.log('Connection closed.');
};

5.4.5 CORS(现代浏览器普遍跨域解决方案)

整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信

6. 前端安全

6.1 XSS攻击(跨站脚本攻击)

英文全称:Cross Site Script,XSS攻击,通常指黑客通过“HTML注入”篡改了网页,插入了恶意的脚本,从而在用户浏览网页时,控制用户浏览器的一种攻击

6.1.1 XSS的分类

  1. 反射型 XSS

原理: 反射型XSS,也叫非持久型XSS,是指发生请求时,XSS代码出现在请求URL中,作为参数提交到服务器,服务器解析并响应。响应结果中包含XSS代码,最后浏览器解析并执行

实现: 攻击者通过给用户发送带有恶意脚本代码参数的URL,当URL地址被打开时,特有的恶意代码参数被HTML解析、执行。
反射性XSS攻击

  1. 存储型XSS

原理: 一般是攻击者输入的恶意代码“存储”在服务器端,主要是将XSS代码发送到服务器(不管是数据库、内存还是文件系统等。),只要受害者浏览包含此恶意代码的页面就会执行恶意代码。

实现: 存储型 XSS 一般出现在网站留言、评论、博客日志等交互处。 例如:黑客提交了一条包含XSS代码的留言到数据库。当目标用户查询留言时,那些留言的内容会从服务器解析之后加载出来。浏览器发现有XSS代码,就当做正常的HTML和JS解析执行。XSS攻击就发生了

6.1.2 XSS的防御

  • HttpOnly
    浏览器禁止页面的Javascript访问带有HttpOnly属性的cookie。(实质解决的是:XSS后的cookie劫持攻击)如今已成为一种“标准”的做法

  • 输入检查(XSS Filter)
    让一些基于特殊字符的攻击失效。(常见的Web漏洞如XSS、SQLInjection等,都要求攻击者构造一些特殊字符)

  • 输出检查
    在变量输出到HTML页面时,使用编码或转义的方式来防御XSS攻击

6.2 CSRF(跨站请求伪造)

CSRF就是利用你所在网站的登录的状态,以你的名义向网站发送恶意请求。CSRF能做的事情包括利用你的身份发邮件、发短信、进行交易转账等,盗取你的账号,甚至购买商品,虚拟货币转账……造成的问题包括:个人隐私泄露以及财产安全

原理图:

例子:

用户登录自己的博客网站(本地cookie已经保存了登录信息)

攻击者构造一个页面:http://www.a.com/csrf.html

其内容为

使用了一个img标签,其地址指向了删除Iid为156714243的博客文章

然后攻击者诱使用户访问这个页面

用户进去看到一张无法显示的图片,这时自己的那篇博客文章已经被删除了

关键点:

  1. 用户登录受信任网站A,并且在本地生成Cookie
  2. 在不登出网站A的情况下,访问危险网站B

CSRF的防御

  • 增加token

在请求中放入攻击者所不能伪造的信息,并且该信总不存在于cookie之中。鉴于此,系统开发人员可以在HTTP请求中以参数的形式加入一个随机产生的token,并在服务端进行token校验,如果请求中没有token或者token内容不正确,则认为是CSRF攻击而拒绝该请求

  • 通过Referer识别

根据HTTP协议,在HTTP头中有一个字段叫Referer,它记录了该HTTP请求的来源地址。在通常情况下,访问一个安全受限的页面的请求都来自于同一个网站

  • 网站重要操作增加验证码

CSRF攻击过程中,用户在不知情的情况下构造了网络请求,添加验证码后,强制用户必须与应用进行交互

7. 渲染机制

7.1 DOCTYPE作用

DTD(document type define,文档类型定义)是一系列的语法规则,用来定义XML或(X)HTML的文件类型。浏览器会使用它来判断文档类型,决定使用何种协议来解析,以及切换浏览器模式

DOCTYPE是用来声明文档类型和DTD规范的,一个主要的用途是文件的合法性验证。如果文件代码不合法,那么浏览器解析时便会出一些差错

7.2 浏览器渲染过程

7.2.1 具体过程

img

  1. 解析HTML,生成DOM树(DOM)
  2. 解析CSS,生成CSSOM树(CSSOM)
  3. 将DOM和CSSOM合并,生成渲染树(Render-Tree)
  4. 计算渲染树的布局(Layout)
  5. 将布局渲染到屏幕上(Paint)

7.2.2 几个关键概念

CSS阻塞渲染:由于CSSOM负责存储渲染信息,浏览器就必须保证在合成渲染树之前,CSSOM是完备的,这种完备是指所有的CSS(内联、内部和外部)都已经下载完,并解析完,只有CSSOM和DOM的解析完全结束,浏览器才会进入下一步的渲染。CSS阻塞渲染意味着,在CSSOM完备前,页面将一直处理白屏状态,这就是为什么样式放在head中,仅仅是为了更快的解析CSS,保证更快的首次渲染。

JS阻塞页面:JS可以操作DOM来修改DOM结构,可以操作CSSOM来修改节点样式,这就导致了浏览器在解析HTML时,一旦碰到script,就会立即停止HTML的解析,也阻塞了其后的CSS解析,整个解析进程必须等待JS的执行完成才能够继续。从性能角度上讲,将script放在页面底部,也就合情合理了

重排(Reflow):DOM结构中的各个元素都有自己的盒子(模型),这些都需要浏览器根据各种样式来计算并根据计算结果将元素放到它该出现的位置,这个过程称之为reflow

触发重排(reflow):

  • 1、当增加、删除、修改DOM节点时,会导致reflow或repaint
  • 2、当移动DOM的位置,或是插入动画的时候
  • 3、当修改CSS样式的时候
  • 4、当Resize窗口的时候,或是滚动的时候
  • 5、当修改网页的默认字体时

重绘(Repaint):当各种盒子的位置、大小以及其他属性,例如颜色、字体大小等都确定下来后,浏览器便把这些元素都按照各自的特性绘制了一遍,于是页面的内容出现了,这个过程称之为repaint。

触发重绘(Repaint):

  • 1、DOM改动
  • 2、CSS改动

最小化重绘和重排 1.一次性修改样式:减少内联样式使用 样式合并写法 2.批量修改DOM:使用文档片段创建一个子树,然后再拷贝到文档中(document.fragment) 3.缓存布局信息

1
2
3
4
5
6
7
8
//每次需要查询div.offsetLeft 浪费性能
div.style.left = 1 + div.offsetLeft + 'px';
div.style.top = 1 + div.offsetTop + 'px';

//将这个值保存下来,避免重复取值 性能优化
current = div.offsetLeft;
div.style.left = 1 + ++current + 'px';
div.style.top = 1 + ++current + 'px';

8. 性能优化

Yahoo Developer Network–包含 7 个类别共 35 条前端性能优化最佳实践

性能优化

前端常用方法:

8.1 资源合并压缩 减少HTTP请求

  • 尽量合并和压缩html css和js文件 借助前端工具 例如 webpack gulp grunt…
  • 开启gzip压缩

8.2 图片优化

雪碧图 图片压缩 svg base64

8.3 懒加载 / 预加载

  • 懒加载:图片进入可视区域之后请求图片资源 对于电商等图片很多,页面很长的业务场景适用 并发加载的资源过多会阻塞 js 的加载,影响网站的正常使用
  • 预加载:图片等静态资源在使用之前的提前请求 资源使用到时能从缓存中加载,提升用户体验

8.4 浏览器存储 localStorage

大小为 5M 左右仅在客户端使用,不和服务端进行通信 浏览器本地缓存方案 indexedDB:用于客户端存储大量结构化数据 为应用创建离线版本

8.5 浏览器缓存

8.5.1 强缓存

  • expires:

缓存过期时间,用来指定资源到期的时间,是服务器端的绝对时间 告诉浏览器在过期时间前浏览器可以直接从浏览器缓存取数据,而无需再次请求

  • cache-control:max-age = xxx

声明该资源在加载后的xxx秒内都直接使用缓存 使用的是相对时间 即加载文件本机的时间

强缓存cache-control

如果在Cache-Control响应头设置了 “max-age” 或者 “s-max-age” 指令,那么 Expires 头会被忽略。

8.5.2 协商缓存

触发条件

  1. Cache-Control 的值为 no-cache (不强缓存)
  2. max-age 过期了 (强缓存,但总有过期的时候)
  • Last-Modified / If-Modified-Since

Last-Modified ——- response header

If-Modified-Since ——- request header

缺点:某些服务端不能获取精确的修改时间;文件修改时间改了,但文件内容却没有变

  • Etag / If-None-Match

文件内容的 hash 值

etag ——- response header

if-none-match ——- request header

8.6 CDN 内容分发网络

9. 前端错误监控以及上报

9.1 前端错误分类

  • 即时运行错误:代码错误
  • 资源加载错误
  • 对于跨域的代码运行错误会显示 Script error. 对于这种情况我们需要给 script 标签添加 crossorigin 属性,并且服务器添加Access-Control-Allow-Origin

9.2 即时运行错误捕获

  • try ….catch
  • window.onerror 或者 window.addEventListener 记住事件捕获阶段获得,不是冒泡阶段

9.3 资源加载错误

  • object.onerror,如img.onerror
  • erformance.getEntries (getEntries api返回一个资源加载完成数组,假设为img,再查询页面中一共有多少个img,二者的差就是没有加载上的资源)
  • Error事件捕获

9.4 错误如何上报

9.4.1 ajax

9.4.2 image的src上报

(new Image()).src = ‘错误上报的请求地址’

一般来说,大厂都是采用利用image对象的方式上报错误的;使用图片发送get请求,上报信息,由于浏览器对图片有缓存,同样的请求,图片只会发送一次,避免重复上报

10. 前端模块化

模块化就是将一个复杂的系统分解成多个独立的模块的代码组织方式。

在很长的一段时间里,前端只能通过一系列的script标签来维护我们的代码关系,但是一旦我们的项目复杂度提高的时候,这种简陋的代码组织方式便是如噩梦般使得我们的代码变得混乱不堪。所以,在开发大型Javascript应用程序的时候,就必须引入模块化机制。

由于早期官方并没有提供统一的模块化解决方案,所以在群雄争霸的年代,各种前端模块化方案层出不穷。

前端模块化发展之路: IIFE(自执行函数)>>AMD(RequireJS实现)>>CMD(SeaJS实现)>>CommonJS(NodeJs)>>ES6 Modules(模块化直接成为了Javascript语言规范中的一部分)

模块化

-------------本文结束感谢您的阅读-------------

欢迎关注我的其它发布渠道