JS手写题梳理

34 阅读5分钟

一、数据处理题

数组扁平化

// 一、concat、push实现
function flat(arr) {
  let res = []
  for(let item of arr){
    if(Array.isArray(item)){
      // res.push(...flat(item))
      res = res.concat(flat(item)) // concat不会改变原数组,需重新赋值
    }else{
      res.push(item)
    }
  }
  return res
}

// 二、reduce实现
function flat(arr) {
  return arr.reduce((pre, cur) => {
    return pre.concat(Array.isArray(cur) ? flat(cur) : cur)
  }, [])
}

// 三、栈实现
function flat(arr) {
  const res = []
  // 数组是复杂数据类型,直接赋值只是指向同一个地址,会一起改变
  const stack = [].concat(arr)
  while(stack.length){
    const current = stack.pop()
    if(Array.isArray(current)){
      // 如果是数组,展开一层再重新入栈
      stack.push(...flat(current))
    }else{
      res.unshift(current)
    }
  }
  return res
}

// 四、String
function flat(arr) {
  return String(arr).split(',').map(Number)
}

// 五、参数控制扁平层数
function flat(arr, num = 1) {
  return num > 0 ? arr.reduce((pre, cur) => {
    return pre.concat(Array.isArray(cur) ? flat(cur, num - 1) : cur)
  }, []) : arr.slice()
}

console.log(flat([1,2,3,[5,5,[55]]]))

注:
ES5 对空位的处理,大多数情况下会忽略空位。

  • forEach(), filter(), reduce(), every() 和some() 都会跳过空位。
  • map() 会跳过空位,但会保留这个值。
  • join() 和 toString() 会将空位视为 undefined,而undefined 和 null 会被处理成空字符串。

ES6 明确将空位转为 undefined。

  • entries()、keys()、values()、find() 和 findIndex() 会将空位处理成 undefined。
  • for...of 循环会遍历空位。
  • fill() 会将空位视为正常的数组位置。
  • copyWithin() 会连空位一起拷贝。
  • 扩展运算符(...)也会将空位转为 undefined。
  • Array.from 方法会将数组的空位,转为 undefined。

参考:segmentfault.com/a/119000002…

对象扁平

function flattenObject(obj, parentKey = '', result = {}) {
    for (let key in obj) {
        let newKey = `${parentKey}${parentKey ? '.' : ''}${key}`;
        if (typeof obj[key] === 'object' && obj[key] !== null) {
            flattenObject(obj[key], newKey, result);
        } else {
            result[newKey] = obj[key];
        }
    }
    return result;
}

// 测试
let testObj = {
    a: 1,
    b: [1, 2, 3],
    c: {
        d: 4,
        e: {
            f: 5,
        },
    }
};
console.log(flattenObject(testObj));
// { a: 1, 'b.0': 1, 'b.1': 2, 'b.2': 3, 'c.d': 4, 'c.e.f': 5 }

对象相等

function deepEqual(a, b) {
    // 检查两者类型是否严格相等
    if (typeof a !== typeof b) return false;
    
    // 如果两者都是null或都是undefined,那么它们是相等的
    if (a === null || a === undefined) return a === b;

    // 如果两者都是对象,则递归比较每个属性
    if (typeof a === 'object') {
        const keysA = Object.keys(a);
        const keysB = Object.keys(b);

        // 检查两个对象是否具有相同数量的属性
        if (keysA.length !== keysB.length) return false;

        // 对于每一个key,检查它们在两个对象中的值是否相等
        for (let key of keysA) {
            if (!keysB.includes(key)) return false;
            if (!deepEqual(a[key], b[key])) return false;
        }

        // 如果每个属性都相等,那么这两个对象就是相等的
        return true;
    }

    // 如果两者不是对象,比较它们是否严格相等
    return a === b;
}

// 使用示例:
const objA = { a: 1, b: { c: 3 } };
const objB = { a: 1, b: { c: 3 } };

console.log(deepEqual(objA, objB));  // 输出:true

数组去重

// 一、Set实现
function unique(arr){
  return Array.from(new Set(arr))
}

// 二、filter + indexOf
function unique(arr){
  return arr.filter((item, index) => index === arr.indexOf(item))
}

// 三、Map解法
function unique(arr){
  const res = []
  const myMap = new Map()
  for(let item of arr){
    if(!myMap.has(item)){
      myMap.set(item, 1)
      res.push(item)
    }
  }
  return res
}

console.log(unique([1,2,1,1,6,8,98,8]))

千位符分割

// 一、使用toLocaleString
function formatNumber(num) {
  // 直接使用 JavaScript 内置的 toLocaleString 方法格式化数字。
  return num.toLocaleString();
}

// 二、数组处理
function formatNumber(num) {
  num = String(num).split('.')  // 分隔小数点
  let arr = num[0].split('').reverse()
  let res = []
  for(let i = 0; i < arr.length; i++){
    if(i % 3 === 0 && i !== 0){
      res.push(',')
    }
    res.push(arr[i])
  }
  num[0] = res.reverse().join('')
  return num.join('.')
}

// 三、正则
function formatNumber(num) {
  // 将数字转换为字符串
  num = num.toString();
  // 使用正则表达式匹配字符串,然后在匹配的位置插入逗号
  return num.replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,');
}

console.log(formatNumber(465116.48));

1.num.replace(regexp|substr, newSubStr|function) 是 JavaScript 字符串的 replace 方法用于在字符串中用一些字符替换其他字符,或替换一个与正则表达式匹配的子串。
2./(\d)(?=(\d{3})+(?!\d))/g 是正则表达式,用于匹配字符串中的一部分。这个正则表达式的详细解释如下:

  • /(\d)(?=(\d{3})+(?!\d))/g 中,/g 表示全局匹配,会匹配输入的所有符合条件的值。
  • /(\d) 是一个捕获组,匹配数字字符。
  • (?=(\d{3})+(?!\d)) 是看前瞻断言,它匹配字符后面跟着3个数字字符且不超过3个数字的字符串,但它只匹配结果的位置,并不包含匹配项本身。

3.'1,replace方法的第二个参数,表示要替换的内容。其中1,' 是replace方法的第二个参数,表示要替换的内容。其中 1 是个特殊的变量,表示正则表达式第一个括号匹配的内容,$1, 就表示用匹配的内容后面加个逗号来替换原有匹配的内容。

字符串与驼峰

//空格式
let a='hello world'
var b=a.split(' ').map(item=>{
    return item[0].toUpperCase()+item.slice(1,item.length)
}).join('')
console.log(b)

//横线式
const str = 'helloWorld';
function getKebabCase(str) {
    let arr = str.split('');
    let result = arr.map((item) => {
        if (item.toUpperCase() === item) {
            return '-' + item.toLowerCase();
        } else {
            return item;
        }
    }).join('');
    return result;
}

console.log(getCamelCase(str))

对象数组转树状结构

function arrayToTree(arr) {
    var map = {}, node, roots = [], i;
    for (i = 0; i < arr.length; i += 1) {
        map[arr[i].id] = i; // 初始化 map,id 为 key,i 为 value
        arr[i].children = []; // 初始化 children
    }
    for (i = 0; i < arr.length; i += 1) {
        node = arr[i];
        if (node.parentId !== "0") {
            // 如果存在父节点
            arr[map[node.parentId]].children.push(node);
        } else {
            // 如果 parentId 为 '0',则表示该节点为根节点
            roots.push(node);
        }
    }
    return roots;
}

// 示例输入
let items = [
    { id: '1', name: 'A', parentId: '0' },
    { id: '2', name: 'B', parentId: '1' },
    { id: '3', name: 'C', parentId: '2' },
    { id: '4', name: 'D', parentId: '1' },
    { id: '5', name: 'E', parentId: '0' }
]

console.log(JSON.stringify(arrayToTree(items)));

解析 URL

function getQueryParams(url) {
    const params = {};
    const urlObj = new URL(url);
    const queryParams = new URLSearchParams(urlObj.search);

    for(let pair of queryParams.entries()) {
        params[pair[0]] = pair[1];
    }

    return params;
}

// 使用示例:
const url = 'https://example.com?param1=value1&param2=value2';
console.log(getQueryParams(url));  // 输出:{ param1: 'value1', param2: 'value2' }

大数相加

function bigNumberAdd(a, b){
  const arr1 = a.split('').reverse()
  const arr2 = b.split('').reverse()
  let carry = 0
  const res = []
  const len = Math.max(arr1.length, arr2.length)

  for(let i = 0; i < len; i++){
    let sum = (Number(arr1[i]) || 0) + (Number(arr2[i]) || 0) + carry
    if(sum / 10 >= 1){
      sum -= 10
      carry = 1
    }else{
      carry = 0
    }
    res.push(sum)
  }
  if(carry === 1) res.push(1)
  return res.reverse().join('')
}

console.log(bigNumberAdd('111111111111111111111', '222222222222222222'));
// 111333333333333333333

大数相乘

function bigNumberMultiply(a, b) {
    var res = Array(a.length + b.length).fill(0);  // 初始化结果数组
    for (var i = a.length - 1; i >= 0; i--) {  // 从低位遍历第一个数
        for (var j = b.length - 1; j >= 0; j--) {  // 从低位遍历第二个数
            var product = a[i] * b[j];  // 相乘的结果
            var sum = res[i+j+1] + product; //加上上一次计算的结果
            res[i+j+1] = sum % 10;  // 结果当前位
            res[i+j] += Math.floor(sum / 10);  // 结果进位
        }
    }
    // 移除前导零
    while(res[0] === 0) {
        res.shift();  
    }
    return res.join('');
}

bigNumberMultiply('123456789', '987654321');  // 返回结果:'121932631112635269'

二、JS 原生题

节流

含义:规定事件内只执行一次
适用场景

  1. 滚动事件监听:监听滚动事件的大列表页面,每次滚动都会触发事件处理函数,这可能会导致频繁的DOM操作,而通过节流函数可以有效地降低处理函数的触发频率。
  2. 窗口调整:在窗口调整(resize)的时候,节流函数能帮助我们减少不必要的计算和DOM操作。
  3. 输入事件监听:对于用户的输入事件,比如搜索框的输入,我们也可以使用节流函数来降低请求服务器的频率。
  4. 动画效果:做一个动画需要频繁地更新界面,使用节流函数可以在不影响动画流畅性的同时,减少计算和渲染的次数。
// 简易版节流
function throttle(fn, delay){
  let now = Date.now()
  return function(){
    const current = Date().now()
    if(current - now > delay){
      fn.apply(this, arguments)
      now = current
    }
  }
}

高级版节流

// 高级版节流
function throttle(func, wait, options = {}) {
  let timeout, context, args, result;
  let previous = 0;

  // 定义执行函数
  let later = function () {
    previous = options.leading === false ? 0 : Date.now();
    timeout = null;
    result = func.apply(context, args);
    if (!timeout) {
      context = args = null;
    }
  };

  let throttled = function () {
    let now = Date.now();
    if (!previous && options.leading === false) {
      previous = now;
    }
    // 剩余时间
    let remaining = wait - (now - previous);
    context = this;
    args = arguments;
    // 如果没有剩余的时间了或者你改了系统时间
    if (remaining <= 0 || remaining > wait) {
      if (timeout) {
        clearTimeout(timeout);
        timeout = null;
      }
      previous = now;
      result = func.apply(context, args);
      if (!timeout) {
        context = args = null;
      }
    } else if (!timeout && options.trailing !== false) { // 最后一次需要触发的情况
      timeout = setTimeout(later, remaining);
    }
    return result;
  };

  throttled.cancel = function () {
    clearTimeout(timeout);
    previous = 0;
    timeout = context = args = null;
  };

  // 获取剩余时间
  throttled.remaining = function () {
    if (!timeout) {
      return 0;
    }
    let now = Date.now();
    return wait - (now - previous);
  };

  return throttled;
};

解析详细解析一下这个节流函数的实现过程:

  1. 设置所需变量:在函数体的开始,我们定义了几个变量,包括timeout(用来存储定时器的引用)、context、args(用来在后面执行函数时存储this和arguments),result(用来存储函数执行结果),以及一个名为previous的时间戳(用来计算函数下一次执行的间隔时间)。
  2. 定义执行函数(later):在这个函数中,我们首先通过判断options.leading(一个标志位,决定了是否立即执行函数)来决定如何更新时间戳previous。接着清空定时器引用timeout, 并执行目标函数func。函数运行结束后,如果没有定时器,我们就清空上下文和参数。
  3. 定义节流函数(throttled):这个函数在每次触发时,都会更新时间戳,并保存上下文和参数。然后判断当前时间与上次执行函数的时间间隔是否超过了指定等待时间wait,或者是用户修改了系统时间。如果是,则清空定时器,并立即执行函数。否则,如果我们没有设置定时器,且允许函数在最后一次触发时再执行一次(这是由options.trailing控制的),就设置一个定时器在剩余时间后执行函数。
  4. 提供取消功能:我们提供了一个cancel方法来清除定时器和重置所有状态,能够在需要时立即停止函数的执行。
  5. 提供查询功能:还提供了一个remaining方法来获取距离下一次执行函数还有多少时间,便于我们了解函数何时会被触发。

防抖

含义:规定时间内只执行最后一次
适用场景

  1. 搜索框实时查询:当用户在搜索框中输入内容时,我们可能希望在他们停止输入一段时间后再发送请求,而不是每输入一个字符就发送一个。这样可以显著降低服务器的压力。
  2. 窗口大小调整(resize):用户在调整浏览器窗口大小的时候,我们可能需要重新计算一些布局,但是并不需要在每一次尺寸改变的时候都去做这个操作。防抖可以使我们在用户结束调整后再执行一次。
  3. 表单按钮提交:避免用户连续点击按钮,导致表单被多次提交。
  4. 滚动事件:尽管滚动事件可能更常见的是使用节流,但在某些情况下,你可能希望等到滚动停止后再执行操作,比如懒加载图片。防抖能够保证只有在用户停止滚动之后,才会加载图片。
  5. 阅读位置保存:在用户阅读文章或者书籍的时候,可能会希望保存用户的阅读位置。而用户在不断地滚动页面,我们并不需要实时地保存位置,可以应用防抖,在用户停止滚动一段时间后再保存当前位置。
  6. 文本编辑器自动保存:在用户使用文本编辑器的时候,为了防止数据丢失,我们可能会采用自动保存的策略。此时,如果每修改一次都进行保存,这显然是不合理的。这时候,我们就可以使用防抖,用户停止操作后,对文档进行保存。
// 
function debounce(fn, delay){
  let timer = null
  return function(){
    if(!timer){
      clearTimeout()
    }
    timer = setTimeout(() => {
      fn.apply(this, arguments)
      timer = null
    }, delay)
  }
}

// 支持立即执行版
function debounce(func, wait, immediate) {
  let timeout;
  return function() {
    // 在新函数中,通过闭包机制,保存新函数的上下文(this)和参数(arguments)
    let context = this;
    let args = arguments;
    // 定义一个待执行的函数,如果满足条件(immediate为false等),则在wait毫秒后,执行func函数
    let later = function() {
      // 在wait毫秒后,清除定时器ID
      timeout = null;
      // 当immediate的值为false时,才调用func函数
      if (!immediate) func.apply(context, args);
    };
    // 判断在一个wait时间段开始时,是否要立即执行func函数
    let callNow = immediate && !timeout;
    // 每次都需要清理上一个定时器
    clearTimeout(timeout);
    // 设置新的定时器,将later函数延迟wait毫秒后执行
    timeout = setTimeout(later, wait);
    // 如果满足在一个wait时间段开始时立即执行func函数的条件,则执行func函数
    if (callNow) func.apply(context, args);
  };
}

深拷贝

function deepClone(obj, cache = new WeakMap()) {
  // 如果 obj 不是对象,直接返回 obj
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }
  // 检查是否已经复制过该对象,如果是,则直接返回缓存的副本
  if (cache.has(obj)) {
    return cache.get(obj);
  }
  // 处理特殊对象,如日期对象、正则表达式
  if (obj instanceof Date) {
    return new Date(obj);
  }
  if (obj instanceof RegExp) {
    return new RegExp(obj);
  }
  // 创建一个与原对象具有相同原型链的新对象
  const clone = Object.create(Object.getPrototypeOf(obj));
  // 将该对象缓存,以防止循环引用
  cache.set(obj, clone);
  // 遍历原对象的属性并递归复制它们到新对象
  for (const key in obj) {
    if (Object.prototype.hasOwnProperty.call(obj, key)) {
      // 或者使用obj.hasOwnProperty(key)
      clone[key] = deepClone(obj[key], cache);
    }
  }
  return clone;
}

// 示例用法
const originalObj = {
  a: 1,
  b: {
    c: 2,
    d: [3, 4],
  },
};
const clonedObj = deepClone(originalObj);
console.log(clonedObj);

instanceof

function myInstanceof(left, right) {
  // 保证运算符右侧必须为函数(因为只有函数才有prototype属性)
  if (typeof right !== 'function') {
    throw new Error('Right-hand side of "instanceof" is not callable');
  }
  // 如果左侧对象为null或者undefined则直接返回false(只有对象或函数才能有原型链)
  if (left === null || typeof left !== 'object' && typeof left !== 'function') {
    return false;
  }
  // 获取右侧函数的 prototype 值
  let prototype = right.prototype;
  // 获得 left 的原型对象
  let leftProto = Object.getPrototypeOf(left);
  while (true) {
    // 如果原型链已经查到最顶层(Object.prototype.__proto__ === null)
    if (leftProto === null) {
      return false;
    }
    // 在原型链上找到了对应的原型,证明是其实例
    if (prototype === leftProto) {
      return true;
    }
    // 沿原型链向上查找
    leftProto = Object.getPrototypeOf(leftProto);
  }
}

console.log(myInstanceof([], Date));

new

function myNew(ctor, ...args) {
  if (typeof ctor !== "function") {
    throw "ctor must be a function";
  }
  // 创建新的空对象,链接原型
  let newObj = Object.create(ctor.prototype);
  // 调用构造函数,并将this指向新的对象
  let result = ctor.apply(newObj, args);
  // 如果构造函数返回了一个对象,则返回这个对象,否则返回那个新对象
  return result instanceof Object ? result : newObj;
} 

function Person(name, age) {
  this.name = name;
  this.age = age;
}
let person = myNew(Person, 'Tom', 20);
console.log(person.name);  // 输出 'Tom'
console.log(person.age);   // 输出 20

apply

Function.prototype.myApply = function(context, arr) {
    // 如果没有提供`this`参数,默认为全局对象
    context = context || window;
    // 创建一个独有的属性键,从而避免占用或改变对象上的其他属性
    const uniqueProp = Symbol();
    // `this`指向调用`myApply`的对象,即要改变`this`指向的函数
    context[uniqueProp] = this;
    let result;
    // 检查是否传入第二个参数,如果没有传入参数,直接调用函数
    if (!arr) {
        result = context[uniqueProp]();
    } else {
      // 如果传入了参数,将参数数组展开,然后传入函数
        result = context[uniqueProp](...arr);
    }
    delete context[uniqueProp];
    return result;
};

// 使用示例:
function showInfo(age, country) {
    console.log(`Hello, my name is ${this.name}, I'm ${age} years old, from ${country}`);
}

const usr = { name: 'Tom' };

// 用myApply方法来调用showInfo函数,`this`指向usr对象
showInfo.myApply(usr, [23, 'USA']);  // 输出:Hello, my name is Tom, I'm 23 years old, from USA

call

// 在原型上挂载
Function.prototype.myCall = function (thisArg, ...args) {
  if (typeof this !== "function") {
    throw new TypeError("myCall can only be used on functions");
  }
  // 当函数作为对象里的方法被调用时,this 被设置为调用该函数的对象
  // 此处this为原函数(test),thisArg是传入的需设置为this的对象(obj)
  const key = Symbol("key");
  thisArg[key] = this;
  // 将thisArg[key](...args)的值存下来
  const result = thisArg[key](...args);
  // 删除多追加的test方法
  delete thisArg[key];
  return result;
};

const obj = {
  name: "Leo",
};
function test(a, b) {
  console.log(this);
  console.log(a, b);
  return a + b;
}

const res = test.myCall(obj, 10, 15);
console.log("两数和为:", res);

bind

Function.prototype.myBind = function(context) {
  if (typeof this !== "function") {
    throw new TypeError("Error");  // 如果调用对象不是函数,则抛出错误
  }
  
  // 获取参数,args为传入的参数数组,去除第一个上下文对象context
  var args = [...arguments].slice(1), 
      fn = this;  // this指向调用者,即需要绑定上下文的函数
  
  // 返回一个新的函数
  return function Fn() {  
    // 如果新函数作为构造函数(使用new关键字)被调用,this指向新创建的实例对象
    // 如果新函数作为普通函数被调用,this指向绑定的上下文对象context
    return fn.apply(
      this instanceof Fn ? this : context,  
      // 传入的参数为,myBind时附带的预置参数 和 执行时新传入的参数
      args.concat(...arguments)
    );
  };
};

function greeting(msg) {
    console.log(msg + ', ' + this.name);
}

var obj = {name: 'Alice'};
// 我们使用 `myBind` 方法将 `this` 绑定到 obj 对象
var boundGreeting = greeting.myBind(obj);
//然后我们调用 `boundGreeting` 函数,并传递一个消息
boundGreeting('Hello'); // 输出:Hello, Alice

快速排序

function quickSort(arr){
  if(arr.length < 2) return arr

  const midIndex = Math.floor(arr.length / 2)
  const midValue = arr[midIndex]
  const left = []
  const right = []

  for(let i = 0; i < arr.length; i++){
    if(i === midIndex) continue
    if(arr[i] < midValue){
      left.push(arr[i])
    }else if(arr[i] > midValue){
      right.push(arr[i])
    }
  }

  return [...quickSort(left), midValue, ...quickSort(right)]
}

函数柯里化

function curry(fn, args) {
  // 获取原函数 fn 的期望参数个数。
  const length = fn.length; 
  // 如果没有传入第二个参数 args 或者 args 是 falsey 值,那么 args 将被设定为空数组,args 用于收集参数。
  args = args || []; 
  
  // 返回一个新函数,用于收集参数。
  return function() { 
    // 创建一个新数组 newArgs,用来存放 args 数组和新函数收到的参数。
    const newArgs = args.concat(Array.from(arguments)); 

    // 如果收集到的参数个数还不足以满足原函数需要的参数个数,那么就继续返回 curry 函数,继续收集参数。
    if (newArgs.length < length) { 
      // 递归调用 curry 函数,继续收集参数。
      return curry.call(this, fn, newArgs); 
    } else {
      // 当收集到的参数数量达到原函数 fn 的参数个数时,执行原函数并返回结果。利用 apply 方法将收集到的参数传给原函数。
      return fn.apply(this, newArgs); 
    }
  };
}

function sum(a, b, c) {
  console.log(a, b, c);
}
var curriedSum = curry(sum);
curriedSum(1)(2, 3)

三、场景题

颜色转换

  • RGB 转 16 进制
function rgbToHex(r, g, b) {
    var hex = ((r << 16) | (g << 8) | b).toString(16);
    return "#" + new Array(Math.abs(hex.length-7)).join("0") + hex;
}

console.log(rgbToHex(255, 255, 255));  // 输出:#ffffff
  • 16 进制 转 RGB
function hexToRgb(hex) {
    var rgb = [];
    for(var i=1; i<7; i+=2) {
        rgb.push(parseInt("0x" + hex.slice(i, i+2)));
    }
    return rgb;
}

console.log(hexToRgb("#ffffff"));  // 输出:[255, 255, 255]

每秒打印一个数字

async function printNumbers() {
  for (let i = 0; i < 10; i++) {
    console.log(i);
    await sleep(1000);
  }
}
// 使用 Promise 来实现一个 sleep 函数。这个函数可以暂停一段指定的时间,然后再继续执行
function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

printNumbers();

求 a 值

// 当a为何值时会有输出
var a = ?
if(a == '1' && a == '2' && a == '3'){
  console.log(a)
}

// 实现
var a = {
    i: 1,
    toString: function () {
        return a.i++;
    }
}

实现一个计数器

var createCounter = function(init) {
    return {
        initValue: init,
        increment: function(){
            return ++init
        },
        decrement: function(){
            return --init
        },
        reset: function(){
            init = this.initValue
            return init
        }
    }
};