数组扁平化

ES6的flat方法

flat() 方法将以指定的深度递归遍历数组,并将所有元素与遍历的子数组中的元素合并到一个新数组中以返回。

1
2
3
4
const arr = [1,[2,[3,[4,5]]],6]
const res = JSON.stringify(arr).replace(/\[|\]/g,'')
const res2 = JSON.parse('[' + res + ']')
console.log(res2)

使用正则

首先是使用 JSON.stringify 把 arr 转为字符串接着使用正则把字符串里面的 [ 和 ] 去掉然后再拼接数组括号转为数组对象。

1
2
3
4
const arr = [1,[2,[3,[4,5]]],6]
const res = JSON.stringify(arr).replace(/\[|\]/g,'')
const res2 = JSON.parse('[' + res + ']')
console.log(res2)

递归

1
2
3
4
5
6
7
8
9
10
11
12
13
const array = []
const fn = (arr)=>{
for(let i = 0;i<arr.length; i++){
if(Array.isArray(arr[i])){
fn(arr[i])
}
else {
array.push(arr[i])
}
}
}
fn(arr)
console.log(array)

reduce

reduce 方法: 可以用来给数组求和

concat() 方法用于连接两个或多个数组。

concat() 方法不会更改现有数组,而是返回一个新数组,其中包含已连接数组的值。

1
2
3
4
5
6
const newArr = (arr)=>{
return arr.reduce((pre,cur)=>{
return pre.concat(Array.isArray(cur) ? newArr(cur) : cur)
},[])
}
console.log(newArr(arr),"reduce方法")

使用栈的思想实现 flat 函数

栈(stack)又名堆,它是一种运算受限的线性表。限定仅在表尾进行插入和删除操作的线性表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 栈思想function flat(arr) {
const newArr = [];
const stack = [].concat(arr); // 将数组元素拷贝至栈,直接赋值会改变原数组//如果栈不为空,则循环遍历while (stack.length !== 0) {
const val = stack.pop(); // 删除数组最后一个元素,并获取它if (Array.isArray(val)) {
stack.push(...val); // 如果是数组再次入栈,并且展开了一层
} else {
newArr.unshift(val); // 如果不是数组就将其取出来放入结果数组中
}
}
return newArr;
}

let arr = [12, 23, [34, 56, [78, 90, 100, [110, 120, 130, 140]]]];
console.log(flat(arr));
// [12, 23, 34, 56, 78, 90, 100, 110, 120, 130, 140]

深拷贝和浅拷贝

知识点讲解

1、什么是数据类型?

数据分为基本数据类型 (String, Number, Boolean, Null, Undefined,Symbol) 和引用数据类型。

基本数据类型的特点:直接存储在栈 (stack) 中的数据

引用数据类型的特点:存储的是该对象在栈中引用,真实的数据存放在堆内存里

引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。

2、ES11新增了什么数据类型?

bigint!

bigint使用方式:

1
2
3
4
5
let max=Number.MAX_SAFE_INTEGER
console.log(BigInt(max))//9007199254740991n
console.log(BigInt(max)+BigInt(1))//9007199254740992n 正确
console.log(BigInt(max)+BigInt(2))//9007199254740993n 正确
console.log(BigInt(max)+BigInt(4))//9007199254740995n 正确

3、为什么要使用深拷贝?

我们希望在改变新的数组(对象)的时候,不改变原数组(对象)

4、赋值和浅拷贝的区别?

当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。

JavaScript 数组复制操作创建浅拷贝。(所有 JavaScript 对象的标准内置复制操作都会创建浅拷贝,而不是深拷贝)。
浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。

浅拷贝

浅拷贝是一点一点地拷贝一个对象。它将创建一个新对象。此对象具有原始对象属性值的精确副本。如果属性是基本类型,它将复制基本类型的值;如果属性是内存地址(引用类型),则复制内存地址。因此,如果一个对象更改地址,另一个对象将受到影响。也就是说,默认复制构造函数只复制浅层复制中的对象(依次复制成员),即只复制对象空间,而不复制资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var obj1 ={
name:'张三',
age:8,
pal:['王五','王六','王七']
}
var obj3 = shallowCopy(obj1)
function shallowCopy (src){
var newObj = {};
for(var prop in src ){
console.log(prop)
if(src.hasOwnProperty(prop)){
newObj[prop] = src[prop]
}
}
return newObj
}
obj3.name = '李四'
obj3.pal[0] = '王麻子'

console.log("obj1", obj1); //{age: 8, name: "张三", pal: ['王麻子', '王六', '王七']}
console.log("obj3", obj3); //{age: 8, name: "李四", pal: ['王麻子', '王六', '王七']}


深拷贝

方法一:递归实现深拷贝

实现深拷贝原理的递归方法:遍历对象、数组甚至内部都是基本的数据类型,然后复制它们,即深度复制。

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
var obj = {   //原数据,包含字符串、对象、函数、数组等不同的类型
name:"test",
main:{
a:1,
b:2
},
fn:function(){

},
friends:[1,2,3,[22,33]]
}

function copy(obj){
let newobj = null; //声明一个变量用来储存拷贝之后的内容

//判断数据类型是否是复杂类型,如果是则调用自己,再次循环,如果不是,直接赋值即可,
//由于null不可以循环但类型又是object,所以这个需要对null进行判断
if(typeof(obj) == 'object' && obj !== null){

//声明一个变量用以储存拷贝出来的值,根据参数的具体数据类型声明不同的类型来储存
newobj = obj instanceof Array? [] : {};

//循环obj 中的每一项,如果里面还有复杂数据类型,则直接利用递归再次调用copy函数
for(var i in obj){
newobj[i] = copy(obj[i])
}
}else{
newobj = obj
}
console.log('77',newobj)
return newobj; //函数必须有返回值,否则结构为undefined
}

var obj2 = copy(obj)
obj2.name = '修改成功'
obj2.main.a = 100
console.log(obj)
console.log(obj2)

方法二:递归实现深拷贝

  • 递归处理 引用类型

  • 保持数组类型

1
2
3
4
5
6
7
8
9
10
11
function deepCopy(target) {
if (typeof target === 'object') {
const newTarget = Array.isArray(target) ? [] : Object.create(null)
for (const key in target) {
newTarget[key] = deepCopy(target[key])
}
return newTarget
} else {
return target
}
}

方法三:递归实现深拷贝

  • 哈希表 Map 支持 循环引用

    • Map 支持引用类型数据作为键
1
2
3
4
5
6
7
8
9
10
11
12
13
function deepCopy(target, h = new Map) {
if (typeof target === 'object') {
if (h.has(target)) return h.get(target)
const newTarget = Array.isArray(target) ? [] : Object.create(null)
for (const key in target) {
newTarget[key] = deepCopy(target[key], h)
}
h.set(target, newTarget)
return newTarget
} else {
return target
}
}

方法四:递归实现深拷贝

哈希表 WeakMap 代替 Map

WeakMap 的键是弱引用,告诉 JS 垃圾回收机制,当键回收时,对应 WeakMap 也可以回收,更适合大量数据深拷

链接:https://leetcode.cn/leetbook/read/interview-coding-frontend/dq08bm/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function deepCopy(target, h = new WeakMap) {
if (typeof target === 'object') {
if (h.has(target)) return h.get(target)
const newTarget = Array.isArray(target) ? [] : Object.create(null)
for (const key in target) {
newTarget[key] = deepCopy(target[key], h)
}
h.set(target, newTarget)
return newTarget
} else {
return target
}
}




防抖节流

防抖

防抖是指短时间内大量触发同一事件,只会在最后一次事件完成后延迟执行一次函数。例如,在输入用户名的过程中,需要反复验证用户名。此时,您应该等待用户停止输入,然后进行验证,否则将影响用户体验。 防抖实现的原理是在触发事件后设置计时器。在计时器延迟过程中,如果事件再次触发,则重置计时器。在没有触发事件之前,计时器将再次触发并执行相应的功能。

声明定时器

返回函数

一定时间间隔,执行回调函数

回调函数

已执行:清空定时器
未执行:重置定时器

1
2
3
4
5
6
7
8
9
10
function debounce(fn, delay) {
let timer = null
return function (...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
timer = null
fn.apply(this, args)
}, (delay + '') | 0 || 1000 / 60)
}
}

节流

节流是指每隔一段时间就执行一次函数。就像未拧紧的水龙头一样,水龙头每隔一段时间就会滴水。即使在这段时间管道里有更多的水,水龙头也不会掉更多的水。

节流的原理是在触发事件后设置计时器。在计时器延迟过程中,即使事件再次触发,计时器的延迟时间也不会改变。在计时器执行功能之前,计时器不会复位。

声明定时器

返回函数

一定时间间隔,执行回调函数

回调函数

已执行:清空定时器
未执行:返回

1
2
3
4
5
6
7
8
9
10
11
function throttle(fn, interval) {
let timer = null
return function (...args) {
if (timer) return
timer = setTimeout(() => {
timer = null
fn.apply(this, args)
}, (interval +'')| 0 || 1000 / 60)
}
}

第四章:for..in 和 for..of 用法

for..in

for…in 循环只遍历可枚举属性(包括它的原型链上的可枚举属性)。像 Array 和 Object 使用内置构造函数所创建的对象都会继承自 Object.prototype 和 String.prototype 的不可枚举属性,例如 String 的 indexOf() 方法或 Object 的 toString() 方法。循环将遍历对象本身的所有可枚举属性,以及对象从其构造函数原型中继承的属性(更接近原型链中对象的属性覆盖原型属性)。

for of

Q: ES6 的 for of 可以遍历对象吗?

A: ES6 的 “for of” 不能遍历对象。

原因:ES6 中引入了 Iterator 接口,只有提供了 Iterator 接口的数据类型才可以使用 “for-of” 来循环遍历;而普通对象默认没有提供 Iterator 接口,因此无法用 “for-of” 来进行遍历。

第五章 数组

从数组 [1,2,3,4,5,6] 中找出值为 2 的元素

本章讲实现数组中找值问题的 2 种方法。

方法一:filter()

filter() 方法创建一个新数组, 包含通过所提供函数实现的测试的所有元素。

1
2
3
4
function isBigEnough(element) {
return element == 2;
}
var filtered = [1,2,3,4,5,6].filter(isBigEnough);

方法二:find()

find() 方法返回数组中满足提供的测试函数的第一个元素的值。否则返回 undefined。

1
2
3
4
5
var arr = [1, 2, 3, 4, 5, 6];
function finds(x) {
return x === '2';
}
console.log(arr.find(finds));

常用方法总结:

方法名 参数 描述 返回值
includes
searchElement,fromlndex
判断数组中是否包含指定的值
布尔值
indexOf
searchElement,fromlndex
查找元素在数组中首次出现的索引值
索引值,或者-1
lastIndexOf
searchElement,fromlndex
查找元素在数组中最后一次出现的索引值
索引值,或者-1
some
callback[,thisArg]
判断数组中是否有符合条件的元素
布尔值
every
callback[,thisArg]
判断数组中是否每个元素都符合条件
布尔值
filter
callback[,thisArg]
返回符合条件的所有元素组成的数组
数组
find
callback[,thisArg]
返回数组中符合条件的第一个元素
数组中的元素,或者undefined
findIndex
callback[,thisArg]
返回符合条件的第一个元素的索引
索引值,或者-1

不使用filter如何实现筛选元素?

在 JavaScript 中,如果你想筛选数据但不想使用内置的 filter() 函数,你同样可以使用多种方法来实现。以下是几种常见的替代方案:

  1. 使用 for 循环:
    你可以通过基本的 for 循环来遍历数组,并在循环内部使用条件语句来决定是否将元素添加到新数组中。

    1
    2
    3
    4
    5
    6
    7
    8
    let numbers = [1, 2, 3, 4, 5, 6];
    let evenNumbers = [];
    for (let i = 0; i < numbers.length; i++) {
    if (numbers[i] % 2 === 0) {
    evenNumbers.push(numbers[i]);
    }
    }
    console.log(evenNumbers); // 输出: [2, 4, 6]
  2. 使用 forEach() 方法:
    forEach() 方法允许你对数组的每个元素执行一次提供的函数。这不是一个返回新数组的方法,但你可以在内部构建一个新数组。

    1
    2
    3
    4
    5
    6
    7
    8
    let numbers = [1, 2, 3, 4, 5, 6];
    let evenNumbers = [];
    numbers.forEach(function(number) {
    if (number % 2 === 0) {
    evenNumbers.push(number);
    }
    });
    console.log(evenNumbers); // 输出: [2, 4, 6]
  3. 使用 for…of 循环:
    for...of 循环提供了一种简洁的方法来迭代数组(和其他可迭代对象)的值。

    1
    2
    3
    4
    5
    6
    7
    8
    let numbers = [1, 2, 3, 4, 5, 6];
    let evenNumbers = [];
    for (let number of numbers) {
    if (number % 2 === 0) {
    evenNumbers.push(number);
    }
    }
    console.log(evenNumbers); // 输出: [2, 4, 6]
  4. 使用 reduce() 方法:
    虽然 reduce() 方法通常用于从数组中派生一个单一值(如总和),但它也可以用来构建新数组,通过累加符合条件的元素。

    1
    2
    3
    4
    5
    6
    7
    8
    let numbers = [1, 2, 3, 4, 5, 6];
    let evenNumbers = numbers.reduce((accumulator, current) => {
    if (current % 2 === 0) {
    accumulator.push(current);
    }
    return accumulator;
    }, []);
    console.log(evenNumbers); // 输出: [2, 4, 6]

这些方法提供了灵活性来选择适合不同情景的数据筛选方式,就像在 Python 中一样,你可以根据实际的应用需求和性能考量来选择最合适的方法。

第六章:两栏布局和三栏布局

两栏布局

两栏布局非常常见,往往是以一个定宽栏和一个自适应的栏并排展示存在。

image-20230128103818724

方法一:浮动布局

  • 使用 float 左浮左边栏
  • 右边模块使用 margin-left 撑出内容块做内容展示
  • 为父级元素添加 BFC,防止下方元素飞到上方内容
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
<style>
.box{
overflow: hidden; 添加BFC
}
.left {
float: left;
width: 200px;
background-color: gray;
height: 400px;
}
.right {
margin-left: 210px;
background-color: lightgray;
height: 200px;
}
</style>
<div class="box">
<div class="left">左边</div>
<div class="right">右边</div>
</div>


链接:https://leetcode.cn/leetbook/read/interview-coding-frontend/dq4vvd/


方法二:flex 弹性布局

flex 容器的一个默认属性值: align-items: stretch;

这个属性导致了列等高的效果。 为了让两个盒子高度自动,需要设置: align-items: flex-start。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<style>
.box{
display: flex;
}
.left {
width: 100px;
}
.right {
flex: 1;
}
</style>
<div class="box">
<div class="left">左边</div>
<div class="right">右边</div>
</div>

三栏布局

image-20230128104025419

实现三栏布局中间自适应的布局方式有:

  1. 两边使用 float,中间使用 margin
  2. 两边使用 absolute,中间使用 margin
  3. 两边使用 float 和负 margin
  4. flex 实现
  5. grid 网格布局

方法一:两边使用 float,中间使用 margin

需要将中间的内容放在 html 结构最后,否则右侧会臣在中间内容的下方

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
<style>
.wrap {
background: #eee;
overflow: hidden; <!-- 生成BFC,计算高度时考虑浮动的元素 -->
padding: 20px;
height: 200px;
}
.left {
width: 200px;
height: 200px;
float: left;
background: coral;
}
.right {
width: 120px;
height: 200px;
float: right;
background: lightblue;
}
.middle {
margin-left: 220px;
height: 200px;
background: lightpink;
margin-right: 140px;
}
</style>
<div class="wrap">
<div class="left">左侧</div>
<div class="right">右侧</div>
<div class="middle">中间</div>
</div>

方法二:两边使用 absolute,中间使用 margin

基于绝对定位的三栏布局:注意绝对定位的元素脱离文档流,相对于最近的已经定位的祖先元素进行定位。无需考虑 HTML 中结构的顺序

左右两边使用绝对定位,固定在两侧。

中间占满一行,但通过 margin 和左右两边留出 10px 的间隔

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
<style>
.container {
position: relative;
}

.left,
.right,
.main {
height: 200px;
line-height: 200px;
text-align: center;
}

.left {
position: absolute;
top: 0;
left: 0;
width: 100px;
background: green;
}

.right {
position: absolute;
top: 0;
right: 0;
width: 100px;
background: green;
}

.main {
margin: 0 110px;
background: black;
color: white;
}
</style>

<div class="container">
<div class="left">左边固定宽度</div>
<div class="right">右边固定宽度</div>
<div class="main">中间自适应</div>
</div>

方法三:两边使用 float 和负 margin

实现过程:

中间使用了双层标签,外层是浮动的,以便左中右能在同一行展示

左边通过使用负 margin-left:-100%,相当于中间的宽度,所以向上偏移到左侧

右边通过使用负 margin-left:-100px,相当于自身宽度,所以向上偏移到最右侧

缺点:

增加了 .main-wrapper 一层,结构变复杂

使用负 margin,调试也相对麻烦

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
<style>
.left,
.right,
.main {
height: 200px;
line-height: 200px;
text-align: center;
}

.main-wrapper {
float: left;
width: 100%;
}

.main {
margin: 0 110px;
background: black;
color: white;
}

.left,
.right {
float: left;
width: 100px;
margin-left: -100%;
background: green;
}

.right {
margin-left: -100px; /* 同自身宽度 */
}
</style>

<div class="main-wrapper">
<div class="main">中间自适应</div>
</div>
<div class="left">左边固定宽度</div>
<div class="right">右边固定宽度</div>


链接:https://leetcode.cn/leetbook/read/interview-coding-frontend/d6h6x2/


方法四:使用 flex 实现

利用 flex 弹性布局,可以简单实现中间自适应。

实现过程:

仅需将容器设置为 display:flex; ,

盒内元素两端对其,将中间元素设置为 100% 宽度,或者设为 flex:1 ,即可填充空白

盒内元素的高度撑开容器的高度

优点:

结构简单直观

可以结合 flex 的其他功能实现更多效果,例如使用 order 属性调整显示顺序,让主体内容优先加载,但展示在中间

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
<style type="text/css">
.wrap {
display: flex;
justify-content: space-between;
}

.left,
.right,
.middle {
height: 100px;
}

.left {
width: 200px;
background: coral;
}

.right {
width: 120px;
background: lightblue;
}

.middle {
background: #555;
width: 100%;
margin: 0 20px;
}
</style>
<div class="wrap">
<div class="left">左侧</div>
<div class="middle">中间</div>
<div class="right">右侧</div>
</div>

方法五:grid 网格布局

flex 弹性布局一样的简单

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
<style>
.wrap {
display: grid;
width: 100%;
grid-template-columns: 300px auto 300px;
}

.left,
.right,
.middle {
height: 100px;
}

.left {
background: coral;
}

.right {
background: lightblue;
}

.middle {
background: #555;
}
</style>
<div class="wrap">
<div class="left">左侧</div>
<div class="middle">中间</div>
<div class="right">右侧</div>
</div>

第七章:实现多行文本溢出的省略

本章讲实现多行文本溢出的省略问题的 2 种方法。

方法一:使用定位伪元素遮盖末尾文字

给父元素设置:

1
2
3
4
5
overflow: hidden;/* 溢出隐藏 */
line-height: 20px;/* 设置行高与容器高度成倍数关系,这样避免文本溢出时,文字显示一半 */
text-align: justify;/* 设置文本对齐方式为两端对齐,这样在伪元素内容遮盖末尾文字时才能对齐*/
position: relative;/* 子绝父相,这里是为了给伪元素设置定位*/

给父元素设置伪元素 ::after ,并为其设置属性:

1
2
3
4
5
6
7
8
9
10
     content: "...";/* 省略号是放在文本最后面的 */
width: 1em;/* 设置伪元素的宽度为1em,是为了遮盖的时候正好遮盖中原来的一个字的大小*/
background-color: pink;/* 设置与父元素相同的背景颜色,同理,也是为了和原来的内容样式保持一致*/
position: absolute;/*设置定位,其位置就是文本的右下角 */
right: 0;bottom: 0;


链接:https://leetcode.cn/leetbook/read/interview-coding-frontend/dg4wsi/


方法二: 利用旧版弹性盒

步骤:

给容器元素类型转换为 display:-webkit-box ;

设置弹性盒子垂直排列 -webkit-box-orient:vertical ;

控制要显示的行数 -webkit-line-clamp:数值 ;

溢出隐藏 overflow:hidden ;

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
/* 多行文本溢出隐藏显示省略号方法二:
兼容性写法,因使用了WebKit的CSS扩展属性,该方法适用于WebKit浏览器及移动端
*/
.box{
width: 200px;
/* height: 100px; 如果设置了高度,且高度超过文本显示行数,在第三行会正常出现省略号,但是三行之后的仍然正常显示*/
background-color: pink;
/* 设置高度是行高的倍数,防止文本露出一半 */
line-height: 20px;
/* 旧版弹性盒 */
display: -webkit-box;
/* 弹性盒子元素垂直排列 */
-webkit-box-orient: vertical;
/* 控制要显示的行数 */
-webkit-line-clamp: 3;
overflow: hidden;
}

</style>
</head>
<body>
<div class="box">
我是文本我是文本我是文本我是文我是文我是文我是文我是文我是文我是文我是是文本我是文本我是文本我是文本我是文本我是文本我是文本我是文本我是文本
</div>
</body>
</html>

第八章:用 CSS 实现三角符号

本章讲用 CSS 实现三角符号问题的 8 种类型。

  1. 类型一:上三角
  2. 类型二:下三角
  3. 类型三:左三角
  4. 类型四:右三角
  5. 类型五:左下三角
  6. 类型六:右下三角
  7. 类型七:右上三角
  8. 类型八:左上三角

第九章:实现九宫格布局

本章讲实现九宫格布局的 4 种方法。

方法一:flex方法二:float方法三:grid[方法四:table

第十章:单行多行文字隐藏显示省略号

本章讲解实现单行多行文字隐藏显示省略号的不同方法。

单行文字实现

文字单行隐藏:给它设定一个宽和高,对于文字超出部分进行隐藏,多余的部分用省略号来表示。

1
2
3
text-overflow: ellipsis;
overflow: hidden ;
white-space: nowrap;

多行文字实现

多行隐藏:多行隐藏的 div 的高度不能设置,让其自动 3 行隐藏即可。

1
2
3
4
overflow : hidden;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
display: -webkit-box;

第十一章 函数柯里化

本章讲解实现函数柯里化的不同方法。

什么是函数柯里化

函数柯里化是指将使用多个参数的函数转化成一系列使用一个参数的函数的技术, 它返回一个新的函数, 这个新函数去处理剩余的参数

1
2
3
4
5
6
// ex:
function add(a, b) {
return a + b;
}
add(1, 2) // 3
// 柯里化const addCurry = curry(add);addCurry(1)(2) // 3

函数柯里化的实现

实现思路: 通过函数的 length 属性获取函数的形参个数, 形参的个数就是所需参数的个数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function curry(fn) {
return _curry.call(this, fn, fn.length);
}
function _curry(fn, len, ...args) {
return function(...params) {
const _args = args.concat(params);
if (_args.length >= len) {
return fn.apply(this, _args);
} else {
return _curry.call(this, fn, len, ..._args);
}
}
}
// test
function add (a, b, c, d) {
return a + b + c + d;
}
const addCurry = curry(add);
console.log(addCurry(1)(2)(3)(4)) // 10
console.log(addCurry(1, 2, 3)(4)) // 10

函数柯里化的作用

1、参数复用: 本质上来说就是降低通用性, 提高适用性

假如一个函数需要两个参数, 其中一个参数可能多次调用并不会发生更改, 比如商品打折的力度, 此时可以根据折扣进行封装

2、提前返回

经典实例: 元素绑定事件监听器, 区分 IE 浏览器的 attachEvent 方法

3、延迟计算: 柯里化函数不会立即执行计算,第一次只是返回一个函数,后面的调用才会进行计算

经典面试题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 实现一个add方法,使计算结果能够满足如下预期:
console.log(add(1)(2)(3)) // 6;
console.log(add(1, 2, 3)(4)) // 10;
console.log(add(1)(2)(3)(4)(5)) // 15;

function add() {
let args = [...arguments];
function _add() {
args = args.concat([...arguments]);
return _add;
}
_add.toString = function() {
return args.reduce((pre, cur) => {
return pre + cur;
})
}
return _add;
}

第十二章 图片懒加载

本章讲实现图片懒加载问题的 3 种方法。

方法一:滚动监听 + scrollTop + offsetTop + innerHeight

  • scrollTop:指网页元素被滚动条卷去的部分。

  • offsetTop:元素相对父元素的位置

  • innerHeight:当前浏览器窗口的大小。需要注意兼容性问题。

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
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
img {
background: url('./img/loading.gif') no-repeat center;
width: 250px;
height: 250px;
display: block;
}
</style>
</head>

<body>
<img src="./img/pixel.gif" data-url="./img/1.jpeg">
<img src="./img/pixel.gif" data-url="./img/2.jfif">
<img src="./img/pixel.gif" data-url="./img/3.jfif">
<img src="./img/pixel.gif" data-url="./img/4.jfif">
<img src="./img/pixel.gif" data-url="./img/5.jfif">
<img src="./img/pixel.gif" data-url="./img/6.webp">

<script>

let imgs = document.getElementsByTagName('img')
// 1. 一上来立即执行一次
fn()
// 2. 监听滚动事件
window.onscroll = lazyload(fn, true)
function fn() {
// 获取视口高度和内容的偏移量
let clietH = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;
var scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop;
console.log(clietH, scrollTop);
for (let i = 0; i < imgs.length; i++) {
let x = scrollTop + clietH - imgs[i].offsetTop //当内容的偏移量+视口高度>图片距离内容顶部的偏移量时,说明图片在视口内
if (x > 0) {
imgs[i].src = imgs[i].getAttribute('data-url'); //从dataurl中取出真实的图片地址赋值给url
}
}
}
// 函数节流
function lazyload(fn, immediate) {
let timer = null
return function () {
let context = this;
if (!timer) {
timer = setTimeout(() => {
fn.apply(this)
timer = null
}, 200)
}
}
}


</script>
</body>

</html>


链接:https://leetcode.cn/leetbook/read/interview-coding-frontend/dzv8fv/


方法二:滚动监听 + getBoundingClientRect()

  • rectObject.top:元素上边到视窗上边的距离;

  • rectObject.right:元素右边到视窗左边的距离;

  • rectObject.bottom:元素下边到视窗上边的距离;

  • rectObject.left:元素左边到视窗左边的距离;

  • rectObject.width:元素自身的宽度;

  • rectObject.height:元素自身的高度;

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
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
* {
margin: 0;
padding: 0;
}

img {
background: url('./img/loading.gif') no-repeat center;
width: 250px;
height: 250px;
display: block;
}
</style>
</head>

<body>
<img src="./img/pixel.gif" data-url="./img/1.jpeg">
<img src="./img/pixel.gif" data-url="./img/2.jfif">
<img src="./img/pixel.gif" data-url="./img/3.jfif">
<img src="./img/pixel.gif" data-url="./img/4.jfif">
<img src="./img/pixel.gif" data-url="./img/5.jfif">
<img src="./img/pixel.gif" data-url="./img/6.webp">

<script>

let imgs = document.getElementsByTagName('img')
// 1. 一上来立即执行一次
fn()
// 2. 监听滚动事件
window.onscroll = lazyload(fn, true)
function fn() {
// 获取视口高度和内容的偏移量
let offsetHeight = window.innerHeight || document.documentElement.clientHeight
Array.from(imgs).forEach((item, index) => {
let oBounding = item.getBoundingClientRect() //返回一个矩形对象,包含上下左右的偏移值
console.log(index, oBounding.top, offsetHeight);
if (0 <= oBounding.top && oBounding.top <= offsetHeight) {
item.setAttribute('src', item.getAttribute('data-url'))
}
})
}
// 函数节流
function lazyload(fn, immediate) {
let timer = null
return function () {
let context = this;
if (!timer) {
timer = setTimeout(() => {
fn.apply(this)
timer = null
}, 200)
}
}
}


</script>
</body>

</html>


链接:https://leetcode.cn/leetbook/read/interview-coding-frontend/dzwibj/


方法三: intersectionObserve()

  • IntersectionObserver : 浏览器原生提供的构造函数,接收两个参数

  • IntersectionObserver API 是异步的,不会与目标元素的滚动同步触发

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
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
img {
background: url('./img/loading.gif') no-repeat center;
width: 250px;
height: 250px;
display: block;
}
</style>
</head>

<body>
<img src="./img/pixel.gif" data-url="./img/1.jpeg">
<img src="./img/pixel.gif" data-url="./img/2.jfif">
<img src="./img/pixel.gif" data-url="./img/3.jfif">
<img src="./img/pixel.gif" data-url="./img/4.jfif">
<img src="./img/pixel.gif" data-url="./img/5.jfif">
<img src="./img/pixel.gif" data-url="./img/6.webp">

<script>

let imgs = document.getElementsByTagName('img')
// 1. 一上来立即执行一次
let io = new IntersectionObserver(function (entires) {
//图片进入视口时就执行回调
entires.forEach(item => {
// 获取目标元素
let oImg = item.target
// console.log(item);
// 当图片进入视口的时候,就赋值图片的真实地址
if (item.intersectionRatio > 0 && item.intersectionRatio <= 1) {
oImg.setAttribute('src', oImg.getAttribute('data-url'))
}
})
})
Array.from(imgs).forEach(element => {
io.observe(element) //给每一个图片设置监听
});
</script>
</body>

</html>

第十三章:bind、apply、call 的用法

本章讲解 call、apply、bind 的相同点和不同点以及手撕代码。

相同点和不同点

相同点

  1. 三个都是用于改变 this 指向;

  2. 接收的第一个参数都是 this 要指向的对象;

  3. 都可以利用后续参数传参。

不同点

  1. call 和 bind 传参相同,多个参数依次传入的;

  2. apply 只有两个参数,第二个参数为数组;

  3. call 和 apply 都是对函数进行直接调用,而 bind 方法不会立即调用函数,而是返回一个修改 this 后的函数。

call的用法

1
fn.call(thisArg, arg1, arg2, arg3, ...)

调用 fn.call 时会将 fn 中的 this 指向修改为传入的第一个参数 thisArg;将后面的参数传入给 fn,并立即执行函数 fn。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let obj = {
name: "xiaoming",
age: 24,
sayHello: function (job, hobby) {
console.log(`我叫${this.name},今年${this.age}岁。我的工作是: ${job},我的爱好是: ${hobby}。`);
}
}
obj.sayHello('程序员', '看美女'); // 我叫xiaoming,今年24岁。我的工作是: 程序员,我的爱好是: 看美女。


let obj1 = {
name: "lihua",
age: 30
}
// obj1.sayHello(); // Uncaught TypeError: obj1.sayHello is not a function
obj.sayHello.call(obj1, '设计师', '画画'); // 我叫lihua,今年30岁。我的工作是: 设计师,我的爱好是: 画画。

手撕call的写法:

  • 第一参数接收 this 对象

  • 改变 this 指向:将函数作为传入 this 对象的方法

  • 展开语法,支持传入和调用参数列表

  • 调用并删除方法,返回结果

1
2
3
4
5
6
7
8
9
10
11
12
Function.prototype.myCall = function(_this, ...args) {
if (!_this) _this = Object.create(null)
_this.fn = this
const res = _this.fn(...args)
delete _this.fn
return res
}
// 使用
function sum (a, b) {
return this.v + a + b
}
sum.myCall({v: 1}, 2, 3) // 6

apply的用法

1
apply(thisArg, [argsArr])

fn.apply 的作用和 call 相同:修改 this 指向,并立即执行 fn。区别在于传参形式不同, apply 接受两个参数,第一个参数是要指向的 this 对象,第二个参数是一个数组,数组里面的元素会被展开传入 fn , 作为 fn 的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
let obj = {
name: "xiaoming",
age: 24,
sayHello: function (job, hobby) {
console.log(`我叫${this.name},今年${this.age}岁。我的工作是: ${job},我的爱好是: ${hobby}。`);
}
}
obj.sayHello('程序员', '看美女'); // 我叫xiaoming,今年24岁。我的工作是: 程序员,我的爱好是: 看美女。
let obj1 = {
name: "lihua",
age: 30
}
obj.sayHello.apply(obj1, ['设计师', '画画']); // 我叫lihua,今年30岁。我的工作是: 设计师,我的爱好是: 画画。

手撕 apply 的写法:

  • 第一参数接收 this 对象

  • 改变 this 指向:将函数作为传入 this 对象的方法

  • 第二个参数默认数组

  • 展开语法,支持调用参数列表

  • 调用并删除方法,返回结果

1
2
3
4
5
6
7
8
9
10
11
12
Function.prototype.myApply = function(_this, args = []) {
if (!_this) _this = Object.create(null)
_this.fn =this
const res = _this.fn(...args)
delete _this.fn
return res
}
// 使用
function sum (a, b) {
return this.v + a + b
}
sum.myApply({v: 1}, [2, 3]) // 6

bind 的用法

1
bind(thisArg, arg1, arg2, arg3, ...)

fn.bind 的作用是只修改 this 指向,但不会立即执行 fn ;会返回一个修改了 this 指向后的 fn 。需要调用才会执行: bind(thisArg, arg1, arg2, arg3, …)()。bind 的传参和 call 相同。

手撕bind的写法:

  • 第一个参数接收 this 对象

  • 返回函数,根据使用方式

链接:https://leetcode.cn/leetbook/read/interview-coding-frontend/dzi9zh/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Function.prototype.myBind = function(_this, ...args) {
const fn = this
return function F(...args2) {
return this instanceof F ? new fn(...args, ...args2)
: fn.apply(_this, args.concat(args2))
}
}
//使用
function Sum (a, b) {
this.v= (this.v || 0)+ a + b
return this
}
const NewSum = Sum.myBind({v: 1}, 2)
NewSum(3) // 调用:{v: 6}
new NewSum(3) // 构造函数:{v: 5} 忽略 myBind 绑定this

第十四章:手写 new

  • 第一参数作为构造函数,其余参数作为构造函数参数
  • 继承构造函数原型创建新对象
  • 执行构造函数
  • 结果为对象,返回结果,反之,返回新对象
1
2
3
4
5
6
7
8
9
10
11
function myNew(...args) {
const Constructor = args[0]
const o = Object.create(Constructor.prototype)
const res = Constructor.apply(o, args.slice(1))
return res instanceof Object ? res : o
}
// 使用
function P(v) {
this.v = v
}
const p = myNew(P, 1) // P {v: 1}

第十五章:promise 的用法

本章讲解 promise 的用法及对应函数的手撕代码。

promise 相关概念

回调方法:就是将一个方法 func2 作为参数传入另一个方法 func1 中,当 func1 执行到某一步或者满足某种条件的时候才执行传入的参数 func2

Promise 是 ES6 引入的异步编程的新解决方案。

Promise 对象三种状态:初始化、成功、失败 pending-进行中、resolved-已完成、rejected-已失败

就好像,你跟你女朋友求婚,她跟你说她要考虑一下,明天才能给你答案,这就是承诺(promise)。同时,这也是一个等待的过程(pending),然后你就等,等到明天你女朋友给你答复,同意(resolved)或者拒绝(rejected),如果同意就准备结婚了,如果不同意就等下次再求婚,哈哈哈。

promise 是用来解决两个问题的:

  • 回调地狱,代码难以维护, 常常第一个的函数的输出是第二个函数的输入这种现象

  • promise 可以支持多个并发的请求,获取并发请求中的数据

  • 这个 promise 可以解决异步的问题,本身不能说 promise 是异步的

promise 基本用法

这样构造 promise 实例,然后调用 .then.then.then 的编写代码方式,就是 promise。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let p = new Promise((resolve, reject) => {      //    调用了Promise构造函数
// 做一些事情
// 然后在某些条件下resolve,或者reject
if (/* 条件随便写^_^ */) {
resolve()
} else {
reject()
}
})

p.then(() => { // 调用了promise实例的.then方法
// 如果p的状态被resolve了,就进入这里
}, () => {
// 如果p的状态被reject
})

声明一个 Promise 对象

1
2
3
4
5
6
7
8
9
10
11
12
13
new Promise((resolve, reject) => {     // 这两个方法主要是用来修改状态的
console.log("开始求婚。")
console.log("。。。。。")
console.log("考虑一下。")
setTimeout(() => {
if (isHandsome || isRich) { // 当我们调用 resolve 函数的时候,Promise 的状态就变成 resolved
resolve('我同意!')
} else { // 当我们调用 reject 函数的时候,Promise 的状态就变成 reject
reject("拒绝:我们八字不合")
}
}, 2000)
})
// 如果一个 promise 已经被兑现(resolved)或被拒绝(rejected),那么我们也可以说它处于已敲定(settled)状态。

Promise.prototype.then 方法

已成功 resolved 的回调和已失败 rejected 的回调

1
2
3
4
5
6
// 调用 Promise 对象的then方法,两个参数为函数
p.then(function(value){ // 成功
console.log(value);
}, function(season){ // 失败
console.log(season);
});

getNumber()

1
2
3
4
5
6
7
8
9
getNumber()
.then(function(data){
console.log('resolved');
console.log(data);
})
.catch(function(reason){
console.log('rejected');
console.log(reason);
});

Promise.prototype.catch 方法

catch() 的作用是捕获 Promise 的错误

其实它和 then 的第二个参数一样,用来指定 reject 的回调,用法是这样:

在执行 resolve 的回调(也就是上面 then 中的第一个参数)时,如果抛出异常了(代码出错了),那么并不会报错卡死 js,而是会进到这个 catch 方法中。请看下面的代码:

1
2
3
4
5
promise.then(
() => { console.log('this is success callback') }
).catch(
(err) => { console.log(err) }
)

效果和写在 then 的第二个参数里面一样。不过它还有另外一个作用:在执行 resolve 的回调(也就是上面 then 中的第一个参数)时,如果抛出异常了(代码出错了),那么并不会报错卡死 js ,而是会进到这个 catch 方法中。请看下面的代码:

1
2
3
4
5
6
7
8
9
10
getNumber()
.then(function(data){
console.log('resolved');
console.log(data);
console.log(somedata); //此处的somedata未定义
})
.catch(function(reason){
console.log('rejected');
console.log(reason);
});

Promise.all() 方法

有了 all,你就可以并行执行多个异步操作,并且在一个回调中处理所有的返回数据。

「谁跑的慢,以谁为准执行回调」

Promise 的 all 方法提供了并行执行异步操作的能力,并且在所有异步操作执行完后才执行回调

1
2
3
4
5
Promise
.all([runAsync1(), runAsync2(), runAsync3()])
.then(function(results){
console.log(results);
});

Promise.all 手撕代码题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const myPromiseAll =(arr)=>{
let result = [];
return new Peomise ((resolve,reject)=>{
for (let i=0;i<arr.length;i++){
arr[i].then(data=>{
result[i] = data;
if(result.length === arr.length)//所有的都成功才执行成功的回调
{
resolve(result)
}//这里可以用计数器更好点
},reject)//有一个失败则执行失败的回调
}
})
}

Promise.race() 方法

  • 「谁跑的快,以谁为准执行回调」

  • 1 秒后 runAsync1 已经执行完了,此时then里面的就执行了

  • 在 then 里面的回调开始执行时,runAsync2() 和 runAsync3() 并没有停止,仍旧再执行。

  • 于是再过 1 秒后,输出了他们结束的标志。

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
  var p = new Promise(function(resolve, reject){
var img = new Image();
img.onload = function(){
resolve(img);
}
img.src = 'xxxxxx';
});
return p;
}

//延时函数,用于给请求计时
function timeout(){
var p = new Promise(function(resolve, reject){
setTimeout(function(){
reject('图片请求超时');
}, 5000);
});
return p;
}

Promise
.race([requestImg(), timeout()])
.then(function(results){
console.log(results);
})
.catch(function(reason){
console.log(reason);
});

Promise.race()手撕代码用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function promiseRace(promises) {
if (!Array.isArray(promises)) {
throw new Error("promises must be an array")
}
return new Promise(function (resolve, reject) {
promises.forEach(p =>
Promise.resolve(p).then(data => {
resolve(data)
}, err => {
reject(err)
})
)
})
}

Promise.prototype.finally() 方法

finally 方法用于指定无论 Promise 对象的最终状态如何,都将执行 finally。Finally 不接受参数。Finally 独立于先前的执行状态,不依赖于先前的运行结果。

1
2
3
4
5
6
7
const promise4 = new Promise((resolve, reject) => {
console.log(x + 1);});
promise4
.then(() => {
console.log("你好");}).catch((err) => {
console.log(err);}).finally(() => {
console.log("finally");});// finally

第十六章:解析 URL

什么是解析URL?

js解析url,就是将如下url: const url = https://www.baidu.com/m?f=8&ie=utf-8&rsv_bp=1&tn=monline_3_dg&wd=session 解析为:

1
2
3
4
5
{f:'8',
ie:'utf-8'
rsv_bp:'1',
tn:'monline_3_dg',
wd:'session'}

利用 splice 分割 + 循环依次取出

  • 解析 url 并将其存储在新对象中,因此初始化一个空对象,让 obj = {}
  • 首先判断 url 后面是否有 ?传参, 如果没有 ? 传参,直接返回空对象

if (url.indexOf(‘?’) < 0) return obj

  • 进行参数分割 let arr = url.split(‘?’)

此时的效果是将 ? 前后,一分为二

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
function queryURLparams(url) {
let obj = {}
if (url.indexOf('?') < 0) return obj
let arr = url.split('?')
url = arr[1]
let array = url.split('&')
for (let i = 0; i < array.length; i++) {
let arr2 = array[i]
let arr3 = arr2.split('=')
obj[arr3[0]] = arr3[1]
}
return obj

}
console.log(queryURLparams(url));
var url = "https://www.baidu.com/m?f=8&ie=utf-8&rsv_bp=1&tn=monline_3_dg&wd=session";
function getURL(url){
let str = url.split("?")[1];
let str1 = str.split("&");
let obj = {};
for(let i = 0; i<str1.length; i++){
let str2 = str1[i].split("=");
let key = str2[0];
let value = str2[1];
obj[key] = value;

}
return obj;
}
console.log(getURL(url))

正则 + arguments

  • 正则匹配规则 /([^?=&]+)=([^?=&]+)/g
  • 利用 replace 替换
  • 用伪数组进行键值对拼接
1
2
3
4
5
6
7
8
function queryURLparamsRegEs5(url) {
let obj = {}
let reg = /([^?=&]+)=([^?=&]+)/g
url.replace(reg, function() {
obj[arguments[1]] = arguments[2]
})
return obj
}

正则 + ..arg

  • 就是用 ES6 的 …arg
  • 其实和 arguments 差不多 ,就是 arguments 是伪数组,…arg 是真数组
1
2
3
4
5
6
7
8
function queryURLparamsRegEs6(url) {
let obj = {}
let reg = /([^?=&]+)=([^?=&]+)/g
url.replace(reg, (...arg) => {
obj[arg[1]] = arg[2]
})
return obj
}

利用URL构造函数解析整个URL

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
function parseUrlParams(url) {
// 创建一个空对象用于存储结果
const params = {};

// 使用URL API处理传入的URL,获取查询字符串
// 注意:对于老版本的IE浏览器,URL API可能不可用
const urlObject = new URL(url);
const queryString = urlObject.search;

// 如果查询字符串存在,使用URLSearchParams API解析参数
if (queryString) {
const searchParams = new URLSearchParams(queryString);
// 遍历所有参数,并添加到结果对象中
searchParams.forEach((value, key) => {
// 如果参数名在对象中已存在(意味着参数可能出现多次),将其转换为数组
if (params.hasOwnProperty(key)) {
// 如果已经是数组,直接添加
if (Array.isArray(params[key])) {
params[key].push(value);
} else { // 否则,创建数组并添加当前值和新值
params[key] = [params[key], value];
}
} else {
// 参数名第一次出现,直接添加到对象中
params[key] = value;
}
});
}

return params;
}

// 示例使用:
const url = 'https://example.com/?name=John&age=30&hobby=reading&hobby=cycling';
const params = parseUrlParams(url);
console.log(params);
// 输出: { name: 'John', age: '30', hobby: ['reading', 'cycling'] }

技术栈:react+next.js+qiankun+vue2.6+vue-router+vuex+websocket+node.js+axios+echarts
描述:项目包括轻量看板、瀑布模式、迭代模式三种项目管理方式,涵盖项目管理全生命周期的综合平台,包括工作台、项目管理、需求池、统计报表、甘特图等模块,实现从市场需求、项目管理到应用上线交付的业务闭环。
运用qiankun构建微前端,使用前端路由守卫进行权限管理,实现子应用间状态的有效共享和同步更新;
实现权限控制系统,实现了精细化的权限校验逻辑,前端采用recoil全局状态管理,高效管理用户状态信息,支持基于角色的ui显示逻辑、动态控制操作权限;
开发需求管理组件和实现需求评审流程配置功能,运用vue.js、vuex等技术实现复杂表单的动态生成和校验,处理多层嵌套组件的通信和状态管理,优化异步数据的加载和处理逻辑,实现了需求数据的动态加载和实时更新;
封装富文本编辑器组件,通过vue的响应式原理和web存储api实现内容的实时捕获、安全存储和快速恢复,大幅提升用户编辑体验,并有效防止内容丢失;
开发文件批量上传与管理组件,支持文件多种操作(如上传、预览、关联文档等),通过客户端验证减少服务器压力,提高项目文档管理的效率和用户体验;
开发甘特图视图,包括精确的时间线对齐、动态数据绑定、以及用户交互的拖拽和缩放功能。采用虚拟滚动和延迟加载技术解决大规模数据渲染带来的性能挑战,开发自定义列配置组件,利用localstorage实现了用户配置的跨会话持久化,从而提升了用户体验。实现的视图切换和动态数据过滤功能,提高甘特图的查看和操作效率。
技术栈:react+redux+nextjs+typescript+webpack+antd+axios+websocket+sentry
描述:基于开源开发的数据管理平台,允许用户创建、管理和共享数据表、表单和仪表板。项目采用了模块化的设计和现代web技术,提供了强大的数据操作和协作功能。支持多种视图类型和扩展性,包含个人待办事项、团队待办事项和模板库,通过引入自定义待办事项和丰富的模板库,极大提升了用户在项目管理、数据分析和团队协作方面的效率。
登录系统集成无痕验证码技术,通过服务器端自动执行,减少了传统验证码对用户体验的干扰。该系统全面涵盖了用户数据处理、前端校验、无痕验证,以及服务器端的认证与状态管理,确保用户操作的便捷性和系统的安全性;
实现动态的页面导航,通过postmessage实现了iframe中的应用与主页面的安全通信,根据不同的应用场景,如分享视图、嵌入视图、个人路径等,实现了条件性导航逻辑,在不同上下文中都能够正确地引导用户到期望的视图;
通过sentry用于错误监控和性能优化,提高错误追踪的准确性和应用性能监控的效率,设计自定义hooks用于封装复杂的业务逻辑和api请求,包括节点的增删改查、个人待办事项的管理、共享和收藏节点操作等;
负责甘特图视图内任务内容的动态渲染组件开发,使用延迟加载和按需渲染的策略,减少了页面的初始加载时间和运行时的性能开销,采用 react和 konva库实现客户端绘制,通过 next.js动态导入技术优化加载速度;
开发个人待办事项管理组件,通过hooks和高阶组件等技术实现功能模块的解耦和复用,利用自定义hooks处理数据请求和副作用,在处理共享目录的逻辑时,通过redux和封装自定义hooks,实现了状态的更新和数据的同步;
负责轻任务应用的主导航栏组件,支持在不同设备和屏幕尺寸上的操作体验,处理了用户登录、空间选择、通知计数等多个状态的同步和更新,根据用户权限动态调整导航栏展示的功能模块,对于非管理员用户隐藏空间管理入口,对于删除空间的用户限制访问等,实现了细粒度的功能控制。