手写 Promise
const PENDING = "pending";
const RESOLVED = "resolved";
const REJECTED = "rejected";
function MyPromise(fn) {
// 保留初始化状态
var self = this;
// 初始化状态
this.state = PENDING;
// 用于保留 resolve 或者 rejected 传入的值
this.value = null;
// 用于保留 resolve 的回调函数
this.resolvedCallbacks = [];
// 用于保留 reject 的回调函数
this.rejectedCallbacks = [];
// 状态转变为 resolved 办法
function resolve(value) {
// 判断传入元素是否为 Promise 值,如果是,则状态扭转必须期待前一个状态扭转后再进行扭转
if (value instanceof MyPromise) {return value.then(resolve, reject);
}
// 保障代码的执行程序为本轮事件循环的开端
setTimeout(() => {
// 只有状态为 pending 时能力转变,if (self.state === PENDING) {
// 批改状态
self.state = RESOLVED;
// 设置传入的值
self.value = value;
// 执行回调函数
self.resolvedCallbacks.forEach(callback => {callback(value);
});
}
}, 0);
}
// 状态转变为 rejected 办法
function reject(value) {
// 保障代码的执行程序为本轮事件循环的开端
setTimeout(() => {
// 只有状态为 pending 时能力转变
if (self.state === PENDING) {
// 批改状态
self.state = REJECTED;
// 设置传入的值
self.value = value;
// 执行回调函数
self.rejectedCallbacks.forEach(callback => {callback(value);
});
}
}, 0);
}
// 将两个办法传入函数执行
try {fn(resolve, reject);
} catch (e) {
// 遇到谬误时,捕捉谬误,执行 reject 函数
reject(e);
}
}
MyPromise.prototype.then = function(onResolved, onRejected) {
// 首先判断两个参数是否为函数类型,因为这两个参数是可选参数
onResolved =
typeof onResolved === "function"
? onResolved
: function(value) {return value;};
onRejected =
typeof onRejected === "function"
? onRejected
: function(error) {throw error;};
// 如果是期待状态,则将函数退出对应列表中
if (this.state === PENDING) {this.resolvedCallbacks.push(onResolved);
this.rejectedCallbacks.push(onRejected);
}
// 如果状态曾经凝固,则间接执行对应状态的函数
if (this.state === RESOLVED) {onResolved(this.value);
}
if (this.state === REJECTED) {onRejected(this.value);
}
};
手写 call 函数
call 函数的实现步骤:
- 判断调用对象是否为函数,即便咱们是定义在函数的原型上的,然而可能呈现应用 call 等形式调用的状况。
- 判断传入上下文对象是否存在,如果不存在,则设置为 window。
- 解决传入的参数,截取第一个参数后的所有参数。
- 将函数作为上下文对象的一个属性。
- 应用上下文对象来调用这个办法,并保留返回后果。
- 删除方才新增的属性。
- 返回后果。
// call 函数实现
Function.prototype.myCall = function(context) {
// 判断调用对象
if (typeof this !== "function") {console.error("type error");
}
// 获取参数
let args = [...arguments].slice(1),
result = null;
// 判断 context 是否传入,如果未传入则设置为 window
context = context || window;
// 将调用函数设为对象的办法
context.fn = this;
// 调用函数
result = context.fn(...args);
// 将属性删除
delete context.fn;
return result;
};
实现节流函数(throttle)
节流函数原理: 指频繁触发事件时,只会在指定的时间段内执行事件回调,即触发事件间隔大于等于指定的工夫才会执行回调函数。总结起来就是:事件,依照一段时间的距离来进行触发。
像 dom 的拖拽,如果用消抖的话,就会呈现卡顿的感觉,因为只在进行的时候执行了一次,这个时候就应该用节流,在肯定工夫内屡次执行,会晦涩很多
手写简版
应用工夫戳的节流函数会在第一次触发事件时立刻执行,当前每过 wait 秒之后才执行一次,并且最初一次触发事件不会被执行
工夫戳形式:
// func 是用户传入须要防抖的函数
// wait 是等待时间
const throttle = (func, wait = 50) => {
// 上一次执行该函数的工夫
let lastTime = 0
return function(...args) {
// 以后工夫
let now = +new Date()
// 将以后工夫和上一次执行函数工夫比照
// 如果差值大于设置的等待时间就执行函数
if (now - lastTime > wait) {
lastTime = now
func.apply(this, args)
}
}
}
setInterval(throttle(() => {console.log(1)
}, 500),
1
)
定时器形式:
应用定时器的节流函数在第一次触发时不会执行,而是在 delay 秒之后才执行,当最初一次进行触发后,还会再执行一次函数
function throttle(func, delay){
var timer = null;
returnfunction(){
var context = this;
var args = arguments;
if(!timer){timer = setTimeout(function(){func.apply(context, args);
timer = null;
},delay);
}
}
}
实用场景:
DOM
元素的拖拽性能实现(mousemove
)- 搜寻联想(
keyup
) - 计算鼠标挪动的间隔(
mousemove
) Canvas
模仿画板性能(mousemove
)- 监听滚动事件判断是否到页面底部主动加载更多
- 拖拽场景:固定工夫内只执行一次,避免超高频次触发地位变动
- 缩放场景:监控浏览器
resize
- 动画场景:防止短时间内屡次触发动画引起性能问题
总结
- 函数防抖:将几次操作合并为一次操作进行。原理是保护一个计时器,规定在 delay 工夫后触发函数,然而在 delay 工夫内再次触发的话,就会勾销之前的计时器而从新设置。这样一来,只有最初一次操作能被触发。
- 函数节流:使得肯定工夫内只触发一次函数。原理是通过判断是否达到肯定工夫来触发函数。
Array.prototype.forEach()
Array.prototype.forEach = function(callback, thisArg) {if (this == null) {throw new TypeError('this is null or not defined');
}
if (typeof callback !== "function") {throw new TypeError(callback + 'is not a function');
}
const O = Object(this);
const len = O.length >>> 0;
let k = 0;
while (k < len) {if (k in O) {callback.call(thisArg, O[k], k, O);
}
k++;
}
}
Promise.race
Promise.race = function(promiseArr) {return new Promise((resolve, reject) => {
promiseArr.forEach(p => {
// 如果不是 Promise 实例须要转化为 Promise 实例
Promise.resolve(p).then(val => resolve(val),
err => reject(err),
)
})
})
}
实现非负大整数相加
JavaScript 对数值有范畴的限度,限度如下:
Number.MAX_VALUE // 1.7976931348623157e+308
Number.MAX_SAFE_INTEGER // 9007199254740991
Number.MIN_VALUE // 5e-324
Number.MIN_SAFE_INTEGER // -9007199254740991
如果想要对一个超大的整数 (> Number.MAX_SAFE_INTEGER
) 进行加法运算,然而又想输入个别模式,那么应用 + 是无奈达到的,一旦数字超过 Number.MAX_SAFE_INTEGER
数字会被立刻转换为迷信计数法,并且数字精度相比以前将会有误差。
实现一个算法进行大数的相加:
function sumBigNumber(a, b) {
let res = '';
let temp = 0;
a = a.split('');
b = b.split('');
while (a.length || b.length || temp) {temp += ~~a.pop() + ~~b.pop();
res = (temp % 10) + res;
temp = temp > 9
}
return res.replace(/^0+/, '');
}
其次要的思路如下:
- 首先用字符串的形式来保留大数,这样数字在数学示意上就不会发生变化
- 初始化 res,temp 来保留两头的计算结果,并将两个字符串转化为数组,以便进行每一位的加法运算
- 将两个数组的对应的位进行相加,两个数相加的后果可能大于 10,所以可能要仅为,对 10 进行取余操作,将后果保留在以后位
- 判断以后位是否大于 9,也就是是否会进位,若是则将 temp 赋值为 true,因为在加法运算中,true 会主动隐式转化为 1,以便于下一次相加
- 反复上述操作,直至计算完结
参考 前端进阶面试题具体解答
JSONP
script 标签不遵循同源协定,能够用来进行 跨域申请,长处就是兼容性好但仅限于 GET 申请
const jsonp = ({url, params, callbackName}) => {const generateUrl = () => {
let dataSrc = '';
for (let key in params) {if (Object.prototype.hasOwnProperty.call(params, key)) {dataSrc += `${key}=${params[key]}&`;
}
}
dataSrc += `callback=${callbackName}`;
return `${url}?${dataSrc}`;
}
return new Promise((resolve, reject) => {const scriptEle = document.createElement('script');
scriptEle.src = generateUrl();
document.body.appendChild(scriptEle);
window[callbackName] = data => {resolve(data);
document.removeChild(scriptEle);
}
})
}
实现一个 call
call 做了什么:
- 将函数设为对象的属性
- 执行 & 删除这个函数
- 指定 this 到函数并传入给定参数执行函数
- 如果不传入参数,默认指向为 window
// 模仿 call bar.mycall(null);
// 实现一个 call 办法:Function.prototype.myCall = function(context) {
// 此处没有思考 context 非 object 状况
context.fn = this;
let args = [];
for (let i = 1, len = arguments.length; i < len; i++) {args.push(arguments[i]);
}
context.fn(...args);
let result = context.fn(...args);
delete context.fn;
return result;
};
应用 Promise 封装 AJAX 申请
// promise 封装实现:function getJSON(url) {
// 创立一个 promise 对象
let promise = new Promise(function(resolve, reject) {let xhr = new XMLHttpRequest();
// 新建一个 http 申请
xhr.open("GET", url, true);
// 设置状态的监听函数
xhr.onreadystatechange = function() {if (this.readyState !== 4) return;
// 当申请胜利或失败时,扭转 promise 的状态
if (this.status === 200) {resolve(this.response);
} else {reject(new Error(this.statusText));
}
};
// 设置谬误监听函数
xhr.onerror = function() {reject(new Error(this.statusText));
};
// 设置响应的数据类型
xhr.responseType = "json";
// 设置申请头信息
xhr.setRequestHeader("Accept", "application/json");
// 发送 http 申请
xhr.send(null);
});
return promise;
}
实现一个治理本地缓存过期的函数
封装一个能够设置过期工夫的
localStorage
存储函数
class Storage{constructor(name){this.name = 'storage';}
// 设置缓存
setItem(params){
let obj = {
name:'', // 存入数据 属性
value:'',// 属性值
expires:"", // 过期工夫
startTime:new Date().getTime()// 记录何时将值存入缓存,毫秒级
}
let options = {};
// 将 obj 和传进来的 params 合并
Object.assign(options,obj,params);
if(options.expires){
// 如果 options.expires 设置了的话
// 以 options.name 为 key,options 为值放进去
localStorage.setItem(options.name,JSON.stringify(options));
}else{
// 如果 options.expires 没有设置,就判断一下 value 的类型
let type = Object.prototype.toString.call(options.value);
// 如果 value 是对象或者数组对象的类型,就先用 JSON.stringify 转一下,再存进去
if(Object.prototype.toString.call(options.value) == '[object Object]'){options.value = JSON.stringify(options.value);
}
if(Object.prototype.toString.call(options.value) == '[object Array]'){options.value = JSON.stringify(options.value);
}
localStorage.setItem(options.name,options.value);
}
}
// 拿到缓存
getItem(name){let item = localStorage.getItem(name);
// 先将拿到的试着进行 json 转为对象的模式
try{item = JSON.parse(item);
}catch(error){
// 如果不行就不是 json 的字符串,就间接返回
item = item;
}
// 如果有 startTime 的值,阐明设置了生效工夫
if(item.startTime){let date = new Date().getTime();
// 何时将值取出减去刚存入的工夫,与 item.expires 比拟,如果大于就是过期了,如果小于或等于就还没过期
if(date - item.startTime > item.expires){
// 缓存过期,革除缓存,返回 false
localStorage.removeItem(name);
return false;
}else{
// 缓存未过期,返回值
return item.value;
}
}else{
// 如果没有设置生效工夫,间接返回值
return item;
}
}
// 移出缓存
removeItem(name){localStorage.removeItem(name);
}
// 移出全副缓存
clear(){localStorage.clear();
}
}
用法
let storage = new Storage();
storage.setItem({
name:"name",
value:"ppp"
})
上面我把值取出来
let value = storage.getItem('name');
console.log('我是 value',value);
设置 5 秒过期
let storage = new Storage();
storage.setItem({
name:"name",
value:"ppp",
expires: 5000
})
// 过期后再取出来会变为 false
let value = storage.getItem('name');
console.log('我是 value',value);
实现一个 JS 函数柯里化
事后解决的思维,利用闭包的机制
- 柯里化的定义:接管一部分参数,返回一个函数接管残余参数,接管足够参数后,执行原函数
- 函数柯里化的次要作用和特点就是
参数复用
、提前返回
和提早执行
- 柯里化把屡次传入的参数合并,柯里化是一个高阶函数
- 每次都返回一个新函数
- 每次入参都是一个
当柯里化函数接管到足够参数后,就会执行原函数,如何去确定何时达到足够的参数呢?
有两种思路:
- 通过函数的
length
属性,获取函数的形参个数,形参的个数就是所需的参数个数 - 在调用柯里化工具函数时,手动指定所需的参数个数
将这两点联合一下,实现一个简略 curry
函数
通用版
// 写法 1
function curry(fn, args) {
var length = fn.length;
var args = args || [];
return function(){newArgs = args.concat(Array.prototype.slice.call(arguments));
if (newArgs.length < length) {return curry.call(this,fn,newArgs);
}else{return fn.apply(this,newArgs);
}
}
}
// 写法 2
// 分批传入参数
// redux 源码的 compose 也是用了相似柯里化的操作
const curry = (fn, arr = []) => {// arr 就是咱们要收集每次调用时传入的参数
let len = fn.length; // 函数的长度,就是参数的个数
return function(...args) {let newArgs = [...arr, ...args] // 收集每次传入的参数
// 如果传入的参数个数等于咱们指定的函数参数个数,就执行指定的真正函数
if(newArgs.length === len) {return fn(...newArgs)
} else {
// 递归收集参数
return curry(fn, newArgs)
}
}
}
// 测试
function multiFn(a, b, c) {return a * b * c;}
var multi = curry(multiFn);
multi(2)(3)(4);
multi(2,3,4);
multi(2)(3,4);
multi(2,3)(4)
ES6 写法
const curry = (fn, arr = []) => (...args) => (
arg => arg.length === fn.length
? fn(...arg)
: curry(fn, arg)
)([...arr, ...args])
// 测试
let curryTest=curry((a,b,c,d)=>a+b+c+d)
curryTest(1,2,3)(4) // 返回 10
curryTest(1,2)(4)(3) // 返回 10
curryTest(1,2)(3,4) // 返回 10
// 柯里化求值
// 指定的函数
function sum(a,b,c,d,e) {return a + b + c + d + e}
// 传入指定的函数,执行一次
let newSum = curry(sum)
// 柯里化 每次入参都是一个参数
newSum(1)(2)(3)(4)(5)
// 偏函数
newSum(1)(2)(3,4,5)
// 柯里化简略利用
// 判断类型,参数多少个,就执行多少次收集
function isType(type, val) {return Object.prototype.toString.call(val) === `[object ${type}]`
}
let newType = curry(isType)
// 相当于把函数参数一个个传了,把第一次先缓存起来
let isString = newType('String')
let isNumber = newType('Number')
isString('hello world')
isNumber(999)
实现每隔一秒打印 1,2,3,4
// 应用闭包实现
for (var i = 0; i < 5; i++) {(function(i) {setTimeout(function() {console.log(i);
}, i * 1000);
})(i);
}
// 应用 let 块级作用域
for (let i = 0; i < 5; i++) {setTimeout(function() {console.log(i);
}, i * 1000);
}
版本号排序的办法
题目形容: 有一组版本号如下 ['0.1.1', '2.3.3', '0.302.1', '4.2', '4.3.5', '4.3.4.5']
。当初须要对其进行排序,排序的后果为 ['4.3.5','4.3.4.5','2.3.3','0.302.1','0.1.1']
arr.sort((a, b) => {
let i = 0;
const arr1 = a.split(".");
const arr2 = b.split(".");
while (true) {const s1 = arr1[i];
const s2 = arr2[i];
i++;
if (s1 === undefined || s2 === undefined) {return arr2.length - arr1.length;}
if (s1 === s2) continue;
return s2 - s1;
}
});
console.log(arr);
二叉树深度遍历
// 二叉树深度遍历
class Node {constructor(element, parent) {
this.parent = parent // 父节点
this.element = element // 以后存储内容
this.left = null // 左子树
this.right = null // 右子树
}
}
class BST {constructor(compare) {
this.root = null // 树根
this.size = 0 // 树中的节点个数
this.compare = compare || this.compare
}
compare(a,b) {return a - b}
add(element) {if(this.root === null) {this.root = new Node(element, null)
this.size++
return
}
// 获取根节点 用以后增加的进行判断 放右边还是放左边
let currentNode = this.root
let compare
let parent = null
while (currentNode) {compare = this.compare(element, currentNode.element)
parent = currentNode // 先将父亲保存起来
// currentNode 要不停的变动
if(compare > 0) {currentNode = currentNode.right} else if(compare < 0) {currentNode = currentNode.left} else {currentNode.element = element // 相等时 先笼罩后续解决}
}
let newNode = new Node(element, parent)
if(compare > 0) {parent.right = newNode} else if(compare < 0) {parent.left = newNode}
this.size++
}
// 前序遍历
preorderTraversal(visitor) {
const traversal = node=>{if(node === null) return
visitor.visit(node.element)
traversal(node.left)
traversal(node.right)
}
traversal(this.root)
}
// 中序遍历
inorderTraversal(visitor) {
const traversal = node=>{if(node === null) return
traversal(node.left)
visitor.visit(node.element)
traversal(node.right)
}
traversal(this.root)
}
// 后序遍历
posterorderTraversal(visitor) {
const traversal = node=>{if(node === null) return
traversal(node.left)
traversal(node.right)
visitor.visit(node.element)
}
traversal(this.root)
}
// 反转二叉树:无论先序、中序、后序、层级都能够反转
invertTree() {
const traversal = node=>{if(node === null) return
let temp = node.left
node.left = node.right
node.right = temp
traversal(node.left)
traversal(node.right)
}
traversal(this.root)
return this.root
}
}
先序遍历
二叉树的遍历形式
// 测试
var bst = new BST((a,b)=>a.age-b.age) // 模仿 sort 办法
bst.add({age: 10})
bst.add({age: 8})
bst.add({age:19})
bst.add({age:6})
bst.add({age: 15})
bst.add({age: 22})
bst.add({age: 20})
// 先序遍历
// console.log(bst.preorderTraversal(),'先序遍历')
// console.log(bst.inorderTraversal(),'中序遍历')
// ![](http://img-repo.poetries.top/images/20210522214837.png)
// console.log(bst.posterorderTraversal(),'后序遍历')
// 深度遍历:先序遍历、中序遍历、后续遍历
// 广度遍历:档次遍历(同层级遍历)// 都可拿到树中的节点
// 应用访问者模式
class Visitor {constructor() {this.visit = function (elem) {elem.age = elem.age*2}
}
}
// bst.posterorderTraversal({// visit(elem) {
// elem.age = elem.age*10
// }
// })
// 不能通过索引操作 拿到节点去操作
// bst.posterorderTraversal(new Visitor())
console.log(bst.invertTree(),'反转二叉树')
查找字符串中呈现最多的字符和个数
例: abbcccddddd -> 字符最多的是 d,呈现了 5 次
let str = "abcabcabcbbccccc";
let num = 0;
let char = '';
// 使其依照肯定的秩序排列
str = str.split('').sort().join('');
// "aaabbbbbcccccccc"
// 定义正则表达式
let re = /(\w)\1+/g;
str.replace(re,($0,$1) => {if(num < $0.length){
num = $0.length;
char = $1;
}
});
console.log(` 字符最多的是 ${char},呈现了 ${num}次 `);
封装异步的 fetch,应用 async await 形式来应用
(async () => {
class HttpRequestUtil {async get(url) {const res = await fetch(url);
const data = await res.json();
return data;
}
async post(url, data) {
const res = await fetch(url, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
const result = await res.json();
return result;
}
async put(url, data) {
const res = await fetch(url, {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
data: JSON.stringify(data)
});
const result = await res.json();
return result;
}
async delete(url, data) {
const res = await fetch(url, {
method: 'DELETE',
headers: {'Content-Type': 'application/json'},
data: JSON.stringify(data)
});
const result = await res.json();
return result;
}
}
const httpRequestUtil = new HttpRequestUtil();
const res = await httpRequestUtil.get('http://golderbrother.cn/');
console.log(res);
})();
实现数组的乱序输入
次要的实现思路就是:
- 取出数组的第一个元素,随机产生一个索引值,将该第一个元素和这个索引对应的元素进行替换。
- 第二次取出数据数组第二个元素,随机产生一个除了索引为 1 的之外的索引值,并将第二个元素与该索引值对应的元素进行替换
- 依照下面的法则执行,直到遍历实现
var arr = [1,2,3,4,5,6,7,8,9,10];
for (var i = 0; i < arr.length; i++) {const randomIndex = Math.round(Math.random() * (arr.length - 1 - i)) + i;
[arr[i], arr[randomIndex]] = [arr[randomIndex], arr[i]];
}
console.log(arr)
还有一办法就是倒序遍历:
var arr = [1,2,3,4,5,6,7,8,9,10];
let length = arr.length,
randomIndex,
temp;
while (length) {randomIndex = Math.floor(Math.random() * length--);
temp = arr[length];
arr[length] = arr[randomIndex];
arr[randomIndex] = temp;
}
console.log(arr)
验证是否是邮箱
function isEmail(email) {var regx = /^([a-zA-Z0-9_\-])+@([a-zA-Z0-9_\-])+(\.[a-zA-Z0-9_\-])+$/;
return regx.test(email);
}
实现有并行限度的 Promise 调度器
题目形容:JS 实现一个带并发限度的异步调度器 Scheduler
,保障同时运行的工作最多有两个
addTask(1000,"1");
addTask(500,"2");
addTask(300,"3");
addTask(400,"4");
的输入程序是:2 3 1 4
整个的残缺执行流程:一开始 1、2 两个工作开始执行
500ms 时,2 工作执行结束,输入 2,工作 3 开始执行
800ms 时,3 工作执行结束,输入 3,工作 4 开始执行
1000ms 时,1 工作执行结束,输入 1,此时只剩下 4 工作在执行
1200ms 时,4 工作执行结束,输入 4
实现代码如下:
class Scheduler {constructor(limit) {this.queue = [];
this.maxCount = limit;
this.runCounts = 0;
}
add(time, order) {const promiseCreator = () => {return new Promise((resolve, reject) => {setTimeout(() => {console.log(order);
resolve();}, time);
});
};
this.queue.push(promiseCreator);
}
taskStart() {for (let i = 0; i < this.maxCount; i++) {this.request();
}
}
request() {if (!this.queue || !this.queue.length || this.runCounts >= this.maxCount) {return;}
this.runCounts++;
this.queue
.shift()()
.then(() => {
this.runCounts--;
this.request();});
}
}
const scheduler = new Scheduler(2);
const addTask = (time, order) => {scheduler.add(time, order);
};
addTask(1000, "1");
addTask(500, "2");
addTask(300, "3");
addTask(400, "4");
scheduler.taskStart();
对象数组列表转成树形构造(解决菜单)
[
{
id: 1,
text: '节点 1',
parentId: 0 // 这里用 0 示意为顶级节点
},
{
id: 2,
text: '节点 1_1',
parentId: 1 // 通过这个字段来确定子父级
}
...
]
转成
[
{
id: 1,
text: '节点 1',
parentId: 0,
children: [
{
id:2,
text: '节点 1_1',
parentId:1
}
]
}
]
实现代码如下:
function listToTree(data) {let temp = {};
let treeData = [];
for (let i = 0; i < data.length; i++) {temp[data[i].id] = data[i];
}
for (let i in temp) {if (+temp[i].parentId != 0) {if (!temp[temp[i].parentId].children) {temp[temp[i].parentId].children = [];}
temp[temp[i].parentId].children.push(temp[i]);
} else {treeData.push(temp[i]);
}
}
return treeData;
}