面试题
手写继承
1、寄生组合式继承
function inheritPrototype(subType, superType) {
// 创建对象,创建父类原型的一个副本
var prototype = Object.create(superType.prototype);
// 增强对象,弥补因重写原型而失去的默认的constructor 属性
prototype.constructor = subType;
// 指定对象,将新创建的对象赋值给子类的原型
subType.prototype = prototype;
}
测试用例:
// 父类初始化实例属性和原型属性
function Father(name) {
this.name = name
this.colors = ['red', 'blue', 'green']
}
Father.prototype.sayName = function () {
alert(this.name)
}
// 借用构造函数传递增强子类实例属性(支持传参和避免篡改)
function Son(name, age) {
Father.call(this, name)
this.age = age
}
// 将父类原型指向子类
inheritPrototype(Son, Father)
// 新增子类原型属性
Son.prototype.sayAge = function () {
alert(this.age)
}
var demo1 = new Son('TianTian', 21)
var demo2 = new Son('TianTianUp', 20)
demo1.colors.push('2') // ["red", "blue", "green", "2"]
demo2.colors.push('3') // ["red", "blue", "green", "3"]
2、class继承
class Rectangle {
// constructor
constructor(height, width) {
this.height = height
this.width = width
}
// Getter
get area() {
return this.calcArea()
}
// Method
calcArea() {
return this.height * this.width
}
}
const rectangle = new Rectangle(40, 20)
console.log(rectangle.area)
// 输出 800
// 继承
class Square extends Rectangle {
constructor(len) {
// 子类没有this,必须先调用super
super(len, len)
// 如果子类中存在构造函数,则需要在使用“this”之前首先调用 super()。
this.name = 'SquareIng'
}
get area() {
return this.height * this.width
}
}
const square = new Square(20)
console.log(square.area)
// 输出 400
extends的核心实现逻辑如下:
function _inherits(subType, superType) {
// 创建对象,创建父类原型的一个副本
// 增强对象,弥补因重写原型而失去的默认的constructor 属性
// 指定对象,将新创建的对象赋值给子类的原型
subType.prototype = Object.create(superType && superType.prototype, {
constructor: {
value: subType,
enumerable: false,
writable: true,
configurable: true
}
})
if (superType) {
Object.setPrototypeOf ? Object.setPrototypeOf(subType, superType) : (subType.__proto__ = superType)
}
}
手写call/apply/bind
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')
手写new
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();
手写Object.create()
function create(proto) {
function Fn() {};
// 将Fn的原型指向传入的 proto
Fn.prototype = proto;
Fn.prototype.constructor = Fn;
return new Fn();
};
手写typeof
typeof 可以正确识别:Undefined、Boolean、Number、String、Symbol、Function 等类型的数据,但是对于其他的都会认为是 object,比如 Null、Date 等,所以通过 typeof 来判断数据类型会不准确。但是可以使用 Object.prototype.toString 实现。
function typeOf(obj) {
//let res = Object.prototype.toString.call(obj).split(' ')[1]
// res = res.substring(0, res.length - 1).toLowerCase()
// return res
// 更好的写法
return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase()
}
typeOf([]) // 'array'
typeOf({}) // 'object'
typeOf(new Date) // 'date'
手写instanceof
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));
手写浅拷贝、深拷贝、深比较、深合并
浅拷贝
浅拷贝是创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
//实现浅拷贝
function shallowCopy (obj){
// 只拷贝对象,基本类型或null直接返回
if(typeof obj !== 'object' || obj === null) {
return obj;
}
// 判断是新建一个数组还是对象
let newObj = Array.isArray(obj) ? []: {};
//for…in会遍历对象的整个原型链,如果只考虑对象本身的属性,需要搭配hasOwnProperty
for(let key in obj ){
//hasOwnProperty判断是否是对象自身属性,会忽略从原型链上继承的属性
if(obj.hasOwnProperty(key)){
newObj[key] = obj[key];//只拷贝对象本身的属性
}
}
return newObj;
}
//测试
var obj ={
name:'张三',
age:8,
pal:['王五','王六','王七']
}
let obj2 = shallowCopy(obj);
obj2.name = '李四'
obj2.pal[0] = '王麻子'
console.log(obj); //{age: 8, name: "张三", pal: ['王麻子', '王六', '王七']}
console.log(obj2); //{age: 8, name: "李四", pal: ['王麻子', '王六', '王七']}
深拷贝
简单的JSON.parse(JSON.stringify(sourceObj))局限性:
- 不可以拷贝 undefined , function, RegExp 等类型;
- 会抛弃对象的 constructor,所有的构造函数会指向 Object;
- 对象有循环引用,会报错。
// 定义一个深拷贝函数 接收目标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;
}
深比较
function _assignDeep(obj1, obj2) {
// 先把OBJ1中的每一项深度克隆一份赋值给新的对象
let obj = _cloneDeep(obj1);
// 再拿OBJ2替换OBJ中的每一项
for (let key in obj2) {
if (!obj2.hasOwnProperty(key)) break;
let v2 = obj2[key],
v1 = obj[key];
// 如果OBJ2遍历的当前项是个对象,并且对应的OBJ这项也是一个对象,此时不能直接替换,需要把两个对象重新合并一下,合并后的最新结果赋值给新对象中的这一项
if (typeof v1 === "object" && typeof v2 === "object") {
obj[key] = _assignDeep(v1, v2);
continue;
}
obj[key] = v2;
}
return obj;
}
手写节流与防抖函数
防抖是 N 秒内函数只会被执行一次,如果 N 秒内再次被触发,则重新计算延迟时间(举个极端的例子:如果 window 滚动事件添加了防抖 2s 执行一次,如果你不停地滚动永远不停下,那这个回调函数就永远无法执行)。
节流是规定一个单位时间,在这个单位时间内最多只能触发一次函数执行(例如滚动事件:如果你一直不停地滚动,那么每隔 2 秒就会执行一次回调)。
// 防抖
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)
);
数组操作(扁平化、去重、求和、最大最小值、交并差集、排序、乱序)
1、数组扁平化
let flatDeep = (arr) => {
return arr.reduce((res, cur) => {
if(Array.isArray(cur)){
return [...res, ...flatDep(cur)]
}else{
return [...res, cur]
}
},[])
}
「你想给面试官留下一个深刻印象的话」,可以这么写,👇
function flatDeep(arr, d = 1) {
return d > 0 ?
arr.reduce(
(acc, val) => acc.concat(Array.isArray(val) ? flatDeep(val, d - 1) : val), [])
: arr.slice();
};
// var arr1 = [1,2,3,[1,2,3,4, [2,3,4]]];
// flatDeep(arr1, Infinity);
可以传递一个参数,数组扁平化几维,简单明了,看起来逼格满满.
2、实现数组map方法
- 用法:
let array = [1, 2, 3].map((item) => {
return item * 2;
});
console.log(array); // [2, 4, 6]
- 实现:
Array.prototype.map = function(fn) {
let arr = [];
for(let i = 0; i < this.length; i++) {
arr.push(fn(this[i], i, this));
}
return arr;
};
3、实现数组reduce方法
- 特点:
- 初始值不传时的特殊处理:会默认用数组中的第一个元素
- 函数的返回结果会作为下一次循环的 prev
- 回调函数一共接收4个参数,分别是「上一次调用回调时返回的值、正在处理的元素、正在处理的元素的索引,正在遍历的集合对象」
- 用法:
let total = [1, 2, 3].reduce((prev, next, currentIndex, array) => {
return prev + next;
}, 0);
console.log(total); // 6
- 实现:
Array.prototype.reduce = function(fn, prev) {
for(let i = 0; i < this.length; i++) {
// 初始值不传时的处理
if (typeof prev === 'undefined') {
// 明确回调函数的参数都有哪些
prev = fn(this[i], this[i+1], i+1, this);
++i;
} else {
prev = fn(prev, this[i], i, this)
}
}
// 函数的返回结果会作为下一次循环的 prev
return prev;
};
4、实现数组去重
实现效果:把数组中的重复项去掉。例如: [1,2,3,4,2,1] => [1,2,3,4]
时间复杂度为O(n^2)的实现
let ary = [1, 2, 2, 3, 4, 2, 1, 3, 1, 3];
function unique(ary) {
// 双重for循环拿当前项和后面的每一项进行对比
for (let i = 0; i < ary.length; i++) {
let item = ary[i];
for (let j = i + 1; j < ary.length; j++) {
if (item == ary[j]) {
//如果当前项和后面的这一项相等,那么末尾一项赋值给后面的这一项,并且长度减一,
ary[j] = ary[ary.length - 1];
ary.length--;
j--;
}
}
}
return ary;
}
console.log(unique(ary));
时间复杂度为O(n)的实现
let ary = [1, 2, 2, 3, 4, 2, 1, 3, 1, 3];
function unique(ary) {
let obj = {};
for (let i = 0; i < ary.length; i++) {
let item = ary[i];
if (obj[item] !== undefined) {
ary[i] = ary[ary.length - 1];
ary.length--;
//解决数组塌陷
i--;
}
obj[item] = item;
}
return ary;
}
console.log(unique(ary));
使用ES6的Set实现
Set 是ES6中一种没有重复项的数据结构:阮一峰老师ES6语法的讲解
let ary = [1, 2, 2, 3, 4, 2, 1, 3, 1, 3];
ary = Array.from(new Set(ary));
console.log(ary);
5、数组乱序
function chaosArr(arr) {
for (let i = 0; i < arr.length; i ++) {
const randomIndex = Math.round(Math.random() * (arr.length - i - 1)) + i;
[arr[i], arr[randomIndex]] = [arr[randomIndex], arr[i]] // 交换位置
}
return arr;
}
字符串操作
1、数字千分化
// 保留三位小数
parseToMoney(1234.56); // return '1,234.56'
parseToMoney(123456789); // return '123,456,789'
parseToMoney(1087654.321); // return '1,087,654.321'
function parseToMoney(num) {
num = parseFloat(num.toFixed(3));
let [integer, decimal] = String.prototype.split.call(num, '.');
integer = integer.replace(/\d(?=(\d{3})+$)/g, '$&,');
return integer + '.' + (decimal ? decimal : '');
}
正则表达式(运用了正则的前向声明和反前向声明):
function parseToMoney(str){
// 仅仅对位置进行匹配
let re = /(?=(?!\b)(\d{3})+$)/g;
return str.replace(re,',');
}
2、驼峰命名转化
var s1 = "get-element-by-id"
// 转化为 getElementById
var f = function(s) {
return s.replace(/-\w/g, function(x) {
return x.slice(1).toUpperCase();
})
}
3、字符串查找
请使用最基本的遍历来实现判断字符串 a 是否被包含在字符串 b 中,并返回第一次出现的位置(找不到返回 -1)。
a='34';b='1234567'; // 返回 2
a='35';b='1234567'; // 返回 -1
a='355';b='12354355'; // 返回 5
isContain(a,b);
function isContain(a, b) {
for (let i in b) {
if (a[0] === b[i]) {
let tmp = true;
for (let j in a) {
if (a[j] !== b[~~i + ~~j]) {
tmp = false;
}
}
if (tmp) {
return i;
}
}
}
return -1;
}
4、解析 URL Params 为对象
let url = 'http://www.domain.com/?user=anonymous&id=123&id=456&city=%E5%8C%97%E4%BA%AC&enabled';
parseParam(url)
/* 结果
{ user: 'anonymous',
id: [ 123, 456 ], // 重复出现的 key 要组装成数组,能被转成数字的就转成数字类型
city: '北京', // 中文需解码
enabled: true, // 未指定值得 key 约定为 true
}
*/
function parseParam(url) {
const paramsStr = /.+?(.+)$/.exec(url)[1]; // 将 ? 后面的字符串取出来
const paramsArr = paramsStr.split('&'); // 将字符串以 & 分割后存到数组中
let paramsObj = {};
// 将 params 存到对象中
paramsArr.forEach(param => {
if (/=/.test(param)) { // 处理有 value 的参数
let [key, val] = param.split('='); // 分割 key 和 value
val = decodeURIComponent(val); // 解码
val = /^\d+$/.test(val) ? parseFloat(val) : val; // 判断是否转为数字
if (paramsObj.hasOwnProperty(key)) { // 如果对象有 key,则添加一个值
paramsObj[key] = [].concat(paramsObj[key], val);
} else { // 如果对象没有这个 key,创建 key 并设置值
paramsObj[key] = val;
}
} else { // 处理没有 value 的参数
paramsObj[param] = true;
}
})
return paramsObj;
}
5、用正则实现trim()
String.prototype.trim = function(){
return this.replace(/^\s+|\s+$/g, '')
}
//或者
function trim(string){
return string.replace(/^\s+|\s+$/g, '')
}
手写柯里化
- 特点: 柯里化就是将一个函数的功能细化,把接受「多个参数」的函数变换成接受一个「单一参数」的函数,并且返回接受「余下参数」返回结果的一种应用。
- 判断传递的参数是否达到执行函数的fn个数
- 没有达到的话,继续返回新的函数,将fn函数继续返回并将剩余参数累加
- 达到fn参数个数时,将累加后的参数传给fn执行
- 用法:
function sum(a, b, c, d, e) {
return a+b+c+d+e;
};
let a = curring(sum)(1,2)(3,4)(5);
console.log(a); // 15
- 实现:
const curring = (fn, arr = []) => {
let len = fn.length;
return function (...args) {
arr = [...arr, ...args];
if (arr.length < len) {
return curring(fn, arr);
} else {
return fn(...arr);
}
};
};
手写反柯里化
- 特点: 使用
call、apply可以让非数组借用一些其他类型的函数,比如,Array.prototype.push.call,Array.prototype.slice.call,uncrrying把这些方法泛化出来,不在只单单的用于数组,更好的语义化。 - 用法:
// 利用反柯里化创建检测数据类型的函数
let checkType = Object.prototype.toString.uncurring()
checkType(1); // [object Number]
checkType("hello"); // [object String]
checkType(true); // [object Boolean]
- 实现:
Function.prototype.uncurring = function () {
var self = this;
return function () {
return Function.prototype.call.apply(self, arguments);
}
};
手写promise
//这里使用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?
Promise.race()方法可以用来竞争 Promise 谁的状态先变更就返回谁。
那么可以借助这个api自己包装一个假的 promise 与要发起的 promise 竞争来实现。
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("被拦截了");
实现一个同时允许任务数量最大为n的函数(控制最大并发数为n)
function limitRunTask(tasks, n) {
return new Promise((resolve, reject) => {
let index = 0,
finish = 0,
start = 0,
res = []
function run() {
if (finish == tasks.length) {
resolve(res)
return
}
while (start < n && index < tasks.length) {
// 每一阶段的任务数量++
start++
let cur = index
tasks[index++]().then(v => {
start--
finish++
res[cur] = v
run()
})
}
}
run()
})
// 大概解释一下:首先如何限制最大数量n
// while 循环start < n,然后就是then的回调
}
手写async/await
function run(genF) {
// 返回值是Promise
return new Promise((resolve, reject) => {
const gen = genF();
function step(nextF) {
let next;
try {
// 执行该函数,获取一个有着value和done两个属性的对象
next = nextF();
} catch (e) {
// 出现异常则将该Promise变为rejected状态
reject(e);
}
// 判断是否到达末尾,Generator函数到达末尾则将该Promise变为fulfilled状态
if (next.done) {
return resolve(next.value);
}
// 没到达末尾,则利用Promise封装该value,直到执行完毕,反复调用step函数,实现自动执行
Promise.resolve(next.value).then((v) => {
step(() => gen.next(v))
}, (e) => {
step(() => gen.throw(e))
})
}
step(() => gen.next(undefined));
})
}
用async await实现一个中间件,计算函数执行时间
function createTimingMiddleware() {
return async (ctx, next) => {
const start = Date.now();
console.log('Function execution started at:', start);
// 调用下一个中间件或操作
await next();
const end = Date.now();
console.log('Function execution completed at:', end);
console.log(`Function execution took: ${end - start}ms`);
};
}
// 使用示例
const middleware = createTimingMiddleware();
// 假设你有一个需要计算执行时间的函数
const myFunction = async () => {
// 一些异步操作
await new Promise(resolve => setTimeout(resolve, 1000));
};
// 应用中间件
const timedFunction = middleware(myFunction);
// 执行被计算执行时间的函数
(async () => {
await timedFunction();
})();
手写JSON.stringify()、JSON.parse()
1、实现JSON.stringify
JSON.stringify(value[, replacer [, space]]):
Boolean | Number| String类型会自动转换成对应的原始值。undefined、任意函数以及symbol,会被忽略(出现在非数组对象的属性值中时),或者被转换成null(出现在数组中时)。- 不可枚举的属性会被忽略
- 如果一个对象的属性值通过某种间接的方式指回该对象本身,即循环引用,属性也会被忽略。
function jsonStringify(obj) {
let type = typeof obj;
if (type !== "object") {
if (/string|undefined|function/.test(type)) {
obj = '"' + obj + '"';
}
return String(obj);
} else {
let json = []
let arr = Array.isArray(obj)
for (let k in obj) {
let v = obj[k];
let type = typeof v;
if (/string|undefined|function/.test(type)) {
v = '"' + v + '"';
} else if (type === "object") {
v = jsonStringify(v);
}
json.push((arr ? "" : '"' + k + '":') + String(v));
}
return (arr ? "[" : "{") + String(json) + (arr ? "]" : "}")
}
}
jsonStringify({x : 5}) // "{"x":5}"
jsonStringify([1, "false", false]) // "[1,"false",false]"
jsonStringify({b: undefined}) // "{"b":"undefined"}"
2、实现JSON.parse
JSON.parse(text[, reviver])
用来解析JSON字符串,构造由字符串描述的JavaScript值或对象。提供可选的reviver函数用以在返回之前对所得到的对象执行变换(操作)。
3.1 第一种:直接调用 eval
function jsonParse(opt) {
return eval('(' + opt + ')');
}
jsonParse(jsonStringify({x : 5}))
// Object { x: 5}
jsonParse(jsonStringify([1, "false", false]))
// [1, "false", falsr]
jsonParse(jsonStringify({b: undefined}))
// Object { b: "undefined"}
避免在不必要的情况下使用
eval,eval() 是一个危险的函数, 他执行的代码拥有着执行者的权利。如果你用 eval()运行的字符串代码被恶意方(不怀好意的人)操控修改,您最终可能会在您的网页/扩展程序的权限下,在用户计算机上运行恶意代码。
它会执行JS代码,有XSS漏洞。
如果只想记这个方法,就得对参数json做校验。
var rx_one = /^[],:{}\s]*$/;
var rx_two = /\(?:["\/bfnrt]|u[0-9a-fA-F]{4})/g;
var rx_three = /"[^"\\n\r]*"|true|false|null|-?\d+(?:.\d*)?(?:[eE][+-]?\d+)?/g;
var rx_four = /(?:^|:|,)(?:\s*[)+/g;
if (
rx_one.test(
json
.replace(rx_two, "@")
.replace(rx_three, "]")
.replace(rx_four, "")
)
) {
var obj = eval("(" +json + ")");
}
3.2 第二种:Function
核心:Function与eval有相同的字符串参数特性。
var func = new Function(arg1, arg2, ..., functionBody);
在转换JSON的实际应用中,只需要这么做。
var jsonStr = '{ "age": 20, "name": "jack" }'
var json = (new Function('return ' + jsonStr))();
eval 与 Function 都有着动态编译js代码的作用,但是在实际的编程中并不推荐使用。
这里写这两种就够了。至于第三,第四种,涉及到繁琐的递归和状态机相关原理,具体可以看:
基于ES5/ES6实现双向绑定
1、es5版本
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<script>
const obj = {
value: ''
}
function onKeyUp(event) {
obj.value = event.target.value
}
// 对 obj.value 进行拦截
Object.defineProperty(obj, 'value', {
get: function() {
return value
},
set: function(newValue) {
value = newValue
document.querySelector('#value').innerHTML = newValue // 更新视图层
document.querySelector('input').value = newValue // 数据模型改变
}
})
</script>
</head>
<body>
<p>
值是:<span id="value"></span>
</p>
<input type="text" onkeyup="onKeyUp(event)">
</body>
</html>
2、es6版本
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<script>
const obj = {}
const newObj = new Proxy(obj, {
get: function(target, key, receiver) {
return Reflect.get(target, key, receiver)
},
set: function(target, key, value, receiver) {
if(key === 'value') {
document.querySelector('#value').innerHTML = value
document.querySelector('input').value = value
}
return Reflect.set(target, key, value, receiver)
}
})
function onKeyUp(event) {
newObj.value = event.target.value
}
</script>
</head>
<body>
<p>
值是:<span id="value"></span>
</p>
<input type="text" onkeyup="onKeyUp(event)">
</body>
</html>
手写sleep(一段时间后就去执行某个函数)
function sleep(fn, time) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(fn)
}, time)
})
}
let saySomething = name => console.log(`hello,${name}`)
async function autoPlay() {
let demo = await sleep(saySomething('TianTian'), 1000)
let demo2 = await sleep(saySomething('李磊'), 1000)
let demo3 = await sleep(saySomething('掘金的好友们'), 1000)
}
autoPlay()
事件委托
给li绑定点击事件
<ul id="xxx">
下面的内容是子元素1
<li>li内容>>> <span> 这是span内容123</span></li>
下面的内容是子元素2
<li>li内容>>> <span> 这是span内容123</span></li>
下面的内容是子元素3
<li>li内容>>> <span> 这是span内容123</span></li>
</ul>
错误版本:
ul.addEventListener('click', function (e) {
console.log(e,e.target)
if (e.target.tagName.toLowerCase() === 'li') {
console.log('打印') // 模拟fn
}
})
正确版本:
function delegate(element, eventType, selector, fn) {
element.addEventListener(eventType, e => {
let el = e.target;
while (!el.matches(selector)) {
if (element === el) { el = null; break; }
el = el.parentNode
}
el && fn.call(el, e, el);
},true);
return element
}
手写可以拖拽的div
<div id="xxx"></div>
var dragging = false
var position = null
xxx.addEventListener('mousedown',function(e){
dragging = true
position = [e.clientX, e.clientY]
})
document.addEventListener('mousemove', function(e){
if(dragging === false) return null
const x = e.clientX
const y = e.clientY
const deltaX = x - position[0]
const deltaY = y - position[1]
const left = parseInt(xxx.style.left || 0)
const top = parseInt(xxx.style.top || 0)
xxx.style.left = left + deltaX + 'px'
xxx.style.top = top + deltaY + 'px'
position = [x, y]
})
document.addEventListener('mouseup', function(e){
dragging = false
})
手写模板引擎
// ===== my-template.html =====
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
{{name}} {{age}}
{%arr.forEach(item => {%}
<li>{{item}}</li>
{%})%}
</body>
</html>
const fs = require('fs');
const path = require('path');
const renderFile = (filePath, obj, cb) => {
fs.readFile(filePath, 'utf8', function(err, html) {
if(err) {
return cb(err, html);
}
html = html.replace(/\{\{([^}]+)\}\}/g, function() {
console.log(arguments[1], arguments[2]);
let key = arguments[1].trim();
return '${' + key + '}';
});
let head = `let str = '';\r\n with(obj){\r\n`;
head += 'str+=`';
html = html.replace(/\{\%([^%]+)\%\}/g, function() {
return '`\r\n' + arguments[1] + '\r\nstr+=`\r\n';
});
let tail = '`}\r\n return str;';
let fn = new Function('obj', head + html + tail);
cb(err, fn(obj));
});
};
renderFile(path.resolve(__dirname, 'my-template.html'),{name: 'Cherry', age: 27, arr: [1, 2, 3]}, function(err, data) {
console.log(data);
});
常用算法(排序、深度/广度优先搜索)
冒泡排序
var ary = [3, 1, 5, 2];
function bubble(ary) {
// 比的轮数,最后一轮不用,前面几轮比完之后,最后那个肯定就是最小的
for (var i = 0; i < ary.length - 1; i++) {
//两两进行比较
for (var j = 0; j < ary.length - 1 - i; j++) {
if (ary[j] > ary[j + 1]) {
//解构赋值
[ary[j], ary[j + 1]] = [ary[j + 1], ary[j]]
}
}
}
return ary;
}
var res = bubble(ary);
console.log(res);
快速排序
function quickSort(ary){
if(ary.length<1){
return ary;
}
var centerIndex=Math.floor(ary.length/2);
// 拿到中间项的同时,把中间项从数组中删除掉
var centerValue=ary.splice(centerIndex,1)[0];
// 新建两个数组:leftAry,rightAry;把ary中剩余的项,给中间项做对比,如果大项就放到右数组,小项就放到左数组.
var leftAry=[],rightAry=[];
for(var i=0;i<ary.length;i++){
if(ary[i]<centerValue){
leftAry.push(ary[i]);
}else{
rightAry.push(ary[i]);
}
}
return quickSort(leftAry).concat(centerValue,quickSort(rightAry));
}
var ary=[12,15,14,13,16,11];
var res=quickSort(ary);
console.log(res);
插入排序
var ary = [34, 56, 12, 66, 12];
function insertSort(ary) {
//最终排序好的数组盒子
var newAry = [];
//拿出的第一项放进去,此时盒子中只有一项,不用个比较
newAry.push(ary[0]);
// 依次拿出原数组中的每一项进行插入
for (var i = 1; i < ary.length; i++) {
var getItem = ary[i];
// 在插入的时候需要跟新数组中的每一项进行比较(从右向左)
for (var j = newAry.length - 1; j >= 0; j--) {
var newItemAry = newAry[j];
if (getItem >= newItemAry) {
// 如果拿出的项比某项大或者相等,就放到此项的后面
newAry.splice(j + 1, 0, getItem);
// 插入完毕,不用再继续比较停止循环;
break;
}
if (j == 0) {
//如果都已经比到第一项了,还没满足条件,说明这个就是最小项,我们之间插入到数组的最前面
newAry.unshift(getItem);
}
}
}
return newAry;
}
var res = insertSort(ary);
console.log(res);
堆排序
将乱序数组[5,8,0,10,4,6,1]降序排列
步骤:
- 构造最小堆
- 循环提取根节点, 直到全部提取完
const minHeapSort = (arr) => {
// 1. 构造最小堆
buildMinHeap(arr);
// 2. 循环提取根节点arr[0], 直到全部提取完
for (let i = arr.length - 1; i > 0; i--) {
let tmp = arr[0];
arr[0] = arr[i];
arr[i] = tmp;
siftDown(arr, 0, i - 1);
}
};
// 把整个数组构造成最小堆
const buildMinHeap = (arr) => {
if (arr.length < 2) {
return arr;
}
const startIndex = Math.floor(arr.length / 2 - 1);
for (let i = startIndex; i >= 0; i--) {
siftDown(arr, i, arr.length - 1);
}
};
// 从startIndex索引开始, 向下调整最小堆
const siftDown = (arr, startIndex, endIndex) => {
const leftChildIndx = 2 * startIndex + 1;
const rightChildIndx = 2 * startIndex + 2;
let swapIndex = startIndex;
let tmpNode = arr[startIndex];
if (leftChildIndx <= endIndex) {
if (arr[leftChildIndx] < tmpNode) {
// 待定是否交换, 因为right子节点有可能更小
tmpNode = arr[leftChildIndx];
swapIndex = leftChildIndx;
}
}
if (rightChildIndx <= endIndex) {
if (arr[rightChildIndx] < tmpNode) {
// 比left节点更小, 替换swapIndex
tmpNode = arr[rightChildIndx];
swapIndex = rightChildIndx;
}
}
if (swapIndex !== startIndex) {
// 1.交换节点
arr[swapIndex] = arr[startIndex];
arr[startIndex] = tmpNode;
// 2. 递归调用, 继续向下调整
siftDown(arr, swapIndex, endIndex);
}
};
测试:
var arr1 = [5, 8, 0, 10, 4, 6, 1];
minHeapSort(arr1);
console.log(arr1); // [10, 8, 6, 5,4, 1, 0]
var arr2 = [5];
minHeapSort(arr2);
console.log(arr2); // [ 5 ]
var arr3 = [5, 1];
minHeapSort(arr3);
console.log(arr3); //[ 5, 1 ]
深度优先遍历
DFS 的主流实现方式有 2 种.
- 递归(简单粗暴)
- 利用
栈存储遍历路径
function Node() {
this.name = '';
this.children = [];
}
function dfs(node) {
console.log('探寻阶段: ', node.name);
node.children.forEach((child) => {
dfs(child);
});
console.log('回溯阶段: ', node.name);
}
- 使用栈
function Node() {
this.name = '';
this.children = [];
// 因为要分辨探寻阶段和回溯阶段, 所以必须要一个属性来记录是否已经访问过该节点
// 如果不打印探寻和回溯, 就不需要此属性
this.visited = false;
}
function dfs(node) {
const stack = [];
stack.push(node);
// 栈顶元素还存在, 就继续循环
while ((node = stack[stack.length - 1])) {
if (node.visited) {
console.log('回溯阶段: ', node.name);
// 回溯完成, 弹出该元素
stack.pop();
} else {
console.log('探寻阶段: ', node.name);
node.visited = true;
// 利用栈的先进后出的特性, 倒序将节点送入栈中
for (let i = node.children.length - 1; i >= 0; i--) {
stack.push(node.children[i]);
}
}
}
}
LRU
class LRU {
constructor(capacity) {
this.cache = new Map()
this.capacity = capacity
}
get(key) {
if (this.cache.has(key)) {
const temp = this.cache.get(key)
this.cache.delete(key)
this.cache.set(key, temp)
return temp
}
return undefined
}
set(key, value) {
if (this.cache.has(key)) {
this.cache.delete(key)
} else if (this.cache.size >= this.capacity) {
this.cache.delete(this.cache.keys().next().value)
}
this.cache.set(key, value)
}
}
淘汰算法的一种,最久远使用频率最低的元素最容易被淘汰,通过一个Map来维护一个队列结构,当队列长度超过设置好的阈值,则将队尾元素出队。这里使用Map结构存储数据,因为JS里面的Map结构可以保持数据的存入顺序,符合队列先进先出的特性。
常见设计模式(观察者模式和发布订阅模式区别)
单例模式
1、全局对象(对象字面量)
const Singleton = {
property: 'value',
method: function() {
console.log('This is a singleton method.');
}
};
// 使用示例
Singleton.method(); // 输出: This is a singleton method.
2、es6中class
// singleton.js
class Singleton {
constructor() {
if (!Singleton.instance) {
Singleton.instance = this;
}
return Singleton.instance;
}
someMethod() {
console.log('Singleton method called.');
}
}
export default new Singleton();
3、闭包和模块导出
const getSingleton = (function() {
let instance;
function createInstance() {
const object = new Singleton(); // 假设Singleton是一个类定义
return object;
}
return {
getInstance: function() {
if (!instance) {
instance = createInstance();
}
return instance;
}
};
})();
class Singleton {
someMethod() {
console.log('Singleton method called.');
}
}
// 使用示例
const singleton = getSingleton.getInstance();
singleton.someMethod(); // 输出: Singleton method called.
4、使用proxy
const _instance = (function() {
let instance;
function createInstance() { /* 一些初始化代码 */ }
return { get: function() { return instance || (instance = createInstance()); } };
})();
const singleton = new Proxy({}, {
get: function(target, prop) { return _instance.get()[prop]; }
});
singleton.someMethod = function() {
console.log('Singleton method called.');
};
singleton.someMethod(); // 输出: Singleton method called.
发布订阅模式
// 手写发布订阅模式 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");
观察者模式
class Subject{
constructor(name){
this.name = name
this.observers = []
this.state = 'XXXX'
}
// 被观察者要提供一个接受观察者的方法
attach(observer){
this.observers.push(observer)
}
// 改变被观察着的状态
setState(newState){
this.state = newState
this.observers.forEach(o=>{
o.update(newState)
})
}
}
class Observer{
constructor(name){
this.name = name
}
update(newState){
console.log(`${this.name}say:${newState}`)
}
}
// 被观察者 灯
let sub = new Subject('灯')
let mm = new Observer('小明')
let jj = new Observer('小健')
// 订阅 观察者
sub.attach(mm)
sub.attach(jj)
sub.setState('灯亮了来电了')
vue源码
react源码
手写redux核心原理
createStore里的实现,根据是否传入了中间件做处理
export default function createStore(reducer, enhancer) {
if (typeof enhancer !== 'undefined') {
return enhancer(createStore)(reducer)
}
let state = null
const listeners = []
const subscribe = (listener) => {
listeners.push(listener)
}
const getState = () => state
const dispatch = (action) => {
state = reducer(state, action)
listeners.forEach((listener) => listener())
}
dispatch({})
return { getState, dispatch, subscribe }
}
- 中间件实现,通过
reduce,将上次的结果逐个传入,核心在于compose,支持了多个中间件使用.
import compose from './compose';
export default function applyMiddleware(...middlewares) {
return (createStore) => (reducer) => {
const store = createStore(reducer)
let dispatch = store.dispatch
let chain = []
const middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
}
chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
export default function compose(...funcs) {
return funcs.reduce((a, b) => (...args) => a(b(...args)));
}
webpack源码(tappable、HMR)
-
webpack打包过程
- 1.识别入口文件
- 2.通过逐层识别模块依赖。(Commonjs、amd或者es6的import,webpack都会对其进行分析。来获取代码的依赖)
- 3.webpack做的就是分析代码。转换代码,编译代码,输出代码
- 4.最终形成打包后的代码
-
webpack打包原理
- 1.先逐级递归识别依赖,构建依赖图谱
- 2.将代码转化成
AST抽象语法树 ,一个AST抽象语法树如下所示:
Node { type: 'File', start: 0, end: 32, loc: SourceLocation { start: Position { line: 1, column: 0 }, end: Position { line: 1, column: 32 } }, program: Node { type: 'Program', start: 0, end: 32, loc: SourceLocation { start: [Position], end: [Position] }, sourceType: 'module', interpreter: null, body: [ [Node] ], directives: [] }, comments: [] }- 3.在
AST阶段中去处理代码 - 4.把
AST抽象语法树变成浏览器可以识别的代码, 然后输出
-
核心实现过程
-
将代码转化成AST,并且收集依赖
const fs = require('fs');
const path = require('path');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
// traverse 采用的 ES Module 导出,我们通过 requier 引入的话就加个 .default
const babel = require('@babel/core');
const read = fileName => {
const buffer = fs.readFileSync(fileName, 'utf-8');
const AST = parser.parse(buffer, { sourceType: 'module' });
console.log(AST);
// 依赖收集
const dependencies = {};
// 使用 traverse 来遍历 AST
traverse(AST, {
ImportDeclaration({ node }) { // 函数名是 AST 中包含的内容,参数是一些节点,node 表示这些节点下的子内容
const dirname = path.dirname(filename); // 我们从抽象语法树里面拿到的路径是相对路径,然后我们要处理它,在 bundler.js 中才能正确使用
const newDirname = './' + path.join(dirname, node.source.value).replace('\', '/'); // 将dirname 和 获取到的依赖联合生成绝对路径
dependencies[node.source.value] = newDirname; // 将源路径和新路径以 key-value 的形式存储起来
}
})
// 将抽象语法树转换成浏览器可以运行的代码
const { code } = babel.transformFromAst(AST, null, {
presets: ['@babel/preset-env']
})
return {
filename,
dependencies,
code
}
};
read('./test1.js');
- 绘制依赖图谱
// 创建依赖图谱函数, 递归遍历所有依赖模块
const makeDependenciesGraph = (entry) => {
const entryModule = read(entry)
const graghArray = [ entryModule ]; // 首先将我们分析的入口文件结果放入图谱数组中
for (let i = 0; i < graghArray.length; i ++) {
const item = graghArray[i];
const { dependencies } = item; // 拿到当前模块所依赖的模块
if (dependencies) {
for ( let j in dependencies ) { // 通过 for-in 遍历对象
graghArray.push(read(dependencies[j])); // 如果子模块又依赖其它模块,就分析子模块的内容
}
}
}
const gragh = {}; // 将图谱的数组形式转换成对象形式
graghArray.forEach( item => {
gragh[item.filename] = {
dependencies: item.dependencies,
code: item.code
}
})
console.log(gragh)
return gragh;
}
- 打印
gragh得到的对象:
{ './app.js':
{ dependencies: { './test1.js': './test1.js' },
code:
'"use strict";\n\nvar _test = _interopRequireDefault(require("./test1.js"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n\nconsole.log(test
1);' },
'./test1.js':
{ dependencies: { './test2.js': './test2.js' },
code:
'"use strict";\n\nvar _test = _interopRequireDefault(require("./test2.js"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n\nconsole.log('th
is is test1.js ', _test["default"]);' },
'./test2.js':
{ dependencies: {},
code:
'"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\nexports["default"] = void 0;\n\nfunction test2() {\n console.log('this is test2 ');\n}\n\nvar _default = tes
t2;\nexports["default"] = _default;' } }
- 获取编译后的代码
const generateCode = (entry) => {
// 注意:我们的 gragh 是一个对象,key是我们所有模块的绝对路径,需要通过 JSON.stringify 来转换
const gragh = JSON.stringify(makeDependenciesGraph(entry));
// 我们知道,webpack 是将我们的所有模块放在闭包里面执行的,所以我们写一个自执行的函数
// 注意: 我们生成的代码里面,都是使用的 require 和 exports 来引入导出模块的,而我们的浏览器是不认识的,所以需要构建这样的函数
return `
(function( gragh ) {
function require( module ) {
// 相对路径转换成绝对路径的方法
function localRequire(relativePath) {
return require(gragh[module].dependencies[relativePath])
}
const exports = {};
(function( require, exports, code ) {
eval(code)
})( localRequire, exports, gragh[module].code )
return exports;
}
require('${ entry }')
})(${ gragh })
`;
}
const code = generateCode('./app.js');
console.log(code)
- 得到编译输出的代码
code如下:
(function( gragh ) {
function require( module ) {
// 相对路径转换成绝对路径的方法
function localRequire(relativePath) {
return require(gragh[module].dependencies[relativePath])
}
const exports = {};
(function( require, exports, code ) {
eval(code)
})( localRequire, exports, gragh[module].code )
return exports;
}
require('./app.js')
})({"./app.js":{"dependencies":{"./test1.js":"./test1.js"},"code":""use strict";\n\nvar _test = _interopRequireDefault(require("./test1.js"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n\nconsole.log(_test["default"]);"},"./test1.js":{"dependencies":{"./test2.js":"./test2.js"},"code":""use strict";\n\nvar _test = _interopRequireDefault(require("./test2.js"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n\nconsole.log('this is test1.js ', _test["default"]);"},"./test2.js":{"dependencies":{},"code":""use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\nexports["default"] = void 0;\n\nfunction test2() {\n console.log('this is test2 ');\n}\n\nvar _default = test2;\nexports["default"] = _default;"}})
- 复制这段代码到浏览器中运行即可
实现tree-shaking
import ast
def is_node_side_effect_free(node):
# 判断节点是否为副作用免的表达式
return isinstance(node, ast.Expr) and isinstance(node.value, ast.Constant)
def perform_tree_shaking(tree):
# 遍历AST,移除副作用免的节点
for node in tree.body:
if is_node_side_effect_free(node):
tree.body.remove(node)
# 示例代码
code = """
console.log('Hello, World!');
var x = 1 + 2;
console.log(x);
"""
# 使用ast模块解析代码
tree = ast.parse(code)
# 执行tree-shaking
perform_tree_shaking(tree)
# 将修改后的AST编译回代码
compiled_code = astunparse.unparse(tree)
print(compiled_code)
nodejs源码
1、实现中间件/洋葱模型
// 定义中间件函数类型
function middleware(ctx, next) {
// 这里可以编写中间件的前置逻辑
console.log('前置逻辑');
// 调用 next 函数触发下一个中间件
next();
// 这里可以编写中间件的后置逻辑
console.log('后置逻辑');
}
// 定义洋葱模型类
class Onion {
constructor() {
// 用于存储中间件的数组
this.middlewares = [];
}
// 注册中间件的方法
use(middleware) {
this.middlewares.push(middleware);
return this;
}
// 执行中间件的方法
execute(ctx) {
const dispatch = (index) => {
if (index === this.middlewares.length) {
return;
}
const currentMiddleware = this.middlewares[index];
return currentMiddleware(ctx, () => dispatch(index + 1));
};
return dispatch(0);
}
}
// 使用示例
const onion = new Onion();
// 注册中间件
onion.use((ctx, next) => {
console.log('中间件 1 前置逻辑');
next();
console.log('中间件 1 后置逻辑');
}).use((ctx, next) => {
console.log('中间件 2 前置逻辑');
next();
console.log('中间件 2 后置逻辑');
});
// 创建上下文对象
const context = {};
// 执行中间件
onion.execute(context);
2、手写koa
// mykoa.js
const http = require('http');
class MyKoa {
constructor() {
// 中间件数组
this.middlewares = [];
}
use(middleware) {
this.middlewares.push(middleware);
}
listen(port) {
// 创建server
const server = http.createServer((req, res) =>
this.handleRequest(req, res)
);
// 监听端口
server.listen(port);
}
handleRequest(req, res) {
const ctx = this.createContext(req, res);
return this.compose(this.middlewares, ctx)
.then(() => {
if (!ctx.body) {
res.statusCode = 404;
res.end('Not Found');
} else {
res.end(ctx.body);
}
})
.catch((err) => {
res.statusCode = 503;
res.end('Server Error');
console.error(err);
});
}
createContext(req, res) {
const context = {
req,
res,
state: {},
};
context.request = context.req;
context.response = context.res;
return context;
}
// 中间件最核心的代码
compose(middlewares, context) {
// 返回promise处理异步函数
const dispatch = (i) => {
if (i >= middlewares.length) return Promise.resolve();
const middleware = middlewares[i];
// 对应middleware的函数(ctx, next) => {}
// 其中next使用递归dispatch(i + 1)处理
return Promise.resolve(middleware(context, () => dispatch(i + 1)));
};
return dispatch(0);
}
}
module.exports = MyKoa;
// server.js
const MyKoa = require('./mykoa')
const app = new MyKoa();
app.use(async (ctx, next) => {
console.log(`${ctx.req.method} ${ctx.req.url}`);
console.log(`1`);
await next();
console.log(`2`);
});
app.use(async (ctx, next) => {
console.log(`3`);
ctx.body = 'Hello, MyKoa!';
});
app.listen(3000);
手写微前端框架原理
-
微前端的模式:
-
微前端原理:
- 通过fetch请求,通过配置的
entry入口,去对应的地址拉取index.html文件,获取他们所需的资源和所有标签、DOM节点 - 拉取他们的资源全部字符串化.
- 把资源生成对应
DOM节点和标签塞入基座中以及用key-value形式缓存内存中(避免重复发送请求拉取) - 像子应用一样渲染和交互
- 通过fetch请求,通过配置的
-
代码实现:
-
加载子应用
async function loadApp() {
const shouldMountApp = Apps.filter(shouldBeActive);
const App = shouldMountApp.pop();
fetch(App.entry)
.then(function (response) {
return response.text();
})
.then(async function (text) {
const dom = document.createElement('div');
dom.innerHTML = text;
const entryPath = App.entry;
const subapp = document.querySelector('#subApp-content');
subapp.appendChild(dom);
handleScripts(entryPath, subapp, dom);
handleStyles(entryPath, subapp, dom);
});
}
- 拉取&生成资源标签等
async function handleScripts(entryPath, subapp, dom) {
const scripts = dom.querySelectorAll('script');
const paromiseArr =
scripts &&
Array.from(scripts).map((item) => {
if (item.src) {
const url = window.location.protocol + '//' + window.location.host;
return fetch(`${entryPath}/${item.src}`.replace(url, '')).then(
function (response) {
return response.text();
}
);
} else {
return Promise.resolve(item.textContent);
}
});
const res = await Promise.all(paromiseArr);
if (res && res.length > 0) {
res.forEach((item) => {
const script = document.createElement('script');
script.innerText = item;
subapp.appendChild(script);
});
}
}
实现红绿灯效果
const timeout = (time) => {
return new Promise(resolve => setTimeout(resolve, time))
}
var el = document.querySelector('body')
function change(){
timeout(2000).then(() => {
el.style.backgroundColor = 'red';
return timeout(3000)
}).then(() => {
el.style.backgroundColor = 'yellow';
return timeout(1000)
}).then(() => {
el.style.backgroundColor = 'green';
change()
})
}
change();
图片懒加载
1)首先,不要将图片地址放到src属性中,而是放到其它属性(data-original)中。
2)页面加载完成后,根据scrollTop判断图片是否在用户的视野内,如果在,则将data-original属性中的值取出存放到src属性中。
3)在滚动事件中重复判断图片是否进入视野,如果进入,则将data-original属性中的值取出存放到src属性中。
elementNode.getAttribute(name):方法通过名称获取属性的值。
elementNode.setAttribute(name, value):方法创建或改变某个新属性。
elementNode.removeAttribute(name):方法通过名称删除属性的值。
//懒加载代码实现
var viewHeight = document.documentElement.clientHeight;//可视化区域的高度
function lazyload () {
//获取所有要进行懒加载的图片
let eles = document.querySelectorAll('img[data-original][lazyload]');//获取属性名中有data-original的
Array.prototype.forEach.call(eles, function(item, index) {
let rect;
if(item.dataset.original === '') {
return;
}
rect = item.getBoundingClientRect();
//图片一进入可视区,动态加载
if(rect.bottom >= 0 && rect.top < viewHeight) {
!function () {
let img = new Image();
img.src = item.dataset.original;
img.onload = function () {
item.src = img.src;
}
item.removeAttribute('data-original');
item.removeAttribute('lazyload');
}();
}
})
}
lazyload();
document.addEventListener('scroll', lazyload);
将VirtualDom转化为真实DOM结构
这是当前SPA应用的核心概念之一。
// vnode结构:
// {
// tag,
// attrs,
// children,
// }
//Virtual DOM => DOM
function render(vnode, container) {
container.appendChild(_render(vnode));
}
function _render(vnode) {
// 如果是数字类型转化为字符串
if (typeof vnode === 'number') {
vnode = String(vnode);
}
// 字符串类型直接就是文本节点
if (typeof vnode === 'string') {
return document.createTextNode(vnode);
}
// 普通DOM
const dom = document.createElement(vnode.tag);
if (vnode.attrs) {
// 遍历属性
Object.keys(vnode.attrs).forEach(key => {
const value = vnode.attrs[key];
dom.setAttribute(key, value);
})
}
// 子数组进行递归操作
vnode.children.forEach(child => render(child, dom));
return dom;
}
渲染几万条数据不卡住页面
渲染大数据时,合理使用createDocumentFragment和requestAnimationFrame,将操作切分为一小段一小段执行。
setTimeout(() => {
// 插入十万条数据
const total = 100000;
// 一次插入的数据
const once = 20;
// 插入数据需要的次数
const loopCount = Math.ceil(total / once);
let countOfRender = 0;
const ul = document.querySelector('ul');
// 添加数据的方法
function add() {
const fragment = document.createDocumentFragment();
for(let i = 0; i < once; i++) {
const li = document.createElement('li');
li.innerText = Math.floor(Math.random() * total);
fragment.appendChild(li);
}
ul.appendChild(fragment);
countOfRender += 1;
loop();
}
function loop() {
if(countOfRender < loopCount) {
window.requestAnimationFrame(add);
}
}
loop();
}, 0)