「一劳永逸」最全的手写JS面试题

654 阅读17分钟

「这是我参与2022首次更文挑战的第25天,活动详情查看:2022首次更文挑战

前言

  1. 面试中手写题是必考的,大家一定要多写几遍,不要存在侥幸心理
  2. 每一道题目都有测试用例,大家面试中写题目的时候,不要上来就写方法,可以先写测试用例,然后有写不上来的地方,看看测试用例,就好写了
  3. 因为现在typescript的火热,也是未来的趋势,后面我会把每一个函数都改成ts版本的
  4. 如何记忆,其实每一道题目都有其核心的地方,记住核心的地方
  5. 还有就是每一道手写题我们都应该考虑到报错,异常,这是加分项

如何一遍写对手写题

坚持两个习惯

  1. 写手写题之前,要先把测试用例写一下
  2. 然后写完手写题以后,一定要把一个数据或者多个数据带进函数或类中在脑海中验证一遍。

内容

1.实现防抖函数

防抖函数原理:在事件被触发 n 秒后再执行回调,如果在这 n 秒内又被触发,则重新计时

防抖动和节流本质是不一样的。防抖动是将多次执行变为最后一次执行,节流是每隔一段时间只能执行一次

  • 按钮提交场景:防止多次提交按钮,只执行最后提交的一次。

步骤

  1. 建立一个timernull
  2. 返回一个函数,函数的参数为...args,用于接收参数
  3. if存在timerclearTimeout
  4. 如果不存在则执行函数

通过此题可以学到什么?

  1. 为什么要返回一个闭包?闭包是用来保存timer的状态, 如果不是闭包的话, 那么以前的timer都无法消除, 他们也都会执行, 所以什么时候需要使用闭包,就是需要记录状态的时候
  2. 为什么要使用apply?我不使用apply不行么,答案是不行的, 因为args是一个数组,那么接收数组只能用apply
  3. 为什么返回的闭包函数第一个参数是this?在 TypeScript 中,当我们需要明确指定函数中 this 的类型时,可以将 this 作为函数的第一个参数声明,但这并不是一个真正的参数,而是一种类型注释。这是 TypeScript 特有的语法。

要返回一个函数

function myDebounce<T extends (...args: any[]) => void>(fn: T, delay = 1000) {
    let timer: ReturnType<typeof setTimeout> | null = null;
    return function (this: any, ...args: Parameters<T>) {
        const context = this;
        if (timer) {
            clearTimeout(timer);
        }
        timer = setTimeout(() => {
            fn.apply(context, args);
            timer = null; // 避免内存泄漏, 调用后清除定时器引用, 允许垃圾回收
        }, delay);
    }
};

测试代码

function fn(name: string) {
    console.log(name);
}

const btnDebounce = myDebounce(fn, 800);

btnDebounce('Alice');
setTimeout(() => btnDebounce('Bob'), 1000);

在node中使用一下命令执行

npx ts-node myDebounce.ts
node myDebounce.ts       

2.实现节流函数

节流函数原理:规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效

使用场景:

  • 像 dom 的拖拽,如果用防抖的话,就会出现卡顿的感觉,因为只在停止的时候执行了一次,这个时候就应该用节流,在一定时间内多次执行,会流畅很多
  • 缩放场景:监控浏览器resize
  • 攻击角色子弹冷却时间:每一秒中只允许发射一次子弹

步骤

  1. 定义now,preTime都为0
  2. 返回一个函数,函数的参数为...args
  3. 更新now
  4. 如果now-pre> time则执行函数,并更新pre

通过此题可以学到什么?

可以学习闭包的使用

function myThrottle<T extends (...args: any[]) => void>(func: T, wait: number) {
    let preTime = 0;
    return function (this: any, ...args: Parameters<T>) {
        const nowTime = Date.now();
        if (nowTime - preTime >= wait) {
            preTime = nowTime;
            func.apply(this, args);
        }
    }
}

const throttle = myThrottle((name) => {
    console.log('throttle' + name);
}, 1000);

说一下这里面的this指向,每个函数在创建的时候,会确定三个东西,创建变量对象,创建作用域链,确定this指向,throttle的this指向window,因为是window调用的,那么返回的函数中的this也会和父级作用域的一样,指向window

测试代码

throttle('2');
throttle('4');
throttle('1');

3.实现一个简易深拷贝

一句话总结浅拷贝就是新建一个对象

简洁版本

const newObj = JSON.parse(JSON.stringify(oldObj));

局限性:

  • 他无法实现对函数 ,RegExp, Date, Set, Map 等特殊对象的克隆
  • 对象有循环引用,会报错
const a = {val:2};
a.target = a;

拷贝a会出现系统栈溢出,因为出现了无限递归的情况。

export function shallowClone(source) {
  // 要判断是不是对象
  if(source && typeof source === 'object'){
      // 定义一个新的对象
      const target = {};
      // for in 遍历source,但是for in的缺点就是会遍历其原型链上的属性
      for (let i in source) {
         // 所以要判断是不是自己的属性
        if (source.hasOwnProperty(i)) {
          // 重点
          target[i] = source[i];
        }
      }
      return target;
  }
  return source;  
 
}

4.实现一个深拷贝

思路: 调用深拷贝方法,若属性为值类型,则直接返回;若属性为引用类型,则递归遍历。这就是我们在解这一类题时的核心的方法。

基本思路:
// 基本类型
// 引用类型: 可遍历的:set,map,array 不可遍历的:date, regExp, object (复杂类型我们手写除了function和math的其他类型)

function deepClone(obj) {
  if (typeof obj !== "object" || obj === null) {
    return obj;
  }
  let copy = {};

  if (obj.construct === Array) {
    copy = [];
  }
  for (let i in obj) {
    if (obj.hasOwnProperty(i)) {
      copy[i] = deepClone(obj[i]);
    }
  }
  return copy;
}

这只是面试够用版

我们再来看一下 简洁版本

const newObj = JSON.parse(JSON.stringify(oldObj));

局限性:

  • 他无法实现对函数, RegExp,Set, Map 等特殊对象的克隆
  • 对象有循环引用,会报错
let b = {
  a: 1,
  b: function () { },
  c: [],
  d: new Date(),
  e: /\.js$/,
  f: new Set([1, 2]),
  g: new Map(),
}

let x = JSON.parse(JSON.stringify(b));
console.log(x);
// { a: 1, c: [], d: '2022-05-05T14:12:24.157Z', e: {}, f: {}, g: {} }

那我们就来解决这两个问题

1.解决循环应用,那么我们就可以使用 map 就行记录,如果是已经被拷贝过的对象,拿我们就直接返回

function deepClone(obj, map = new Map()) {
  if (typeof obj !== "object" || obj === null) {
    return obj;
  }
  if (map.get(obj)) {
    return obj;
  }
  map.set(obj, true);
  let copy = {};
  if (obj.construct === Array) {
    copy = [];
  }
  for (let i in obj) {
    if (obj.hasOwnProperty(i)) {
      copy[i] = deepClone(obj[i]);
    }
  }
  return copy;
}

问题又来了,map 成了强引用,在程序执行完之前,map 一直不会被垃圾回收掉,拿我们只需要把 Map 替换成 weakMap 就可以了

2.无法实现对函数,RegExp,Date,Set,Map

这些类型分为可比遍历对象和不可遍历对象

判断是否可以遍历

function isIterable(obj){
  return obj !== null && (!!obj[Symbol.iterator] || Object.prototype.toString.call(obj)==='[object Object]'); 
}

然后在针对不同的可遍历的和不可遍历的用不同的操作方法,完整版

我们这个深拷贝考虑了基础类型,Date,RegExp,Map,Set,Object

没有考虑的类型function,Symbol,element

// 考察获取数据类型
function getType(target){
  return Object.prototype.toString.call(target).replace('[object ','').replace(']','').toLowerCase();
}

// 判断是否是引用类型
function isObject(target){
  return target instanceof Object;
}

function isIterable(target:any) {
  return !!target[Symbol.iterator];
}

function getInit(target){
  const Ctor = target.constructor;
  return new Ctor();
}

function deepCloneOtherType(target){
  switch(getType(target)){
    case 'date':
      return new Date(target);
    case 'regexp':
       return new RegExp(target);
    default:
      let target: any = {};
      for (let key in source) {
        if (source.hasOwnProperty(key)) {
          target[key] = deepCopy(source[key]);
        }
      }
      return;
  }
}

function deepClone(source: any): any {
  if (!isObject(source)) {
    return source;
  }
  if (isIterable(source)) {
    let res = getInit(source);
    // 如果是map类型
    if (getType(source) === 'map') {
      source.forEach((value: any,key: any) => {
        res.set(key,deepCopy(value));
      })
    };

    if (getType(source) === 'set') {
      source.forEach((value: any) => {
        res.add(value);
      })
    }
    for (let index of source) {
      res.push(deepCopy(source[index]));
    }
    return res;
  } else {
    return deepCloneOtherType(source);
  }
}

测试数据

let b = {
  a: 1,
  c: [],
  d: new Date(),
  e: /\.js$/,
  f: new Set([1, 2]),
  g: new Map(),
}
let x = deepCopy(b);
console.log(x.f === b.f);

5.实现一个 call 函数

作用:改变函数中的 this 指向,并执行函数 重点中的重点:相当于给call函数小括号中的第一个参数,加了一个fn属性,然后执行fn属性 注意点:

  • 挂载到Function原型: 一定要挂载到Function.property上,因为引用类型的property上都有Function,这样才能使用Function上面的方法
  • 处理上下文参数: 如果未提供上下文,使用globalThis
  • 临时属性: 使用symbol创建唯一属性名,避免覆盖上下文原有属性
Function.prototype.myCall = function (_this, ...rest) {
  const context = _this || globalThis;
  const fnKey = Symbol('fn');
  context[fnKey] = this;
  const result = context[fnKey](...rest);
  Reflect.deleteProperty(context, fnKey);
  return result;
}

测试

function getName(age) {
  this.name = 'ws';
  this.age = age;
  return this;
}

let obj = {

}

console.log(getName.myCall(obj,'2'));// { name: 'ws', age: '2' }

6.实现 apply 方法

注意点:apply 方法和 call 方法的不同其实就是参数的不同,所以我们能轻易的写出 apply 方法

Function.prototype.myApply = function (_this, args) {
  const context = _this || globalThis;
  const fnKey = Symbol('fn');
  context[fnKey] = this;
  const result = context[fnKey](...args);
  Reflect.deleteProperty(context, fnKey);
  return result;
}

测试代码

function getName(age) {
  this.name = 'ws';
  this.age = age;
  return this;
}

let obj = {

}


console.log(getName.myApply(obj,[1,2,3]));// { name: 'ws', age: [ 1, 2, 3 ] }

7.实现 bind 方法

注意点

  • 是一个闭包
  • apply必须在闭包内,否则就执行了
  • 两个可以传参数的地方,参数是字符串
Function.prototype.myBind = function (context, ...args) {
  if (typeof this !== 'function') {
    throw new TypeError('Error');
  }
  const fn = this;
  return function (...innerArgs) {
    console.log(this); // window
    return fn.apply(context, args.concat(innerArgs));
  }
}

8.实现 EventEmitter

EventEmitter 既是 node 中各个模块的基石,又是前端组件通信的依赖手段之一,同时涉及了订阅-发布设计模式,是非常重要的基础

思路:

  • 步骤一: 存储事件的数据结构,为了方便查找,使用对象(字典),this._cache
  • 步骤二:emit 就是执行对应的数组
class EventEmitter {
  protected eventList: {};
  constructor() {
    this.eventList = {};
  }

  // subscribe
  on(event, callback) {
    // first judge whether the event exist
    if (this.eventList[event]) {
      this.eventList[event] = [...this.eventList[event], callback];
    } else {
      this.eventList[event] = [callback];
    }
  }

  // publish
  emit(event) {
    // whether the event exist
    if (this.eventList[event]) {
      const callbacks = this.eventList[event];
      callbacks.forEach((callback) => {
        callback();
      });
    }
  }

  remove(event) {
    if (this.eventList[event]) {
      delete this.eventList[event];
    }
  }

  once(event) {
    this.emit(event);
    this.remove(event);
  }
}



测试用例

const event1 = new EventEmitter();
event1.on('input', function input(data) {
  console.log('输入'+data);
});

event1.on('output', function output(data) {
  console.log('输出'+data);
});

console.log(event1);

event1.emit('input', 1)
event1.emit('output',2)

5月7日思考

我的callback都是匿名的,他是如何知道判断是否存在的,以及如何解绑的?

匿名函数的地址都是不一样的,所以没法判断是否存在,还有解绑

所以要想判断是否存在,必须要使地址一样

const handler = (name: string, age: number) => {
  console.log(`再来一个${name}${age}`);
}

const event1 = new EventEmitter();
event1.on('tap', () => {
  console.log('你好');
})
event1.on('tap', handler)
event1.on('tap', handler)
event1.emit('tap','ws', 10);
event1.off('tap', handler)
event1.emit('tap');

9.实现 instanceOf 方法

原理实例的__proto__ = 实例的构造函数(也就是类)的prototype

注意是__proto__prototype进行比较

思路:

  • 步骤 1:先取得当前类的原型,当前实例对象的原型链

  • 步骤 2:一直循环(执行原型链的查找机制)

    • 取得left的__proto__(函数和对象都有)
    • 只要proto有就一直向上找,直到找到为止返回true,没找到返回false

我们可以学到什么:

  • 学到原型链原理
  • getPrototypeOf的使用
 const instanceOf = function (left,right) {
     // 获取的是__proto__
    let proto = Object.getPrototypeOf(left);
    // 只要不为null就一直寻找
    while (proto) {
       // 这句话是最重要的,实例的__proto__= 实例的构造函数的prototype
        if (proto === right.prototype) {
            return true;
        }
        proto = Object.getPrototypeOf(proto);
     }
    return false;
}

测试用例
instanceof主要用于复杂类型检测

const a = ['10'];
console.log(MyInstanceOf(a,Array));

10.实现 new

细心的同学会发现 new 和继承实现上很相近,继承请看第 11

继承属性:用 call 或者 apply 都可以 继承方法并返回一个新的对象: const instance = Object.create(fn.prototype);Object.create 是创建了父类原型的副本,与父类原型完全隔离 判断是否是对象:返回不同的值

默认情况下函数的返回值为undefined(即没有显示地定义返回值的话),但是构造函数除外,new构造函数在没有return的情况下默认返回新创建的对象。但是在有显示返回值的情况下,如果返回值为基本数据类型的话(string,number,boolean,undefined,null),返回值仍然为新创建的对象,这一点比较奇怪,需要注意。只有在显示返回一个非基本数据类型的对象的时候,函数的返回值才为指定的对象。在这种情况下,this值所引用的对象就被丢弃了。

function myNew(fn,...args){
   // 继承prototype
  const instance = Object.create(fn.prototype);
  // fn内部如果有return,
  // 继承属性
  const res = fn.apply(instance,args);
  // 此处注意如果构造函数返回的不是object,那么就返回instance
  return typeof res ==='object'?res:instance;
}

测试用例

const p = myNew(Person,'ws');
console.log(p.name);

2022年3月2日思考

function Person(name){
    this.age=12;
}

Person.prototype.eat = function(){
    console.log("吃饭");
}

构造函数内部的age是如何拿到的?eat是如何拿到的?是如何通过new实现的?

11.模拟 object.create

Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的 __proto__

  • object.create 的作用就像尤里复仇里面的复制中心,进去里面一个大兵,就能出来一个具有以前属性和方法的大兵, 所以继承prototype必备
// 模拟 Object.create

function create(proto) {
  function F() {}
  F.prototype = proto;
  const obj = new F();
  // 此时也就具备了obj.__proto__ = obj.constructor.prototype =  F.prototype = proto;
  return obj;
}

测试用例

function Person(name:string) {
  this.name = name;
}

Person.prototype.study = function () {
  console.log('学习');
}

const instance = myCreate(Person.prototype);
instance.study();

3月6日思考

Object.setPrototypeOf(obj, prototype)他是将prototype作为已知对象obj的原型

Object.create(prototype)是创建一个以prototype为原型的对象

12. 实现类的继承

思路:

  • 继承属性:通过 call 拿到父类的属性值
  • 继承方法并返回一个新的对象:child.prototype=Object.create(Parent.prototype),Object.create 是创建了父类原型的副本,与父类原型完全隔离
  • 更改子类的构造函数
  • 一想到继承或者new方法的实现,其实就是更改this指向和prototype
function Parent() {
  this.name = "parent";
  this.play = [1, 2, 3];
}
function Child() {
  Parent.call(this);
  this.type = "child";
}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;

14.实现迭代器生成函数

Iterator(迭代器)是一种接口,也可以说是一种规范。为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署Iterator接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。

iterator语法

const obj = {
    [Symbol.iterator]:function(){}
}

原型部署了Iterator接口的数据结构有三种,具体包含四种,分别是数组,类似数组的对象,Set和Map结构

为什么对象(Object)没有部署Iterator接口呢?

  • 一是因为对象的哪个属性先遍历,哪个属性后遍历是不确定的,需要开发者手动指定。然而遍历遍历器是一种线性处理,对于非线性的数据结构,部署遍历器接口,就等于要部署一种线性转换
  • 对对象部署Iterator接口并不是很必要,因为Map弥补了它的缺陷,又正好有Iterator接口
function iteratorGenerator(list) {
  let index = 0;
  return {
      next:()=> {
        if (index < list.length) {
          return {
            value: list[index++],
            done: false
          };
        } else {
          return {value: undefined,done: true}
        }
      }
   }  
}

测试用例

let obj = {
  data: ['hello', 'world'],
  [Symbol.iterator](){
    let index = 0;
    return {
      next:()=> {
        if (index < this.data.length) {
          return {
            value: this.data[index++],
            done: false
          };
        } else {
          return {value: undefined,done: true}
        }
      }
    }
  }
}

let iterator = obj[Symbol.iterator]();
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());

15.const 定义常量实现

思路: 由于 ES5 环境没有 block 的概念,所以是无法百分百实现 const,只能是挂载到某个对象下,要么是全局的 window,要么就是自定义一个 object 来当容器

const 的核心特性是:声明的变量不能被重新赋值,且声明的基本类型值不能被修改(对于引用类型,其引用地址不可变,但内部属性可修改)

  • 通过defineProperty实现
function _const(key, value) {
  globalThis[key] = value;
  Object.defineProperty(globalThis, key, {
    configurable: false,
    get: () => {
      return value;
    },
    set: () => {
      throw new TypeError('Assignment to constant variable');
    }
  })
}

测试用例

_const('name', 'ws');
// console.log(name);
name = 'dq'; // 报错


_const('name', {a: 'ws'});
// console.log(name);
name.a = 'cc';
console.log(name);
  • 通过proxy实现实现
// 创建一个不可修改的代理对象
function createConstProxy(initialObj) {
  return new Proxy(initialObj, {
    // 拦截属性赋值操作
    set(target, key, value) {
      // 如果属性已存在,则禁止修改
      if (Reflect.has(target, key)) {
        throw new TypeError(`Assignment to constant variable: ${key}`);
      }
      // 允许添加新属性(可选,根据需求调整)
      Reflect.set(target, key, value);
      return true;
    },
    // 拦截属性删除操作
    deleteProperty(target, key) {
      if (Reflect.has(target, key)) {
        throw new TypeError(`Delete of an unmodifiable property: ${key}`);
      }
      return true;
    }
  });
}

// 使用示例
const container = createConstProxy({
  num: 100,
  obj: { name: 'test' }
});

// 尝试修改已有属性(会抛出错误)
try {
  container.num = 200;
} catch (e) {
  console.log(e.message); // 输出:"Assignment to constant variable: num"
}

// 引用类型的引用地址不可修改,但内部属性可改
try {
  container.obj = { age: 20 }; // 尝试修改引用,抛出错误
} catch (e) {
  console.log(e.message); // 输出:"Assignment to constant variable: obj"
}
container.obj.name = 'newTest'; // 允许修改内部属性
console.log(container.obj.name); // 输出:'newTest'

// 尝试删除属性(会抛出错误)
try {
  delete container.num;
} catch (e) {
  console.log(e.message); // 输出:"Delete of an unmodifiable property: num"
}

17.获取数据类型

这个题目很简单,3行代码搞定

export const getType = function (obj) {
    return Object.prototype.toString.call(obj).replace('[object ', '').replace(']', '').toLowerCase();
};

18.实现 map 方法

map方法可以接受两个参数:第一个参数callback是一个函数,这个参数是必需的。callback中有三个可选参数,分别是当前的正在处理的数组元素、当前处理元素的索引、调用map方法的数组;第二个参数是可选的,它的作用是用来定义执行callback时的this指向。

2022年3月17日思考
我们写方法的时候一定要注意不要用箭头函数,我觉得箭头函数最好在类里面用,类外面就最好不要用箭头函数

2026年1月11日思考 在ts中function的第一个参数是this

2026年1月12日思考
interface Array<T>{ myMap<U>(callback: (value: T, index: number, array: T[]) => U, thisArg?: any): U[]; } 为什么要用T,U两个值? T是数组元素的类型,U是myMap结果中数组的类型,代表两个类型是不一致的

interface Array<T>{
  myMap<U>(callback: (value: T, index: number, array: T[]) => U,   thisArg?: any): U[];
}

Array.prototype.myMap = function <T, U>(
  callback: (value: T, index: number, array: T[]) => U,
  thisArg?: any): U[] {
  const result: U[] = [];
  const array = this;
  for (let i = 0; i < this.length; i++) {
    // 使用call绑定回调的this指向thisArg,传递三个核心参数
    const mappedValue = callback.call(thisArg, array[i], i, array);
    result.push(mappedValue);
  }
  return result;
}

const obj = {
  name: '张三',
};
const arr = [1, 2, 3];
console.log(arr.myMap(function(this: { name: string }, item) {
  return item * 2 + this.name;
}, obj));

19.实现 reduce 方法

核心思想:它的核心作用是将一个数组 “缩减”(reduce)为单个值,虽然 reduce 也能做一些看似不是 “缩减” 的事(比如将数组转为对象),但核心逻辑依然是 “逐步合并”

思路:

  • 1.初始值不传怎么办? 如果传的话,默认使用初始值,如果不传的话,那么使用数组的第一项
  • 2.回调函数的参数有哪些,返回值如何处理。 把初始值、当前值、索引、当前数组返回去。调用的时候传到函数参数中

3月17日思考:
其实这道题目的难点是建立一个startIndex,可能猛一想想不起来,这个要注意


interface Array<T> {
  MyReduce(callbackfn: (pre: T, cur: T, currentIndex: number, array: T[]) => T, initialValue: T): T;
}

Array.prototype.myReduce = function(callback, init){
  let arr = this;
  // 因为每次都需要上一次的值,所以要提前定义
  let result = init || arr[0];
  let startIndex = init ? 0 : 1;
  for(let i = startIndex; i<arr.length; i++){
    result = callback(result, arr[i], i, arr);
  }
  return result;

}

测试

let arr = [1, 3, 3, 5, 6];
console.log(arr.MyReduce((pre, cur) => pre + cur, 0));

20.实现 flat 方法

思路
方法一:

  • 1.使用递归
  • 2.递归返回的结果进行合并
interface Array<T> {
    myFlat(): T[];
}


Array.prototype.myFlat = function <T>() {
    const result: T[] = [];
    const arr = this;
    for (let i = 0; i < arr.length; i++) {
        if (Array.isArray(arr[i])) {
            result.push(...arr[i].myFlat());
        } else {
            result.push(arr[i]);
        }
    }
    return result;
}

const arr1 = [1, 2, [3, 4, [5, 6]]];
console.log(arr1.myFlat());

3月16日思考:
数组方法题的模板是这个样子

Array.prototype.myXXX = function(callback){
    const arr = this;
    let result = [];//有的不是返回数组
    for(let i=0;i<arr.length;i++){
        // 对callback进行处理
    }
    return result;
}

方法二:

Array.prototype.myFlat = function () {
  const arr = this;
  return arr.reduce((pre, cur) => {
    if (Array.isArray(cur)) {
      return pre.concat(cur.myFlat());
    } else {
      return [...pre, cur];
    }
  },[])
}

测试用例

let a = [1,[3,4],2];
console.log(a.myFlat());

21.对象扁平化

思路

1.实现思路同上 2.只是实现方法不一样

Object.prototype.flatObj = function(){
  let obj = this;
  let result = {};
  Object.entries(obj).forEach((item)=>{
    if(Object.prototype.toString.call(item[1])==='[object Object]') {
      let res = item[1].flatObj();
      result = {...result,...res};
    } else {
      result[item[0]] = item[1];
    }
  })
  return result;
}

22.实现 compose 函数(本质是一个中间件)

思路:

compose是用来把多个函数组合成一个,返回一个函数,那么应该用什么来实现呢?用reduce最简单,因为你return返回什么类型,最后结果就是什么类型

组合多个函数,从左到右,比如:compose(f, g, h) 最终得到这个结果 (...args) => f(g(h(...args))).

注意: 返回一个函数

function compose(...funcs) {
  // 判断如果funs只有一个的时候
  if (funcs.length === 1) {
    return funcs[0];
  }
  // 使用reduce来执行多个函数
  return funs.reduce((a, b) => {
      // 肯定要返回一个函数,里面的函数先执行,后面的函数后执行
    return (...args) => b(a(...args));
  })
}

测试代码

let sayHello = (...str:string[]) => `Hello , ${str.join(" And ")}`;
let toUpper = (str:string) => str.toUpperCase();
let merge = compose(
  sayHello,
  toUpper
);

console.log(merge("jack", "bob"));
// HELLO , JACK AND BOB

23.Object.is 方法

思路:Object.is 和===差不多,都是用来对比两个值是否相等的

首先是 Object.is 和===区别是什么

  • NaN 在===是不相等的,在 Object.is 是相等的
  • +0 和-0 在===是相等的,在 Object.is 是不相等的
/**
 * 实现 Object.is 的 TS 版本
 * 核心差异:NaN === NaN 为 true;+0 !== -0 为 true;其余与 === 一致
 * @param a 比较值 A(支持任意类型)
 * @param b 比较值 B(支持任意类型)
 * @returns 两值是否严格相等(按 Object.is 规则)
 */
function myObjectIs(a: unknown, b: unknown): boolean {
  // 1. 处理 NaN 特殊情况:Number.isNaN 仅对 NaN 返回 true(比全局 isNaN 更精准)
  if (Number.isNaN(a) && Number.isNaN(b)) {
    return true;
  }

  // 2. 处理 +0 / -0 特殊情况:利用 1/+0 = Infinity,1/-0 = -Infinity 的特性
  if (a === 0 && b === 0) {
    // 类型断言:已确认 a/b 是 0(number 类型),安全转换
    return 1 / (a as number) === 1 / (b as number);
  }

  // 3. 其他情况:直接复用 === 的判定逻辑
  return a === b;
}

24.请用setTimeout实现setInterval

setTimeout(function () {
 console.log('我被调用了');
 setTimeout(arguments.callee, 100);
}, 100);

callee 是 arguments 对象的一个属性。它可以用于引用该函数的函数体内当前正在执行的函数。在严格模式下,第5版 ECMAScript (ES5) 禁止使用arguments.callee()。当一个函数必须调用自身的时候, 避免使用 arguments.callee(), 通过要么给函数表达式一个名字,要么使用一个函数声明.

setTimeout(function fn() {
    console.log('我被调用了');
    setTimeout(fn, 1000);
},1000)

25.判断是否是引用类型

截止到2022年3月25日,前端基本类型一共有七种类型,分别是undefined,null,Number,string,Boolean,Symbol,BigInt

复杂类型有一种object

  1. 判断类型的方法中typeof主要判断基本类型的,instanceof主要判断复杂类型的
  2. 所有复杂数据类型的原型链都会指向Object

所以判断是否是引用类型就很好写

function isObject(target){
  return target instanceof Object;
}

26.判断是否可遍历

Iterator(迭代器)是一种接口,也可以说是一种规范。为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署Iterator接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)

所以我们判断一个数据结构是否可以遍历就看这个数据结构的是否含有[Symbol.iterator]了,含有[Symbol.iterator]属性也就证明可以使用for...of...进行遍历,没有则不行

(拓展知识点:每个对象都有一个 propertyIsEnumerable 方法。此方法可以确定对象中指定的属性是否可以被 for...in 循环枚举)

含有[Symbol.iterator]属性的数据结构有Array,Set,Map,是没有对象的,所以判断是否可以遍历还要加上对象

function isIterable(obj){
  return obj !== null && (!!obj[Symbol.iterator] || Object.prototype.toString.call(obj)==='[object Object]'); 
}

27.请实现一个add函数,满足以下功能

先了解一个知识点:
1.调用函数加括号 fn() :执行函数体fn,执行后得到其返回值;
2.调用函数不加括号 fn:不会执行函数体,而是得到函数体的源码。函数名其实就是指向函数的指针,它只是传递了函数体所在的地址位置,在需要执行时找到函数体去执行。

“在Function需要转换为字符串时,通常会自动调用函数的 toString 方法,toString() 方法返回一个表示当前函数源代码的字符串。”

上面的引用来自:mdn关于Function.prototype.toString的详解

add(1); 			// 1
add(1)(2);  	// 3
add(1)(2)(3);// 6
add(1)(2, 3); // 6
add(1, 2)(3); // 6
add(1, 2, 3); // 6

分析一下题目的要求

  1. 要能保存参数,需要定义一个函数,每次返回这个函数,为了能每次都保存新的args
  2. 要能实现链式调用
  3. 最后触发,需要隐式调用
function add(...args) {
  function fn() {
    args = args.concat([...arguments]);
    return fn;
  }
  fn.toString = function () {
    return args.reduce((pre, cur) => pre + cur);
  }
  return fn;
}

测试用例

console.log(+add(1));//1
console.log(+add(1,2));//3
console.log(+add(1)(2))//3

28.使用 ReactDom.createPortal 开发一个自定义Modal弹窗

传送门

29.实现虚拟列表

传送门

30.实现懒加载

传送门

32.实现双向绑定

let data = { name: 'my name', age: 12 };
observe(data);
function observe(data) {
   // 首先检查是否有效
  if (!data || typeof data !== 'object') {
    return;
  }
  // 对于每个属性名,进行dataChange处理
  Object.keys(data).forEach((key) => {
    dataChange(data,key,data[key]);
  })
}

function dataChange(data, key, value) {
  // 递归调用observe函数, 防止value是对象的情况
  observe(value);
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    // 在访问对应key值的时候触发
    get: function () {
      return value;
    },
    // 在设置对应key值的时候触发
    set: function (newValue) {
      console.log('data发生了变化', newValue);
      value = newValue;
    }
  })
}

实现数据监听defineProperty

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<input type="text" id="input">
<div id="text"></div>
</body>
<script>
    const input = document.getElementById("input");
    const text = document.getElementById("text");
    const global = {
        text: '',
    };
    function watch(key, callback){
      const value = global[key];
      Object.defineProperty(global,key,{
        get:function () {
        // 千万不要在这里写global[key],否则会无线循环
            return value;
        },
        set:function (val) {
            callback(val);
                     
        }
    })
    }
   
    
    input.addEventListener('input',function (e) {
        global.text = e.target.value;
    })
    watch('text',(val) => {
      text.innerText = val;
    })

    
</script>
</html>

实时监听数组proxy

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <meta name="viewport" content="width=device-width,height=device-height"/>
</head>
<body>
<input type="text" id="input">
<div id="text"></div>
</body>
<script>
    const input = document.getElementById("input");
    const text = document.getElementById("text");
    let global = {
      text: ''
    };
    function watch(obj, callback){
      let handler = {
        get: function(target, key){
          return Reflect.get(target, key);
        },
        set: function(target, key, value){
          callback(target, key, value);
          return Reflect.set(target, key, value);
        }
      }
      return new Proxy(obj, handler);
    }
   
    
  

    let p = watch(global, (target, key, value) => {
      if(key === 'text'){
          text.innerText = value;
      }
      
    })

    input.addEventListener('input',function (e) {
      p.text=e.target.value;
    })

    
</script>
</html>

33.redux中间件

35.数组去重

set方法去重

function unique(arr) {
  return Array.from(new Set(arr));
}

36.对象数组去重

  1. 肯定要返回一个新的数组,因为我们要筛选,所以filter最符合语义
  2. 再用一个set数组记录是否有这个key(为什么用set,setarray的区别就是set专门用来判断数组中有没有这个值的)
function unique(arr, key){
  let temp = new Set();
  let newArray = arr.filter((item)=>{
    if(temp.has(item[key])){
      return false;
    }
    temp.add(item[key]);
    return true;
  })
  return newArray;
}
var resources = [
  { name: "张三", age: "18" },
  { name: "张三", age: "19" },
  { name: "张三", age: "20" },
  { name: "李四", age: "19" },
  { name: "王五", age: "20" },
  { name: "赵六", age: "21" }
]
console.log(unique(resources, 'name'));

36.手动实现每次访问一个属性时,值加一,使得a==1&&a==2&a==3成为可能

let b = 0;
Object.defineProperty(globalThis, 'a', {
  get(){
    b++;
    return b;
  }
})

测试用例

if (a == 1 && a == 2 && a == 3) {
  console.log(111);
}

39. 使用requestAnimationFrame实现倒计时

  • setTimeout,setInterval是js线程的
  • requestAnimationFrame 是GUI线程的

在加载一张很大的图,用setInterval制作的倒计时会出现卡顿然后突然加速的情况,原因在于JS里的JS线程和GUI线程是互斥的,如果在执行GUI线程很久,会对JS线程进行阻塞

 let animationRef = null;
  let count = 3;
  let pre = 0;
  function animation(){
      let now = new Date();
      if(now-pre>1000){
        count--;
        console.log(count);
        pre = new Date;
      }
      if(count<=0){
        console.log(count);
        cancelAnimationFrame(animationRef);
        // 必须要有return,否则不会结束
        return;
      }
      animationRef = requestAnimationFrame(animation);
  }
  animationRef = requestAnimationFrame(animation);

测试用例

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <input type="text" id="input"/>
  <div id="text"></div>
</body>
<script>
let text = document.querySelector("#text");
let animationRef = null;
let count = 10;
let pre = 0,now = 0;
animationRef = requestAnimationFrame(function (){
  now = new Date();
  if(now-pre>1000){
    count--;
    text.innerHTML=count;
    pre = new Date();
  }
  if(count<=0){
    cancelAnimationFrame(animationRef);
    return;
  }
  animationRef = requestAnimationFrame(arguments.callee)
})

</script>
</html>

41. input千分符分割金额

number.toLocaleString("en-US")

42. URL.createObjectUrl文件上传,文件下载,文件预览

  • 我们先来看一下URL.createObjectURL的返回值是什么,返回值是一个Blob URL

Blob URLBlob Uniform Resource Locator)用于在浏览器中处理二进制数据,特别是在使用 JavaScript 进行文件操作时。它提供了一种将二进制数据转换为可访问的 URL 的机制,从而可以更方便地对二进制数据进行处理和操作。

blob url可以放到imagevideo, audioa标签中,方便文件预览和下载

File对象可以看作一种特殊的Blob(binary large object)对象

Blob下载文件

我们可以通过window.URL.createObjectURL,接收一个Blob(File)对象,将其转化为Blob URL,然后赋给 a.download属性,然后在页面上点击这个链接就可以实现下载了

<!-- html部分 -->
<a id="h">点此进行下载</a>
<!-- js部分 -->
<script>
  var blob = new Blob(["Hello World"]);
  var url = window.URL.createObjectURL(blob);
  var a = document.getElementById("h");
  a.download = "helloworld.txt";
  a.href = url;
</script>

Blob图片本地显示

window.URL.createObjectURL生成的Blob URL还可以赋给img.src,从而实现图片的显示

<!-- html部分 -->
<input type="file" id='f' />
<img id='img' style="width: 200px;height:200px;" />
<!-- js部分 -->
<script>
  document.getElementById('f').addEventListener('change', function (e) {
    var file = this.files[0];
    const img = document.getElementById('img');
    const url = window.URL.createObjectURL(file);
    img.src = url;
    img.onload = function () {
        // 释放一个之前通过调用 URL.createObjectURL创建的 URL 对象
        window.URL.revokeObjectURL(url);
    }
  }, false);
</script>

Blob文件分片上传

通过Blob.slice(start,end)可以分割大Blob为多个小Blob

xhr.send是可以直接发送Blob对象的

前端

<!-- html部分 -->
<input type="file" id='f' />
<!-- js部分 -->
<script>
  function upload(blob) {
    var xhr = new XMLHttpRequest();
    xhr.open('POST', '/ajax', true);
    xhr.setRequestHeader('Content-Type', 'text/plain')
    xhr.send(blob);
  }
  document.getElementById('f').addEventListener('change', function (e) {
    var blob = this.files[0];
    const CHUNK_SIZE = 20; .
    const SIZE = blob.size;
    var start = 0;
    var end = CHUNK_SIZE;
    while (start < SIZE) {
        upload(blob.slice(start, end));
        start = end;
        end = start + CHUNK_SIZE;
    }
  }, false);
</script>

43.reduce用法

语法

array.reduce(function(total, currentValue, currentIndex, arr), initialValue);
/*
  total: 必需。初始值, 或者计算结束后的返回值。
  currentValue: 必需。当前元素。
  currentIndex: 可选。当前元素的索引;                     
  arr: 可选。当前元素所属的数组对象。
  initialValue: 可选。传递给函数的初始值,相当于total的初始值。
*/

reduceRight() 该方法用法与reduce()其实是相同的,只是遍历的顺序相反,它是从数组的最后一项开始,向前遍历到第一项

数组求和

const arr = [12, 34, 23];
const sum = arr.reduce((total, num) => total + num);

// 设定初始值求和
const arr = [12, 34, 23];
const sum = arr.reduce((total, num) => total + num, 10);  // 以10为初始值求和


// 对象数组求和
var result = [
  { subject: 'math', score: 88 },
  { subject: 'chinese', score: 95 },
  { subject: 'english', score: 80 }
];
const sum = result.reduce((accumulator, cur) => accumulator + cur.score, 0); 
const sum = result.reduce((accumulator, cur) => accumulator + cur.score, -10);  // 总分扣除10分

数组最大值

const a = [23,123,342,12];
const max = a.reduce((pre,next)=>pre>cur?pre:cur,0); // 342

数组转对象

var streams = [{name: '技术', id: 1}, {name: '设计', id: 2}];
var obj = streams.reduce((accumulator, cur) => {accumulator[cur.id] = cur; return accumulator;}, {});

扁平一个二维数组

var arr = [[1, 2, 8], [3, 4, 9], [5, 6, 10]];
var res = arr.reduce((x, y) => x.concat(y), []);

数组去重

var newArr = arr.reduce(function (prev, cur) {
    prev.indexOf(cur) === -1 && prev.push(cur);
    return prev;
},[]);

对象数组去重

const dedup = (data, getKey = () => { }) => {
    const dateMap = data.reduce((pre, cur) => {
        const key = getKey(cur)
        if (!pre[key]) {
            pre[key] = cur
        }
        return pre
    }, {})
    return Object.values(dateMap)
}

求字符串中字母出现的次数

const str = 'sfhjasfjgfasjuwqrqadqeiqsajsdaiwqdaklldflas-cmxzmnha';

const res = str.split('').reduce((pre,next)=>{
 pre[next] ? pre[next]++ : pre[next] = 1
 return pre 
},{})
// 结果
-: 1
a: 8
c: 1
d: 4
e: 1
f: 4
g: 1
h: 2
i: 2
j: 4
k: 1
l: 3
m: 2
n: 1
q: 5
r: 1
s: 6
u: 1
w: 2
x: 1
z: 1

44.简单实现jest

  1. 写一个期望函数 I expect findMax([1, 2, 4, 3]) to be 4;
  2. 再写一个test函数,第一个参数是描述信息,第二个函数式测试函数
function test(msg, func) {
  try {
    func();
    console.log(`${msg}测试通过`);
  } catch (error) {
    console.log(`${error}测试不通过`);
  }
}

function expect(value) {
  return {
    toBe: (toBeValue) => {
      if (toBeValue === value) {
        console.log('测试通过');
      } else {
        throw new Error('测试不通过');
      }
    }
  }
}

测试用例

function findMax(arr) {
  return Math.max(...arr);
}

test('findMax函数输出', ()=>{
  expect(findMax([1,2,4])).toBe(4);
});

45.用JavaScript编写一个函数,实现精确计算两个浮点数的和

这道题目我们可以学到以下知识点

  1. 为什么0.1+0.2不等于0.3
  2. 如何使用isFinite函数
  3. 如何使用padEnd函数
  4. 如何使用bigInt函数
  5. bigInt如何转化为number
  6. 如何使用**运算符

第一个问题为什么0.1+0.2!==0.3?

产生原因

计算机中数字都是以二进制存储的,而小数在计算机中并不能精确的表示出来,只能是一个近似值,所以在进行计算的时候就会出现误差。

  • 0.1​ 转二进制 → 无限循环小数 0.0001100110011...(像π的小数位永无止境) ​- 0.2​ 转二进制 → 也是无限循环 0.001100110011... 然后再把二进制转化为十进制,相加后得到 0.30000000000000004
计算过程

0.1和0.2先转化为二进制,得出一个无限循环小数,再转化为十进制,相加后得到一个无限不循环的小数,最后转化为二进制。

/**
 * 精确计算两个浮点数的和(仅处理普通小数,不考虑科学计数法)
 * @param {number} a - 第一个浮点数(如 0.1, 1.23)
 * @param {number} b - 第二个浮点数(如 0.2, 4.56)
 * @returns {number} 精确的和
 */
function accurateAdd(number1, number2) {
    if (isNaN(number1) || isNaN(number2)) {
        return NaN;
    }
    if (!isFinite(number1) || !isFinite(number2)) {
        return Infinity
    }
    const [integer1, decimal1 = ''] = number1.toString().split('.');
    const [integer2, decimal2 = ''] = number2.toString().split('.');
    const length1 = decimal1.length;
    const length2 = decimal2.length;
    const maxLength = Math.max(length1, length2);
    const dec1 = Number(decimal1.padEnd(maxLength, '0'));
    const dec2 = Number(decimal2.padEnd(maxLength, '0'));
    const int1 = Number(integer1) * Math.pow(10, maxLength);
    const int2 = Number(integer2) * Math.pow(10, maxLength);
    const sum = BigInt(int1 + int2 + dec1 + dec2);
    return Number(sum) / Math.pow(10, maxLength);
}


// 测试案例
console.log(accurateAdd(0.1, 0.2)); // 0.3
console.log(accurateAdd(0.001, 0.002)); // 0.003
console.log(accurateAdd(1.234, 5.678)); // 6.912
console.log(accurateAdd(10, 20.5)); // 30.5
console.log(accurateAdd(-0.1, 0.3)); // 0.2
console.log(accurateAdd(123.45, 67.89)); // 191.34

总结

  • 1.写方法很重要的一点就是要记住是如何用的,其实有时候很多同学都不知道某些方法如何使用,总是用的时候在百度,这就像英语老师让你背单词,你记不住,说字典上有,用的时候会查字典一样,那考试的时候怎么办呢?

参考