面试题 随记

82 阅读10分钟

总结自己学习过程中遇到的面试题 会持续补充

手写一个防抖 / 节流函数

防抖

防止抖动,单位时间内事件触发会被重置,业务场景有避免登录按钮多次点击的重复提交。

function debounce(fn, wait) {
  let timer;
  return function () {
    let _this = this;
    let args = arguments;
    if (timer) {
      clearTimeout(timer);
    }
    timer = setTimeout(function () {
      console.info('------', args);
      fn.apply(_this, args);
    }, wait);
  };
}

节流

节流 触发后 触发一次时间 n 秒后不会触发

function throttle(fn, wait) {
  let timer;
  return function () {
    let _this = this;
    let args = arguments;

    if (!timer) {
      timer = setTimeout(function () {
        timer = null;
        fn.apply(_this, args);
      }, wait);
    }
  };
}

this 指向问题

在简单调用时,this 默认指向的是 window / global / undefined (浏览器/node/严格模式)

  • 对象调用时,绑定在对象上;
  • 使用 call . apply . bind 时,绑定在指定的参数上;
  • 使用 new 关键字是,绑定到新创建的对象上; (以上三条优先级:new > apply/call/bind > 对象调用)
  • 使用箭头函数,根据外层的规则决定。
var a = 1;
function fn1() {
  var a = 2;
  console.log(this.a + a);
}
function fn2() {
  var a = 10;
  fn1();
}
fn2(); //3 1+2  this -> window
var Fn3 = function () {
  this.a = 3;
};
Fn3.prototype = {
  a: 4,
};
var fn = new Fn3();
fn1.call(fn); //5   this -> fn3
function Foo() {
  getName = function () {
    console.log(1);
  };
  return this;
}
Foo.getName = function () {
  console.log(2);
};
Foo.prototype.getName = function () {
  console.log(3);
};
var getName = function () {
  console.log(4);
};

function getName() {
  console.log(5);
}

//请写出以下输出结果:
Foo.getName(); // 2
getName(); // 4
Foo().getName(); //1
getName(); //????
new Foo.getName(); //2
new Foo().getName(); //3
function foo() {
  console.log(this.a);
}

var a = 2;

(function () {
  'use strict';

  foo();
})();

简单的 Promise

class Prom {
  static resolve(value) {
    if (value && value.then) {
      return value;
    }
    return new Prom((resolve) => resolve(value));
  }

  constructor(fn) {
    this.value = undefined;
    this.reason = undefined;
    this.status = 'PENDING';

    // 维护一个 resolve/pending 的函数队列
    this.resolveFns = [];
    this.rejectFns = [];

    const resolve = (value) => {
      // 注意此处的 setTimeout
      setTimeout(() => {
        this.status = 'RESOLVED';
        this.value = value;
        this.resolveFns.forEach(({ fn, resolve: res, reject: rej }) =>
          res(fn(value)),
        );
      });
    };

    const reject = (e) => {
      setTimeout(() => {
        this.status = 'REJECTED';
        this.reason = e;
        this.rejectFns.forEach(({ fn, resolve: res, reject: rej }) =>
          rej(fn(e)),
        );
      });
    };

    fn(resolve, reject);
  }

  then(fn) {
    if (this.status === 'RESOLVED') {
      const result = fn(this.value);
      // 需要返回一个 Promise
      // 如果状态为 resolved,直接执行
      return Prom.resolve(result);
    }
    if (this.status === 'PENDING') {
      // 也是返回一个 Promise
      return new Prom((resolve, reject) => {
        // 推进队列中,resolved 后统一执行
        this.resolveFns.push({ fn, resolve, reject });
      });
    }
  }

  catch(fn) {
    if (this.status === 'REJECTED') {
      const result = fn(this.value);
      return Prom.resolve(result);
    }
    if (this.status === 'PENDING') {
      return new Prom((resolve, reject) => {
        this.rejectFns.push({ fn, resolve, reject });
      });
    }
  }
}

Prom.resolve(10)
  .then((o) => o * 10)
  .then((o) => o + 10)
  .then((o) => {
    console.log(o);
  });

return new Prom((resolve, reject) => reject('Error')).catch((e) => {
  console.log('Error', e);
});

script 脚本异步执行| async 与 defer 作用

脚本执行顺序: 加载 解析 执行

相同点

  • 都是异步加载脚本

不同点

  • async 加载完立即执行 阻塞 dom 渲染
  • defer 加载完毕等待 解析完毕 执行

React/Vue 中的 router 实现原理如何| hash 和 history 路由

监听路由的地址

hash 模式

通过 location.hash 跳转路由 通过 hashchange event 监听路由变化

history 模式

通过 history.pushState() 跳转路由 通过 popstate event 监听路由变化,但无法监听到 history.pushState() 时的路由变化

两种模式的区别:

hash 只能改变#后的值,而 history 模式可以随意设置同源 url; hash 只能添加字符串类的数据,而 history 可以通过 API 添加多种类型的数据; hash 的历史记录只显示之前的www.a.com而不会显示 hash 值,而 history 的每条记录都会进入到历史记录; hash 无需后端配置且兼容性好,而 history 需要配置 index.html 用于匹配不到资源的情况。

什么是事件冒泡和事件捕获 (重学 重点阻止 + 兼容)

事件冒泡流

从一个具体的元素开始触发,然后不断向上传递最终传递给 document 也就是说 事件从一个具体的元素不断向上传递给不具体的元素, 用圆圈比喻 就是 从圆心向外传递

事件捕获流

事件捕获流是指 事件会从不具体的元素传递给具体的元素,相当于是事件最开始是从最外部触发,然后不断向内传递,最终传递给具体的元素 还是用圆圈比喻的话 就是从圆圈外部不断向圆心传递

如何阻止冒泡

// 课程

宏任务 微任务

setTimeout(() => console.log(0));
new Promise((resolve) => {
  console.log(1);
  resolve(2);
  console.log(3);
}).then((o) => console.log(o));

new Promise((resolve) => {
  console.log(4);
  resolve(5);
})
  .then((o) => console.log(o))
  .then(() => console.log(6));
// 1 => 3 => 4 => 2 => 5 => 6 => 0

错误 复习

setTimeout(() => {
  console.log('A');
  Promise.resolve().then(() => {
    console.log('B');
  });
}, 1000);

Promise.resolve().then(() => {
  console.log('C');
});

new Promise((resolve) => {
  console.log('D');
  resolve('');
}).then(() => {
  console.log('E');
});

async function sum(a, b) {
  console.log('F');
}

async function asyncSum(a, b) {
  await Promise.resolve();
  console.log('G');
  return Promise.resolve(a + b);
}

sum(3, 4);
asyncSum(3, 4);
console.log('H');

// D F H C E G A B

柯里化函数

就是一个函数 传入了很多参数 通过 arguments 数组 一个个重新传入到函数里面 apply

// 简单 案例
function curry(f) {
  // curry(f) 执行柯里化转换
  return function (a) {
    return function (b) {
      return f(a, b);
    };
  };
}

// 用法
function sum(a, b) {
  return a + b;
}

let curriedSum = curry(sum);

curriedSum(1)(2); // 3

// 高级 案例
function curry(func) {
  return function curried(...args) {
    if (args.length >= func.length) {
      return func.apply(this, args);
    } else {
      return function (...args2) {
        return curried.apply(this, args.concat(args2));
      };
    }
  };
}

function sum(a, b, c) {
  return a + b + c;
}

let curriedSum = curry(sum);

alert(curriedSum(1, 2, 3)); // 6,仍然可以被正常调用
alert(curriedSum(1)(2, 3)); // 6,对第一个参数的柯里化
alert(curriedSum(1)(2)(3)); // 6,全柯里化
function curry(fn) {
    // 获取原函数的参数长度
    const argLen = fn.length;
    console.log('获取原函数的参数长度', argLen);
    // 保存预置参数
    const presetArgs = [].slice.call(arguments, 1)
    console.log('保存预置参数', presetArgs);
    // 返回一个新函数
    return function () {
        // 新函数调用时会继续传参
        const restArgs = [].slice.call(arguments)
        console.log('新函数调用时会继续传参', arguments, [...presetArgs, ...restArgs]);
        const allArgs = [...presetArgs, ...restArgs]
        if (allArgs.length >= argLen) {
            // 如果参数够了,就执行原函数
            return fn.apply(this, allArgs)
        } else {
            // 否则继续柯里化
            return curry.call(null, fn, ...allArgs)
        }
    }
}
function fn(a, b, c) {
    return a + b + c;
}

var curried = curry(fn);
console.log(curried(1, 2)(3));; // 6


判断数据类型的方法

1 typeof

typeof '1' 能够准确检查除了 null 之外的基础数据类型(number, string, symbol, bigInt, undefined, boolean, null) 缺点 无法区分引用数据 数组 对象 null 都是'object'

2.instanceof

[] instanceof Array

instanceof 是用来判断 A 是否为 B 的实例 instanceof 只能用来判断两个对象是否属于实例关系, 而不能判断一个对象实例具体属于哪种类型。

3.constructor

new Number(1).constructor === Number

  1. null 和 undefined 是无效的对象,因此是不会有 constructor 存在的,这两种类型的数据需要通过其他方式来判断。

  2. 函数的 constructor 是不稳定的,这个主要体现在自定义对象上,当开发者重写 prototype 后,原有的 constructor 引用会丢失,constructor 会默认为 Object

4 Object.prototype.toString.call 无敌

function type(data) {
  if (arguments.length === 0) return new Error('type 方法未传参');
  var typeStr = Object.prototype.toString.call(data);
  return typeStr.match(/\[object (.\*?)\]/)[1].toLowerCase();
}

简单方法

反转

split('').reverse().join('')

去重

Array.from(new Set(a)) 循环遍历 。。。

100 长度 0 的数组

Array(100).fill(0); Array.from(Array(100), (x) => 0); Array.from({ length: 100 }, (x) => 0);

map 和 Set

Set add has get clean delete Map set has get clean delete

手写一个 发送请求方法

let httpRequest = new XMLHttpRequest();
httpRequest.open('GET', 'http://www.baidu.com/');
httpRequest.setRequestHeader('headname', 'headValue');
httpRequest.onreadystatechange = () => {
  if (httpRequest.readyState === 4) {
    if (httpRequest.status === 200) {
    }
  }
};
httpRequest.send(body);

原型和原型链

function Person(name) {
  this.name = name;
}
Person.prototype.getName = function () {
  console.log(this.name);
};
const p = new Person('菜鸡');

//1. new 创建一个对象,指向构造函数的原型
p.__proto__ === Person.prototype;
//2. 构造函数上,有个原型(是个对象),里面有个 constructor 函数,就是这个构造函数本身。
Person.prototype.constructor === Person;
//3. p对象的构造函数,是 Person
p.constructor === Person;
                  constructor
    【Person】                    【Person 原型 】
                  prototype
        new

    【p】

原型继承

function Parent(name) {
  this.name = name;
}

Parent.prototype.getName = function () {
  console.log(this.name);
};

function Child() {}

Child.prototype = new Parent();
Child.prototype.constructor = Child;
// 隐含的问题
// 1. 如果有属性是引用的属性,一旦某个实例修改了这个属性,那么都会被修改。
// 2. 创建的 child 的时候,是不能传参数的

构造函数的继承

function Parent(actions, name) {
  this.actions = actions;
  this.name = name;
}

function Child(id) {
  console.info('-------', Array.prototype.slice.call(arguments, 1));
  Parent.apply(this, Array.prototype.slice.call(arguments, 1));
  this.id = id;
}
// 隐含的问题
// 1. 属性或者方法,想被继承的话,只能在构造函数中定义
// 2. 如果方法在构造函数中定义了,每次都会创建。

组合继承

function Parent(actions, name) {
  this.actions = actions;
  this.name = name;
}
Parent.prototype.getName = function () {
  console.log(this.name);
};

function Child(id) {
  Parent.apply(this, Array.prototype.slice.call(arguments, 1));
  this.id = id;
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;

组合寄生式继承

function Parent(actions, name) {
  this.actions = actions;
  this.name = name;
}
Parent.prototype.getName = function () {
  console.log(this.name);
};

function Child(id) {
  Parent.apply(this, Array.prototype.slice.call(arguments, 1));
  this.id = id;
}
Child.prototype = Object.create(Parent.prototype);
// Child.prototype = inherit(Parent.prototype);
Child.prototype.constructor = Child;

///////// class 继承中做的,而狭义上的组合寄生式继承,没有做的。 /////////
for (var k in Parent) {
  if (Parent.hasOwnProperty(k) && !(k in child)) {
    Child[k] = Parent[k];
  }
}

function inherit(p) {
  if (p == null) throw TypeError();
  if (Object.create) {
    return Object.create(p);
  }

  var t = typeof p;
  if (t !== 'object' && t !== 'function') throw TypeError();
  function f() {}
  f.prototype = p;
  return new f();
}

组合寄生继承 和 class 继承有什么区别?

  • class 继承,会继承静态属性
  • 子类中,必须在 constructor 调用 super, 因为子类自己的 this 对象,必须先通过父类的构造函数完成。

new 关键字,到底干了什么?

  • 创建了一个对象
  • 该对象的原型,指向了这个 Function 的 prototype
  • 该对象实现了这个构造函数的方法;
  • 根据一些特定情况,返回对象
    • 如果没有返回值,则返回我创建的这个对象;
    • 如果有返回值,是一个对象,则返回该对象;
    • 如果有返回值,不是一个对象,则返回我创建的这个对象;
function Person(name) {
  this.name = name;
}

function newFunc(Father) {
  if (typeof Father !== 'function') {
    throw new Error('new operator function the frist param must be a function');
  }
  var obj = Object.create(Father.prototype);
  var result = Father.apply(obj, Array.prototype.slice.call(arguments, 1));
  return result && typeof result === 'object' && result !== null ? result : obj;
}

const p = newFunc(Person, name);

call()

Function.prototype.call call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。

Function.prototype._call = function (ctx, ...args) {
  const o = ctx;
  // 给 ctx 添加独一无二的属性
  const key = Symbol();
  o[key] = this;
  // 执行函数,得到返回结果
  const result = o[key](...args);
  // 删除该属性
  delete o[key];
  return result;
};
// 其中比较难理解的就是o[key] = this,
// 这行代码的意思就是把f.call(obj) 中的f函数挂载到obj上。
// 当执行f._call() 时, _call this 指向 调用者f,所以这里的this就是f函数。
const obj = {
  name: '11',
  fun(a) {
    console.log(this.name, '--', a);
  },
};

const obj2 = { name: '22' };
obj.fun(); // 11 -- undefined
obj.fun.call(obj2); // 22 -- undefined
obj.fun._call(obj2, 2233); // 22 -- 2233

bind()

Function.prototype.bind bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。

const obj = {
  name: '11',
  fun() {
    console.log(this.name);
  },
};
Function.prototype._bind = function (ctx, ...args) {
  // 获取函数体
  const _self = this;
  // 用一个新函数包裹,避免立即执行
  const bindFn = (...reset) => {
    return _self.call(ctx, ...args, ...reset);
  };
  return bindFn;
};
const obj2 = { name: '22' };
obj.fun(); // 11
const fn = obj.fun.bind(obj2);
const fn2 = obj.fun._bind(obj2);
fn(); // 22
fn2(); // 22

new 关键字

New 关键字 new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。

function _new(constructor) {
  // 创建一个空对象
  const obj = {};
  // 原型链挂载
  obj.__proto__ = constructor.prototype;
  // 将 obj 复制给构造体中的 this,并且返回结果
  const result = constructor.call(obj);
  // 如果返回对象不为一个对象则直接返回刚才创建的对象
  return typeof result === 'object' && result !== null ? result : obj;
}

Array.prototype.map

  • map 中的 exc 接受三个参数,分别是: 元素值、元素下标和原数组
  • map 返回的是一个新的数组,地址不一样
// 这里不能直接使用箭头函数,否则无法访问到 this
Array.prototype._map = function (exc) {
  const result = [];
  this.forEach((item, index, arr) => {
    
    result[index] = exc(item, index, arr);
  });
  return result;
};
const a = new Array(2).fill(2);
console.log(a.map((item, index, arr) => item * index + 1)); // [1,3]
console.log(a._map((item, index, arr) => item * index + 1)); // [1,3]

Array.prototype.filter

  • filter 中的 exc 接受三个参数,与 map 一致,主要实现的是数组的过滤功能,会根据 exc 函数的返回值来判断是否“留下”该值。
  • filter 返回的是一个新的数组,地址不一致。
Array.prototype._filter = function (exc) {
  const result = [];
  this.forEach((item, index, arr) => {
    if (exc(item, index, arr)) {
      result.push(item);
    }
  });
  return result;
};
const b = [1, 3, 4, 5, 6, 2, 5, 1, 8, 20];

console.log(b._filter((item) => item % 2 === 0)); // [ 4, 6, 2, 8, 20 ]

reduce

  • reduce 接受两个参数,第一个为 exc 函数,第二个为初始值,如果不传默认为 0
  • reduce 最终会返回一个值,当然不一定是 Number 类型的,取决于你是怎么计算的,每次的计算结果都会作为下次 exc 中的第一个参数
Array.prototype._reduce = function (exc, initial = 0) {
  let result = initial;
  this.forEach((item) => {
    result = exc(result, item);
  });
  return result;
};
let b =[1,2,3,4,5,6]
console.log(b.reduce((pre, cur) => pre + cur, 0)); // 55
console.log(b._reduce((pre, cur) => pre + cur, 0)); // 55

浅拷贝

const _shallowClone = (target) => {
  // 基本数据类型直接返回
  if (typeof target === 'object' && target !== null) {
    // 获取target 的构造体
    const constructor = target.constructor;
    // 如果构造体为以下几种类型直接返回
    if (/^(Function|RegExp|Date|Map|Set)$/i.test(constructor.name)) return target;
    // 判断是否是一个数组
    const cloneTarget = Array.isArray(target) ? [] : {};
    for (prop in target) {
      // 只拷贝其自身的属性
      if (target.hasOwnProperty(prop)) {
        cloneTarget[prop] = target[prop];
      }
    }
    return cloneTarget;
  } else {
    return target;
  }
};

深拷贝

  • 函数 正则 日期 ES6 新对象 等不是直接返回其地址,而是重新创建
  • 需要避免出现循环引用的情况
const _completeDeepClone = (target, map = new WeakMap()) => {
  // 基本数据类型,直接返回
  if (typeof target !== 'object' || target === null) return target;
  // 函数 正则 日期 ES6新对象,执行构造题,返回新的对象
  const constructor = target.constructor;
  if (/^(Function|RegExp|Date|Map|Set)$/i.test(constructor.name)) return new constructor(target);
  // map标记每一个出现过的属性,避免循环引用
  if (map.get(target)) return map.get(target);
  map.set(target, true);
  const cloneTarget = Array.isArray(target) ? [] : {};
  for (prop in target) {
    if (target.hasOwnProperty(prop)) {
      cloneTarget[prop] = _completeDeepClone(target[prop], map);
    }
  }
  return cloneTarget;
};
//  数组对象 深拷贝
function deepClone(source){
  const targetObj = Array.isArray(source) ? [] : {}; // 判断复制的目标是数组还是对象
  for(let keys in source){ // 遍历目标
    if(source.hasOwnProperty(keys)){
      if(source[keys] && typeof source[keys] === 'object'){ // 如果值是对象,就递归一下
        targetObj[keys] =  Array.isArray( source[keys]) ? [] : {};
        targetObj[keys] = deepClone(source[keys]);
      }else{ // 如果不是,就直接赋值
        targetObj[keys] = source[keys];
      }
    } 
  }
  return targetObj;
}

deepClone()