手写题
/**
*
* @param task 返回一个promise的异步任务
* @param count 需要重试的次数
* @param time 每次重试间隔多久
* @returns 返回一个新promise
*/
const retry = (task, count = 5, time = 3 * 1000) => {
return new Promise((resolve, reject) => {
let errorCount = 0;
const run = () => {
task()
.then((res) => resolve(res))
.catch((err) => {
errorCount++;
if (errorCount < count) {
setTimeout(run, time);
} else {
reject(err);
}
});
};
run();
});
};
防抖节流
防抖
在给定的时间间隔内只允许你提供的回调函数执行一次,以此降低它的执行频率
实现
/**
* 防抖函数
* @param {*} fn 回调函数
* @param {*} delay 延迟的时间
* @param {*} immediate 立即执行
*/
const debounce = (fn, delay, immediate) => {
let timer = null; // 使用闭包缓存结果
return function () {
let callNow = immediate && !timer; // 立即执行
// 如果已经在防抖了
if (timer) {
clearTimeout(timer);
}
// 有立即执行, 即马上执行,否则等待执行
if (callNow) {
fn.call(this, ...arguments);
} else {
timer = setTimeout(() => {
fn.call(this, ...arguments);
// 执行完毕后
timer = null;
}, delay);
}
};
};
防抖场景
- search 搜索联想,用户在不断输入值时,用防抖来节约请求资源。
- window 触发 resize 的时候,不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发一次
节流
单位时间内只能触发一次
实现
/**
* 节流函数
* @param {*} fn 回调函数
* @param {*} delay 延迟时间
* @returns
*/
function throttle(fn, delay) {
var flag = false; // 开始的时候, false标志未开始, true标志开始
return function () {
if (flag) return;
flag = true; // 标志开始
setTimeout(() => {
fn.apply(this, arguments); // 当运行完这次事件
flag = false; // 标志回未开始
}, delay);
};
}
节流场景
- 鼠标不断点击触发,mousedown(单位时间内只触发一次)
- 监听滚动事件,比如是否滑到底部自动加载更多,用 throttle 来判断
深拷贝
实现
const isObject = (val) => typeof val === "object" && val !== null;
const toTypeString = (val) => Object.prototype.toString.call(val);
const deepCopy = (obj, hash = new WeakMap()) => {
// 非对象类型不拷贝
if (!isObject(obj)) return obj;
// 对象为正则或者日期 对象, 也不进行拷贝
if (toTypeString(obj) === "[object RegExp]") return obj;
if (toTypeString(obj) === "[object Date]") return obj;
// 如果有循环引的现象, 不在继续拷贝,返回之前的
if (hash.has(obj)) return obj;
// 开始拷贝
let newObj = Array.isArray(obj) ? [] : {};
hash.set(obj, newObj); // 存储
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
// 不拷贝继承的属性
newObj[key] = isObject(obj[key]) ? deepCopy(obj[key], hash) : obj[key];
}
}
return newObj;
};
注意点
- 循环引用的处理
- 不拷贝继承的属性的处理
模板字符串解析
题目描述
let template = "我是{{name}},年龄{{age}},性别{{sex}}";
let data = {
name: "小明",
age: 18,
};
// 要求写一个函数使编译结果为
render(template, data); // 我是小明,年龄18,性别undefined
实现
function render(template, data) {
// \为转义
let str = template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
/*
*match =>{{name}} 字符串中匹配到的
*key =>name 代表括号匹配的字符串
*/
return data[key];
});
return str;
}
LRU 算法
描述
LRU 算法也叫做
最近最少使用算法
LRU 算法的原理: 每次获取值的时候都会重新设置值, 所以最近最少使用的必然是最前面的那个
实现
/**
* capacity 存储的最大容量
*/
class LRUCache {
constructor(capacity) {
this.secretKey = new Map();
this.capacity = capacity;
}
// 获取值
get(key) {
if (this.secretKey.has(key)) {
let templateValue = this.secretKey.get(key);
this.secretKey.delete(key); // 删除当前的
this.secretKey.set(key, templateValue); // 重新设置, 并排在最后
return templateValue;
} else {
return -1;
}
}
// 设置值
set(key, value) {
// 如果已经存在, 修改
if (this.secretKey.has(key)) {
this.secretKey.delete(key); // 删除当前的
this.secretKey.set(key, value); // 重新设置, 并排在最后
} else if (this.secretKey.size < this.capacity) {
// 如果cache足够, 继续放
this.secretKey.set(key, value); // 重新设置, 并排在最后
} else {
// 先添加进去
this.secretKey.set(key, value);
// 然后再删除第一个(即最近最少使用的)
this.secretKey.delete(this.secretKey.keys().next().value);
}
}
}
// 测试
let cache = new LRUCache(2); // 最大容量2
// 存放俩个
cache.set("test", "test组件");
cache.set("test2", "test2组件");
// 获取
cache.get("test"); // test组件
// 再次存放, 因为最大容量为2, 并且在上面已经获取了test ,因此最少使用的是test2, 所以剔除
cache.set("test3", "test3组件");
// cache => Map(2) {'test' => 'test组件', 'test3' => 'test3组件'}
数字千分位分割
为什么数字使用千分位分割, 有以下几个原因
- 防止被浏览器表示为手机号码,影响本意
- 能让语音阅读整体阅读, 比如 123456789,读 一亿两千三百四十五万六千七百八十九;而不是 一二三四五六七八九
- 更加符合国际通用规范
实现
function formatNum(num) {
// 1. 转化为字符串更好操作
let strNum = num.toString();
// 2. 看是否有小数点以及获取到小数
let decimals = "";
if (strNum.includes(".")) {
// 包含小数点
let [num, decimal] = strNum.split("."); // 数组解构 => 解构出来例如123.45 => [123, 45] 45为小数
strNum = num;
decimals = decimal;
}
// 3. 看num长度
let len = strNum.length;
if (len < 3) {
// 整数部分不够三位, 不分割
return decimals ? strNum + `.${decimals}` : decimals;
} else {
// 分割
// 可以进行
let temp = "";
let remainder = len % 3; // 获取到是否3的倍数
decimals ? (temp = "." + decimals) : temp; // 获取到分数
if (remainder > 0) {
let beforeNum = strNum.slice(0, remainder); // 截取前面不足3位的
/**
* match返回的是一个数组
* 如果使用g标志,则将返回与完整正则表达式匹配的所有结果,但不会返回捕获组。
* 如果未使用g标志,则仅返回第一个完整匹配及其相关的捕获组(Array)。
* 例如: '123455'.match(/\d{3}/g) => ['123', '455']
* '123455'.match(/\d{3}/) 没有使用g标志 ['123', index: 0, input: '123455', groups: undefined]
*/
let middleNum = strNum.slice(remainder, len).match(/\d{3}/g).join(","); // 截取中间
let afterNum = temp; // 小数点
return beforeNum + middleNum + afterNum;
} else {
let middleNum = strNum.slice(0, len).match(/\d{3}/g).join(",");
let afterNum = temp;
return middleNum + afterNum;
}
}
}
函数柯里化
描述
- 柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术
实现
/**
*
* @param {*} fn 函数
* @param {...any} args 多个参数
*/
function currying(fn, ...args) {
// 1.获取到fn参数个数
const len = fn.length;
// 2.收集参数
let allArgs = [...args];
const dfs = (...args2) => {
// 继续收集参数
allArgs = [...allArgs, ...args2]; // 3.收集返回函数的参数(使用闭包, 一直保存allArgs)
if (len === allArgs.length) {
// 4.1收集完毕
return fn(...allArgs);
} else {
// 4.2继续收集
return dfs;
}
};
return dfs;
}
let fn = (a, b, c) => {
return a + b + c;
};
// let add = currying(fn, 3, 4)
// let ret = add(5)
// 连续使用 -- 收集到一定参数才会去调用函数, 否则还是返回函数
let ret = currying(fn, 3, 4)(5);
console.log(ret); //12
new 操作符
new 操作符分析
- new 出来的对象 this 指向实例本身
- 实例.__proto === 构造函数.prototype
- 会自行函数
- 根据返回值类型返回
实现
/**
*
* @param {*} fn 构造函数
* @param {...any} args 剩余参数
*/
const myNew = (fn, ...args) => {
// 1.创建一个对象, 并且以fn.prototype作为原型
let obj = Object.create(fn);
// 2.调用函数, 并将this执行obj
let ret = fn.call(obj, ...args);
// 3.判断返回值
return ret instanceof Object ? ret : obj;
};
// 测试
function Person(name) {
this.name = name;
}
const p = myNew(Person, "xm");
列表与树形结构互转
列表转成树形结构
let data = [
{
id: 1,
text: "节点1",
parentId: 0, //这里用0表示为顶级节点
},
{
id: 2,
text: "节点1_1",
parentId: 1, //通过这个字段来确定子父级
},
{
id: 3,
text: "节点2_1",
parentId: 2, //通过这个字段来确定子父级
},
];
// 转成
// [
// {
// id: 1,
// text: '节点1',
// parentId: 0,
// children: [
// {
// id:2,
// text: '节点1_1',
// parentId:1
// }
// ]
// }
// ]
转化函数
function listToTree(data) {
let temp = {};
let treeDate = [];
// 减少循环操作
data.forEach((item) => {
temp[item.id] = item;
});
// 遍历加入
for (let key in temp) {
// 如果不是最大父级
if (temp[key].parentId !== 0) {
// 找到父亲, 判断有没有children属性
if (!temp[temp[key].parentId].children) {
temp[temp[key].parentId].children = [];
}
// 有就加入进来
temp[temp[key].parentId].children.push(temp[key]);
} else {
treeDate.push(temp[key]);
}
}
return treeDate;
}
listToTree(data);
树形结构转成列表
let data = [
{
id: 1,
text: "节点1",
parentId: 0,
children: [
{
id: 2,
text: "节点1_1",
parentId: 1,
},
],
},
];
// 转成
// [
// {
// id: 1,
// text: '节点1',
// parentId: 0 //这里用0表示为顶级节点
// },
// {
// id: 2,
// text: '节点1_1',
// parentId: 1 //通过这个字段来确定子父级
// }
// ...
// ]
转化函数
function treeToList(data) {
let res = [];
const dfs = (tree) => {
tree.forEach((item) => {
if (item.children) {
// 递归循环(dfs => 深度优先搜索)
dfs(item.children);
delete item.children;
}
res.push(item);
});
};
dfs(data);
return res;
}
treeToList(data);
继承
继承本次要实现 5 种继承
1.原型链继承
function Student() {
this.name = ["vvv", "vvv2"];
}
Student.prototype.say = function () {
return this.name;
};
function Vvv() {}
Vvv.prototype = new Student(); // 核心
// Vvv.prototype.constructor === Student 为什么呢
// 因为let s = new Student() 即Vvv.prototype === s (即Vvv的原型是Student的实例)
// 那么Vvv.prototype.constructor = s.constructor
// s.constructor是Student
Vvv.prototype.constructor = Vvv;
let vvv3 = new Vvv();
vvv3.name.push("vvv3");
let vvv4 = new Vvv();
console.log(vvv4.name); // ['vvv', 'vvv2', 'vvv3']
原型链继承缺点
- 问题 1:原型中包含的引用类型属性将被所有实例共享; 可以看到 p1 push 的时候 p2 也改变了
- 问题 2:子类在实例化的时候不能给父类构造函数传参;
2.借用构造函数继承
function Student(name) {
this.name = name;
this.getName = function () {
return this.name;
};
}
function Vvv(name) {
Student.call(this, name); // 核心
}
let vvv = new Vvv("vvv");
vvv.getName(); // vvv
let vvv2 = new Vvv("vvv2");
vvv2.getName(); // vvv2
借用构造函数继承优缺点
- 优点: 借用构造函数实现继承解决了原型链继承的 2 个问题:引用类型共享问题以及传参问题
- 缺点: 由于方法必须定义在构造函数中,所以会导致每次创建子类实例都会创建一遍方法
3.组合继承
function Student(name) {
this.name = name;
}
Student.prototype.getName = function () {
return this.name;
};
function Vvv(name) {
// 第二次
Student.call(this, name);
}
// 第一次
Vvv.prototype = new Student();
Vvv.prototype.constructor = Vvv;
let vvv = new Vvv("vvv");
vvv.getName(); // vvv
let vvv2 = new Vvv("vvv2");
vvv2.getName(); // vvv2
组合继承优缺点
优点: 解决原型链继承和借用构造函数继承分别的缺点
缺点: 调用了俩次父类构造函数
4.寄生组合继承
function Student(name) {
this.name = name;
}
Student.prototype.getName = function () {
return this.name;
};
function Vvv(name) {
Student.call(this, name);
}
Vvv.prototype = Object.create(Student.prototype);
// 解释一下 Vvv.prototype = Object.create(Student.prototype)
// 用Object.create以父原型为原型创建对象赋值给子原型
// let obj = Object.create(Student.prototype)
// Vvv.prototype = obj
// obj 是什么呢? obj是以Student.prototype为原型的对象
Vvv.prototype.constructor = Vvv;
let vvv = new Vvv("vvv");
vvv.getName(); // vvv
let vvv2 = new Vvv("vvv2");
vvv2.getName(); // vvv2
寄生组合继承优点
组合继承已经相对完善了,但还是存在问题,它的问题就是调用了 2 次父类构造函数, 第一次是在
new Student()
,第二次是在Student.call()
这里优点: 解决组合继承 2 次调用父类构造函数
5.class 继承
class Student {
constructor(name) {
this.name = name;
}
}
// 核心: extends
class Vvv extends Student {
constructor(name, age) {
super(name);
this.age = 18;
}
}
class 继承
es6 class 实现继承已经相对很简单了
其原理就是使用了寄生组合继承
sleep 函数
描述
sleep 函数作用是让线程休眠,等到指定时间在重新唤起
实现
function sleep(time) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, time * 1000);
});
}
对象的层次获取
实现
/*
*obj 传入的对象
*keyName 也就是a.c这样的形式
*/
function lookup(obj, keyName) {
//首先判断keyName是否含有.
if (keyName.indexOf('.') != -1 && keyName != '.') {
//有就拆分
let keys = keyName.split(".");
let temp = obj;
for (let i=0; i < keys.length; i++) {
temp = temp[keys[i]];
}
//循环结束后返回temp
return temp;
}
//没有. 就直接使用
return obj[keyName];
}
// 测试
var obj = {
a:{
c:{
d:123
}
}
}
lookup({a:{c:{d:123}}},'a.c.d') //123
大数相加
在 js 中, Number.MAX_SAFE_INTEGER
常量表示在 JavaScript 中最大的安全整数(maxinum safe integer), 一旦超出这个数,就会出现精度丢失问题, 例如:
let c = 9007199254740991;
let d = 123456789999999999;
d > Number.MAX_SAFE_INTEGER; // true
c + d; // 132463989254741000
我们想要的结果是132463989254740990
, 但是由于 d 超出了 JavaScript 中最大的安全整数, 所以导致运算错误, 因此我们需要实现一个add
方法使得大数可以相加
实现
function add(a, b) {
// 1.0 补齐位数
let maxLength = Math.max(a.length, b.length);
/**
* mdn: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/String/padStart
* padStart() 方法用另一个字符串填充当前字符串 (如果需要的话,会重复多次),
* 以便产生的字符串达到给定的长度。从当前字符串的左侧开始填充。
*/
a = a.padStart(maxLength, 0); // '009007199254740991' 从前开始补齐 用0来补齐
b = b.padStart(maxLength, 0); // '123456789999999999'
let t = 0;
let f = 0; // 进位
let sum = ""; // 拼接
for (let i = maxLength - 1; i >= 0; i--) {
t = parseInt(a[i]) + parseInt(b[i]) + f;
f = Math.floor(t / 10); // 是否有进位
sum = (t % 10) + sum;
}
// 是否还存在进位
if (f !== 0) {
sum = "" + f + sum;
}
return sum;
}
有效括号
给定一个只包括 '(',')','{','}','[',']'
的字符串 s ,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。 左括号必须以正确的顺序闭合。
实现
const isValid = (s) => {
// 字符为单数 或者字符以)]} 开头的都会无法闭合
if (s.length % 2 === 1 || s[0] === ")" || s[0] === "]" || s[0] === "}") {
return false;
}
// 存储括号闭合的类型
const pairs = new Map();
pairs.set(")", "(");
pairs.set("]", "[");
pairs.set("}", "{");
// 使用栈的方式进行判断 先进后出
const stack = [];
// 遍历所有字符串
for (let ch of s) {
// ch中是否存在)]} 的一种
if (pairs.has(ch)) {
// 判断最后一组是否是有效括号
if (!stack.length || stack[stack.length - 1] !== pairs.get(ch)) {
return false;
}
stack.pop();
} else {
stack.push(ch);
}
}
return !stack.length;
};
说明
举例子 例如: s = ()[]{} 开始进栈( 然后下一次)进栈, 然后进入pairs.has(ch) -> true
-> stack.pop() 当前栈为空 下一次进栈[ 同理 (记住 )]} 这三个是不会进入到栈中的 )
实现 forOf
MDN 解释: for...of
语句在可迭代对象(包括 Array
,Map
,Set
,String
,TypedArray类数组
,arguments 对象等等)上创建一个迭代循环,调用自定义迭代钩子(迭代器
),并为每个不同属性的值执行语句
可迭代对象
首先需要知道什么是可迭代对象
typeof obj[Symbol.iterator] == "function";
如果上述式子成立的话, 那么我们称obj
是可迭代对象
tips:只有可迭代对象才能使用for..of
迭代器原理
在上面介绍了可迭代对象, 在可迭代对象中肯定会存在迭代器
, 实现迭代器
const createIterator = (obj) => {
let i = 0;
return {
next: function () {
// done 表示是否已经迭代完成
const done = i >= obj.length;
const val = !done ? obj[i++] : undefined;
return {
done: done,
value: val,
};
},
};
};
实现 forOf
认识了可迭代对象
和迭代器原理
就可以实现 forOf 了
/**
*
* @param {*} obj 可迭代对象
* @param {*} callback 回调函数
*/
const forOf = (obj, callback) => {
// iterable迭代器
// result结果
let iterable, result;
// 如果传入对象Symbol.iterator属性类型不是function,抛出错误 (说明不是可迭代的对象)
if (typeof obj[Symbol.iterator] !== "function") {
throw new TypeError(result + "is not iterable");
}
// iterable = obj[Symbol.iterator]()
// 也可以使用上述的createIterator
iterable = createIterator(obj);
result = iterable.next();
while (!result.done) {
// 回调处理val值
callback(result.value);
result = iterable.next();
}
};
// 测试
forOf([1, 2, 3], (item) => {
console.log(item); // 1,2,3
});
setTimeout 模拟实现 setInterval
为什么用 setTimeout
替代 setInterval
? setInterval
有什么问题?
实现
/**
* setTimeout 模拟实现 setInterval
* @param {*} callback 回调函数
* @param {*} delay 延迟时间
*/
const mySetInterval = (callback, delay) => {
let timer = null;
const interval = () => {
callback(); // 执行回调
timer = setTimeout(interval, delay); // 递归调用
};
interval(); // 初始执行
return {
// 清除定时器
cancel: () => {
clearTimeout(timer);
},
};
};
// 测试
let test = mySetInterval(() => {
console.log(123);
}, 1000);
test.cancel(); // 清除定时器
实现对象扁平化
要求
实现 flattenObj 函数,为一个 javascript 对象(Object 或者 Array),返回值为扁平化后的结果
例如:
const input = {
a: 1,
b: [1, 2, { c: true }, [3]],
d: { e: 2, f: 3 },
g: null,
};
const flattenRes = flattenObj(input)
// 测试结果如下
{
"a": 1,
"b[0]": 1,
"b[1]": 2,
"b[2].c": true,
"b[3][0]": 3,
"d.e": 2,
"d.f": 3,
// "g": null, 值为null或者undefined,丢弃
}
实现
const flattenObj = obj => {
// 接受结果
let res = {}
// 递归
const dfs = (target, oldKey) => {
// 遍历target
for (let key in target) {
let newKey // 用于作为老的key
if (oldKey) {
// 递归有老key 则组合起来
if (Array.isArray(target)) {
// 数组变为 老key[0]
newKey = `${oldKey}[${key}]`
} else {
// 对象: 老key.a
newKey = `${oldKey}.${key}`
}
} else {
// 没有oldKey, 即初始化状况
if (Array.isArray(target)) {
// 数组变为 [0] [1]
newKey = `[${key}]`
} else {
// 对象变为 'a' 'b'
newKey = key
}
}
if (Object.prototype.toString.call(target[key]) === '[object Object]' || Array.isArray(target[key])) {
// 递归数组和对象 传进组织好的老key
dfs(target[key], newKey)
} else if (target[key] !== null && target[key] !== undefined) {
// 递归出口 常规数据 直接赋值
res[newKey] = target[key]
}
}
}
dfs(obj, '')
return res
}
// 测试
const input = {
a: 1,
b: [1, 2, { c: true }, [3]],
d: { e: 2, f: 3 },
g: null
}
const flattenRes = flattenObj(input)
// 测试结果如下
{
"a": 1,
"b[0]": 1,
"b[1]": 2,
"b[2].c": true,
"b[3][0]": 3,
"d.e": 2,
"d.f": 3,
// "g": null, 值为null或者undefined,丢弃
}
实现 Object.assign
Object.assign
MDN: Object.assign()
方法将所有可枚举(Object.propertyIsEnumerable()
返回 true)和自有(Object.hasOwnProperty()
返回 true)属性从一个或多个源对象复制到目标对象,返回修改后的对象。直达 MDN 文档查看
从 MDN 文档中可以知道实现Object.assign
的几个要求
- 如果目标对象与源对象具有相同的 key,则目标对象中的属性将被源对象中的属性覆盖,后面的源对象的属性将类似地覆盖前面的源对象的属性。
Object.assign
方法只会拷贝源对象 可枚举的 和 自身的 属性到目标对象。该方法使用源对象的[[Get]]
和目标对象的[[Set]]
,它会调用 getters 和 setters。故它分配属性,而不仅仅是复制或定义新的属性。如果合并源包含 getters,这可能使其不适合将新属性合并到原型中。(就是不希望合并原型上的属性)- 不可枚举的属性不会合并
- 基本类型会被包装成对象
实现
/**
* Object._assign
* @param {*} target 目标对象,接收源对象属性的对象,也是修改后的返回值
* @param {...any} sources 源对象,包含将被合并的属性
*/
Object._assign = (target, ...sources) => {
// 普通类型包装成对象 比如字符串等...
target = Object(target);
for (let i = 0; i < sources.length; i++) {
// 过滤掉要合并的对象为null和undefined的情况
// (null == null -> true )(null == undefined -> true)
if (sources[i] == null) continue; // 结束本次循环
// 遍历要合并对象的属性
for (let key in sources[i]) {
// in运算符会查找原型对象上的可枚举属性,
// 所以需要通过Object.prototype.hasOwnProperty方法过滤掉对象原型对象上的属性
if (sources[i].hasOwnProperty(key)) {
target[key] = sources[i][key];
}
}
}
return target;
};
测试
const proto = { p: "proto" };
const obj1 = { a: "aa" };
const obj2 = { b: "bb" };
// 以proto作为新对象的原型
const obj3 = Object.create(proto, {
c: {
value: "cc",
enumerable: true,
},
});
console.log(obj3); // {c: 'cc'}
// 输出obj3的构造函数的原型对象
console.log(obj3.__proto__); // {p: 'proto'}
// 说明不会合并原型链(__proto__) 上面的属性
const t1 = Object._assign({}, obj1, obj2);
console.log(t1); // {a: "aa", b: "bb"}
// 过滤合并对象为null、undefined的情况
const t2 = Object._assign({}, obj1, null, obj2, undefined);
console.log(t2); // {a: "aa", b: "bb"}
// 合并属性
const t3 = Object._assign({}, obj1, obj2, obj3);
console.log(t3); // {a: "aa", b: "bb", c: "cc"}
测试 2
const v1 = "abc";
const v2 = true;
const v3 = 10;
const v4 = Symbol("foo");
const obj = Object.assign({}, v1, null, v2, undefined, v3, v4);
// 基本类型会被包装,null和undefined会被忽略.
// 只有字符串包装器可以有自己的可枚举属性, 所以true 和 10不会被放进来
console.log(obj); // { "0": "a", "1": "b", "2": "c" }
实现 compose 组合函数
在函数式编程当中有一个很重要的概念就是函数组合
, 实际上就是把处理数据的函数像管道一样连接起来, 然后让数据穿过管道得到最终的结果。例如:
function add(a, b) {
return a + b;
}
function len(str) {
return str.length;
}
function preFix(str) {
return `###${str}`;
}
console.log(preFix(len(add(1, "1")))); // ###2
我们想输出的是一个多层函数嵌套
的运行结果,即把前一个函数的运行结果赋值给后一个函数
。但是如果需要嵌套多层函数,那这种类似于f(g(h(x)))
的写法可读性太差,我们考虑能不能写成(f, g, h)(x)
这种简单直观的形式,于是 compose()函数就正好帮助我们实现。
实现
const composeRight = (...fns) => {
return function (...args) {
const len = fns.length; // 获取到函数个数
// 处理fns < 2 的情况
if (len === 0) return args;
if (len === 1) return fns[0](...args);
// 处理fns > 2的情况
const lastFn = fns && fns.pop(); // 获取到最后一个函数
// 获取到最后一个函数并执行, 并获取函数返回结果作为reduce的初始值
let prev = lastFn(...args);
// reduceRight 从右往左进行reduce
return fns.reduceRight((prev, cur) => {
return cur(prev);
}, prev);
};
};
测试
function add(a, b) {
return a + b;
}
function len(str) {
return str.length;
}
function preFix(str) {
return `###${str}`;
}
let retFn = composeRight(preFix, len, add); // 返回一个函数
console.log(retFn);
// ...args => 1, '1', 然后开始执行
// lastFn(...args) = > add(1, '1') => 11 => 开始进行reduce
/**
* return fns.reduceRight((prev, cur) => {
* return cur(prev) // 开始的cur是len => len(11) => 2, 最后是preFix
* }, 11)
*/
console.log(retFn(1, "1")); // ###2
实现 instanceof
MDN 描述: instanceof
运算符用于检测构造函数的 prototype
属性是否出现在某个实例对象的原型链上。
语法:
object instanceof constructor
object -> 某个实例对象
constructor -> 某个构造函数
理解: instanceof
运算符用来检测 constructor.prototype
是否存在于参数 object
的原型链上。
实现
/**
* instanceof
* @param {*} left 某个实例对象
* @param {*} right 某个构造函数
*/
const myInstanceof = (left, right) => {
// 处理边界情况
if ((typeof left !== "object" || left === null) && typeof left !== "function")
return false;
if (!right.prototype)
throw new TypeError("Right-hand side of 'myInstanceof' is not an object");
let leftProto = Object.getPrototypeOf(left);
const rightPrototype = right.prototype;
while (leftProto !== null) {
if (leftProto === rightPrototype) {
return true;
}
leftProto = Object.getPrototypeOf(leftProto);
}
return false;
};
测试 1
左边不为对象
console.log(myInstanceof(1, Object)); // false
1 instanceof Object; // false
测试 2
右边不为构造函数
const obj = {};
console.log(myInstanceof(obj, 1)); // Uncaught TypeError: Right-hand side of 'myInstanceof' is not an object
obj instanceof 1; // 报错 Uncaught TypeError: Right-hand side of 'instanceof' is not an object
测试 3
左边为对象, 右边为构造函数
const fn = () => {};
console.log(myInstanceof(fn, Function)); // true
fn instanceof Function; // true
不匹配的现象
const fn = () => {};
console.log(myInstanceof(fn, Array)); // false
fn instanceof Array; // false
实现发布订阅模式
实现发布订阅模式 具有on
off
once
emit
方法
实现
class EventEmitter {
constructor() {
// 所有订阅事件(事件中心)
this.events = {};
}
/**
* 订阅事件
* @param {*} type 订阅的类型
* @param {*} callback 事件
*/
on(type, callback) {
if (!this.events[type]) {
this.events[type] = [callback];
} else {
this.events[type].push(callback);
}
}
// 取消订阅事件
off(type, callback) {
this.events[type] &&
(this.events[type] = this.events[type].filter((fn) => fn !== callback));
}
// 只触发一次事件, 之后不再触发
once(type, callback) {
const fn = () => {
callback();
this.off(type, fn);
};
// 在订阅事件的同时, 触发一次后就会取消订阅的事件
this.on(type, fn);
}
// 发布(触发)事件
emit(type, ...rest) {
this.events[type] && this.events[type].forEach((fn) => fn(...rest));
}
}
测试
测试emit
const events = new EventEmitter();
const handle = (...rest) => {
console.log(rest);
};
events.on("click", handle);
events.emit("click", 1, 2, 3, 4); // [1, 2, 3, 4]
测试off
const events = new EventEmitter();
const handle = (...rest) => {
console.log(rest);
};
events.on("click", handle);
events.emit("click", 1, 2, 3, 4); // [1, 2, 3, 4]
events.off("click", handle);
events.emit("click", 1, 2); // 取消了不打印
测试once
events.once("dbClick", () => {
console.log(123456);
});
events.emit("dbClick"); // 123456, 只有第一次触发
events.emit("dbClick");
events.emit("dbClick");
events.emit("dbClick");
events.emit("dbClick");
实现 call/apply/bind
call
apply
bind
这三种方法都是可以改变 this 的指向的, bind 的实现会较难
call
call 的作用
- 1.会改变 this 指向
- 2.会自动调用函数
实现
/**
*
* @param {*} context context (改变的this指向对象)
* @param {...any} args 参数
*/
Function.prototype.myCall = function (context, ...args) {
// 1.context为空或者null时 ,this指向window
if (!context || context == null) {
context = window;
}
// 2. 创建独一无二的一个 fn
let fn = Symbol();
// 3. 将this指向context中的fn, this指的是调用者
context[fn] = this;
// 4. 调用函数fn, 并获的返回值
const ret = context[fn](...args);
// 5.删除 fn
delete context[fn];
// 返回函数返回值
return ret;
};
测试
// 测试
let obj = {
a(params) {
console.log(this, params);
},
};
let obj2 = {
b() {},
};
let obj3 = {
c() {},
};
// obj.a(123) // this->obj
obj.a.call(obj2, 1234); // this -> obj2
obj.a.myCall(obj2, 1234); // this -> obj2
总结
实现 call 共 6 步
- 1.context 为空或者 null 时 ,this 指向 window
- 2.创建独一无二的一个 fn
- 3.将 this 指向 context 中的 fn,
this指的是调用者
- 4.调用函数 fn, 并获的返回值
- 5.删除 fn
- 6.返回函数返回值
apply
apply 的作用
- 1.会改变 this 指向
- 2.会自动调用函数
- 3.参数为数组(
跟call的区别
)
实现
/**
* 实现方法和call差不多
* apply
* @param {*} context context (改变的this指向对象)
* @param {...any} args 参数
*/
Function.prototype.myApply = function (context, args) {
// 1.context为空或者null时 ,this指向window
if (!context || context == null) {
context = window;
}
// 2.创建独一无二的一个 fn
let fn = Symbol();
// 3.将this指向context中的fn, this指的是调用者
context[fn] = this;
// 4.调用函数fn, 并获的返回值
const ret = context[fn](...args);
// 5.删除 fn
delete context[fn];
// 6.返回函数返回值
return ret;
};
测试方式和总结跟call
的差不多
bind(重点)
bind 的作用
- 1.新函数的 this 被指定为 bind 的第一个参数
- 2.
返回一个函数
- 3.其余参数将作为新函数的参数
- 4.在 bind 的时候,可以进行传参
- 5.当 bind 返回的函数作为构造函数的时候,bind 时指定的 this 值会失效,但传入的参数依然生效
实现
Function.prototype.myBind = function (context, ...args1) {
// 1.context为空或者null时 ,this指向window
if (!context || context == null) {
context = window;
}
// 2. 创建独一无二的一个 fn
let fn = Symbol();
context[fn] = this;
let self = this; // 需要保存当前指向
// 3.会返回一个函数, 这个函数也可以传入参数
const result = function (...args2) {
// 第一种情况 :若是将 bind 绑定之后的函数当作构造函数,通过 new 操作符使用,则不绑定传入的 this,而是将 this 指向实例化出来的对象
// 此时由于new操作符作用 this指向result实例对象 而result又继承自传入的self 根据原型链知识可得出以下结论
// this.__proto__ === result.prototype // this instanceof result =>true
// this.__proto__.__proto__ === result.prototype.__proto__ === self.prototype; // this instanceof self =>true
if (this instanceof self) {
// 此时this指向指向result的实例 这时候不需要改变this指向
this[fn] = self;
this[fn](...[...args1, ...args2]); // 这里使用es6的方法让bind支持参数合并
} else {
// 如果只是作为普通函数调用 那就很简单了 直接改变this指向为传入的context
context[fn](...[...args1, ...args2]);
}
};
// 如果绑定的是构造函数 那么需要继承构造函数原型属性和方法
// 实现继承的方式: 使用Object.create
result.prototype = Object.create(this.prototype);
return result;
};
测试
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.add = function () {
return this.x + this.y;
};
let F = Point.myBind(null, 1);
// let F = Point.bind(null, 1)
let f = new F(6);
f.add(); // 7
总结
bind
的实现相对较难理解
bind
返回的是一个函数- 返回的函数可以作为构造函数(此时将不在改变指向, 使用的是
new出来的实例作为this指向
) 并且在可以二次传入参数, 第一个是在使用bind的时候, 第二次是在返回的函数身上(重点)
实现 create
MDN 描述: Object.create()
方法用于创建一个新对象,使用现有的对象来作为新创建对象的原型(prototype)。
为什么使用 Object.create
语法
// 返回一个新对象,带着指定的原型对象和属性。
Object.create(proto,[propertiesObject])
proto:新创建对象的原型对象,必须为
null
或者原始包装对象,否则会抛出异常propertiesObject:可选参数,需要是一个对象,该传入对象的自有可枚举属性(即其自身定义的属性,而不是其原型链上的枚举属性)将为新创建的对象添加指定的属性值和对应的属性描述符
为何要使用Object.create
创建对象,而不直接使用对象字面量的形式或者使用Object
创建对象呢?
const obj = {
name: "nordon",
};
const newObj = Object.create(obj);
const nweObj2 = Object(obj);
通过输出可以看到通过字面量和使用Object
创建的对象是一致的,且其引用地址是一致的:obj === newObj2
为true
通过Object.create
常见的对象会在obj
和newObj
之间增加一层,这个时候引用地址是解耦的:obj === newObj
为 false,这样的好处可以保证新创建的对象和原有对象解耦,当我们操作newObj
时并不会影响原有数据
Object.create 的应用场景
利用Object.create
实现继承
function Person(name) {
this.name = name;
this.permission = ["user", "salary", "vacation"];
}
Person.prototype.say = function () {
console.log(`${this.name} 说话了`);
};
function Staff(name, age) {
Person.call(this, name);
this.age = age;
}
Staff.prototype = Object.create(Person.prototype, {
constructor: {
// 若是不将Staff constructor指回到Staff, 此时的Staff实例zs.constructor则指向Person
value: Staff,
},
});
let s = new Staff("xm", 18);
s.say(); // xm 说话了
实现
/**
* Object.create()
* 用于创建一个新对象,使用现有的对象来作为新创建对象的原型(prototype)
* @param {*} proto 原型(对象)
* @returns 返回对象
*/
const create = (proto) => {
// 创建一个构造函数
function F() {}
F.prototype = proto;
return new F(); // 返回实例
};
Object 的实现相对来说比较简单, 但是也需要理解原型与原型链上得知识
实现并行限制的 Promise 调度器
描述
题目描述: 实现一个带并发限制的异步调度器 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);
}
// 开始任务
start() {
for (let i = 0; i < this.maxCount; i++) {
this.request();
}
}
// 执行任务
request() {
if (!this.queue.length || this.runCounts >= this.maxCount) return;
// 从队列中拿出任务开始执行
this.runCounts++;
const task = this.queue.shift();
// 执行(调用)任务后返回promise
task().then(() => {
// 执行完毕后runCounts--
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.start(); // 2 3 1 4
// 一开始1、2两个任务开始执行
// 500ms时,2任务执行完毕,输出2,任务3开始执行
// 800ms时,3任务执行完毕,输出3,任务4开始执行
// 1000ms时,1任务执行完毕,输出1,此时只剩下4任务在执行
// 1200ms时,4任务执行完毕,输出4
总结
实现并行限制的 Promise 调度器需要掌握promise
的知识,通过定时器来控制输出任务的顺序
实现 LazyMan
题目描述
const vvv = new LazyMan('vvv')
vvv.sleep(2).eat('dinner')
// 输出
Hi! my name is vvv
等待2s
eat dinner
vvv.eat('dinner').sleep(2)
// 输出
Hi! my name is vvv
eat dinner
等待2s
vvv.eat("dinner").sleepFirst(2);
// 输出
sleep
方法会延迟后面的输出, eat
方法直接输出, sleepFirst
方法延迟会在开始
实现
class LazyMan {
constructor(name) {
this.tasks = []; // 任务列表
const task = () => {
console.log(`Hi! my name is ${name}`);
this.run(); // 执行一个任务
};
this.tasks.push(task);
// 开始执行任务
setTimeout(() => {
this.run();
}, 0);
}
// 直接输出
eat(food) {
const task = () => {
console.log(`eat ${food}`);
this.run();
};
this.tasks.push(task);
return this;
}
// 执行任务, 取出第一个, 调用
run() {
const task = this.tasks.shift(); // 取第一个任务执行
task && task();
}
// 延迟输出
sleep(time) {
this.sleepWrapper(time);
return this;
}
// 将延迟会在开始
sleepFirst(time) {
this.sleepWrapper(time, true);
return this;
}
sleepWrapper(time, first) {
const task = () => {
setTimeout(() => {
console.log(`等待${time}s`);
this.run();
}, time * 1000);
};
if (first) {
this.tasks.unshift(task);
} else {
this.tasks.push(task); // 在栈尾加入
}
}
}
总结
lazyMan 主要考察的点是对链式调用
以及栈结构
实现 maxRequest
描述
实现 maxRequest,成功后 resolve 结果,失败后重试,尝试超过一定次数才返回真正的 reject
实现
/**
* maxRequest
* @param {*} fn 需要执行的函数
* @param {*} max 最大尝试次数
*/
const maxRequest = (fn, max) => {
return new Promise((resolve, reject) => {
// idx最大请求次数
const dfs = (idx) => {
Promise.resolve(
fn()
.then((val) => {
// 成功的话直接resolve
resolve(val);
})
.catch((err) => {
// 失败就直接给idx-1 再递归
if (idx - 1 > 0) {
help(idx - 1);
} else {
// 如果最后一次了就reject返回
reject(err);
}
})
);
};
dfs(max);
});
};
⭐️⭐️⭐️ 好啦!!!本文章到这里就结束啦。⭐️⭐️⭐️
✿✿ ヽ(°▽°)ノ ✿
撒花 🌸🌸🌸🌸🌸🌸