# 中级前端所有手写内容

# 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)
  }
}
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

一般不用看 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
}
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

# 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
}
1
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))
1
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
}
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

# 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))
1
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'))
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

核心思路是:

  1. 为传入的context扩展一个属性,将原函数指向这个属性
  2. context之外的所有参数全部传递给这个新属性,并将运行结果返回。

一些细节:

  1. 利用rest 参数(…args)可以存储函数多余的参数
  2. 为传入的context扩展参数扩展新属性使用了**Symbol()数据类型**,这样确保不会影响到传入的context,因为 Symbol 值一定是独一无二的。
  3. 扩展运算符()将原来是数组的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
}
1
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`)
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

# 8.手写 eventEmitter

观察者模式是我们工作中经常能接触到的一种设计模式。用过 jquery 的应该对这种设计模式都不陌生。eventEmitternode 中的核心,主要方法包括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] }
 */
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

# 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()
1
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

以此类推。
1
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')
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

# 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
1
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
1
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
1
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]
  })
}
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')
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

参考链接: https://juejin.cn/post/6844903960495538189 https://zhuanlan.zhihu.com/p/69070129

在线客服