常见手写代码

5/3/2023 javascript面试

# 实现 sleep 函数

作用:暂停 JavaScript 的执行一段时间后再继续执行

function sleep(time) {
  return new Promise((resolve) => {
    setTimeout(resolve, time)
  })
}

// 使用
async function test() {
  console.log('start')
  await sleep(2000)
  console.log('end')
}
test()
1
2
3
4
5
6
7
8
9
10
11
12
13

# 函数柯里化

柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术

函数柯里化定义:接收一部分参数,返回一个函数接收剩余参数,接收足够参数后,执行原函数。

实现:

const myCurried = (fn, ...args) => {
  if (args.length < fn.length) {
    // 未接受完参数
    return (..._args) => myCurried(fn, ...args, ..._args)
  } else {
    // 接受完所有参数,直接执行
    return fn(...args)
  }
}

function add(a, b, c) {
  return a + b + c
}

const curriedAdd = myCurried(add)

console.log(curriedAdd(1)(2)(3)) // 输出 6
console.log(curriedAdd(1, 2)(3)) // 输出 6
console.log(curriedAdd(1)(2, 3)) // 输出 6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 获取 URL 参数

  • 字符串分割
function getParams() {
  const params = {}
  const search = location.search.substring(1) // 去掉问号
  const pairs = search.split('&') // 按 & 分割参数

  for (let i = 0; i < pairs.length; i++) {
    const pair = pairs[i].split('=')
    const key = decodeURIComponent(pair[0]) // 解码参数名
    const value = decodeURIComponent(pair[1] || '') // 解码参数值(如果没有值,则默认为 "")
    params[key] = value // 存储为对象属性
  }

  return params
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  • 使用 URLSearchParams
const getSearchParams = () => {
  const searchPar = new URLSearchParams(window.location.search)
  const paramsObj = {}
  for (const [key, value] of searchPar.entries()) {
    paramsObj[key] = value
  }
  return paramsObj
}
1
2
3
4
5
6
7
8

# 手写 new 的执行过程

首先我们知道 new 的执行过程如

  1. 创建一个空对象
  2. 将对象的 __proto__ 指向构造函数的 prototype(将对象与构造函数通过原型链连接起来)
  3. 将这个对象作为构造函数的 this
  4. 确保返回值为对象,如果构造函数返回了一个对象,则返回该对象;否则返回步骤 1 中创建的空对象。(根据构建函数返回类型作判断,如果是原始值则被忽略,如果是返回对象,需要正常处理)
function myNew(Con, ...arg) {
  let obj = Object.create(Con.prototype)
  let result = Con.apply(obj, arg)
  return typeof result === 'object' ? result : obj
}
1
2
3
4
5

说说new操作符具体干了什么? (opens new window)

# 手写实现 Object.create()

Object.create() 是创建一个新对象并将其原型设置为指定对象的方法

function(obj){
  // 参数必须是一个对象或 null
  if (typeof obj !== "object" && typeof obj !== "function") {
    throw new TypeError("Object prototype may only be an Object or null.");
  }
  // 创建一个空的构造函数
  function F(){}
  // 将构造函数的原型指向传入的对象
  F.prototype = obj
  // 返回一个新的实例对象,该对象的原型为传入的对象
  return new F()
}
1
2
3
4
5
6
7
8
9
10
11
12

首先检查参数是否是一个对象或 null,因为只有这两种情况才能作为对象的原型。然后它创建一个空函数 F,并将其原型设置为传入的参数对象,最后返回用 F 创建的新对象,它的原型是传入的参数对象。

# 防抖

使用场景:input 搜索

function debounce(fn, delay = 500) {
  let timer = null
  return function () {
    timer && clearTimeout(timer)
    timer = setTimeout(() => {
      fn.apply(this, arguments)
      timer = null
    }, delay)
  }
}
1
2
3
4
5
6
7
8
9
10

# 节流

使用场景:scroll 函数

function throttle(fn, delay = 500) {
  let timer
  return function (...args) {
    if (timer) return
    timer = setTimeout(() => {
      fn.apply(this, args)
      timer = null
    }, delay)
  }
}

function throttle(fn, delay = 500) {
  let start = +Date.now()
  return function (...args) {
    const now = +Date.now()
    if (now - start >= delay) {
      fn.apply(this, ...args)
      start = now
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 手写深拷贝

  • 简单版
function deepClone(obj) {
  if (typeof obj !== 'object' || obj === null) {
    return obj
  }
  const newObj = Array.isArray(obj) ? [] : {}
  for (const key in obj) {
    if (Object.prototype.hasOwnProperty(key)) {
      newObj[key] = deepClone(obj[key])
    }
  }
  return newObj
}
1
2
3
4
5
6
7
8
9
10
11
12
  • 考虑更多情况
function deepClone(obj, hash = new WeakMap()) {
  if (Object(obj) !== obj) {
    return obj // 基本数据类型直接返回
  }

  if (hash.has(obj)) {
    return hash.get(obj) // 避免循环引用
  }

  let cloneObj
  const Constructor = obj.constructor

  switch (Constructor) {
    case RegExp:
      cloneObj = new Constructor(obj)
      break
    case Date:
      cloneObj = new Constructor(obj.getTime())
      break
    default:
      cloneObj = new Constructor()
  }

  hash.set(obj, cloneObj) // 存储克隆对象,用于避免循环引用

  // 遍历对象并克隆属性
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      cloneObj[key] = deepClone(obj[key], hash)
    }
  }

  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
31
32
33
34

cloneObj = new Constructor(); 这句是什么意思

# 手写 ajax

function get() {
  //创建ajax实例
  let req = new XMLHTTPRequest()
  if (req) {
    //执行open 确定要访问的链接 以及同步异步
    req.open('GET', 'http://test.com/?keywords=手机', true)
    //监听请求状态
    req.onreadystatechange = function () {
      if (req.readystate === 4) {
        if (req.statue === 200) {
          console.log('success')
        } else {
          console.log('error')
        }
      }
    }
    //发送请求
    req.send()
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 手写 Promise

要实现一个符合 Promises/A+ 规范的 Promise,说实话真挺难的,这里附一个参考手把手一行一行代码教你“手写 Promise“,完美通过 Promises/A+ 官方 872 个测试用例 (opens new window)

# 手写 instanceof

instanceof 用于判断一个对象是否是某个构造函数(或者其原型链上)的实例

function myInstanceof(obj, constructor) {
  while (obj !== null) {
    // 直到 obj === null (原型链底端)时停止循环
    if (obj.__proto__ === constructor.prototype) {
      return true // obj 的原型是构造函数的原型,它是构造函数的实例
    }
    obj = obj.__proto__ // 沿着原型链向上查找
  }
  return false // obj 不是构造函数的实例
}
1
2
3
4
5
6
7
8
9
10

# 手写 apply call

区别:传参不同,apply 第二个参数为数组,a 开头,参数也是 arrry 形式,call 后边参数为函数本身的参数,一个个传

在非严格模式下使用 call 或者 apply 时,如果第一个参数被指定为 null 或 undefined,那么函数执行时的 this 指向全局对象(浏览器环境中是 window);如果第一个参数被指定为原始值,该原始值会被包装。

  • call
/**
 *
 * 如果传入值类型,返回对应类型构造函数创建的实例
 * 如果传入对象,则返回对象本身
 * 如果传入 undefined 或者 null 会返回空对象
 */
Function.prototype._call = function (ctx, ...args) {
  if (ctx == null) ctx = globalThis
  if (typeof ctx !== 'object') ctx = new Object(ctx)
  //给context新增一个独一无二的属性以免覆盖原有属性
  const key = Symbol()
  ctx[key] = this
  // 立即执行一次
  const res = ctx[key](...args)
  // 删除这个属性,防止污染
  delete ctx[key]
  // 把函数的返回值赋值给_call的返回值
  return res
}

let name = '一尾流莺'
const obj = {
  name: 'warbler',
}
function foo() {
  console.dir(this)
  return 'success'
}

foo._call(undefined) // window
foo._call(null) // window
foo._call(1) // Number
foo._call('11') // String
foo._call(true) // Boolean
foo._call(obj) // {name: 'warbler'}
console.log(foo._call(obj)) // success
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
  • apply

apply 传参为数组形式

Function.prototype._apply = function (ctx, args = []) {
  if (ctx == null) ctx = globalThis
  if (typeof ctx !== 'object') ctx = new Object(ctx)
  //给context新增一个独一无二的属性以免覆盖原有属性
  const key = Symbol()
  ctx[key] = this
  // 立即执行一次
  const res = ctx[key](...args)
  // 删除这个属性
  delete ctx[key]
  // 把函数的返回值赋值给_apply的返回值
  return res
}
1
2
3
4
5
6
7
8
9
10
11
12
13

参考 (opens new window)

# 手写 bind

bind() 方法会创建一个新函数,当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一系列参数将会在传递的实参前传入作为它的参数。

Function.prototype.myBind = function (context, ...args1) {
  const self = this // 当前的函数本身
  return function (...args2) {
    return self.apply(context, [...args1, ...args2]) // apply 参数数组
  }
}
1
2
3
4
5
6

# 手写一个深度比较 isEqual

function isEqual(obj1, obj2) {
  //不是对象,直接返回比较结果
  if (typeof obj1 !== 'object' || typeof obj2 !== 'object') {
    return obj1 === obj2
  }
  //都是对象,且地址相同,返回true
  if (obj2 === obj1) return true
  //是对象或数组
  let keys1 = Object.keys(obj1)
  let keys2 = Object.keys(obj2)
  //比较keys的个数,若不同,肯定不相等
  if (keys1.length !== keys2.length) return false
  for (let k of keys1) {
    //递归比较键值对
    let res = isEqual(obj1[k], obj2[k])
    if (!res) return false
  }
  return true
}

const obj1 = {
  a: 100,
  b: {
    x: 100,
    y: 200,
  },
}
const obj2 = {
  a: 200,
  b: {
    x: 100,
    y: 200,
  },
}
console.log(isEqual(obj1, obj2)) //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

# 实现 jsonp

其原理是通过动态创建<script>标签,给该标签设置 src 属性,以达到跨域请求数据的目的。服务端应返回一段 JavaScript 代码,并在其中调用回调函数,将请求到的数据作为参数传入回调函数中,从而实现数据的传递。

function jsonp(url, callbackName, success) {
  const script = document.createElement('script')
  script.src = `${url}?callback=${callbackName}`
  document.body.appendChild(script)
  window['callbackName'] = (response) => {
    success(response)
    document.body.removeChild(script)
  }
}

jsonp(url, 'handleResponse', (response) => console.log(response))

// 服务端将返回响应
handleResponse({
  name: '树哥',
  age: 18,
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 使用 setTimeout 实现 setInterval

function mySetinterval(callback, interval) {
  let timeoutId = null
  function repeat() {
    callback()
    timeoutId = setTimeout(repeat, interval)
  }

  repeat()

  return {
    cancel: () => clearTimeout(timeoutId),
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

在这个函数中,我们将 timeoutId 设置为 null,并在 repeat 函数内用 setTimeout 更新它的值,以确保始终拥有最新的 ID。在返回时,我们返回一个包含 cancel() 方法的对象,该方法使用 clearTimeout 函数取消定时器。

使用:

const interval = mySetinterval(() => console.log('Hello'), 1000)

setTimeout(() => {
  interval.cancel()
}, 5000)
1
2
3
4
5

# 实现 flat

function _flat(arr, depth) {
  if (!Array.isArray(arr) || depth <= 0) {
    return arr
  }
  return arr.reduce((prev, cur) => {
    if (Array.isArray(cur)) {
      return prev.concat(_flat(cur, depth - 1))
    } else {
      return prev.concat(cur)
    }
  }, [])
}
1
2
3
4
5
6
7
8
9
10
11
12

使用:

const list = [1, 2, [3, 4, [5]], 6, 7]

console.log(_flat(list, 1)) // [1, 2, 3, 4, [5], 6, 7]

console.log(_flat(list)) // [1, 2, 3, 4, 5, 6, 7]
1
2
3
4
5

# js 实现红黄绿循环打印

  • 用 promise 实现
const task = (timer, light) =>
  new Promise((resolve, reject) => {
    setTimeout(() => {
      if (light === 'red') {
        red()
      } else if (light === 'green') {
        green()
      } else if (light === 'yellow') {
        yellow()
      }
      resolve()
    }, timer)
  })
const step = () => {
  task(1000, 'red')
    .then(() => task(2000, 'green'))
    .then(() => task(3000, 'yellow'))
    .then(step)
}
step()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  • 用 async/await 实现
const task = (light, timeout) => {
  return new Promise((resolve) => {
    setTimeout(() => resolve(console.log(light)), timeout)
  })
}

const taskRunner = async () => {
  await task('red', 1000)
  await task('green', 2000)
  await task('yellow', 3000)
  taskRunner()
}

taskRunner()
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 用 promise 如何实现异步加载图片

  1. 创建一个 Promise 对象,该对象包含一个异步操作,例如加载图片。
  2. 在异步操作中,使用 Image 对象加载图片,并监听其 onload 和 onerror 事件。
  3. 如果图片加载成功,调用 resolve 方法,并将 Image 对象作为参数传递给 resolve 方法。
  4. 如果图片加载失败,调用 reject 方法,并将错误信息作为参数传递给 reject 方法。
  5. 在使用异步操作时,调用 Promise 对象的 then 方法,如果图片加载成功,则在 then 方法中获取 Image 对象并使用它;如果图片加载失败,则在 catch 方法中处理错误。
function loadImg(url) {
  return new Promise((resolve, reject) => {
    const img = new Image()
    img.url = url
    img.onload = () => {
      resolve(img)
    }
    img.onerror = () => {
      reject(`图片加载失败-${url}`)
    }
  })
}

// 使用
loadImg('xxx.png')
  .then((res) => {
    console.log(res)
  })
  .catch((error) => {
    console.log(error)
  })
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 设计一个图片懒加载 SDK

const throttle = (fn, delay) => {
  let timer = null
  return function (...args) {
    if (!timer) {
      setTimeout(() => {
        fn.apply(this, args)
        timer = null
      }, delay)
    }
  }
}

const lazyLoadImages = () => {
  const images = document.querySelectorAll('img[data-src]')
  images.forEach((img) => {
    const rect = img.getBoundingClientRect()
    if (rect.top < window.innerHeight) {
      img.src = img.dataSet.src
      img.removeAttribute('data-src')
    }
  })
}

const throttledLazyLoad = throttle(lazyLoadImages, 100)

window.addEventListener('scroll', throttledLazyLoad)
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

使用 IntersectionObserver:

function lazyLoadImg() {
  var targets = document.querySelectorAll('img[data-src]');
  var intersectionObserver = new IntersectionObserver(function(entries) {
    entries.forEach(function(entry) {
      if (entry.intersectionRatio > 0 && entry.intersectionRatio <= 1) {
        var lazyImg = entry.target;
        var src = lazyImg.getAttribute('data-src');
        lazyImg.removeAttribute('data-src');
        lazyImg.setAttribute('src', src);
        intersectionObserver.unobserve(lazyImg);
      }
    });
  }, {
    root: null,
    rootMargin: '0px',
    threshold: [0, 0.5, 1]
  });
  targets.forEach(function(target) {
    intersectionObserver.observe(target);
  });
}

window.addEventListener('load', function () {
  lazyLoadImg();
});
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

解释:

  1. 首先获取所有需要进行图片懒加载的 img 元素,这里通过属性选择器 [data-src] 来实现。
  2. 创建一个 IntersectionObserver 实例,传入一个回调函数和一些配置参数。回调函数会在监听目标进入或离开视口时被调用。
  3. 在回调函数中判断观察目标的交叉比率是否大于 0 且小于等于 1,如果是就将真实图片地址赋值给 img 的 src 属性并移除 data-src 属性。然后取消该 img 元素的监测。
  4. 为每个需要进行图片懒加载的 img 元素添加关键方法 observe。
  5. 在页面加载完毕后,触发监听器对可视区域中的图像进行加载。需要在浏览器兼容checker中检查IntersectionObserver 与还是否被支持。

注意:必须设置图片的 data-src 属性来存储真实的图片路径,而不是直接将其放在 src 属性上,否则会破坏图片懒加载的效果。

# 实现一个函数,对一个 url 进行请求,失败就再次请求,超过最大次数就走失败回调,任何一次成功都走成功回调

/**
    @params url: 请求接口地址;
    @params body: 设置的请求体;
    @params succ: 请求成功后的回调
    @params error: 请求失败后的回调
    @params maxCount: 设置请求的数量
*/
function request(url, body, succ, error, maxCount = 5) {
    return fetch(url, body)
        .then(res => succ(res))
        .catch(err => {
            if (maxCount <= 0) return error('请求超时');
            return request(url, body, succ, error, --maxCount);
        });
}

// 调用请求函数
request('https://java.some.com/pc/reqCount', { method: 'GET', headers: {} },
    (res) => {
        console.log(res.data);
    },
    (err) => {
        console.log(err);
    })
```

## 实现单例模式

**确保一个类只有一个实例**

场景:Redux/Vuex 中的 store、JQ 的$

```js
let cache
class A {
  // ...
}

function getInstance() {
  if (cache) return cache
  return (cache = new A())
}

const x = getInstance()
const y = getInstance()
console.log(x === y) // true
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

# 观察者模式

class Subject {
  constructor() {
    this.observers = []
  }

  addObserver(observer) {
    this.observers.push(observer)
  }
  removeObserver(observer) {
    this.observers = this.observers.filter((item) => item !== observer)
  }

  notify() {
    this.observers.forEach((observer) => observer.update())
  }
}

class Observer {
  constructor(data) {
    this.data = data
  }
  update() {
    console.log('data:', this.data)
  }
}

// 创建主题对象
const subject = new Subject()

// 创建两个观察者对象
const observer1 = new Observer('Hello啊,树哥!')
const observer2 = new Observer('Hello')

// 将观察者添加到主题对象中
subject.addObserver(observer1)
subject.addObserver(observer2)

// 通知观察者
subject.notify()
// data: Hello啊,树哥!
// data: Hello
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

# 发布订阅模式

class EventEmitter {
  constructor() {
    this.events = {}
  }

  on(eventName, callback) {
    if (!this.events[eventName]) {
      this.events[eventName] = []
    }
    this.events[eventName].push(callback)
  }

  emit(eventName, ...args) {
    const callbacks = this.events[eventName] || []
    callbacks.forEach((cb) => cb.apply(this, args))
  }

  off(eventName, callback) {
    const callbacks = this.events[eventName] || []
    const index = callbacks.indexOf(callback)
    if (index !== -1) {
      callbacks.splice(index, 1)
    }
  }

  // 只监听一次事件
  once(eventName, callback) {
    // 定义一个新函数 wrapper,它接收任意数量的参数,并在调用原始回调函数后通过 off() 方法将自己从订阅者集合中移除。
    const wrapper = (...args) => {
      callback.apply(this, args)
      this.off(eventName, wrapper)
    }
    // 传入的是封装后的 wrapper 函数
    this.on(eventName, wrapper)
  }
}

const emitter = new EventEmitter()

// 订阅 'event1' 事件
emitter.on('event1', function (data) {
  console.log(`event1 is triggered with data: ${data}`)
})

// 订阅 'event2' 事件
emitter.on('event2', function () {
  console.log('event2 is triggered')
})

// 触发 'event1' 事件
emitter.emit('event1', 'hello world')

// 移除订阅的 'event1' 事件
emitter.off('event1')

// 再次触发 'event1' 事件,但不会执行任何回调函数
emitter.emit('event1', 'foo bar')

// 再次触发 'event2' 事件
emitter.emit('event2')

const emitter = new EventEmitter()

// 只监听一次 'event1' 事件
emitter.once('event1', function (data) {
  console.log(`event1 is triggered with data: ${data}`)
})

// 触发 'event1' 事件
emitter.emit('event1', 'hello world')

// 再次触发 'event1' 事件,但不会执行任何回调函数
emitter.emit('event1', 'foo bar')
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

# 实现数组的 push、filter、map 方法

// push
Array.prototype.myPush = function (...args) {
  const length = this.length
  for (let i = 0; i < args.length; i++) {
    this[this.length + i] = args[i]
  }
  return this.length
}

// filter
Array.prototype.myFilter = function (callback) {
  const newArr = []
  for (let i = 0; i < this.length; i++) {
    if (callback(this[i], i, this)) {
      newArr.push(this[i])
    }
  }
  return newArr
}

// map
Array.prototype.myMap = function (callback) {
  const newArr = []
  for (let i = 0; i < this.length; i++) {
    newArr.push(callback(this[i], i, this))
  }
  return newArr
}

const list = [1, 2, 3, 4, 5]

console.log(list.myPush(6)) // 6
console.log(list.myFilter((i) => i > 3)) //[ 4, 5, 6 ]
console.log(list.myMap((i) => i + 1)) // [ 2, 3, 4, 5, 6, 7 ]
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

高频 js 笔试题看这一篇就够了 (opens new window)

Last Updated: 6/11/2023, 8:38:36 PM