前言

其实有时候学习八股并非是一件不好的事情,我们能从八股文里面学到很多平时自己没注意到的知识点,算是查漏补缺

js

js 有多少种数据类型

7+1 USONB undefined string symbol object null number boolean bigint

栈和堆的概念 什么是堆空间共享 如何改变引用地址

栈中存放基本数据类型,栈内的数据大小固定,体积较小,会被频繁使用,拷贝或者引用的时候会创建一份相同的复制

堆存放引用类型,堆内的数据大小不定,占据空间大。当一个引用类型被创建的时候,其属性会被放在堆空间中,指针放在栈中,当被引用的时候,先从栈找到对应的指针,指向堆空间的地址,获取属性。

堆空间共享指的是 多个引用指向同一个地址。

引用地址可以使用 bind call apply 去改变。

栈和堆的存在是为了维持 js 的内存平衡

闭包是怎么存储的

闭包被创建的时候,会创建一个 closure 对象到堆中,用于保存闭包中的变量,这就是为什么闭包可以常驻在内存的原因。且会导致内存泄漏。

什么是内存泄漏

内存泄漏指的是堆内存中的变量由于某种原因无法及时释放造成系统资源的浪费,甚至导致系统崩溃。

怎么解决内存泄漏:
销毁闭包的变量,比如使用 clearTimeout 消除 settimeout 的 id 等

闭包的理解

闭包的本质是当前作用域中存在指向父级作用域的引用。

所以闭包并不一定表现为返回一个函数

1
2
3
4
5
6
7
8
9
var fun3;
function fun1() {
var a = 2;
fun3 = function () {
console.log(a);
};
}
fun1();
fun3();

可以看出其中输出的结果还是 2 因为在给 fun3 赋值的时候,fun3 就可以访问到 window fun1 和本身的作用域,然后由下往上查找 找到了 fun1 中的 2 输出 2

如何解决循环输出问题

let IIFE 注入变量 定时器第三个参数传入变量

undefined 和 null 的理解

undefined 和 null 一般用于对变量赋初始值。需要注意的是 undefined 在 js 中并不是一个保留字,所以 undefined 可以作为变量名,那么要如何获取安全的 undefined 呢?可以使用 void
null 代表空对象,但不是真的空对象

什么是 BigInt 用途是什么 怎么实现一个 BigInt 的运算

js 只支持 2 的 53 次方的数字,大于这个范围会四舍五入,表现在最后一位数不对,BigInt 是数字+n 或者使用 BigInt 构造函数传入数字格式的字符串就可以生成。

注意 bigint 并不是 number 所以不等于 number 类型

bigint 可以用在比如发送请求之前将请求的数字包装成 bigint 类型发送,常用一些库函数。

bigint 之间的运算使用位运算

leetcode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
`给定一个由整数组成的非空数组所表示的非负整数,在该数的基础上加一。
最高位数字存放在数组的首位, 数组中每个元素只存储一个数字。
你可以假设除了整数 0 之外,这个整数不会以零开头。

示例 1:
输入: [1,2,3]
输出: [1,2,4]
解释: 输入数组表示数字 123。

示例 2:
输入: [4,3,2,1]
输出: [4,3,2,2]
解释: 输入数组表示数字 4321。`;

const plusOne = (digits) => {
return (BigInt(digits.join("")) + 1n).toString().split("");
};

0.1+0.2!==0.3?

刚刚说了只支持到 2 的五十三次方,而由于小数计算机是识别不了的,要转二进制,所以 0.1 其实相当于一个无限循环的二进制数0.0001100110011001100...,而考虑到内存的原因,到某一位就被四舍五入了,导致结果计算出来不是 0.3 而是一个近似值。

解决方法:
使用 toFixed 或者变成整数再除以对应的倍数

0.2+0.3===0.5?

因为后面转二进制都是 0 所以刚好是 0.5

那既然 0.1 不是 0.1 了,为什么在console.log(0.1)的时候还是 0.1 呢?

在 console.log 的时候会二进制转换为十进制,十进制再会转为字符串的形式,在转换的过程中发生了取近似值,所以打印出来的是一个近似值的字符串

js 有多少种判断数据的方式 分别是什么

4 typeof instanceof constructor Object.prototype.toString.call()

为什么 typeof null 是 object

typeof 的原理就是通过类型的地址判断,对于 null 和 object 来说,地址开头的前几位都是 0 所以判断相同

注意 typeof 是可以判断 function 的

instanceof 的原理 为什么 instanceof 无法判断 null 和 undefined 手写 instanceof

instanceof 是通过原型链判断的,相当于是在判断该变量是否是某个类的实例。

null 和 undefined 没有构造函数 也就是他没有对应的原型

手写 instanceof

1
2
3
4
5
6
7
8
const myInstanceof = (example, classFn) => {
let proto = Object.getPrototypeOf(example);
while (true) {
if (proto === null) return false;
if (proto === Object.getPrototypeOf(classFn)) return true;
proto = Object.getPrototypeOf(proto);
}
};

使用 constructor 检测有什么缺点?

constructor 是构造函数,会指向原型。使用 constructor 的时候,相当于判断该变量的构造函数是否指向对应的原型。

但由于原型是可以改变的,所以原型指向改变之后,原本的构造函数就会指向新的原型,所以会判断失误

1
2
3
4
5
6
7
8
function Fn() {}

Fn.prototype = new Array();

var f = new Fn();

console.log(f.constructor === Fn); // false
console.log(f.constructor === Array); // true

Object.prototype.toString().call 为什么要用 prototype,为什么要用 call,以及他能判断什么类型,如何封装一个判断类型的函数

类似于 Number,String,Array 上面都重写了 toString 方法.而它们最终指向的 Object 上有共同的方法,所以要使用 prototype 去找 Object 上的方法,使用 call 是为了改变 this 的指向。他能判断所有的类型,以及一些特殊的比如:

1
2
Object.prototype.toString.call(document); //"[object HTMLDocument]"
Object.prototype.toString.call(window); //"[object Window]"

封装一个判断类型函数的思路就是,基本类型除了 null 使用 typeof 判断,非基本类型使用该方法判断

对象是怎么转原始类型的,可以自己设置转换规则吗

对象中有 toPrimitive 方法,该方法优先级是 valueOf,到 toString,能转到原始类型就转

自己设置转换规则的话 使用Symobol.toPrimitive就可以自定义规则了

1
2
3
4
5
6
7
8
9
10
11
12
let a = {
valueOf(){
return 0;
},
toStirng(){
return '1';
}
[Symbol.toPrimitive](){
return 2;
}
}
1+a //3

常考:[1,2,3]转原始类型

==的隐式转换

  1. 其中一个是null或者undefined 另外一个也要是 否则为 false
  2. 其中一个是string/number 转 number
  3. boolean 转 number
  4. object 转原始 (注意!!!两个 obj 比较的是地址值)
  5. NaN 和任何值都不相等 包括他自己

常考

1
2
3
4
5
6
7
8
9
var a = {
value: 0,
valueOf: function () {
this.value++;
return this.value;
},
};
// 注意这里a又可以等于1、2、3
console.log(a == 1 && a == 2 && a == 3); //true

常见坑:

1
2
3
4
5
6
7
8
9
10
11
12
// 小坑
"0" == false // -> true 这里false先被转为0,"0"也会转为0,所以为true
"0" == "" // -> false 两个都是字符串类型,直接比较
0 == '' // -> true 空字符串直接转为0
false == [] // -> true false先转为0;[]空数组转为'',之后ToNumber操作转为0

// 大坑
[] == ![] // -> true [] 这里![]先被强制转换为false,变成[]与fasle的比较,之后fasle->0;[]->''->0,所以为true。
2==[2] // -> true [2]->'2'->2 所以为true
''==[null] // true [null]->''
0=='\n' // -> true '\n'->''->0
'true'==true // -> false true->0;'true'->NaN,所以为false

+的隐式转换

  1. 其中一个是字符串,另外的是基本类型则转 string,引用类型转原始(存在优先级)
  2. 其中一个是数字,另外的是基本类型则转 number,引用类型转原始(存在优先级)
  3. 如果一个是字符串一个是数字则拼接

isNaNNumber.isNaN的区别

前者是 ES6 之前的全局方法,判断的方式是先把传进来的内容使用 Number 进行转换,然后再判断是否为 NaN,后者更方便一点,首先判断传进来的内容是否为数值类型,非数值直接返回 NaN,

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
isNaN(true); // false 布尔值true会转为非0值
isNaN(false); // false 布尔值false会转换成0
isNaN(null); // false null会转为0

// strings
isNaN("37"); // false: 可以被转换成数值37
isNaN("37.37"); // false: 可以被转换成数值37.37
isNaN("37,5"); // true "37,5"不能转换为数值
isNaN("123ABC"); // true:parseInt("123ABC") = 123, 但是Number("123ABC") = NaN
isNaN(""); // false: 空字符串被转换成0
isNaN(" "); // false: 包含空格的字符串被转换成0

isNaN(new Date()); // false 会转换成当前的时间
isNaN(new Date().toString()); // true
isNaN([]); // false []转换成0
isNaN(new String()); // false
isNaN(new Array()); // false
isNaN("Infinity"); // false:"Infinity"转换为Infinity
isNaN(-0.1); // false

isNaN(x) == isNaN(Number(x)); // 不论x为何值 都返回true 哪怕x = undefined
// 因为isNaN(undefined) = true 且 Number(undefined) = NaN 使得isNaN(NaN) = true
isNaN() == isNaN(Number()); // false, because isNaN() = true and Number() = 0

//以下全部返回true
isNaN({}); // 空对象执行toString方法 结果是'[object Object]'
isNaN(String); //
isNaN(Array); //
isNaN("blabla"); // "blabla"不能转换成数值
isNaN("-blabla"); //
isNaN(0 / 0); //
isNaN("0/0"); // "0/0"不能转换成数字
isNaN(Infinity / Infinity); //
isNaN(NaN); //
isNaN(undefined); //
isNaN();

Object.is

用法和===基本一致 不同在于

  1. 0 和+0 是一样的 但是 0 和-0 是不一样的
  2. NaN 与 NaN 为 true
1
2
3
4
Object.is(0, -0); // false
Object.is(+0, -0); // false
Object.is(0, +0); // true
Object.is(NaN, 0 / 0); // true

箭头函数为什么不能和 this 结合

因为箭头函数的指向在定义函数的时候就已经指定好了,所以不能和 this 结合

因为 this 是固定的了,所以也不能去改变
比如使用 call/bind/apply
那么进一步说,new 也不能用在箭头函数实例化,因为 new 的原理其实也是把 this 指向给到新的实例

并且 也没有 arguments,(为什么) 但是可以访问别人的 arguments

this 有多少种指向

5 种

在浏览器中 全局范围的 this 指向 window(控制台)
在函数中 this 指向最后调用它的对象
在构造函数中指向实例
call/apply/bind中指向被强绑定的对象
箭头函数,声明的时候就固定,是静态的

call/apply/bind三者区别

共同点 都能改变 this 指向
call 和 apply 区别在于传参,一个是字符串,一个是数组
bind 的话 会生成一个函数或者是生成一个新的对象(当传入 null)的时候

三者手写

参考手写篇
比较需要注意的点是,call 和 apply 的 context 不传的时候是 window,我们直接在编译器里面拿是拿不到的,可以使用 globalThis 关键字。

bind 的话要考虑那个 new 的情况,也就是传 null 相当于生成了一个构造函数,需要实例化

如何实现柯里化

柯里化有两种:定长和不定长,柯里化的思想是:收集和执行
对于定长的柯里化:
收集当前参数,如果形参长度未达到真实函数形参长度,则继续回调收集,否则执行真实函数

对于不定长的柯里化:
收集当前参数,然后把执行的结果放到函数的 toString 里面,这里涉及到对象转原始类型优先级的知识。

如何给类数组添加 push 方法

其实就是用 bind 改变 push 方法的 this 指向 就可以了

什么时候要使用到 bind

防止 this 指向到不正确的对象/永久绑定 this 指向

1
2
3
4
5
6
7
8
9
10
11
const person = {
nickname: "jojo",
eatWatermelon() {
console.log(this.nickname + " 吃西瓜");
},
};

person.eatWatermelon(); //jojo吃西瓜

const eatWatermelon = person.eatWatermelon;
eatWatermelon(); //undefined

this 的指向变成了 eatWatermelon() 执行时所在作用域的 this。也就是 window,所以找不到

解决方法: 使用 bind 永久绑定 this 指向

1
2
const eatWatermelon = person.eatWatermelon.bind(person);
eatWatermelon(); //jojo吃西瓜

预置函数参数
eg2 预置函数参数

1
2
3
4
5
6
7
8
9
10
11
function add(a, b, c) {
return a + b + c;
}

const addSix = add.bind(null, 6);
const addSixThenAddFour = addSix.bind(null, 4);
addSixThenAddFour(5);
// 15

addSixThenAddFour(7);
// 17

什么是变量提升

var 声明变量的时候 var 变量会提升到作用域顶部为 undefined,然后在对应的位置重新赋值

这里还涉及函数提升 函数提升优于变量提升

常考的:在函数中输出函数的名字 误导性极强

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
// case1
function b() {
var b = "jojo";
console.log(b);
}
b(); //jojo
console.log(b); //[Function: b]

// case2
function b() {
b = "jojo";
console.log(b);
}
b(); //jojo
console.log(b); //jojo

// case3
function b() {
console.log(b);
var b = "jojo";
}
b(); //undefined
console.log(b); //[Function: b]

// case4
function b() {
console.log(b);
b = "jojo";
}
b(); // [Function: b]
console.log(b)(
// jojo

// case5 这里涉及的是闭包的知识,闭包会将函数名存储进去,无法修改
function b() {
b = "jojo";
console.log(b); //[Function: b]
}
)();

什么是暂时性死区

let const 声明的变量在声明前就被调用 产生暂时性死区 表现为 Reference Error

js 执行环境有哪些

全局执行环境和函数执行环境

js 执行上下文有哪几种

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

执行上下文中有哪些重要属性

  • 变量对象(VO),包含变量,函数声明和函数的形参,该属性只能在全局上下文中访问。
  • 作用域链,js 采用词法作用域链,也就是说变量的作用域是在定义的时候决定了。
  • this

js 代码执行的过程

  • 先创建全局执行上下文 global EC
  • 全局执行上下文 caller 逐行 自上而下执行。遇到函数的时候,函数执行上下文 callee 会被 push 到执行栈顶层
  • 执行上下文被激活后,成为 active EC 开始执行函数中的代码,caller 被挂起
  • 函数执行完后 callee 被 pop 出执行栈 控制权还给 caller 继续执行

作用域和作用域链的理解

  • 作用域:作用域就是定义变量的区域,他有一套访问变量的规则,这套规则来管理浏览器引擎如何在当前作用域以及嵌套的作用域中根据变量(标识符)进行变量查找
  • 作用域链:作用域链的作用是保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,我们可以访问到外层环境的变量和函数

作用域链本质上是一个指向变量对象的指针列表。变量对象是一个包含了执行环境中所有变量和函数的对象。作用域链的前端始终是当前执行上下文的变量对象。全局执行上下文的变量对象(也就是全局对象)始终是作用域链的最后一个对象

  • 当我们查找一个变量的时候,如果在当前执行环境中没有找到,我们可以沿着作用域链向后查找
  • 作用域链的创建过程和执行上下文的建立有关。

作用域可以理解为变量的可访问性 总共分为三种类型 分别为:

  • 全局作用域
  • 函数作用域
  • 块级作用域

new 的原理以及实现

  • 创建一个空对象
  • 将对象连接到构造函数原型(装载原型方法)
  • 改变构造函数的 this 指向为新的对象并获取执行构造获取结果
  • 判断结果是否返回了一个对象
    如果是 那么返回该对象 否则 使用连接的新对象

注意 构造函数默认返回实例对象

1
2
3
4
5
6
7
8
9
10
11
12
function create(fn, ...args) {
try {
if (!fn instanceof Function) {
throw new Error("err");
}
let obj = Object.create(fn.prototype);
let res = fn.call(obj, ...args); //改变了this指向的同时获取到构造的结果
return res instanceof Object ? res : obj;
} catch (error) {
console.log(error);
}
}

__proto__,prototype,constructor三者关系

__proto__是隐式原型,用于实例去访问其原型对象,但是不规范,一般使用getPrototypeOf去访问

constructor是构造函数,用于创建对象,以及给新对象设置原型对象(相当于设置了一个指针指向 prototype)。存放于constructor.prototype属性中。

prototype是对象,通常叫他原型对象,存在于构造函数中,是他的一个属性。

常考的东西:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let test = new Array();

// 原型链
console.log(test.__proto__ === Array.prototype);
console.log(Array.prototype.__proto__ === Object.prototype);
console.log(Object.prototype.__proto__ === null);

// 构造函数
console.log(test.constructor === Array); //实例的构造函数
console.log(Array.prototype.constructor === Array); //构造函数自身有原型属性去访问原型,原型又可以通过construtor属性去访问构造函数
console.log(Array.constructor === Array); //原型链 找到原型上的属性

// 构造函数的原型
console.log(Array.__proto__ === Function.prototype);

// 原型的原型
console.log(Array.prototype.__proto__ === Object.prototype);

// 构造函数的原型的原型
console.log(Function.prototype.__proto__ === Object.prototype);

对于实例来说 有两个属性 __proto__constructor分别可以去访问原型和构造函数
对于构造函数来说 有两个属性__proto__prototype分别可以去访问构造函数的原型和原型
对于原型来说 有两个属性 __proto__constructor分别可以去访问原型的原型和构造函数

如何获取对象的原型

  • p.__proto__
  • p.constructor.prototype
  • Object.getPrototypeOf(p)

原型链本质

对象可以通过__proto__来寻找不属于该对象的属性,__proto__ 将对象连接起来组成了原型链。本质上就是链表结构,__proto__算是链表的指针,指向下一个位置

说说你知道的 js 继承方式,为什么要继承

继承的目的是为了 让子类拥有父类的属性和方法,属性放在构造函数中,方法挂载在原型中

组合继承,寄生组合继承,Class 继承

代码参见手写

组合继承有什么问题?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function () {
console.log(this.name);
};

function SubType(name, age) {
// 继承属性
SuperType.call(this, name);
this.age = age;
}

SubType.prototype = new SuperType(); //缺点 使得prototype上面有了不必要的属性
SubType.prototype.sayAge = function () {
console.log(this.age);
};

组合继承让实例的原型继承了不应该继承的东西,因为他是在子类构造函数里用了一次 call 执行父类构造函数 然后在外面链接父类原型的时候(链接原型的目的是为了继承父类的方法,而继承父类方法的方式可以是使用 new) 使用了 new 方法,new 方法又会执行一遍构造函数,于是子类原型上面也有了父类原型的一些属性

而 call 的时候已经继承过了一次属性到子类构造函数中,造成了属性冗余

寄生组合继承怎么解决组合继承的问题?又有什么缺点?

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
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function () {
alert(this.name);
};

function SubType(name, age) {
// 继承属性
// 第二次调用SuperType()
SuperType.call(this, name);
this.age = age;
}

// 继承方法
// 构建原型链
// 第一次调用SuperType()
// SubType.prototype = new SuperType();
// 重写SubType.prototype的constructor属性,指向自己的构造函数SubType
SubType.prototype = Object.create(SuperType.prototype, {
constructor: {
value: SubType,
enumerable: false,
writable: true,
configurable: true,
},
});
// SubType.prototype.__proto__ = SuperType.prototype; //或者这么使用

我们知道。组合继承的问题在于给子类添加父类方法的时候又调用了一遍父类的构造函数使得子类原型上面多了很多无关的属性。

寄生组合继承的解决方法有两种,一种是采用 apiObject.create(子类,constructor)去重写子类的原型的 constructor 属性为子类自身的构造函数

或者直接把子类的原型链接到父类原型上面。这样子类调用方法的时候找不到就去父类找,属于原型链的方法。

缺点:缺点在于不能调用父类静态方法。

什么是 class 继承

class 其实也是一个函数,使用方法是,子类采用 extends 关键字继承父类,然后在子类的构造函数中,采用 super 继承父类的属性。

事件流的三个阶段是什么

捕获 目标 冒泡

preventDefaultstopPropagation的区别

前者是阻止默认事件 比如表单提交
后者是阻止冒泡

onClick 和 addEventListener 的区别

前者不需要销毁 后者需要
前者阻止冒泡需要 stopPropagation,后者在第三个参数决定是否需要冒泡 默认 false 是冒泡

Promise 是什么,解决了什么问题

Promise 是一种状态机,状态有 fulfilled,rejected,pending。状态只能由 fulfilled 向其他两个转换,一旦转换就不能再改变。

Promise 解决了回调地狱的问题,回调地狱指的是回调函数里面又嵌套回调函数,导致代码缩进非常难看,可读性差,Promise 链式调用可以帮我们解决这个痛点。

而实际上 async await 更方便一点点

Promise 中直接 return 一个 error 对象会发生什么

在 promise 中,任何的 return 都视为成功的回调。不写的话默认return Promise.resolve(undefined) return 一个 error 对象也是会进入成功回调的

new Promise 需要注意什么

new Promise 会先执行构造器的内容,常考在异步任务中

Promise.all race any allSettled 具体实现

参考手写代码
all 的话 传入可迭代对象,一般是数组,如果其中的有一个失败则进入失败回调,全都成功才算成功
race 传参相同,以第一个改变状态的任务为最终结果
any 传参相同 全部失败算失败 全部成功算成功
allSettled 传参相同,不会因为一个失败就停止,会返回全部任务的最终状态

Promise 实现 fetch 请求的 abort

利用 promise race API

如果实现异步请求并发限制

all 方法可以保证,promise 数组中所有的 promis 对象都达到 resolve 的状态才执行 then 的回调。

此时如果数组内有几十万个 promise,那么就会在一瞬间发出几十万跳请求,导致内存溢出。

所以需要 promiseall 进行并发限制。每一个时刻并发执行的 promise 数量是固定的。最终执行结果和原来的保持一致。

race 方法的用途

实现一个超时停止请求

Promise finally 的用法

不论最后状态如何都会去执行,一般用做中间件,因为无论状态如何都会走 finally,这样就可以用作过度。

并发和并行的区别

并行是指多个事件在同一时间点执行
并发指的是多个事件在同一时间段执行,但是多个指令进程被快速的交替进行,因此在宏观上长得和并行差不多,但是微观上不一样

all 方法是并发的还是并行的。

并发的,不过promise.all().then()结果中数组的顺序和Promise.all()接收到的数组顺序一致

generator 怎么使用

  • 有一个*符号的标识符 在函数名称前
  • 有一个关键字 yield 用于实现分段执行,当 generator 函数遇到 yield 的时候会暂停并抛出其之后的表达式
  • 有一个关键字 next 表示归还代码的控制权(这一步非常重要,能否理解他决定能否做 async 和 await 的异步题

手写 genertor

js 事件循环是什么

js 的任务分为同步任务和异步任务,异步任务中分为宏任务和微任务。代码由上而下执行,遇到函数调用将函数压入执行栈,先执行同步任务,如果在同步任务中发现了异步事件,先将其挂起,继续执行执行栈中的其他任务。
当同步事件执行完之后,将异步事件的回调加入到异步的任务队列中等待执行。
异步任务队列分为,微任务队列和宏任务队列。
优先执行微任务队列,然后执行宏任务队列。
然后重复以上操作

process.nextTick 是什么

是 node 中的一个 api,他指定的异步任务总是发生于所有异步任务之前

js 为什么是单线程

考虑到用户视角始终只有一个的原因,如果 js 是多线程的话。其中一个线程创建了 DOM 节点,另外一个线程销毁了 DOM 节点。此时浏览器的视图应该以什么为准呢?因为这样的原因,js 在一开始设计上就是单线程的。

js 怎么开启多线程

浏览器的线程主要包括 js 引擎,界面渲染线程,事件触发线程,http 请求线程。

对于 js 来说,他是单线程的,所以一旦有一个任务花费了大量的时间就会造成阻塞。为了解决这个问题,js 提供了一个 worker 类 。

使用这个 worker 类,会向浏览器申请一个新的线程,用于编写耗时代码。且主线程可以和子线程连线,这样就实现了线程之间数据的传递

不过需要注意的一点是,尽量只把耗时的操作交给 worker,其他的操作交给主线程,因为所有 js 里面集成的对象都在主线程中。worker 访问会报错

对 async 和 await 的理解

本质是 generator 的语法糖,async 用于产生一个 promise,await 需要保证顶部是 async。

await 和 next 的作用是一样的,他会暂时返还代码的控制权,也就是会跳出当前任务到外面继续执行同步任务等。

他还会保留堆栈中的东西,假如在 await 这一步操作了变量,他会保留变量的值,

1
2
3
4
5
6
7
8
9
10
var a = 0;
var b = async () => {
a = a + (await 10);
console.log("2", a); // -> '2' 10 //回到awiat之后的内容,保留了堆栈 所以a还是0,0+10= 10
a = (await 10) + a;
console.log("3", a); // -> '3' 20
};
b(); //执行b但是遇到await 跳出
a++;
console.log("1", a); // -> '1' 1 //跳出后执行同步

async/await 对比 promise 的优势

优势在于处理 then 的链式调用,能更清晰的写出代码。
但是缺点在于 await 会阻塞线程,可能导致性能问题。

async/await 如何捕获异常

1
2
3
4
5
6
7
async function fn() {
try {
let a = await Promise.reject("error");
} catch (error) {
console.log(error);
}
}

最近流行的 await-to-js,这种方式可能更为优雅

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
// 原来的Promise
function a(num) {
return new Promise((resolve, reject) => {
if (num < 5) {
resolve("small" + num);
} else {
reject("big" + num);
}
});
}

// to函数来执行一下a(), to函数返回的是一个Promise
function to(P) {
return P.then((data) => {
return [null, data];
}).catch((err) => {
return [err, undefined]; // 因为没有主动抛出异常,所以后续.then是fullfilled的状态
});
}

async function b() {
let b1 = await to(a(8));
console.log(b1); // [ 'big8', undefined ]

let b2 = await to(a(3));
console.log(b2); // [ null, 'small3' ]
}
b();

setTimeout 和 setInterval 和 requestAnimationFrame 各有什么特点

正常的屏幕刷新率是 60hz,相当于 16.7ms 执行一次,我们为了达到这样的效果,要设置更新时间为 1000/60。但是 settimeout 和 setInterval 始终是基于 js 引擎的,也就不可避免遇到阻塞,就造成了并不是每一次循环都是相同的时间间隔

requestAnimationFrame 是基于 GUI 引擎的,他的刷新时间是和系统的刷新率同步的,如果是 60HZ 那么就是 16.7ms 执行一次。这样就能防止掉帧行为。且由于 setTimeout 实现的动画,在浏览器隐藏或者最小化的时候仍然在处于激活状态(虽然已经有浏览器对定时器做了优化),而 requestAnimationFrame 在页面未激活的情况下会停止。节约了 CPU 开销

什么是垃圾回收机制?

遍历对象有几种方法

7 种
forin
Object.keys()、Object.values()、Object.entries()
Object.getOwnPropertyNames()
Object.getOwnPropertySymbols()
Reflect.ownKeys()

什么方法只返回对象本身的属性

Object.keys()、Object.values()、Object.entries()
Object.getOwnPropertyNames()
Object.getOwnPropertySymbols()
Reflect.ownKeys()

什么方法能返回不可枚举的属性

Object.getOwnPropertySymbols()
Reflect.ownKeys()

面向对象的思想包括

继承 封装 多态 抽象

深浅拷贝

JSON.parse(JSON.stringfy())有什么问题

缺点:不能拷贝undefined function 正则 Error对象

深拷贝实现细节 如何判断一个对象是正则对象

深拷贝实现细节 如何判断一个对象是 Date 对象

map 和 object 的区别

访问:map 通过 getkey 访问 object 直接点或者括号
赋值 map 通过 set 赋值 或者直接数组嵌套数组赋值

1
2
3
4
5
6
7
const showContents = new Map([
["title", "标题"],
["tags", "标签"],
["categories", "目录"],
["date", "创建日期"],
["updateTime", "最后更新日期"],
]);

删除 map 通过 delete 去删除 删成功 true 反之 false,不存在也是 false
但 obj 的话 删除不存在的属性也返回 true
大小:map 通过 size 访问元素个数,obj 需要通过Object.keys的转换才能将其转换为数组,再通过数组的length方法去获得或者使用Reflect.ownKeys(obj)也可以获取到 keys 的集合

迭代:map 有迭代器,可以直接遍历。obj 没有,但可以通过 forin 不过是无序的

常用正则表达式有哪些

对 json 的理解

JSON 是一种结构化数据,它是一种数据格式
json 必须用{}包裹,内部只能双引号。没有分号
方法:
JSON.stringify(); 把 js 对象序列化为 json 字符串

JSON.parse(); 把 json 字符串解析为原生 js 值

js 脚本延迟加载的方式有哪些

1:defer 属性,async 属性
2:动态创建 DOM 方式
3:使用 jQuery 的 getScript 方法
4:使用 setTimeout 延迟方法
5:让 JS 最后加载 放到页面底部

Unicode,UTF-8,UTF-16,UTF-32 的区别

常见位运算符及其运算规则

什么是 DOM 和 BOM

DOM (Document ),简称文档对象模型。通过创建树来表示文档,描述了处理网页内容的方法和接口。用 DOM API 可以轻松地删除、添加和替换节点。

BOM (browser object model),简称浏览器对象模型。描述了与浏览器进行交互的方法和接口

BOM 的核心是 window,而 window 对象又具有双重角色,它既是通过 js 访问浏览器窗口的一个接口,又是一个 Global(全局)对象。这意味着在网页中定义的任何对象,变量和函数,都以 window 作为其 global 对象。

那你知道有什么 dom 方法和 bom 方法吗

dom:
访问节点:
document.getElementById( "ID" ) 通过指定的 ID 来返回元素
document.getElementByName( "name" ) 获取所有 name 特性等于指定值的元素
document.getElementsByTagName( "p" ) 使用指定的标签名返回所有的元素列表
document.documentElement 返回存在于 XML以及 HTML 文档中的文档根节点

节点的操作:
creatElement(element) 创建一个新的元素节点
appendChild() 在节点列表后 添加一个新的子节点
removeChild() 从一个给定元素中删除子节点

bom:
window.close();//关闭窗口
window.alert( "message" ); //弹出一个具有 OK 按钮的系统消息框,显示指定的文本

类数组如何转换成数组

a. 使用 Array.from()
b. 使用 Array.prototype.slice.call()
c. 使用 Array.prototype.forEach()进行属性遍历并组成新的数组

对 ajax 的理解 实现一个 ajax

Ajax 的原理简单来说是在用户和服务器之间加了—个中间层(AJAX 引擎),通过 XmlHttpRequest 对象来向服务器发异步请求,从服务器获得数据,然后用 javascript 来操作 DOM 而更新页面。使用户操作与服务器响应异步化。这其中最关键的一步就是从服务器获得请求数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/** 1. 创建连接 **/
var xhr = null;
xhr = new XMLHttpRequest();
/** 2. 连接服务器 **/
xhr.open("get", url, true);
/** 3. 发送请求 **/
xhr.send(null);
/** 4. 接受请求 **/
xhr.onreadystatechange = function () {
if (xhr.readyState == 4) {
if (xhr.status == 200) {
success(xhr.responseText);
} else {
/** false **/
fail && fail(xhr.status);
}
}
};

ajax 的优缺点
优点:

  • 通过异步模式,提升了用户体验.
  • 优化了浏览器和服务器之间的传输,减少不必要的数据往返,减少了带宽占用.
  • Ajax 在客户端运行,承担了一部分本来由服务器承担的工作,减少了大用户量下的服务器负载。
  • Ajax 可以实现动态不刷新(局部刷新)
    缺点:
  • 安全问题 AJAX 暴露了与服务器交互的细节。
  • 对搜索引擎的支持比较弱。
  • 不容易调试。

什么是尾调用,有什么好处

尾调用(Tail Call)是函数式编程的一个重要概念,本身非常简单,一句话就能说清楚,就是指某个函数的最后一步是返回调用另一个函数的执行结果。

注意 结尾返回另外一个函数之后没别的操作才叫尾调用

1
2
3
function f(x) {
return g(x);
}

上面代码中:

1
2
3
4
5
6
7
8
9
10
11
12
13
//情况1
function f(x) {
let y = g(x);
return y;
}
//情况2
function f(x) {
return g(x) + 1;
}
//情况3
function f(x) {
g(x);
}

情况一是调用函数 g 之后,还有赋值操作,所以不属于尾调用,即使语义完全一样。
情况二也属于调用后还有操作,即使写在一行内。
情况三等价于 return undefined
属于尾调用的示例

1
2
3
4
5
function f(x) {
if (x > 0) {
return g(x)
}
return b(x)

尾调用不一定出现在函数尾部,只要是最后一步操作即可。上面代码中,函数 g 和 b 都属于尾调用,因为它们都是函数 f 的最后一步操作。

尾调用的情况下,执行栈中只有当前调用函数,取代了外层函数,这就是尾调用的优化。
链接:尾调用

CJS ES

这里的知识可以联系 bundless

cjs 是 node 的格式 es 是现代浏览器支持的格式
cjs 是运行时 因为他基于 node 传递的是值的拷贝
es 是编译时 传递的是值的引用

es 支持 tree shaking

bundless 的过程其实就是把 cjs 转 es 的过程

动态类型语言和静态类型语言的区别

通常我们所说的动态语言、静态语言是指动态类型语言和静态类型语言。

动态类型语言:动态类型语言是指在运行期间才去做数据类型检查的语言,也就是说,在用动态类型的语言编程时,永远也不用给任何变量指定数据类型,该语言会在你第一次赋值给变量时,在内部将数据类型记录下来。Python 和 Ruby 就是一种典型的动态类型语言,其他的各种脚本语言如 VBScript 也多少属于动态类型语言。

静态类型语言:静态类型语言与动态类型语言刚好相反,它的数据类型是在编译其间检查的,也就是说在写程序时要声明所有变量的数据类型,C/C++是静态类型语言的典型代表,其他的静态类型语言还有 C#、JAVA 等。

对于动态语言与静态语言的区分,套用一句流行的话就是:StaTIc typing when possible, dynamictyping when needed。

强类型语言和弱类型语言的区别

强类型定义语言:强制数据类型定义的语言。也就是说,一旦一个变量被指定了某个数据类型,如果不经过强制转换,那么它就永远是这个数据类型了。举个例子:如果你定义了一个整型变量 a,那么程序根本不可能将 a 当作字符串类型处理。强类型定义语言是类型安全的语言。

弱类型定义语言:数据类型可以被忽略的语言。它与强类型定义语言相反, 一个变量可以赋不同数据类型的值。

强类型定义语言在速度上可能略逊色于弱类型定义语言,但是强类型定义语言带来的严谨性能够有效的避免许多错误。另外,“这门语言是不是动态语言”与“这门语言是否类型安全”之间是完全没有联系的!

例如:Python 是动态语言,是强类型定义语言(类型安全的语言); VBScript 是动态语言,是弱类型定义语言(类型不安全的语言);JAVA 是静态语言,是强类型定义语言(类型安全的语言)。

解释性语言和编译型语言的区别

编译型语言:编译型语言在执行之前要先经过编译过程,编译成为一个可执行的机器语言的文件,比如 exe。因为翻译只做一遍,以后都不需要翻译,所以执行效率高。

解释型语言:解释性语言编写的程序不进行预先编译,以文本方式存储程序代码。执行时才翻译执行。程序每执行一次就要翻译一遍。

fetch,实现一个 fetch

1
2
3
4
5
6
7
fetch('/some/url', { method: 'get', })
// 第一个then 设置请求的格式
.then(e => e.json())
// 第二个then 处理回调
.then((data) => {
<!-- data为真正数据 -->
}).catch(e => console.log("Oops, error", e))

fetch 发送 2 次请求的原因

fetch 发送 post 请求的时候,总是发送 2 次,第一次状态码是 204,第二次才成功? 原因很简单,因为你用 fetch 的 post 请求的时候,导致 fetch 第一次发送了一个 Options 请求,询问服务器是否支持修改的请求头,如果服务器支持,则在第二次中发送真正的 请求。

escape,encodeURL,encodeURIComponent 的区别

什么是同源/同域

协议 域名 端口 三者相同才算同源

如何解决跨域

cors jsonp
服务端鱼服务端之间不存在跨域

cookie跨域

我们请求https://www.google.com/时,浏览器会自动把google.com的Cookie带过去给google的服务器,而不会把https://www.baidu.com/的Cookie带过去给google的服务器。

这就意味着,由于域名不同,用户向系统A登录后,系统A返回给浏览器的Cookie,用户再请求系统B的时候不会将系统A的Cookie带过去。

针对Cookie存在跨域问题,有几种解决方案:

  • 服务端将Cookie写到客户端后,客户端对Cookie进行解析,将Token解析出来,此后请求都把这个Token带上就行了
  • 多个域名共享Cookie,在写到客户端的时候设置Cookie的domain。
  • 将Token保存在SessionStroage中(不依赖Cookie就没有跨域的问题了)

实现数组去重

见数组去重篇 一般是 reduce+includes,set

HTTP

介绍一下 http1-3

http1 是 0.9 的下一个版本,这个版本中他可以传输任何形式的内容不限于文本,还加入了 post 和 head 请求,且加入了更多标识信息的手段,比如 header 头部和响应码,以及对于资源提供了缓存。算是一个大更新,不过缺点也存在,也就是这个版本不支持断点续传,且没有传递主机名 hostname(大概是身份识别的一个影响),最重要的是服务器发送完响应就关闭了 TCP 连接,导致每次请求都要重新建立连接,我们知道 TCP 三次握手的成本是比较高的,不过他有一个缓解的方法,就是设置Connection:keep-alive让服务端先不要关闭 TCP 连接。从而达到了类似 TCP 复用的目的(这里经常忘记断点续传,因为这个内容确实不熟悉)

在 1.1 的时候,引入了长连接,默认 TCP 不去关闭,可以被多个请求复用。且支持断点续传。这个版本还新增了并发连接,管道机制,增加了 PUTDELETEOPTIONSPATCH 等方法,允许响应数据分块,有利于传输大文件,强制要求 host 头,让主机托管成为可能。不过也因为引入了长连接,带来了跨越两个版本的问题,队头阻塞,而且 1.1 版本中越来越多的头字段也暴露了引入 header 的问题,无状态。以及明文传输导致的数据安全性问题。

在 2.0 之前有个叫 spdy 的协议,是改进版本的 1.1,提出了多路复用的方案,通过多个请求 stream 共享一个 TCP 连接的方式,解决了 http 队头阻塞的问题。还设置了请求优先级,重要的请求优先响应() 压缩了 header 并且加密传输和服务端推送

2.0 就是基于 spdy 协议的,通过多路复用和流解决了队头阻塞的问题,使用了 hpack 算法解决了头部过大的问题。2.0 协议的特点是二进制分针,头部压缩,多路复用和服务端推送,流。

不过 2.0 尚未解决 TCP 阻塞的问题,这个问题根本上是传输层的问题,要想改变这个历史已久的协议要同时改变其他硬件厂商和网络厂商的设备,是不太可能的。于是 3.0 面向 UDP 协议做了新的调整。也就是 QUIC 协议

3.0 的 QUIC 协议要求严格加密

TCP 三次握手

三次握手(Three-way Handshake)其实就是指建立一个TCP连接时,需要客户端和服务器总共发送3个包。进行三次握手的主要作用就是为了确认双方的接收能力和发送能力是否正常、指定自己的初始化序列号为后面的可靠性传送做准备。实质上其实就是连接服务器指定端口,建立TCP连接,并同步连接双方的序列号和确认号,交换TCP窗口大小信息。

刚开始客户端处于 Closed 的状态,服务端处于 Listen 状态。
进行三次握手:

第一次握手:客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号 ISN。此时客户端处于 SYN_SENT 状态。

首部的同步位SYN=1,初始序号seq=x,SYN=1的报文段不能携带数据,但要消耗掉一个序号。

第二次握手:服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号 ISN(s)。同时会把客户端的 ISN + 1 作为ACK 的值,表示自己已经收到了客户端的 SYN,此时服务器处于 SYN_RCVD 的状态。

在确认报文段中SYN=1,ACK=1,确认号ack=x+1,初始序号seq=y。

第三次握手:客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 ESTABLISHED 状态。服务器收到 ACK 报文之后,也处于 ESTABLISHED 状态,此时,双方已建立起了连接。

确认报文段ACK=1,确认号ack=y+1,序号seq=x+1(初始为seq=x,第二个报文段所以要+1),ACK报文段可以携带数据,不携带数据则不消耗序号。

发送第一个SYN的一端将执行主动打开(active open),接收这个SYN并发回下一个SYN的另一端执行被动打开(passive open)。

在socket编程中,客户端执行connect()时,将触发三次握手。

为什么 ConnectionKeepalive 不能根本解决问题

头部字段不是标准的,不同浏览器实现起来可能不一样,所以不能从根本上解决问题

并发并行

并发指的是多个事件在同一时间段执行
并行是指多个事件在同一时间点执行

什么是主机托管

什么是无状态

无状态指的是对于请求头没有记忆能力。于是 2.0 提出的 hpack 解决了这个问题

http1 之前还有别的版本吗

0.9 为了传输 HTML(文本)诞生,而且只能发 GET 请求,请求报文只有一行,没有标识信息,服务器发完就关闭了 TCP

讲一下 HTTP 的请求报文

由三部分组成
请求行 请求头 请求体
请求行由请求方法 URL 协议版本组成
请求头
空行(注意)
请求体

请求方法 什么是幂等 什么是不幂等

GET POST HEAD DELETE PATCH OPTION PUT

常见的请求头?

Referer
Cookie
Connection
Content-Length
Accept-language

怎么进行断点续传

前端如何设置缓存

缓存中的 Etag 和 Expires 是什么区别

缓存新鲜度是怎么计算的

有什么响应码

100
101
200
201
202
203
300
301
302
304
400
404
500
504

TCP 和 UDP 什么区别

TCP 连接的过程

说一下 1.1 的管道机制

如何进行大文件传输

前端:文件名转 md5,文件切片,上传完后合并

为什么 1,1 强制要求 host 头

什么是 TCP 阻塞 什么是 HTTP 阻塞 怎么解决

TCP 阻塞是指当一个 TCP 的分节丢失的时候,因为 TCP 本身是可靠传输,所以他的后续分节会一直被接收端保存,直到丢失的这一个分节传输到接收端为止。

这样的可靠传输能保证数据的完整性,但是却影响了后续分节的传输。比如发三个图片第一个图片的分节丢失,也影响到后面的两个图片数据传输。

HTTP 队头阻塞是指 1.1 协议中的管道传输,由于管道传输的性质是可以多个请求同时发送但是要求响应顺序一致,所以导致了 http 阻塞。

解决 HTTP 阻塞靠的是 2.0 中多路复用和流的概念,多路复用有点类似于管道,不过他使得同多个请求共享同一个 TCP 连接,在这个连接中他会将报文拆分成二进制帧发送,服务器接收到乱序的帧重新组合成为对应的报文,所以就不存在先后的问题,也就解决了 HTTP 的队头阻塞

解决 TCP 阻塞靠的是 QUIC 协议,因为 TCP 阻塞本质上是传输层的问题,在改变协议困难的情况下(现在绝大部分硬件厂商网络运营商都是这样的协议)为了解决这个问题,从另外一个协议下手,也就是 UDP。UDP 不管顺序,丢包方面也有 QUIC 对应的方法去处理(多路复用,有多个独立的逻辑数据流)从而解决 TCP 阻塞的问题

http1.1 是怎么解决队头阻塞(实际上是缓解)

使用了域名分片的技术
一个域名拥有 6 个 TCP 连接,有多级域名的时候能吧更多的资源分配出来。
引起别的问题:因为 TCP 连接要经过 dns,三次握手,慢启动等操作。所以对于服务器来说连接太多容易造成网络拥挤

http2.0 有什么新特性

二进制分帧,多路复用,头部压缩,服务端推送,流

Hpack 算法原理

哈希表 传索引 哈夫曼收集整数和

2.0 之前是怎么分帧的

二进制分帧是怎么实现优先级和流量控制的

什么是多路复用

怎么进行服务端推送

2.0 有什么问题

http3.0 有什么新特性

QUIC 有安全问题吗

UDP 丢失数据包的问题在 QUIC 中是怎么解决的

说一下 TLS

RTT 是什么意思

TLS1.3 有什么问题吗

QUIC 是如何确立一个连接的

说一下 HTTP1-3 分别使用了多少个 RTT

Vue

v-if v-show

vue2 为什么不能监听数组

其实是可以监听的,原因在于Object.defineProperty这个 api 上,他是可以暴力监听数组中的所有元素的,以 index 的形式。但是如果数组内部有百万个元素,每次监听都要发生改变,这样就会造成很大的性能问题了。所以选择不去监听。Vue3 中使用了 Proxy,直接劫持整个数组对象,就没这个问题了。

使用过 Object.defineProperty 吗

参加手写篇

可以劫持对象的属性实现响应式 缺点是不支持增加和删除
且对于数组来说 性能问题 导致不采用劫持
另外要实现劫持 可以把value作为形参传递 避免return的时候无限递归
且注意是劫持新对象的属性的时候才是默认不可枚举不可修改不可删除,已有的对象不会有这个操作

nextTick 的原理

vue3 在响应式上做了什么优化

使用了proxy进行一个懒处理,标识了依赖的状态是否重复收集,处理了很多特殊案例比如嵌套effect,多次reactive同个对象,以及同个响应式对象被多次分配,数组等

为什么 Proxy 中需要 Reflect

Reflect的作用是为了修改对象的指向,比如说有个对象继承了另外一个响应式对象,访问对象本身的属性的时候,会被proxy劫持到返回继承对象的属性,如果此时get中传递了receiver,Reflect接收了receiver,那么他就能将其修改为正确的指向,访问对象本身的属性

Proxy 的 get 的三个参数的意义/为什么需要 receiver

get 三个参数是target key receiver
target是指被劫持的对象 key是劫持对象的属性 receiver是代理对象

前两个没啥好说的 主要是receiver这个 他是为了传递正确的指向而产生的,见上面的例子

receiver不仅表示代理对象本身,还有可能表示继承于代理对象的对象

MVC 和 MVVM 的区别

为什么 data 是一个函数

Vue 组件通讯有那些方式?

Vue 的生命周期方法有那些?在哪一步发送请求

说说 Vue 的内置指令。

怎么理解 Vue 的单向数据流

computed 和 watch 的区别和运用的场景

未被 template 使用的元素,computed 中的属性依赖的 data 发生改变了,computed 会重新计算吗

v-if 和 v-for 为什么不建议一起使用。

Vue 响应式数据的原理

vue3.0 用过吗 了解多少

Vue 的父子组件生命周期钩子执行顺序

虚拟 DOM 是什么 有什么优缺点

虚拟 DOM 原理

v-model 的原理

vue 事件绑定原理

v-for 为什么要加 key

手写发布订阅

vue-router 路由钩子函数是什么 执行顺序是什么

vue-router 原理

vue-router 动态路由是什么?有什么问题

Vuex 的理解

Vuex 页面刷新数据丢失怎么解决

Vuex 为什么要分模块而且加命名空间

使用过 Vue SSR 吗?说一说 SSR

Vue 使用了哪些设计者模式

你做过哪些 vue 的性能优化

Vue.mixin 的使用场景和原理

keep-alive 使用场景

LRU

手撕 keep-alive

Vue.set 方法原理

Vue.extend 原理

写过自定义指令吗 原理是啥

什么是 AST 语法树

Vue 修饰符有哪些

Vue 模板编译原理

生命周期钩子是如何实现的

函数式组件使用场景和原理

diff 算法了解吗

react

webpack

#

计网

OSI 七层

操作系统

Node

HTML

如何实现图片的懒加载

行内 块 空元素

行内元素有:a b span img input select strong button
块级元素有:div ul ol li dl dt dd h1 h2 h3 h4 h5 h6 p
空元素,即没有内容的 HTML 元素。空元素是在开始标签中关闭的,也就是空元素没有闭合标签:
常见的有:<br><hr><img><input><link><meta>

linkimport 的区别

link 除了引用样式文件,还可以引用图片等资源文件,而 import 只引用样式文件
link 属于 html 范畴 import 属于 css 范畴

1
<link rel="icon" sizes="any" mask href="//www.baidu.com/img/baidu.svg" />

兼容性不同,link 不存在兼容性的问题,import 在 IE5 以上支持,是 css2.1 新增的
在样式表文件可以使用 import 导入其它的样式表文件,而 link 不可以
link 引用 CSS 时,在页面载入时同时加载;@import 需要页面网页完全载入以后加载。
link 支持使用 Javascript 控制 DOM 去改变样式;而@import 不支持。

你了解的 video 标签

h5 新特性有哪些

H5 离线存储

离线存储指的是:在用户没有与因特网连接时,可以正常访问站点或应用,在用户与因特网连接时,更新用户机器上的缓存文件。

原理:HTML5 的离线存储是基于一个新建的 .appcache 文件的缓存机制(不是存储技术),通过这个文件上的解析清单离线存储资源,这些资源就会像 cookie 一样被存储了下来。之后当网络在处于离线状态下时,浏览器会通过被离线存储的数据进行页面展示

使用方法: (1)创建一个和 html 同名的 manifest 文件,然后在页面头部加入 manifest 属性:

1
<html lang="en" manifest="index.manifest"></html>

CopyErrorCopied
(2)在 cache.manifest 文件中编写需要离线存储的资源:

1
2
CACHE MANIFEST #v0.11 CACHE: js/app.js css/style.css NETWORK: resourse/logo.png
FALLBACK: / /offline.html CopyErrorCopied

CACHE: 表示需要离线存储的资源列表,由于包含 manifest 文件的页面将被自动离线存储,所以不需要把页面自身也列出来。
NETWORK: 表示在它下面列出来的资源只有在在线的情况下才能访问,他们不会被离线存储,所以在离线情况下无法使用这些资源。不过,如果在 CACHE 和 NETWORK 中有一个相同的资源,那么这个资源还是会被离线存储,也就是说 CACHE 的优先级更高。
FALLBACK: 表示如果访问第一个资源失败,那么就使用第二个资源来替换他,比如上面这个文件表示的就是如果访问根目录下任何一个资源失败了,那么就去访问 offline.html 。
(3)在离线状态时,操作 window.applicationCache 进行离线缓存的操作。

如何更新缓存:

(1)更新 manifest 文件

(2)通过 javascript 操作

(3)清除浏览器缓存

dragAPI

一个典型的拖放操作是这样的:用户选中一个可拖拽的(draggable) 元素,并将其拖拽(鼠标不放开)到一个可放置的(droppable) 元素,然后释放鼠标。

在这个过程中,最重要的三个点是:

让元素可拖拽
让另一个元素支持可放置
可拖拽和可放置元素之间的数据传递
dragstart:事件主体是被拖放元素,在开始拖放被拖放元素时触发。
darg:事件主体是被拖放元素,在正在拖放被拖放元素时触发。
dragenter:事件主体是目标元素,在被拖放元素进入某元素时触发。
dragover:事件主体是目标元素,在被拖放在某元素内移动时触发。
dragleave:事件主体是目标元素,在被拖放元素移出目标元素是触发。
drop:事件主体是目标元素,在目标元素完全接受被拖放元素时触发。
dragend:事件主体是被拖放元素,在整个拖放操作结束时触发。

选中 —-> 拖动 —-> 释放

拖动事件:dragstart、drag、dragend

放置事件:dragenter、dragover、drop

拖拽事件流:当拖动一个元素放置到目标元素上的时候将会按照如下顺序依次触发 dragstart->drag->dragenter->dragover->drop->dragend

meta 标签有哪些 有什么作用

meta 标签由 name 和 content 属性定义

用于描述网页文档的属性 比如 网页的作者 关键词 网站描述等。

以下是一些固定的 name 作为大家的共识

常用 meta 标签:

1
2
3
4
5
6
7
8
<!-- 字符集 -->
<meta charset="UTF-8" />
<!-- 页面关键词 -->
<meta name="keywords" content="关键词" />
<!-- 页面描述 -->
<meta name="description" content="页面描述内容" />
<!-- 页面重定向和刷新 -->
<meta http-equiv="refresh" content="0;url=" />

适配移动端

1
2
3
4
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1"
/>

其中,content 参数有以下几种:
width viewport :宽度(数值/device-width)
height viewport :高度(数值/device-height)
initial-scale :初始缩放比例
maximum-scale :最大缩放比例
minimum-scale :最小缩放比例
user-scalable :是否允许用户缩放(yes/no)

搜索引擎索引方式:

1
<meta name="robots" content="index,follow" />

其中,content 参数有以下几种:

all:文件将被检索,且页面上的链接可以被查询;
none:文件将不被检索,且页面上的链接不可以被查询;
index:文件将被检索;
follow:页面上的链接可以被查询;
noindex:文件将不被检索;
nofollow:页面上的链接不可以被查询。

script 标签中 defer 和 async 的区别

如果没有 defer 和 async 标签,浏览器就会立刻加载并执行对应的脚本 不会等待后续加载的文档元素,读到就开始加载
三者区别如下
https://cdn.jsdelivr.net/gh/Zlinni/Pic/img/20220905102104.png

蓝色表示 js 网络加载时间,红色表示 js 脚本执行时间,绿色表示 html 解析

defer 和 async 属性都是去异步的加载 js 脚本 所以不会阻塞页面的解析

区别在于:

  • 执行顺序: 多个带 async 的标签 不能保证执行顺序 多个带 defer 的标签 按序执行
  • 脚本是否并行执行:
    对于 async 来说,后续的文档加载与执行和 js 脚本加载与执行是并行的
    对于 defer 来说,后续文档的加载和 js 脚本加载是并行的,但是执行需要等待文档元素解析完之后,在DOMContentLoaded事件之前执行.

补充,DOMContentLoaded 事件

  • 是等 HTML 文档完全加载完和解析完之后运行的事件
  • 在 load 事件之前。
  • 不用等样式表、图像等完成加载

Doctype 作用

Doctype 是 html 的文档类型声明,告诉浏览器要以什么样的文档定义类型来解析文档,那么此时不同的渲染模式会影响 js 和 css 的解析

而且他必须声明在第一行,(不声明的话 就是 怪异模式)

目前有两种模式:

  1. 标准模式: 告诉浏览器使用 W3C 的标准去解析渲染页面.
  2. 怪异模式: 告诉浏览器使用自己的怪异模式去解析渲染页面,在此模式中以一种向后兼容的方式进行

获取模式:document.compatMode

src 和 href 的区别

都是用于请求相应的网络资源,但主要不同点在于

当浏览器解析到 src 所对应的资源的时候,会暂停其他资源的下载和处理,直到将该资源解析完毕,一般是用于标签内容的替换,比如 img

当浏览器解析到 href 的时候,会并行下载对应的资源,不会造成阻塞

CSS

BFC 的理解

BFC(Block formatting context)直译为”块级格式化上下文”。它是一个独立的渲染区域,只有 Block-level box 参与,
哪些情况会产生 BFC:
根元素
float 属性不为 none
position 为 absolute 或 fixed
display 为 inline-block, table-cell, table-caption, flex, inline-flex
overflow 不为 visible

盒模型的理解

盒模型都是由四个部分组成的,分别是 margin,border,padding 和 content

标准盒模型和 IE 盒模型的区别在于设置 width 和 height 时,所对应的范围不同

标准盒模型的 width 和 height 属性只包括 content
ie 盒模型的 width 和 height 包括 border padding content

可以通过 box-sizing 来改变元素的盒模型
标准盒模型:content-box;
怪异盒模型:border-box;

animation 属性

img 和 backgroundimg 的区别

水平垂直居中

margin-left 和 margin-top 自身宽度的一半和 transform:translate(-50%,-50%),有什么区别

在对于没有设置高度,高度由内容撑起来的盒子上有什么区别,这种盒子可以使用什么方式垂直水平居中

选择器优先级

1.通配符选择器和继承:权重为 0, 2.标签选择器:权重为 0001 3.类选择器:权重为 0010
4.id 选择器:权重为 0100 5.行内样式:权重为 1000
6.!important:权重为无穷大

重绘和回流 为什么要减少回流 什么情况引起回流

重绘简单来说就是重新绘画,当一个元素更换背景,更换颜色,虽然不会影响页面布局,但是颜色或者背景变了,就会重新渲染页面,这就是重绘

flex1 代表什么

默认 1rem 是多少 px

16

chrome 最小字体是多少 px 怎么实现更小的字体

最小 12px

1
-webkit-transform: scale(0.5);

SVG 和 Canvas 区别

外边距塌陷问题

当两个在标准流中相邻(兄弟或父子关系)的块级元素的外边距组合在一起的时候,垂直方向上会发生外边距塌陷的问题,计算方式:

  1. 两个都为正,取最大
  2. 一正一负加起来
  3. 两个负 取绝对值最大

父类高度塌陷问题

如果子元素都是浮动

  1. 直接给父元素设置高度,在子元素改变的情况下,此方法使用起来比较繁杂。
  2. 给父元素直接设置 overflow:hidden;样式
  3. 父元结束标签之前加空 div,样式为 clear:both;
  4. 伪元素选择器,父元素加上 clearfix

伪类和伪元素的区别

1
伪类  :link  :hover         伪元素  ::before    ::after

其中伪类和伪元素的根本区别在于:它们是否创造了新的元素,, 这个新创造的元素就叫 “伪无素” 。
伪元素/伪对象:不存在在 DOM 文档中,是虚拟的元素,是创建新元素。 这个新元素(伪元素) 是某个元素的子元素,这个子元素虽然在逻辑上存在,但却并不实际存在于文档树中.
伪类:存在 DOM 文档中,(无标签,找不到, 只有符合触发条件时才能看到 ), 逻辑上存在但在文档树中却无须标识的“幽灵”分类。

css 继承

只有颜色,文字,字体间距行高对齐方式,和列表的样式可以继承

cssinjs

cssmodule

为什么要使用 tailwind

#

浏览器

浏览器内核

GUI 线程和 JS 线程

浏览器的渲染过程

性能指标,怎么看

设计模式

单例模式

装饰器模式

策略模式

设计模式的原则是什么

  1. 开放封闭原则 对拓展开放 对修改关闭
  2. 单一职责原则 实现类要职责单一
  3. 依赖倒转原则 面向接口编程
  4. 迪米特法则 降低耦合
  5. 接口隔离原则 设计接口的时候要精简单一
  6. 合成/聚合复用原则
  7. 里氏代换原则 不要破坏继承体系

nodejs

node 和 java 有什么区别

node 单线程相比 java 的优点

node 怎么开启多线程

node 洋葱模型

BFF 的理解

其他

二维码扫描多端登录设计思路

项目数据库设计

原生设计一个 dialog,需要设计哪些部分

设计一个中间件

pnpm 的优势

CICD

nginx

nginx 主要是做什么的

静态资源缓存 hash,gzip,负载均衡,超时连接

nginx 怎么代理 websocket,有什么问题

nginx 怎么开启 gzip

nginx 真的解决了跨域吗

服务端和服务端之间不存在跨域

typescript

数据结构

树的种类以及特性

常见的数据结构