1. 1. 说明
  2. 2. es新特性
    1. 2.1. es7
    2. 2.2. es8
    3. 2.3. es9
    4. 2.4. es10
    5. 2.5. es11
  3. 3. 函数
  4. 4. this
    1. 4.1. 具体怎么判断好呢?
  5. 5. call、apply、bind
    1. 5.1. call、apply应该用哪个?
    2. 5.2. 手写call、bind、apply
    3. 5.3. 实现apply/call/bind
    4. 5.4. apply
    5. 5.5. call
    6. 5.6. bind
      1. 5.6.1. 常见面试题
      2. 5.6.2. javascript手写call,apply,bind函数,尤其是bind函数
  6. 6. 闭包
  7. 7. 原型和原型链
  8. 8. 线程,EventLoop
  9. 9. 循环
    1. 9.1. for,forEach,map
    2. 9.2. for..in & for..of
    3. 9.3. filter,find,some,sort,reduce
    4. 9.4. reduce
  10. 10. 深拷贝
  11. 11. 柯里化
  12. 12. reflect和proxy
    1. 12.1. 数据绑定和响应式
    2. 12.2. 日志记录
    3. 12.3. 权限控制
  13. 13. 网络
    1. 13.1. http
    2. 13.2. HTTP/2 和 HTTP/1.1 的对比
      1. 13.2.1. 相关问题
      2. 13.2.2. 回答关键点
    3. 13.3. HTTP/1.1 相较 HTTP/1.0 的改进和优化:
      1. 13.3.1. HTTP/1.1 的缺点:
      2. 13.3.2. HTTP/2 的优点:
      3. 13.3.3. 知识点深入
      4. 13.3.4. 持久连接
      5. 13.3.5. HTTP管道化
      6. 13.3.6. 分块编码传输
      7. 13.3.7. 新增Host头处理
      8. 13.3.8. 断点续传、并行下载
      9. 13.3.9. HTTP/1.1 的缺点
    4. 13.4. HTTP/2
      1. 13.4.1. HTTP/2 的优点
      2. 13.4.2. HTTP和HTTPS的基本概念
  14. 14. xss和csrf
  15. 15. 十大排序算法
    1. 15.1. 冒泡排序
    2. 15.2. 选择排序
    3. 15.3. 插入排序
    4. 15.4. *** 快速排序(Top1 高频)
    5. 15.5. *** 堆排序(面试经常问“Top K”)
    6. 15.6. 计数排序(不太会)
    7. 15.7. 桶排序
    8. 15.8. 基数排序
    9. 15.9. *** 归并排序(分治 + 面试能问很深)
    10. 15.10. 希尔排序
    11. 15.11. 后面单调栈的代码实现有点问题,粗看了一下后面几个题,先不按照这里的算法题复习了哦!转去瓶子算法那,感觉更好懂。
  16. 16. 三大框架和webpack部分跳过,写的不好。
  17. 17. 发布订阅和观察者模式

2025年|前端内参复习

说明

给自己过知识点看的,查漏补缺,建议看原作。
前端内参

es新特性

es7

1
2
3
4
5
6
7
8
9
10
11

// 1. Array.prototype.includes
let arr = [1,2,NaN];
console.log(arr.includes(NaN)); // true
console.log(arr.indexOf(NaN) != -1) // false
console.log(arr.includes(1)); // true

// 2. 指数运算符

console.log(2**3) // 8

es8

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
55
56
// 1. Object.values
let obj1 = {x: 'xxx', y: 'yyy'}
Object.values(obj1)
// ['xxx', 'yyy']
let arr1 = ['e', 's', '8']
Object.values(arr1)
// ['e', 's', '8']
//当把数字当做对象的键的时候,返回的数组以键的值升序排序
let obj2 = {10: 'xxx', 2: 'yyy', 1: 'zzz'}
Object.values(obj2)
// ['zzz', 'yyy', 'xxx']

// --
Object.entries(obj1); // [['x', 'xxx'], ['y', 1]]
Object.entries(arr1); // [['0', 'e'], ['1', 's'], ['2', '8']]
Object.entries(obj2); // [['1', 'yyy'], ['3', 'zzz'], ['10', 'xxx']]
Object.entries('es8'); // [['0', 'e'], ['1', 's'], ['2', '8']]

for (const [key, value] of Object.entries('es8')){
console.log(key, value);
}

const values = Object.entries('es8').map(([k, v]) => v); // ['e', 's', '8']

Object.entries('es8').forEach(([key, value]) => {
console.log(key, value);
});


// --

// 2. 字符串追加
'es8'.padStart(7, 0)
// '0000es8'
'es8'.padEnd(7, 0)
// 'es80000'

// 3. Object.getOwnPropertyDescriptors

let obj3 = {
get es8(){}
}

Object.getOwnPropertyDescriptors(obj3)
// {
// es8: {
// configurable: true,
// enumerable: true,
// get: function es8(){}, //the getter function
// set: undefined
// }
// }

// 4. 结尾允许逗号
// 5. async/await
// 6. 共享内存和Atomics对象

es9

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 1. 异步迭代器
async function func(array) {
for await (let i of array) {//异步迭代
someFunc(i);
}
}
// 2. Promise.prototype.finally
// 3. 重新修订了字面量的转义
/**
ES9之前,\u表示 unicode 转义,\x表示十六进制转义,\后跟一个数字表示八进制转义,这使得创建特定的字符串变得不可能,例如`Windows文件路径C:\uuu\xxx\111`。
要取消转义序列的语法限制,可在模板字符串之前使用标记函数String.raw。
**/
let s = `\u{54}` // 会转义成unicode "T"
console.log(s); //>> T

let str = String.raw`\u{54}`; // 不会被转义
console.log(str); // >> \u{54}

// 4. Rest / Spread(扩展运算符和rest参数)

es10

1
2
3
4
5

// 1. String.prototype.{trimStart,trimEnd}
// 2. Object.fromEntries
// 3. Array.prototype.{flat,flatMap}

es11

1
2
3
4
5
6
7
8
9
10
11
12

// 1. 可选链
let second = obj?.first?.second;

// 2. Nullish coalescing Operator(空值处理)
let a = 0;
let v = a ?? "some value";
console.log(v); //>> 0
// 3. Promise.allSettled
// 4. Bigint
// 5. globalThis

函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 1. 立即执行函数怎么递归
(function (i){
console.log("函数名为"+func.name+",第"+i+"次调用")
if(i<3){//递归出口
arguments.callee(++i);
}
})(1);
//>> 函数名为func,第1次调用
//>> 函数名为func,第2次调用
//>> 函数名为func,第3次调用

// 2. 箭头函数不暴露argument对象
// 箭头函数一个明显作用就是可以保持this的指向,总是指向定义它时所在的上下文环境。最后,箭头函数也没有自己的 super或new.target。

this

  1. this的指向,是在函数被调用的时候确定的,也就是执行上下文被创建时确定的。
  2. this 的指向和函数声明的位置没有任何关系,只取决于函数的调用位置(也即由谁、在什么地方调用这个函数)。
  3. 正因为在执行上下文的创建阶段this的指向就已经被确定了,在执行阶段this指向不可再被更改。
  4. this指向最靠近被调用函数的对象。
  5. this显式指向:call、apply、 bind。

具体怎么判断好呢?

  1. 函数是否在new 中被调用(new 操作符指向),如果是的话,this 绑定的是新创建的对象。

  2. 函数是否被call、apply、bind 改变过this指向,如果是的话,this 绑定的是改变后的对象。

  3. 函数是否被当做某个对象的方法而调用(隐式指向)?如果是的话,this指向的是这个对象。

  4. 若以上都不是的话,使用默认绑定。

  5. null或者undefined作为this指向的对象传入call、apply或者bind,这些值在调用时会被忽略,实际应用的是默认指向规则。

  6. this 默认指向window,但是严格模式下this指向undefined。隐式指向之隐式丢失

  7. 函数是否被bind改变过this指向,如果是的话,this 绑定的是改变后的对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function func() {
    console.log(this.a);
    }
    var a = 2;
    var o = { a: 3, func: func };
    var p = { a: 4 };
    o.func(); //>> 3
    (p.func = o.func)(); //>> 2
    // 赋值表达式 p.func=o.func 的返回值是目标函数的引用,也就是 func 函数的引用
    // 因此调用位置是 func() 而不是 p.func() 或者 o.func()
  8. 箭头函数的this指向外部函数的this。被绑定之后无法修改。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    function func() {
    // 返回一个箭头函数
    return a => {
    //this 继承自 func()
    console.log(this.a);
    };
    }
    var obj1 = {
    a: 2
    };
    var obj2 = {
    a: 3
    };

    var bar = func.call(obj1);
    bar.call(obj2); //>> 2 不是 3 !

    // func() 内部创建的箭头函数会捕获调用时 func() 的 this。
    // 由于 func() 的 this 绑定到 obj1, bar(引用箭头函数)的 this 也会绑定到 obj1,
    // this一旦被确定,就不可更改,所以箭头函数的绑定无法被修改。(new 也不行!)

call、apply、bind

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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77

func.call(thisArg, param1, param2, ...)//func是个函数

func.apply(thisArg, [param1,param2,...])

func.bind(thisArg, param1, param2, ...)

// 返回值:
// call / apply:返回func 执行的结果 ;
// bind:返回func的拷贝,并拥有指定的this值和初始参数。

// 参数:
// thisArg(可选):
// func的this指向thisArg对象;
// 非严格模式下:若thisArg指定为null,undefined,则func的this指向window对象;
// 严格模式下:func的this为undefined;
// 值为原始值(数字,字符串,布尔值)的this会指向该原始值的自动包装对象,如 String、Number、Boolean。
// param1,param2(可选): 传给func的参数。
// 如果param不传或为 null/undefined,则表示不需要传入任何参数.
// apply第二个参数为类数组对象,数组内各项的值为传给func的参数。

// *******必须是函数才能调用call/apply/bind
// ***** 类数组 arrayLike 可以通过下标进行调用,具有length属性,同时也可以通过 for 循环进行遍历。
// 但是需要注意的是:类数组无法使用 forEach、splice、push 等数组原型链上的方法
// 获取DOM节点的方法,使用arguments获取到的所有参数
let domNodes = Array.prototype.slice.call(document.getElementsByTagName("h1")); // 把类数组转换成数组
// 1. 判断数据类型
function isType(data, type) {
const typeObj = {
"[object String]": "string",
"[object Number]": "number",
"[object Boolean]": "boolean",
"[object Null]": "null",
"[object Undefined]": "undefined",
"[object Object]": "object",
"[object Array]": "array",
"[object Function]": "function",
"[object Date]": "date", // Object.prototype.toString.call(new Date())
"[object RegExp]": "regExp",
"[object Map]": "map",
"[object Set]": "set",
"[object HTMLDivElement]": "dom", // document.querySelector('#app')
"[object WeakMap]": "weakMap",
"[object Window]": "window", // Object.prototype.toString.call(window)
"[object Error]": "error", // new Error('1')
"[object Arguments]": "arguments"
};

let name = Object.prototype.toString.call(data); // 借用Object.prototype.toString()获取数据类型
let typeName = typeObj[name] || "未知类型"; // 匹配数据类型
return typeName === type; // 判断该数据类型是否为传入的类型
}

console.log(
isType({}, "object"), //>> true
isType([], "array"), //>> true
isType(new Date(), "object"), //>> false
isType(new Date(), "date") //>> true
);

// 2. 类数组对象
var arrayLike = {
0: "OB",
1: "Koro1",
length: 2
};

Array.prototype.push.call(arrayLike, "添加数组项1", "添加数组项2");

console.log(arrayLike);
//>> {"0":"OB","1":"Koro1","2":"添加数组项1","3":"添加数组项2","length":4}


// 3. 获取数组中的最大值和最小值
const arr = [15, 6, 12, 13, 16];
const max = Math.max.apply(Math, arr); // 16
const min = Math.min.apply(Math, arr); // 6

call、apply应该用哪个?

call,apply的效果完全一样,它们的区别也在于
参数数量/顺序确定就用call,参数数量/顺序不确定的话就用apply。
考虑可读性:参数数量不多就用call,参数数量比较多的话,把参数整合成数组,使用apply。
参数集合已经是一个数组的情况,用apply,比如上文的获取数组最大值/最小值。

插一嘴: ** 页面通信,一般传递回调函数的时候会容易丢失this的值。

手写call、bind、apply

实现apply/call/bind

apply

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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/**
* 1. 用原生JavaScript实现apply
*/
Function.prototype.myApply = function(thisArg) {
if (thisArg === null || thisArg === undefined) {
thisArg = window;
} else {
thisArg = Object(thisArg);
}

//判断是否为【类数组对象】
function isArrayLike(o) {
if (
o && // o不是null、undefined等
typeof o === "object" && // o是对象
isFinite(o.length) && // o.length是有限数值
o.length >= 0 && // o.length为非负值
o.length === Math.floor(o.length) && // o.length是整数
o.length < 4294967296
)
// o.length < 2^32
return true;
else return false;
}

const specialMethod = Symbol("anything");
thisArg[specialMethod] = this;

let args = arguments[1]; // 获取参数数组
let result;

// 处理传进来的第二个参数
if (args) {
// 是否传递第二个参数
if (!Array.isArray(args) && !isArrayLike(args)) {
throw new TypeError(
"第二个参数既不为数组,也不为类数组对象。抛出错误"
);
} else {
args = Array.from(args); // 转为数组
result = thisArg[specialMethod](...args); // 执行函数并展开数组,传递函数参数
}
} else {
result = thisArg[specialMethod]();
}

delete thisArg[specialMethod];
return result; // 返回函数执行结果
};

// 2. 用原生JavaScript实现apply
Function.prototype.apply = function(thisArg, argsArray){
if(typeof this !== 'function'){
throw new TypeError('Function.prototype.apply called on non-function')
}

if(thisArg === undefined || thisArg === null){
thisArg = window;
}else{
thisArg = Object(thisArg)
}

const func = Symbol('func');
thisArg[func] = this;

let result;

if(argArray && typeof argArray === 'obejct' && 'length' in argsArray){
result = thisArg[func](...Array.from(argsArray))
} else if(argsArray && undefined || argsArray === null){
result = thisArg[func]()
} else {
throw new TypeError('argsArray must be an array-like object')
}

delete thisArg(func)

return result;
}


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
Function.prototype.call = function(thisArg, ...argsArray){
if(typeof this !== 'function'){
throw new TypeError('function.prototype.call called on non-function')
}

if(thisArg === undefined || thisArg === null){
thisArg = window;
} else {
thisArg = Object(thisArg)
}

const func = Symbol('func');

thisArg[func] = this;

let result;
if(argsArray.length){
result = thisArg[func](...argsArray)
}else{
result = thisArg[func]()
}

delete thisArg[func];

return result;
}

bind

常见面试题

1
2
3
4
5
6
for(var i=0;i<=5;i++){
setTimeout(function(i){
console.log(i)
}.bind(null, i), i*1000)
}

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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
Function.prototype.bind = function(thisArg, ...argsArray){
if(typeof this !== 'function'){
throw new TypeError('function.prototype.bind called on non-function')
};

if(thisArg === undefined || thisArg === null){
thisArg = window;
}else{
thisArg = Object(thisArg)
}

const func = this;

const bound = function(...boundArgsArray){
let isNew = false;

try{
isNew = this instanceof func;
}catch(e){

}

return func.apply(isNew ? this: thisArg, argsArray.concat(boundArgsArray))
}

const Empty = function(){}
Empty.prototype = this.prototype;
bound.prototype = new Empty();

return bound;
}


/**
* 用原生JavaScript实现bind
*/
Function.prototype.myBind = function(objThis, ...params) {
const thisFn = this;//存储调用函数,以及上方的params(函数参数)
//对返回的函数 secondParams 二次传参
let funcForBind = function(...secondParams) {
//检查this是否是funcForBind的实例?也就是检查funcForBind是否通过new调用
const isNew = this instanceof funcForBind;

//new调用就绑定到this上,否则就绑定到传入的objThis上
const thisArg = isNew ? this : Object(objThis);

//用call执行调用函数,绑定this的指向,并传递参数。返回执行结果
return thisFn.call(thisArg, ...params, ...secondParams);
};

//复制调用函数的prototype给funcForBind
funcForBind.prototype = Object.create(thisFn.prototype);
return funcForBind;//返回拷贝的函数
};

Function.prototype.myBind1 = function(thisArg, ...args){
if(typeof this !== "function"){
throw new Error("is not callable")
}

if(thisArg === undefined || thisArg === null){
thisArg = window
} else {
thisArg = Object(thisArg)
}

let funcSym = this

let functionForBind = function(){
return thisArg.apply(funcSym, ...args, ...arguments)
}

functionForBind.prototype = Object.create(thisArg.prototype)

return functionForBind


}

javascript手写call,apply,bind函数,尤其是bind函数

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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83

Function.prototype.myCall = function(thisArg, ...arr){
if(thisArg === null || thisArg === undefined){
thisArg = window;
} else {
thisArg = Object(thisArg);
}

const specialMethod = Symbol('anything');
thisArg[specialMethod] = this;
let result = thisArg[specialMethod](...arr);

delete thisArg[specialMethod];
return result;
};

let obj = {
name: "coffe1891"
};

function func(){
console.log(this.name)
}

func.myCall(obj)

Function.prototype.myApply = function(thisArg){
if(thisArg === null || thisArg === undefined){
thisArg = window;
} else {
thisArg = Object(thisArg)
}

function isArrayLike(obj){
if(o && typeof o === "object" && isFinite(o.length) && o.length >= 0 && o.length === Math.floor(o.length) && o.length < 4294967296)
return true;
else return false
}

const specialMethod = Symbol("anything")
thisArg[specialMethod] = this;

let args = arguments[1]
let result;

if(args){
if(!Array.isArray(args) && !isArrayLike(args)){
throw new TypeError("args must be an array-like object")
} else{
args = Array.from(args);
result = thisArg[specialMethod](...args);
} else {
result = thisArg[speacialMethod]
}
delete thisArg[specialMethod]
return result;
}

Function.prototype.myBind = function(objThis, ...params){
const thisFn = this;
let funcForBind = function(...secondParams){
const isNew = this instanceof funcForBind;
const thisArg = isNew ? this : objThis;
return thisFn.call(thisArg, ...params, ...secondParams)
};

funcForBind.prototype = Object.create(thisFn.prototype)
return funcForBind;
}

let func = function(p,secondParams){//其实测试用的func其参数可以是任意多个
console.log(p.name);
console.log(this.name);
console.log(secondParams);
}
let obj={
name:"1891"
}
func.myBind(obj,{name:"coffe"})("二次传参");
//>> coffe
//>> 1891
//>> 二次传参

闭包

1
2
3
4
5
6
7
8
9
var report = (function() {
var imgs = [];//在内存里持久化
return function(src) {
var img = new Image();
imgs.push(img);//引用局部变量imgs
img.src = src;
}
}());
report('http://www.xxx.com/getClientInfo');//把客户端信息上报数据

原型和原型链

  • ES6实现的继承,本质仍是基于原型和原型链。一般函数有prototype属性。
  • 每个对象(实例)都有一个属性proto,指向他的构造函数(constructor)的prototype属性。
  • 一个对象的原型就是它的构造函数的prototype属性的值,因此proto也即原型的代名词。
  • 对象的proto也有自己的proto,层层向上,直到proto为null。换句话说,原型本身也有自己的原型。这种由原型层层链接起来的数据结构称为原型链。因为null不再有原型,所以原型链的末端是null。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var a = function(){};
var b=[1,2,3];

//a的构造函数是「Function函数」
console.log(a.__proto__ == Function.prototype);//>> true
//b的构造函数是「Array函数」
console.log(b.__proto__ == Array.prototype);//>> true

//因为「Function函数」和「Array函数」又都是对象,其构造函数
//是「Object函数」,所以,a和b的原型的原型都是Object.prototype
console.log(a.__proto__.__proto__ === Object.prototype);//>> true
console.log(b.__proto__.__proto__ === Object.prototype);//>> true

//Object作为顶级对象的构造函数,它实例的原型本身就不再有原型了,因此它原型
//的__proto__属性为null
console.log(new Object().__proto__.__proto__);//>> null
//也即Object类型对象,其原型(Object.prototype)的__proto__为null
console.log(Object.prototype.__proto__);//>> null
  • 使用最新的方法Object.setPrototypeOf(类似Reflect.setPrototypeOf)可以很方便地给对象设置原型,这个对象会继承该原型所有属性和方法。
  • 但是,setPrototypeOf的性能很差,我们应该尽量使用 Object.create()来为某个对象设置原型。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    //obj的原型是Object.prototype
    var obj={
    methodA(){
    console.log("coffe");
    }
    }

    var newObj = Object.create(obj);//以obj为原型创建一个新的对象

    //methodA实际上是newObj原型对象obj上的方法。也即newObj继承了它的原型对象obj的属性和方法。
    newObj.methodA();//>> coffe
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
class A {
}

A===A.prototype.constructor;//>> true

class A{
constructor() {
// ...
}

toString() {
// ...
}

toValue() {
// ...
}
}

// 等同于
A.prototype = {
constructor() {},
toString() {},
toValue() {},
};

class 作为构造函数的语法糖,同时有prototype属性和proto属性,因此同时存在两条继承链。

  1. 子类的proto属性,表示构造函数的继承,总是指向父类。
  2. 子类prototype属性的proto属性,表示方法的继承,总是指向父类的prototype属性。
1
2
3
4
5
6
7
8
class A {
}

class B extends A {
}

B.__proto__ === A //>> true
B.prototype.__proto__ === A.prototype //>> true
1
2
3
4
5
6
7
8
9
10
// 方法1:使用 in 操作符(检查原型链)
console.log("methodA" in newObj); // true

// 方法2:直接调用方法(如果存在)
if (newObj.methodA) {
newObj.methodA(); // "coffe"
}

// 方法3:检查原型对象
console.log(Object.getPrototypeOf(newObj).hasOwnProperty("methodA")); // true

线程,EventLoop

  1. 浏览器的线程
    JS引擎是单线程的,但是浏览器是多线程的。现代浏览器的一个 tab ,其中的线程包括但不局限于:
    GUI 渲染线程
    JS引擎线程
    事件触发线程
    定时器触发线程
    异步http请求线程
    JavaScript中的异步回调是通过 WebAPIs 去支持的,常见的有 XMLHttpRequest,setTimeout,事件回调(onclik, onscroll等)。而这几个 API 浏览器都提供了单独的线程去运行,所以才会有合适的地方去处理定时器的计时、各种请求的回调。即当代码中出现这几个定义的异步任务,是由浏览器实现了它们与JS引擎的通信,与JS引擎不在同一个线程。
    另外,GUI 渲染和JavaScript执行是互斥的。虽然两者属于不同的线程,但是由于JavaScript执行结果可能会对页面产生影响,所以浏览器对此做了处理,大部分情况下JavaScript线程执行,执行渲染(render)的线程就会暂停,JavaScript的同步代码执行完再去渲染。

Event Loop的作用很简单: 监控调用栈和任务队列,如果调用栈是空的,它就会取出队列中的第一个”callback函数”,然后将它压入到调用栈中,然后执行它。
总的来说,Event Loop 是实现异步回调的一种机制而已。

微任务队列一次只有一个,先清空微任务队列再做宏任务队列,然后微任务又进来,又继续清空微任务队列。微任务队列:promise等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

// 执行顺序问题,考察频率挺高
setTimeout(function() {
console.log(1);
});
new Promise(function(resolve, reject) {
console.log(2);
resolve(3);
}).then(function(val) {
console.log(val);
});
console.log(4);

// 2, 4, 3, 1

循环

for,forEach,map

如果使用箭头函数表达式来传入thisArg 参数会被忽略,因为箭头函数在词法上绑定了this值。
for循环满足特定条件时跳出循环体,或者跳出本次循环直接进入下一次循环。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
for (let i = 0; i < arr.length; i++) {
// 当迭代变量 i == 3 时,跳过此次循环直接进入下一次
if (i == 3) continue;
console.log(arr[i]);
// 当 i > 7 时,跳出循环
if (i > 7) break;
}

//>> 0
//>> 1
//>> 2
//>> 4
//>> 5
//>> 6
//>> 7
//>> 8

for..in & for..of

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
Array.prototype.getLength = function () {
return this.length;
}
var arr = [1, 2, 4, 5, 6, 7]
arr.name = "coffe1981";
console.log("-------for...of--------");
for (var value of arr) {
console.log(value);
}
console.log("-------for...in--------");
for (var key in arr) {
console.log(key);
}

//>> -------for...of--------
//>> 1
//>> 2
//>> 4
//>> 5
//>> 6
//>> 7
//>> -------for...in--------
//>> 0
//>> 1
//>> 2
//>> 3
//>> 4
//>> 5
//>> name
//>> getLength

//for..of适用遍历数组/类数组对象/字符串/map/set等拥有迭代器对象的集合,但是不能遍历对象,因为没有迭代器对象。遍历对象通常用for...in来遍历对象的键名。
//与forEach不同的是,for...of和for...in都可以正确响应break、continue和return语句。

filter,find,some,sort,reduce

reduce

initialValue可选作为第一次调用callback函数时的第一个参数的值。 如果没有提供初始值,则将使用数组中的第一个元素(也即针对数组的arr循环计算少一次,千万要注意这点)。 在没有初始值的空数组上调用reduce将报错。

数组—->对象

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

const arr = [
{
username: 'makai',
displayname: '馆长',
email: 'guanzhang@coffe1891.com'
},
{
username: 'xiaoer',
displayname: '小二',
email: 'xiaoer@coffe1891.com'
},
{
username: 'zhanggui',
displayname: '掌柜',
email: null
},
];

function cb1(acc, person){
return {...acc, [person.username]: person}
}
const obj = arr.reduce(cb1, {})

const emptyArray = [];
const result = emptyArray.reduce((acc, curr) => acc + curr, 0);
console.log(result); // 输出: 0
const result1 = emptyArray.reduce((acc, curr) => acc + curr);
// 报错:TypeError: Reduce of empty array with no initial value

const arrA = [0.3, 1.2, 3.4, 0.2, 3.2, 5.5, 0.4];
function callback(acc, reading) {
return {
minReading: Math.min(acc.minReading, reading),
maxReading: Math.max(acc.maxReading, reading),
};
}
const initMinMax = {
minReading: Number.MAX_VALUE,
maxReading: Number.MIN_VALUE
};
const result = arrA.reduce(callback, initMinMax);
console.log(result);
//>> {minReading: 0.2, maxReading: 5.5}

const arr8 = [1,2,3]

const sum1 = arr8.reduce((acc, cur) => acc + cur,0)

深拷贝

缺点:

  • 会忽略undefined
  • 会忽略symbol
  • 如果对象的属性为Function,因为JSON格式字符串不支持Function,在序列化的时候会自动删除;
  • 诸如 Map, Set, RegExp, Date, ArrayBuffer和其他内置类型在进行序列化时会丢失;
  • 不支持循环引用对象的拷贝。
    1
    2
    let a = {x: 2}
    let b = JSON.sringiy(a)

缺点:
对象嵌套层次超过2层,就会出现浅拷贝的状况;
非可枚举的属性无法被拷贝。

1
2
3
let a = {x:3}
let b = Object.assign({}, a)

缺点:
这个方法是异步的;
拷贝有函数的对象时,还是会报错。

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

// 有undefined + 循环引用
let obj = {
a: 1,
b: {
c: 2,
d: 3,
},
f: undefined
}

obj.c = obj.b;
obj.e = obj.a;
obj.b.c = obj.c;
obj.b.d = obj.b;
obj.b.e = obj.b.c;

function deepClone(obj){
return new Promise((resolve) => {
const {port1, port2} = new MessageChannel();
port2.onmessage = ev => resolve(ev.data);
port1.postMessage(obj);
})
}


deepClone(obj).then((copy) => {
let copyObj = copy;
console.log(copyObj, obj)
console.log(copyObj == obj)
})
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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// 循环引用的深拷贝
// 查询父链,检查是否出现过相同的obj
// 解释:
// originalParent → 上一次 clone 的原对象
// currentParent → 对应的克隆对象
// 从 parent 一层一层往上查,看“之前是否 clone 过这个对象”
// 找到就直接返回,避免死循环
// 等价于:
// “用链表模拟 Map 缓存”
// 每次递归都新增一个 parent 节点,形成一条“父链”:
function deepClone(obj, parent = null){
let result = {};
let keys = Object.keys(obj),
key = null,
temp = null,
_parent = parent;
while(_parent){
if(_parent.originalParent === obj){
return _parent.currentParent
}
_parent = _parent.parent
}

for(let i=0; i<keys.length; i++){
key = keys[i];
temp = obj[key]
if(temp && typeof temp === 'object'){
result[key] = deepClone(temp, {
originalParent: obj, //原对象
currentParent: result, //克隆对象
parent: parent //链式指针
});
}else{
result[key] = temp
}
}
return result;
}


//用weakMap实现深拷贝,key是对象不会阻止对象被垃圾回收。
function deepCloneWeakMapVersion(obj, cache = new WeakMap()) {
if (obj && typeof obj === 'object') {
if (cache.has(obj)) return cache.get(obj);

const clone = Array.isArray(obj) ? [] : {};
cache.set(obj, clone);

for (let key in obj) {
clone[key] = deepCloneWeakMapVersion(obj[key], cache);
}
return clone;
}
return obj;
}

function deepCloneMapVersion(obj, map = new Map()) {
if (obj && typeof obj === 'object') {
if (map.has(obj)) return map.get(obj); // 处理循环引用

const result = Array.isArray(obj) ? [] : {};
map.set(obj, result);

for (let key in obj) {
result[key] = deepCloneMapVersion(obj[key], map);
}
return result;
}
return obj;
}

// Map 做缓存:有对象 → clone 后存入 Map
// 如果再次遇到同一个对象(循环引用)→ 直接返回缓存的 clone

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

function deepClone(obj){
let map = new WeakMap();
function dp(obj){
let result = null;
let keys = null,
key = null,
temp = null,
existsObj = null;

existObj = map.get(obj)
if(existObj){
reurn existObj
}
keys = Object.keys(obj);
result = {};
map.set(obj, result);
for(let i=0; i<keys.length; i++){
key = keys[i];
temp = obj[key];
if(temp && typeof temp === 'object'){
result[key] = dp(temp)
}else{
result[key] = temp;
}
}
return result;

return dp(obj)
}
}


1
2
3
4
// 终极方案是lodash的_.cloneDeep。
_cloneDeep({xx:11})


柯里化

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
// TODO
// bind的柯里化
Function.prototype.bind = function(context) {
if (typeof this !== 'function') {
throw new TypeError('Function.prototype.bind called on incompatible ' + this);
}

const originalFunc = this;
const bindArgs = Array.prototype.slice.call(arguments, 1);

const boundFunc = function() {
const callArgs = Array.prototype.slice.call(arguments);
// 判断是否用作构造函数(通过 new 调用)
const isConstructorCall = this instanceof boundFunc;
const thisContext = isConstructorCall ? this : context;

return originalFunc.apply(thisContext, bindArgs.concat(callArgs));
};

// 维护原型链
const F = function() {};
if (this.prototype) {
F.prototype = this.prototype;
}
boundFunc.prototype = new F();

return boundFunc;
};

给定一个函数fn,设计一个通用封装(currying函数)作用于这个fn,让这个fn可以支持柯里化,该怎么实现呢?思路:

要让fn(a,b)等价于fn(a)(b),那么fn(a)要返回一个函数,这个函数接受参数b,并返回与fn(a,b)相同的结果。

设计一个currying函数,它接受第一个参数是要被柯里化的函数fn,第2~N个参数是原本要传递给fn的参数(这个可用rest操作符实现)。

柯里化主要围绕“处理参数”思考,不管怎么变化传参形式,柯里化之后的函数要把之前函数的参数都统统处理完毕才算合格。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

let promaryCurrying = function(fn, length){

length = length || fn.length;

return function(...args2){

if(args2.length < length){
var combinedArgs = [fn].concat(args2)

return curry(primaryCurrying.apply(this, coombinedArgs), length - args2.length);
}

else {
return fn.apply(this, args2)
}

};
}


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
// 借用之前的初步柯里化函数,让柯里化后的函数fn有多1段的能力。
var primaryCurrying = function (fn,...args) {
return function (...args2) {
var newArgs = args.concat(args2);
return fn.apply(this, newArgs);
}
}

/**
* 设计真正的柯里化函数。
* @param fn 即将被柯里化的函数。
* @param length 用来记录fn应该处理的剩余参数的长度。
*/
function curry(fn, length) {
length = length || fn.length;

return function (...args2) {
//若原本要传给fn的参数还未传完
if (args2.length < length) {
//合并参数
var combinedArgs = [fn].concat(args2);
//递归,进一步柯里化。这里调用了primaryCurrying函数,每调用一次该函数,
//就可以多“1段”以便可以处理掉剩余的参数,直到把所有应传给fn的参数都处理完。
return curry(primaryCurrying.apply(this, combinedArgs), length - args2.length);
}
//若原本要传给fn的参数都已经传完,则直接执行fn函数
else {
return fn.apply(this, args2);
}
};
}

// 实现一个add方法,使计算结果能够满足如下预期:
add(1)(2)(3) = 6;
add(1, 2, 3)(4) = 10;
add(1)(2)(3)(4)(5) = 15;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

function add(){
var _args = Array.prototype.slice.call(arguments);
var _addr = function(){
_args.push(...arguments)
return _addr;
}

_adder.toString = function(){
return _args.reduce(function (acc, cur){
return acc + cur;
}, 0)
}
return _adder;
}


reflect和proxy

eval就是元编程,只是现在不让用了。

数据绑定和响应式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

function createReactive(obj){
return new Proxy(obj, {
get(target, prop){
console.log('reading'+ prop)
return target[prop]
},
set(target, prop, value){
console.log('setting', value)
target[prop] = value;
return true;
}
});
}

const data = createReactive({ count: 0 });
data.count;
data.count = 1

日志记录

1
2
3
4
5
6
7
8
9
10
11
12
13
function createLoggingProxy(obj){
return new Proxy(obj, {
get(target, prop){
console.log('get', [prop])
return target[prop]
}
set(target, prop, value){
console.log(`setting ${prop} = ${value}`)
return Reflect.set(target, prop, value)
}
});
}

权限控制

1
2
3
4
5
6
7
8
9
10
11
function createSecureProxy(obj, allowedProps){
return new Proxy(obj, {
get(target, prop) {
if(allowedProps.includes(prop)){
return target[prop]
}
throw new Error(`Not allowed to access ${prop}`)
}
})
}

网络

常见面试题

  1. 为什么连接的时候是三次握手,关闭的时候却是四次握手?
    答:因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,”你发的FIN报文我收到了”。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。

  2. 为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?
    答:虽然按道理,四个报文都发送完毕,我们可以直接进入CLOSE状态了,但是我们必须假象网络是不可靠的,有可以最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。在Client发送出最后的ACK回复,但该ACK可能丢失。Server如果没有收到ACK,将不断重复发送FIN片段。所以Client不能立即关闭,它必须确认Server接收到了该ACK。Client会在发送出ACK之后进入到TIME_WAIT状态。Client会设置一个计时器,等待2MSL的时间。如果在该时间内再次收到FIN,那么Client会重发ACK并再次等待2MSL。所谓的2MSL是两倍的MSL(Maximum Segment Lifetime)。MSL指一个片段在网络中最大的存活时间,2MSL就是一个发送和一个回复所需的最大时间。如果直到2MSL,Client都没有再次收到FIN,那么Client推断ACK已经被成功接收,则结束TCP连接。

  3. 为什么不能用两次握手进行连接?
    答:3次握手完成两个重要的功能,既要双方做好发送数据的准备工作(双方都知道彼此已准备好),也要允许双方就初始序列号进行协商,这个序列号在握手过程中被发送和确认。现在把三次握手改成仅需要两次握手,死锁是可能发生的。作为例子,考虑计算机S和C之间的通信,假定C给S发送一个连接请求分组,S收到了这个分组,并发 送了确认应答分组。按照两次握手的协定,S认为连接已经成功地建立了,可以开始发送数据分组。可是,C在S的应答分组在传输中被丢失的情况下,将不知道S 是否已准备好,不知道S建立什么样的序列号,C甚至怀疑S是否收到自己的连接请求分组。在这种情况下,C认为连接还未建立成功,将忽略S发来的任何数据分 组,只等待连接确认应答分组。而S在发出的分组超时后,重复发送同样的分组。这样就形成了死锁。

  4. 如果已经建立了连接,但是客户端突然出现故障了怎么办?
    TCP还设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75秒钟发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。

http

HTTP/2 和 HTTP/1.1 的对比

相关问题

  1. 了解 HTTP/2 吗
  2. HTTP/1.0、HTTP/1.1 和 HTTP/2 的区别

    回答关键点

    队头阻塞,持久连接,二进制分帧层,多路复用,服务端推送

HTTP/1.1 相较 HTTP/1.0 的改进和优化:

  • 持久连接
  • HTTP管道化
  • 分块编码传输
  • 新增Host头处理
  • 更多缓存处理
  • 新增更多状态码
  • 断点续传、并行下载

HTTP/1.1 的缺点:

  • 队头阻塞(Head-of-line blocking)
  • 头部冗余
  • TCP连接数限制

HTTP/2 的优点:

  • 二进制分帧层
  • 多路复用
  • Header压缩
  • 服务端推送

知识点深入

  1. HTTP/1.1
  2. 1 相较 HTTP/1.0 的改进和优化
    HTTP/1.1 相比于 HTTP/1.0 的改进和优化主要包含:持久连接、HTTP管道化请求、分块编码传输、新增host头字段、缓存支持、更多状态码等。

持久连接

在HTTP/1.0时期,每进行一次HTTP通信,都需要经过TCP三次握手建立连接。若一个页面引用了多个资源文件,就会极大地增加服务器负担,拉长请求时间,降低用户体验。

HTTP/1.1中增加了持久连接,可以在一次TCP连接中发送和接收多个HTTP请求/响应。只要浏览器和服务器没有明确断开连接,那么该TCP连接会一直保持下去。

持久连接在 HTTP/1.1 中默认开启(请求头中带有 Connection: keep-alive),若不需要开启持久连接,可以在请求头中加上 Connection: close。

HTTP管道化

HTTP管道化是指将多个HTTP请求同时发送给服务器的技术,但是响应必须按照请求发出的顺序依次返回。
但是由于HTTP管道化仍存在诸多问题:

第一个响应慢仍会阻塞后续响应
服务器为了保证能按序返回需要缓存提前完成的响应而占用更多资源
需要客户端 、代理和服务器都支持管道化
所以目前大部分主流浏览器默认关闭HTTP管线化功能。

分块编码传输

在HTTP/1.1协议里,允许在响应头中指定Transfer-Encoding: chunked标识当前为分块编码传输,可以将内容实体分装成一个个块进行传输。

新增Host头处理

在HTTP/1.0中认为每台服务器都绑定一个唯一的IP地址,因此一台服务器也无法搭建多个Web站点。
在HTTP/1.1中新增了host字段,可以指定请求将要发送到的服务器主机名和端口号。

断点续传、并行下载

在HTTP/1.1中,新增了请求头字段Range和响应头字段Content-Range。

前者是用来告知服务器应该返回文件的哪一部分,后者则是用来表示返回的数据片段在整个文件中的位置,可以借助这两个字段实现断点续传和并行下载。

HTTP/1.1 的缺点

队头阻塞
虽然在 HTTP1.1 中增加了持久连接,能在一次 TCP 连接中发送和接收多个 HTTP 请求/响应,但是在一个管道中同一时刻只能处理一个请求,所以如果上一个请求未完成,后续的请求都会被阻塞。

头部冗余
HTTP 请求每次都会带上请求头,若此时 cookie 也携带大量数据时,就会使得请求头部变得臃肿。
TCP 连接数限制

浏览器对于同一个域名,只允许同时存在若干个 TCP 连接(根据浏览器内核有所差异),若超过浏览器最大连接数限制,后续请求就会被阻塞。

HTTP/2

HTTP/2 的优点

二进制分帧层
在HTTP/1.x中传输数据使用的是纯文本形式的报文,需要不断地读入字节直到遇到分隔符为止。而HTTP/2则是采用二进制编码,将请求和响应数据分割为一个或多个的体积小的帧。

多路复用
HTTP/2允许在单个TCP连接上并行地处理多个请求和响应,真正解决了HTTP/1.1的队头阻塞和TCP连接数限制问题。

Header压缩
使用HPACK算法来压缩头部内容,包体积缩小,在网络上传输变快。

服务端推送
允许服务端主动向浏览器推送额外的资源,不再是完全被动地响应请求。例如客户端请求HTML文件时,服务端可以同时将JS和CSS文件发送给客户端。

HTTP和HTTPS的基本概念

  • HTTP:是互联网上应用最为广泛的一种网络协议,是一个客户端和服务器端请求和应答的标准(TCP),用于从WWW服务器传输超文本到本地浏览器的传输协议,它可以使浏览器更加高效,使网络传输减少。
  • HTTPS:是以安全为目标的HTTP通道,简单讲是HTTP的安全版,即HTTP下加入SSL层,HTTPS的安全基础是SSL,因此加密的详细内容就需要SSL。
    HTTPS协议的主要作用可以分为两种:
  • 一种是建立一个信息安全通道,来保证数据传输的安全
  • 另一种就是确认网站的真实性

xss和csrf

XSS原理:攻击者把恶意脚本注入到受信任页面并被浏览器执行,脚本利用页面的信任上下文(cookies、localStorage、DOM)窃取数据或者劫持会话来获取敏感信息。

常见类型:反射型(参数或者路径直接反射到页面并执行);存储型:(恶意内容存储在服务器,其他用户访问时触发)

  1. xss的预防方法是设置csp
    通过 Content-Security-PolicyHTTP头来开启CSP:
    只允许加载本站资源Content-Security-Policy: default-src ‘self’。
    只允许加载HTTPS协议图片 Content-Security-Policy: img-src https://*。
    允许加载任何来源框架 Content-Security-Policy: child-src ‘none’。
  2. httponly开头的cookie
    Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly。
  3. 过滤decoding map
  • CSRF的防范
    防范CSRF可遵循以下几种规则:
  • Get 请求不对数据进行修改。
  • 不让第三方网站访问到用户Cookie。
  • 阻止第三方网站请求接口。
  • 请求时附带验证信息,如验证码或Token。

十大排序算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
排序算法
├─ O(n²) 简单排序
│ ├─ 冒泡 Bubble
│ ├─ 选择 Selection
│ └─ 插入 Insertion

├─ O(n log n) 高级排序(基于分治)
│ ├─ 归并 Merge —— 非原地 + 稳定
│ └─ 快排 Quick —— 原地 + 不稳定

├─ O(n log n) 不基于分治
│ └─ 堆排序 Heap — 原地 + 不稳定

└─ O(n) 非比较排序(限定数据范围)
├─ 计数排序 Counting
├─ 桶排序 Bucket
└─ 基数排序 Radix

冒泡排序

两两比较,大的往后沉,双重循环,交换!

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 bubbleSort(arr){
for(let i = 0; i< arr.length; i++){
for(let j = 0; j< arr.length - i - 1; j++){
if(arr[j] > arr[j+1]){
const temp = arr[j]
arr[j] = arr[j+1]
arr[j+1] = temp
}
}
}
return arr
}

// 优化的话,加个flag判断有没有做交换,没有就break跳出循环。
function bubbleSort1(arr){
for(let i = 0; i< arr.length; i++){
for(let j = 0; j< arr.length - i - 1; j++){
let flag = false
if(arr[j] > arr[j+1]){
const temp = arr[j]
arr[j] = arr[j+1]
arr[j+1] = temp
flag = true
}
if(!flag) break
}
}
return arr
}

选择排序

每次选最小,放到前面,固定位置,找最小index

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

function selectionSort(arr) {
let length = arr.length;
let indexMin;
for (let i = 0; i < length - 1; i++) {
indexMin = i;
for (let j = i + 1; j < length; j++) {
if (arr[indexMin] > arr[j]) {
indexMin = j;
}
}
if (i !== indexMin) {
let temp = arr[i];
arr[i] = arr[indexMin];
arr[indexMin] = temp;
}
}
return arr;
}

插入排序

前面是有序区,把当前数插进去,适合几乎有序数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14

((arr) => {
for(let i=1; i<arr.length; i++){
let current = arr[i];
let = j = i-1;
while(j>=0 && arr[j] > current){
arr[j+1] = arr[j]
j--
}
arr[j+1] = current
}
return arr
})([1,5,4,2,9,7,8])

*** 快速排序(Top1 高频)

快速排序其实是分治法,将待排序数组里的项和基准数对比,比基准数大的放在一边,小的放另一边,然后再对左右两边的子数组重复使用这个思路,直到整个数组排序完毕。
选基准,比它小的放左边,比它大的放右边。里面比较的是合法下标哦,是合法下标的区间哦!

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
function quickSort(arr, left, right) {
if (left >= right) return;

let i = left;
let j = right;
let pivot = arr[left]; // 基准值

while (i < j) {

// 从右边找比 pivot 小的
while (arr[j] >= pivot && i < j) j--;

// 从左边找比 pivot 大的
while (arr[i] <= pivot && i < j) i++;

// 交换
if (i < j) {
[arr[i], arr[j]] = [arr[j], arr[i]];
}
}

// 此时 i===j,把 pivot 放回中间
arr[left] = arr[i];
arr[i] = pivot;

// 递归左右两边
quickSort(arr, left, i - 1);
quickSort(arr, i + 1, right);

return arr;
}

console.log(quickSort([1,5,4,2,9,7,8], 0, 6));

*** 堆排序(面试经常问“Top K”)

建立大顶堆,堆顶最大,不断弹出再重建。
React Fiber = 一棵被不断“重新排序”的最小堆结构 → 面试官特别爱问堆。
React Fiber 的本体结构是一串 单向链表 + 树:
React Fiber 的调度器(Scheduler)用的是“最小堆(min-heap)”结构;
React Fiber 的任务执行顺序(Fiber 树构建 / Reconciliation)本身是用“可中断的链表(队列)结构”。
所以:
Fiber = 链表结构(队列式遍历)
Scheduler = 用最小堆来决定“下一件事做谁”
这两者完全不冲突,只是层级不一样。

React Fiber 本身不是堆,它是一套链表结构的 Fiber 树,用来支持可中断更新。
但 React 的调度器 Scheduler 为了按优先级选择下一个任务,使用了“最小堆”作为优先级队列。
所以:Fiber = 链表;调度 = 堆。

堆的特性:
必须是完全二叉树;
任一结点的值是其子树所有结点的最大值或最小值。 最大值时,称为”最大堆”,也称大顶堆; 最小值时,称为”最小堆”,也称小顶堆。
堆排序主要用到最大堆/最小堆的删除操作,也即根节点已经是最大/小的值,排序的话,只需要把根结点拿(删)掉,放入有序的新数组里,然后用下沉算法处理剩余的结点以便组成新的最大堆/最小堆……如此循环。
所谓下沉算法,拿最小堆来举例说(最大堆同理),就是把完全二叉树根结点R和该树第二层左右子结点的值比较,如果大,结点就互换位置(”下沉”),以此逐层递归,直到处理完整棵树,此时,根节点值最小。

堆的本质是完全二叉树,而完全二叉树最典型的表示方式就是“连续数组”。
只要数组里没有空洞(每个 index 都能访问到),它天然可以作为堆结构。
只要保证数组连续,堆排序一定能用。

1
2
3
4
5
6
function isContinuousArray(arr) {
for (let i = 0; i < arr.length; i++) {
if (!(i in arr)) return false; // 有洞
}
return true;
}
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
55
56

((arr) => {
let result = []
buildMinHeap(arr)
for(let i = 0, length = arr.length; i< length; i++){
// 将堆(完全二叉树)的根结点拿走,放入result数组,此时result是已排序好的。
result.push(arr[0])
// 将堆(完全二叉树)的根结点和最末尾的结点交换
swap(0, length-result.length)
// 然后下沉,重排树,让根节点是最小的。注意数组范围是length-result.length
sink(arr, 0 , length-result.length)
}

// 根据给定的数组建一个最小堆
function buildMinHeap(arr){
let length = arr.length;
let currentIndex; //当前要处理的下沉结点对应的数组索引
// 请注意,currentIndex为什么是从 Math.floor((length - 2) / 2) 开始?
// 读者可以画个草稿图归纳一下。
// 这会让算法的循环次数由N次变为logN,这正是堆排序更高效的关键所在。
for(currentIndex = Math.floor((length - 2) / 2); currentIndex >= 0; currentIndex--){
console.log('正在处理的下沉结点索引' + currentIndex)
sink(arr, currentIndex, length)
}

}

function sink(arr, currentIndex, length){
let minIndex = currentIndex
let leftChildIndex = 2*currentIndex + 1 // 完全二叉树 左子结点索引
let rightChildIndex = 2*currentIndex + 2 // 完全二叉树 右子结点索引
// 左侧下沉
if(leftChildIndex < length && arr[leftChildIndex] < arr[minIndex]){
minIndex = leftChildIndex
}

// 右侧下沉
if(rightChildIndex < length && arr[rightChildIndex] < arr[minIndex]){
minIndex = rightChildIndex
}

if(minIndex !== currentIndex){
swap(minIndex, currentIndex)
sink(arr, minIndex, length)
}
}

function swap(x, y){
let temp = arr[x]
arr[x] = arr[y]
arr[y] = temp
}

return result
})([1,5,4,2,9,7,8])

计数排序(不太会)

统计次数,再按次数依次输出,只适合整数小范围。

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
((arr) => {
function countingSort(arr) {
const n = arr.length;

// 找到数组中的最大值和最小值
let max = arr[0];
let min = arr[0];
for (let i = 1; i < n; i++) {
if (arr[i] > max) max = arr[i];
if (arr[i] < min) min = arr[i];
}

const range = max - min + 1;
const count = new Array(range).fill(0);
const output = new Array(n);

// 统计每个元素出现的次数
for (let i = 0; i < n; i++) {
count[arr[i] - min]++;
}

// 计算每个元素应该放置的位置
for (let i = 1; i < range; i++) {
count[i] += count[i - 1];
}

// 从后往前遍历,保证稳定性
for (let i = n - 1; i >= 0; i--) {
output[count[arr[i] - min] - 1] = arr[i];
count[arr[i] - min]--;
}

// 将结果复制回原数组
for (let i = 0; i < n; i++) {
arr[i] = output[i];
}

return arr;
}

const sortedArr = countingSort(arr);
console.log(sortedArr);
})([1, 5, 4, 2, 9, 7, 8]);//>> [1, 2, 4, 5, 7, 8, 9]

桶排序

分桶 → 各桶排序 → 合并
适合均匀分布的数据。

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
((arr) => {
function bucketSort(arr, bucketSize = 5) {
if (arr.length === 0) return arr;

// 找到数组中的最大值和最小值
let min = arr[0];
let max = arr[0];
for (let i = 1; i < arr.length; i++) {
if (arr[i] < min) min = arr[i];
if (arr[i] > max) max = arr[i];
}

// 创建桶
const bucketCount = Math.floor((max - min) / bucketSize) + 1;
const buckets = new Array(bucketCount);
for (let i = 0; i < bucketCount; i++) {
buckets[i] = [];
}

// 将数据分配到桶中
for (let i = 0; i < arr.length; i++) {
const bucketIndex = Math.floor((arr[i] - min) / bucketSize);
buckets[bucketIndex].push(arr[i]);
}

// 对每个桶中的数据进行排序
for (let i = 0; i < buckets.length; i++) {
buckets[i].sort((a, b) => a - b);
}

// 将桶中的数据按顺序合并
let index = 0;
for (let i = 0; i < buckets.length; i++) {
for (let j = 0; j < buckets[i].length; j++) {
arr[index++] = buckets[i][j];
}
}

return arr;
}

const sortedArr = bucketSort(arr);
console.log(sortedArr);
})([1, 5, 4, 2, 9, 7, 8]);//>> [1, 2, 4, 5, 7, 8, 9]

基数排序

按个位、十位、百位依次排
用桶 + 多次扫描。

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
((arr) => {
function radixSort(arr) {
// 找到数组中的最大值
let max = arr[0];
for (let i = 1; i < arr.length; i++) {
if (arr[i] > max) max = arr[i];
}

// 计算最大值的位数
let maxDigit = 1;
while (Math.floor(max / 10) > 0) {
maxDigit++;
max = Math.floor(max / 10);
}

// 对每一位进行排序
for (let digit = 0; digit < maxDigit; digit++) {
// 创建10个桶(0-9)
const buckets = Array.from({length: 10}, () => []);

// 将数据分配到桶中
for (let i = 0; i < arr.length; i++) {
const bucketIndex = Math.floor(arr[i] / Math.pow(10, digit)) % 10;
buckets[bucketIndex].push(arr[i]);
}

// 将桶中的数据按顺序合并
let index = 0;
for (let i = 0; i < 10; i++) {
for (let j = 0; j < buckets[i].length; j++) {
arr[index++] = buckets[i][j];
}
}
}

return arr;
}

const sortedArr = radixSort(arr);
console.log(sortedArr);
})([1, 5, 4, 2, 9, 7, 8]);//>> [1, 2, 4, 5, 7, 8, 9]

*** 归并排序(分治 + 面试能问很深)

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

function mergeSort(arr){
let array = mergeSortRec(arr)
return array
}

function mergeSortRec(arr){
let length = arr.length;
if(length === 1){
return arr;
}
let mid = Math.floor(length/2),
left = arr.slice(0, mid),
right = arr.slice(mid, length)
return merge(mergeSortRec(left), mergeSortRec(right))
}

function merge(left, right){
let result = [], ileft = 0, iright=0
while(ileft<left.length && iright<right.length){
if(left[ileft] < right[iright]){
result.push(left[ileft++])
} else {
result.push(right[iright++])
}
}

while(ileft<left.length){
result.push(left[ileft++])
}

while(iright<right.length){
result.push(right[iright++])
}

return result
}

希尔排序

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
((arr) => {
function shellSort(arr) {
const n = arr.length;

// 初始间隔设为数组长度的一半
for (let gap = Math.floor(n / 2); gap > 0; gap = Math.floor(gap / 2)) {
// 对每个子序列进行插入排序
for (let i = gap; i < n; i++) {
let temp = arr[i];
let j;

// 在子序列中寻找插入位置
for (j = i; j >= gap && arr[j - gap] > temp; j -= gap) {
arr[j] = arr[j - gap];
}

arr[j] = temp;
}
}

return arr;
}

const sortedArr = shellSort(arr);
console.log(sortedArr);
})([1, 5, 4, 2, 9, 7, 8]);//>> [1, 2, 4, 5, 7, 8, 9]

后面单调栈的代码实现有点问题,粗看了一下后面几个题,先不按照这里的算法题复习了哦!转去瓶子算法那,感觉更好懂。

三大框架和webpack部分跳过,写的不好。

发布订阅和观察者模式

观察者模式:观察者(Observer)直接订阅(Subscribe)主题(Subject),而当主题被激活的时候,会触发(Fire Event)观察者里的事件。
发布订阅模式:订阅者(Subscriber)把自己想订阅的事件注册(Subscribe)到调度中心(Event Channel),当发布者(Publisher)发布该事件(Publish Event)到调度中心,也就是该事件触发时,由调度中心统一调度(Fire Event)订阅者注册到调度中心的处理代码。

差异:
在观察者模式中,观察者是知道 Subject 的,Subject 一直保持对观察者进行记录。然而,在发布订阅模式中,发布者和订阅者不知道对方的存在。它们只有通过消息代理进行通信。
在发布订阅模式中,组件是松散耦合的,正好和观察者模式相反。
观察者模式大多数时候是同步的,比如当事件触发,Subject 就会去调用观察者的方法。而发布-订阅模式大多数时候是异步的(使用消息队列)。
观察者模式需要在单个应用程序地址空间中实现,而发布-订阅更像交叉应用模式。