Skip to content

JS手撕

输出题

异步

js
new Promise((resolve, reject) => {
  console.log(1);
  resolve(true);
  console.log(2);
  throw new Error('err');
  reject(false);
  console.log(3);
})
  .catch(ex => console.log(ex))
  .then(res => console.log(res));

打印:1, 2, true

js
//请写出输出内容
async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}
async function async2() {
	console.log('async2');
}

console.log('script start');

setTimeout(function() {
    console.log('setTimeout');
}, 0)

async1();

new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2');
});
console.log('script end');
bash
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout

this

js
var length = 10;
function fn() {
  return this.length + 1;
}
var obj = {
  length: 5,
  test1: function () {
    return fn();
  },
};
obj.test2 = fn;
//下面代码输出是什么
console.log(obj.test1());
console.log(fn());
console.log(obj.test2());

11,11,6

闭包

js
var a = 0;
var b = 0;
var c = 0;

function fn(a) {
  console.log(a++, c);
  function fn2(b) {
    console.log(a, b, c);
  }
  var c = 4;
  fn = fn2;
}

fn(1);
fn(2);

1,undefined

2,2,4

手写布局

三栏布局,右侧自适应

html
<!DOCTYPE html>
<html lang="en">
<head>
    <title>Flex 三栏布局</title>
    <style>
        body {
            margin: 0;
            padding: 0;
        }
        .container {
            display: flex;
            height: 100vh;
        }
        .left, .middle {
            width: 200px; /* 左侧和中间栏固定宽度 */
            background-color: lightblue;
            padding: 20px;
        }
        .right {
            flex-grow: 1; /* 右侧栏自适应宽度 */
            background-color: lightgreen;
            padding: 20px;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="left">左侧栏</div>
        <div class="middle">中间栏</div>
        <div class="right">右侧栏自适应宽度</div>
    </div>
</body>
</html>

过渡消失

js
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>点击左侧栏使其消失</title>
    <style>
        body {
            margin: 0;
            padding: 0;
        }
        .container {
            display: flex;
            height: 100vh;
        }
        .left {
            width: 200px;
            background-color: lightblue;
            padding: 20px;
            transition: width 0.5s ease, opacity 0.5s ease; /* 添加过渡效果 */
        }
        .middle, .right {
            flex: 1;
            background-color: lightgreen;
            padding: 20px;
        }
        .hidden {
            width: 0;
            opacity: 0; /* 渐变透明度 */
            padding: 0;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="left" id="left-bar">点击我隐藏左侧栏</div>
        <div class="middle" id="mid-bar">中间栏</div>
        <div class="right">右侧栏</div>
    </div>

    <script>
        // 获取左侧栏元素
        const leftBar = document.getElementById('left-bar');
        const midBar = document.getElementById('mid-bar');

        // 点击事件监听器
        leftBar.addEventListener('click', function() {
            leftBar.classList.add('hidden'); // 添加 'hidden' 类,触发过渡效果
        });

        // 点击中间栏恢复左侧
        midBar.addEventListener('click', ()=>{
          leftBar.classList.remove('hidden');
        })
    </script>
</body>
</html>

button 样式

要让一个 div 标签看起来像一个按钮,你可以使用 CSS 来添加样式。以下是一个简单的示例:

html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Div as Button</title>
    <style>
        .button {
            display: inline-block;
            padding: 10px 20px; /* 内边距 */
            background-color: #007BFF; /* 背景颜色 */
            color: white; /* 文字颜色 */
            border: none; /* 去掉边框 */
            border-radius: 5px; /* 圆角 */
            cursor: pointer; /* 鼠标指针变成手型 */
            text-align: center; /* 文字居中 */
            font-size: 16px; /* 字体大小 */
            transition: background-color 0.3s; /* 背景色过渡效果 */
        }

        .button:hover {
            background-color: #0056b3; /* 鼠标悬停时的背景颜色 */
        }

        .button:active {
            background-color: #004085; /* 点击时的背景颜色 */
        }
    </style>
</head>
<body>
    <div class="button" onclick="alert('Button clicked!')">Click Me</div>
</body>
</html>
  1. CSS 样式

    • display: inline-block;:使 div 像块级元素一样显示,同时可以设置宽高。
    • padding:增加内边距,让按钮看起来更大。
    • background-colorcolor:设置背景颜色和文字颜色。
    • borderborder-radius:去掉默认边框并添加圆角。
    • cursor: pointer;:鼠标悬停时显示手型指针。
    • transition:为背景颜色变化添加平滑过渡效果。
    • :hover:active 状态的样式:实现鼠标悬停和点击时的视觉效果。
  2. HTML 结构:一个 div 标签,带有 onclick 事件,可以在点击时触发 JavaScript 函数。

左中右布局

css
* {
    box-sizing: border-box;
}

body {
    margin: 0;
    font-family: Arial, sans-serif;
}

.container {
    display: flex;
    height: 100vh; /* 设置容器高度 */
}

.left-sidebar,
.right-sidebar {
    width: 250px; /* 侧栏宽度 */
    background-color: #f0f0f0;
    padding: 20px;
}

.main-content {
    flex: 1; /* 填充剩余空间 */
    background-color: #fff;
    padding: 20px;
}

闭包

函数柯里化

题目:

js
// 写一个函数,满足:
function add(a , b , c ){
	return a+b+c;
}
let add1 = curry(add, 1);
console.log(add1(2,3)); // 6

console.log(curry(add)(0)(5)(6)); // 11

实现:

js
function curry(func) {
  return function curried(...args) {
    // 如果已经收集到足够参数,直接调用原函数
    if (args.length >= func.length) {
      return func(...args);
    }
    // 否则返回一个新函数,继续收集参数
    return function (...newArgs) {
      return curried(...args, ...newArgs);
    };
  };
}

防抖与节流

在网络请求的场景中,通常推荐使用节流

防抖:n 秒后再执行某一事件,若在 n 秒内被再次触发,则重新计时

js
function debounce(func, wait) {
  let timeout;
  return function (...args) {
    let context = this;

    clearTimeout(timeout);
    timeout = setTimeout(() => {
      func.apply(context, args);
    }, wait);
  };
}

function print(event) {
  console.log('mouse move!', event.clientX, event.clientY);
}

const debouncedPrint = debounce(print, 1000);
document.addEventListener('mousemove', debouncedPrint);

节流:n 秒内只运行一次,若在 n 秒内重复触发,则只有一次生效

js
function throttle(func, wait) {
  let preTime = 0;
  return function (...args) {
    let now = Date.now();
    if (now - preTime > wait) {
      func.apply(this, args); // 确保正确的上下文和参数传递
      preTime = now;
    }
  };
}

function print() {
  console.log('mouse move!');
}

const throttledPrint = throttle(print, 1000);
document.addEventListener('mousemove', throttledPrint);

once 修饰器

js
function once(fn) {
  let called = false;
  let result;

  return function(...args) {
    if (!called) {
      called = true;
      result = fn.apply(this, args);
    }
    return result;
  };
}

// 示例用法
const sayHello = once(function(name) {
  console.log(`Hello, ${name}!`);
});

sayHello('Alice'); // 输出: Hello, Alice!
sayHello('Bob');   // 不会输出任何内容
  1. 闭包once 函数返回一个新函数,内部使用一个 called 标志来跟踪是否已经调用过。
  2. 调用控制:第一次调用时执行原始函数并记录结果;之后的调用会直接返回之前的结果。
  3. 参数处理:使用 ...args 来支持任意数量的参数传递给原始函数。

这个 once 修饰器能够有效地限制函数只被调用一次,后续的调用将不会产生任何效果。

时间间隔重复执行函数

js
const repeat = function (func, times, wait) {
  return (theStr) => {
    for (let i = 1; i <= times; i++) {
      setTimeout(() => {
        func(theStr);
      }, i * wait);
    }
  };
};

function myPrint(theStr) {
  const timeStap = new Date();
  console.log(theStr + timeStap);
}

const repeatPrint = repeat(myPrint, 4, 3000);
repeatPrint('helloworld');

异步

创建 Promise

js
// 传入执行器函数,该函数的参数为 resolve 和 reject
const lotteryPromise = new Promise((resolve, reject) => { 
  if (Math.random() >= 0.5) {
    resolve('WIN! 🏆');
  } else {
    reject('LOSE...💩');
  }
});

实现 sleep(n)

js
function sleep(sec) {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log(`I sleep for ${sec} seconds💤`);
      resolve();
    }, sec * 1000);
  });
}

sleep(0.5).then(() => sleep(1));

手写 Promise.all()

js
Promise.all = function (promises) {
    let results = [];
    let length = promises.length;
    let promiseCount = 0;
    return new Promise((resolve, reject) => {
        for (let i = 0; i < promises.length; i++) {
            Promise.resolve(promises[i]).then(res => {
                results[i] = res;
                promiseCount++;
                if (promiseCount === length) {
                    resolve(results);
                }
            }, err => {
                reject(err);
            })
        }
    })
}

// example
let promise1 = Promise.resolve(1);
let promise2 = Promise.resolve(2);
let promise3 = Promise.resolve(3);

promise.all([promise1, promise2, promise3])
  .then(results => {
    console.log(results); // [1, 2, 3]
  })
  .catch(reason => {
    console.log(reason);
  });

复杂版本:

js
function customPromiseAll(promises) {
  return new Promise((resolve, reject) => {
    const results = [];
    let completedPromises = 0;

    // 如果传入的不是数组,抛出错误
    if (!Array.isArray(promises)) {
      return reject(new TypeError('Argument must be an array'));
    }

    // 如果传入的是空数组,立即resolve空数组
    if (promises.length === 0) {
      return resolve([]);
    }

    promises.forEach((promise, index) => {
      // 确保每个元素都是一个 Promise
      Promise.resolve(promise)
        .then(result => {
          results[index] = result;
          completedPromises++;

          // 当所有 Promise 都完成时,resolve 最终的结果数组
          if (completedPromises === promises.length) {
            resolve(results);
          }
        })
        .catch(error => {
          // 一旦有一个 Promise 被 reject,立即 reject 整个 customPromiseAll
          reject(error);
        });
    });
  });
}

异步任务控制并发

js
class AsyncPool {
  constructor(maxConcurrency) {
    this.maxConcurrency = maxConcurrency;  // 最大并发数
    this.currentCount = 0;                // 当前正在执行的任务数量
    this.taskQueue = [];                  // 任务队列
  }

  // 执行一个任务
  async run(task, ...args) {
    if (this.currentCount >= this.maxConcurrency) {
      // 如果当前执行任务数达到最大并发数,则等待
      await new Promise(resolve => this.taskQueue.push(resolve));
    }

    this.currentCount++;
    try {
      // 执行实际任务
      return await task(...args);
    } finally {
      this.currentCount--;
      if (this.taskQueue.length > 0) {
        // 如果队列中有等待的任务,释放一个
        const next = this.taskQueue.shift();
        next();
      }
    }
  }
}

fetch 超时处理

js
function fetchWithTimeout(url, options = {}, timeout = 5000) {
    return new Promise((resolve, reject) => {
        const timeoutId = setTimeout(() => {
            reject(new Error('请求超时'));
        }, timeout);

        fetch(url, options)
            .then(response => {
                clearTimeout(timeoutId); // 清除超时定时器
                if (!response.ok) {
                    reject(new Error('网络响应不符合规范'));
                } else {
                    resolve(response);
                }
            })
            .catch(err => {
                clearTimeout(timeoutId); // 清除超时定时器
                reject(err);
            });
    });
}

// 示例用法
fetchWithTimeout('https://jsonplaceholder.typicode.com/posts', {}, 3000)
    .then(response => response.json())
    .then(data => console.log(data))
    .catch(error => console.error('错误:', error));

hardMan

js
function HardMan(name) {
  this.name = name;
  this.taskQueue = [];

  this.study = function () {
    this.taskQueue.push(() => {
      console.log('I am studying.');
    });
    return this;
  };
  this.rest = function (sec) {
    this.taskQueue.push(async () => {
      await wait(sec);
      console.log(`rest for ${sec} seconds`);
    });
    console.log(this);
    return this;
  };
  function wait(sec) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve();
      }, sec * 1000);
    });
  }
  Promise.resolve().then(() => {
    let sequence = Promise.resolve();
    console.log(this);
    this.taskQueue.forEach((item) => {
      sequence = sequence.then(item);
    });
  });
}
// 测试代码
let jiaqi = new HardMan('jiaqi');
jiaqi.study().rest(3).study().rest(2);

原型链

判断是否有重复元素

js
Array.prototype.hasDuplicates = function () {
  // this 指向条用这个方法的数组
  let theSet = new Set();
  for(let element of this){ // 不能用 forEach,因为 forEach 不受 return 影响
    if (theSet.has(element)) {
      return true;
    } else {
      theSet.add(element);
    }
  }
  return false;
};

console.log([1, 2, 2].hasDuplicates()); // true

用 function 实现 class

js
// 定义构造函数
function Person(name, age) {
  this.name = name;
  this.age = age;
}

// 添加方法到原型
Person.prototype.greet = function() {
  console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
};

// 创建实例
const person1 = new Person('Alice', 30);
const person2 = new Person('Bob', 25);

// 调用方法
person1.greet(); // 输出: Hello, my name is Alice and I am 30 years old.
person2.greet(); // 输出: Hello, my name is Bob and I am 25 years old.

实现常用函数

flat 拍平数组

js
let nums = [1, 2, [3, 4, [6, 7]], 5];

function myFlat(arr, depth = 1) {
  // 递归结束条件
  if (depth < 1) {
    return arr;
  }
  let res = [];
  arr.forEach(item => {
    if (Array.isArray(item)) {
      res = res.concat(myFlat(item, depth - 1)); 
    } else {
      res.push(item);
    }
  });
  return res;
}

console.log(myFlat(nums, 2)); // [1, 2, 3, 4, 6, 7, 5]

解析 URL

js
let url = "http://www.xxx.com?a=1&b=2&c=3"
js
function parse(url) {
  let obj = {};
  url
    .slice(url.indexOf("?") + 1)
    .split("&")
    .map(item => {
      let [key, val] = item.split("=");
      obj[key] = val;
    });
  return obj;
}

let result = parse(url);
console.log(result);
// { a: '1', b: '2', c: '3' }

手写深拷贝

WeakMap 起到了检测和处理循环引用的关键作用,防止无限递归:

  1. WeakMap 是 JavaScript 中的一种集合类型,键必须是对象,值可以是任意类型。
  2. WeakMap 的键是弱引用,不会阻止键被垃圾回收。WeakMap 的键是弱引用,如果键的对象被垃圾回收机制回收了,该键值对会自动删除。
  3. 没有遍历方法,例如 keys 或 values,这是为了确保弱引用的安全性。

其中映射关系为:

原对象 -> 拷贝的对象
js
function deepClone(srcObj, seen = new WeakMap()) {
  if (srcObj === null || typeof srcObj !== 'object') {
    return srcObj;
  }

  // 检查 weakmap 中是否存在
  if (seen.has(srcObj)) {
    return seen.get(srcObj); // 如果存在,直接返回已拷贝的对象
  }

  const copiedObj = Array.isArray(srcObj) ? [] : {};
  // 将当前对象存储到 weakmap 中
  seen.set(srcObj, copiedObj);
  
  for (let key in srcObj) {
    copiedObj[key] = deepClone(srcObj[key], seen); // 递归拷贝
  }
  return copiedObj;
}

手写 Math.pow()

js
function customPow(base, exponent) {
    let result = 1;
    if (exponent === 0) {
        return result;
    }
    const positiveExponent = Math.abs(exponent);
    for (let i = 0; i < positiveExponent; i++) {
        result *= base;
    }
    return exponent < 0 ? 1 / result : result;
}

优化:快速幂算法,指数分治法

js
function customPow(base, exponent) {
  if (exponent === 0) return 1; // 任何数的 0 次方是 1
  if (exponent < 0) return 1 / customPow(base, -exponent); // 处理负指数

  // 快速幂算法
  let half = customPow(base, Math.floor(exponent / 2));
  if (exponent % 2 === 0) {
      return half * half; // 如果指数是偶数
  } else {
      return half * half * base; // 如果指数是奇数
  }
}

手写 instanceOf

js
function myInstanceof(left, right) {
    // 获取右侧构造函数的原型对象
    let prototype = right.prototype;
    // 获取左侧对象的原型
    left = left.__proto__;
    
    // 不断沿着原型链向上查找
    while (true) {
        // 到达原型链的顶端,仍未找到
        if (left === null) return false;
        // 找到相同的原型对象
        if (left === prototype) return true;
        // 继续向上查找
        left = left.__proto__;
    }
}

// 示例
console.log(myInstanceof([], Array)); // true
console.log(myInstanceof({}, Array)); // false

实现 myBind

可以通过实现一个 myBind 方法来模拟 JavaScript 的 Function.prototype.bind 方法。以下是一个简单的实现:

javascript
Function.prototype.myBind = function(context, ...args) {
    const fn = this; // 保存原函数

    return function(...newArgs) {
        // 使用 apply 调用原函数
        return fn.apply(context, [...args, ...newArgs]);
    };
};

// 示例
function greet(greeting, punctuation) {
    console.log(`${greeting}, ${this.name}${punctuation}`);
}

const person = { name: 'Alice' };
const greetAlice = greet.myBind(person, 'Hello');

greetAlice('!'); // 输出: Hello, Alice!
  1. 保存原函数:将 this 保存到 fn 中。
  2. 返回新函数:返回一个新函数,该函数可以接收额外的参数。
  3. 调用原函数:在新函数中,使用 apply 方法调用原函数,将 context 和合并后的参数传递给它。

这样,你就可以实现一个基本的 myBind 方法,模拟 bind 的行为。

手写 myReduce

js
Array.prototype.myReduce = function (callback, initialValue) {
  // 检查调用者是否为数组
  if (!Array.isArray(this)) {
    throw new TypeError(`${this} is not an array`);
  }
  // 检查回调函数是否是一个函数
  if (typeof callback !== 'function') {
    throw new TypeError(`${callback} is not a function`);
  }

  const array = this; // 当前数组
  let accumulator = initialValue; // 累积值
  let startIndex = 0; // 初始索引

  // 如果未提供初始值
  if (accumulator === undefined) {
    if (array.length === 0) {
      throw new TypeError('Reduce of empty array with no initial value');
    }
    // 使用数组的第一个值作为累积器初始值
    accumulator = array[0];
    startIndex = 1; // 从第二个元素开始遍历
  }

  // 遍历数组
  for (let i = startIndex; i < array.length; i++) {
    if (i in array) { // 跳过稀疏数组中的空值
      accumulator = callback(accumulator, array[i], i, array); // 调用回调函数
    }
  }

  return accumulator; // 返回最终结果
};
  • callbackFn:必需,一个回调函数,为数组中每个元素执行的函数。其返回值将作为下一次调用 callbackFn 时的 accumulator 参数。对于最后一次调用,返回值将作为 reduce() 的返回值。该函数被调用时将传入以下参数:
js
function callback(accumulator, currentValue, index, array) {
  // 逻辑
}
  1. accumulator: 上一次调用 callbackFn 的结果。在第一次调用时,如果指定了 initialValue 则为指定的值,否则为 array[0] 的值。
  2. currentValue: 当前元素的值。在第一次调用时,如果指定了 initialValue,则为 array[0] 的值,否则为 array[1]
  3. index: 在数组中的索引位置。在第一次调用时,如果指定了 initialValue 则为 0,否则为 1
  4. array: 数组本身。
  • initialValue: 可选,第一次调用回调时初始化 accumulator 的值。如果指定了 initialValue,则 callbackFn 从数组中的第一个值作为 currentValue 开始执行。如果没有指定 initialValue,则 accumulator 初始化为数组中的第一个值,并且 callbackFn 从数组中的第二个值作为 currentValue 开始执行。在这种情况下,如果数组为空(没有第一个值可以作为 accumulator 返回),则会抛出错误。

listToTree

js
function listToTree(list) {
    const map = {};
    const tree = [];

    // 将每个节点存入映射表
    list.forEach(item => {
        map[item.id] = { ...item, children: [] }; // 添加 children 属性
    });

    // 构建树形结构
    list.forEach(item => {
        if (item.parentId === null) {
            tree.push(map[item.id]); // 根节点
        } else {
            if (map[item.parentId]) {
                map[item.parentId].children.push(map[item.id]); // 添加子节点
            }
        }
    });

    return tree;
}

// 示例
const list = [
    { id: 1, name: 'Node 1', parentId: null },
    { id: 2, name: 'Node 2', parentId: 1 },
    { id: 3, name: 'Node 3', parentId: 1 },
    { id: 4, name: 'Node 4', parentId: 2 },
    { id: 5, name: 'Node 5', parentId: null }
];

const tree = listToTree(list);
console.log(JSON.stringify(tree, null, 2));

数组去重

一维数组:

js
let a = [1,2,2,3];
a = [...new Set(a)];
console.log(a); // [1,2,3]

二维数组:

js
let aa = [[1,2], [2,3], [1,2]];
let obj = {};
aa.forEach((item)=> obj[item]=item);
aa = Object.values(obj);
console.log(aa);

反转键值

js
function inverse(obj){
  var retobj = {};
  for(var key in obj){
    retobj[obj[key]] = key;
  }
  return retobj;
}

去除数组前0与后0

去除前侧 0 :

js
const height = [0, 0, 3, 9, 0];
const result = height.slice(height.findIndex((num) => num !== 0));
console.log(result); // [3, 9, 0]

去除后侧 0:

js
const height = [0, 0, 3, 9, 0];
const lastNonZeroIndex = height.lastIndexOf(
  [...height].reverse().find((num) => num !== 0)
);
const result = height.slice(0, lastNonZeroIndex + 1);

console.log(result); // [0, 0, 3, 9]

设计模式

发布订阅模式

js
class Event {
	constructor() {
		// 存储事件的数据对象
		// 为了迅速查找,使用字典
		this.events = {};
	}
	// 绑定事件
	on(eventName, callBack) {
		if (!this.events[eventName]) {
			this.events[eventName] = [];
		}
		this.events[eventName].push(callBack);
	}

	// 派发事件:遍历事件列表,对每个事件执行相应的回调函数
	emit(eventName, ...args) {
		if (this.events[eventName]) {
			this.events[eventName].forEach((callBack) => callBack(...args)); 
		}
	}

	// 取消订阅事件,从事件列表中删除指定的回调函数
	off(eventName, callBack) {
		if (this.events[eventName]) {
			if (callBack) {
				// 如果指定了回调函数,就删除该函数
				this.events[eventName] = this.events[eventName].filter(
					(cb) => cb != callBack
				);
			} else {
				// 如果没指定回调函数,就删除所有绑定的函数
				this.events[eventName] = [];
			}
		}
	}

	// 为事件绑定一个只执行一次的回调函数
	once(eventName, callBack) {
		// 将回调函数与取消订阅事件封装起来,成为一个新的函数
		const func = () => {
			callBack();
			this.off(eventName, func);// 注意这里传入的参数是新定义的 func 而不是 callBack
		};
		this.on(eventName, func);
	}
}

function test() {
	console.log("in test");
}

const test2 = function () {
	console.log("in test2");
};

myEvent = new Event();

myEvent.on("test", test);
myEvent.once("test2", test2);
myEvent.emit("test2");

Object.defineProperty 实现 Observer

js
Object.defineProperty(obj, 'count', decrip);
js
const obj = {
  a: 1,
  b: 2,
  c: {
    c1: 1,
    c2: 2,
  },
};

function _isObject(v) {
  return typeof v === 'object' && v !== null;
}
// 由于是对属性的监听,所以必须深度遍历所有属性。
function observe(obj) {
  for (let k in obj) {
    let v = obj[k];
    if (_isObject(v)) {
      observe(v);
    }
    Object.defineProperty(obj, k, {
      get() {
        console.log(k, '读取');
        return v;
      },
      set(val) {
        if (val !== v) {
          console.log(k, '更改');
          v = val;
        }
      },
    });
  }
}

observe(obj);

obj.c.c1;

可以看出,Object.defineProperty 通过递归能够实现深层次数据的监听,但是无法实现对于新增的属性的监听。

Proxy 实现 Observer

js
new Proxy(obj, handler);
js
const obj = {
  a: 1,
  b: 2,
  c: {
    c1: 1,
    c2: 2,
  },
};

function _isObject(v) {
  return typeof v === 'object' && v !== null;
}

function observe(obj) {
  const proxy = new Proxy(obj, {
    get(target, k) {
      let v = target[k];
      if (_isObject(v)) {
        v = observe(v);
      }
      console.log(k, '读取');
      return v;
    },
    set(target, k, val) {
      let v = target[k];
      if (val !== v) {
        target[k] = val;
        console.log(k, '更改');
      }
    },
  });
  return proxy;
}

const proxy = observe(obj);
proxy.c.c1;

其他

LRU

js
class Node {
    constructor(key = 0, value = 0) {
        this.key = key;
        this.value = value;
        this.prev = null;
        this.next = null;
    }
}
class LRUCache {
    constructor(capacity) {
        this.capacity = capacity;
        this.dummy = new Node();
        this.dummy.prev = this.dummy;
        this.dummy.next = this.dummy;
        this.keyToNode = new Map(); // 映射 key 与 Node 的 map
    }
    // 如果已经有这个 key 则更新 value
    put(key, value) {
        let node = this.getNode(key);
        if (node) {
            // 如果key存在,更新value
            node.value = value;
            return;
        }
        // 如果 key 不存在,新建键值对
        let newNode = new Node(key, value);
        this.keyToNode.set(key, newNode);
        // 将新 node 放到链表头部
        this.moveToHead(newNode);
        // 当超过 capacity 的时候,去掉最后一个
        if (this.keyToNode.size > this.capacity) {
            const lastNode = this.dummy.prev;
            this.keyToNode.delete(lastNode.key);
            this.removeNode(lastNode);
        }
    }
    get(key) {
        const node = this.getNode(key);
        return node ? node.value : -1;
    }
    // 获取对应 key 的 node
    getNode(key) {
        if (!this.keyToNode.has(key)) {
            return null;
        }
        const node = this.keyToNode.get(key);
        this.removeNode(node);
        this.moveToHead(node);
        return node;
    }
    // 将一个节点放到链表头部
    moveToHead(node) {
        node.prev = this.dummy;
        node.next = this.dummy.next;
        node.prev.next = node;
        node.next.prev = node;
    }
    // 删除一个节点
    removeNode(node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }
}

事件捕获和事件冒泡

html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>事件冒泡与事件捕获</title>
    <style>
        .outer {
            width: 300px;
            height: 300px;
            background-color: lightblue;
            display: flex;
            justify-content: center;
            align-items: center;
        }

        .inner {
            width: 150px;
            height: 150px;
            background-color: lightcoral;
        }
    </style>
</head>
<body>

<div class="outer" id="outer">外层DIV
    <div class="inner" id="inner">内层DIV</div>
</div>

<script>
    // 事件捕获阶段
    document.getElementById('outer').addEventListener('click', function() {
        alert('外层DIV 捕获阶段');
    }, true); // 第三个参数 true 表示捕获阶段

    document.getElementById('inner').addEventListener('click', function() {
        alert('内层DIV 捕获阶段');
    }, true); // 第三个参数 true 表示捕获阶段

    // 事件冒泡阶段
    document.getElementById('outer').addEventListener('click', function() {
        alert('外层DIV 冒泡阶段');
    }, false); // 第三个参数 false 表示冒泡阶段

    document.getElementById('inner').addEventListener('click', function() {
        alert('内层DIV 冒泡阶段');
    }, false); // 第三个参数 false 表示冒泡阶段
</script>

</body>
</html>

图片懒加载

js
<!-- HTML -->
<img data-src="example.jpg" alt="Example Image" class="lazy">

<!-- JavaScript -->
<script>
  // 选择所有带 data-src 属性的图片
  const lazyImages = document.querySelectorAll('img.lazy');

  // 创建 Intersection Observer
  const observer = new IntersectionObserver((entries, observer) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        const img = entry.target;
        img.src = img.dataset.src; // 将 data-src 替换为 src
        img.classList.remove('lazy'); // 移除懒加载类
        observer.unobserve(img); // 停止观察当前图片
      }
    });
  });

  // 观察每个图片
  lazyImages.forEach(img => observer.observe(img));
</script>

滚动事件监听:

js
<!-- HTML -->
<img data-src="example.jpg" alt="Example Image" class="lazy">

<!-- JavaScript -->
<script>
  // 获取所有懒加载图片
  const lazyImages = document.querySelectorAll('img.lazy');

  // 检测图片是否进入可视区域
  function lazyLoad() {
    const windowHeight = window.innerHeight;

    lazyImages.forEach(img => {
      const rect = img.getBoundingClientRect();
      if (rect.top <= windowHeight && rect.bottom >= 0) {
        img.src = img.dataset.src; // 加载图片
        img.classList.remove('lazy'); // 移除懒加载类
      }
    });
  }

  // 绑定滚动事件
  window.addEventListener('scroll', lazyLoad);

  // 初始化检测
  lazyLoad();
</script>

创建带有过期时间的 localStorage

js
// 存储数据到 LocalStorage
function setItemWithExpiry(key, value, expiryInMinutes) {
  const now = new Date();
  const item = {
    value: value,
    expiry: now.getTime() + expiryInMinutes * 60 * 1000, // 设置过期时间
  };
  localStorage.setItem(key, JSON.stringify(item));
}

// 从 LocalStorage 获取数据
function getItemWithExpiry(key) {
  const itemStr = localStorage.getItem(key);
  
  // 如果不存在,返回 null
  if (!itemStr) {
    return null;
  }

  const item = JSON.parse(itemStr);
  const now = new Date();

  // 检查是否过期
  if (now.getTime() > item.expiry) {
    localStorage.removeItem(key); // 删除过期项
    return null; // 返回 null
  }
  
  return item.value; // 返回有效值
}

// 页面加载时检查过期数据
window.onload = function() {
  const value = getItemWithExpiry('myKey');
  if (value) {
    console.log('有效数据:', value);
  } else {
    console.log('数据已过期或不存在');
  }
};

// 示例使用
setItemWithExpiry('myKey', 'myValue', 1); // 设置1分钟后过期
  1. 存储数据:使用 setItemWithExpiry 存储数据和过期时间。
  2. 加载时检查:在 window.onload 事件中调用 getItemWithExpiry,当页面加载时检查是否有有效数据。
  3. 处理过期:如果数据已过期,则会从 LocalStorage 中删除并返回 null
js
// 获取 Cookie 值的函数
function getCookieValue(name) {
  const cookieText = document.cookie; // 获取所有 cookie
  const cookies = cookieText.split(';').map(cookie => cookie.trim()); // 按分号分隔并去除空格
  for (const cookie of cookies) {
    const [key, value] = cookie.split('=');
    if (key === name) {
      return decodeURIComponent(value); // 返回解码后的值
    }
  }
  return null;
}

根据虚拟 dom 生成真实 dom

js
const vdom = {
  tag: 'div',
  props: { id: 'app' },
  children: [
    {
      tag: 'h1',
      props: {},
      children: ['Hello, World!']
    },
    {
      tag: 'p',
      props: {},
      children: ['This is a simple example.']
    }
  ]
};
js
function createElement(vnode) {
  // 创建元素
  const element = document.createElement(vnode.tag);
  
  // 设置属性
  for (const key in vnode.props) {
    element.setAttribute(key, vnode.props[key]);
  }

  // 添加子节点
  vnode.children.forEach(child => {
    if (typeof child === 'string') {
      // 如果是文本,添加文本节点
      element.appendChild(document.createTextNode(child));
    } else {
      // 否则递归创建子元素
      element.appendChild(createElement(child));
    }
  });

  return element;
}

// 使用虚拟 DOM 创建真实 DOM
const realDOM = createElement(vdom);

// 将生成的真实 DOM 添加到页面中
document.body.appendChild(realDOM);

比较版本号

js
function compareVersion(version1, version2) {
  const v1Parts = version1.split('.');
  const v2Parts = version2.split('.');
  
  const maxLength = Math.max(v1Parts.length, v2Parts.length);
  
  for (let i = 0; i < maxLength; i++) {
    const v1 = parseInt(v1Parts[i]) || 0; // 使用 parseInt 转换,默认值为 0
    const v2 = parseInt(v2Parts[i]) || 0;
    
    if (v1 > v2) return 1; // version1 大于 version2
    if (v1 < v2) return -1; // version1 小于 version2
  }
  
  return 0; // 两个版本相等
}

// 示例用法
console.log(compareVersion('1.0.1', '1.0.0')); // 输出: 1
console.log(compareVersion('1.0.0', '1.0.1')); // 输出: -1
console.log(compareVersion('1.0', '1.0.0'));   // 输出: 0

数组转 dom 并响应式 render

要将一个数组变成 DOM 结构,并实现数组变化时对应的 DOM 也改变,可以使用 JavaScript 来操作 DOM。以下是一个简单的示例,演示如何实现这一点:

html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Array to DOM</title>
</head>
<body>
    <div id="app"></div>
    <button id="add">添加项</button>
    <button id="remove">移除项</button>

    <script>
        const app = document.getElementById('app');
        let dataArray = ['项 1', '项 2', '项 3'];

        function render() {
            app.innerHTML = ''; // 清空当前内容
            dataArray.forEach(item => {
                const div = document.createElement('div');
                div.textContent = item;
                app.appendChild(div);
            });
        }

        // 添加新项
        document.getElementById('add').addEventListener('click', () => {
            dataArray.push(`项 ${dataArray.length + 1}`);
            render();
        });

        // 移除最后一项
        document.getElementById('remove').addEventListener('click', () => {
            dataArray.pop();
            render();
        });

        render(); // 初始渲染
    </script>
</body>
</html>

代码说明:

  1. HTML 结构:包含一个 div 用于显示数组内容,以及两个按钮用于添加和移除数组项。
  2. dataArray:初始化一个数组。
  3. render 函数:清空当前 DOM 内容,并根据 dataArray 创建新的 DOM 元素,显示在页面上。
  4. 事件监听:添加和移除按钮的点击事件,通过修改 dataArray 并调用 render 函数来更新 DOM。

这样,当数组发生变化时,页面中的 DOM 结构也会相应更新。

vdom 和简单 diff 算法

实现一个简单的 Virtual DOM 和 diff 算法的基础版本可以帮助理解如何处理 UI 更新。以下是一个示例,包括 Virtual DOM 的创建和 diff 算法的实现。

Virtual DOM 实现

首先,定义一个简单的 Virtual DOM 结构和创建元素的函数:

javascript
function createElement(tag, props = {}, ...children) {
  return {
    tag,
    props,
    children: children.flat(), // 扁平化子节点
  };
}

Diff 算法实现

接下来,实现一个简单的 diff 函数,比较两个 Virtual DOM 节点并返回变化的部分:

javascript
function diff(oldNode, newNode) {
  const patches = [];

  function walk(oldNode, newNode, index) {
    if (!newNode) {
      patches.push({ type: 'REMOVE', index });
      return;
    }
    
    if (!oldNode) {
      patches.push({ type: 'ADD', newNode, index });
      return;
    }
    
    if (oldNode.tag !== newNode.tag) {
      patches.push({ type: 'REPLACE', newNode, index });
      return;
    }
    
    if (oldNode.tag) {
      const propsPatches = diffProps(oldNode.props, newNode.props);
      if (propsPatches) {
        patches.push({ type: 'UPDATE_PROPS', index, props: propsPatches });
      }
    }

    const maxLength = Math.max(oldNode.children.length, newNode.children.length);
    for (let i = 0; i < maxLength; i++) {
      walk(oldNode.children[i], newNode.children[i], i + 1);
    }
  }

  function diffProps(oldProps, newProps) {
    const patches = {};
    let hasChanges = false;
    
    for (const key in newProps) {
      if (oldProps[key] !== newProps[key]) {
        patches[key] = newProps[key];
        hasChanges = true;
      }
    }

    for (const key in oldProps) {
      if (!(key in newProps)) {
        patches[key] = undefined; // 标记删除
        hasChanges = true;
      }
    }

    return hasChanges ? patches : null;
  }

  walk(oldNode, newNode, 0);
  return patches;
}

应用示例

结合 Virtual DOM 创建和 diff 算法,我们可以测试功能:

javascript
const oldVDOM = createElement('div', { id: 'app' }, 
  createElement('h1', {}, 'Hello World'), 
  createElement('p', {}, 'This is a virtual DOM example.')
);

const newVDOM = createElement('div', { id: 'app' }, 
  createElement('h1', {}, 'Hello Virtual DOM'), 
  createElement('p', {}, 'This is an updated virtual DOM example.')
);

const patches = diff(oldVDOM, newVDOM);
console.log(patches);

输出解释

patches 数组将包含需要对 DOM 进行的所有操作,例如:

  • REMOVE:删除节点
  • ADD:添加节点
  • REPLACE:替换节点
  • UPDATE_PROPS:更新节点属性

判断矩形相交

要判断两个矩形是否相交,可以使用矩形的边界坐标来进行比较。以下是一个简单的实现方法:

矩形的表示

假设每个矩形用一个对象表示,包含左上角和右下角的坐标:

javascript
// 矩形格式: { x1, y1, x2, y2 }
const rect1 = { x1: 1, y1: 1, x2: 4, y2: 4 }; // 矩形1
const rect2 = { x1: 2, y1: 2, x2: 5, y2: 5 }; // 矩形2

判断相交的函数

可以使用以下函数来判断两个矩形是否相交:

javascript
function doRectanglesIntersect(rect1, rect2) {
  // 如果一个矩形在另一个矩形的左侧、右侧、上方或下方,则不相交
  if (
    rect1.x2 < rect2.x1 || // rect1 在 rect2 的左侧
    rect1.x1 > rect2.x2 || // rect1 在 rect2 的右侧
    rect1.y2 < rect2.y1 || // rect1 在 rect2 的上方
    rect1.y1 > rect2.y2     // rect1 在 rect2 的下方
  ) {
    return false; // 不相交
  }
  
  return true; // 相交
}

// 示例用法
console.log(doRectanglesIntersect(rect1, rect2)); // 输出: true

解释

  1. 矩形边界比较:函数检查两个矩形的四个边界。若一个矩形完全在另一个的边界外,则它们不相交。
  2. 返回结果:如果满足任何一个条件,返回 false,否则返回 true,表示两个矩形相交。

这种方法可以高效地判断两个矩形是否相交。

手写 useFetch()

js
import { useState, useEffect } from 'react';

const useFetch = (url, options = {}) => {
  const [data, setData] = useState(null);  // 存储返回的数据
  const [loading, setLoading] = useState(true);  // 加载状态
  const [error, setError] = useState(null);  // 错误状态

  useEffect(() => {
    const fetchData = async () => {
      try {
        setLoading(true);  // 开始加载
        setError(null);  // 清除之前的错误
        const response = await fetch(url, options);
        if (!response.ok) {
          throw new Error('Network response was not ok');
        }
        const result = await response.json();
        setData(result);  // 设置返回的数据
      } catch (err) {
        setError(err.message);  // 设置错误信息
      } finally {
        setLoading(false);  // 加载完成
      }
    };

    fetchData();
  }, [url, options]);  // 依赖项:URL 和 options 改变时重新调用

  return { data, loading, error };
};

export default useFetch;

WebSocket

js
class WebSocketManager {
  constructor(url, maxRetries = 5) {
    this.url = url;
    this.socket = null;
    this.messageQueue = []; // 消息队列
    this.isConnected = false;
    this.reconnectAttempts = 0; // 当前重连次数
    this.maxRetries = maxRetries; // 最大重连次数

    this.init();
  }

  // 初始化
  init() {
    this.socket = new WebSocket(this.url);

    this.socket.onopen = () => {
      console.log('WebSocket connected!');
      this.isConnected = true;
      this.reconnectAttempts = 0; // 重连成功后重置计数
      this.flushQueue(); // 重连后发送消息队列中的消息
    };

    this.socket.onclose = () => {
      console.warn('WebSocket disconnect!');
      this.isConnected = false;
      this.reconnect(); // 尝试重连
    };
  }

  reconnect() {
    if (this.reconnectAttempts >= this.maxRetries) {
      console.error(
        'Max reconnect attempts reached. Please check your network.'
      );
      return; // 达到最大重连次数后停止重连
    }

    const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000); // 指数退避,最大 30 秒
    this.reconnectAttempts++;

    console.log(
      `Reconnecting WebSocket in ${delay / 1000} seconds... (Attempt ${
        this.reconnectAttempts
      }/${this.maxRetries})`
    );

    setTimeout(() => {
      this.init(); // 尝试重新初始化 WebSocket
    }, delay);
  }

  // 发送消息
  sendMessage(message) {
    if (this.socket && this.socket.readyState === WebSocket.OPEN) {
      try {
        this.socket.send(message);
        console.log('Message sent:', message);
      } catch (error) {
        console.error('Error sending message:', error);
        this.messageQueue.push(message);
      }
    } else {
      console.warn('Connection lost, message queued:', message);
      this.messageQueue.push(message); // 入队
    }
  }

  // 清空队列
  flushQueue() {
    while (this.messageQueue.length > 0) {
      const message = this.messageQueue.shift(); // 从队列中取出消息
      this.sendMessage(message); // 重发消息
    }
  }
}

// 使用示例
const wsManager = new WebSocketManager('wss://echo.websocket.org', 3);

// 模拟发送消息
wsManager.sendMessage('Hello, Server!');
wsManager.sendMessage('Another message');

// 模拟断线后重连,未发送的消息会在连接恢复后自动重发
setTimeout(async () => {
  await wsManager.socket.close();
  wsManager.sendMessage('after close');
}, 3000);

视频请求

js
class VideoManager {
  constructor(videos) {
    this.videos = videos; // 视频 URL 列表
    this.currentIndex = 0; // 当前正在播放的视频索引
    this.cache = {}; // 缓存 blob URL
  }

  async loadVideo(index) {
    if (this.cache[index]) return this.cache[index];

    const response = await fetch(this.videos[index]);
    const blob = await response.blob();
    const url = URL.createObjectURL(blob);
    this.cache[index] = url;

    // 预加载下一条
    if (index + 1 < this.videos.length) {
      this.loadVideo(index + 1);
    }

    // 释放上一条
    if (index - 1 >= 0 && this.cache[index - 1]) {
      URL.revokeObjectURL(this.cache[index - 1]);
      delete this.cache[index - 1];
    }

    return url;
  }

  async playVideo(videoElement, index) {
    const url = await this.loadVideo(index);
    videoElement.src = url;
    videoElement.play();
    this.currentIndex = index;
  }
}

// 使用示例
const videoUrls = ['video1.mp4', 'video2.mp4', 'video3.mp4'];
const manager = new VideoManager(videoUrls);

const videoElement = document.querySelector('video');
document.querySelector('#next').addEventListener('click', () => {
  manager.playVideo(videoElement, manager.currentIndex + 1);
});