предисловие
Эта серия является предыдущим постом автораОкончание фронтенда высокочастотного интервьюДополнение будет больше тяготеть к вопросам мидл и хай-энд фронтенд интервью.Конечно, все прошли путь от новичка, и заинтересованные друзья могут его посмотреть.
первоначальное намерение
Я считаю, что одним из самых пугающих аспектов многих студентов во время собеседования является рукописный код Вы, должно быть, слышали это предложение.talk is cheap, show me the codeЭта статья не только содержит классические вопросы, написанные от руки для интервью с исходным кодом, но также содержит много анализа и рекомендаций, которые надеются помочь всем питаться лучше (Комментарии приветствуются, и банк дополнительных вопросов будет время от времени обновляться.)
Уведомление
Все написанные от руки реализации исходного кода в этой статье основаны на es6.Причины нежелания использовать нативные реализации следующие: С одной стороны, в Интернете слишком много схем нативной реализации.
1 promise
Думать сначала?
- что такое обещание
Решение для асинхронного обратного вызова
- Как обещание гарантирует выполнение кода после асинхронного выполнения?
Использование ключевого слова then принимает два параметра. Первый параметр (функция) будет выполнен после разрешения обещания. Второй параметр (функция) будет выполнен после отклонения обещания.
- Почему функция может быть запущена после обратного вызова завершения выполнения асинхронного события?
Внедрите механизм регистрации событий (зарегистрируйте код, чтобы инициировать событие после завершения асинхронного выполнения).
- Как обеспечить цепочку промисов в виде promise.then().then()
Затем каждый возвращает объект обещания
- Как узнать, что асинхронное событие завершилось или не удалось выполнить?
требуется представительство государства
Конкретная реализация выглядит следующим образом
//这里使用es6 class实现
class Mypromise {
constructor(fn) {
// 表示状态
this.state = "pending";
// 表示then注册的成功函数
this.successFun = [];
// 表示then注册的失败函数
this.failFun = [];
let resolve = val => {
// 保持状态改变不可变(resolve和reject只准触发一种)
if (this.state !== "pending") return;
// 成功触发时机 改变状态 同时执行在then注册的回调事件
this.state = "success";
// 为了保证then事件先注册(主要是考虑在promise里面写同步代码) promise规范 这里为模拟异步
setTimeout(() => {
// 执行当前事件里面所有的注册函数
this.successFun.forEach(item => item.call(this, val));
});
};
let reject = err => {
if (this.state !== "pending") return;
// 失败触发时机 改变状态 同时执行在then注册的回调事件
this.state = "fail";
// 为了保证then事件先注册(主要是考虑在promise里面写同步代码) promise规范 这里模拟异步
setTimeout(() => {
this.failFun.forEach(item => item.call(this, err));
});
};
// 调用函数
try {
fn(resolve, reject);
} catch (error) {
reject(error);
}
}
// 实例方法 then
then(resolveCallback, rejectCallback) {
// 判断回调是否是函数
resolveCallback =
typeof resolveCallback !== "function" ? v => v : resolveCallback;
rejectCallback =
typeof rejectCallback !== "function"
? err => {
throw err;
}
: rejectCallback;
// 为了保持链式调用 继续返回promise
return new Mypromise((resolve, reject) => {
// 将回调注册到successFun事件集合里面去
this.successFun.push(val => {
try {
// 执行回调函数
let x = resolveCallback(val);
//(最难的一点)
// 如果回调函数结果是普通值 那么就resolve出去给下一个then链式调用 如果是一个promise对象(代表又是一个异步) 那么调用x的then方法 将resolve和reject传进去 等到x内部的异步 执行完毕的时候(状态完成)就会自动执行传入的resolve 这样就控制了链式调用的顺序
x instanceof Mypromise ? x.then(resolve, reject) : resolve(x);
} catch (error) {
reject(error);
}
});
this.failFun.push(val => {
try {
// 执行回调函数
let x = rejectCallback(val);
x instanceof Mypromise ? x.then(resolve, reject) : reject(x);
} catch (error) {
reject(error);
}
});
});
}
//静态方法
static all(promiseArr) {
let result = [];
//声明一个计数器 每一个promise返回就加一
let count = 0
return new Mypromise((resolve, reject) => {
for (let i = 0; i < promiseArr.length; i++) {
promiseArr[i].then(
res => {
//这里不能直接push数组 因为要控制顺序一一对应(感谢评论区指正)
result[i] = res
count++
//只有全部的promise执行成功之后才resolve出去
if (count === promiseArr.length) {
resolve(result);
}
},
err => {
reject(err);
}
);
}
});
}
//静态方法
static race(promiseArr) {
return new Mypromise((resolve, reject) => {
for (let i = 0; i < promiseArr.length; i++) {
promiseArr[i].then(
res => {
//promise数组只要有任何一个promise 状态变更 就可以返回
resolve(res);
},
err => {
reject(err);
}
);
}
});
}
}
// 使用
let promise1 = new Mypromise((resolve, reject) => {
setTimeout(() => {
resolve(123);
}, 2000);
});
let promise2 = new Mypromise((resolve, reject) => {
setTimeout(() => {
resolve(1234);
}, 1000);
});
// Mypromise.all([promise1,promise2]).then(res=>{
// console.log(res);
// })
// Mypromise.race([promise1, promise2]).then(res => {
// console.log(res);
// });
promise1
.then(
res => {
console.log(res); //过两秒输出123
return new Mypromise((resolve, reject) => {
setTimeout(() => {
resolve("success");
}, 1000);
});
},
err => {
console.log(err);
}
)
.then(
res => {
console.log(res); //再过一秒输出success
},
err => {
console.log(err);
}
);
Расширения: какотменить обещание
Думать сначала?
Как я могу отменить асинхронность, которая уже была инициирована?
Метод Promise.race() можно использовать для конкуренции с обещанием, чье состояние изменяется первым и возвращается
Затем вы можете использовать это, чтобы обернуть поддельное обещание и обещание, которое будет инициировано для достижения
Конкретная реализация выглядит следующим образом
function wrap(pro) {
let obj = {};
// 构造一个新的promise用来竞争
let p1 = new Promise((resolve, reject) => {
obj.resolve = resolve;
obj.reject = reject;
});
obj.promise = Promise.race([p1, pro]);
return obj;
}
let testPro = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(123);
}, 1000);
});
let wrapPro = wrap(testPro);
wrapPro.promise.then(res => {
console.log(res);
});
wrapPro.resolve("被拦截了");
2 Дроссельная защита от сотрясений
подумай сначала?
- Разница между анти-встряской и дросселированием
СтабилизаторДа, функция будет выполняться только один раз в течение N секунд, если она будет запущена снова в течение N секунд, топересчитыватьВремя задержки (в крайнем случае, если событие прокрутки окна добавляет 2 секунды защиты от сотрясения и выполняется один раз, если вы продолжаете прокручивать и никогда не останавливаетесь, эта функция обратного вызова никогда не будет выполняться)
дросселированиезаключается в том, чтобы указать единицу времени, в течение которойФункция может быть запущена не более одного разаВыполнить (или событие прокрутки, если вы продолжаете прокручивать, тогда обратный вызов будет выполняться каждые 2 секунды)
- Как гарантировать защиту от тряски
Событие задерживается и снова запускается в течение заданного времени, и его необходимо очистить.Легко думать о setTimeout.
- Как гарантировать дросселирование
Срабатывает один раз в единицу времени, больше не действует, можно управлять флажком.
Конкретная реализация выглядит следующим образом
// 防抖
function debounce(fn, delay=300) {
//默认300毫秒
let timer;
return function() {
var args = arguments;
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
fn.apply(this, args); // 改变this指向为调用debounce所指的对象
}, delay);
};
}
window.addEventListener(
"scroll",
debance(() => {
console.log(111);
}, 1000)
);
// 节流
//方法一:设置一个标志
function throttle(fn, delay) {
let flag = true;
return () => {
if (!flag) return;
flag = false;
timer = setTimeout(() => {
fn();
flag = true;
}, delay);
};
}
//方法二:使用时间戳
function throttle(fn, delay) {
let startTime = new Date();
return () => {
let endTime = new Date();
if (endTime - startTime >= delay) {
fn();
startTime = endTime;
} else {
return;
}
};
}
window.addEventListener(
"scroll",
throttle(() => {
console.log(111);
}, 1000)
);
Дросселирование с защитой от сотрясений — это точка оптимизации производительности. Чтобы увидеть больше расширений для оптимизации производительности, нажмитеоптимизация производительности
3 EventEmitter (режим публикации и подписки — простая версия)
Думать сначала?
- Что такое модель публикации-подписки
Модель «публикация-подписка» фактически представляет собой зависимость между объектами «один ко многим».. Когда состояние объекта изменяется, все объекты, которые зависят от него, будут уведомлены об изменении состояния.
- Как добиться «один ко многим»
Поскольку для планирования событий в режиме «один ко многим» должен быть центр планирования событий, подписчики могут регистрировать события (включено) в центре событий. Издатели могут публиковать события (отправлять) в центр планирования. Подписчики также могут отписываться (отключаться) или только подписываться однажды (один раз)
Конкретная реализация выглядит следующим образом
// 手写发布订阅模式 EventEmitter
class EventEmitter {
constructor() {
this.events = {};
}
// 实现订阅
on(type, callBack) {
if (!this.events) this.events = Object.create(null);
if (!this.events[type]) {
this.events[type] = [callBack];
} else {
this.events[type].push(callBack);
}
}
// 删除订阅
off(type, callBack) {
if (!this.events[type]) return;
this.events[type] = this.events[type].filter(item => {
return item !== callBack;
});
}
// 只执行一次订阅事件
once(type, callBack) {
function fn() {
callBack();
this.off(type, fn);
}
this.on(type, fn);
}
// 触发事件
emit(type, ...rest) {
this.events[type] && this.events[type].forEach(fn => fn.apply(this, rest));
}
}
// 使用如下
const event = new EventEmitter();
const handle = (...rest) => {
console.log(rest);
};
event.on("click", handle);
event.emit("click", 1, 2, 3, 4);
event.off("click", handle);
event.emit("click", 1, 2);
event.once("dbClick", () => {
console.log(123456);
});
event.emit("dbClick");
event.emit("dbClick");
4 позвонить, подать заявку, связать
Думать сначала?
- использование вызовов
Первый параметр может изменить this вызывающей функции, чтобы указать на второй и последующие параметры в качестве параметров входящей функции.
let obj = {
a: 1
};
function fn(name, age) {
console.log(this.a); //1
console.log(name);
console.log(age);
}
fn.call(obj, "我是 lihua", "18");
- Как изменить этот указатель
Согласно вызову метода объекта этого атрибута, this внутри метода указывает на этот объект
let obj = {
a: 1,
fn(name, age) {
console.log(this.a); //1
console.log(name);
console.log(age);
}
};
obj.fn("我是lihua", "18");
- Как получить входящие переменные параметры?
Используйте es6 ... args, чтобы получить метод оставшихся параметров (остальные)
Конкретная реализация выглядит следующим образом
Function.prototype.myCall = function(context, ...args) {
if (!context || context === null) {
context = window;
}
// 创造唯一的key值 作为我们构造的context内部方法名
let fn = Symbol();
context[fn] = this; //this指向调用call的函数
// 执行函数并返回结果 相当于把自身作为传入的context的方法进行调用了
return context[fn](...args);
};
// apply原理一致 只是第二个参数是传入的数组
Function.prototype.myApply = function(context, args) {
if (!context || context === null) {
context = window;
}
// 创造唯一的key值 作为我们构造的context内部方法名
let fn = Symbol();
context[fn] = this;
// 执行函数并返回结果
return context[fn](...args);
};
//测试一下 call 和 apply
let obj = {
a: 1
};
function fn(name, age) {
console.log(this.a);
console.log(name);
console.log(age);
}
fn.myCall(obj, "我是lihua", "18");
fn.myApply(obj, ["我是lihua", "18"]);
let newFn = fn.myBind(obj, "我是lihua", "18");
newFn();
//bind实现要复杂一点 因为他考虑的情况比较多 还要涉及到参数合并(类似函数柯里化)
Function.prototype.myBind = function (context, ...args) {
if (!context || context === null) {
context = window;
}
// 创造唯一的key值 作为我们构造的context内部方法名
let fn = Symbol();
context[fn] = this;
let _this = this
// bind情况要复杂一点
const result = function (...innerArgs) {
// 第一种情况 :若是将 bind 绑定之后的函数当作构造函数,通过 new 操作符使用,则不绑定传入的 this,而是将 this 指向实例化出来的对象
// 此时由于new操作符作用 this指向result实例对象 而result又继承自传入的_this 根据原型链知识可得出以下结论
// this.__proto__ === result.prototype //this instanceof result =>true
// this.__proto__.__proto__ === result.prototype.__proto__ === _this.prototype; //this instanceof _this =>true
if (this instanceof _this === true) {
// 此时this指向指向result的实例 这时候不需要改变this指向
this[fn] = _this
this[fn](...[...args, ...innerArgs]) //这里使用es6的方法让bind支持参数合并
delete this[fn]
} else {
// 如果只是作为普通函数调用 那就很简单了 直接改变this指向为传入的context
context[fn](...[...args, ...innerArgs]);
delete context[fn]
}
};
// 如果绑定的是构造函数 那么需要继承构造函数原型属性和方法
// 实现继承的方式一: 构造一个中间函数来实现继承
// let noFun = function () { }
// noFun.prototype = this.prototype
// result.prototype = new noFun()
// 实现继承的方式二: 使用Object.create
result.prototype = Object.create(this.prototype)
return result
};
//测试一下
function Person(name, age) {
console.log(name); //'我是参数传进来的name'
console.log(age); //'我是参数传进来的age'
console.log(this); //构造函数this指向实例对象
}
// 构造函数原型的方法
Person.prototype.say = function() {
console.log(123);
}
let obj = {
objName: '我是obj传进来的name',
objAge: '我是obj传进来的age'
}
// 普通函数
function normalFun(name, age) {
console.log(name); //'我是参数传进来的name'
console.log(age); //'我是参数传进来的age'
console.log(this); //普通函数this指向绑定bind的第一个参数 也就是例子中的obj
console.log(this.objName); //'我是obj传进来的name'
console.log(this.objAge); //'我是obj传进来的age'
}
// 先测试作为构造函数调用
// let bindFun = Person.myBind(obj, '我是参数传进来的name')
// let a = new bindFun('我是参数传进来的age')
// a.say() //123
// 再测试作为普通函数调用
let bindFun = normalFun.myBind(obj, '我是参数传进来的name')
bindFun('我是参数传进来的age')
Реализация привязки Используйте знания о цепочке прототипов Если вы не знакомы с цепочкой прототипов js и наследованием, нажмитепортал
5 новый оператор
Думать сначала?
- Каково использование нового?
Создает объект экземпляра из конструктора. Конструктор this указывает на созданную функцию экземпляра и может использовать свойства и методы прототипа конструктора.
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.say = function() {
console.log(this.age);
};
let p1 = new Person("lihua", 18);
console.log(p1.name);
p1.say();
- Как добиться этого точки изменить?
call apply
- Как реализовать использование свойств и методов прототипа конструктора
цепочка прототипов наследование прототипов
Конкретная реализация выглядит следующим образом
function myNew(fn, ...args) {
// 1.创造一个实例对象
let obj = {};
// 2.生成的实例对象继承构造函数原型
// 方法一 粗暴的改变指向 完成继承
obj.__proto__ = fn.prototype;
// 方法二 利用Object.create实现
// obj=Object.create(fn.prototype)
// 3.改变构造函数this指向为实例对象
let result = fn.call(obj, ...args);
// 4. 如果构造函数执行的结果返回的是一个对象或者函数,那么返回这个对象或函数
if ((result && typeof result === "object") || typeof result === "function") {
return result;
}
//不然直接返回boj
return obj;
}
// 测试一下
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.say = function() {
console.log(this.age);
};
let p1 = myNew(Person, "lihua", 18);
console.log(p1.name);
console.log(p1);
p1.say();
Для глубокого понимания и изучения цепочки прототипов рекомендуется взглянутьпортал
6 instanceof
Думать сначала?
- Принцип instanceof?
Является ли объект-прототип (прототип) объекта справа выше цепочки прототипов объекта слева
- Как пройти по цепочке прототипов объекта слева — это ключевой момент?
while(true) проходит до конца цепочки прототипов, если null не равен, значит не существует и возвращает false
Конкретная реализация выглядит следующим образом
function myInstanceof(left, right) {
let leftProp = left.__proto__;
let rightProp = right.prototype;
// 一直会执行循环 直到函数return
while (true) {
// 遍历到了原型链最顶层
if (leftProp === null) {
return false;
}
if (leftProp === rightProp) {
return true;
} else {
// 遍历赋值__proto__做对比
leftProp = leftProp.__proto__;
}
}
}
// 测试一下
let a = [];
console.log(myInstanceof(a, Array));
7 глубокая копия
Думать сначала?
- Что такое глубокая копия?
Когда js копирует данные ссылочного типа, глубокая копия не копирует ссылку ссылочного типа, а копирует все значения ссылочного типа для формирования нового ссылочного типа, так что проблема путаницы ссылок не будет Позволяет нам использовать одни и те же данные несколько раз, не беспокоясь о конфликтах между данными.
- Как я могу скопировать их все?
Рекурсивно проходите до тех пор, пока тип данных не станет ссылочным типом перед назначением
Конкретная реализация выглядит следующим образом
// 定义一个深拷贝函数 接收目标target参数
function deepClone(target) {
// 定义一个变量
let result;
// 如果当前需要深拷贝的是一个对象的话
if (typeof target === 'object') {
// 如果是一个数组的话
if (Array.isArray(target)) {
result = []; // 将result赋值为一个数组,并且执行遍历
for (let i in target) {
// 递归克隆数组中的每一项
result.push(deepClone(target[i]))
}
// 判断如果当前的值是null的话;直接赋值为null
} else if(target===null) {
result = null;
// 判断如果当前的值是一个RegExp对象的话,直接赋值
} else if(target.constructor===RegExp){
result = target;
}else {
// 否则是普通对象,直接for in循环,递归赋值对象的所有值
result = {};
for (let i in target) {
result[i] = deepClone(target[i]);
}
}
// 如果不是对象的话,就是基本数据类型,那么直接赋值
} else {
result = target;
}
// 返回最终结果
return result;
}
расширение: эксплойтJSONметод достиженияПростойглубокая копия
let targetObj = JSON.parse(JSON.stringify(sourceObj))
но у него естьограничение
-
Невозможно скопировать undefined , функцию, регулярное выражение и т. д.
-
Конструктор объекта будет отброшен, и все конструкторы будут указывать на объект.
-
Объект имеет циклическую ссылку, будет сообщено об ошибке