熟悉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 }