手写代码

223 阅读5分钟

版本号比较

function compareVersion(version1, version2) {
  // 将版本号按 . 分割为数组,并转为数字类型
  const v1 = version1.split('.').map(Number);
  const v2 = version2.split('.').map(Number);
  
  // 取两个版本号数组的最大长度,确保所有位数都能比较
  const maxLength = Math.max(v1.length, v2.length);
  
  for (let i = 0; i < maxLength; i++) {
    // 若某版本号位数不足,用 0 补充
    const num1 = v1[i] || 0;
    const num2 = v2[i] || 0;
    
    // 逐位比较
    if (num1 > num2) return 1;   // version1 更大
    if (num1 < num2) return -1;  // version2 更大
  }
  
  // 所有位数都相同,版本号相等
  return 0;
}

深度拷贝

深拷贝和浅拷贝只针对像Object和Array这样的复杂对象的,String,Number等简单类型不存在深拷贝。 浅拷贝只会将对象的各个属性进行依次复制,并不会进行递归复制。深拷贝则不同,它不仅将原对象的各个属性逐个复制出去,而且将原对象各个属性所包含的对象也依次采用深拷贝的方法递归复制到新对象上

JSON.parse缺点:

  • 如果obj里面有时间对象,则JSON.stringify后再JSON.parse的结果,时间将只是字符串的形式,而不是对象的形式
  • 如果obj里有RegExp(正则表达式的缩写)、Error对象,则序列化的结果将只得到空对象;
  • 如果obj里有函数,undefined,则序列化的结果会把函数或 undefined丢失;
  • 如果obj里有NaN、Infinity和-Infinity,则序列化的结果会变成null
  • JSON.stringify()只能序列化对象的可枚举的自有属性,例如 如果obj中的对象是有构造函数生成的, 则使用JSON.parse(JSON.stringify(obj))深拷贝后,会丢弃对象的constructor;
  • 如果对象中存在循环引用的情况也无法正确实现深拷贝;
// 简单
function deepClone(obj) {
  // 检查输入是否为对象类型
  if (typeof obj !== 'object' || obj === null) {
    return obj;
  }

  // 创建一个新的对象或数组
  let clone;
  if (Array.isArray(obj)) {
    clone = [];
  } else {
    clone = {};
  }

  // 递归拷贝对象或数组的每个属性
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      let value = obj[key];
      if (value instanceof Date) {
        clone[key] = new Date(value.getTime());
      } else if (value instanceof RegExp) {
        clone[key] = new RegExp(value);
      } else {
        clone[key] = deepClone(value);
      }
    }
  }

  return clone;
}
// 复杂考虑 正则日期数据
const isType = (obj, type) => {
  if (typeof obj !== 'object') return false;
  const typeString = Object.prototype.toString.call(obj);
  let flag;
  switch (type) {
    case 'Array':
      flag = typeString === '[object Array]';
      break;
    case 'Date':
      flag = typeString === '[object Date]';
      break;
    case 'RegExp':
      flag = typeString === '[object RegExp]';
      break;
    default:
      flag = false;
  }
  return flag;
};
const getRegExp = re => {
  var flags = '';
  if (re.global) flags += 'g';
  if (re.ignoreCase) flags += 'i';
  if (re.multiline) flags += 'm';
  return flags;
};
const clone = parent => {
  // 维护两个储存循环引用的数组
  const parents = [];
  const children = [];

  const _clone = parent => {
    if (parent === null) return null;
    if (typeof parent !== 'object') return parent;

    let child, proto;

    if (isType(parent, 'Array')) {
      // 对数组做特殊处理
      child = [];
    } else if (isType(parent, 'RegExp')) {
      // 对正则对象做特殊处理
      child = new RegExp(parent.source, getRegExp(parent));
      if (parent.lastIndex) child.lastIndex = parent.lastIndex;
    } else if (isType(parent, 'Date')) {
      // 对Date对象做特殊处理
      child = new Date(parent.getTime());
    } else {
      // 处理对象原型
      proto = Object.getPrototypeOf(parent);
      // 利用Object.create切断原型链
      child = Object.create(proto);
    }

    // 处理循环引用
    const index = parents.indexOf(parent);

    if (index != -1) {
      // 如果父数组存在本对象,说明之前已经被引用过,直接返回此对象
      return children[index];
    }
    parents.push(parent);
    children.push(child);

    for (let i in parent) {
      // 递归
      child[i] = _clone(parent[i]);
    }

    return child;
  };
  return _clone(parent);
};

防抖和节流

1)防抖
原理:在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。
适用场景:
按钮提交场景:防止多次提交按钮,只执行最后提交的一次
搜索框联想场景:防止联想发送请求,只发送最后一次输入
// 有时希望立刻执行函数,然后等到停止触发 n 秒后,才可以重新触发执行。
function debounce(func, wait, immediate) {
  let timeout;
  return function () {
    const context = this;
    const args = arguments;
    if (timeout) clearTimeout(timeout);
    if (immediate) {
      const callNow = !timeout;
      timeout = setTimeout(function () {
        timeout = null;
      }, wait)
      if (callNow) func.apply(context, args)
    } else {
      timeout = setTimeout(function () {
        func.apply(context, args)
      }, wait);
    }
  }
}
2)节流
原理:规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。
适用场景
拖拽场景:固定时间内只执行一次,防止超高频次触发位置变动
缩放场景:监控浏览器resize
function throttle(func, wait) {
  let timeout;
  return function () {
    const context = this;
    const args = arguments;
    if (!timeout) {
      timeout = setTimeout(function () {
        timeout = null;
        func.apply(context, args)
      }, wait)
    }

  }
}

sleep 函数

/*
 问题:异步编程
 1. 实现一个sleep方法
 2. 循环调用sleep方法20次
*/
// 1
const sleep = (time) => new Promise((resolve) => setTimeout(resolve, time))

sleep(1000).then(() => {
  console.log(1)
})

async function sleepLoop() {
  for (let i = 0; i < 20; i++) {
    await sleep(1000)
    console.log(`Hello #${i}`)
  }
}
sleepLoop()

// 2
async function sleep(wait) {
  await new Promise(resolve => {
    setTimeout(resolve, wait);
  });
}
sleep(500).then(() => {
  console.log("hello");
});

// 3
function sleep(callback, time) {
  if (typeof callback === 'function')
    setTimeout(callback, time)
}
for (let i = 0; i < 20; i++) {
  sleep(() => console.log(`Hello #${i}`), 1000 * i)
}

LazyMan链式调用

class LazyMan {
  constructor(name) {
    this.name = name;
    this.tasks = [];
    console.log(`Hi I am ${name}`);
    setTimeout(() => this.next(), 0);
  }

  sleep(time) {
    this.tasks.push(() => {
      return new Promise(resolve => {
        setTimeout(() => {
          console.log(`等待了${time}秒...`);
          resolve();
        }, time * 1000);
      });
    });
    return this;
  }

  sleepFirst(time) {
    this.tasks.unshift(() => {
      return new Promise(resolve => {
        setTimeout(() => {
          console.log(`等待了${time}秒...`);
          resolve();
        }, time * 1000);
      });
    });
    return this;
  }

  eat(food) {
    this.tasks.push(() => {
      console.log(`I am eating ${food}`);
      return Promise.resolve();
    });
    return this;
  }

  async next() {
    const task = this.tasks.shift();
    if (task) {
      await task();
      this.next();
    }
  }
}

// 使用示例
function lazyMan(name) {
  return new LazyMan(name);
}

// 测试用例
lazyMan("Tony")
  .eat("lunch")
  .eat("dinner")
  .sleep(2)
  .eat("junk food")
  .sleepFirst(3); 
Hi I am Tony
等待了3秒...
I am eating lunch
I am eating dinner
等待了2秒...
I am eating junk food 

单例模式

/*
 问题:写一个单例
*/
// 1
function CreateSingleton(name) {
  this.name = name;
  // this.getName();
};

const Singleton = (() => {
  let instance;
  return function (name) {
    if (!instance) {
      instance = new CreateSingleton(name);
    }
    return instance;
  }
})();

// 2
let getSingleton = function(fn) {
  var result;
  return function() {
      return result || (result = fn.apply(this, arguments)); // 确定this上下文并传递参数
  }
}
const createAlertMessage = ()=>{}
let createSingleAlertMessage = getSingleton(createAlertMessage);

简单工厂模式

由一个工厂对象决定创建某一种产品对象类的实例。

//User类
class User {
  //构造器
  constructor(opt) {
    this.name = opt.name;
    this.viewPage = opt.viewPage;
  }

  //静态方法
  static getInstance(role) {
    switch (role) {
      case 'superAdmin':
        return new User({ name: '超级管理员', viewPage: ['首页', '通讯录', '发现页', '应用数据', '权限管理'] });
        break;
      case 'admin':
        return new User({ name: '管理员', viewPage: ['首页', '通讯录', '发现页', '应用数据'] });
        break;
      default:
        throw new Error('参数错误, 可选参数:superAdmin、admin、user')
    }
  }
}

//调用
let superAdmin = User.getInstance('superAdmin');
let admin = User.getInstance('admin');

自定义事件处理器

/*
 问题:编写一个简单的自定义事件处理器
 1. 具备 on 方法绑定事件
 2. 具备 off 方法解绑事件
*/
class EventEmitter {
  constructor() {
    this.events = {}
  }

  on(type, listener, isUnshift = false) {
    // 因为其他的类可能继承自EventEmitter,子类的events可能为空,保证子类必须存在此实例属性
    if (!this.events) {
      this.events = {}
    }
    if (this.events[type]) {
      if (isUnshift) {
        this.events[type].unshift(listener)
      } else {
        this.events[type].push(listener)
      }
    } else {
      this.events[type] = [listener]
    }
  }

  trigger(type, ...args) {
    this.events[type] && this.events[type].forEach((fn) => fn.call(this, ...args))
    this.events['*'] && this.events['*'].forEach((fn) => fn.call(this, ...args))
  }

  off(type, listener) {
    if (this.events[type]) {
      const index = this.events[type].indexOf(listener)
      this.events[type].splice(index, 1)
    }
  }

  has(type) {
    return !!(this.events[type] && this.events[type].length)
  }

  once(type, listener) {
    let oneTime = (...args) => {
      listener.call(this, ...args)
      this.off(type, oneTime)
    }
    this.on(type, oneTime)
  }

  remove(type) {
    this.events[type] = []
  }
}

function EventEmitter() {
  let all = new Map();

  return {
    all,

    on(type, handler) {
      const handlers = all.get(type);
      const added = handlers && handlers.push(handler);
      if (!added) {
        all.set(type, [handler]);
      }
    },

    off(type, handler) {
      const handlers = all.get(type);
      if (handlers) {
        handlers.splice(handlers.indexOf(handler) >>> 0, 1);
      }
    },

    emit(type, evt) {
      ((all.get(type) || [])).slice().map((handler) => { handler(evt); });
      ((all.get('*') || [])).slice().map((handler) => { handler(type, evt); });
    }
  };
}

图片懒加载

// IntersectionObserver
function lazyLoad(){
    const imageToLazy = document.querySelectorAll('img[data-src]');
    const loadImage = function (image) {
        image.setAttribute('src', image.getAttribute('data-src'));
        image.addEventListener('load', function() {
            image.removeAttribute("data-src");
        })
    }


    const intersectionObserver = new IntersectionObserver(function(items, observer) {
        items.forEach(function(item) {
            if(item.isIntersecting) {
                loadImage(item.target);
                observer.unobserve(item.target);
            }
        });
    });

    imageToLazy.forEach(function(image){
        intersectionObserver.observe(image);
    })
}
// 
let lazyImages = [...document.querySelectorAll('.lazy-load')]
let inAdvance = 300
function lazyLoad() {
    lazyImages.forEach(image => {
        if (image.offsetTop < window.innerHeight + window.pageYOffset + inAdvance) {
            image.src = image.dataset.src;   // 替换真实图片的  URL
        }
    })
}
lazyLoad();
window.addEventListener('scroll', _.throttle(lazyLoad, 50))
window.addEventListener('resize', _.throttle(lazyLoad, 50))

JavaScript的几种继承方式

  • 原型链继承 子类型的原型为父类型的一个实例对象
function Parent() {
    this.name = 'bigStar';
    this.colors = ['red', 'blue', 'yellow'];
}
Parent.prototype.getName = function() {
    console.log(this.name)
}

function Child() {
    this.subName = 'litterStar';
}
// 核心代码: 子类型的原型为父类型的一个实例对象
Child.prototype = new Parent();

let child1 = new Child();
let child2 = new Child();
child1.getName(); // bigStar


child1.colors.push('pink');
// 修改 child1.colors 会影响 child2.colors
console.log(child1.colors); // [ 'red', 'blue', 'yellow', 'pink' ]
console.log(child2.colors); // [ 'red', 'blue', 'yellow', 'pink' ]
特点:
父类新增在构造函数上面的方法,子类都能访问到
#缺点:
来自原型对象的所有属性被所有实例共享,child1修改 colors 会影响child2的 colors
创建子类实例时,无法向父类的构造函数传参
  • 借助构造函数继承(经典继承) 在子类的构造函数中使用 call()或者 apply() 调用父类型构造函数
function Parent(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'yellow'];
}
Parent.prototype.getName = function() {
    console.log(this.name)
}

function Child(name, age) {
    // 核心代码:“借调”父类型的构造函数
    Parent.call(this, name);
    this.age = age;
}

let child1 = new Child('litterStar');
let child2 = new Child('luckyStar');
console.log(child1.name); // litterStar
console.log(child2.name); // luckyStar

// 这种方式只是实现部分的继承,如果父类的原型还有方法和属性,子类是拿不到这些方法和属性的。
child1.getName(); // TypeError: child1.getName is not a function
特点:
避免引用类型的属性被所有实例共享
创建子类实例时,可以向父类传递参数
#缺点
实例并不是父类的实例,只是子类的实例
只能继承父类的实例属性和方法,不能继承原型属性和方法
无法实现函数复用,每次创建实例都会创建一遍方法,影响性能
  • 组合继承:原型链 + 借用构造函数(最常用)
function Parent(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'yellow'];
}
Parent.prototype.getName = function() {
    console.log(this.name)
}

function Child(name, age) {
    // 核心代码①
    Parent.call(this, name);

    this.age = age;
}
// 核心代码②: 子类型的原型为父类型的一个实例对象
Child.prototype = new Parent();
Child.prototype.constructor = Child;


// 可以通过子类给父类的构造函数传参
let child1 = new Child('litterStar');
let child2 = new Child('luckyStar');
child1.getName(); // litterStar
child2.getName(); // luckyStar

child1.colors.push('pink');
// 修改 child1.colors 不会影响 child2.colors
console.log(child1.colors); // [ 'red', 'blue', 'yellow', 'pink' ]
console.log(child2.colors); // [ 'red', 'blue', 'yellow' ]
特点
融合了原型链继承和借用构造函数的优点,称为JavaScript中最常用的继承模式。
#缺点
调用了两次父类构造函数,生成了两份实例
一次是设置子类型实例的原型的时候 Child.prototype = new Parent();
一次是创建子类型实例的时候 let child1 = new Child('litterStar');, 调用 new 会执行 Parent.call(this, name);,此时会再次调用一次 Parent构造函数
  • 原型式继承 (Object.create)
//借助原型可以基于现有方法来创建对象,var B = Object.create(A) 以A对象为原型,生成A对象,B继承了A的所有属性和方法。
const person = {
    name: 'star',
    colors: ['red', 'blue'],
}

// 核心代码:Object.create
const person1 = Object.create(person);
const person2= Object.create(person);

person1.name = 'litterstar';
person2.name = 'luckystar';

person1.colors.push('yellow');

console.log(person1.colors); // [ 'red', 'blue', 'yellow' ]
console.log(person2.colors); // [ 'red', 'blue', 'yellow' ]
特点
没有严格意义上的构造函数,借助原型可以基于已有对象创建新对象
#缺点
来自原型对象的所有属性被所有实例共享,person1修改 colors 会影响person2的 colors,这点跟原型链继承一样。
  • 寄生式继承
创建一个用于封装继承过程的函数,该函数在内部以某种方式来增强对象
function createObj (original) {
    // 通过调用函数创新一个新对象
    var clone = Object.create(original);
    // 以某种方式来增强这个对象
    clone.sayName = function () {
        console.log('hi');
    }
    // 返回这个对象
    return clone;
}
缺点: 每次创建对象都会创建一遍方法,跟借助构造函数模式一样
  • 寄生组合式继承(最理想)
我们可以先回忆一下JavaScript最常用的继承模式: 组合继承(原型链 + 借用构造函数),它的最大缺点是会调用两次父构造函数(Child.prototype = new Parent();和 let child1 = new Child('litterStar');)。

我们是否可以想办法是调用一次?可以让 Child.prototype 访问到 Parent.prototype// 我们不能直接使用 Child.prototype = Parent.prototype来实现,因为会出现一些副作用,你可能在修改 Child.prototype 的时候会修改Parent.prototype。

// 可以使用 Object.create(...)来实现

// Object.create MDN上的解释:它会创建一个新对象,使用现有的对象来提供新创建的对象的__proto__

function Parent(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'yellow'];
}
Parent.prototype.getName = function() {
    console.log(this.name)
}

function Child(name, age) {
    // 核心代码①
    Parent.call(this, name);

    this.age = age;
}
// 核心代码②
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
  • ES6中的继承 constructor 方法是类的构造函数,是一个默认方法,通过 new 命令创建对象实例时,自动调用该方法,一般 constructor 方法返回实例对象 this

先创建父类实例this 通过class丶extends丶super关键字定义子类,并改变this指向,super本身是指向父类的构造函数但做函数调用后返回的是子类的实例,实际上做了父类.prototype.constructor.call(this),做对象调用时指向父类.prototype,从而实现继承。

子类在构造函数中使用super, 必须放到 this 前面 (必须先调用父类的构造方法,再使用子类构造方法),否则新建实例时会报错(this is not defined)。因为子类没有自己的 this 对象,而是继承父类的 this 对象。如果不调用 super 方法,子类就得不到 this 对象。

super 这个关键字,既可以当做函数使用,也可以当做对象使用。

当做函数使用 : 在 constructor 中必须调用 super 方法,因为子类没有自己的 this 对象,而是继承父类的 this 对象,然后对其进行加工,而 super 就代表了父类的构造函数。super 虽然代表了父类 A 的构造函数,但是返回的是子类 B 的实例,即 super 内部的 this 指的是 B,因此 super() 在这里相当于A.prototype.constructor.call(this, props)

当做对象使用:在普通方法中,指向父类的原型对象;在静态方法中,指向父类。通过 super 调用父类的方法时,super 会绑定子类的 this。

实现new

// 创建一个新的空对象 obj
// 将新对象的的原型指向当前函数的原型
// 新创建的对象绑定到当前this上
// 如果没有返回其他对象,就返回 obj,否则返回其他对象
function _new(constructor, ...arg) {
    // ① 创建一个新的空对象 obj
    const obj = {};
    // ② 将新对象的的原型指向当前函数的原型
    obj.__proto__ = constructor.prototype;
    // ③ 新创建的对象绑定到当前this上
    const result = constructor.apply(obj, arg); 
    // ④ 如果没有返回其他对象,就返回 obj,否则返回其他对象
    return typeof result === 'object' ? result : obj;
}

实现async

//
function asyncFn(generatorFn) {
  const gen = generatorFn();

  function next(value) {
    const { value: promise, done } = gen.next(value);

    if (done) {
      return promise;
    }

    return promise.then((result) => next(result));
  }

  return next();
}

// 使用示例
function* main() {
  console.log('Entry');

  const result1 = yield new Promise((resolve) =>
    setTimeout(() => resolve('Slow'), 2000)
  );
  console.log(result1);

  const result2 = yield new Promise((resolve) =>
    setTimeout(() => resolve('Fast'), 1000)
  );
  console.log(result2);

  console.log('Exit');
  return 'Return';
}

asyncFn(main).then((result) => {
  console.log(result);
});

//
function myAsync(gen) {
  var args = [].slice.call(arguments, 1), it;
  it = gen.apply(this, args);

  function fn(nextVal) {
    if (!nextVal.done) {
      return Promise.resolve(nextVal.value).then(
        function (v) {
          return fn(it.next(v));
        },
        function (err) {
          return fn(it.throw(err));
        }
      )
    } else {
      return Promise.resolve(nextVal.value)
    };
  }

  return fn(it.next());
}

实现一个Iterator

//(1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
//(2)第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。
//(3)第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。
//(4)不断调用指针对象的next方法,直到它指向数据结构的结束位置。
function makeIterator(array) {
  var nextIndex = 0;
  return {
    next: function() {
      return nextIndex < array.length ?
        { 
            value: array[nextIndex++],
            done: false
        } 
        :
        {
            value: undefined,
            done: true
        };
    }
  };
}
const it = makeIterator(['a', 'b']);
it.next() 

简单实现一个Virtual DOM

// diff.js
import patch from './patch';
function diff(oldTree, newTree) {
    const patches = {};
    const index = {
        value: 0,
    }
    dfsWalk(oldTree, newTree, index, patches);
    return patches;
}
// 比较属性的变化
function diffProps(oldProps, newProps, index, currentIndexPatches) {
    // 遍历旧的属性,找到被删除和修改的情况
    for (const propKey in oldProps) {
        // 新属性中不存在,旧属性存在,属性被删除
        if (!newProps.hasOwnProperty(propKey)) {
            currentIndexPatches.push({
                type: patch.NODE_ATTRIBUTE_DELETE,
                key: propKey,
            })
        } else if (newProps[propKey] !== oldProps[propKey]) {
            // 新旧属性中都存在,但是值不同: 属性被修改
            currentIndexPatches.push({
                type: patch.NODE_ATTRIBUTE_MODIFY,
                key: propKey,
                alue: newProps[propKey],
            })
        }
    }

    // 遍历新元素,找到添加的部分
    for (const propKey in newProps) {
        // 旧属性中不存在,新属性中存在: 添加属性
        if (!oldProps.hasOwnProperty(propKey)) {
            currentIndexPatches.push({
                type: patch.NODE_ATTRIBUTE_ADD,
                key: propKey,
                value: newProps[propKey]
            })
        }
    }
}
// 顺序比较子元素的变化
function diffChildren(oldChildren, newChildren, index, currentIndexPatches, patches) {
    const currentIndex = index.value;
    if (oldChildren.length < newChildren.length) {
        // 有元素被添加
        let i = 0;
        for (; i < oldChildren.length; i++) {
            
            index.value++;
            dfsWalk(oldChildren[i], newChildren[i], index, patches)
        }
        for (; i < newChildren.length; i++) {
            currentIndexPatches.push({
                type: patch.NODE_ADD,
                value: newChildren[i]
            })
        }
    } else {
        // 对比新旧子元素的变化
        for(let i = 0; i< oldChildren.length; i++) {
            index.value++;
            dfsWalk(oldChildren[i], newChildren[i], index, patches)
        }
    }
}
// 比较innerHTML的变化
function dfsWalk(oldNode, newNode, index, patches) {
    const currentIndex = index.value;
    const currentIndexPatches = [];
    if(newNode === undefined) {
        // 节点被移除
        currentIndexPatches.push({
            type: patch.NODE_DELETE,
        })
    } else if(typeof oldNode === 'string' && typeof newNode === 'string') {
        // 文本节点被修改
        if(oldNode !== newNode) {
            currentIndexPatches.push({
                type: patch.NODE_TEXT_MODIFY,
                value: newNode,
            })
        }
    } else if(oldNode.tagName === newNode.tagName && oldNode.key === newNode.key) {
        // 同时根据tagName和key来进行对比
        diffProps(oldNode.props, newNode.props, index, currentIndexPatches);
        diffChildren(oldNode.children, newNode.children, index, currentIndexPatches, patches);
    } else {
        currentIndexPatches.push({
            type: patch.NODE_REPLACE,
            value: newNode,
        })
    }
    if(currentIndexPatches.length > 0) {
        patches[currentIndex] = currentIndexPatches;
    }
}

export default diff;
// element.js
class Element {
    constructor(tagName, ...args) {
        this.tagName = tagName;
        // 判断下面还有没有子元素
        if(Array.isArray(args[0])) {
            this.props = {};
            this.children = args[0];
        } else {
            this.props = args[0];
            this.children = args[1];
        }
        this.key = this.props.key || void 0;
    }
    render() {
        // 创建一个元素
        const $dom = document.createElement(this.tagName);
        // 给元素加上所有的属性
        for(const proKey in this.props) {
            $dom.setAttribute(proKey, this.props[proKey]);
        }
        // 如果存在子节点
        if(this.children) {
            this.children.forEach(child => {
                // 如果子元素还包含子元素,则递归
                if(child instanceof Element) {
                    $dom.appendChild(child.render());
                } else {
                    $dom.appendChild(document.createTextNode(child))
                }
            });
        }
        return $dom;
    }
};
export default Element;

// patch.js
function patch($dom, patches) {
    const index = {
        value: 0,
    }
    dfsWalk($dom, index, patches);
}
patch.NODE_DELETE = 'NODE_DELETE'; // 节点被删除
patch.NODE_TEXT_MODIFY = 'NODE_TEXT_MODIFY'; // 文本节点被更改
patch.NODE_REPLACE = 'NODE_REPLACE'; // 节点被替代
patch.NODE_ADD = 'NODE_ADD'; // 添加节点
patch.NODE_ATTRIBUTE_MODIFY = 'NODE_ATTRIBUTE_MODIFY'; // 更新属性
patch.NODE_ATTRIBUTE_ADD = 'NODE_ATTRIBUTE_ADD'; // 添加属性
patch.NODE_ATTRIBUTE_DELETE = 'NODE_ATTRIBUTE_DELETE'; // 删除属性

// 根据不同类型的差异对当前节点进行 DOM 操作:
function dfsWalk($node, index, patches, isEnd = false) {
    if (patches[index.value]) {
        patches[index.value].forEach(p => {
            switch (p.type) {
                case patch.NODE_ATTRIBUTE_MODIFY:
                    {
                        $node.setAttribute(p.key, p.value);
                        break;
                    }
                case patch.NODE_ATTRIBUTE_DELETE:
                    {
                        $node.removeAttribute(p.key, p.value);
                        break;
                    }
                case patch.NODE_ATTRIBUTE_ADD:
                    {
                        $node.setAttribute(p.key, p.value);
                        break;
                    }
                case patch.NODE_ADD:
                    {
                        $node.appendChild(p.value.render());
                        break;
                    }
                case patch.NODE_TEXT_MODIFY:
                    {
                        $node.textContent = p.value;
                        break;
                    }
                case patch.NODE_REPLACE:
                    {
                        $node.replaceWith(p.value.render());
                        break;
                    }
                case patch.NODE_DELETE:
                    {
                        $node.remove();
                        break;
                    }
                default:
                    {
                        console.log(p);
                    }

            }

        });
    }
    if (isEnd) {
        return;
    }
    if ($node.children.length > 0) {
        for (let i = 0; i < $node.children.length; i++) {
            index.value++;
            dfsWalk($node.children[i], index, patches);
        }
    } else {
        index.value++;
        dfsWalk($node, index, patches, true);
    }
};

export default patch;

柯里化

function sum(...args) {
  // 计算当前参数的和
  const currentSum = args.reduce((acc, val) => acc + val, 0);

  // 创建一个新的函数用于接收更多参数
  function adder(...newArgs) {
    return sum(currentSum, ...newArgs);
  }

  // 当函数被转为原始值(如字符串或数字)时,返回累加结果
  adder.valueOf = () => currentSum;
  adder.toString = () => String(currentSum);

  return adder;
}

// 示例验证
console.log(sum(1)); // 1
console.log(sum(1, 2)); // 3
console.log(sum(1, 2)(3)); // 6
console.log(sum(1)(2)(3)(4)); // 10

const curry = (fn, ...args) => 
    // 函数的参数个数可以直接通过函数数的.length属性来访问
    args.length >= fn.length // 这个判断很关键!!!
    // 传入的参数大于等于原始函数fn的参数个数,则直接执行该函数
    ? fn(...args)
    /**
     * 传入的参数小于原始函数fn的参数个数时
     * 则继续对当前函数进行柯里化,返回一个接受所有参数(当前参数和剩余参数) 的函数
    */
    : (..._args) => curry(fn, ...args, ..._args);
function curry (fn, arr = []) {
    return fn.length === arr.length ? fn.apply(null, arr) : function (...args) {
        return curry (fn, arr.concat(args))
    }
}

setTimeout 实现 setInterval

function setInterval2 (handler,timeout,...args) {
  let isBrowser = typeof window !=='undefined'
  if(isBrowser && this!==window){
    throw 'TypeError: Illegal invocation'
  }
  let timer = {}
  if(isBrowser){
    // 浏览器上处理
    timer = {
      value:-1,
      valueOf: function (){
        return this.value
      }
    }
    let callback = ()=>{
      // 区别在这
      timer.value = setTimeout(callback,timeout)
      handler.apply(this,args)
    }
    timer.value = setTimeout(callback,timeout)
  } else {
    // nodejs的处理
    let callback = ()=>{
      // 区别在这
      Object.assign(timer,setTimeout(callback,timeout))
      handler.apply(this,args)
    }
    Object.assign(timer,setTimeout(callback,timeout))
  }
  return timer
}
// 2
setTimeout(function () {
 console.log('我被调用了');
 setTimeout(arguments.callee, 100);
}, 100);

Vue 双向绑定

// Object.defineProperty()
class Observer {
    constructor(data) {
        // 遍历参数data的属性,给添加到this上
        for(let key of Object.keys(data)) {
            if(typeof data[key] === 'object') {
                data[key] = new Observer(data[key]);
            }
            Object.defineProperty(this, key, {
                enumerable: true,
                configurable: true,
                get() {
                    console.log('你访问了' + key);
                    return data[key]; // 中括号法可以用变量作为属性名,而点方法不可以;
                },
                set(newVal) {
                    console.log('你设置了' + key);
                    console.log('新的' + key + '=' + newVal);
                    if(newVal === data[key]) {
                        return;
                    }
                    data[key] = newVal;
                }
            })
        }
    }
}
// Proxy 
const obj = {
    name: 'app',
    age: '18',
    a: {
        b: 1,
        c: 2,
    },
}
const p = new Proxy(obj, {
    get(target, propKey, receiver) {
        console.log('你访问了' + propKey);
        return Reflect.get(target, propKey, receiver);
    },
    set(target, propKey, value, receiver) {
        console.log('你设置了' + propKey);
        console.log('新的' + propKey + '=' + value);
        Reflect.set(target, propKey, value, receiver);
    }
});

轮播图

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        * {
            margin:0;
            padding:0;
        }
        a {
            text-decoration: none;
        }
        .container {
            position: relative;
            width: 600px;
            height: 400px;
            margin: 100px auto 0 auto;
            box-shadow: 0 0 5px #ccc;
            overflow: hidden;
        }
        .pageBottonBtn {
            width: 600px;
            height: 400px;
            margin: 100px auto 0 auto;
        }
        .wrap {
            position: absolute;
            /* 7 * 600px */
            width: 4200px;
            height: 400px;
            z-index: 1;
        }
        .img {
            float: left;
            width: 600px;
            height: 400px;
            text-align: center;
            font-size: 60px;
            color: #fff;
            line-height: 400px;
        }
        .div1 {
            background-color: chocolate;
        }
        .div2 {
            background-color: cyan;
        }
        .div3 {
            background-color: olive;
        }
        .div4 {
            background-color: pink;
        }
        .div5 {
            background-color: purple;
        }
        .container .buttons {
            position: absolute;
            right: 200px;
            bottom:20px;
            width: 160px;
            height: 10px;
            z-index: 2;
        }
        .container .buttons span {
            margin-left: 5px;
            display: inline-block;
            width: 20px;
            height: 20px;
            line-height: 20px;
            border-radius: 50%;
            background-color: cornflowerblue;
            text-align: center;
            color:white;
            cursor: pointer;
        }
        .container .buttons span.on{
            background-color: red;
        }
        .container .arrow {
            position: absolute;
            top: 35%;
            color: cornflowerblue;
            padding: 0px 14px;
            border-radius: 50%;
            font-size: 50px;
            z-index: 2;
            display: none;
        }
        .container .arrowLeft {
            left: 10px;
        }
        .container .arrowRight {
            right: 10px;
        }
        .container:hover .arrow {
            display: block;
        }
        .container .arrow:hover {
            background-color: rgba(0,0,0,0.2);
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="wrap" style="left: -600px">
            <!-- 实现无限滚动的关键,开头多放一张5-->
            <div class="img div5">5</div>
            <div class="img div1">1</div>
            <div class="img div2">2</div>
            <div class="img div3">3</div>
            <div class="img div4">4</div>
            <div class="img div5">5</div>
            <!-- 实现无限滚动的关键 末尾多放一张5 -->
            <div class="img div1">1</div>
        </div>
       


        <div class="buttons">
            <span class="on">1</span>
            <span>2</span>
            <span>3</span>
            <span>4</span>
            <span>5</span>
        </div>
        <a href="javascript:;" class="arrow arrowLeft">&lt;</a>
        <a href="javascript:;" class="arrow arrowRight">&gt;</a>
    </div>
</body>
<script>
  // 因为下面要获取DOM,所以必须放到 onload事件中
    window.onload = function() {
        const container = document.querySelector('.container');
        const wrap = document.querySelector('.wrap');
        const next = document.querySelector('.arrowRight');
        const prev = document.querySelector(".arrowLeft");

        const buttons = document.querySelector('.buttons').getElementsByTagName('span');
        // 显示当前哪个按钮
        let index = 1;
        // 优化:不停的点击,不停地调用 animate,非常容易造成卡顿,可以做个判断,只有当前的动画完成之后,点击,才会触发animate
        // 是否正在动画中
        let animated = false;
        /**
         * 处理当前按钮的状态
        */
        function showButton() {
            // 关掉之前的高亮效果
            for(let i = 0; i < buttons.length; i++) {
                if(buttons[i].className === 'on') {
                    buttons[i].className = '';
                    break;
                }
            }
            // 当前的亮起
            buttons[index - 1].className = 'on';
        }

        function animate(offset) {
            animated = true;
            let newLeft =  parseInt(wrap.style.left) + offset;

            /**
             * 添加动画逻辑,从一张图切换到另外一张图的逻辑
             * 可以通过修改 time 和 interval的值可以修改动画的速度
             */ 
            let time = 300; // 位移总时间 300ms
            let interval = 10; // 位移时间间隔 10ms
             // 每次移动多少  = 偏移量 / 次数
             // 次数 = 位移总时间 / 位移时间间隔
            let speed = offset / (time / interval);

            // 位移的判断以及是否位移
            function go() {
                // 直接获取到的是 字符串,比如 '100px', 需要用 parseInt转成数字做计算
                let oldLeft = parseInt(wrap.style.left);
                // speed < 0 向左移动,并且目标值小于 原有值
                if((speed < 0 && oldLeft > newLeft) || (speed > 0 && oldLeft < newLeft)) {
                    wrap.style.left = oldLeft + speed + 'px';
                    setTimeout(go, interval);
                } else {
                    animated = false;
                    // 下面是处理滚动到头处理空白图片的逻辑
                    // 滚动到右边,再向左滚动
                    // left是 0 滚动到假的第五张图上,归位到第五张图上,
                    if(newLeft > -600) {
                        newLeft = -3000;
                    }
                    // 滚动到边,再向左滚动
                    // 到 -3600 时,滚动到假的第一张图上, 归位到第一张图上,
                    if(newLeft < -3000) {
                        newLeft = -600;
                    }
                    wrap.style.left = newLeft + 'px';
                }
            }
            go();
        }
        
        next.onclick = function() {
            // 到最后一张图时,重置为1
            if(index === 5) {
                index = 1
            } else {
                index++;
            }
            showButton();
            // 可以通过打印的 111和222进行测试
            // console.log(111);
            // 不是动画状态
            if(!animated) {
                // console.log(222);
                animate(-600);
            }
        }
        prev.onclick = function() {
            // 到第一张图时,重置为5
            if(index === 1) {
                index = 5
            } else {
                index++;
            }
            index--;
            showButton();
            // 不是动画状态时,点击才会执行
            if(!animated) {
                animate(600);
            }
        }
        // 允许按钮点击
        for(let i = 0; i < buttons.length; i++) {
            buttons[i].onclick = function() {
                // (目标值 - 原始值) * -600
                let targetIndex = i + 1;
                if(targetIndex === index) {
                    return;
                }
                let offset = (targetIndex - index ) * -600;
                // 更新index
                index = targetIndex;
                // 修改按钮的显示状态
                showButton();
                // 图片的切换
                animate(offset);
            }
        }

        // 自动滚动,每隔3s自动切换,就是每隔3s,自动点击一下 下一页的按钮
        let timer = null;
        function play() {
            timer = setInterval(() => {
                next.onclick();
            }, 3000);
        }
        // 鼠标移上去需要清除
        function stop() {
            clearInterval(timer);
        }
        // 给整个容器添加鼠标移入和移出的事件
        container.onmouseover = stop;
        container.onmouseout = play;
        //最初是自动播放的状态
        play();
    }
    
</script>

</html>

统计HTML标签中出现次数最多的标签

const tags = document.getElementsByTagName('*');
let map = new Map();
let maxStr = '';
let max = 0;
// 只是使用下标来获取,没有使用数组的方法,所以不需要将类数组转为数组
for(let i = 0; i < tags.length; i++) {
    let value = map.get(tags[i].tagName)
    if(value) {
        map.set(tags[i].tagName, ++value)
    } else {
        map.set(tags[i].tagName, 1);
    }
    if(value > max) {
        maxStr = tags[i].tagName;
        max = value;
    }
}

斐波那契数列

// 递归
var fib = function(N) {
    // N小于等于1,则直接返回 N
    if (N <= 1) {
        return N;
    }
    // 通过递归关系调用自身
    return fib(N-1) + fib(N-2);
};
// dp
function fib(N) {
    const dp = [0, 1];
    for(let i = 2; i <= N; i++) {
        dp[i] = dp[i -1] + dp[i-2];
    }
    return dp[N];
}

随机抽奖

function lottery(arr, n) {
    const result = [];
    let i = 0;
    while( i < n) {
        // 每次从数组中随机抽出一个值,使用 slice将该值从原数组数组中删除,将该值添加到 result中
        const value = arr.splice(Math.floor(Math.random() * arr.length), 1)
        result.push(value[0]);
        i++;
    }
    return result;
}

洗牌算法:从最后一个元素开始,从数组中随机选出一个位置,交换,直到第一个元素。

function disorder(array) {
    const length = array.length;
    let current = length - 1;
    while(current > -1) {
        const random = Math.floor(length * Math.random());
        // 数组的每个值都会和另外一个随机位置的值交换位置
        // 使用数组的解构赋值,交换数组中的两个元素的位置
        [ array[current], array[random] ] = [ array[random], array[current] ];
        current--;
    }
    return array;
}

时间倒计时,今年还剩多少

//获取系统当前时间
var now = new Date();
//实例化今年跨年时间
var targDate = new Date(now.getFullYear() + 1, 0, 1);
//跨年时间与此时此刻时间差(毫秒)
var long = targDate - now;

var leftDay = parseInt(long / 1000 / 60 / 60 / 24);
long = long % (1000 * 60 * 60 * 24);
var leftHour =parseInt( long / 1000 / 60 / 60);
long = long % (1000 * 60 * 60);
var leftMinute = parseInt( long / 1000 / 60) ;
long = long % (1000 * 60 );
var leftSeconde = parseInt( long / 1000);
txt.value = now.getFullYear() + "年还剩" + leftDay
+ "天" + leftHour + "时"+leftMinute+"分"+leftSeconde+"秒";

盛最多水的容器

var maxArea = function (height) {
  let max = 0;
  for (let i = 0; i < height.length - 1; i++) {
    for (let j = i + 1; j < height.length; j++) {
      let area = (j - i) * Math.min(height[i], height[j]);
      max = Math.max(max, area);
    }
  }

  return max;
};
// 双指针
var maxArea = function (height) {
  let max = 0;
  let i = 0;
  let j = height.length - 1;
  while (i < j) {
    let minHeight = Math.min(height[i], height[j]);
    let area = (j - i) * minHeight;
    max = Math.max(max, area);
    if (height[i] < height[j]) {
      i++;
    } else {
      j--;
    }
  }
  return max;
};

爬楼梯,每次可爬1个或2个

var climbStairs = function (n) {
  let climbs = [];
  climbs[0] = 1;
  climbs[1] = 1;
  for (let i = 2; i <= n; i++) {
    climbs[i] = climbs[i - 1] + climbs[i - 2];
  }
  return climbs[n];
};

给定一个只包括'(',')','{','}','[',']' 的字符串,判断字符串是否有效。

var isValid = function (s) {
  let arr = [];
  let len = s.length;
  if (len % 2 !== 0) return false;
  for (let i = 0; i < len; i++) {
    let letter = s[i];
    switch (letter) {
      case "(": {
        arr.push(letter);
        break;
      }
      case "{": {
        arr.push(letter);
        break;
      }
      case "[": {
        arr.push(letter);
        break;
      }
      case ")": {
        if (arr.pop() !== "(") return false;
        break;
      }
      case "}": {
        if (arr.pop() !== "{") return false;
        break;
      }
      case "]": {
        if (arr.pop() !== "[") return false;
        break;
      }
    }
  }
  return !arr.length;
};
// hashmap
var isValid = function (s) {
  let map = {
    "(": ")",
    "{": "}",
    "[": "]",
  };
  let stack = [];
  let len = s.length;
  if (len % 2 !== 0) return false;
  for (let i of s) {
    if (i in map) {
      stack.push(i);
    } else {
      if (i !== map[stack.pop()]) return false;
    }
  }
  return !stack.length;
};

给定一个数组 nums,有一个大小为 k 的滑动窗口从最左侧移动到最右侧。, 返回滑动窗口中的最大值

var maxSlidingWindow = function (nums, k) {
  let len = nums.length;
  if (len === 0) return [];
  if (k === 1) return nums;
  let resArr = [];
  for (let i = 0; i <= len - k; i++) {
    let max = Number.MIN_SAFE_INTEGER;
    for (let j = i; j < i + k; j++) {
      max = Math.max(max, nums[j]);
    }
    resArr.push(max);
  }
  return resArr;
};
var maxSlidingWindow = function (nums, k) {
  // 缓存数组的长度
  const len = nums.length;
  const res = [];
  const deque = [];
  for (let i = 0; i < len; i++) {
    // 队尾元素小于当前元素
    while (deque.length && nums[deque[deque.length - 1]] < nums[i]) {
      deque.pop();
    }
    deque.push(i);

    // 当队头元素的索引已经被排除在滑动窗口之外时
    while (deque.length && deque[0] <= i - k) {
      // 队头元素出对
      deque.shift();
    }
    if (i >= k - 1) {
      res.push(nums[deque[0]]);
    }
  }
  return res;
};

根据每日气温列表,请重新生成一个列表,对应位置的输出是需要再等待多久温度才会升高超过该日的天数。给定一个列表 temperatures = [73, 74, 75, 71, 69, 72, 76, 73],你的输出应该是 [1, 1, 4, 2, 1, 1, 0, 0]。

function temp(arr) {
    let res = [];
    for (let i = 0; i < arr.length; i++) {
        res.push(0)
        for (let j = i + 1; j < arr.length; j++) {
            if (arr[j] > arr[i]) { res[i] = (j - i); break }
        }
    }
    return res;
}
var dailyTemperatures = function (T) {
  const len = T.length;
  const stack = [];
  const res = new Array(len).fill(0);
  for (let i = 0; i < len; i++) {
    while (stack.length && T[i] > T[stack[stack.length - 1]]) {
      const top = stack.pop();
      res[top] = i - top;
    }
    stack.push(i);
  }
  return res;
};

22. 括号生成

数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。

输入:n = 3
输出:[       "((()))",       "(()())",       "(())()",       "()(())",       "()()()"     ]
     
  var generateParenthesis = function (n) {
  const res = [];

  const dfs = (lRemain, rRemain, str) => { // 左右括号所剩的数量,str是当前构建的字符串
    if (str.length == 2 * n) { // 字符串构建完成
      res.push(str);           // 加入解集
      return;                  // 结束当前递归分支
    }
    if (lRemain > 0) {         // 只要左括号有剩,就可以选它,然后继续做选择(递归)
      dfs(lRemain - 1, rRemain, str + "(");
    }
    if (lRemain < rRemain) {   // 右括号比左括号剩的多,才能选右括号
      dfs(lRemain, rRemain - 1, str + ")"); // 然后继续做选择(递归)
    }
  };

  dfs(n, n, ""); // 递归的入口,剩余数量都是n,初始字符串是空串
  return res;
};

电话字母组合,给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。

输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
var letterCombinations = function (digits) {
  let res = [];
  if (!digits) return [];
  let map = {
    2: "abc",
    3: "def",
    4: "ghi",
    5: "jkl",
    6: "mno",
    7: "pqrs",
    8: "tuv",
    9: "wxyz",
  };
  function generate(i, str) {
    let len = digits.length;
    if (i === len) {
      res.push(str);
      return;
    }
    let chars = map[digits[i]];
    for (let j = 0; j < chars.length; j++) {
      generate(i + 1, str + chars[j]);
    }
  }
  generate(0, "");
  return res;
};

对每个孩子 i ,都有一个胃口值 gi ,并且每块饼干 j ,都有一个尺寸 sj 。如果 sj >= gi ,我们可以将这个饼干 j 分配给孩子 i

var findContentChildren = function (g, s) {
  g = g.sort((a, b) => a - b);
  s = s.sort((a, b) => a - b);
  let gLen = g.length,
    sLen = s.length,
    i = 0,
    j = 0,
    maxNum = 0;
  while (i < gLen && j < sLen) {
    if (s[j] >= g[i]) {
      i++;
      maxNum++;
    }
    j++;
  }
  return maxNum;
};

买卖股票的最佳时机 ,前一天买,后一天卖

输入: [7,1,5,3,6,4]
输出: 7
输入: [1,2,3,4,5]
输出: 4
var maxProfit = function (prices) {
  let profit = 0;
  for (let i = 0; i < prices.length - 1; i++) {
    if (prices[i + 1] > prices[i]) profit += prices[i + 1] - prices[i];
  }
  return profit;
};

一个机器人位于一个 m x n 网格的左上角,机器人每次只能向下或者向右移动一步。有多少不同的路径

var uniquePaths = function (m, n) {
  let dp = Array(m).fill(Array(n).fill(1));
  for (let i = 1; i < m; i++) {
    for (let j = 1; j < n; j++) {
      dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
    }
  }
  return dp[m - 1][n - 1];
};

前端路由实现

juejin.cn/post/684490…

hash 模式

class Routers {
  constructor() {
    this.routes = {};
    this.currentUrl = '';
    this.refresh = this.refresh.bind(this);
    window.addEventListener('load', this.refresh, false);
    window.addEventListener('hashchange', this.refresh, false);
  }

  route(path, callback) {
    this.routes[path] = callback || function() {};
  }

  refresh() {
    this.currentUrl = location.hash.slice(1) || '/';
    this.routes[this.currentUrl]();
  }
}

// 新增 后退功能
class Routers {
  constructor() {
    // 储存hash与callback键值对
    this.routes = {};
    // 当前hash
    this.currentUrl = '';
    // 记录出现过的hash
    this.history = [];
    // 作为指针,默认指向this.history的末尾,根据后退前进指向history中不同的hash
    this.currentIndex = this.history.length - 1;
    this.refresh = this.refresh.bind(this);
    this.backOff = this.backOff.bind(this);
    // 默认不是后退操作
    this.isBack = false;
    window.addEventListener('load', this.refresh, false);
    window.addEventListener('hashchange', this.refresh, false);
  }

  route(path, callback) {
    this.routes[path] = callback || function() {};
  }

  refresh() {
    this.currentUrl = location.hash.slice(1) || '/';
    if (!this.isBack) {
      // 如果不是后退操作,且当前指针小于数组总长度,直接截取指针之前的部分储存下来
      // 此操作来避免当点击后退按钮之后,再进行正常跳转,指针会停留在原地,而数组添加新hash路由
      // 避免再次造成指针的不匹配,我们直接截取指针之前的数组
      // 此操作同时与浏览器自带后退功能的行为保持一致
      if (this.currentIndex < this.history.length - 1)
        this.history = this.history.slice(0, this.currentIndex + 1);
      this.history.push(this.currentUrl);
      this.currentIndex++;
    }
    this.routes[this.currentUrl]();
    console.log('指针:', this.currentIndex, 'history:', this.history);
    this.isBack = false;
  }
  // 后退功能
  backOff() {
    // 后退操作设置为true
    this.isBack = true;
    this.currentIndex <= 0
      ? (this.currentIndex = 0)
      : (this.currentIndex = this.currentIndex - 1);
    location.hash = `#${this.history[this.currentIndex]}`;
    this.routes[this.history[this.currentIndex]]();
  }
}

history

class Routers {
  constructor() {
    this.routes = {};
    // 在初始化时监听popstate事件
    this._bindPopState();
  }
  // 初始化路由
  init(path) {
    history.replaceState({path: path}, null, path);
    this.routes[path] && this.routes[path]();
  }
  // 将路径和对应回调函数加入hashMap储存
  route(path, callback) {
    this.routes[path] = callback || function() {};
  }

  // 触发路由对应回调
  go(path) {
    history.pushState({path: path}, null, path);
    this.routes[path] && this.routes[path]();
  }
  // 监听popstate事件
  _bindPopState() {
    window.addEventListener('popstate', e => {
      const path = e.state && e.state.path;
      this.routes[path] && this.routes[path]();
    });
  }
}

统计函数执行次数

var getFunCallTimes = (function() {
    
    // 装饰器,在当前函数执行前先执行另一个函数
    function decoratorBefore(fn, beforeFn) {
        return function() {
            var ret = beforeFn.apply(this, arguments);

            // 在前一个函数中判断,不需要执行当前函数
            if (ret !== false) {
                fn.apply(this, arguments);
            }
        };
    }
    
    // 执行次数
    var funTimes = {};
    
    // 给fun添加装饰器,fun执行前将进行计数累加
    return function(fun, funName) {
        // 存储的key值
        funName = funName || fun;
        
        // 不重复绑定,有则返回
        if (funTimes[funName]) {
            return funTimes[funName];
        }
        
        // 绑定
        funTimes[funName] = decoratorBefore(fun, function() {
            // 计数累加
            funTimes[funName].callTimes++;

            console.log('count', funTimes[funName].callTimes);
        });
        
        // 定义函数的值为计数值(初始化)
        funTimes[funName].callTimes = 0;

        return funTimes[funName];
    }
})();

//
function someFunction() {
    
}

someFunction = getFunCallTimes(someFunction, 'someFunction');

统计函数执行时间

var getFunExecTime = (function() {
    
    // 装饰器,在当前函数执行前先执行另一个函数
    function decoratorBefore(fn, beforeFn) {
        return function() {
            var ret = beforeFn.apply(this, arguments);

            // 在前一个函数中判断,不需要执行当前函数
            if (ret !== false) {
                fn.apply(this, arguments);
            }
        };
    }

    // 装饰器,在当前函数执行后执行另一个函数
    function decoratorAfter(fn, afterFn) {
        return function() {
            fn.apply(this, arguments);
            afterFn.apply(this, arguments);
        };
    }
    
    // 执行次数
    var funTimes = {};
    
    // 给fun添加装饰器,fun执行前后计时
    return function(fun, funName) {
        return decoratorAfter(decoratorBefore(fun, function() {
            // 执行前
            console.time(funName);
        }), function() {
            // 执行后
            console.timeEnd(funName);
        });
    }
})();

// 
function someFunction() {
    for (var i = 0; i < 100000; ++i) {

    }
}
someFunction = getFunExecTime(someFunction, 'someFunction');

控制函数的调用次数

function someFunction() {
    console.log(1);
}

function otherFunction() {
    console.log(2);
}


function setFunCallMaxTimes(fun, times, nextFun) {
    return function() {
        if (times-- > 0) {
            // 执行函数
            return fun.apply(this, arguments);
        } else if (nextFun && typeof nextFun === 'function') {
            // 执行下一个函数
            return nextFun.apply(this, arguments);
        }
    };
}

var fun = setFunCallMaxTimes(someFunction, 3, otherFunction);

feetch

  1. fetch默认不携带cookie fetch发送请求默认是不发送cookie的,不管是同域还是跨域;那么问题就来了,对于那些需要权限验证的请求就可能无法正常获取数据,这时可以配置其credentials项,其有3个值:

omit: 默认值,忽略cookie的发送 same-origin: 表示cookie只能同域发送,不能跨域发送 include: cookie既可以同域发送,也可以跨域发送 2. fetch请求对某些错误http状态不会reject 这主要是由fetch返回promise导致的,因为fetch返回的promise在某些错误的http状态下如400、500等不会reject,相反它会被resolve;只有网络错误会导致请求不能完成时,fetch 才会被 reject;

function checkStatus(response) {
  if (response.status >= 200 && response.status < 300) {
    return response;
  }
  const error = new Error(response.statusText);
  error.response = response;
  throw error;
}
function parseJSON(response) {
  return response.json();
}
export default function request(url, options) {
  let opt = options||{};
  return fetch(url, {credentials: 'include', ...opt})
    .then(checkStatus)
    .then(parseJSON)
    .then((data) => ( data ))
    .catch((err) => ( err ));
}
  1. fetch不支持超时timeout处理
var oldFetchfn = fetch; //拦截原始的fetch方法
window.fetch = function(input, opts){//定义新的fetch方法,封装原有的fetch方法
    var fetchPromise = oldFetchfn(input, opts);
    var timeoutPromise = new Promise(function(resolve, reject){
        setTimeout(()=>{
             reject(new Error("fetch timeout"))
        }, opts.timeout)
    });
    retrun Promise.race([fetchPromise, timeoutPromise])
}
  1. fetch不支持JSONP
  2. fetch不支持progress事件
var xhr = new XMLHttpRequest()
xhr.open('POST', '/uploads')
xhr.onload = function() {}
xhr.onerror = function() {}
function updateProgress (event) {
  if (event.lengthComputable) {
    var percent = Math.round((event.loaded / event.total) * 100)
    console.log(percent)
  }
xhr.upload.onprogress =updateProgress; //上传的progress事件
xhr.onprogress = updateProgress; //下载的progress事件
}
xhr.send();
fetch(url).then(response => {
  // response.body is a readable stream.
  // Calling getReader() gives us exclusive access to the stream's content
  var reader = response.body.getReader();
  var bytesReceived = 0;

  // read() returns a promise that resolves when a value has been received
  reader.read().then(function processResult(result) {
    // Result objects contain two properties:
    // done  - true if the stream has already given you all its data.
    // value - some data. Always undefined when done is true.
    if (result.done) {
      console.log("Fetch complete");
      return;
    }

    // result.value for fetch streams is a Uint8Array
    bytesReceived += result.value.length;
    console.log('Received', bytesReceived, 'bytes of data so far');

    // Read some more, and call this function again
    return reader.read().then(processResult);
  });
});

观察者模式 和 发布订阅模式

  • 在观察者模式中,观察者需要直接订阅目标事件;在目标发出内容改变的事件后,直接接收事件并作出响应
  • 在发布订阅模式中,发布者和订阅者之间多了一个发布通道;一方面从发布者接收事件,另一方面向订阅者发布事件;订阅者需要从事件通道订阅事件;以此避免发布者和订阅者之间产生依赖关系
/**
 * 观察监听一个对象成员的变化
 * @param {Object} obj 观察的对象
 * @param {String} targetVariable 观察的对象成员
 * @param {Function} callback 目标变化触发的回调
 */
function observer(obj, targetVariable, callback) {
  if (!obj.data) {
    obj.data = {}
  }
  Object.defineProperty(obj, targetVariable, {
    get() {
      return this.data[targetVariable]
    },
    set(val) {
      this.data[targetVariable] = val
      // 目标主动通知观察者
      callback && callback(val)
    },
  })
  if (obj.data[targetVariable]) {
    callback && callback(obj.data[targetVariable])
  }
}
class Event {
  constructor() {
    // 所有 eventType 监听器回调函数(数组)
    this.listeners = {}
  }
  /**
   * 订阅事件
   * @param {String} eventType 事件类型
   * @param {Function} listener 订阅后发布动作触发的回调函数,参数为发布的数据
   */
  on(eventType, listener) {
    if (!this.listeners[eventType]) {
      this.listeners[eventType] = []
    }
    this.listeners[eventType].push(listener)
  }
  /**
   * 发布事件
   * @param {String} eventType 事件类型
   * @param {Any} data 发布的内容
   */
  emit(eventType, data) {
    const callbacks = this.listeners[eventType]
    if (callbacks) {
      callbacks.forEach((c) => {
        c(data)
      })
    }
  }
}

const event = new Event()
event.on('open', (data) => {
  console.log(data)
})
event.emit('open', { open: true })

0.1 + 0.2 === 0.3

function add() {
        const args = [...arguments]
        const maxLen = Math.max.apply(
          null,
          args.map(item => {
            const str = String(item).split('.')[1]
            return str ? str.length : 0
          })
        )
        return (
          args.reduce((sum, cur) => sum + cur * 10 ** maxLen, 0) / 10 ** maxLen
        )
      }

大数相加

function addBigNumbers(a, b) {
  // 初始化指针(指向字符串末尾)和进位
  let i = a.length - 1;
  let j = b.length - 1;
  let carry = 0;
  let result = [];
  
  // 从末位开始逐位相加,直到两个数都处理完且无进位
  while (i >= 0 || j >= 0 || carry > 0) {
    // 取出当前位的数字(若已超出字符串长度则用 0 代替)
    const digitA = i >= 0 ? parseInt(a[i], 10) : 0;
    const digitB = j >= 0 ? parseInt(b[j], 10) : 0;
    
    // 计算当前位总和(包括进位)
    const sum = digitA + digitB + carry;
    // 保留当前位的结果(总和 % 10)
    result.push(sum % 10);
    // 更新进位(总和 // 10)
    carry = Math.floor(sum / 10);
    
    // 移动指针到前一位
    i--;
    j--;
  }
  
  // 结果数组是倒序的,反转后拼接成字符串
  return result.reverse().join('');
}

lodash.set

function set(object, path, value, customizer = (obj, key) => ({})) {
  if (object === null || object === undefined) {
    return object; // or throw an error
  }

  const pathArray = Array.isArray(path) ? path : path.split(/\.|$$(\d+)$$/g).filter(Boolean);
  let current = object;

  for (let i = 0; i < pathArray.length - 1; i++) {
    const key = pathArray[i];
    const numKey = parseInt(key, 10);
    if (isNaN(numKey)) {
      if (!current.hasOwnProperty(key)) {
        current[key] = customizer(current, key);
      }
      current = current[key];
    } else {
      if (!Array.isArray(current) || numKey < 0) {
        return object; // or throw an error
      }
      if (numKey >= current.length) {
        current.length = numKey + 1;
      }
      current = current[numKey];
    }
  }

  current[pathArray[pathArray.length - 1]] = value;
  return object;
}

// 测试用例 (使用自定义设置函数)
const object = {};
set(object, 'a.b.c', 3, (obj, key) => ({ [key]: 'default' }));
console.log(object); // { a: { b: { c: 3 } } }  (customizer 没有实际效果,因为已有值)

lodash.get

function get(object, path, defaultValue = undefined) {
  if (object === null || object === undefined || path === null || path === undefined || path === '') {
    return defaultValue;
  }

  const pathArray = Array.isArray(path) ? path : path.split(/\.|$$(\d+)$$/g).filter(Boolean);
  let current = object;

  for (const key of pathArray) {
    const numKey = parseInt(key, 10);
    if (isNaN(numKey)) {
      if (current === null || current === undefined || !current.hasOwnProperty(key)) {
        return defaultValue;
      }
      current = current[key];
    } else {
      if (current === null || current === undefined || !Array.isArray(current) || numKey < 0 || numKey >= current.length) {
        return defaultValue;
      }
      current = current[numKey];
    }
  }

  return current;
}

// 测试用例 (包含更多异常情况)
const object = { a: [{ b: { c: 3 } }] };
console.log(get(object, 'a[0].b.c')); // 3

逗号千分位

function formatWithCommas(num) {
  // 分割整数和小数部分
  const parts = num.toString().split('.');
  let integerPart = parts[0];
  const decimalPart = parts[1] || '';
  
  let result = '';
  // 从右向左处理整数部分,每三位加一个逗号
  while (integerPart.length > 3) {
    // 截取最后三位,拼到结果前
    result = ',' + integerPart.slice(-3) + result;
    // 剩余部分继续处理
    integerPart = integerPart.slice(0, integerPart.length - 3);
  }
  // 拼接剩余的整数部分和小数部分
  return integerPart + result + (decimalPart ? '.' + decimalPart : '');
}

Map转Tree

function mapToTree(items) {
  const idToItemMap = new Map();
  const tree = [];

  // 第一步:将所有节点存入 Map,方便快速查找
  items.forEach(item => {
    // 复制节点,避免修改原始数据
    const newItem = { ...item, children: [] };
    idToItemMap.set(item.id, newItem);
  });

  // 第二步:构建树结构
  items.forEach(item => {
    const currentItem = idToItemMap.get(item.id);
    const parentId = item.parentId;

    // 根节点(parentId 不存在或为 0、null 等)直接加入树
    if (parentId === undefined || parentId === null || parentId === 0) {
      tree.push(currentItem);
    } else {
      // 非根节点,找到父节点并加入其 children
      const parentItem = idToItemMap.get(parentId);
      if (parentItem) {
        parentItem.children.push(currentItem);
      }
    }
  });

  return tree;
}

function mapToTree(items) {
  // 查找所有根节点(parentId 不存在或为 0、null 等)
  return items
    .filter(item => item.parentId === undefined || item.parentId === null || item.parentId === 0)
    .map(item => ({
      ...item,
      children: getChildren(item.id, items)
    }));

  // 递归获取子节点
  function getChildren(parentId, allItems) {
    return allItems
      .filter(item => item.parentId === parentId)
      .map(item => ({
        ...item,
        children: getChildren(item.id, allItems)
      }));
  }
}