前端实习八股整理-手写场景代码(JavaScript)

一、手写计时器

1.用setTimeout实现倒计时

function countDown(seconds, callback) {
  let remaining = seconds;

  const timer = setTimeout(function tick() {
    if (remaining <= 0) {
      callback && callback();
      clearTimeout(timer);
    } else {
      console.log(`剩余时间: ${remaining}`);
      remaining--;
      setTimeout(tick, 1000); // 1秒后再次执行
    }
  }, 1000);
}

// 使用示例
countDown(5, () => console.log("倒计时结束!"));

2.用setInterval实现正计时

function startTimer(duration, callback) {
  let elapsed = 0;
  const timer = setInterval(() => {
    elapsed++;
    console.log(`已过去: ${elapsed}`);
    if (elapsed >= duration) {
      clearInterval(timer);
      callback && callback();
    }
  }, 1000);
}

// 使用示例
startTimer(5, () => console.log("计时结束!"));

3.基于performance.now实现更精确的计时器
setTimeout 和 setInterval 受浏览器事件循环影响,可能有延迟。使用 performance.now() 可以获取更高精度的计时:

function preciseTimer(duration, callback) {
  const startTime = performance.now();

  function checkTime() {
    const elapsed = (performance.now() - startTime) / 1000; // 转为秒
    if (elapsed >= duration) {
      callback && callback();
    } else {
      console.log(`精确计时: ${elapsed.toFixed(2)}`);
      requestAnimationFrame(checkTime); // 使用 requestAnimationFrame 优化
    }
  }

  checkTime();
}

// 使用示例
preciseTimer(5, () => console.log("精确计时结束!"));

requestAnimationFrame() 是浏览器提供的一个专为动画优化设计的 API,它会在浏览器下一次重绘(repaint)之前执行指定的回调函数,通常以 60 FPS(帧/秒) 的速率运行(即每帧约 16.7ms)。它的核心目标是实现更流畅、更高效的动画渲染,同时减少不必要的性能开销。

二、手撕解析URL为JS对象

function parseURLParams(url) {
  // 创建一个正则表达式来匹配查询字符串
  const regex = /[?&]([^=#]+)=([^&#]*)/g;
  const params = {};
  let match;

  // 使用正则表达式循环匹配查询字符串
  while ((match = regex.exec(url))) {
    const key = decodeURIComponent(match[1]);
    const value = decodeURIComponent(match[2]);

    // 如果参数名已经存在,将值存储为数组
    if (params[key]) {
      if (!Array.isArray(params[key])) {
        params[key] = [params[key]];
      }
      params[key].push(value);
    } else {
      params[key] = value;
    }
  }

  return params;
}

const url = "https://example.com?name=John&name=Jane&age=30";
const params = parseURLParams(url);
console.log(params); // { name: ["John", "Jane"], age: "30" }

三、手撕getType函数(获取详细的变量类型)

function getType(data) {
  // 获取到 "[object Type]",其中 Type 是 Null、Undefined、Array、Function、Error、Boolean、Number、String、Date、RegExp 等。
  const originType = Object.prototype.toString.call(data)
  // 可以直接截取第8位和倒数第一位,这样就获得了 Null、Undefined、Array、Function、Error、Boolean、Number、String、Date、RegExp 等
  const type = originType.slice(8, -1)
  // 再转小写,得到 null、undefined、array、function 等
  return type.toLowerCase()
}

四、手撕call和apply

Function.prototype.call2 = function (context) {
  var context = context || window
  context.fn = this

  var args = []
  for (var i = 1, len = arguments.length; i < len; i++) {
    args.push('arguments[' + i + ']')
  }

  var result = eval('context.fn(' + args + ')')

  delete context.fn
  return result
}

Function.prototype.apply = function (context, arr) {
  var context = Object(context) || window
  context.fn = this

  var result
  if (!arr) {
    result = context.fn()
  } else {
    var args = []
    for (var i = 0, len = arr.length; i < len; i++) {
      args.push('arr[' + i + ']')
    }
    result = eval('context.fn(' + args + ')')
  }

  delete context.fn
  return result
}

五、手写红绿灯

模拟一个红绿灯变化,红灯 1 秒,绿灯 1 秒,黄灯 1 秒,然后循环

function red() {
  console.log('red')
}

function green() {
  console.log('green')
}

function yellow() {
  console.log('yellow')
}

function light(cb, wait) {
  return new Promise((resolve) => {
    setTimeout(() => {
      cb()
      resolve()
    }, wait)
  })
}

function start() {
  return Promise.resolve()
    .then(() => {
      return light(red, 1000)
    })
    .then(() => {
      return light(green, 1000)
    })
    .then(() => {
      return light(yellow, 1000)
    })
    .finally(() => {
      return start()
    })
}

start()

六、手撕LRU缓存

LRU(Least Recently Used)是一种缓存淘汰策略,它会优先删除最近最少使用的数据。下面提供两种实现方式:使用 Map 的简单实现和不使用 Map 的基础实现。

1.使用 Map 的实现

class LRUCache {
  constructor(capacity) {
    this.cache = new Map()
    this.capacity = capacity
  }

  get(key) {
    if (!this.cache.has(key)) return -1

    // 将访问的元素移到最新使用的位置
    const value = this.cache.get(key)
    this.cache.delete(key)
    this.cache.set(key, value)
    return value
  }

  put(key, value) {
    // 如果 key 已存在,先删除
    if (this.cache.has(key)) {
      this.cache.delete(key)
    }
    // 如果达到容量限制,删除最久未使用的元素
    else if (this.cache.size >= this.capacity) {
      // Map 的 keys() 会按插入顺序返回键
      const firstKey = this.cache.keys().next().value
      this.cache.delete(firstKey)
    }

    this.cache.set(key, value)
  }
}

// 使用示例
const cache = new LRUCache(2)
cache.put(1, 1) // 缓存是 {1=1}
cache.put(2, 2) // 缓存是 {1=1, 2=2}
console.log(cache.get(1)) // 返回 1
cache.put(3, 3) // 删除 key 2,缓存是 {1=1, 3=3}
console.log(cache.get(2)) // 返回 -1 (未找到)

2.使用双向链表的实现(不依赖 Map)

// 双向链表节点
class Node {
  constructor(key, value) {
    this.key = key
    this.value = value
    this.prev = null
    this.next = null
  }
}

class LRUCache {
  constructor(capacity) {
    this.capacity = capacity
    this.cache = {} // 哈希表用于O(1)查找
    this.count = 0
    // 创建头尾哨兵节点
    this.head = new Node(0, 0)
    this.tail = new Node(0, 0)
    this.head.next = this.tail
    this.tail.prev = this.head
  }

  // 将节点移到双向链表头部
  moveToHead(node) {
    this.removeNode(node)
    this.addToHead(node)
  }

  // 从链表中删除节点
  removeNode(node) {
    node.prev.next = node.next
    node.next.prev = node.prev
  }

  // 在链表头部添加节点
  addToHead(node) {
    node.prev = this.head
    node.next = this.head.next
    this.head.next.prev = node
    this.head.next = node
  }

  // 删除链表尾部节点
  removeTail() {
    const node = this.tail.prev
    this.removeNode(node)
    return node
  }

  get(key) {
    if (key in this.cache) {
      const node = this.cache[key]
      this.moveToHead(node)
      return node.value
    }
    return -1
  }

  put(key, value) {
    if (key in this.cache) {
      // 如果 key 存在,更新值并移到头部
      const node = this.cache[key]
      node.value = value
      this.moveToHead(node)
    } else {
      // 创建新节点
      const newNode = new Node(key, value)
      this.cache[key] = newNode
      this.addToHead(newNode)
      this.count++

      // 如果超过容量,删除最久未使用的
      if (this.count > this.capacity) {
        const tail = this.removeTail()
        delete this.cache[tail.key]
        this.count--
      }
    }
  }
}

// 使用示例
const cache = new LRUCache(2)
cache.put(1, 1)
cache.put(2, 2)
console.log(cache.get(1)) // 返回 1
cache.put(3, 3) // 删除 key 2
console.log(cache.get(2)) // 返回 -1 (未找到)
cache.put(4, 4) // 删除 key 1
console.log(cache.get(1)) // 返回 -1 (未找到)
console.log(cache.get(3)) // 返回 3
console.log(cache.get(4)) // 返回 4

实现原理说明:
1.Map 实现版本:
·利用 Map 的特性,它能够记住键的原始插入顺序
·get 操作时将访问的元素移到最后(最新使用)
·put 操作时如果超出容量,删除第一个元素(最久未使用)
2.双向链表实现版本:
·使用哈希表实现 O(1) 的查找
·使用双向链表维护数据的使用顺序
·最近使用的数据放在链表头部
·最久未使用的数据在链表尾部
性能分析:
1.时间复杂度:
·get 操作:O(1)
·put 操作:O(1)
2.空间复杂度:
·O(capacity),其中 capacity 是缓存的容量

七、手写 VNode 对象,表示如下 DOM 节点

<div class="container">
  <img src="x1.png" />
  <p>hello</p>
</div>
const vnode = {
  tag: 'div',
  props: {
    class: 'container',
  },
  children: [
    {
      tag: 'img',
      props: {
        src: 'x1.png',
      },
    },
    {
      tag: 'p',
      props: {},
      children: ['hello'],
    },
  ],
}

八、手写 compose 函数

compose 函数是函数式编程中的一个重要概念,它将多个函数组合成一个函数,从右到左执行。
1.基础实现(使用 reduce)

function compose(...fns) {
  if (fns.length === 0) return (arg) => arg
  if (fns.length === 1) return fns[0]

  return fns.reduce(
    (a, b) =>
      (...args) =>
        a(b(...args))
  )
}

// 使用示例
const add1 = (x) => x + 1
const multiply2 = (x) => x * 2
const addThenMultiply = compose(multiply2, add1)
console.log(addThenMultiply(5)) // (5 + 1) * 2 = 12

九、手写curry函数,实现函数柯里化


function sum(...initialArgs) {
  let total = initialArgs.reduce((acc, val) => acc + val, 0);
  
  const curried = function(...args) {
    if (args.length === 0) {
      return curried;
    }
    total += args.reduce((acc, val) => acc + val, 0);
    return curried;
  };
  
  curried.value = function() {
    return total;
  };
  
  return curried;
}

// 测试用例
console.log(sum(1, 2)(3, 4).value()); // 输出: 10
console.log(sum(1)(2)(3)(4).value());  // 输出: 10
console.log(sum(1, 2, 3)(4).value());  // 输出: 10
console.log(sum(1)(2, 3, 4).value());  // 输出: 10

十、手写Promise

class MyPromise {
  // 构造方法
  constructor(executor) {
    // 初始化值
    this.initValue()
    // 初始化this指向
    this.initBind()
    // 执行传进来的函数
    executor(this.resolve, this.reject)
  }

  initBind() {
    // 初始化this
    this.resolve = this.resolve.bind(this)
    this.reject = this.reject.bind(this)
  }

  initValue() {
    // 初始化值
    this.PromiseResult = null // 终值
    this.PromiseState = 'pending' // 状态
  }

  resolve(value) {
    // 如果执行resolve,状态变为fulfilled
    this.PromiseState = 'fulfilled'
    // 终值为传进来的值
    this.PromiseResult = value
  }

  reject(reason) {
    // 如果执行reject,状态变为rejected
    this.PromiseState = 'rejected'
    // 终值为传进来的reason
    this.PromiseResult = reason
  }
}

十一、手写Promise.all

static all(promises) {
  const result = []
  let count = 0
  return new MyPromise((resolve, reject) => {
    const addData = (index, value) => {
        result[index] = value
        count++
        if (count === promises.length) resolve(result)
    }
    promises.forEach((promise, index) => {
        if (promise instanceof MyPromise) {
            promise.then(res => {
                addData(index, res)
            }, err => reject(err))
        } else {
            addData(index, promise)
        }
    })
  })
}

十二、手写Promise.race

static race(promises) {
  return new MyPromise((resolve, reject) => {
    promises.forEach(promise => {
      if (promise instanceof MyPromise) {
          promise.then(res => {
              resolve(res)
          }, err => {
              reject(err)
          })
      } else {
          resolve(promise)
      }
    })
  })
}

十三、手写Promise.allSettled

static allSettled(promises) {
  return new Promise((resolve, reject) => {
    const res = []
    let count = 0
    const addData = (status, value, i) => {
      res[i] = {
          status,
          value
      }
      count++
      if (count === promises.length) {
          resolve(res)
      }
    }
    promises.forEach((promise, i) => {
      if (promise instanceof MyPromise) {
        promise.then(res => {
          addData('fulfilled', res, i)
        }, err => {
          addData('rejected', err, i)
        })
      } else {
        addData('fulfilled', promise, i)
      }
    })
  })
}

十四、手写防抖Debounce

function debounce(func,delay){
    let debounceTimer;
    return function(){
        const context=this;
        const args=arguments;
        clearTimeout(debounceTimer);
        debounceTimer = setTimeout(()=>{
            func.apply(context,args);
        },delay)
    }
}
windows.addEventListener('resize',debounce(()=>{
    console.log('resizing...')
},500))

十五、手写节流Throttle

function throttle(func,limit){
    let inThrottle;
    return function(){
        const context = this;
        const args = arguments;
        if(!inThrottle){
            func.apply(context,args);
            inThrottle=True;
            setTimeout(()=>{
                inThrottle=false;
            },limit)
        }
    }
}
window.addEventListener('scroll',throttle(()=>{
    console.log('Scrolling')
},1000))

十六、输入一个字符串,将字符串中的-_字符后面的字母变成大写字母

function capitalizeAfterHyphenOrUnderscore(str) {
    return str.replace(/[-_](\w)/g, (match, char) => char.toUpperCase());
}

// 测试用例
console.log(capitalizeAfterHyphenOrUnderscore("hello-world")); // 输出: "helloWorld"
console.log(capitalizeAfterHyphenOrUnderscore("foo_bar_baz")); // 输出: "fooBarBaz"
console.log(capitalizeAfterHyphenOrUnderscore("a-b-c-d"));     // 输出: "aBCD"

十七、手写new操作符

在 JavaScript 中,new 操作符用于创建一个用户定义的对象类型的实例或具有构造函数的内置对象类型的实例。要实现一个类似 new 操作符的功能,可以通过以下步骤来模拟:
1.创建一个空对象:这个空对象将作为新对象的实例。
2.设置原型:将新对象的原型指向构造函数的 prototype。
3.执行构造函数:将构造函数的 this 指向新创建的对象,并执行构造函数。
4.返回新对象:返回新创建的对象。
以下是实现 new 操作符的代码:

function myNew(constructor, ...args) {
    // 创建一个空对象,并将其原型指向构造函数的 prototype
    const obj = Object.create(constructor.prototype);

    // 将构造函数的 this 指向新创建的对象,并执行构造函数
    const result = constructor.apply(obj, args);

    // 如果构造函数返回一个对象,则返回该对象;否则返回新创建的对象
    return result instanceof Object ? result : obj;
}

// 测试用例
function Person(name, age) {
    this.name = name;
    this.age = age;
}

Person.prototype.sayHello = function () {
    console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
};

const person1 = myNew(Person, "Alice", 25);
console.log(person1); // 输出: Person { name: 'Alice', age: 25 }
person1.sayHello(); // 输出: Hello, my name is Alice and I am 25 years old.

const person2 = myNew(Person, "Bob", 30);
console.log(person2); // 输出: Person { name: 'Bob', age: 30 }
person2.sayHello(); // 输出: Hello, my name is Bob and I am 30 years old.

十八、对版本号数组进行排序,比如[0.1.2.3, 1.2.1.0, 4.2.1.0, 0.1.2.0]

function compareVersions(a, b) {
  // 将版本号按点号分割成数组
  var aParts = a.split('.');
  var bParts = b.split('.');

  // 比较每个部分
  for (var i = 0; i < Math.max(aParts.length, bParts.length); i++) {
    // 如果一个版本号较短,我们可以认为缺失的部分为0
    var aPart = aParts[i] || 0;
    var bPart = bParts[i] || 0;

    // 将部分转换为数字并进行比较
    var aNum = parseInt(aPart, 10);
    var bNum = parseInt(bPart, 10);

    if (aNum > bNum) {
      return 1;
    } else if (aNum < bNum) {
      return -1;
    }
  }

  // 如果所有部分都相等,则版本号相等
  return 0;
}

// 使用比较函数对版本号数组进行排序
var versions = ['0.1.2.3', '1.2.1.0', '4.2.1.0', '0.1.2.0'];
versions.sort(compareVersions);

console.log(versions); // 输出排序后的数组

十九、手写 call 和 apply

Function.prototype.call2 = function (context) {
  var context = context || window
  context.fn = this

  var args = []
  for (var i = 1, len = arguments.length; i < len; i++) {
    args.push('arguments[' + i + ']')
  }

  var result = eval('context.fn(' + args + ')')

  delete context.fn
  return result
}

Function.prototype.apply = function (context, arr) {
  var context = Object(context) || window
  context.fn = this

  var result
  if (!arr) {
    result = context.fn()
  } else {
    var args = []
    for (var i = 0, len = arr.length; i < len; i++) {
      args.push('arr[' + i + ']')
    }
    result = eval('context.fn(' + args + ')')
  }

  delete context.fn
  return result
}

二十、手写 EventBus 自定义事件

class EventBus {
  constructor() {
    this.eventObj = {}
    this.callbcakId = 0
  }

  $on(name, callbcak) {
    if (!this.eventObj[name]) {
      this.eventObj[name] = {}
    }
    const id = this.callbcakId++
    this.eventObj[name][id] = callbcak
    return id
  }
  $emit(name, ...args) {
    const eventList = this.eventObj[name]
    for (const id in eventList) {
      eventList[id](...args)
      if (id.indexOf('D') !== -1) {
        delete eventList[id]
      }
    }
  }
  $off(name, id) {
    delete this.eventObj[name][id]
    if (!Object.keys(this.eventObj[name]).length) {
      delete this.eventObj[name]
    }
  }
  $once(name, callbcak) {
    if (!this.eventObj[name]) {
      this.eventObj[name] = {}
    }
    const id = 'D' + this.callbcakId++
    this.eventObj[name][id] = callbcak
    return id
  }
}

二十一、怎么实现将一个图标拖拽到另一个地方?

实现图标拖拽功能可以通过 HTML5 的拖放 API(Drag and Drop API)来完成。以下是一个简单的示例,展示如何将一个图标拖拽到另一个地方。
示例代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>拖拽示例</title>
  <style>
    .draggable {
      width: 50px;
      height: 50px;
      background-color: blue;
      color: white;
      text-align: center;
      line-height: 50px;
      cursor: pointer;
      margin: 10px;
    }
    .dropzone {
      width: 200px;
      height: 200px;
      background-color: lightgray;
      margin: 10px;
      padding: 10px;
      border: 2px dashed black;
    }
  </style>
</head>
<body>
  <div class="draggable" draggable="true" id="draggable">拖拽我</div>
  <div class="dropzone" id="dropzone">拖到这里</div>

  <script src="script.js"></script>
</body>
</html>

script.js

document.addEventListener("DOMContentLoaded", () => {
  const draggable = document.getElementById("draggable");
  const dropzone = document.getElementById("dropzone");

  // 拖拽开始时触发
  draggable.addEventListener("dragstart", (event) => {
    event.dataTransfer.setData("text/plain", event.target.id); // 传递被拖拽元素的 ID
  });

  // 拖拽进入目标区域时触发
  dropzone.addEventListener("dragover", (event) => {
    event.preventDefault(); // 阻止默认行为,允许放置
  });

  // 拖拽离开目标区域时触发
  dropzone.addEventListener("dragleave", (event) => {
    console.log("离开目标区域");
  });

  // 拖拽结束时触发
  dropzone.addEventListener("drop", (event) => {
    event.preventDefault(); // 阻止默认行为
    const draggableId = event.dataTransfer.getData("text/plain"); // 获取被拖拽元素的 ID
    const draggableElement = document.getElementById(draggableId); // 获取被拖拽的元素
    dropzone.appendChild(draggableElement); // 将被拖拽的元素移动到目标区域
  });
});

二十二、手写实现sleep

用setTimeout

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

// 使用示例
async function example() {
  console.log("Hello");
  await sleep(2000); // 等待2秒
  console.log("World");
}

example();
### 前端实习面试常见问题及答案 #### HTML部分 对于HTML的理解不仅仅局限于标签的记忆,更在于如何合理运用这些标签来构建语义化的网页结构。 - **什么是DOCTYPE声明的作用?** DOCTYPE声明位于文档的最前面,在浏览器解析页面之前告知其使用的HTML版本。这有助于确保浏览器按照标准模式渲染页面而不是兼容模式[^1]。 #### CSS部分 掌握CSS不仅意味着能够编写样式表,还涉及对盒模型、布局机制以及响应式设计原理的理解。 - **解释下CSS中的BFC是什么?** BFC (Block Formatting Context) 是一种独立的渲染区域,其中子元素不会影响外部其他元素,并且解决了浮动溢出等问题。创建新的BFC可以防止父级高度塌陷现象的发生。 #### JavaScript部分 JavaScript作为前端的核心技术之一,考察范围广泛,从基本语法到高级特性都会涉及到。 - **闭包的概念及其应用场景有哪些?** 当函数嵌套定义时,内部函数会形成对外部作用域变量的引用关系即构成闭包。利用这一特点可以在回调函数保存状态信息;实现数据封装隐藏私有成员等功能。 #### HTTP协议相关 网络请求是Web应用不可或缺的一环,了解HTTP的工作方式至关重要。 - **GET与POST方法的区别在哪里?** GET主要用于获取资源,参数通过URL传递长度有限制;而POST用于提交数据给服务器处理,支持更大的消息体传输量并具有更高的安全性因为不暴露于地址栏中。 #### Vue框架基础 随着单页应用程序(SPA)的发展趋势日益明显,Vue.js成为许多企业青睐的技术栈选项。 - **组件间通信的方式都有哪些呢?** 子向父传值可通过`$emit()`触发自定义事件携带payload对象完成;反之则借助props属性注入子组件所需的数据流。除此之外还有Vuex全局状态管理库可供选择当面临复杂场景需求时[^3]。 ```javascript // 父组件监听来自子组件的消息 <ChildComponent @child-event="handleEvent"/> ... methods: { handleEvent(payload){ console.log('接收到子组件发送的信息:', payload); } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值