1. 手写一个JS函数,实现数组扁平化Array Flatten
1.1只减少一级嵌套
如输入[1,[2,[3]],4] 返回[1,2,[3],4]
/*
思路一:利用push
定义空数组result=[],遍历当前数组
如果item是数组,则累加到result
如果item不是数组,则遍历之后累加到result
**/
function ArrayFlatten(array) {
const result = [];
array.forEach((item, index) => {
if(Array.isArray(item)) {
item.forEach(item => result.push(item));
} else {
result.push(item);
}
})
return result;
}
// 思路二:利用concat(item: any[] | any ):arr 不影响原数组
function ArrayFlatten(array) {
let result = [];
array.forEach((item, index) => {
result = result.concat(item);
})
return result;
}
1.2深度扁平化
// 先实现一级扁平化,然后递归调用,知道全部拍平
function deepFlatten(array) {
let res = [];
function fn(arr) {
arr.forEach(item => {
if(Array.isArray(item)) {
fn(item);
} else {
res.push(item);
}
})
}
fn(array);
return res;
}
// 利用concat
function deepFlatten(arr) {
let res = [];
arr.forEach(item => {
if (Array.isArray(item)) {
const flatItem = deepFlatten(item);
res = res.concat(flatItem);
} else {
res = res.concat(item);
}
})
return res;
}
arr.join(',').split(',')和arr.toString().split(',')
对于数组有引用对象,像{x: 1}这种就不适用了,不是完美方案
对象扁平化
function objectFlat(obj = {}) {
const res = {}
function flat(item, preKey = '') {
Object.entries(item).forEach(([key, val]) => {
const newKey = preKey ? `${preKey}.${key}` : key
if (val && typeof val === 'object') {
flat(val, newKey)
} else {
res[newKey] = val
}
})
}
flat(obj)
return res
}
// 测试
const source = { a: { b: { c: 1, d: 2 }, e: 3 }, f: { g: 2 } }
console.log(objectFlat(source));
2.getType
手写一个getType函数,无论传入任意变量,可准确获取类型,包括number、string、boolean等值类型,还有object、array、map、regexp等引用类型
1、typeof 能判断值类型,function和object,像map、set这些都判断成object,有局限性,且typeof null也是object
2、instanceof需要两个参数去判断,而不是获取类型
// 利用Object.prototype.toString.call
function getType(x) {
const originType = Object.prototype.toString.call(x);
const spaceIndex = originType.indexOf(' ');
const type = originType.slice(spaceIndex + 1, -1);
return type.toLowerCase();
}
3、手写new
new 的过程: 创建一个空对象obj,继承构造函数的原型 执行构造函数(将obj作为this) 返回obj
function customNew<T>(constructor: Function, ...args: any[]): T {
// 1、创建空对象,继承构造函数constructor原型
var obj = Object.create(constructor.prototype);
// 2、将obj作为this,执行constructor,传入参数
const res = constructor.apply(obj, args);
// 3、返回obj
if (typeof res === "function" || (typeof res === "object" && res !== null)) {
return res
}
return obj;
}
// 测试
function Foo(name, n) {
this.name = name;
this.city = '北京';
this.n = n;
}
Foo.prototype.getName = function () {
return this.name;
}
const obj = customNew<Foo>(Foo, '小明', 18)
Object.create 和{}区别: Object.create创建的空对象原型链指向传入的参数 {}.proto===Object.prototype 遍历index
防抖节流
防抖
function debounce(fn, delay = 300) {
let timer = null;
return function(...args) {
if(timer) {
clearTimeout(timer);
timer = null;
}
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, delay);
}
}
节流
function throttle(fn, delay) {
let timer = 0;
return function(...args) {
if(timer) return;
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, delay);
}
}
lazyMan
支持sleep和eat,链式调用
const me = new LazyMan('小明');
me.eat('苹果').eat('香蕉').sleep(5).eat('葡萄'); // 打印结果如下
// '小明eat苹果'
// '小明eat香蕉'
// 等待5s
// '小明eat葡萄'
由于有sleep功能,函数不能直接在调用时触发 初始化一个列表,把函数注册进去 由每一个item触发next执行(遇到sleep则异步调用)
class LazyMan {
constructor(name) {
this.name = name;
this.list = [];
setTimeout(() => {
this.next();
}, 0)
}
next() {
if (this.list.length) {
const task = this.list.shift();
task();
}
}
eat(val) {
let task = () => {
console.log(`${this.name}吃${val}`)
this.next();
};
this.list.push(task);
return this;
}
sleep(time) {
let task = () => {
setTimeout(() => {
this.next();
}, time*1000)
};
this.list.push(task);
return this;
}
}
也可以用promise来实现
class LazyMan {
constructor(name) {
this.name = name;
this.pro = Promise.resolve();
}
eat(val) {
this.pro = this.pro.then(() => {
console.log(`${this.name}吃${val}`);
})
return this;
}
sleep(time) {
this.pro = this.pro.then(() => {
return new Promise((res)=> {
setTimeout(() => {
res();
}, time*1000)
})
})
return this;
}
}
手写curry
function curry(fn) {
//...
}
function add(a,b,c) {
return a + b + c;
}
const curryAdd = curry(add);
console.log(curryAdd(1)(2)(3)); // 6
curry返回的是一个函数,
function curry(fn) {
let fnArgsLength = fn.length;//传入的函数形参长度
let args = [];
function calc() {
// 积累参数
args = [...args, ...arguments];
if (args.length < fnArgsLength) {
// 参数不够,返回函数
return calc;
} else {
// 参数够了,执行函数
return fn.apply(this, args.slice(0, fnArgsLength);
}
}
return calc;
}
手写instanceof
function myInstanceOf(left, right) {
if (left == null) return false; // null undefined 返回false
const type = typeof left; // 基础数据类型返回false
if (type !== 'object' && type !== 'function') {
return false;
}
let tempInstance = left // 为了防止修改left
while (tempInstance) {
if(tempInstance === right.prototype) {
return true;
}
tempInstance = tempInstance.__proto__;
}
return false;
}
手写bind
返回新函数 绑定this 同时绑定执行时的参数(apply或call)
Function.prototype.myBind = function (context = globalThis) {
const fn = this
const args = Array.from(arguments).slice(1)
const newFunc = function () {
const newArgs = args.concat(...arguments)
if (this instanceof newFunc) {
// 通过 new 调用
return new fn(...newArgs)
} else {
// 通过普通函数形式调用,绑定 context
return fn.apply(context, newArgs)
}
}
newFn.prototype = Object.create(fn.prototype);
return newFunc;
}
var fn = function(a, b, c){
console.log(this, a, b, c);
}
var a = fn.myBind({}, 1,2)
a(3)
// {} 1 2 3
// 支持new
const me = { name: 'Jack' }
function say() {
console.log(`My name is ${this.name || 'default'}`);
}
const meSay = say.myBind(me)
const obj = new meSay();
obj.__proto__ === say.prototype;// true
手写apply,call
如const obj = {x: 100,fn(){this.x}} 执行obj.fn(),此时fn的内部this指向obj 可借此来实现函数绑定this
Function.prototype.customCall = function(context, ...args) {
if (context == null) context = globalThis; // fn.call(null)时,浏览器环境下相当于fn.call(window)
if (typeof context !== 'object') context = new Object(context); // 值类型,变为
const fnKey = Symbol() // 不会出现属性名称覆盖
context[fnKey] = this; // this指向当前的函数
var res = context[fnKey](...args); // 绑定this
delete context[fnKey]; // 清理掉 fn,防止污染
return res;
}
var fn = function(a, b, c){
console.log(this, a, b, c);
}
fn.customCall({}, 1,2,3)
Function.prototype.customApply = function(context, args = []) {
if (context == null) context = globalThis; // fn.call(null)时,浏览器环境下相当于fn.call(window)
if (typeof context !== 'object') context = new Object(context); // 值类型,变为对象
const fnKey = Symbol() // 不会出现属性名称覆盖
context[fnKey] = this; // this指向当前的函数
var res = context[fnKey](...args); // 绑定this
delete context[fnKey]; // 清理掉 fn,防止污染
return res;
}
var fn = function(a, b, c){
console.log(this, a, b, c);
}
fn.customApply({}, [1,2,3])
#深拷贝
function deepCopy(obj, cache = new WeakMap()) {
if (!obj instanceof Object) return obj
// 防止循环引用
if (cache.get(obj)) return cache.get(obj)
// 支持函数
if (obj instanceof Function) {
return function () {
obj.apply(this, arguments)
}
}
// 支持日期
if (obj instanceof Date) return new Date(obj)
// 支持正则对象
if (obj instanceof RegExp) return new RegExp(obj.source, obj.flags)
// 还可以增加其他对象,比如:Map, Set等,根据情况判断增加即可,面试点到为止就可以了
// 数组是 key 为数字素银的特殊对象
const res = Array.isArray(obj) ? [] : {}
// 缓存 copy 的对象,用于处理循环引用的情况
cache.set(obj, res)
Object.keys(obj).forEach((key) => {
if (obj[key] instanceof Object) {
res[key] = deepCopy(obj[key], cache)
} else {
res[key] = obj[key]
}
});
return res
}
// 测试
const source = {
name: 'Jack',
meta: {
age: 12,
birth: new Date('1997-10-10'),
ary: [1, 2, { a: 1 }],
say() {
console.log('Hello');
}
}
}
source.source = source
const newObj = deepCopy(source)
console.log(newObj.meta.ary[2] === source.meta.ary[2]); // false
console.log(newObj.meta.birth === source.meta.birth); // false
eventBus
on和once注册函数,存储起来、 emit时找到对应的函数,执行 off找到对应函数,从对象中删除
class eventBus {
/**
* {
* 'key1': [{
* fn: function,
* isOnce: false
* }],
* 'key2': [],
* 'key3': []
* }
*/
private events: {
[key: string]: Array<{fn: Function; isOnce: Boolean}>
}
constructor() {
this.events = [];
}
on(type: string, fn: Function, isOnce: boolean = false) {
const events = this.events;
if(events[type] == null) {
events[type] = [];
}
events[type].push({ fn, isOnce });
}
once(type: string, fn: Function) {
this.on(type, fn, true);
}
off(type: string, fn?: Function) {
if (!fn) {
// 解绑所有type的函数
this.events[type] = [];
} else {
// 解绑单个函数
const fnList = this.events[type]
if (fnList) {
this.events[type] = fnList.filter(item => item.fn !== fn)
}
}
}
emit(type: string, ...args: any[]) {
const fnList = this.events[type]
if (fnList) {
this.events[type] = fnList.filter(item => {
const { fn, isOnce} = item;
fn(...args);
// once 执行一次就要被过滤掉
return !isOnce;
})
}
}
}
const e = new eventBus();
function fn1(a, b) {console.log('fn1', a, b)}
function fn2(a, b) {console.log('fn2', a, b)}
function fn3(a, b) {console.log('fn3', a, b)}
e.on('key1', fn1)
e.on('key1', fn2)
e.once('key1', fn3)
e.on('xxx', fn3)
e.emit('key1', 10, 20) // 出发fn1 fn2 fn3
e.emit('key1', 11, 21)
e.off('key1', fn1)
e.emit('key1', 100, 200) // 触发fn2
es5实现继承
function create(proto) {
const type = typeof proto
const isObject = type === 'function' || type === 'object' && !!proto
if (!isObject) return {}
function F() {}
F.prototype = proto;
return new F();
}
// Parent
function Parent(name) {
this.name = name
}
Parent.prototype.sayName = function () {
console.log(this.name)
};
// Child
function Child(age, name) {
Parent.call(this, name) // 执行父类的构造函数
this.age = age
}
Child.prototype = create(Parent.prototype) // 子类原型链接父类原型
Child.prototype.constructor = Child // 子类原型constructor指向构造函数
Child.prototype.sayAge = function () {
console.log(this.age)
}
// 测试
const child = new Child(18, 'Jack')
child.sayName()
child.sayAge()
实现LRU缓存
class LRUCache {
constructor(length) {
if (this.length < 1) throw new Error('invalid length');
this.length = length;
this.data = new Map()
}
set(key, val) {
const data = this.data;
if(data.has(key)) {
data.delete(key);
}
data.set(key, val);
if (data.size > this.length) {
// 如果超出了容量,则删除Map最老的元素
const delKey = data.keys().next().value;
data.delete(delKey);
}
}
get(key) {
const data = this.data;
if (!data.has(key)) return null;
const value = data.get(key);
data.delete(key);
data.set(key, value);
return value;
}
}
const lruCache = new LRUCache(2);
lruCache.set(1, 1);
lruCache.set(2, 2);
console.log(lruCache.get(1));// 1{2=2,1=1}
lruCache.set(3, 3); // {1=1,3=3}
console.log(lruCache.get(2));// null{1=1,3=3}
lruCache.set(4, 4);// {3=3, 4=4}
console.log(lruCache.get(1)); // null {3=3, 4=4}
console.log(lruCache.get(3)); // 3 {4=4, 3=3}
console.log(lruCache.get(4)); // 4 {3=3, 4=4}
异步并发数限制
/**
* 关键点
* 1. new promise 一经创建,立即执行
* 2. 使用 Promise.resolve().then 可以把任务加到微任务队列,防止立即执行迭代方法
* 3. 微任务处理过程中,产生的新的微任务,会在同一事件循环内,追加到微任务队列里
* 4. 使用 race 在某个任务完成时,继续添加任务,保持任务按照最大并发数进行执行
* 5. 任务完成后,需要从 doingTasks 中移出
*/
function limit(count, array, iterateFunc) {
const tasks = []
const doingTasks = []
let i = 0
const enqueue = () => {
if (i === array.length) {
return Promise.resolve()
}
const task = Promise.resolve().then(() => iterateFunc(array[i++]))
tasks.push(task)
const doing = task.then(() => doingTasks.splice(doingTasks.indexOf(doing), 1))
doingTasks.push(doing)
const res = doingTasks.length >= count ? Promise.race(doingTasks) : Promise.resolve()
return res.then(enqueue)
};
return enqueue().then(() => Promise.all(tasks))
}
// test
const timeout = i => new Promise(resolve => setTimeout(() => resolve(i), i))
limit(2, [1000, 1000, 1000, 1000], timeout).then((res) => {
console.log(res)
})
异步串行 | 异步并行
// 字节面试题,实现一个异步加法
function asyncAdd(a, b, callback) {
setTimeout(function () {
callback(null, a + b);
}, 500);
}
// 解决方案
// 1. promisify
const promiseAdd = (a, b) => new Promise((resolve, reject) => {
asyncAdd(a, b, (err, res) => {
if (err) {
reject(err)
} else {
resolve(res)
}
})
})
// 2. 串行处理
async function serialSum(...args) {
return args.reduce((task, now) => task.then(res => promiseAdd(res, now)), Promise.resolve(0))
}
// 3. 并行处理
async function parallelSum(...args) {
if (args.length === 1) return args[0]
const tasks = []
for (let i = 0; i < args.length; i += 2) {
tasks.push(promiseAdd(args[i], args[i + 1] || 0))
}
const results = await Promise.all(tasks)
return parallelSum(...results)
}
// 测试
(async () => {
console.log('Running...');
const res1 = await serialSum(1, 2, 3, 4, 5, 8, 9, 10, 11, 12)
console.log(res1)
const res2 = await parallelSum(1, 2, 3, 4, 5, 8, 9, 10, 11, 12)
console.log(res2)
console.log('Done');
})()
vue reactive
// Dep module
class Dep {
static stack = []
static target = null
deps = null
constructor() {
this.deps = new Set()
}
depend() {
if (Dep.target) {
this.deps.add(Dep.target)
}
}
notify() {
this.deps.forEach(w => w.update())
}
static pushTarget(t) {
if (this.target) {
this.stack.push(this.target)
}
this.target = t
}
static popTarget() {
this.target = this.stack.pop()
}
}
// reactive
function reactive(o) {
if (o && typeof o === 'object') {
Object.keys(o).forEach(k => {
defineReactive(o, k, o[k])
})
}
return o
}
function defineReactive(obj, k, val) {
let dep = new Dep()
Object.defineProperty(obj, k, {
get() {
dep.depend()
return val
},
set(newVal) {
val = newVal
dep.notify()
}
})
if (val && typeof val === 'object') {
reactive(val)
}
}
// watcher
class Watcher {
constructor(effect) {
this.effect = effect
this.update()
}
update() {
Dep.pushTarget(this)
this.value = this.effect()
Dep.popTarget()
return this.value
}
}
// 测试代码
const data = reactive({
msg: 'aaa'
})
new Watcher(() => {
console.log('===> effect', data.msg);
})
setTimeout(() => {
data.msg = 'hello'
}, 1000)
promise
// 建议阅读 [Promises/A+ 标准](https://promisesaplus.com/)
class MyPromise {
constructor(func) {
this.status = 'pending'
this.value = null
this.resolvedTasks = []
this.rejectedTasks = []
this._resolve = this._resolve.bind(this)
this._reject = this._reject.bind(this)
try {
func(this._resolve, this._reject)
} catch (error) {
this._reject(error)
}
}
_resolve(value) {
setTimeout(() => {
this.status = 'fulfilled'
this.value = value
this.resolvedTasks.forEach(t => t(value))
})
}
_reject(reason) {
setTimeout(() => {
this.status = 'reject'
this.value = reason
this.rejectedTasks.forEach(t => t(reason))
})
}
then(onFulfilled, onRejected) {
return new MyPromise((resolve, reject) => {
this.resolvedTasks.push((value) => {
try {
const res = onFulfilled(value)
if (res instanceof MyPromise) {
res.then(resolve, reject)
} else {
resolve(res)
}
} catch (error) {
reject(error)
}
})
this.rejectedTasks.push((value) => {
try {
const res = onRejected(value)
if (res instanceof MyPromise) {
res.then(resolve, reject)
} else {
reject(res)
}
} catch (error) {
reject(error)
}
})
})
}
catch(onRejected) {
return this.then(null, onRejected);
}
}
// 测试
new MyPromise((resolve) => {
setTimeout(() => {
resolve(1);
}, 500);
}).then((res) => {
console.log(res);
return new MyPromise((resolve) => {
setTimeout(() => {
resolve(2);
}, 500);
});
}).then((res) => {
console.log(res);
throw new Error('a error')
}).catch((err) => {
console.log('==>', err);
})
图片懒加载
// <img src="default.png" data-src="https://xxxx/real.png">
function isVisible(el) {
const position = el.getBoundingClientRect()
const windowHeight = document.documentElement.clientHeight
// 顶部边缘可见
const topVisible = position.top > 0 && position.top < windowHeight;
// 底部边缘可见
const bottomVisible = position.bottom < windowHeight && position.bottom > 0;
return topVisible || bottomVisible;
}
function imageLazyLoad() {
const images = document.querySelectorAll('img')
for (let img of images) {
const realSrc = img.dataset.src
if (!realSrc) continue
if (isVisible(img)) {
img.src = realSrc
img.dataset.src = ''
}
}
}
// 测试
window.addEventListener('load', imageLazyLoad)
window.addEventListener('scroll', imageLazyLoad)
// or
window.addEventListener('scroll', throttle(imageLazyLoad, 1000))