前端面试题系列文章:
【12】「2022」性能优化相关知识点
【13】「2022」H5相关知识点
X-Mind源文件
JavaScript基础
手写 new 操作符
在调用 new的过程中会发生以下四件事情:
- 创建一个新的空对象
- 给该空对象设置原型,将对象的原型指向为函数的
prototype对象 - 让函数的
this指向这个新对象,执行构造函数的代码(为这个新对象添加属性) - 判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象
/**
* @Desc 手写 New 操作符
*/
const myNew = (Cla) => {
// step1:创建一个空实例对象
const newObject = {};
// step2:将空实例对象的 __proto__ 指向函数的 prototype
newObject.__proto__ = Cla.prototype;
// step3: 执行构造函数的代码(为新对象添加属性),同时将函数内部的this替换成新对象
const result = Cla.call(newObject);
// step4: 对函数执行结果的类型进行判断:如果是简单类型则直接返回新对象,如果是引用类型就返回这个引用类型对象
return typeof result === 'object' || typeof result === 'function' ? result : newObject
}
function Car() {
this.name = 'Lamborghini';
this.prize = 10000000;
return { name: 'BMW' }
}
const myCar = myNew(Car);
console.log('myCar', myCar);
手写 Promise
首先,在写Promise之前需要了解几个点:Promise 内部有三个状态:pending(初始态)、resolved、rejected,状态一旦改变为resolved或rejected后则不能修改。
- 首先
Promise的构造函数是立即执行的,并且接受两个能改变内部状态的回调函数:触发rejected状态的函数、触发resolved状态的回调函数 - 在执行成功的回调函数和失败的回调函数之前判断
Promise内部的状态是否为初始态pending,如果已经改变过状态,则不响应。 Promise原型上有一个then方法,接受两个参数,分别是rejected状态的回调函数,resovled状态的回调函数,他们大概率不是立即执行的,所以需要在Promise内部维护一个数组去存储回调函数,等到状态发生改变再批量执行。
function MyPromise(executor){
const self = this;
// 内部维护一个状态,初始状态为 pending;
this.state = PENDING;
// 内部维护一个value,用来保存 resolve || reject 时的值
this.value = "";
this.onResolvedCallbacks = []; // Promise 可能在2s后状态才发生改变,所以需要将回调函数先保存下来
this.onRejectedCallbacks = []; // Promise 可能在2s后状态才发生改变,所以需要将回调函数先保存下来
function resolve(value){
// step2: resolve、reject调用之前必须保证状态没有被改变过,即pending状态
if (self.state === PENDING) {
self.state = RESOLVED;
self.value = value;
self.onResolvedCallbacks.forEach(fn => {fn(value)})
}
};
function reject(value){
// step2: resolve、reject调用之前必须保证状态没有被改变过,即pending状态
if (self.state === PENDING) {
self.state = REJECTED;
self.value = value;
self.onRejectedCallbacks.forEach(fn => {fn(value)})
}
};
// step1: 首先 executor 是立即执行的,接受 resolve,reject 函数作为参数
try {
executor(resolve, reject);
} catch (e) {
reject(e)
}
};
// step3: Promise的实例上有一个.then方法,接受两个参数:1. 成功的回调 2. 失败的回调
MyPromise.prototype.then = function(onResolved, onRejected){
// step4: 大概率会存在这一的情况:我们的.then在执行的时候,状态还没有改变。 我可能在 setTimeout 2s后才改变状态
if (this.state === PENDING) {
this.onResolvedCallbacks.push(() => onResolved(this.value));
this.onRejectedCallbacks.push(() => onRejected(this.value));
}
if (this.state === RESOLVED) {
onResolved(this.value);
}
if (this.state === REJECTED) {
onRejected(this.value);
}
}
手写 Promise.then
基于上面写的MyPromise,对.then做一些改造:首先.then返回的是一个新的Promise,支持链式调用;并且.then中拿到的回调必须是上一个Promise的成功回调。
- then函数中必须返回一个
Promise对象 - 在上一个
Promise完成后,返回一个结果。如果这个结果是个简单的值,就直接调用新的Promise的resolve,让其状态改变。如果返回的结果是个Promise,则需要等它完成之后再触发新Promise的resolve,保证链式调用的顺序
// Promise 如上
MyPromise.prototype.then = function(onResolved, onRejected){
const self = this;
// step1: 为了支持Promise的链式调用,Promise.then 会返回一个新的Promise
return new MyPromise((resolve, reject) => {
// 封装前一个Promise成功时执行函数
const onResolvedFn = () => {
try {
// 既然是成功的回调,那么当然先执行 onResolved 方法
const result = onResolved(self.value); // 承前
// 重点看这里!!!
// step2:上一个 onResolved 的结果如果是 Promise 对象 直接调用Promise.then。递归此过程,直到result不为Promise对象
return result instanceof MyPromise ? result.then(resolve, reject) : resolve(result) // 启后
} catch(e) {
reject(e);
}
}
// 封装前一个Promise失败时的执行函数
const onRejectedFn = () => {
try {
const result = onRejected(self.value);
return result instanceof MyPromise? result.then(resolve, reject) : reject(result);
} catch(e) {
console.log('e', e);
}
}
if (this.state === PENDING) {
this.onResolvedCallbacks.push(onResolvedFn);
this.onRejectedCallbacks.push(onRejectedFn);
}
if (this.state === RESOLVED) {
onResolvedFn();
}
if (this.state === REJECTED) {
onRejectedFn();
}
})
}
promise1.then(
(value) => {
console.log('then1 - value', value);
return new MyPromise((resolve) => {
resolve(value);
})
},
).then(
(value) => {
console.log('then2 - value', value);
value
}
);
手写Promise.all
- 首先Promise.all是绑定在类上的一个静态方法
- Promise.all接收一个数组,或者说是具有Iterator接口的对象作为参数
- 这个方法返回一个新的Promise
- 参数数组中所有的回调成功才是成功,且返回的的顺序和输入顺序保持一致
- 参数数组中有一个失败,则触发失败状态,第一个触发失败状态的 Promise 错误信息作为Promise.all的错误信息
MyPromise.all = function (promiseArr) {
if (!Array.isArray(promiseArr)) {
console.error("入参必须是数组");
}
return new MyPromise((resolve, reject) => {
let resultCount = 0; // 已经有结果的Promise
const resolvedArr = []; // 存放Promise resovled结果
for (let i = 0; i < promiseArr.length; i++) {
promiseArr[i].then((res) => {
resolvedArr.push(res);
resultCount += 1;
if (resultCount === promiseArr.length) {
// 所有成功了才成功
resolve(resolvedArr);
}
}, (error) => {
// 只要有一个失败了就失败
reject(error);
});
}
});
};
const p1 = new MyPromise((resolve, reject) => {
setTimeout(() => {
// reject('p1 reject');
resolve('p1 success');
}, 1000)
})
const p2 = new MyPromise((resolve) => {
setTimeout(() => {
resolve('p2 success');
}, 2000)
})
MyPromise.all([p1, p2]).then((res) => {
console.log('promise.all success!', res);
}, (res) => {
console.log('promise.all fail!', res);
})
手写 Promise.race
类比Promise.all,Promise.race只要有一个Promise的状态变为fullfilled | rejected的时候就执行。
MyPromise.race = function(promiseArr){
if (!Array.isArray(promiseArr)) {
console.err('入参必须为数组');
}
return new MyPromise((resolve, reject) => {
try {
for(let i = 0; i < promiseArr.length; i++) {
promiseArr[i].then((val) => {
return resolve(val)
}, (val) => {
return reject(val)
})
}
} catch(e) {
return reject(e);
}
})
}
const promiseArr = [Promise.resolve(1), Promise.resolve(2)]
// const promiseArr = [Promise.reject(1), Promise.resolve(2)]
MyPromise.race(promiseArr).then((resovledCB) => {
console.log('resovledCB', resovledCB);
}, (rejectedCB) => {
console.log('rejectedCB', rejectedCB);
});
手写防抖函数
函数防抖是指在时间被触发n秒后再执行回调,如果在n秒内事件又被触发,则重新计时。
- 首先,我们要知道防抖函数接收一个函数作为要防抖的函数,wait表示防抖的间隔时间,并且返回一个新的函数
- 我们利用计时器来保证在事件在一定时间内只触发一次
- 每次点击我们都将之前的计时器清除,重新启动一个新的计时器
function debounce(fn, wait) {
let timerId = null;
return function() {
// 保留上下文
const self = this;
const args = arguments;
// 如果有正在计时的定时器,清除该定时器
if (timerId) {
clearTimeout(timerId);
timerId = null;
}
// 重新计时
timerId = setTimeout(() => {
fn.apply(self, args);
}, wait)
}
}
手写节流函数
节流函数是指在n秒内只会执行一次。
function throttle(fn, wait) {
let canRun = true;
// 为了防止防抖函数第一次执行的时候 也需等待 wait 秒
let immediate = true;
return function () {
if (!canRun) {
return;
}
isFirst = false
const self = this;
const args = arguments;
canRun = false;
setTimeout(() => {
fn.apply(self, args);
canRun = true;
immediate = false;
}, immediate ? 0 : wait);
};
}
手写 call
在手写 call 函数之前,我们要明确两个点:首先 call 函数里的this指的是需要改变指向的函数,第一个参数为this绑定的对象
- 直接在函数的原型上加上
MyCall属性,所以在使用之前要判断下类型
// 在写MyCall 之前要特别注意两个点: 1. MyCall 里的this指的是需要改变指向的函数 2. 第一个参数为 this 绑定的对象
Function.prototype.MyCall = function(context) {
// 调用的对象类型必须是 Function 类型
if (typeof this !== 'function') {
console.error('调用的对象类型必须是 Function 类型');
}
// 获取传入的参数
const args = [...arguments].slice(1);
// 判断 context 是否传入,如果没有则为window
context = context || window;
// 将要执行的函数绑定为对象的方法
context.fn = this;
const result = context.fn(...args);
delete context.fn;
return result
}
function getName() {
this.name = 'zr111';
}
const obj = {name: 'zr'}
getName.call(obj);
console.log('obj', obj);
手写 apply
手写apply和call其实没什么区别,无非是传参的方式不一样罢了。
Function.prototype.MyApply = function(context) {
// 确保调用对象为函数
if (typeof this !== 'function') {
console.error('type error');
return;
}
// 获取参数
const args = arguments[1] || [];
// 判断 context 是否传入,没有默认为window
context = context || window;
// 将调用函数设为对象的方法
context.fn = this;
// 使用对象调用函数,并传参
const result = context.fn(...args);
// 删除 fn 方法,并将执行结果返回
return result;
}
之前一直对call和apply的API有所疑问,而且经常记不清哪个传参是直接展开的。其实call是比较早提出来的,后来为了方便开发者传参,又提供了一个apply的方法,两者在使用上还是有区别的,call通常来说性能更好。
手写 bind
bind的传参方式和call相同,不同的是bind内部不会立即执行得到结果,而是返回一个函数,让使用者决定在什么时候调用。
Function.prototype.Mybind = function (context) {
if (typeof context === "function") {
console.error("type error");
return;
}
// 绑定 this 对象兜底为 window
context = context || window;
// 获取参数
const args = [...arguments].slice(1);
// 将 this 函数作为 context 的属性
context.fn = this;
const returnFn = () => {
context.fn(...args);
};
delete context.fn;
return returnFn;
};
函数柯里化
函数柯里化的概念和作用就不再介绍了。柯里化后的函数只有在入参满足原函数个数时才会返回执行结果!
- 先判断参数个数是否大于原函数所需参数,如果参数个数足够,直接调用
- 如果参数不够,就先将当次调用的参数先保存下来,用户在下次调用的时候和下次的参数合并。
function curry(func,...args) {
// step1: 先判断参数个数是否大于原函数所需参数
const argsLen = func.length;
if (args.length >= argsLen) {
// return func(...args);
return func.apply(this, args);
} else {
// step2: 如果参数不够,就先将当次调用的参数先保存下来,用户在下次调用的时候和下次的参数合并。
return function(...nextArgs) {
return curry.apply(this, [func, ...args, ...nextArgs])
}
}
}
function log(date, importance, message) {
console.log((`[${date.getHours()}:${date.getMinutes()}] [${importance}] ${message}`));
}
const curriedLog = curry(log);
curriedLog(new Date, "WARNING", 'please check you passport !')
curriedLog(new Date())("DEBUG")("there’s some problem happend from your cache");
手写深拷贝
深拷贝主要是对飞简单类型进行递归处理,但是要注意几个细节:
- 正确处理正则、Date类型
- 保持原型链
- 正确处理key为
Symbol类型的字段 - 对象内的循环引用
function cloneDeep(target, map = new Map()) {
if (typeof target === "null") {
return null;
}
if (typeof target !== "object") {
return target;
}
// step1: 正确处理 Date 和 RegExp 类型
if (typeof target.constructor === Date) {
return new Date(target);
}
if (typeof target.constructor === RegExp) {
return new RegExp(target);
}
if (map.has(target)) return map.get(target);
map.set(target, newTarget);
// step2: 保持原型链
const newTarget = new target.constructor();
// step3: 正确处理key为`Symbol`类型的字段
Reflect.ownKeys(target).forEach((key) => {
newTarget[key] = cloneDeep(target, map);
});
return newTarget;
}