手写代码系列
复杂版深克隆:基于简单版的基础上,还考虑了内置对象比如 Date、RegExp 等对象和函数以及解决了循环引用的问题。
const isObject = (target) => (typeof target === "object" || typeof target === "function") && target !== null;
function deepClone(target, map = new WeakMap()) {
if (map.get(target)) {
return target;
}
// 获取当前值的构造函数:获取它的类型
let constructor = target.constructor;
// 检测当前对象target是否与正则、日期格式对象匹配
if (/^(RegExp|Date)$/i.test(constructor.name)) {
// 创建一个新的特殊对象(正则类/日期类)的实例
return new constructor(target);
}
if (isObject(target)) {
map.set(target, true); // 为循环引用的对象做标记
const cloneTarget = Array.isArray(target) ? [] : {};
for (let prop in target) {
if (target.hasOwnProperty(prop)) {
cloneTarget[prop] = deepClone(target[prop], map);
}
}
return cloneTarget;
} else {
return target;
}
}
1、防抖与节流
1、函数防抖是指在事件被触发 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则重新计时。 这可以使用在一些点击请求或者input输入的事件上,避免因为用户的多次点击向后端发送多次请求。
function debounce(fn) {
let timeout = null
return function(arguments) {
// 重新计时
clearTimeout(timeOut)
timeout = setTimeout(() => {
fn.apply(this, arguments)
}, 500)
}
}
function successFn(val) {console.log(val)}
let test = debounce(successFn)
inputEl.addEventListener('input', function(e) {
test(e.target.value)
}, false)
2、函数节流是指规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如 果在同一个单位时间内某事件被触发多次,只有一次能生效。节流可以使用在 scroll 函数或者窗口缩放的 事件监听上,通过事件节流来降低事件调用的频率。
function throttle() {
// 创建一个闭包标记
let flag = true
return function() {
if(!flag) {
return false
}
flag = false
setTimeout(() => {
fn.apply(this, arguments)
flag = true
}, 500)
}
}
function throtten(fn, delay) {
let preTime = Date.now()
return function(arguments) {
let nowTime = Date.now()
let context = this
if(nowTime - preTime > delay) {
return fn.apply(context, arguments)
preTime = nowTime
}
}
}
function sF() {alert('节流成功')}
window.addEventListener('resize', throttle(sF), false)
2、程序效果,停顿指定的时间,例:5秒执行5次
function sleep(timeout) {
return new Promise((resolve, reject) => {
setTimeout(resolve, timeout)
})
}
async function fn() {
for(let i = 0; i < 5; i++) {
console.log(i)
await sleep(1000)
}
}
fn()
3、手写new
function F(name) {
this.name = name
}
F.prototype.sayName = function() {
console.log(this.name)
}
function _new(fn, ...args) {
let obj = Object.create(fn.prorotype)
// 修改this指向
let o = fn.apply(obj, args)
// 判断为null 或者 undefined 返回obj 否者新对象
return o instanceof Object ? o : obj
}
1、创建空对象obj = {}
2、修改obj.__proto__( obj.constructor.prototype) = fn.prototype
3、修改this指向并传参
4、返回null和undefined 不处理
4、模拟实现一个Promise.finally
Promise符合A+规范源码,以及Promise所有相关API源码
/**
**@description 原型方法finally
**@param {*} 回调函数
**/
Promise.prototype.finally = function(callback) {
// Promise.resolve(callback()) 处理回调函数是promise的情况
return this.then(res => {
// 成功了继续向下传递成功
return Promise.resolve(callback()).then(() => res)
}, err => {
// 失败了继续向下抛出错误
return Promise.resolve(callback()).then(() => {throw err})
})
}
5、使用 JavaScript Proxy 实现简单的数据绑定
<input type="text" id="input" />
<div id="show">展示输入内容</div>
<button id="button">添加todoList</button>
list内容:
<ul id="ul"></ul>
const input = document.getElementById('input')
const showText = document.getElementById('show')
const button = document.getElementById('button')
const ul = document.getElementById('ul')
const inputObj = new Proxy({}, {
get(target, key, value, receiver) {
return Reflect.get(target, key, value, receiver)
},
set(target, key, value, receiver) {
if(key === 'text') {
input.value = value
show.innerHTML = value
}
return Reflect.set(target, key, value, receiver)
}
})
class Render{
constructor(arr) {
this.arr = arr
}
init() {
const fragment = document.createDocumentFragment()
let len = this.arr.length
for(let i = 0; i < len; i++) {
let li = document.createElement('li')
li.textContent = this.arr[i]
fragment.appendChild(li)
}
ul.appendChild(fragment)
}
addList(text) {
let li = document.createElement('li')
li.textContent = text
ul.appendChild(li)
}
}
const todoList = new Proxy([], {
get(target, key, value, receiver) {
return Reflect.get(target, key, value, receiver)
},
set(target, key, value, receiver) {
if(key !== 'length') {
render.addList(value)
}
return Reflect.set(target, key, value, receiver)
}
})
render = new Render([])
render.init()
input.addEventListener('keyup', function(e) {
let value = e.target.value
inputObj.text = value
}, false)
button.addEventListener('click', function() {
todoList.push(inputObj.text)
inputObj.text = ''
})
6、在输入框中如何判断输入的是一个正确的网址。
function isUrl(url) {
const a = document.createElement('a')
a.href = url
return (
[
/^(http|https):$/.test(a.protocal),
a.host,
a.pathname !== url,
a.pathname !== `/${url}`
].find(item => !item) === undefined
)
}
7、实现 convert 方法,把原始 list 转换成树形结构
要求尽可能降低时间复杂度
以下数据结构中,id 代表部门编号,name 是部门名称,parentId 是父部门编
原始list
const list = [
{ id: 1, name: '部门 A', parentId: 0 },
{ id: 2, name: '部门 B', parentId: 0 },
{ id: 3, name: '部门 C', parentId: 1 },
{ id: 4, name: '部门 D', parentId: 1 },
{ id: 5, name: '部门 E', parentId: 2 },
{ id: 6, name: '部门 F', parentId: 3 },
{ id: 7, name: '部门 G', parentId: 2 },
{ id: 8, name: '部门 H', parentId: 4 }
]
转换后的
let result = [
{
id: 1,
name: '部门 A',
parentId: 0,
children: [
{
id: 3,
name: '部门 C',
parentId: 1,
children: [
{
id: 6,
name: '部门 F',
parentId: 3
},
{
id: 16,
name: '部门 L',
parentId: 3
}
]
},
{
id: 4,
name: '部门 D',
parentId: 1,
children: [
{
id: 8,
name: '部门 H',
parentId: 4
}
]
}
]
转换函数
function convert(list) {
const res = []
const map = list.reduce((res, cur) => {
res[cur.id] = cur;
return res
}, {})
for(let item of list) {
if(item.parentId == 0) {
res.push(item)
continue
}
if(item.parentId in map) {
const parent = map[item.parentId]
parent.children = parent.children || []
parent.children.push(item)
}
}
return res
}
/**
* 数组转树形结构
* @param list 源数组
* @param tree 树
* @param parentId 父ID
*/
const listToTree = (list, tree, parentId) => {
list.forEach(item => {
// 判断是否为父级菜单
if (item.parentId === parentId) {
const child = {
...item,
key: item.key || item.name,
children: []
}
// 迭代 list, 找到当前菜单相符合的所有子菜单
listToTree(list, child.children, item.id)
// 删掉不存在 children 值的属性
if (child.children.length <= 0) {
delete child.children
}
// 加入到树中
tree.push(child)
}
})
}
写一个通用的事件侦听函数
const UntilEvent = {
// 添加事件
addEvent(el, type, fn) {
if(el.addEventListener) {
el.addEventListener(type, fn, false)
} else if(el.attachEvent) {
el.attachEvent('on'+type, fn)
} else {
el['on' + type] = fn
}
},
// 移除事件
removeEvent(el, type, fn) {
if(el.removeEventListener) {
el.removeEventListener(type, fn, false)
} else if(el.detachEvent) {
el.detachEvent('on'+type, fn)
} else {
el['on'+type] = fn
}
},
// 获取事件目标
getTarget(event) {
return event.target || event.srcElement
},
// 获取event对象的引用, 取到事件的所有信息,确保随时能用到event
getEvent(event) {
return event || window.event
},
// 阻止冒泡
stopPropagation(event) {
if(event.stopPropagation) {
event.stopPagation()
} else {
event.cancelBubble = false
}
},
// 取消事假的默认行为
preventDefault(event) {
if(event.preventDefault) {
event.preventDefault()
} else {
event.returnValue = false
}
}
}
设计实现promise.race
Promise.myrace = function (iterator) {
return new Promise((resolve, reject) => {
try {
let it = iterator[Symbol.iterator]()
while(true) {
let res = it.next()
if(res.done) {
break
}
if(res.value instanceof Promise) {
res.value.then(resolve, reject)
} else {
resolve(res.value)
}
}
} catch (error) {
reject(error)
}
})
}
Promise._race = (arr) => {
return new Promise((resolve, reject) => {
arr.forEach(promise => {
promise.then(resolve, reject)
})
})
}
给定一组 url 实现并发请求
1、使用promiseAll
const urls = ['./1.json', './2.json', './3.json'];
function getData(url) {
// 返回一个 Promise 利用 Promise.all 接受
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.responseType = 'json';
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
resolve(xhr.response);
}
}
};
xhr.open('GET', url, true);
xhr.send(null);
});
}
function getMultiData(urls) {
// Promise.all 接受一个包含 promise 的数组,如果不是 promise 数组会被转成 promise
Promise.all(urls.map(url => getData(url))).then(results => {
console.log(results);
});
}
2、不使用promiseAll
我们可以写一个方法,加个回调函数,等数据全部回来之后,触发回调函数传入得到的数据,那么数据全部回来的就是我们要考虑的核心问题,我们可以用个数组或者对象,然后判断一下数组的 length 和传入的 url 的长度是否一样来做判断
const urls = ['./1.json', './2.json', './3.json'];
function getAllDate(urls, cd) {
const result = {};
function getData(url, idx) {
const xhr = new XMLHttpRequest();
xhr.responseType = 'json';
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
result[idx] = xhr.response;
// 如果两者 length 相等说明都请求完成了
if (Object.keys(result).length === urls.length) {
// 给对象添加length属性,方便转换数组
result.length = urls.length;
cd && cd(Array.from(result));
}
}
}
};
}
// 触发函数执行
urls.forEach((url, idx) => getData(url, idx));
}
// 使用
getAllDate(urls, data => {
console.log(data);
});
function getGroupData(urls, cb) {
const results = [];
let count = 0;
const getData = url => {
const xhr = new XMLHttpRequest();
xhr.responseType = 'json';
xhr.onreadystatechange = _ => {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
results.push(xhr.response);
if (++count === urls.length) {
cb && cb(results);
}
}
}
};
xhr.open('GET', url, true);
xhr.send(null);
};
urls.forEach(url => getData(url));
}
getGroupData(urls, data => {
console.log(data);
});
手写promise.all
Promise.prototype.all = function(promiseList) {
return new Promise((resolve, reject) => {
if(promiseList.length === 0) {
return resolve([])
}
let result = [],
count = 0;
promiseList.forEach((promise, index) => {
Promise.resolve(promise).then(value => {
result[index] = value
if(++index === promiseList) {
resolve(result)
}
})
})
}, reason => reject(reason))
手写call,apply, bind
bind方法
JavaScript深入之bind的模拟实现 · Issue #12 · mqyqingfeng/Blog · GitHub
// 第三版
Function.prototype.bind2 = function (context) {
var self = this;
var args = Array.prototype.slice.call(arguments, 1);
var fBound = function () {
var bindArgs = Array.prototype.slice.call(arguments);
// 当作为构造函数时,this 指向实例,此时结果为 true,将绑定函数的 this 指向该实例,可以让实例获得来自绑定函数的值
// 以上面的是 demo 为例,如果改成 `this instanceof fBound ? null : context`,实例只是一个空对象,将 null 改成 this ,实例会具有 habit 属性
// 当作为普通函数时,this 指向 window,此时结果为 false,将绑定函数的 this 指向 context
return self.apply(this instanceof fBound ? this : context, args.concat(bindArgs));
}
// 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值
fBound.prototype = this.prototype;
return fBound;
}
但是在这个写法中,我们直接将 fBound.prototype = this.prototype,我们直接修改 fBound.prototype 的时候,也会直接修改绑定函数的 prototype。这个时候,我们可以通过一个空函数来进行中转:
// 第四版
Function.prototype.bind2 = function (context) {
var self = this;
var args = Array.prototype.slice.call(arguments, 1);
var fNOP = function () {};
var fBound = function () {
var bindArgs = Array.prototype.slice.call(arguments);
return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
}
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
}
call, apply
JavaScript深入之call和apply的模拟实现 · Issue #11 · mqyqingfeng/Blog · GitHub
Function.prototype.call2 = function (context) {
var context = context || window;
context.fn = this;
var args = [];
for(var i = 1, len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
}
var result = eval('context.fn(' + args +')');
delete context.fn
return result;
}
Function.prototype.apply = function (context, arr) {
var context = Object(context) || window;
context.fn = this;
var result;
if (!arr) {
result = context.fn();
}
else {
var args = [];
for (var i = 0, len = arr.length; i < len; i++) {
args.push('arr[' + i + ']');
}
result = eval('context.fn(' + args + ')')
}
delete context.fn
return result;
}
发布订阅事件
class EventEmitter {
constructor() {
this.events = {}
}
on(type, cb) {
const events = this.events[type] || []
events.push(cb)
this.events[type] = events
return this
}
off(type, cb) {
const events = this.events[type]
this.events[type] = events && events.filter(callback => callback !== cb)
return this
}
emit(type, ...args) {
const events = this.events[type]
this.events[type].forEach(cb => cb(...args))
return this
}
once(type, cb) {
const that = this
let wrapFn = function(...args) {
cb(...args)
that.off(type, fn)
}
this.on(type, wrapFn)
return this;
}
}