前端核心笔试题(全网最系统,强烈建议收藏)

843 阅读21分钟

基础篇

@对象操作

手写深拷贝

function deepCopy(data){
    switch (true) {
        /* 基本数据类型 */
        case typeof(data)!=="object" && typeof(data)!=="function":
            return data;

        /* 函数类型 */
        case typeof(data)==="function":
            return data;

        /* 数组类型 */
        case Array.isArray(data):
            const arr = []
            // 递归深拷贝所有元素 丢入arr
            for(let i=0;i<data.length;i++){
                arr[i] = deepCopy(data[i])
            }

            return arr;

        /* 对象类型 */
        case typeof(data)==="object":
            const obj = {}
            // 递归深拷贝对象中的所有key-value 丢入obj
            for(let key in data){
                obj[key] = deepCopy(data[key])
            }
            return obj;

        default:
            return data;
    }
}

提取查询参数

手写getSearchParams从url中提取所有查询参数,以对象形式返回

/**
 * 从url中提取查询参数 /add?a=2&b=3 => {a:2,b:3}
 * @param {string} url 用户请求的路径
 * @returns 提取好的查询参数形成的对象
 */
function getSearchParams(url) {
  // 创建结果对象
  const obj = {};

  // 摘出a=2&b=3
  // "/add?a=123&b=456&c=789#abc".match(/\w+=\w+/g)
  // const reg = /?(.*)/;
  // const str = reg.exec(url)[1];
  // 使用&做分隔符 肢解字符串为[a=2,b=3]
  // const arr = str.split("&");

  const reg = /\w+=\w+/g
  const arr = url.match(reg)//[子串1,子串2] [a=2,b=3]
  console.log("getSearchParams:arr=",arr);

  // 遍历上述数组 将每个元素以=肢解为 [a,2] 将这一组key-value收集到结果对象中
  arr.forEach((item) => {
    let [key,value] = item.split("=");
    obj[key] = value//obj.a = 2 obj.b=3
  });

  // 返回结果对象
  return obj;//{a:2,b:3}
}

虚拟dom转真实dom

const vnode = {
    tag: 'DIV',
    attrs: {
        id: 'app'
    },
    children: [{
        tag: 'SPAN',
        children: [{
            tag: 'A',
            children: []
        }]
    },
    {
        tag: 'SPAN',
        children: [{
            tag: 'A',
            children: []
        },
        {
            tag: 'A',
            children: []
        }
        ]
    }
    ]
}

function render(vnode, container) {
    return container.appendChild(_render(vnode));
}

function _render(vnode) {
    if (typeof vnode === 'number') {
        vnode = String(vnode);
    }
    //处理文本节点
    if (typeof vnode === 'string') {
        const textNode = document.createTextNode(vnode)
        return textNode;
    }
    //处理组件
    if (typeof vnode.tag === 'function') {
        const component = createComponent(vnode.tag, vnode.attrs);
        setComponentProps(component, vnode.attrs);
        return component.base;
    }
    //普通的dom
    const dom = document.createElement(vnode.tag);
    if (vnode.attrs) {
        Object.keys(vnode.attrs).forEach(key => {
            const value = vnode.attrs[key];
            setAttribute(dom, key, value);    // 设置属性
        });
    }
    vnode.children.forEach(child => render(child, dom));    // 递归渲染子节点
    return dom;    // 返回虚拟dom为真正的DOM
}

//实现dom挂载到页面某个元素
const ReactDOM = {
    render: (vnode, container) => {
        container.innerHTML = '';
        return render(vnode, container);
    }
}

@数组操作

元素速查

给几个数组, 可以通过数值找到对应的数组名称

// 比如这个函数输入一个1,那么要求函数返回A
const A = [1, 2, 3];
const B = [4, 5, 6];
const C = [7, 8, 9];

const test = (num) => {
    const newArr = [A, B, C];
    let i = 0;
    while (i < newArr.length) {
        if (newArr[i].includes(num)) return newArr[i];
        i++;
    }
    return [];
}

console.log(test(5));

有序数组原地去重

// 快慢指针
const res = [0, 0, 1, 1, 2, 2, 2, 2, 4, 4, 5, 5, 6]

const removeDuplicates = (nums) => {
    let slow = 0, fast = 1;
    if (nums.length === 0) return
    while (fast < nums.length) {
        if (nums[fast] !== nums[slow]) {
            slow++;
            nums[slow] = nums[fast];
        }
        fast++;
    }
    return nums.splice(0, slow + 1);
}

全排列

不定长二维数组的全排列

// 输入 [['A', 'B', ...], [1, 2], ['a', 'b'], ...]
// 输出 ['A1a', 'A1b', ....]
let res = arr.reduce((prev, cur) => {
    if (!Array.isArray(prev) || !Array.isArray(cur)) {
        return
    }
    if (prev.length === 0) {
        return cur
    }
    if (cur.length === 0) {
        return prev
    }
    const emptyVal = []
    prev.forEach(val => {
        cur.forEach(item => {
            emptyVal.push(`${val}${item}`)
        })
    })
    return emptyVal
}, [])

console.log(res); 

@字符串处理

字符串去重

去除字符串中出现次数最少的字符,不改变原字符串的顺序

// “ababac” —— “ababa”
// “aaabbbcceeff” —— “aaabbb”

const changeStr = (str) => {
    let obj = {};
    const _str = str.split("");
    _str.forEach(item => {
        if (obj[item]) {
            obj[item]++;
        } else {
            obj[item] = 1;
        }
    })

    var _obj = Object.values(obj).sort();
    var min = _obj[0];
    for (let key in obj) {
        if (obj[key] <= min) {
            var reg = new RegExp(key, "g")
            str = str.replace(reg, "")
        }
    }
    return str;
}

console.log(changeStr("aaabbbcceeff"));

最少操作完成变形

两个字符串对比, 得出结论都做了什么操作, 比如插入或者删除

// pre = 'abcde123'
// now = '1abc123'
// a前面插入了1,c后面删除了de

function compareStrings(pre, now) {
    let diff = JsDiff.diffChars(pre, now);
    let result = [];
    diff.forEach(function (part) {
        let type = part.added ? '插入' : part.removed ? '删除' : '保持不变';
        if (type !== '保持不变') {
            result.push('在pre的第' + part.index + '个位置' + type + '了' + part.value);
        }
    });
    return result;
}

let pre = 'abcde123';
let now = '1abc123';

let result = compareStrings(pre, now);
result.forEach(function (part) {
    console.log(part);
});

//在pre的第0个位置插入了1
//在pre的第3个位置删除了d
//在pre的第4个位置删除了e
function compareStrings(pre, now) {
    let result = [];
    let i = 0;
    let j = 0;
    let pre_len = pre.length;
    let now_len = now.length;
    while (i < pre_len && j < now_len) {
        if (pre[i] === now[j]) {
            i++;
            j++;
        } else {
            let pre_end = i;
            let now_end = j;
            while (pre_end < pre_len && pre[pre_end] !== now[j]) {
                pre_end++;
            }
            while (now_end < now_len && now[now_end] !== pre[i]) {
                now_end++;
            }
            if (pre_end - i < now_end - j) {
                result.push('在pre的第' + i + '个位置删除了' + pre.substring(i, pre_end));
                i = pre_end;
            } else {
                result.push('在pre的第' + i + '个位置插入了' + now.substring(j, now_end));
                j = now_end;
            }
        }
    }
    while (i < pre_len) {
        result.push('在pre的第' + i + '个位置删除了' + pre[i]);
        i++;
    }
    while (j < now_len) {
        result.push('在pre的第' + i + '个位置插入了' + now[j]);
        j++;
    }
    return result;
}

let pre = 'abcde123';
let now = '1abc123';
let result = compareStrings(pre, now);
result.forEach(function (part) {
    console.log(part);
});

//在pre的第0个位置插入了1
//在pre的第3个位置删除了de

数值转汉语

写出一个函数trans,将数字转换成汉语的输出,输入为不超过10000亿的数字

//将数字(整数)转为汉字,从零到一亿亿,需要小数的可自行截取小数点后面的数字直接替换对应arr1的读法就行了
const convertToChinaNum = (num) => {
    var arr1 = new Array('零', '一', '二', '三', '四', '五', '六', '七', '八', '九');
    var arr2 = new Array('', '十', '百', '千', '万', '十', '百', '千', '亿', '十', '百', '千', '万', '十', '百', '千', '亿');//可继续追加更高位转换值
    if (!num || isNaN(num)) {
        return "零";
    }
    var english = num.toString().split("")
    var result = "";
    for (var i = 0; i < english.length; i++) {
        var des_i = english.length - 1 - i;//倒序排列设值
        result = arr2[i] + result;
        var arr1_index = english[des_i];
        result = arr1[arr1_index] + result;
    }
    //将【零千、零百】换成【零】 【十零】换成【十】
    result = result.replace(/零(千|百|十)/g, '零').replace(/十零/g, '十');
    //合并中间多个零为一个零
    result = result.replace(/零+/g, '零');
    //将【零亿】换成【亿】【零万】换成【万】
    result = result.replace(/零亿/g, '亿').replace(/零万/g, '万');
    //将【亿万】换成【亿】
    result = result.replace(/亿万/g, '亿');
    //移除末尾的零
    result = result.replace(/零+$/, '')
    //将【零一十】换成【零十】
    //result = result.replace(/零一十/g, '零十');//貌似正规读法是零一十
    //将【一十】换成【十】
    result = result.replace(/^一十/g, '十');
    return result;
}

@OOP

扩展现有类

扩展数组实现arr.countItems,以对象形式返回每个元素分别出现多少次

Array.prototype.countItems = function(){
    const result = {}
    for(let i of this){
        if(result[i]){
            result[i]++
        }else{
            result[i] = 1
        }
    }
    return result
}

原型链

手写Animal-Person-Student三级继承关系(含静态与覆写)原型链实现

  function Animal(type, food) {
    this.type = type;
    this.food = food;
  }

  Animal.prototype.eat = function () {
    console.log(`一只${this.type}正在享用${this.food}`);
  };

  /*  */
  function Person(name) {
    // this.type = type;
    // this.food = food;

    /* ES5的属性继承 */
    Animal.call(this, "人类", "五谷杂粮");

    this.name = name;
  }

  /* 【以一个父类实例做子类原型】 */
  Person.prototype = new Animal();

  Person.prototype.think = function () {
    console.log(`${this.name}正在思考`);
  };

  /* 对父类的eat实现不满意 就重写覆盖override它 */
  Person.prototype.eat = function(){
    Animal.prototype.eat.apply(this)
    console.log(`${this.name}正在享用${this.food}`);
  }

  Person.introduction = "两足无毛大脑袋动物"
  Person.getPopulation = function(){
    //查UN的数据库
    return 8 * Math.pow(10,9)
  }


  /*  */
  function Student(name,major){
    Person.apply(this,[name])
    this.major = major
  }

  Student.prototype = new Person()

  Student.prototype.study = function(){
    console.log(`${this.name}正在研习${this.major}`);
  }

  ~(function () {
    // const dog = new Animal("狗狗", "大骨棒");
    // dog.eat();

    // const p = new Person("张三疯");
    // p.think();

    const xm = new Student("小明","前端开发")
    console.log(xm);
    // xm.study()

    xm.eat()

  })();

class

手写Animal-Person-Student三级继承关系(含静态与覆写)class实现

class Animal {
  constructor(type, food) {
    this.type = type;
    this.food = food;
  }

  eat() {
    console.log(`一只${this.type}正在摄入${this.food}`);
  }
}

/* extends的一刹那 已经将Animal的实例绑定给Person做为原型了 */
class Person extends Animal {
  /* 与具体实例无关的属性和方法——静态成员 */
  static legs = 4;
  static getInfo() {
    // console.log("this in static method", this);
    return `人类是一种四足无毛动物`;
  }

  constructor(name, age) {
    // Must call super constructor in derived class before accessing 'this' or returning from derived constructor
    // Animal.call(this,"人类", "大米白面")
    super("人类", "大米白面");
    this.name = name;
    this.age = age;
  }

  /* 扩展父类方法(通过重写/覆盖/override) */
  eat() {
    // 调用父类方法
    super.eat(); //Animal.prototype.eat.call(this)
    console.log(`一只${this.type}${this.name}正在享用${this.food}`);
  }

  /* 人类会思考 */
  // Person.prototype.think = function(){}
  think() {
    console.log(`${this.name}正在思考`);
  }
}

class Student extends Person {
  // 如果想扩展属性 就要重新写构造器
  constructor(name, age, major) {
    // 只要重新写构造器 就一定要先调用父类构造器
    // Person.call(this,name,age)
    super(name, age);
    this.major = major;
  }

  study() {
    console.log(`${this.name}正在学习${this.major}`);
  }

  /* 重写父类think方法 */
  think() {
    super.think();
    console.log(`学生【${this.name}】正在潜心钻研${this.major}`);
  }

  toString() {
    return `[姓名:${this.name},专业:${this.major}]`;
  }
}

手写MyMap

  class MyMap {
    constructor(dataObj) {
      // 用于存储底层数据的对象
      this.dataObj = dataObj || {};
      this.updateSize()
    }

    updateSize() {
      // Object.keys(obj)将对象的【自有属性/非继承属性】以数组形式罗列
      this.size = this.keys().length;
    }

    set(key, value) {
      // 往【底层数据对象】中丢入一个key-value
      this.dataObj[key] = value;
      this.updateSize();
    }

    get(key) {
      return this.dataObj[key];
    }

    /* 获取全部keys */
    keys() {
      return Object.keys(this.dataObj);
    }

    /* 获取全部值 */
    values() {
      // 将keys数组映射成value数组
      return this.keys().map((key) => this.get(key));
    }

    /* 获取全部键值 */
    entries() {
      // 将keys数组映射成{key,value}对象数组
      return this.keys().map((key) => ({ key: key, value: this.get(key) }));
    }

    /* 查询key是否存在 */
    has(key) {
      // 判断对象中是否有【自有属性(继承来的不算)】
      return this.dataObj.hasOwnProperty(key);
    }

    /* 删除key-value */
    delete(key) {
      delete this.dataObj[key];
      this.updateSize();
    }

    /* 批处理函数forEach */
    forEach(handler) {
      for (let key in this.dataObj) {
        handler(this.get(key), key);
      }
    }

    /* filter */
    filter(handler) {
      const noahMap = {};
      for (let key in this.dataObj) {
        let ok = handler(this.get(key), key);
        ok && (noahMap[key] = this.get(key));
      }
      return new MyMap(noahMap);
    }
  }

<script>
  const mmap = new MyMap();

  /* 增加Key-value */
  mmap.set("a", "foo");
  mmap.set("b", "bar");
  mmap.set("c", "baz");
  mmap.set("d", "boo");

  /* 根据Key查询value */
  console.log(mmap.get("a"));
  console.log(mmap.get("b"));

  // 查询尺寸
  console.log(mmap.size);

  // 获取全部keys
  console.log(mmap.keys());

  // 获取全部values
  console.log(mmap.values());

  // 获取全部entries
  console.log(mmap.entries());

  /* 遍历mmap中的全部entry */
  for (let entry of mmap.entries()) {
    console.log(entry);
  }

  // 查询key是否存在
  console.log("has a?", mmap.has("a"));
  console.log("has toString?", mmap.has("toString"));
  console.log("has c?", mmap.has("c"));

  // 删除Key-value
  mmap.delete("b");
  console.log(mmap);

  // 修改
  mmap.set("a", "张真人");
  console.log(mmap.get("a"));

  /* for-Each */
  mmap.forEach((value, key) => {
    console.log(`{key:${key},value:${value}}`);
  });

  mmap.set("tesla", { boss: "mask", age: 48 });
  mmap.set("microsoft", { name: "bill", age: 80 });
  mmap.set("meta", { name: "zuckberg", age: 38 });
  mmap.set("alibaba", { name: "jack", age: 55 });

  /* 过滤掉年龄大于50的家伙 */
  const newMap = mmap.filter((value, key) => value.age < 50);
  console.log(newMap);

</script>

手写MySet

  class MySet {
    constructor(dataArr){
      this.dataArr = dataArr || []
      this.removeRepeat()

      this.updateSize()
    }

    /* 过滤掉重复元素 */
    removeRepeat(){
      this.dataArr = this.dataArr.filter(
        (item,index) => this.dataArr.indexOf(item)===index
      )
    }

    /* 更新size */
    updateSize(){
      this.size = this.dataArr.length
    }

    /* 追加一个元素 */
    add(value){
      // 当前dataArr中如果没有value 就追加之
      if(this.dataArr.indexOf(value)===-1){
        this.dataArr.push(value)
      }
      this.updateSize()
    }

    /* 删除元素 */
    delete(value){
      const index = this.dataArr.indexOf(value)
      if(index!==-1){
        this.dataArr.splice(index,1)
        this.updateSize()
        return true
      }else{
        return false
      }
    }

    /* 清空set */
    clear(){
      this.dataArr = []
      this.updateSize()
    }

    /* 得到所有元素形成的数组 */
    values(){
      return this.dataArr
    }

    /* 查询元素是否存在 */
    has(value){
      return this.dataArr.indexOf(value)!==-1
    }

    /* forEach */
    forEach(handler){
      for(let i=0;i<this.dataArr.length;i++){
        handler.apply(null,[this.dataArr[i],i,this])
      }
    }
  }
  
  <script>
  const mset = new MySet([3,1,4,1,5,9,2,6,5,3,5,8,9,7,9,3])

  mset.add("foo")
  mset.add("bar")
  mset.add("bar")

  mset.delete("bar")
  mset.delete(5)
  // mset.clear()
  console.log(mset);

  console.log(mset.values());

  console.log(mset.has(3));
  console.log(mset.has(13));

  mset.forEach(
    (value,index,set)=>{
      console.log(index,value,set);
    }
  )
</script>

@异步编程

Promise链式编程

手写Promise链求5的阶乘

  function multiply(a, b, callback) {
    setTimeout(() => {
      const ret = a * b;
      callback(ret);
    }, 2000);
  }
  // multiply(2,3,ret=>console.log("ret=",ret))

  /* 但凡耗时任务的结果,要么回调,要么Promise */
  function mulPromise(a, b) {
    return new Promise((resolve, reject) => {
      multiply(2, 3, (ret) => resolve(ret));
    });
  }

  mulPromise(2, 3)
    // .then((ret) => ret * 4)
    .then((ret) => mulPromise(ret, 4))
    .then((ret) => mulPromise(ret, 4))
    .then((ret) => console.log("最终结果", ret));

async / await

手写async-await求5的阶乘

  function multiply(a, b, callback) {
    setTimeout(() => {
      const ret = a * b;
      callback(ret);
    }, 2000);
  }
  // multiply(2,3,ret=>console.log("ret=",ret))

  /* 但凡耗时任务的结果,要么回调,要么Promise */
  function mulPromise(a, b) {
    return new Promise((resolve, reject) => {
      multiply(2, 3, (ret) => resolve(ret));
    });
  }

  /* await */
  ~(async function awaitDemo() {
    try {
      let ret = await mulPromise(2, 3);
      ret = await mulPromise(ret, 4);
      ret = await mulPromise(ret, 5);
      console.log("最终结果", ret);
    } catch (err) {
      console.log("err=", err);
    }
  })();

三大静态调度

手写Promise三大静态调度API demo各一个

  /* 连坐期约:全部Promise成员都履约才视为整体履约 只要有一个Promise成员毁约 整体就视为毁约 */
  function demoPromiseAll(){
    Promise.all(
      [
        // 成员Promise1
        Promise.resolve("foo"),

        // 成员Promise2
        // Promise.resolve("bar"),
        Promise.reject("我妈喊我回家吃饭"),

        // 成员Promise3
        new Promise(
          (resolve,reject)=>setTimeout(() => {
            resolve("baz")
          }, 3000)
        )
      ]
    )

    // 全部Promise成员都履约才视为整体履约
    .then(
      values => console.log("整体成功:values=",values)
    )

    // 只要有一个Promise成员毁约 整体就视为毁约
    .catch(
      err => console.error("整体失败:err=",err)
    )

  }
  demoPromiseAll()

  /* 详查期约: 所有的Promise成员都独立汇报结果:人人都得尘埃落定(谁也不许浑水摸鱼)=>all settled */
  function demoPromiseAllSettled(){
    Promise.allSettled(
      [
        Promise.resolve("foo"),
        Promise.reject("宝宝喊我回家吃饭"),
        new Promise((resolve,reject)=>{
          setTimeout(() => {
            Math.random()>0.5?resolve("bar"):reject("春霞喊我做核酸")
          }, 3000);
        })
      ]
    )

    /* 
    0: {status: 'fulfilled', value: 'foo'}
    1: {status: 'rejected', reason: '宝宝喊我回家吃饭'}
    2: {status: 'rejected', reason: '春霞喊我做核酸'} 
    */
    .then(results=>console.log("所有成员都尘埃落定了:results=",results))
  }
  demoPromiseAllSettled()

  /* 竞速期约:以最快出结果的成员Promise的结果为整体结果 */
  function demoPromiseRace(){
    Promise.race(
      [
        // Promise成员1
        new Promise((resolve)=>setTimeout(() => {
          resolve("foo")
        }, 3000)),

        // Promise成员2
        new Promise((resolve,reject)=>setTimeout(() => {
          // resolve("bar")
          reject("宝宝和春霞都喊我去做核酸,opacity:0")
        }, 500)),

      ]
    )
    .then(value=>console.log("整体成功了:value=",value))
    .catch(err=>console.log("整体失败了:err=",err))
  }
  demoPromiseRace()

手封ajaxPromise

/* 使用合并对象的方式做配置的合并 */
function ajax(conf) {
  /* 使用合并对象的方式做默认配置 */

  // 默认配置
  const defaultConf = {
    method: "GET",
    onSuccess: (data) => console.log("data=", data),
    onFail: (err) => console.error("err=", err),

    // 将用户配置中的所有key-value打散 合并到defaultConf中
    ...conf,
  };

  let { method, url, data, dataType, onSuccess, onFail } = defaultConf;
  // console.log(method, url, data, dataType, onSuccess, onFail);

  /* 如果用户未传递URL 办它/弄死这只傻鸟 */
  if (!url) {
    // 弄死它的目的是为了引起其注意 以提前修正错误
    throw new Error("亲爱的傻鸟,URL必须传递哦~");
  }

  let body = null;

  const xhr = new XMLHttpRequest();

  xhr.onload = () => {
    onSuccess(xhr.responseText);
  };

  xhr.onerror = (err) => onFail(err);

  /* 为GET请求拼接查询参数字符串 */
  if (method === "GET" && data) {
    url += `?${getSearchParams(data)}`;
  }

  xhr.open(method, url);

  /* 设置Content-Type请求头(必须在xhr打开连接以后才能配置) */
  switch (true) {
    case dataType === "form":
      xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
      body = getSearchParams(data);
      break;

    case dataType === "json":
      xhr.setRequestHeader("Content-Type", "application/json");
      body = JSON.stringify(data);
      break;

    default:
      break;
  }

  xhr.send(body);
}

/* 得到一个执行AJAX任务的Promise对象 */
function ajaxPromise(conf) {
  /* 返回一个履约/毁约未可知的Promise对象 */
  return new Promise(
    // 执行网络请求
    (resolve, reject) => {
      ajax({
        // method,url,data,dataType...
        ...conf,

        /* 重构成功与失败回调 */       
        // 成功时resolve数据
        onSuccess: (data) => resolve(data),

        // 失败时reject错误信息
        onFail: (err) => reject(err),
      });
    }
  );
  
}

限制并发量

实现一个批量请求函数, 能够限制并发量

const sendRequests = (reqs, max, callback = () => { }) => {
  let waitList = [];
  let currentNum = 0;
  let NumReqDone = 0;
  const results = new Array(reqs.length).fill(false);

  const init = () => {
    reqs.forEach((element, index) => {
      request(index, element);
    });
  }

  const request = async (index, reqUrl) => {
    if (currentNum >= max) {
      await new Promise(resolve => waitList.push(resolve))
    }
    reqHandler(index, reqUrl);
  }

  const reqHandler = async (index, reqUrl) => {
    currentNum++;
    try {
      const result = await fetch(reqUrl);
      results[index] = result;
    } catch (err) {
      results[index] = err;
    } finally {
      currentNum--;
      NumReqDone++;
      if (waitList.length) {
        waitList[0]();
        waitList.shift();
      }
      if (NumReqDone === max) {
        callback(results);
      }
    }
  }
  init()
}
const allRequest = [
  "https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=1",
  "https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=2",
  "https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=3"
];
sendRequests(allRequest, 2, (res) => console.log(res))

睡眠阻塞

实现sleep(milliseeconds)函数

  • 循环阻塞法
function sleep(delay) {
    var start = (new Date()).getTime();
    while ((new Date()).getTime() - start < delay) {
        continue;
    }
}

function test() {
    console.log('111');
    sleep(2000);
    console.log('222');
}
  • 定时器回调法
function sleep(ms, callback) {
    setTimeout(callback, ms)
}

sleep(2000, () => {
    console.log("sleep")
})
  • Promise定时履约
const sleep = time => {
    return new Promise(resolve => setTimeout(resolve, time))
}

sleep(1000).then(() => { console.log(1) })
  • async/await
function sleep(time) {
    return new Promise(resolve =>
        setTimeout(resolve, time))
}

async function output() {
    let out = await sleep(1000);
    console.log(1);
    return out;
}

output();
  • 使用生成器
function sleepGenerator(time) {
    yield new Promise(function (resolve, reject) {
        setTimeout(resolve, time);
    })
}

sleepGenerator(1000).next().value.then(() => { console.log(1) }) 

自动重试

实现一个函数, fetchWithRetry 会自动重试3次,任意一次成功直接返回

const fetchWithRetry = async (
    url,
    options,
    retryCount = 0,
) => {
    const { MAXRET = 3, ...remainingOptions } = options;
    try {
        return await fetch(url, remainingOptions);
    } catch (error) {
        // 如果重试次数没有超过,那么重新调用
        if (retryCount < maxRetries) {
            return fetchWithRetry(url, options, retryCount + 1);
        }
        // 超过最大重试次数
        throw error;
    }
}

// 补充超时和取消
// 创建一个 reject 的 promise 
// `timeout` 毫秒
const throwOnTimeout = (timeout) =>
    new Promise((_, reject) =>
        setTimeout(() =>
            reject(new Error("Timeout")),
            timeout
        ),
    );

const fetchWithTimeout = (
    url,
    options = {},
) => {
    const { timeout, ...remainingOptions } = options;
    // 如果超时选项被指定,那么 fetch 调用和超时通过 Promise.race 竞争
    if (timeout) {
        return Promise.race([
            fetch(url, remainingOptions),
            throwOnTimeout(timeout),
        ]);
    }
    return fetch(url, remainingOptions);
}

// 取消
const fetchWithCancel = (url, options = {}) => {
    const controller = new AbortController();
    const call = fetch(
        url,
        { ...options, signal: controller.signal },
    );
    const cancel = () => controller.abort();
    return [call, cancel];
};

算法篇

@树的操作

树的深度优先遍历

树的深度优先遍历有三种方式:前序遍历、中序遍历和后序遍历。其中,前序遍历的遍历顺序是先遍历根节点,然后遍历左子树,最后遍历右子树;中序遍历的遍历顺序是先遍历左子树,然后遍历根节点,最后遍历右子树;后序遍历的遍历顺序是先遍历左子树,然后遍历右子树,最后遍历根节点。以下是JS实现树的深度优先遍历的代码,附有详细的注释说明:

// 定义树节点类
class TreeNode {
    constructor(val) {
        this.val = val;
        this.left = null;
        this.right = null;
    }
}

// 构建二叉树
const root = new TreeNode(1);
root.left = new TreeNode(2);
root.right = new TreeNode(3);
root.left.left = new TreeNode(4);
root.left.right = new TreeNode(5);
root.right.left = new TreeNode(6);
root.right.right = new TreeNode(7);

// 前序遍历
function preorderTraversal(root, result = []) {
    if (root) {
        result.push(root.val); // 访问根节点
        preorderTraversal(root.left, result); // 遍历左子树
        preorderTraversal(root.right, result); // 遍历右子树
    }
    return result;
}

// 中序遍历
function inorderTraversal(root, result = []) {
    if (root) {
        inorderTraversal(root.left, result); // 遍历左子树
        result.push(root.val); // 访问根节点
        inorderTraversal(root.right, result); // 遍历右子树
    }
    return result;
}

// 后序遍历
function postorderTraversal(root, result = []) {
    if (root) {
        postorderTraversal(root.left, result); // 遍历左子树
        postorderTraversal(root.right, result); // 遍历右子树
        result.push(root.val); // 访问根节点
    }
    return result;
}

// 测试
console.log(preorderTraversal(root)); // [1, 2, 4, 5, 3, 6, 7]
console.log(inorderTraversal(root)); // [4, 2, 5, 1, 6, 3, 7]
console.log(postorderTraversal(root)); // [4, 5, 2, 6, 7, 3, 1]

树的广度优先遍历

树的广度优先遍历是按照层次顺序遍历树的节点,从根节点开始,逐层遍历直到最后一层。在遍历每一层时,按照从左到右的顺序访问节点。以下是JS实现树的广度优先遍历的代码,附有详细的注释说明:

// 定义树节点类
class TreeNode {
    constructor(val) {
        this.val = val;
        this.left = null;
        this.right = null;
    }
}

// 构建二叉树
const root = new TreeNode(1);
root.left = new TreeNode(2);
root.right = new TreeNode(3);
root.left.left = new TreeNode(4);
root.left.right = new TreeNode(5);
root.right.left = new TreeNode(6);
root.right.right = new TreeNode(7);

// 广度优先遍历
function bfsTraversal(root) {
    const queue = [root]; // 定义队列,初始值为根节点
    const result = []; // 定义遍历结果数组
    while (queue.length) { // 当队列不为空时
        const node = queue.shift(); // 取出队首节点
        result.push(node.val); // 访问队首节点
        if (node.left) { // 如果队首节点有左子节点,则将左子节点加入队列
            queue.push(node.left);
        }
        if (node.right) { // 如果队首节点有右子节点,则将右子节点加入队列
            queue.push(node.right);
        }
    }
    return result;
}

// 测试
console.log(bfsTraversal(root)); // [1, 2, 3, 4, 5, 6, 7]

数组转树结构

const arr = [{
        id: 2,
        name: '部门B',
        parentId: 0
    },
    {
        id: 3,
        name: '部门C',
        parentId: 1
    },
    {
        id: 1,
        name: '部门A',
        parentId: 2
    },
    {
        id: 4,
        name: '部门D',
        parentId: 1
    },
    {
        id: 5,
        name: '部门E',
        parentId: 2
    },
    {
        id: 6,
        name: '部门F',
        parentId: 3
    },
    {
        id: 7,
        name: '部门G',
        parentId: 2
    },
    {
        id: 8,
        name: '部门H',
        parentId: 4
    }
];

const transTree = (list, pId) => {
  const loop = (pId) => {
    let res = [];
    let i = 0;
    while (i < list.length) {
      let item = list[i];
      i++;
      if (item.pid !== pId) continue
      item.children = loop(item.id);
      res.push(item);
    }
    return res;
  }
  return loop(pId);
}

const transTree = (list, pId) => {
  const loop = (pId) => {
    return list.reduce((pre, cur) => {
      if(cur.pid === pId) {
        cur.children = loop(cur.id);
        pre.push(cur);
      };
      return pre;
    },[])
  }
  return loop(pId);
}

树的层序遍历

二叉树层序遍历, 每层的节点放到一个数组里

// 给定一个二叉树,返回该二叉树层序遍历的结果,(从左到右,一层一层地遍历)例如:
// 给定的二叉树是{ 3, 9, 20,#,#, 15, 7 },
// 该二叉树层序遍历的结果是[[3], [9, 20], [15, 7]]

var levelOrder = function (root) {
    let res = [];
    if (root === null) return res;
    let list = [];
    list.push(root);
    while (list.length) {
        let curLen = list.length;//上一轮剩下的节点,全属于下一层
        let newLevel = [];
        for (let i = 0; i < curLen; i++) {//同层所有节点
            let node = list.shift();//出列
            newLevel.push(node.val);//push进newLevel数组
            //左右子节点push进队列
            if (node.left) list.push(node.left);
            if (node.right) list.push(node.right);
        }
        res.push(newLevel);//push到res数组
    };
    return res;
};

console.log(levelOrder(res));

光照树问题

二叉树光照,输出能被光照到的节点, dfs能否解决

// 输入: [1, 2, 3, null, 5, null, 4]
// 输出: [1, 3, 4]

/**
 * @param {TreeNode} root
 * @return {number[]}
 */
function exposedElement(root) {
    // 实现之
};
// 将list转化为树结构
class TreeNode {
    constructor(val, left, right) {
        this.val = (val === undefined ? 0 : val);
        this.left = (left === undefined ? 0 : left);
        this.right = (right === undefined ? 0 : right);
    }
}

function buildTree(arr) {
    if (!arr || arr.length === 0) {
        return null;
    }
    let root = new TreeNode(arr.shift());

    let nodeQueue = [root];

    while (arr.length > 0) {
        let node = nodeQueue.shift();
        if (!arr.length) {
            break;
        }
        let left = new TreeNode(arr.shift());
        node.left = left;
        nodeQueue.push(left);

        if (!arr.length) {
            break;
        }

        // 左侧叶子拼完,右边一样的操作
        let right = new TreeNode(arr.shift());
        node.right = right;
        nodeQueue.push(right);
    }

    // 最后返回根结点,通过根结点就能得到整个二叉树的结构
    return root;
}

const rightSideView = (root) => {
    const res = [];
    const dfs = (node, level) => {
        if (!node) return;
        res[level] = node.val;
        dfs(node.left, level + 1);
        dfs(node.right, level + 1);
    }
    dfs(root, 0);
    return res;
}

统计层节点之和

多叉树, 获取每一层的节点之和

const res = {
    value: 2,
    children: [
        { value: 6, children: [{ value: 1 }] },
        { value: 3, children: [{ value: 2 }, { value: 3 }, { value: 4 }] },
        { value: 5, children: [{ value: 7 }, { value: 8 }] }
    ]
};

const layerSum = function (root) {
    let result = [], index = 0;
    const level = (root, index) => {
        if (!root) return;
        if (!result[index]) result[index] = 0;
        result[index] += root.value;
        if (root.children) root.children.forEach(child => level(child, index + 1))
    };
    level(root, index);
    return result;
};

console.log(levelOrder(res));

@链表操作

删除链表的一个节点

/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */

/**
 * @param {ListNode} head
 * @param {number} val
 * @return {ListNode}
 */
var deleteNode = function (head, val) {
    // 定义虚拟节点
    const res = new ListNode(-1);
    // 虚拟节点连接到head
    res.next = head;
    // 定义p指针,最开始指向虚拟节点
    let p = res;
    // 从虚拟节点开始遍历链表
    while (p?.next) {
        // 如果下一个值等于val,则删除下一个值
        if (p.next.val === val)
            p.next = p.next.next;
        p = p.next;
    }
    return res.next;
};

链表中环的入口节点

/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */

/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var detectCycle = function (head) {
    var hashSet = new Set()
    while (head) {
        if (hashSet.has(head)) {
            return head
        }
        hashSet.add(head)
        head = head.next
    }
    return null
};

// 快慢指针
var detectCycle = function (head) {
    if (!head) return null;
    let fast = head, slow = head;
    while (fast != null && fast.next != null) {
        fast = fast.next.next;
        slow = slow.next;
        if (fast === slow) {
            fast = head;
            while (fast !== slow) {
                fast = fast.next;
                slow = slow.next;
            }
            return slow;
        }
    }
    return null;
};

@经典算法

冒泡排序

/**
 * 冒泡排序number数组(升序)
 * @param { number[] } arr
 */
function bubbleSort(arr) {
  console.log("bubbleSort", arr);
  var temp;

  for (var n = 1; n < arr.length; n++) {
    // 从01比干到89比
    for (var i = 0; i < arr.length - n; i++) {
      if (arr[i] > arr[i + 1]) {
        temp = arr[i];
        arr[i] = arr[i + 1];
        arr[i + 1] = temp;
      }
    }
  }

  console.log(arr);
}

选择排序

/**
 * 选择排序算法对number数组升序排序
 * @param {Array} arr
 */
function selectSort(arr) {
  console.log("selectSort", arr);

  /* 选择排序 */
  // 用于互换位置的暂存箱
  var temp;

  /* 依次锁定/选定【0~倒数第二把】交椅 */
  for (var i = 0; i < arr.length - 1; i++) {
    /* 将[i,末尾]区间内最小的元素找出来 跟i互换位置 */

    // 先假定i号位最小
    var minValue = arr[i];
    var minIndex = i;

    // 遍历[i+1,末尾]的所有元素
    for (var j = i + 1; j < arr.length; j++) {
      if (arr[j] < minValue) {
        minValue = arr[j];
        minIndex = j;
      }
    }

    // 最小元素跟i互换位置
    temp = arr[i];
    arr[i] = minValue;
    arr[minIndex] = temp;
  }
  console.log(arr);
}

插入排序

插入排序是一种简单直观的排序算法,其基本思想是将一个记录插入到已经排好序的有序表中,从而得到一个新的有序表。以下是JS实现插入排序的代码,带有详细的注释说明:

function insertionSort(arr) {
  // 遍历数组,从第二个元素开始插入排序
  for (let i = 1; i < arr.length; i++) {
    // 将当前元素保存到临时变量temp中
    let temp = arr[i];
    let j = i - 1;
    // 在已经排好序的元素中查找插入位置
    while (j >= 0 && arr[j] > temp) {
      // 如果当前元素比插入元素大,就将当前元素后移一位
      arr[j + 1] = arr[j];
      j--;
    }
    // 将插入元素放入正确的位置
    arr[j + 1] = temp;
  }
  // 返回排好序的数组
  return arr;
}

// 测试
const arr = [5, 3, 8, 4, 2];
console.log(insertionSort(arr)); // [2, 3, 4, 5, 8]
  • 在上面的代码中,我们使用for循环遍历数组,从第二个元素开始进行插入排序。
  • 对于每个元素,我们使用while循环在已经排好序的元素中查找插入位置,如果当前元素比插入元素大,就将当前元素后移一位,直到找到正确的位置。
  • 最后,将插入元素放入正确的位置,完成一次插入排序。
  • 重复这个过程,直到所有元素都排好序。

快速排序

快速排序是一种高效的排序算法,其基本思想是选择一个基准元素,将数组分成两个部分,小于基准元素的放在左边,大于基准元素的放在右边,然后对左右两个部分分别进行递归排序。以下是JS实现快速排序的代码,附有详细的注释说明:

function quickSort(arr) {
    // 如果数组长度小于等于1,就直接返回
    if (arr.length <= 1) {
        return arr;
    }

    // 选择一个基准元素
    const pivot = arr[0];

    // 定义左右两个数组
    const left = [];
    const right = [];

    // 将数组分成两个部分
    for (let i = 1; i < arr.length; i++) {
        if (arr[i] < pivot) {
            left.push(arr[i]);
        } else {
            right.push(arr[i]);
        }
    }
    
    // 分别对左右两个部分进行递归排序
    return quickSort(left).concat([pivot], quickSort(right));
}

// 测试
const arr = [5, 3, 8, 4, 2];
console.log(quickSort(arr)); // [2, 3, 4, 5, 8]

二分查找

二分查找是一种高效的查找算法,它的基本思想是将有序数组分成两半,然后取中间元素进行比较,如果相等就返回,如果小于中间元素就在左半边继续查找,如果大于中间元素就在右半边继续查找。以下是JS实现二分查找的代码,附有详细的注释说明:

function binarySearch(arr, target) {
    let left = 0; // 左边界
    let right = arr.length - 1; // 右边界
    while (left <= right) {
        const mid = Math.floor((left + right) / 2); // 中间位置
        if (arr[mid] === target) {
            return mid; // 找到目标元素,返回下标
        } else if (arr[mid] < target) {
            left = mid + 1; // 目标元素在右半边,更新左边界
        } else {
            right = mid - 1; // 目标元素在左半边,更新右边界
        }
    }
    return -1; // 没有找到目标元素,返回-1
}

// 测试
const arr = [1, 2, 3, 4, 5, 6];
console.log(binarySearch(arr, 3)); // 2
console.log(binarySearch(arr, 7)); // -1

  • 在上面的代码中,我们使用while循环来进行二分查找。
  • 对于一个有序数组,我们首先获取左右边界,然后计算中间位置,比较中间元素和目标元素的大小关系,如果相等就返回下标,如果小于中间元素就在右半边继续查找,如果大于中间元素就在左半边继续查找。
  • 重复这个过程,直到找到目标元素或者左右边界重合。
  • 如果没有找到目标元素,就返回-1。

实现LRU算法

  • 缓存淘汰算法:内存容量是有限的,当你要缓存的数据超出容量,就得有部分数据删除,这时候哪些数据删除,哪些数据保留,就是LRU算法和LFU算法,FU强调的是访问次数,而LRU强调的是访问时间。
  • 选择内存中最近最久未使用的页面予以淘汰,如果我们想要实现缓存机制 – 满足最近最少使用淘汰原则,我们就可以使用LRU算法缓存机制。如:vue 中 keep-alive 中就用到了此算法。
  • LRU:即Least Recently Used(最近最少使用算法)。把长期不使用的数据被认定为无用数据,在缓存容量满了后,会优先删除这些被认定的数据。
  • keep-alive内部即使用LRU算法
var LRUCache = function (capacity) {
    this.cache = new Map();
    this.capacity = capacity;
};

LRUCache.prototype.get = function (key) {
    if (this.cache.has(key)) {
        let temp = this.cache.get(key);
        this.cache.delete(key);
        this.cache.set(key, temp);
        return temp;
    }
    return -1;
}

LRUCache.prototype.put = function (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);
}

设计篇

@设计模式

组合+观察者

使用观察者模式实现以下需求:放学铃响10分钟后,所有电灯熄灭+大门反锁+服务器备份数据

略,请参考以下案例自行实现 小米智能家居案例

@闭包 & 函数式编程

柯里化

手写多参柯里化高阶函数curry

function curry(fn){
    return function cfn(...args){
        // 如果参数给够 则直接调用fn求结果并返回
        // fn.length指的是fn定义的参数个数
        if(args.length === fn.length){
            return fn.apply(null,args)
        }

        /* 
        否则继续返回函数 以继续接收后续参数 
        利用外层函数缓存内层函数的所有参数
        */
        return function(...bs){
            // 外层闭包先将bs统计起来
            args = args.concat(...bs)

            /* 继续返回函数或结果 */
            return cfn(...args)
        }
    }
}

const add = (a,b,c,d) => a+b+c+d
const cadd = curry(add)
console.log(cadd(1,2,3,4))
console.log(cadd(1)(2,3)(4))

同步函数的Promise化

手写同步函数的Promise化高阶函数promisify

/* 同步函数的Promise化 */
function promisify(fn){
    return function pfn(...args){
        /* 有回调函数 */
        if(typeof args[args.length-1] === "function"){
            fn.apply(null,args)
            return
        }

        /* 无回调函数 */
        return new Promise(
            (resolve,reject)=>{
                try {
                    const result = fn.apply(null,args)
                    resolve(result)
                } catch (err) {
                    reject(err)
                }
            }
        )
        
    }
}

/* 代码规范:如果有回调函数,确保只有一个,且放在队伍末尾 */
const add = (a,b,callback)=>{
    const result = a + b
    if(!callback){
        return result
    }
    callback(result)
}

const padd = promisify(add)

padd(2,3)
.then(ret => console.log("then",ret))

padd(2,3,ret => console.log("callback",ret))

管道与组合

手写管道高阶函数pipe

const pipe = (...fns) => (initialValue) => fns.reduce(
    (pv,cv)=>cv(pv),
    initialValue
)

const len = (n) => (n + "").length; //求数值的长度
const pow = (n) => n * n; //求n的平方
const log = (n) => console.log(n); //打印n

/* 生成流水线函数:先长度=>对长度做平方=>打印平方值 */
const streamline = pipe(pow, len, log);

streamline(10); //3

手写组合高阶函数compose

// 不reverse的话也可以直接使用fns.reduceRight()
const compose =
  (...fns) =>
  (initialValue) =>
    fns.reverse().reduce((pv, cv) => cv(pv), initialValue);

const len = (n) => (n + "").length; //求数值的长度
const pow = (n) => n * n; //求n的平方
const cubicRoot = (n) => Math.cbrt(n);// 求立方根

console.log(compose(len, pow, cubicRoot)(1000));//3

手写防抖节流

手写防抖函数debounce

function debounce(fn, delay) {
  let timer = null;

  return function (...args) {
    if (timer) {
      clearTimeout(timer);
      timer = null
    }

    timer = setTimeout(() => {
      fn.apply(null, args);
      timer = null;
    }, delay);
  };
}

手写节流函数throttle

function throttle(fn, delay) {
  let lock = false;

  return function (...args) {
    if (!lock) {
      fn.apply(null, args);
      lock = true;

      /* 接下来的1秒内禁止点击回调触发 */
      setTimeout(() => {
        lock = false;
      }, delay);
    }
  };
}

场景篇

大文件上传

// 拆分的方法
function slice(file, piece = 1024 * 1024 * 5) {
    let totalSize = file.size; // 文件总大小

    let start = 0; // 每次上传的开始字节
    let end = start + piece; // 每次上传的结尾字节

    let chunks = []

    while (start < totalSize) {
        // 根据长度截取每次需要上传的数据
        // File对象继承自Blob对象,因此包含slice方法
        let blob = file.slice(start, end);
        chunks.push(blob)

        start = end;
        end = start + piece;
    }

    return chunks
}

// 举个栗子
// 获取context,同一个文件会返回相同的值
function createContext(file,chunks) {
    return file.name + file.length + "-" + chunks.length
}

/* 完成切片 */
let file = document.querySelector("[name=file]").files[0];
const LENGTH = 1024 * 1024 * 0.1;
let chunks = slice(file, LENGTH);

// 获取对于同一个文件,获取其的context
let context = createContext(file);


let tasks = [];
chunks.forEach((chunk, index) => {

    let fd = new FormData();
    fd.append("file", chunk);

    // 传递context
    fd.append("context", context);

    // 传递切片索引值
    fd.append("chunk", index + 1);

    // 上传单片
    tasks.push(post("/mkblk.php", fd));
});

/* 通知服务端组装 */
function notifyUploadCompleted(context) {
    /* 所有切片都上传成功后通知服务端组装 */
    let fd = new FormData();
    fd.append("context", context);
    // fd.append("chunks", chunks.length);
    post("/mkfile.php", fd).then(res => {
        console.log(res);
    });
}

// 所有切片上传完毕后,调用mkfile接口
function upload(tasks) {
    const rejectedTasks = []

    Promise.allSettled(tasks).then(rets => {
        rets.forEach((ret, i) => {
            if (ret.status === "rejected") {
                rejectedTasks.push(tasks[i])
            }
        })

        if (!rejectedTasks.length) {
            upload(rejectedTasks)
        } else {
            notifyUploadCompleted(context)
        }

    });
}

虚拟列表

虚拟列表是一种优化长列表渲染性能的技术,它的基本思想是只渲染可见区域内的部分列表项,而不是渲染整个列表。在可见区域之外的列表项可以通过滚动来进行懒加载,从而减少渲染开销。以下是JS实现虚拟列表的代码,附有详细的注释说明:

// 定义虚拟列表组件
class VirtualList {
  constructor(container, total, itemHeight) {
    this.container = container; // 列表容器
    this.total = total; // 列表项总数
    this.itemHeight = itemHeight; // 列表项高度
    this.renderCount = Math.ceil(container.clientHeight / itemHeight) + 1; // 渲染列表项数量
    this.renderItems = []; // 已渲染的列表项
    this.lastScrollTop = 0; // 上次滚动位置
    this.render(); // 渲染列表
    this.bindEvent(); // 绑定滚动事件
  }

  // 渲染列表
  render() {
    const { container, total, itemHeight, renderCount } = this;
    const fragment = document.createDocumentFragment(); // 创建文档片段
    for (let i = 0; i < renderCount; i++) {
      const item = document.createElement('div'); // 创建列表项
      item.className = 'item';
      item.style.height = itemHeight + 'px';
      item.innerText = i < total ? i : ''; // 设置列表项内容
      fragment.appendChild(item);
      this.renderItems.push(item); // 将列表项添加到已渲染列表中
    }
    container.appendChild(fragment); // 将文档片段添加到容器中
  }

  // 绑定滚动事件
  bindEvent() {
    const { container } = this;
    container.addEventListener('scroll', this.handleScroll.bind(this));
  }

  // 处理滚动事件
  handleScroll() {
    const { container, total, itemHeight, renderCount, renderItems, lastScrollTop } = this;
    const scrollTop = container.scrollTop; // 获取当前滚动位置
    const direction = scrollTop > lastScrollTop ? 'down' : 'up'; // 判断滚动方向
    const offset = Math.floor(scrollTop / itemHeight); // 计算偏移量
    const start = direction === 'down' ? offset : Math.max(offset - renderCount + 1, 0); // 计算起始位置
    const end = start + renderCount - 1; // 计算结束位置
    if (start === 0) {
      container.scrollTop = 0; // 到达顶部,重置滚动位置
    } else if (end === total - 1) {
      container.scrollTop = container.scrollHeight - container.clientHeight; // 到达底部,重置滚动位置
    }
    for (let i = 0; i < renderCount; i++) {
      const item = renderItems[i];
      const index = start + i;
      if (index >= 0 && index < total) {
        item.innerText = index; // 更新列表项内容
      } else {
        item.innerText = ''; // 隐藏不可见的列表项
      }
    }
    this.lastScrollTop = scrollTop; // 更新上次滚动位置
  }
}

// 测试
const container = document.getElementById('container');
new VirtualList(container, 1000, 30);
  • 在上面的代码中,我们使用ES6的class语法定义了一个虚拟列表组件。
  • 在构造函数中,我们首先计算渲染列表项数量,然后创建文档片段和列表项,并将列表项添加到已渲染列表中。
  • 在绑定滚动事件时,我们使用bind方法将this绑定到当前实例上,以便在处理滚动事件时可以访问到实例的属性和方法。
  • 在处理滚动事件时,我们首先获取当前滚动位置和滚动方向,然后计算偏移量、起始位置和结束位置。
  • 根据起始位置和结束位置,我们更新已渲染的列表项的内容,将不可见的列表项隐藏起来。
  • 最后,我们更新上次滚动位置。

图片懒加载

图片懒加载是一种优化网页性能的技术,它可以减少页面的加载时间和带宽消耗。图片懒加载的原理是,当页面滚动到某个位置时,才加载该位置的图片。以下是JS实现图片懒加载的代码,附有详细的注释说明:

// 获取所有需要懒加载的图片元素
const lazyImages = document.querySelectorAll('.lazy');

// 定义一个变量,用于存储可视区域的高度
let viewHeight = window.innerHeight || document.documentElement.clientHeight;

// 定义一个函数,用于判断图片是否在可视区域内
function isInView(el) {
  const rect = el.getBoundingClientRect(); // 获取元素相对于视口的位置信息
  return rect.top >= 0 && rect.bottom <= viewHeight; // 判断元素是否在可视区域内
}

// 定义一个函数,用于加载图片
function loadImages() {
  lazyImages.forEach((img) => {
    if (isInView(img)) { // 如果图片在可视区域内
      img.src = img.dataset.src; // 加载图片
      img.classList.remove('lazy'); // 移除lazy类
    }
  });
}

// 初始化页面时加载可视区域内的图片
loadImages();

// 监听页面滚动事件,滚动时加载可视区域内的图片
window.addEventListener('scroll', () => {
  loadImages();
});

  • 在上面的代码中,我们首先获取所有需要懒加载的图片元素,并定义一个变量viewHeight,用于存储可视区域的高度。
  • 然后,我们定义一个函数isInView,用于判断图片是否在可视区域内。该函数首先使用getBoundingClientRect方法获取元素相对于视口的位置信息,然后判断元素是否在可视区域内。
  • 接下来,我们定义一个函数loadImages,用于加载图片。该函数遍历所有需要懒加载的图片元素,如果图片在可视区域内,则加载该图片,并移除lazy类。
  • 最后,我们在初始化页面时加载可视区域内的图片,并在页面滚动时监听滚动事件,加载可视区域内的图片。
  • 在HTML中,我们需要为需要懒加载的图片元素添加lazy类,并将图片的真实地址存储在data-src属性中,例如:
<img class="lazy" data-src="image.jpg" alt="image">

这样,当页面滚动到某个位置时,才会加载该位置的图片,从而减少页面的加载时间和带宽消耗。