破酥 | C4iN
javascript / typescript 手写题

javascript / typescript 手写题

数据类型判断

1
2
3
4
5
6
7
8
function typeOf(obj: any) {
return Object.prototype.toString.call(obj).slice(8, -1).toLocaleLowerCase()
}

let arr: any[] = []
let obj = {}
let data = new Date()
console.log(typeOf(arr), typeOf(obj), typeOf(data))

继承

实现类继承。

原型链继承

原型链继承的核心思想是利用原型对象的属性和方法来实现继承。(js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Animal() {
this.color = ['black', 'white']
}

Animal.prototype.getColor = function() {
return this.color
}

function Dog() {}
Dog.prototype = new Animal()

let dog1 = new Dog()
dog1.color.push("brown")
let dog2 = new Dog
console.log(dog2.color)

原型链继承存在的问题:

  • 原型中包含的引用类型属性将被所有实例共享,在上面的例子中我们操作的是dog1,但是dog2的属性也改变了;

  • 子类在实例化的时候不能给父类构造函数传参;

构造函数实现继承

1
2
3
4
5
6
7
8
9
10
function Animal(name) {
this.name = name
this.getName = function() {
return this.name
}
}
function Dog(name) {
Animal.call(this, name)
}
Dog.prototype = new Animal()

方法必须定义在构造函数中,所以会导致每次创建子类实例都会创建一遍方法。

组合继承

基本的思路是使用原型链继承原型上的属性和方法,而通过盗用构造函数继承实例属性。这样既可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Animal(name) {
this.name = name
this.colors = ['black', 'white']
}

Animal.prototype.getName = function() {
return this.name
}

function Dog(name, age) {
Animal.call(this, name)
this.age = age
}

Dog.prototype = new Animal()
Dog.prototype.constructor = Dog


let dog1 = new Dog('奶昔', 2)
dog1.colors.push('brown')
let dog2 = new Dog('哈赤', 1)
console.log(dog2)

寄生式组合继承

组合继承已经相对完善了,但还是存在问题,它的问题就是调用了 2 次父类构造函数,第一次是在new Animal(),第二次是在Animal.call()这里。解决方案就是不直接调用父类构造函数给子类原型赋值,而是通过创建空函数 F 获取父类原型的副本。

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 Animal(name) {
this.name = name
this.colors = ['black', 'white']
}

Animal.prototype.getName = function() {
return this.name
}

function Dog(name, age) {
Animal.call(this, name)
this.age = age
}

inheritPrototype(Dog, Animal)

function object(o) {
function F() {}
F.prototype = o
return new F()
}
function inheritPrototype(child, parent) {
let prototype = object(parent.prototype)
prototype.constructor = child
child.prototype = prototype
}


let dog1 = new Dog('奶昔', 2)
dog1.colors.push('brown')
let dog2 = new Dog('哈赤', 1)
console.log(dog2)


另一种写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function Parent(name) {
this.name = name;
this.colors = ['red', 'green', 'blue'];
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
function Child(name, age) {
// 执行父类构造函数
Parent.call(this, name);
this.age = age;
}
// 将子类的原型 指向父类
Child.prototype = Object.create(Parent.prototype);
// 此时的构造函数为父类的 需要指回自己
Child.prototype.constructor = Child;

Child.prototype.sayAge = function() {
console.log(this.age);
};
var child1 = new Child('Tom', 18);
child1.sayName(); // 'Tom'
child1.sayAge(); // 18

class

直接使用extends关键字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Animal {
constructor(name) {
this.name = name
}
getName() {
return this.name
}
}
class Dog extends Animal {
constructor(name, age) {
super(name)
this.age = age
}
}

数组去重

1
2
3
4
5
6
7
8
function unique(arr: any[]) {
return arr.filter((item: any, index: number) => {
return arr.indexOf(item) === index
})
}

const arr = [1,1,1,2,3,4,5,5,5,6,7,7,8]
console.log(unique(arr))

使用Set

1
2
3
function unique(arr: any[]) {
return [...new Set(arr)]
}

数组扁平化

方法一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function flatten(arr: any[]) {
let result: any[] = []
for (let i = 0; i < arr.length; i++) {
if (Array.isArray(arr[i])) {
result = result.concat(flatten(arr[i])) // result.push(...flatten(arr[i]))
} else {
result.push(arr[i])
}
}

return result
}

const arr = [1, [2, 3]]
console.log(flatten(arr))

方法二:

1
2
3
4
5
6
function flatten(arr: any[]) {
return [].concat(...arr)
}

const arr = [1, [2, 3]]
console.log(flatten(arr))

深浅拷贝

浅拷贝:

  • Object.align
  • { ...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
function shallowCopy<T extends object>(obj: T): T {
if (typeof obj !== 'object' || obj === null) {
throw new TypeError('The provided argument is not an object');
}

const newObj = obj instanceof Array ? [] as unknown as T : {} as unknown as T;
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = obj[key];
}
}
return newObj;
}

const originalObject = {
name: "C4iN",
age: 30,
skills: ["TypeScript", "JavaScript"]
};

const originalArray = [1, 2, 3, 4, 5];

const copiedObject = shallowCopy(originalObject);
console.log(copiedObject);

const copiedArray = shallowCopy(originalArray);
console.log(copiedArray);

深拷贝:实现一个深拷贝函数,支持拷贝常见的数据类型,例如对象、数组、函数、正则、日期等,并且能够正常处理循环引用。

超级简单版:

1
2
3
4
function deepCloneEasy<T>(obj: T): T {
// 不能处理函数、正则、undefined、循环引用
return JSON.parse(JSON.stringify(obj));
}

完整版(zbwer’s blog):

  • 使用 WeakMap 作为哈希表,记录已经拷贝过的对象,避免循环引用导致的栈溢出。
  • 对于特殊的数据类型,例如 DateRegExp,直接创建新的实例。

没有考虑传入参数为 Map、Set 等特殊对象的情况,使用 obj instanceof Map 然后做类似处理即可。

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
function deepClone<T>(obj: T, hashMap = new WeakMap()) {
if (obj === null || typeof obj !== "object") return obj;
if (obj instanceof Date) return new Date(obj) as any;
if (obj instanceof RegExp) return new RegExp(obj) as any;

// 如果hash已存在,直接返回
if (hashMap.has(obj)) return hashMap.get(obj)

// 数组
if (Array.isArray(obj)) {
const copy: any[] = []
hashMap.set(obj, copy)
// 递归,并将结果推入数组
obj.forEach(item => copy.push(deepClone(item, hashMap)))
return copy
} else {
const copy: Record<string, any> = {}
hashMap.set(obj, copy)
// 递归,并给对应键赋值
Object.entries(obj).forEach(
([key, value]) => (copy[key] = deepClone(value, hashMap))
);
return copy
}
}

const originalObject = {
name: "C4iN",
age: 30,
skills: ["TypeScript", "JavaScript"]
};
console.log(deepClone(originalObject))

const originalArray = [1, 2, 3, 4, 5];
console.log(deepClone(originalArray))

知识点:

它们是 JavaScript 中的两种不同的键值对集合,主要区别如下:

  1. map的键可以是任意类型,weakMap键只能是对象类型。
  2. map 使用常规的引用来管理键和值之间的关系,因此即使键不再使用,map 仍然会保留该键的内存。weakMap 使用弱引用来管理键和值之间的关系,因此如果键不再有其他引用,垃圾回收机制可以自动回收键值对。

事件总线 EventBus

发布订阅模式的实现。

发布订阅模式是一种常用的设计模式,它定义了一种一对多的关系,让多个订阅者对象同时监听某一个主题对象,当主题对象发生变化时,它会通知所有订阅者对象,使它们能够自动更新 。

Event Bus / Event Emitter 作为全局事件总线,它起到的是一个沟通桥梁的作用。我们可以把它理解为一个事件中心,我们所有事件的订阅/发布都不能由订阅方和发布方“私下沟通”,必须要委托这个事件中心帮我们实现。

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
class EventBus {
handlers: Map<any, any[]>
constructor() {
// handlers是一个map,用于存储事件与回调之间的对应关系
this.handlers = new Map()
}

/**
* 安装事件监听器
* @param eventName - 目标事件名
* @param cb - 回调函数
*/
on(eventName: string, cb: Function) {
// 检查事件是否有监听函数队列
if (!this.handlers.get(eventName)) {
// 没有则设置
this.handlers.set(eventName, [])
}

// 添加回调
this.handlers.get(eventName)?.push(cb)

}

/**
* 用于触发目标事件
* @param eventName - 事件名
* @param args - 监听函数入参
*/
emit(eventName: string, ...args: any) {
// 检查目标事件是否有监听事件函数队列
if (this.handlers.get(eventName)) {
const handlers = this.handlers.get(eventName)
// 触发时间函数队列
handlers?.forEach(callback => {
callback(...args)
})
}
}

/**
* 移除某个事件回调队列里的指定回调函数
* @param eventName
* @param cb
*/
off(eventName: string, cb: Function) {
const cbs = this.handlers.get(eventName)
if (cbs) {
const index = cbs.indexOf(cb)
cbs.splice(index, 1)
}
}

/**
* 为事件注册单次监听器
* @param eventName
* @param cb
*/
once(eventName: string, cb: Function) {
// 包装回调函数,执行完毕自动清除
const wrapper = (...args: any) => {
cb(...args)
this.off(eventName, wrapper)
}
this.on(eventName, wrapper)
}

}

const eventBus = new EventBus()
eventBus.on('test', (val: any) => {
console.log("test: ", val)
})
eventBus.emit("test", 21)

解析 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
38
39
40
41
42
43
type URLParams = { [key: string]: any[] | boolean};

function parseParam(url: string): URLParams {
let paramsStr = ""
if (/.+\?(.+)$/.exec(url)) {
// 将 ? 后面的字符串取出来
const params = /.+\?(.+)$/.exec(url)
if (params) {
paramsStr = params[1]
}
}

const paramsArr = paramsStr.split("&")
let paramsObj: URLParams = {}
paramsArr.forEach((param: string) => {
if (/=/.test(param)) {
// 处理等号
let [key, val] = param.split('=')
// 解码
val = decodeURIComponent(val)

if(paramsObj.hasOwnProperty(key)) {
// 如果对象有 key,则添加一个值
if (Array.isArray(paramsObj[key])) {
paramsObj[key].push(val)
} else {
// 如果不是数组,则转换为数组并添加值
paramsObj[key] = [paramsObj[key], val];
}
} else { // 如果对象没有这个 key,创建 key 并设置值
paramsObj[key] = [val];
}
} else {
// 处理没有 value 的参数
paramsObj[param] = true
}
})

return paramsObj
}

const url = "https://lingrui.studio/directions/#/?name=crypto"
console.log(parseParam(url))

字符串模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function render(template: string, data: {[key: string]: string | number}) {
const reg = /\{\{(\w+)\}\}/
if (reg.test(template)) {
const match = reg.exec(template)
if (match) {
const name = match[1]
template = template.replace(reg, data[name] as string)
return render(template, data)
}
}
return template
}

let template = '我是{{name}},年龄{{age}},性别{{sex}}';
let person = {
name: '布兰',
age: 12,
sex: "男"
}

console.log(render(template, person))

防抖

事件触发后等待一段时间再执行回调函数,如果在等待期间内再次触发了同一事件,则重新计时,以避免回调函数的多次执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
function debounce<T extends (...args: any[]) => any>(
fn: T,
wait: number
): (...args: Parameters<T>) => void {
let timeout: ReturnType<typeof setTimeout> | undefined

return function(this: any, ...args: Parameters<T>){
clearTimeout(timeout)
setTimeout(() => {
fn.call(this, ...args)
}, wait)
}
}

最终版

除了支持 this 和 event 外,还支持以下功能:

  • 支持立即执行;
  • 函数可能有返回值;
  • 支持取消功能;
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
interface debouncedFunc<T extends (...args: any[]) => any> {
(...args: Parameters<T>): any
cancel: () => void
}

function debounce<T extends (...args: any[]) => any>(
this: any,
fn: T,
wait: number,
immediate: boolean
): debouncedFunc<T> {
let timeout: ReturnType<typeof setTimeout> | null
let result: any

const ctx = this

const debounced: debouncedFunc<T> = function( ...args: Parameters<T>) {
if (timeout) clearTimeout(timeout)
if (immediate) {
const callNow = !timeout
timeout = setTimeout(() => {
timeout = null
}, wait)

// 如果已经执行过,则不再执行
if (callNow) fn.call(ctx, ...args)
} else {
timeout = setTimeout(() => {
fn.call(ctx, ...args)
}, wait)
}

return result
}

debounced.cancel = function() {
if (timeout) {
clearTimeout(timeout)
}
timeout = null
}

return debounced
}

节流

在一定时间内,事件多次触发只执行一次回调函数。不论事件触发多频繁,都会按照固定的时间间隔执行。

1
2
3
4
5
6
7
8
9
10
11
12
function throttle(fn: Function, wait = 0) {
let isThrottle = false
return function(this: any, ...args: any[]) {
if (!isThrottle) {
isThrottle = true
fn.call(this, args)
setTimeout(() => {
isThrottle = false
}, wait)
}
}
}

最终版

支持取消节流;另外通过传入第三个参数,options.leading来表示是否可以立即执行一次,opitons.trailing表示结束调用的时候是否还要执行一次,默认都是 true。 注意设置的时候不能同时将leadingtrailing设置为 false。

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
interface throttleOptions {
/**
* 立即执行一次
*/
leading?: true
/**
* 结束调用的时候是否还要执行一次
*/
trailing?: true
}

interface throttledFunc<T extends (...args: any[]) => any> {
(...args: Parameters<T>): any
cancel: () => void
}


function throttle<T extends (...args: any[]) => any>(
this: any,
fn: T,
wait: number,
options = {
leading: true,
trailing: true
}
): throttledFunc<T> {
let timeout: ReturnType<typeof setTimeout> | null
let context = this
let result: any
// 记录上一次函数执行的时间戳
let previous = 0

// 在时间间隔结束后执行fn函数
const later = function(...args: Parameters<T>) {
previous = !options.leading ? 0 : new Date().getTime()
timeout = null
fn.call(context, ...args)
if (!timeout) {
context = null
}
}

const throttled: throttledFunc<T> = function(this:any, ...args: Parameters<T>) {
const now = new Date().getTime()
if (!previous && !options.leading) previous = now
let remaining = wait - (now - previous)
context = this

// 如果时间间隔已经过去,或者剩余时间比总时间还要长,则执行函数
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout)
timeout = null
}
previous = now
fn.call(this, ...args)
if (!timeout) context = null
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining)
}
}

throttled.cancel = function() {
if (timeout) {
clearTimeout(timeout)
}
previous = 0
timeout = null
}

return throttled
}

图片懒加载

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
let imgList = [...document.querySelectorAll('img')]
let len = imgList.length

const lazyLoading = (function() {
let count = 0

return () => {
let deleteIndexList: any[] = []
imgList.forEach((img, index) => {
let rect = img.getBoundingClientRect()
if (rect.top < window.innerHeight) {
img.src = img.getAttribute("data-src") as string
deleteIndexList.push(index)
count++
if (count === len) {
document.removeEventListener("scroll", lazyLoading)
}
}
})

imgList = imgList.filter((img, index) => !deleteIndexList.includes(index))
}
})()

throttle(() => document.addEventListener('scroll', lazyLoading), 1000)()

函数柯里化

将使用多个参数的函数转换成一系列使用一个参数的函数的技术。

judge 函数检查传递给它的参数数量(args.length)是否等于原始函数 fn 所需的参数数量(fn.length)。

  • 如果相等,说明已经收集到了足够的参数,可以直接调用原始函数 fn 并使用 ...args 将所有收集到的参数展开传递给 fn
  • 如果不相等,说明参数数量还不够,judge 函数将返回一个新的箭头函数,这个新函数同样接受一个 rest 参数 ...arg
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
function curry(fn: Function) {
let judge = (...args: any[]) => {
console.log(args)
if (args.length === fn.length) return fn(...args)
return (...arg: any[]) => judge(...arg s, ...arg)
}

return judge
}

function add(a: number, b: number, c: number) {
return a + b + c
}
add(1, 2, 3)
let addCurry = curry(add)
console.log(addCurry(1)(2)(3))


/*
output:
[ 1 ]
[ 1, 2 ]
[ 1, 2, 3 ]
6
*/

偏函数

Author:破酥 | C4iN
Link:https://c4in1.github.io/2024/10/23/面筋/javascript&typescript手写题/
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可