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 )
原型链继承存在的问题:
构造函数实现继承
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 (); child1.sayAge ();
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])) } 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))
深浅拷贝
浅拷贝:
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 { return JSON .parse (JSON .stringify (obj)); }
完整版(zbwer’s
blog ):
使用 WeakMap
作为哈希表,记录已经拷贝过的对象,避免循环引用导致的栈溢出。
对于特殊的数据类型,例如
Date
、RegExp
,直接创建新的实例。
没有考虑传入参数为 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 ; 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
中的两种不同的键值对集合,主要区别如下:
map
的键可以是任意类型,weakMap
键只能是对象类型。
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 ( ) { this .handlers = new Map () } on (eventName : string , cb : Function ) { if (!this .handlers .get (eventName)) { this .handlers .set (eventName, []) } this .handlers .get (eventName)?.push (cb) } emit (eventName : string , ...args : any ) { if (this .handlers .get (eventName)) { const handlers = this .handlers .get (eventName) handlers?.forEach (callback => { callback (...args) }) } } off (eventName : string , cb : Function ) { const cbs = this .handlers .get (eventName) if (cbs) { const index = cbs.indexOf (cb) cbs.splice (index, 1 ) } } 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)) { if (Array .isArray (paramsObj[key])) { paramsObj[key].push (val) } else { paramsObj[key] = [paramsObj[key], val]; } } else { paramsObj[key] = [val]; } } else { 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。
注意设置的时候不能同时将leading
或trailing
设置为
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 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 ))
偏函数