Skip to content

手撕

Object.create()

在该函数中传入一个对象,从而创建一个继承自该对象的对象

js
function create(obj,propertiesObject) {
  function F() {}      // 1. 创建一个空的构造函数 F
  F.prototype = obj    // 2. 将 F 的原型指向传入的 obj
  const instance = new F()       // 3. 返回 F 的实例对象,该实例对象的原型是 obj
    
   // 处理第二个参数:属性描述符对象
  if (propertiesObject !== undefined) {
    Object.defineProperties(instance, propertiesObject);
  }
  
  return instance;
}

instanceOf

A instanceOf B,用于判断A是否是B的实例对象

js
function myInstanceof(left, right) {
  // 获取 left 对象的原型,即__proto__属性
  let proto = Object.getPrototypeOf(left);
  // 获取 right 构造函数的 prototype 对象
  let prototype = right.prototype;

  // 在原型链上查找
  while (true) {
    if (!proto) return false;  // 到达原型链末端
    if (proto === prototype) return true;  // 找到匹配的原型
    
    // 继续向上查找原型链
    proto = Object.getPrototypeOf(proto);
  }
}

new操作符

js
function objectFactory() {
  let newObject = null; 
  let constructor = Array.prototype.shift.call(arguments); // 用于取出arguments的第一个值,同时保留剩下的
  let result = null;
  // 判断参数是否是一个函数
  if (typeof constructor !== "function") {
    console.error("type error");
    return;
  }
  // 新建一个空对象,对象的原型为构造函数的 prototype 对象
  newObject = Object.create(constructor.prototype);
  // 将 this 指向新建对象,并在对象内执行构造函数,如果构造函数有返回值,result会接受返回值
  result = constructor.apply(newObject, arguments);
  // 判断result的返回类型
  let flag = result && (typeof result === "object" || typeof result === "function");
  // 判断返回结果。如果是对象或者函数类型,则直接返回,如果不是,就需要返回object包装的类型
  return flag ? result : newObject;
}
// 使用方法
objectFactory(构造函数, 初始化参数);

Promise

js
const PENDING = 'pending';
const RESOLVED = 'resolved';
const REJECTED = 'rejected';

class MyPromise {
  constructor(executor) {
    this.state = PENDING;
    this.value = undefined;
    this.resolvedCallbacks = [];
    this.rejectedCallbacks = [];

    // 使用箭头函数自动绑定 this
    const resolve = (value) => {
      // 可写可不写,用于当resolve一个promise的时候,会等待该promise执行完毕后再执行
      if (value instanceof MyPromise) {
        return value.then(resolve, reject);
      }
	// 为了实现优先执行Promise后的同步任务,在Promise内部设置宏任务
      setTimeout(() => {
        if (this.state === PENDING) {
          this.state = RESOLVED;
          this.value = value;
          this.resolvedCallbacks.forEach(cb => cb(value));
        }
      });
    };

    const reject = (reason) => {
      setTimeout(() => {
        if (this.state === PENDING) {
          this.state = REJECTED;
          this.value = reason;
          this.rejectedCallbacks.forEach(cb => cb(reason));
        }
      });
    };

    try {
      executor(resolve, reject); // 执行传入Promise的回调函数,向外暴露resolve和reject
    } catch (err) {
      reject(err);
    }
  }

  then(onResolved, onRejected) {
    // 值穿透处理
    onResolved = typeof onResolved === 'function' ? onResolved : v => v;
    onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err };
	// Promise未执行时逻辑
    if (this.state === PENDING) {
      // 返回新 Promise 支持链式调用
      return new MyPromise((resolve, reject) => {
        // 传入回调函数,当Promise状态发生变化的时候,会取出执行
        this.resolvedCallbacks.push((value) => {
          try {
            const result = onResolved(value);
            resolve(result);
          } catch (err) {
            reject(err);
          }
        });

        this.rejectedCallbacks.push((reason) => {
          try {
            const result = onRejected(reason);
            resolve(result);
          } catch (err) {
            reject(err);
          }
        });
      });
    }

    // 状态已确定时直接执行
    return new MyPromise((resolve, reject) => {
      setTimeout(() => {
        try {
          const callback = this.state === RESOLVED ? onResolved : onRejected;
          const result = callback(this.value);
          resolve(result);
        } catch (err) {
          reject(err);
        }
      });
    });
  }
  // catch实则是语法糖
  catch(onRejected) {
    return this.then(null, onRejected);
  }
}

防抖

js
function debounce(fn, wait) {
  let timer = null;
  
  return function(...args) {  // ⭐️ 使用 ...args 替代 arguments 
    const context = this;    // 保存下调用环境下的this(谁调用,this就指向谁)
    
    if (timer) clearTimeout(timer);
    
    timer = setTimeout(() => {   // setTimeout会返回一个计时器id,用于取消计时器
      fn.apply(context, args); // 传递保存的参数
    }, wait);
  };
}

let callDebounce = doubounce(hello,300);
callDebounce('hello');  // hello 就是传入的arguments

节流

js
// 函数节流的实现;
function throttle(fn, delay) {
  let curTime = Date.now();

  return function(...args) {
    let context = this,
        nowTime = Date.now();

    // 如果两次时间间隔超过了指定时间,则执行函数。
    if (nowTime - curTime >= delay) {
      curTime = Date.now();
      return fn.apply(context, args);
    }
  };
}

类型判断

typeof的痛点:typeof null 会返回object,对部分引用类型,返回类型均为object

js
function getType(value) {
  if (value === null) return "null";
  
  const type = typeof value;
  if (type !== "object") return type;
  
  // 引用类型统一处理
  return Object.prototype.toString.call(value) // 该函数会返回[object xxx]
    .slice(8, -1) // 直接截取 "Array"、"Date" 等
    .toLowerCase();
}

map

js
// array.map(value,index,array) map参数
Array.prototype.myMap = function(callbackFn, thisArg) {
  // this 指向调用 myMap 的数组实例
  const arr = this;
  const result = [];

  for (let i = 0; i < arr.length; i++) {
    // 检查数组中是否存在该索引(处理稀疏数组,如 [1, , 3]),这样写可以跳过空值
    if (i in arr) {
      result[i] = callbackFn.call(thisArg, arr[i], i, arr);
    }
  }

  return result;
};

// 使用reduce
Array.prototype.myMap = function(callbackFn, thisArg) {
    return this.reduce((mappedArray, currentValue, index, array) => {
    // 仅当原数组中该索引存在时才添加映射结果,空槽无法通过array.hasOwnProperty(index)
        if (array.hasOwnProperty(index)) {
          mappedArray[index] = callbackFn.call(thisArg, currentValue, index, array);
        }
        return mappedArray;
  }, []);
}

call

js
// call函数实现
Function.prototype.myCall = function(context, ...args) {
  // 类型检查:确保调用 myCall 的是一个函数(this指向)
  if (typeof this !== 'function') {
    throw new TypeError('Not a function');
  }
  // 处理 context:如果为 null/undefined,则指向全局对象(浏览器中是 window)。??操作符:当左侧为null或undefined时返回右侧
  context = context ?? window;
  // 使用唯一的 Symbol 作为属性名,避免冲突
  const fnSymbol = Symbol('fn');
  context[fnSymbol] = this; // 将原函数挂载为 context 的临时方法
  // 执行函数并传递参数,保存结果
  const result = context[fnSymbol](...args);
  // 删除临时属性,避免污染 context 对象
  delete context[fnSymbol];
  return result;
};

apply

js
Function.prototype.myApply = function(context) {
    // 确保调用 myApply 的是一个函数
    if (typeof this !== 'function') {
        throw new TypeError('当前对象不是函数');
    }

    // 处理 context,若为 null/undefined 则指向全局对象
    context = context ?? window;

    // 使用 Symbol 避免属性名冲突
    const fnSymbol = Symbol('fn');
    context[fnSymbol] = this;

    // 获取参数数组(第二个参数为数组或 undefined)
    const args = arguments[1]; // 不传参为undefined

    // 执行函数并传递参数
    let result;
    if (args === undefined || args === null) {
        // 无参数或参数为 null/undefined 时直接调用
        result = context[fnSymbol]();
    } else {
        // 确保参数是数组类型
        if (!Array.isArray(args)) {
            throw new TypeError('第二个参数必须是数组或类数组对象');
        }
        // 使用扩展运算符展开参数数组
        result = context[fnSymbol](...args);
    }

    // 删除临时属性,避免污染 context
    delete context[fnSymbol];

    return result;
};

bind

js
Function.prototype.myBind = function(context, ...presetArgs) {
  const originalFunc = this;
  const boundFunc = function(...callArgs) {
    const allArgs = [...presetArgs, ...callArgs];
    // 使用 Reflect.construct 确保实例继承自 boundFunc.prototype
    if (this instanceof boundFunc) {
      return Reflect.construct(originalFunc, allArgs, boundFunc);
    }
    // 普通调用时绑定 context
    return originalFunc.apply(context, allArgs);
  };
  // 使用 Object.setPrototypeOf 确保 boundFunc 继承 originalFunc 的静态属性
  Object.setPrototypeOf(boundFunc, Object.getPrototypeOf(originalFunc));
  return boundFunc;
};

sleep

需要通过sleep函数来阻塞进程

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

// 使用示例(配合async/await)
async function main() {
  console.log('开始');
  await sleep(1000); // 等待1秒
  console.log('延迟1秒后执行');
  await sleep(500); // 再等待0.5秒
  console.log('结束');
}

main();

函数柯里化

将一个需要传入多个参数的函数,分成可以接受任意数量参数的函数,直到参数数量符合原函数要求后才执行原函数

js
function curry(fn, args) {
  // 获取函数需要的参数长度
  let length = fn.length;

  args = args || [];

  return function() {
    // 浅拷贝参数,从而不影响原先参数
    let subArgs = args.slice();

    // 拼接得到现有的所有参数,arguments是return的函数传入的参数
    for (let i = 0; i < arguments.length; i++) {
      subArgs.push(arguments[i]);
    }

    // 判断参数的长度是否已经满足函数所需参数的长度
    if (subArgs.length >= length) {
      // 如果满足,执行函数
      return fn.apply(this, subArgs);
    } else {
      // 如果不满足,递归返回科里化的函数,等待参数的传入
      // 每次调用该方法都会重走上面的逻辑
      return curry.call(this, fn, subArgs); 
    }
  };
}

// es6 实现
function curry(fn, ...args) {
  return fn.length <= args.length ? fn(...args) : curry.bind(null, fn, ...args);
}

实现浅拷贝

  • Object.assign(target,param1,param2,...)
  • 扩展运算符(...)
  • 数组浅拷贝,两个方法不传入任何参数就是实现数组的浅拷贝
    • .slice()
    • .concat()
js
// 手撕浅拷贝
function shallowCopy(object) {
  // 只拷贝对象
  if (!object || typeof object !== "object") return;

  // 根据 object 的类型判断是新建一个数组还是对象
  let newObject = Array.isArray(object) ? [] : {};

  // 遍历 object,并且判断是 object 的属性才拷贝,如果是object[key],会访问原型链上的属性
  for (let key in object) {
    // 也可以使用obj.hasOwnProperty(key),前提是obj的该方法没有被重写
    if (Object.hasOwnProperty.call(obj, key)) {
      newObject[key] = object[key];
    }
  }

  return newObject;
}

实现深拷贝

  • JSON.parse(JSON.stringify(obj)),但是存在问题,拷贝的对象中如果有函数,undefined,symbol,当使用过JSON.stringify()进行处理之后,都会消失。

  • lodash的_.cloneDeep方法

  • js
    // 深拷贝的实现
    function deepCopy(object) {
      if (!object || typeof object !== "object") return;
    
      let newObject = Array.isArray(object) ? [] : {};
    
      for (let key in object) {
        if (Object.hasOwnProperty.call(obj, key)))) {
          newObject[key] =
            typeof object[key] === "object" ? deepCopy(object[key]) : object[key]; // 如果是对象的话,递归调用
        }
      }
    
      return newObject;
    }

object.assign()

js
// sources为多个对象
Object.myAssign = function(target, ...sources) {
  if (target == null) {
    throw new TypeError('Cannot convert undefined or null to object');
  }
  // 使用Object包装target,避免target是String或者Number
  const result = Object(target);
  // 遍历source,并将属性添加到target上
  sources.forEach(obj => {
    if (obj != null) {
      for (const key in obj) {
        if (obj.hasOwnProperty(key)) {
          result[key] = obj[key];
        }
      }
    }
  });
  return result;
};

数组扁平化

相当于实现es6中的flat功能

js
// 递归实现
let arr = [1, [2, [3, 4, 5]]];
function flatten(arr) {
  let result = [];

  for(let i = 0; i < arr.length; i++) {
    if(Array.isArray(arr[i])) {
      result = result.concat(flatten(arr[i])); // 递归子数组
    } else {
      result.push(arr[i]);
    }
  }
  return result;
}
flatten(arr);  //  [1, 2, 3, 4,5]
js
// 扩展运算符实现
let arr = [1, [2, [3, 4]]];
function flatten(arr) {
    while (arr.some(item => Array.isArray(item))) { // 查看每次遍历的数组是否还有子数组,...操作符可以解构子数组
        arr = [].concat(...arr);
    }
    return arr;
}
console.log(flatten(arr)); //  [1, 2, 3, 4,5]
js
// toString会将数组递归的形成字符串,然后再通过,分割
let arr = [1, [2, [3, 4]]];
function flatten(arr) {
    return arr.toString().split(',');
}
console.log(flatten(arr)); //  [1, 2, 3, 4,5]
js
// 使用reduce
const flat = arr => {
  return arr.reduce((pre, cur) => {
    return pre.concat(Array.isArray(cur) ? flat(cur) : cur); // 递归扩展数组
  }, []);
};
js
// 带deepth,即拍平的层数
// reduce + 递归
function flat(arr, num = 1) {
  return num > 0
    ? arr.reduce(
        (pre, cur) =>
          pre.concat(Array.isArray(cur) ? flat(cur, num - 1) : cur),
        []
      )
    : arr.slice();
}
const arr = [1, 2, 3, 4, [1, 2, 3, [1, 2, 3, [1, 2, 3]]], 5]
flat(arr, Infinity);
// [1, 2, 3, 4, 1, 2, 3, 1, 2, 3, 1, 2, 3, 5 ];

数组去重

js
// es6,使用set和Array.from
const array = [1, 2, 3, 5, 1, 5, 9, 1, 2, 8];
Array.from(new Set(array)); // [1, 2, 3, 5, 9, 8]
js
const array = [1, 2, 3, 5, 1, 5, 9, 1, 2, 8];
uniqueArray(array); // [1, 2, 3, 5, 9, 8]\

// 通过map来唯一
function uniqueArray(array) {
  let map = {};
  let res = [];
  for(var i = 0; i < array.length; i++) {
    if(!map.hasOwnProperty([array[i]])) {
      map[array[i]] = 1;
      res.push(array[i]);
    }
  }
  return res;
}

字符串repeat

js
function repeat(s, n) {
    return (new Array(n + 1)).join(s); // new Array(n+1)会构造一个[,,...,]的数组,可容纳n+1个元素,.join(s)方法可以让数组中的元素以s连接起来,最后构成n个s相连
}

将对象数组转换成树形结构

js
const deptList = [
  { id: 1, name: "总公司", parentId: null },
  { id: 2, name: "技术部", parentId: 1 },
  { id: 3, name: "市场部", parentId: 1 },
  { id: 4, name: "前端组", parentId: 2 },
  { id: 5, name: "后端组", parentId: 2 },
  { id: 6, name: "销售组", parentId: 3 },
];

function arrayToTree(arr) {
  // 辅助函数:查找某个父节点的所有子节点
  function findChildren(parentId) {
    return arr.filter(item => item.parentId === parentId)
      .map(item => {
        const children = findChildren(item.id);
        return { ...item, children }; // 保留item内的属性,同时添加一个children属性,值为子节点
      });
  }

  // 找到所有根节点(parentId 为 null 或 0)
  return findChildren(null);
}

// 使用示例
const tree = arrayToTree(deptList)
[
  {
    id: 1, 
    name: "总公司", 
    parentId: null,
    children: [
      {
        id: 2, 
        name: "技术部", 
        parentId: 1,
        children: [
          { id: 4, name: "前端组", parentId: 2, children: [] },
          { id: 5, name: "后端组", parentId: 2, children: [] }
        ]
      },
      {
        id: 3, 
        name: "市场部", 
        parentId: 1,
        children: [
          { id: 6, name: "销售组", parentId: 3, children: [] }
        ]
      }
    ]
  }
]

红绿灯

js
// 定义Promise
const task = (timer, light) => 
    new Promise((resolve, reject) => {
        setTimeout(() => {
            if (light === 'red') {
                red()
            }
            else if (light === 'green') {
                green()
            }
            else if (light === 'yellow') {
                yellow()
            }
            resolve()
        }, timer)
    })

// 使用.then递归实现
const step = () => {
    task(3000, 'red')
        .then(() => task(2000, 'green'))
        .then(() => task(1000, 'yellow'))
        .then(step) // 循环调用
}
step()

// 使用async/await
const taskRunner =  async () => {
    await task(3000, 'red')
    await task(1000, 'green')
    await task(2000, 'yellow')
    taskRunner()  // 循环调用
}
taskRunner()

约瑟夫环

js
function josephus(n, m) {
    // 常用生成数组方法,第一个参数是生成了一个类数组,第二个参数是mapFn,箭头函数的参数是(值,索引)
    const people = Array.from({ length: n }, (_, i) => i + 1); // 初始化编号1~n
    let index = 0; // 当前报数位置

    for (let i = n; i > 1; i--) { // 直到剩下1人
        index = (index + m - 1) % i; // 计算下一个淘汰者的索引(从0开始)
        people.splice(index, 1); // 淘汰该人
    }
    return people[0]; // 返回幸存者
}
console.log(josephus(5, 3)); // 输出: 4

规定位置写入元素

js
<ul class="ulBox">
    <!-- first -->
    <li><a>链接1</a></li>
    <li><a>链接2</a></li>
    <li><a>链接3</a></li>
    <!-- second -->
</ul>

<script>
    // 获取所有注释节点
    const comments = document.querySelector('.ulBox');
    const place1 = comments.firstChild; // 可以获取到注释(comment节点),nodeType的值为8,nodeName的值为“#comment”,nodeValue的值是注释的内容
    const place2 = comments.lastChild;  // 同理
    // 创建新的li元素
    const newLi1 = document.createElement('li');
    const newLi2 = document.createElement('li');

    // 创建a元素并设置文本内容
    const newA1 = document.createElement('a');
    newA1.textContent = '新链接1';
    const newA2 = document.createElement('a');
    newA2.textContent = '新链接2';

    // 将a元素添加到li元素中
    newLi1.appendChild(newA1);
    newLi2.appendChild(newA2);

    comments.insertBefore(newLi1, place1);
    comments.insertBefore(newLi2, place2);
</script>

随机打乱数组

js
// 洗牌算法
function shuffleArray(array){
    let arr = [...array];
    for(let i=arr.length-1;i>0;--i){
        let j = Math.floor(Math.random()*(i+1))
        [arr[i],arr[j]] = [arr[j],arr[i]]
    }
    return arr
}