# 中级前端所有手写内容
# 1.手写 js 防抖、节流
原理都是利用闭包保存变量。
防抖是任务频繁触发的情况下,只有任务触发的间隔超过指定间隔的时候,任务才会执行,一般用于输入框实时搜索;
节流是规定函数在指定的时间间隔内只执行一次,一般用于 scroll 事件。
// 防抖
function debounce(fn, time) {
let timer = null
return function() {
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
fn.apply(this, arguments)
}, time)
}
}
// 节流
function throttle(fn, time) {
let canRun = true
return function() {
if (!canRun) {
return
}
canRun = false
setTimeout(() => {
fn.apply(this, arguments)
canRun = true
}, time)
}
}
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
一般不用看 https://segmentfault.com/a/1190000018445196
# 2.深拷贝和浅拷贝
一般不用看 https://www.cnblogs.com/c2016c/articles/9328725.html
// 1.
function deepClone(obj) {
var result = Array.isArray(obj) ? [] : {}
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
if (typeof obj[key] === 'object' && obj[key] !== null) {
result[key] = deepClone(obj[key])
} else {
result[key] = obj[key]
}
}
}
return result
}
//2.
function deepClone(arr) {
return JSON.parse(JSON.stringify(arr))
}
// 浅拷贝
function shallowClone(obj) {
let cloneObj = {}
for (let i in obj) {
cloneObj[i] = obj[i]
}
return cloneObj
}
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
# 3.数组乱序
// 1.取巧的一种算法,但是每个位置乱序的概率不同
function mixArr(arr) {
return arr.sort(() => {
return Math.random() - 0.5
})
}
// 2.著名的Fisher–Yates shuffle 洗牌算法
// 费雪耶兹洗牌算法
function shuffle(arr) {
let m = arr.length
while (m > 1) {
let index = parseInt(Math.random() * m--)
;[arr[index], arr[m]] = [arr[m], arr[index]]
}
return arr
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 4.数组去重:
// 1.
let resultArr = [...new Set(originalArray)]
// 2.
let resultArr = Array.from(new Set(originalArray))
// 3.
const resultArr = new Array()
const originalArray = [1, 2, 3, 4, 1, 2, 4, 6]
originalArray.forEach(element => {
if (!resultArr.includes(element)) {
resultArr.push(element)
}
})
console.log(resultArr)
// 4.
console.log(_.uniq(originalArray))
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 5.数组 flat
数组 flat 方法是 ES6 新增的一个特性,可以将多维数组展平为低维数组。如果不传参默认展平一层,传参可以规定展平的层级。
// 展平一级
function flat(arr) {
var result = []
for (var i = 0; i < arr.length; i++) {
if (Array.isArray(arr[i])) {
result = result.concat(flat(arr[i]))
} else {
result.push(arr[i])
}
}
return result
}
//展平多层
function flattenByDeep(array, deep) {
var result = []
for (var i = 0; i < array.length; i++) {
if (Array.isArray(array[i]) && deep >= 1) {
result = result.concat(flattenByDeep(array[i], deep - 1))
} else {
result.push(array[i])
}
}
return result
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 6.数组 filter
filter 方法经常用,实现起来也比较容易。需要注意的就是 filter 接收的参数依次为数组当前元素、数组 index、整个数组,并返回结果为 ture 的元素。
Array.prototype.filter = function(fn, context) {
console.log(`context`, context)
if (typeof fn != 'function') {
throw new TypeError(`${fn} is not a function`)
}
let arr = this
let result = []
for (let i = 0; i < arr.length; i++) {
let temp = fn.call(context, arr[i], i, arr)
if (temp) {
result.push(arr[i])
}
}
return result
}
const a = [1, 2, 3, 4, 0, 0, '']
console.log(a.filter(Boolean))
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 7.手写 call、bind、apply
call,bind,apply 的用法及区别
call,bind,apply 都是用于改变函数的 this 的指向,三者第一个参数都是 this 要指向的对象,如果没有这个参数或参数为 undefined 或 null,则默认 this 指向全局 window 或 global。区别:
- apply 与 call 的区别: apply 接收两个参数,分别为 this 和函数执行的参数数组;call 接收 n 个参数,第一个参数为 this,其他的为函数执行的参数列表。
- bind 与 apply 和 call 的区别:bind 是返回函数 A 绑定 this 之后的函数 B,之后可以继续操作函数 B 或者调用函数 B;apply 和 call 则是立即给函数 A 传参并执行。
// 手写 call
Function.prototype.myCall = function(context, ...args) {
// 判断是否是 undefined 和 null
// 从用户的视角,context 一般就是用户要传入的 this
if (typeof context === 'undefined' || context === null) {
context = globalThis
}
console.log(`globalThis`, globalThis)
const fnSymbol = Symbol()
// this 是 myCall 的调用者,就是 fn
context[fnSymbol] = this
console.log(`myCall this`, this)
// 这一步将 fn 的调用者改为了 context
const res = context[fnSymbol](...args)
delete context[fnSymbol]
return res
}
const fn = function(m) {
console.log(`fn this`, this)
console.log(m)
return m + ` test`
}
const a = {
b: 'this is b'
}
console.log(`fn.myCall(a, "mmm")`, fn.myCall(a, 'mmm'))
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
核心思路是:
- 为传入的
context
扩展一个属性,将原函数指向这个属性 - 将
context
之外的所有参数全部传递给这个新属性,并将运行结果返回。
一些细节:
- 利用rest 参数(
…args
)可以存储函数多余的参数 - 为传入的
context
扩展参数扩展新属性使用了**Symbol()
数据类型**,这样确保不会影响到传入的context
,因为 Symbol 值一定是独一无二的。 - 用扩展运算符(
…
)将原来是数组的args
转发为逗号分隔一个个参数传入到函数中。为什么能找到this.name
呢?因为方法context[fnSymbol]
中的this
指向的是context
。
// 手写 apply
Function.prototype.myApply = function(context, args) {
// 判断是否是undefined和null
if (typeof context === 'undefined' || context === null) {
context = globalThis
}
const fnSymbol = Symbol()
context[fnSymbol] = this
const res = context[fnSymbol](...args)
delete context[fnSymbol]
return res
}
2
3
4
5
6
7
8
9
10
11
12
思路和call
是一样的只是传参不同方式而已
// 手写 bind
Function.prototype.myBind = function(context) {
// 判断是否是undefined和null
if (typeof context === 'undefined' || context === null) {
context = globalThis
}
const fnSymbol = Symbol()
context[fnSymbol] = this
return function(...args) {
const res = context[fnSymbol](...args)
delete context[fnSymbol]
return res
}
}
const fn = function(m) {
console.log(`this in fn`, this)
console.log(m)
}
const obj = { a: 'this is obj.a' }
const _fn = fn.myBind(obj)
_fn(`this is message`)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 8.手写 eventEmitter
观察者模式是我们工作中经常能接触到的一种设计模式。用过 jquery
的应该对这种设计模式都不陌生。eventEmitter
是 node
中的核心,主要方法包括on、emit、off、once
。
class EventEmitter {
constructor() {
this.events = {}
}
on(name, cb) {
this.events[name] = this.events[name] || []
this.events[name].push(cb)
}
emit(name, ...arg) {
if (this.events[name]) {
this.events[name].forEach(fn => {
fn.call(this, ...arg)
})
}
}
off(name, cb) {
if (this.events[name]) {
this.events[name] = this.events[name].filter(fn => {
return fn != cb
})
}
}
once(name, fn) {
var onlyOnce = () => {
console.log(`this`, this)
console.log(`arguments`, arguments)
fn.apply(this, arguments)
this.off(name, onlyOnce)
}
this.on(name, onlyOnce)
return this
}
}
const fn = function() {
console.log(`arguments`, arguments)
}
const bus = new EventEmitter()
bus.once('a', fn)
bus.emit('a', 1, 2, 3)
/**
this EventEmitter { events: { a: [ [Function: onlyOnce] ] } }
arguments [Arguments] { '0': 'a', '1': [Function: fn] }
arguments [Arguments] { '0': 'a', '1': [Function: fn] }
*/
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
# 9.手写继承
// ES6
class Parent {
constructor(name, age) {
this.name = name
this.age = age
}
say() {
console.log(`this is parent`)
}
}
class Child extends Parent {
constructor(name, age, sex) {
super(name, age)
this.sex = sex // 必须先调用super,才能使用this
}
}
const child = new Child()
child.say()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 10.手写 lazyMan
实现一个LazyMan,可以按照以下方式调用:
LazyMan("Hank")输出:
Hi! This is Hank!
LazyMan("Hank").sleep(10).eat("dinner")输出
Hi! This is Hank!
//等待10秒..
Wake up after 10
Eat dinner~
LazyMan("Hank").eat("dinner").eat("supper")输出
Hi This is Hank!
Eat dinner~
Eat supper~
LazyMan("Hank").sleepFirst(5).eat("supper")输出
//等待5秒
Wake up after 5
Hi This is Hank!
Eat supper
以此类推。
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
这道题主要考察的是链式调用、任务队列、流程控制等。关键是用手动调用 next 函数来进行下次事件的调用,类似express
中间件和vue-router
路由的执行过程。
class LazyMan {
constructor(name) {
this.nama = name
this.queue = []
this.queue.push(() => {
console.log('Hi! This is ' + name + '!')
this.next()
})
setTimeout(() => {
this.next()
}, 0)
}
next() {
const fn = this.queue.shift()
fn && fn()
}
eat(name) {
this.queue.push(() => {
console.log('Eat ' + name + '~')
this.next()
})
return this
}
sleep(time) {
this.queue.push(() => {
setTimeout(() => {
console.log('Wake up after ' + time + 's!')
this.next()
}, time * 1000)
})
return this
}
sleepFirst(time) {
this.queue.unshift(() => {
setTimeout(() => {
console.log('Wake up after ' + time + 's!')
this.next()
}, time * 1000)
})
return this
}
}
function LazyManFactory(name) {
return new LazyMan(name)
}
let lazyMan = LazyManFactory('pengjie')
lazyMan = LazyManFactory('pengjie')
.sleep(10)
.eat('dinner')
lazyMan = LazyManFactory('pengjie')
.sleepFirst(5)
.eat('supper')
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
# 11.函数柯里化(currying)
函数柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术,是高阶函数的一种用法。比如求和函数add(1,2,3)
, 经过柯里化后变成add(1)(2)(3)
// 普通的add函数
function add(x, y) {
return x + y
}
// Currying后
function curryingAdd(x) {
return function(y) {
return x + y
}
}
add(1, 2) // 3
curryingAdd(1)(2) // 3
2
3
4
5
6
7
8
9
10
11
12
13
14
好处
- 1、参数复用
// 正常正则验证字符串 reg.test(txt)
// 普通情况
function check(reg, txt) {
return reg.test(txt)
}
check(/\d+/g, 'test') //false
check(/[a-z]+/g, 'test') //true
// Currying后
function curryingCheck(reg) {
return function(txt) {
return reg.test(txt)
}
}
var hasNumber = curryingCheck(/\d+/g)
var hasLetter = curryingCheck(/[a-z]+/g)
hasNumber('test1') // true
hasNumber('testtest') // false
hasLetter('21212') // false
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- 2、延迟执行
其实Function.prototype.bind
就是科里化的实现例子
function sayKey(key) {
console.log(this[key])
}
const person = {
name: 'Sunshine_Lin',
age: 23
}
// call不是科里化
sayKey.call(person, 'name') // 立即输出 Sunshine_Lin
sayKey.call(person, 'age') // 立即输出 23
// bind是科里化
const say = sayKey.bind(person) // 不执行
// 想执行再执行
say('name') // Sunshine_Lin
say('age') // 23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 12.日期格式化
function formatDate(t, str) {
var obj = {
yyyy: t.getFullYear(),
yy: t.getFullYear() % 100,
M: t.getMonth() + 1,
MM: ('0' + (t.getMonth() + 1)).slice(-2),
d: t.getDate(),
dd: ('0' + t.getDate()).slice(-2),
HH: ('0' + t.getHours()).slice(-2),
H: t.getHours(),
h: t.getHours() % 12,
hh: ('0' + (t.getHours() % 12)).slice(-2),
mm: ('0' + t.getMinutes()).slice(-2),
m: t.getMinutes(),
ss: ('0' + t.getSeconds()).slice(-2),
s: t.getSeconds(),
w: ['日', '一', '二', '三', '四', '五', '六'][t.getDay()]
}
return str.replace(/[a-z]+/gi, function($1) {
return obj[$1]
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 13.判断电子邮件
var isEmail = function(val) {
var pattern = /^([A-Za-z0-9_\-\.])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/
var domains = [
'qq.com',
'163.com',
'vip.163.com',
'263.net',
'yeah.net',
'sohu.com',
'sina.cn',
'sina.com',
'eyou.com',
'gmail.com',
'hotmail.com',
'42du.cn'
]
if (pattern.test(val)) {
var domain = val.substring(val.indexOf('@') + 1)
for (var i = 0; i < domains.length; i++) {
if (domain == domains[i]) {
return true
}
}
}
return false
}
// 输出 true
isEmail('cn42du@163.com')
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
参考链接: https://juejin.cn/post/6844903960495538189 https://zhuanlan.zhihu.com/p/69070129
在线客服