【前端面试】之JavaScript手写题篇

144 阅读10分钟

熟悉JavaScript高频功能实现以及其内部方法的实现对于前端开发来说非常重要,万变不离其宗,只要是基于JavaScript实现的框架和技术都脱离不了本尊,作为一枚前端,把JavaScript学透、学烂毫不为过。

1.手写一个JavaScript防抖函数

概念: 触发事件后,在 n 秒内函数只能执行一次,如果触发事件后在 n 秒内又触发了事件,则会重新计算函数延执行时间。

应用场景

  • 搜索框搜索输入。只需用户最后一次输入完,再发送请求;
  • 用户名、手机号、邮箱输入验证;
  • 浏览器窗口大小改变后,只需窗口调整完后,再执行 resize 事件中的代码,防止重复渲染。
<!DOCTYPE html>
<html lang="zh-cmn-Hans">
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="x-ua-compatible" content="IE=edge, chrome=1" />
    <title>debounce</title>
    <style>
      #container {
        width: 100%;
        height: 200px;
        line-height: 200px;
        text-align: center;
        color: #fff;
        background-color: #444;
        font-size: 30px;
      }
    </style>
  </head>

  <body>
    <div id="container"></div>
  </body>
  <script>
    var count = 1
    var container = document.getElementById('container')

    // 防抖函数
    function debounce(fn, wait) {
      let timer = null
      return function () {
        let context = this
        // 绑定getUserAction()中属性的this
        let args = arguments

        if (timer) {
          clearTimeout(timer)
        }
        timer = setTimeout(function () {
          fn.apply(context, args)
        }, wait)
      }
    }

    function getUserAction(e) {
      console.log(e)
      container.innerHTML = count++
    }

    container.onmousemove = debounce(getUserAction, 1000)
  </script>
</html>

参考文章:JavaScript专题之跟着underscore学防抖

2.手写一个JavaScript节流函数

概念: 如果你持续触发某个事件,特定的时间间隔内,只执行一次。

应用场景

  • 浏览器scroll事件
<!DOCTYPE html>
<html lang="zh-cmn-Hans">
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="x-ua-compatible" content="IE=edge, chrome=1" />
    <title>throttle</title>
    <style>
      #container {
        width: 100%;
        height: 200px;
        line-height: 200px;
        text-align: center;
        color: #fff;
        background-color: #444;
        font-size: 30px;
      }
    </style>
  </head>

  <body>
    <div id="container"></div>
  </body>
  <script>
    var count = 1
    var container = document.getElementById('container')

    // 节流函数实现方式1
    function throttle(fn, wait) {
      let timer = null

      return function () {
        let context = this
        let args = arguments
        if (!timer) {
          timer = setTimeout(function () {
            timer = null
            fn.apply(context, args)
          }, wait)
        }
      }
    }

    // 节流函数实现方式2
    function throttle(fn, wait) {
      let previous = 0

      return function () {
        let now = +new Date()
        let context = this
        let args = arguments
        if (now - previous > wait) {
          fn.apply(context, args)
          previous = now
        }
      }
    }

    function getUserAction(e) {
      console.log(e)
      container.innerHTML = count++
    }

    container.onmousemove = throttle(getUserAction, 1000)
  </script>
</html>

参考文章:JavaScript专题之跟着 underscore 学节流

3.手写JavaScript浅拷贝

概念:对象类型在赋值的过程中其实是复制了地址,从而会导致改变了一方其他也都被改变的情况。通常在开发中我们不希望出现这样的问题,我们可以使用浅拷贝来解决这个情况。

let a = {
  age: 1
}
let b = a
a.age = 2
console.log(b.age) // 2
// 方式1:通过Object.assign()
let a = {
  age: 1
}
// Object.assign 只会拷贝所有的属性值到新的对象中,如果属性值是对象的话,拷贝的是地址
let b = Object.assign({}, a)
a.age = 2
console.log(b.age) // 1


// 方式2:通过es6展开运算符(...)
let a = {
  age: 1
}
let b = { ...a }
a.age = 2
console.log(b.age) // 1


// 方式3:通过concat()
var arr = ['old', 1, true, null, undefined]
var new_arr = arr.concat()
new_arr[0] = 'new'
console.log(arr) //["old", 1, true, null, undefined]
console.log(new_arr) //["new", 1, true, null, undefined]


// 方式4:通过slice()
var arr = ['old', 1, true, null, undefined];
var new_arr = arr.slice();
new_arr[0] = 'new';
console.log(arr) // ["old", 1, true, null, undefined]
console.log(new_arr) // ["new", 1, true, null, undefined]

4.手写JavaScript深拷贝

概念:深拷贝就是增加一个指针,并且申请一个新的内存地址,使这个增加的指针指向这个新的内存,然后将原变量对应内存地址里的值逐个复制过去

// 方式1:通过JSON.stringify()
// 缺点:会忽略 undefined;会忽略 symbol;会忽略函数;不能序列化函数;不能解决循环引用的对象
let a = {
  age: 1,
  jobs: {
    first: 'FE'
  }
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE

// 方式2:通过递归(简单实现)
function deepClone(target) {
  if (typeof target === 'object') {
    // let cloneTarget = Array.isArray(target) ? [] : {}
    let cloneTarget = target instanceof Array ? [] : {}
    for (const key in target) {
      if (target.hasOwnProperty(key)) {
        cloneTarget[key] = deepClone(target[key])
      }
    }
    return cloneTarget
  } else {
    return target
  }
}
let b = deepClone(a)
a.jobs.first = 'native'
console.log(a)
console.log(b)

所含前置知识

let obj = {
  a: 1,
  b: 2,
  c: 3,
}
for (const key in obj) {
  console.log(key) //a b c
  console.log(obj[key]) //1 2 3
}

let arr = [1,2,3,4]
for (const key in arr) {
  console.log(key) //0 1 2 3
  console.log(arr[key]) //1 2 3 4
}

参考文章

5.手写Promise

class MyPromise {
  constructor(executor) {
    this.status = 'pending' // 初始状态为等待
    this.value = null // 成功的值
    this.reason = null // 失败的原因
    this.onFulfilledCallbacks = [] // 成功的回调函数存放的数组
    this.onRejectedCallbacks = [] // 失败的回调函数存放的数组
    let resolve = (value) => {
      if (this.status === 'pending') {
        this.status = 'fulfilled'
        this.value = value
        this.onFulfilledCallbacks.forEach((fn) => fn()) // 调用成功的回调函数
      }
    }
    let reject = (reason) => {
      if (this.status === 'pending') {
        this.status = 'rejected'
        this.reason = reason
        this.onRejectedCallbacks.forEach((fn) => fn()) // 调用失败的回调函数
      }
    }
    try {
      executor(resolve, reject)
    } catch (err) {
      reject(err)
    }
  }
  then(onFulfilled, onRejected) {
    // onFulfilled如果不是函数,则修改为函数,直接返回value
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : (value) => value
    // onRejected如果不是函数,则修改为函数,直接抛出错误
    onRejected = typeof onRejected === 'function' ? onRejected : (err) => { throw err }
    return new MyPromise((resolve, reject) => {
      if (this.status === 'fulfilled') {
        setTimeout(() => {
          try {
            let x = onFulfilled(this.value)
            x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
          } catch (err) {
            reject(err)
          }
        })
      }
      if (this.status === 'rejected') {
        setTimeout(() => {
          try {
            let x = onRejected(this.reason)
            x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
          } catch (err) {
            reject(err)
          }
        })
      }
      if (this.status === 'pending') {
        this.onFulfilledCallbacks.push(() => {
          // 将成功的回调函数放入成功数组
          setTimeout(() => {
            let x = onFulfilled(this.value)
            x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
          })
        })
        this.onRejectedCallbacks.push(() => {
          // 将失败的回调函数放入失败数组
          setTimeout(() => {
            let x = onRejected(this.reason)
            x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
          })
        })
      }
    })
  }
}

// 测试
function p1() {
  return new MyPromise((resolve, reject) => {
    setTimeout(resolve, 1000, 1)
  })
}
function p2() {
  return new MyPromise((resolve, reject) => {
    setTimeout(resolve, 1000, 2)
  })
}
p1().then((res) => {
  console.log(res) // 1
  return p2()
}).then((ret) => {
  console.log(ret) // 2
})

参考文章

6.手写ES5寄生组合继承

function Parent(name) {
  this.name = name
  this.play = [1, 2, 3];
}
Parent.prototype.eat = function () {
  console.log(this.name + ' is eating')
}

function Child(name, age) {
  Parent.call(this, name)
  this.age = age
}
Child.prototype = Object.create(Parent.prototype)
Child.prototype.contructor = Child


// 测试
let xm = new Child('xiaoming', 12)
xm.play.push(4)
console.log(xm.name) // xiaoming
console.log(xm.age) // 12
console.log(xm.play) // [1,2,3,4]
xm.eat() // xiaoming is eating
console.log(xm instanceof Child, xm instanceof Parent) // true true
console.log(xm.constructor) // f Parent(name) {}

let zs = new Child('zhangsan', 22)
console.log(zs.name) // zhangsan
console.log(zs.age) // 22
console.log(zs.play) // [1,2,3]
zs.eat() // zhangsan is eating
console.log(zs instanceof Child, zs instanceof Parent) // true true
console.log(zs.constructor) // f Parent(name) {}

参考文章

7.手写ES6继承

class Parent {
  constructor(name) {
    this.name = name
  }
  eat() {
    console.log(this.name + ' is eating')
  }
}

class Child extends Parent {
  constructor(name, age) {
    super(name)
    this.age = age
  }
}

// 测试
let xm = new Child('xiaoming', 12)
console.log(xm.name) // xiaoming
console.log(xm.age) // 12
xm.eat() // xiaoming is eating

8.手写一个异步控制并发数的方法

function limitRequest(urls = [], limit = 3) {
  return new Promise((resolve, reject) => {
    const len = urls.length
    let count = 0

    // 同时启动limit个任务
    while (limit > 0) {
      start()
      limit -= 1
    }

    function start() {
      const url = urls.shift() // 从数组中拿取第一个任务
      if (url) {
        axios.post(url).then((res) => {
          // todo
        }).catch((err) => {
          // todo
        }).finally(() => {
          if (count == len - 1) {
            // 最后一个任务完成
            resolve()
          } else {
            // 完成之后,启动下一个任务
            count++
            start()
          }
        })
      }
    }
  })
}

// 测试
limitRequest([
  'http://xxa',
  'http://xxb',
  'http://xxc',
  'http://xxd',
  'http://xxe',
])

9.实现数组去重

let arr = [1,2,3,4,5,6,6]

// 方式1
let newArr1 = [...new Set(arr)]
let newArr2 = Array.from(new Set(arr))
console.log(newArr1) // [1,2,3,4,5,6]
console.log(newArr2) // [1,2,3,4,5,6]

// 方式2
function resetArr(arr) {
  let newArr = []
  arr.forEach(item=>{
    if(newArr.indexOf(item) === -1) {
      newArr.push(item)
    }
  })
  return newArr
}
console.log(resetArr(arr)) // [1,2,3,4,5,6]

10.手写一个获取url参数的方法

function getParams(url) {
  const res = {}
  if (url.includes('?')) {
    const str = url.split('?')[1]
    const arr = str.split('&')
    console.log(arr) // ['user=%E9%98%BF%E9%A3%9E','age=16']
    arr.forEach((item) => {
      const key = item.split('=')[0]
      const val = item.split('=')[1]
      res[key] = decodeURIComponent(val) // 解码
    })
  }
  return res
}

// 测试
const user = getParams('http://www.baidu.com?user=%E9%98%BF%E9%A3%9E&age=16')
console.log(user) // { user: '阿飞', age: '16' }

11.手写一个发布订阅模式

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

  on(name, fn) {
    if (this.cache[name]) {
      this.cache[name].push(fn)
    } else {
      this.cache[name] = [fn]
    }
  }

  off(name, fn) {
    const tasks = this.cache[name]
    if (tasks) {
      const index = tasks.findIndex((f) => f === fn || f.callback === fn)
      if (index >= 0) {
        tasks.splice(index, 1)
      }
    }
  }

  emit(name, once = false) {
    if (this.cache[name]) {
      // 创建副本,如果回调函数内继续注册相同事件,会造成死循环
      const tasks = this.cache[name].slice()
      for (let fn of tasks) {
        fn()
      }
      if (once) {
        delete this.cache[name]
      }
    }
  }
}

// 测试
const eventBus = new EventEmitter()
const task1 = () => {
  console.log('task1')
}
const task2 = () => {
  console.log('task2')
}
const task3 = () => {
  console.log('task3')
}

eventBus.on('task', task1)
eventBus.on('task', task2)
eventBus.on('task', task3)
eventBus.off('task', task2)
setTimeout(() => {
  eventBus.emit('task') // task1 task3
}, 1000)

参考文章

12.手写new的实现过程

function Parent(name, age) {
  this.name = name
  this.age = age
}
let p1 = new Parent('张三',33)
console.log(p1.name) //张三
console.log(p1.age) //33


// 手写new基本实现
function myNew(fn, ...args) {
  if (typeof fn !== 'function') {
    throw 'must is not a constructor'
  }
  // (1) 创建一个新对象
  const obj = {}
  // (2) 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象)
  obj.__proto__ = fn.prototype
  // (3) 执行构造函数中的代码(为这个新对象添加属性)
  fn.apply(obj, args)
  // (4) 返回新对象
  return obj
}
let p2 = myNew(Parent, '李四', 88)
console.log(p2.name) //李四
console.log(p2.age) //88

参考文章

13.手写一个type方法能判断所有数据类型

var classType = {}

// 生成classType映射
let types = 'Boolean Number String Function Array Date RegExp Object Error'
types.split(' ').map((item) => {
  classType['[object ' + item + ']'] = item.toLowerCase()
})

function type(obj) {
  if (obj == null) {
    return obj + ''
  }
  let isObjOrFn = typeof obj === 'object' || typeof obj === 'function'
  let classTypes = classType[Object.prototype.toString.call(obj)] || 'object'
  return isObjOrFn ? classTypes: typeof obj
}

console.log(type(123)) //number
console.log(type('123')) //string
console.log(type(true)) //boolean
console.log(type([1,2,3])) //array
console.log(type(null)) //null
console.log(type(undefined)) //undefined
console.log(type({a: '123'})) //object
console.log(type(new Date())) //date

/* 
{
  '[object Boolean]': 'boolean',
  '[object Number]': 'number',
  '[object String]': 'string',
  '[object Function]': 'function',
  '[object Array]': 'array',
  '[object Date]': 'date',
  '[object RegExp]': 'regexp',
  '[object Object]': 'object',
  '[object Error]': 'error'
}
*/
console.log(classType)

参考文章:

14.手写instanceof的实现

var a = []
var b = {}
function Foo(){}
var c = new Foo()
console.log(a instanceof Array) //true
console.log(b instanceof Object) //true
console.log(c instanceof Foo) //true
console.log('-----------------')

// 手写instanceof实现
function myInstanceOf(father, child) {
  const fp = father.prototype
  var cp = child.__proto__
  while (cp) {
    if (cp === fp) {
      return true
    }
    cp = cp.__proto__
  }
  return false
}
console.log(myInstanceOf(Array, a)) //true
console.log(myInstanceOf(Object, b)) //true
console.log(myInstanceOf(Foo, c)) //true

参考文章

15.用setTimeout实现setInterval

function mySetTimout(fn, delay) {
  let timer = null
  const interval = () => {
    fn()
    timer = setTimeout(interval, delay)
  }
  setTimeout(interval, delay)
  return {
    cancel: () => {
      clearTimeout(timer)
    },
  }
}

// 测试
const { cancel } = mySetTimout(() => console.log(888), 1000)
// 4s后清除定时器
setTimeout(() => {
  cancel()
}, 4000)

16.用setInterval实现setTimeout

function mySetInterval(fn, delay) {
  const timer = setInterval(() => {
    fn()
    clearInterval(timer)
  }, delay)
}

// 测试
mySetInterval(() => console.log(888), 1000)

17.实现一个compose函数,达到以下效果

实现效果:

function fn1(x) {
  return x + 1
}
function fn2(x) {
  return x + 2
}
function fn3(x) {
  return x + 3
}
function fn4(x) {
  return x + 4
}
const a = compose(fn1, fn2, fn3, fn4)
console.log(a)
console.log(a(1)) // 1+2+3+4=11

代码:

function compose(...fn) {
  if (fn.length === 0) return (num) => num
  if (fn.length === 1) return fn[0]
  return fn.reduce((pre, next) => {
    return (num) => {
      return next(pre(num))
    }
  })
}

18.实现一个科里化函数,达到以下效果

实现效果:

const add = (a, b, c) => a + b + c
const a = currying(add, 1)
console.log(a(2, 3)) // 1 + 2 + 3=6

代码:

function currying(fn, ...args1) {
  // 获取fn参数有几个
  const length = fn.length
  let allArgs = [...args1]
  const res = (...arg2) => {
    allArgs = [...allArgs, ...arg2]
    // 长度相等就返回执行结果
    if (allArgs.length === length) {
      return fn(...allArgs)
    } else {
      // 不相等继续返回函数
      return res
    }
  }
  return res
}

19.实现一个实现JSON.parse()

const obj = {
  a: '123',
  b: [1,2,3]
}
const newObj = JSON.stringify(obj)
console.log(typeof newObj) //string
console.log(newObj) //{"a":"123","b":[1,2,3]}
console.log(typeof JSON.parse(newObj)) //object
console.log(JSON.parse(newObj)) //{ a: '123', b: [ 1, 2, 3 ] }
console.log('-------')


// 代码实现
function parse (json) {
  return eval("(" + json + ")");
}
console.log(typeof parse(newObj)) //object
console.log(parse(newObj)) //{ a: '123', b: [ 1, 2, 3 ] }

20.将DOM转化成树结构对象

场景描述:

<div>
  <span></span>
  <ul>
    <li></li>
    <li></li>
  </ul>
</div>

将上方的DOM转化为下面的树结构对象
{
  tag: 'DIV',
  children: [
    { tag: 'SPAN', children: [] },
    {
      tag: 'UL',
      children: [
        { tag: 'LI', children: [] },
        { tag: 'LI', children: [] }
      ]
    }
  ]
}

代码实现:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="box">
      <span></span>
      <ul>
        <li></li>
        <li></li>
      </ul>
    </div>

    <script>
      let boxDom = document.getElementById('box')
      console.log(boxDom)

      // 代码实现
      function dom2tree(dom) {
        const obj = {}
        obj.tag = dom.tagName
        obj.children = []
        dom.childNodes.forEach((child) => {
          // 过滤空白节点
          if(child.nodeType !==3) {
            return obj.children.push(dom2tree(child))
          }
        })
        return obj
      }
      console.log(dom2tree(boxDom))
    </script>
  </body>
</html>

21.将树结构对象转换成DOM

let obj = {
  tag: 'DIV',
  children: [
    { tag: 'SPAN', children: [] },
    {
      tag: 'UL',
      children: [
        { tag: 'LI', children: [] },
        { tag: 'LI', children: [] },
      ],
    },
  ],
}

// 真正的渲染函数
function _render(vnode) {
  // 如果是数字类型转化为字符串
  if (typeof vnode === 'number') {
    vnode = String(vnode)
  }
  // 字符串类型直接就是文本节点
  if (typeof vnode === 'string') {
    return document.createTextNode(vnode)
  }
  // 普通DOM
  const dom = document.createElement(vnode.tag)
  if (vnode.attrs) {
    // 遍历属性
    Object.keys(vnode.attrs).forEach((key) => {
      const value = vnode.attrs[key]
      dom.setAttribute(key, value)
    })
  }
  // 子数组进行递归操作
  vnode.children.forEach((child) => {
    return dom.appendChild(_render(child))
  })
  return dom
}

console.log(_render(obj))

22.判断一个对象有环引用

实现思路:用一个数组存储每一个遍历过的对象,下次找到数组中存在,则说明环引用

var obj = {
  a: {
    c: [1, 2],
  },
  b: 1,
}
obj.a.c.d = obj

// 代码实现
function cycleDetector(obj) {
  const arr = [obj]
  let flag = false
  function cycle(o) {
    const keys = Object.keys(o)
    for (const key of keys) {
      const temp = o[key]
      if (typeof temp === 'object' && temp !== null) {
        if (arr.indexOf(temp) >= 0) {
          flag = true
          return
        }
        arr.push(temp)
        cycle(temp)
      }
    }
  }
  cycle(obj)

  return flag
}
console.log(cycleDetector(obj)) // true

23.计算一个对象的层数

const obj = {
  a: { b: [1] },
  c: { d: { e: { f: 1 } } },
}

function loopGetLevel(obj) {
  var res = 1
  function computedLevel(obj, level) {
    var level = level ? level : 0
    if (typeof obj === 'object') {
      for (var key in obj) {
        if (typeof obj[key] === 'object') {
          computedLevel(obj[key], level + 1)
        } else {
          res = level + 1 > res ? level + 1 : res
        }
      }
    } else {
      res = level > res ? level : res
    }
  }
  computedLevel(obj)

  return res
}

console.log(loopGetLevel(obj)) // 4

24.实现对象的扁平化

const obj = {
  a: {
    b: 1,
    c: 2,
    d: { e: 5 },
  },
  b: [1, 3, { a: 2, b: 3 }],
  c: 3,
}

//  代码实现
const isObject = (val) => typeof val === 'object' && val !== null

function flatten(obj) {
  if (!isObject(obj)) return
  const res = {}
  const dfs = (cur, prefix) => {
    if (isObject(cur)) {
      if (Array.isArray(cur)) {
        cur.forEach((item, index) => {
          dfs(item, `${prefix}[${index}]`)
        })
      } else {
        for (let key in cur) {
          dfs(cur[key], `${prefix}${prefix ? '.' : ''}${key}`)
        }
      }
    } else {
      res[prefix] = cur
    }
  }
  dfs(obj, '')

  return res
}

/* 
{
  'a.b': 1,
  'a.c': 2,
  'a.d.e': 5,
  'b[0]': 1,
  'b[1]': 3,
  'b[2].a': 2,
  'b[2].b': 3,
  c: 3
}
*/
console.log(flatten(obj))

25.实现数组的扁平化

const testArray = [1, [2, [3, [4, [5, [6, [7, [[[[[[8, ['ha']]]]]]]]]]]]]]
// 方式1:使用Array.flat()
const result1 = testArray.flat(Infinity)
console.log(result1) // [ 1, 2, 3, 4, 5, 6, 7, 8, 'ha']


// 方式2:递归
function myFlatten1(arr) {
  let result = []
  arr.forEach((item) => {
    if (Array.isArray(item)) {
      // 是数组的话,递归调用
      result = result.concat(myFlatten1(item))
    } else {
      // 不是数组的话push
      result.push(item)
    }
  })
  return result
}
const result2 = myFlatten1(testArray)
console.log(result2) // [ 1, 2, 3, 4, 5, 6, 7, 8, 'ha']


// 方式3:使用reduce
function myFlatten2(arr) {
  return arr.reduce((prev, curv) => {
    return prev.concat(Array.isArray(curv) ? myFlatten2(curv) : curv)
  }, [])
}
const result3 = myFlatten2(testArray)
console.log(result3) // [ 1, 2, 3, 4, 5, 6, 7, 8, 'ha']


// 方式4:扩展运算符和Array.some()
function myFlatten3(arr) {
  while (arr.some((item) => Array.isArray(item))) {
    arr = [].concat(...arr)
  }
  return arr
}
const result4 = myFlatten3(testArray)
console.log(result4) // [ 1, 2, 3, 4, 5, 6, 7, 8, 'ha']

26.手写事件冒泡

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title></title>
    <style>
      ul li {
        height: 40px;
        line-height: 40px;
        width: 100px;
        color: #fff;
        background-color: burlywood;
        text-align: center;
        margin-bottom: 10px;
        border-radius: 8px;
      }
      li:hover {
        cursor: pointer;
      }
    </style>
  </head>
  <body>
    <ul id="ul-test">
      <li>0</li>
      <li>1</li>
      <li>2</li>
      <li>3</li>
    </ul>
  </body>
  <script type="text/javascript">
    var oUl = document.getElementById('ul-test')
    oUl.addEventListener('click', function (ev) {
      var ev = ev || window.event
      var target = ev.target || ev.srcElement
      //如果点击的最底层是li元素
      if (target.tagName.toLowerCase() === 'li') {
        alert(target.innerHTML)
      }
    })
  </script>
</html>

27.手写统计数组内部元素的个数

const arr = ['a', 1, 1, 2, 2, 2, 'b', 'a', 'a']
const count = arr.reduce((result, value) => {
  result[value] = result[value] ? ++result[value] : 1
  console.log(result)
  return result
}, {})
console.log(count) //{ '1': 2, '2': 3, a: 3, b: 1 }

未完...待续,持续更新