前端面试---手写源码

54 阅读16分钟

面试题

手写继承

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方法

  • 特点:
  1. 初始值不传时的特殊处理:会默认用数组中的第一个元素
  2. 函数的返回结果会作为下一次循环的 prev
  3. 回调函数一共接收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, '') 
}

手写柯里化

  • 特点: 柯里化就是将一个函数的功能细化,把接受「多个参数」的函数变换成接受一个「单一参数」的函数,并且返回接受「余下参数」返回结果的一种应用。
  1. 判断传递的参数是否达到执行函数的fn个数
  2. 没有达到的话,继续返回新的函数,将fn函数继续返回并将剩余参数累加
  3. 达到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);
    }
  };
};

手写反柯里化

  • 特点: 使用callapply可以让非数组借用一些其他类型的函数,比如,Array.prototype.push.call, Array.prototype.slice.calluncrrying把这些方法泛化出来,不在只单单的用于数组,更好的语义化。
  • 用法:
// 利用反柯里化创建检测数据类型的函数
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

来源 神奇的eval()与new Function()

核心:Functioneval有相同的字符串参数特性。

var func = new Function(arg1, arg2, ..., functionBody);

在转换JSON的实际应用中,只需要这么做。

var jsonStr = '{ "age": 20, "name": "jack" }'
var json = (new Function('return ' + jsonStr))();

evalFunction 都有着动态编译js代码的作用,但是在实际的编程中并不推荐使用。

这里写这两种就够了。至于第三,第四种,涉及到繁琐的递归和状态机相关原理,具体可以看:

《JSON.parse 三种实现方式》

基于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]降序排列

步骤:

  1. 构造最小堆
  2. 循环提取根节点, 直到全部提取完
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 种.

  1. 递归(简单粗暴)
  2. 利用存储遍历路径
function Node() {
  this.name = '';
  this.children = [];
}


function dfs(node) {
  console.log('探寻阶段: ', node.name);
  node.children.forEach((child) => {
    dfs(child);
  });
  console.log('回溯阶段: ', node.name);
}
  1. 使用栈
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

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形式缓存内存中(避免重复发送请求拉取)
    • 像子应用一样渲染和交互
  • 代码实现:

  • 加载子应用

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;
}

渲染几万条数据不卡住页面

渲染大数据时,合理使用createDocumentFragmentrequestAnimationFrame,将操作切分为一小段一小段执行。

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)

参考:

Js 实现 Bind 的这五层,你在第几层?

手写一个基于 Proxy 的缓存库

编写一个自定义事件类,包含on/off/emit/once方法