# 字节跳动面试题

如果需要了解了解算法以及没有很大把握的同学,建议把面试时间约在一周或者两周后,期间好好准备

另外面试时间可以调整,至少提前一天修改时间

面试可以现场可以视频,如果时间不方便可以约视频面试

面试共计四轮: 第一轮笔试或直接面试,重基础,细节 第二轮面试技术问题更深入一些 第三轮着重项目经验和技术广都和深度 第四轮 HR 面试,看稳定性,意向性,性价比,了解薪资 最后是 offer 沟通,定级、出薪资方案、入职时间等等 技术面试要知其然并知其所以然,每一轮面试或多或少都有算法,请做一些准备

刷题网站

最齐全前端进阶汇总(学习 5)

前端题库:仅做参考,不同的面试官有不同的面试风格和提问方式、内容 笔试:

# 1、换行字符串格式化

# 考虑兼容性

<style>
  .container  {
      box-sizing: border-box;
      min-height: 100vh;
      padding-bottom: 100px;
  }
  header,
   footer  {
      height: 100px;
  }
  footer  {
      margin-top: -100px;
  }
</style>
<div class="container">
  <header></header>
  <main></main>
</div>
<footer></footer>

# flex 布局

<style>
  .container {
    display: flex;
    height: 100%;
    flex-direction: column;
  }

  header,
  footer {
    min-height: 100px;
  }

  main {
    flex: 1;
  }
</style>
<div class="container">
  <header></header>
  <main>
    <div>...</div>
  </main>
  <footer></footer>
</div>

# 3、日期转化为 2 小时前,1 分钟前等

const minute = 1000 * 60;
const hour = minute * 60;
const day = hour * 24;
const week = day * 7;
const month = day * 30;

function getTimer(stringTime) {
  const time1 = new Date().getTime(); //当前的时间戳
  console.log(time1);
  const time2 = Date.parse(new Date(stringTime)); //指定时间的时间戳
  console.log(time2);
  const time = time1 - time2;

  let result = null;
  if (time < 0) {
    alert('设置的时间不能早于当前时间!');
  } else if (time / month >= 1) {
    result = '发布于' + parseInt(time / month) + '月前!';
  } else if (time / week >= 1) {
    result = '发布于' + parseInt(time / week) + '周前!';
  } else if (time / day >= 1) {
    result = '发布于' + parseInt(time / day) + '天前!';
  } else if (time / hour >= 1) {
    result = '发布于' + parseInt(time / hour) + '小时前!';
  } else if (time / minute >= 1) {
    result = '发布于' + parseInt(time / minute) + '分钟前!';
  } else {
    result = '刚刚发布!';
  }
  console.log(result);
}
getTimer('2020-05-23 10:11:12'); // 发布于1小时前!

# 4、多个 bind 连接后输出的值

bind 的多次绑定只有第一次是有效的, 在 Javascript 中,多次 bind() 是无效的。更深层次的原因, bind() 的实现,相当于使用函数在内部包了一个 call / apply ,第二次 bind() 相当于再包住第一次 bind() ,故第二次以后的 bind 是无法生效的

# 5、原码,补码,反码

原码、反码、补码 详解

# 1. 原码

原码就是符号位加上真值的绝对值,即用第一位表示符号,其余位表示值。比如:如果是 8 位二进制:

  • [+1]原 = 0000 0001
  • [-1]原 = 1000 0001

原码是人脑最容易理解和计算的表示方式。

# 2. 反码

反码的表示方法是: 正数的反码是其本身; 负数的反码是在其原码的基础上,符号位不变,其余各个位取反。

  • [+1] = [0000 0001]原 = [0000 0001]反
  • [-1] = [1000 0001]原 = [1111 1110]反

可见如果一个反码表示的是负数,人脑无法直观的看出来它的数值。通常要将其转换成原码再计算。

# 3. 补码

补码的表示方法是: 正数的补码就是其本身; 负数的补码是在其原码的基础上,符号位不变,其余各位取反,最后+1。(也即在反码的基础上+1)

  • [+1] = [0000 0001]原 = [0000 0001]反 = [0000 0001]补
  • [-1] = [1000 0001]原 = [1111 1110]反 = [1111 1111]补

对于负数,补码表示方式也是人脑无法直观看出其数值的。通常也需要转换成原码再计算其数值。

# 6、事件委托

就是事件代理

3 个使用场景助你用好 DOM 事件

# 7、输入一个日期 返回几秒前 几天前或者几月前;

const seconds = 1000;
const minute = seconds * 60;
const hour = minute * 60;
const day = hour * 24;
const week = day * 7;
const month = day * 30;

function getTimer(stringTime) {
  const time1 = new Date().getTime(); //当前的时间戳
  console.log(time1);
  const time2 = Date.parse(new Date(stringTime)); //指定时间的时间戳
  console.log(time2);
  const time = time1 - time2;

  let result = null;
  if (time < 0) {
    alert('设置的时间不能早于当前时间!');
  } else if (time / month >= 1) {
    result = parseInt(time / month) + '月前!';
  } else if (time / week >= 1) {
    result = parseInt(time / week) + '周前!';
  } else if (time / day >= 1) {
    result = parseInt(time / day) + '天前!';
  } else if (time / hour >= 1) {
    result = parseInt(time / hour) + '小时前!';
  } else if (time / minute > 1) {
    result = parseInt(time / minute) + '分钟前!';
  } else if (time / seconds >= 1 && time / seconds <= 60) {
    result = parseInt(time / seconds) + '秒前!';
  } else {
    result = '刚刚发布!';
  }
  console.log(result);
}
getTimer('2020-05-23 11:27:12'); // 24秒前!

# 8、153812.7 转化 153,812.7

字符串原生方法

function thousandsSplit(num = 0) {
  if (num !== null) {
    return num.toLocaleString();
  } else {
    return '-';
  }
}

console.log(thousandsSplit(153812.7)); // 153,812.7

正则

function micrometerLevel(value) {
  if (typeof value === 'undefined' || value === null || isNaN(value)) {
    return value;
  }
  const stringValue = value.toString();
  const [integer, decimal] = stringValue.split('.');
  if (integer.length <= 3) {
    return stringValue;
  }
  let total = '';
  for (let i = integer.length - 1, j = 1; i > -1; i--, j++) {
    const num = j % 3 === 0 ? `,${integer[i]}` : integer[i];
    total = num + total;
  }
  total = total.replace(/^,/, '') + (decimal ? `.${decimal}` : '');
  return total;
}

console.log(micrometerLevel(153812.7));

# 11、实现一个 Promise.all

// Promise.all 表示全部成功才成功 有任意一个失败 都会失败
Promise.all = function(promises) {
  return new Promise((resolve, reject) => {
    let arr = [],
      currentIndex = 0;

    function processData(index, value) {
      arr[index] = value;
      currentIndex++;
      if (currentIndex === promises.length) {
        resolve(arr);
      }
    }
    for (let i in promises) {
      promises[i].then(data => {
        processData(i, data);
      }, reject);
    }
  });
};

# 12.手写代码

给定一个数组,形如 [1, 1, 2 , 3, 3, 3, 3, 4, 6, 6],给定一个数 n,例如 3,找出给定的数 n 在数组内出现的次数,要求时间复杂度小于 O(n)

const countOccurrences = (arr, value) => arr.reduce((a, v) => (v === value ? a + 1 : a + 0), 0);
console.log(countOccurrences([1, 1, 2, 3, 3, 3, 3, 4, 6, 6], 3)); // 4

# 一轮

# 1.dom react 原理

【React 深入】深入分析虚拟 DOM 的渲染原理和特性

# 2.css 布局

实际上我们在讨论布局的时候,会把网页上特定的区域进行分列操作。按照分列数目,可以大致分为 3 类,即单列布局、2 列布局、3 列布局。

掌握 CSS 精髓:布局

# 3.js 原型链继承

prototype

每个函数都有 prototype 属性,除了 Function.prototype.bind(),该属性指向原型。

每个对象都有 __proto__ 属性,指向了创建该对象的构造函数的原型。其实这个属性指向了 [[prototype]],但是 [[prototype]] 是内部属性,我们并不能访问到,所以使用 __proto__ 来访问。

对象可以通过 __proto__ 来寻找不属于该对象的属性,__proto__ 将对象连接起来组成了原型链。

如果你想更进一步的了解原型,可以仔细阅读 深度解析原型中的各个难点

总结

  • Object 是所有对象的爸爸,所有对象都可以通过 __proto__ 找到它
  • Function 是所有函数的爸爸,所有函数都可以通过 __proto__ 找到它
  • Function.prototypeObject.prototype 是两个特殊的对象,他们由引擎来创建
  • 除了以上两个特殊对象,其他对象都是通过构造器 new 出来的
  • 函数的 prototype 是一个对象,也就是原型
  • 对象的 __proto__ 指向原型, __proto__ 将对象和原型连接起来组成了原型链

# 4.fetch 取消

如何取消 Fetch 请求?

JavaScript 的 promise 一直是该语言的一大胜利——它们引发了异步编程的革命,极大地改善了 Web 性能。原生 promise 的一个缺点是,到目前为止,还没有可以取消 fetch 的真正方法。 JavaScript 规范中添加了新的 AbortController,允许开发人员使用信号中止一个或多个 fetch 调用。

以下是取消 fetch 调用的工作流程:

  1. 创建一个 AbortController 实例
  2. 该实例具有 signal 属性
  3. 将 signal 传递给 fetch option 的 signal
  4. 调用 AbortController 的 abort 属性来取消所有使用该信号的 fetch。

实现代码

const controller = new AbortController();
const { signal } = controller;

fetch('http://localhost:8000', { signal })
  .then(response => {
    console.log(`Request 1 is complete!`);
  })
  .catch(e => {
    if (e.name === 'AbortError') {
      // We know it's been canceled!
    }
  });

// Abort request
controller.abort();

# 5.eventloop

浏览器

node

# 6.instanceof

instanceof 可以正确的判断对象的类型,因为内部机制是通过判断对象的原型链中是不是能找到类型的 prototype。

我们也可以试着实现一下 instanceof

function instanceof(left, right) {
  // 获得类型的原型
  let prototype = right.prototype;
  // 获得对象的原型
  left = left.__proto__;
  // 判断对象的类型是否等于类型的原型
  while (true) {
    if (left === null) return false;
    if (prototype === left) return true;
    left = left.__proto__;
  }
}

# 7.promise 封装 setstate

很多时候业务需要,在 setState 后再进行操作,比如改变下拉框的 state 值需要刷新 table 列表。这时候不可避免的需要使用 setState 中自带的 callback 方式。callback 方式缺点是不好定位错误,很容易就进入了回调地狱。 所以想到了可以将 setState 进行 promise 化,用.then 的方式取代回调。

/**
 * 将setState方法promise化,用promise的方式去取代回调。
 * @param state
 * @returns {Promise}
 */
const setStateP = function(state) {
  return new Promise(resolve => this.setState(state, resolve));
};

# 8.redux 基本组成和设计单向数据流

# 9.https 协议的过程

# 10.https 获取加密密钥的过程

HTTPS 简介 同上

# 11.http 的方法有哪几种

如何理解 HTTP 的请求方法

# 12.类式继承的方案

# 13.prototype 继承的实现

# 14.数字千分位处理,正则和非正则都要实现

同上 - 153812.7 转化 153,812.7

# 15.借用构造继承,几种组合继承方式

# 16.看编程代码说出运行结果

Process.nextTick,setImmediate 和 promise.then 的优先级

# 17.实现一个 bind 函数

bindcall、apply两个方法作用也是一致的,只是该方法会返回一个函数。并且我们可以通过 bind 实现柯里化。

同样的,也来模拟实现下 bind

Function.prototype.myBind = function(context) {
  if (typeof this !== 'function') {
    throw new TypeError('Error');
  }
  var _this = this;
  var args = [...arguments].slice(1);
  // 返回一个函数
  return function F() {
    // 因为返回了一个函数,我们可以 new F(),所以需要判断
    if (this instanceof F) {
      return new _this(...args, ...arguments);
    }
    return _this.apply(context, args.concat(...arguments));
  };
};

# 18.千位加逗号

同上 - 153812.7 转化 153,812.7

# 19.三个继承方式的优缺点

看上面的继承

# 20.nodejs 的事件循环

node

# 21.bfc

# 22.css 实现正方形 div 水平垂直居中

实现居中的方案有很多,这里介绍下纯 CSS 使用 absolute 配合 margin 的方案。

# 1. div 宽高固定

.box {
  width: 400px;
  height: 200px;
  position: absolute;
  top: 50%;
  left: 50%;
  margin-top: -100px;
  margin-left: -200px;
}

margin-top 为-(height / 2),margin-left 为-(width / 2)。当然也可以不用 margin,即 top 为 calc(100% - height) / 2,left 为 calc(100% - width) / 2,但是建议可以不用 calc() 就不要用。

就像这样

.box {
  width: 400px;
  height: 200px;
  position: absolute;
  top: calc((100% - 100px) / 2);
  left: calc((100% - 400px) / 2);
}

也可以不用 margintranslate(),如下:

.box {
  width: 50%;
  height: 50%;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translateX(-50%) translateY(-50%);
}

# 2. div 宽高不固定

五马分尸式

.box {
  width: 50%;
  height: 50%;
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  margin: auto;
}

注意,这适用于宽高需指定的情况,比如使用百分比或者用 js 动态修改,不然可能被当成 100%处理。

# 3. CSS3 不定宽高水平垂直居中

.box {
  justify-content: center; /* 子元素水平居中 */
  align-items: center; /* 子元素垂直居中 */
  display: -webkit-flex;
}

在父级元素上添加,即可实现子元素水平垂直居中。

# 23.koa1 的原理,继承

# 24.最后是一个写代码, 处理有依赖的异步任务 加重试

# 25.自己实现 bind 函数

同上

# 26.什么是闭包

闭包是指有权访问另外一个函数作用域中的变量的函数

关键在于下面两点:

  • 是一个函数
  • 能访问另外一个函数作用域中的变量

深入浅出图解作用域链和闭包

# 27.最长子序列

/**
 * @param {number[]} nums
 * @return {number}
 */
var lengthOfLIS = function(nums) {
  let n = nums.length;
  if (n == 0) {
    return 0;
  }
  let dp = new Array(n).fill(1);
  let max = 0;
  for (let i = 0; i < n; i++) {
    for (let j = 0; j < i; j++) {
      if (nums[j] < nums[i]) {
        dp[i] = Math.max(dp[i], dp[j] + 1);
      }
    }
    max = Math.max(max, dp[i]);
  }
  return max;
};

leetcode - 最长上升子序列

# 28.二叉树的前序遍历

遍历顺序是:左 -> 中 -> 右

方法一:简单粗暴的递归方法

/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number[]}
 */
var preorderTraversal = function(root) {
  return root ? [root.val, ...preorderTraversal(root.left), ...preorderTraversal(root.right)] : [];
};

方法二:老老实实用迭代法

/**
 *
 * @param {TreeNode} root
 * @return {number[]}
 */
var preorderTraversal = function(root) {
  let res = [],
    arr = [];
  root && arr.push(root);
  while (arr && arr.length) {
    const current = arr.pop();
    res.push(current.val);
    // 栈结构是先进后出,因此先将右节点压栈、再将左节点压栈(右 -> 左),最后弹栈的顺序是左->右
    current.right !== null && arr.push(current.right);
    current.left !== null && arr.push(current.left);
  }
  return res;
};

# 29.二叉树的中序遍历

遍历顺序是:中 -> 左 -> 右

方法一:递归大法好

/**
 * @param {TreeNode} root
 * @return {number[]}
 */
const inorderTraversal = root => {
  const res = [];
  const pushValue = node => {
    if (node != null) {
      node.left && pushValue(node.left);
      res.push(node.val);
      node.right && pushValue(node.right);
    }
  };
  pushValue(root);
  return res;
};

方法二:老老实实用迭代法一

/**
 * @param {TreeNode} root
 * @return {number[]}
 */
const inorderTraversal = root => {
  let res = [];
  if (root == null) return res;
  const stack = [];
  let curr = root;
  while (curr != null || stack.length > 0) {
    if (curr) {
      stack.push(curr);
      curr = curr.left;
    } else {
      const node = stack.pop();
      res.push(node.val);
      curr = curr.right;
    }
  }
  return res;
};

方法二:老老实实用迭代法二

/**
 * @param {TreeNode} root
 * @return {number[]}
 */
// 如果 left 节点存在, 就入栈, 然后跳 left
// 如果 left 和 right 都不存在, 则保存当前节点, 然后出栈, 并让 left 等于 null
// 如果 right 节点存在, 并且 left 为 null, 则保存当前节点, 然后跳 right
const inorderTraversal = root => {
  // 用来保存节点
  let res = [],
    // 存放根节点
    stack = [];
  while (root || stack.length) {
    if (root.left) {
      // 如果 left 节点存在, 就入栈
      stack.push(root);
      // 跳到 left
      root = root.left;
    } else if (!root.left && !root.right) {
      // 如果 left 和 right 都不存在, 则保存当前节点
      res.push(root.val);
      // 出栈
      root = stack.pop();
      // 让 left 等于 null
      root && (root.left = null);
    } else if (root.right) {
      // 如果 right 节点存在, 并且 left 为 null, 则保存当前节点
      res.push(root.val);
      // 跳 right
      root = root.right;
    }
  }
  return res;
};

# 28.二叉树的后序遍历

遍历顺序是:左 -> 右 -> 中

方法一:递归大法好

/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number[]}
 */
var postorderTraversal = function(root) {
  let res = [];
  const pushNode = node => {
    if (node != null) {
      // 利用栈的特点:先进后出
      node.left != null && pushNode(node.left);
      node.right != null && pushNode(node.right);
      // 一个个压栈
      res.push(node.val);
    }
  };
  pushNode(root);
  return res;
};

方法二:使用迭代

/**
 * @param {TreeNode} root
 * @return {number[]}
 */
var postorderTraversal = function(root) {
  let res = [],
    stack = [];
  while (root || stack.length) {
    // 每次都把值插入到最前面
    res.unshift(root.val);
    // 先入左节点,再入右节点(利用栈的特点:先进后出)
    if (root.left) stack.push(root.left);
    if (root.right) stack.push(root.right);
    root = stack.pop();
  }
  return res;
};

# http 握手原理

卧槽!牛皮了,头一次见有大佬把 TCP 三次握手四次挥手解释的这么明白

# 29.react 新版本的特性

# 35.上中下三栏布局

flex

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>

  <body>
    <style>
      .container {
        display: flex;
        height: 100vh;
        flex-direction: column;
      }

      header,
      footer {
        min-height: 100px;
        background-color: aqua;
      }

      main {
        flex: 1;
      }
    </style>
    <div class="container">
      <header></header>
      <main>
        <div></div>
      </main>
      <footer></footer>
    </div>
  </body>
</html>

考虑兼容性

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>

  <body>
    <style>
      .container {
        box-sizing: border-box;
        min-height: 100vh;
        padding-bottom: 100px;
      }

      header,
      footer {
        background-color: aqua;
        height: 100px;
      }

      footer {
        margin-top: -100px;
      }
    </style>
    <div class="container">
      <header></header>
      <main></main>
    </div>
    <footer></footer>
  </body>
</html>

# 37.实现一个子类实例可以继承父类的所有方法

# 38.实现 sum(1)(2)(3).valueOf(),实现这么一个 sum 函数,返回 6

function sum() {
  // 类数组转换成真正的数组,也可以用 [...arguments] or Array.from(arguments)
  // 接收第一次传入的参数 -> 1
  const args = [].slice.call(arguments);

  const fn = function() {
    // 接收后面调用传入的参数, [1] -> [1,2] -> [1,2,3]多个参数不断收集到数组中
    const args2 = [].slice.call(arguments);
    return sum.apply(null, args.concat(args2));
  };

  fn.valueOf = function() {
    return args.reduce((a, b) => a + b);
  };
  return fn;
}

console.log(sum(1)(2)(3).valueOf()); // 6

# 串行异步任务

taskSum(1000,()=>{console.log(1)}).task(1200,()=>{console.log(2)}).task(1300,()=>{console.log(3)}),这里等待 1s,打印 1,之后等待 1.2s,打印 2,之后打印 1.3s,打印 3
class Queue {
  constructor() {
    this.queue = [];
  }
  task(f, ms) {
    this.queue.push({ f, ms });
    return this;
  }
  async run() {
    while (this.queue.length > 0) {
      // 遵循队列的先进先出原则,取出最前面一项来执行
      let { f, ms } = this.queue.shift();
      // 等待promise成功,阻塞后面的同步代码执行
      await sleep(f, ms);
    }
  }
}
// 睡眠ms后让promise状态转为成功态
const sleep = (f, ms) =>
  new Promise((resolve, reject) => {
    setTimeout(() => {
      f();
      resolve();
    }, ms);
  });
const start = () => {
  new Queue()
    .task(() => {
      console.log(1);
    }, 1000)
    .task(() => {
      console.log(2);
    }, 2000)
    .task(() => {
      console.log(3);
    }, 3000)
    .run();
};
start();

# 31.Jsonp 跨域

# 32.js 原型继承 & 原型链

重新认识构造函数、原型和原型链

# 33.promise

# 34.二叉树搜寻算法(另外一篇文章中)

# 35.算法:前端做并发请求控制(另外一篇文章中)

# 38. express ctx 中间件代码实现

# 39.. vue 发布订阅和虚拟 dom 代码实现

# 40. 请实现如下的函数,可以批量请求数据,所有的 URL 地址在 urls 参数中,同时可以通过 max 参数控制请求的并发度,当所有请求结束之后,需要执行 callback 回调函数。发请求的函数可以直接使用 fetch 即可

function sendRequest(arr, max, callback) {
  let fetchArr = [], // 存储并发max的promise数组
    i = 0;

  function toFetch() {
    if (i === arr.length) {
      // 所有的都处理完了, 返回一个resolve
      return Promise.resolve();
    }

    let one = fetch(arr[i++]); // 取出第i个url, 放入fetch里面 , 每取一次i++
    fetchArr.push(one); //将当前的promise存入并发数组中,先push进去,再等promise执行完了之后再删除。
    one.then(() => {
      fetchArr.splice(fetchArr.indexOf(one), 1);
    }); // 当promise执行完毕后,从数组删除

    let p = Promise.resolve();
    if (fetchArr.length >= max) {
      // 当并行数量达到最大后, 用race比较 第一个完成的, 然后再调用一下函数自身。
      p = Promise.race(fetchArr);
    }
    return p.then(() => toFetch());
  }

  // arr循环完后, 现在fetchArr里面剩下最后max个promise对象, 使用all等待所有的都完成之后执行callback
  toFetch()
    .then(() => Promise.all(fetchArr))
    .then(() => {
      callback();
    });
}

# 41.二叉树遍历

从上到下打印二叉树

# 42.并发请求最大值是 10,怎么处理队列

同上

# 43.css 画出一个三角形

#div1 {
  border-style: solid;
  border-width: 100px 100px 100px 100px;
  border-color: transparent transparent blue transparent;
  width: 0px;
  height: 0px;
}

# 44.node 网关

当您希望在 API 网关中执行简单的操作,比如将请求路由到特定服务,您可以使用像 nginx 这样的反向代理。但在某些时候,您可能需要实现一般代理不支持的逻辑。在这种情况下,您可以在 Node.js 中实现自己的 API 网关。

在 Node.js 中,您可以使用 http-proxy 软件包简单地代理对特定服务的请求,也可以使用更多丰富功能的 express-gateway 来创建 API 网关。

在我们的第一个 API 网关示例中,我们在将代码委托给 user 服务之前验证请求。

const express = require('express');
const httpProxy = require('express-http-proxy');
const app = express();

const userServiceProxy = httpProxy('https://user-service');

// 身份认证
app.use((req, res, next) => {
  // TODO: 身份认证逻辑
  next();
});

// 代理请求
app.get('/users/:userId', (req, res, next) => {
  userServiceProxy(req, res, next);
});

另一种示例可能是在您的 API 网关中发出新的请求,并将响应返回给客户端:

const express = require('express');
const request = require('request-promise-native');
const app = express();

// 解决: GET /users/me
app.get('/users/me', async (req, res) => {
  const userId = req.session.userId;
  const uri = `https://user-service/users/${userId}`;
  const user = await request(uri);
  res.json(user);
});

Node.js API 网关总结

API 网关提供了一个共享层,以通过微服务架构来满足客户需求。它有助于保持您的服务小而专注。您可以将不同的通用逻辑放入您的 API 网关,但是您应该避免 API 网关的过度使用,因为很多逻辑可以从服务团队中获得控制。

# 45.csrf/xss 攻击原理(前端攻击有哪些)

谈谈 web 安全问题及解决方案

# 46.react diff 原理

react-DOM-DIFF

# 47.事件循环

# 48.react diff 算法,key 的作用,setData 的机制,事件合成

# vue 相关面试题

Vue 面试题汇总

# wepack-dev-server 热更新功能实现原理

开始一个 React 项目(二) 彻底弄懂 webpack-dev-server 的热更新

# 59.[1,2,3].map(parseInt) 执行结果

  • parseInt('1', 0); // radix 为 0 时,使用默认的 10 进制。
  • parseInt('2', 1); // radix 值 1 不在 2-36 范围,因此无法解析,返回 NaN
  • parseInt('3', 2); // 基数为 2,2 进制数表示的数中,最大值小于 3,无法解析,返回 NaN

# 67.hybrid 实现 bridge 的方法

# 69.小程序框架的实现原理

小程序底层框架实现原理解析

# 78.vue-router 路由监听的原理

VueRouter 源码解析

# 79.webpack 打包的原理,webpack 有没有针对打包过程做一些优化提升打包速度

# 85.小程序架构优化

小程序性能和体验优化方法

# 86.写一个 eventBus

EventBus 事件车

# 88.vuex mobx

# 91.CSS 栅格布局

很多 UI 框架都提供了栅格系统来帮助页面实现等分或等比布局,比如 Bootstrap 提供了 12 列栅格,elment ui 和 ant design 提供了 24 列栅格。

那么你思考过栅格系统设定这些列数背后的原因吗?

首先从 12 列说起,12 这个数字,从数学上来说它具有很多约数 1、2、3、4、6、12,也就是说可以轻松实现 1 等分、2 等分、3 等分、4 等分、6 等分、12 等分,比例方面可以实现 1:11、1:5、1:3、1:2、1:1、1:10:1、1:4:1 等。如果换成 10 或 8,则可实现的等分比例就会少很多,而更大的 16 似乎是个不错的选择,但对于常用的 3 等分就难以实现。

至于使用 24 列不使用 12 列,可能是考虑宽屏幕(PC 端屏幕宽度不断增加)下对 12 列难以满足等分比例需求,比如 8 等分。同时又能够保证兼容 12 列情况下的等分比例(方便项目迁移和替换)。

# 92. CSS 伪类和伪元素

  • css 伪类:

    css 伪类用于向某些选择器添加特殊的效果。 锚伪类::link, :visited, :hover, :focus, :active, :lang

    • css3 新增的伪类: :first-child, :last-child, :only-child, :first-of-type, :last-of-type, :only-of-type, :nth-child(n), :nth-last-child(n), :nth-of-type(n), :nth-last-of-type(n), :root, :empty, :target, :enabled, :disabled, :checked, :not(selector),
  • css 伪元素:

    css 伪元素用于向某些选择器设置特殊效果。 ::first-letter, ::first-line, ::before, ::after css3 新增的伪元素 ::selection

  • css 伪类和伪元素的区别

    1. 为了避免大家混淆伪类和伪元素,css3 中的标准规定伪类使用单冒号“:” ,伪元素使用双冒号“::”,但在此之前都使用的单冒号“:”,所以为了保证兼容伪元素两种使用方法都是可以的。
    2. 伪类可以叠加使用,而伪元素在一个选择器中只能出现一次,并且只能出现在末尾。
    .box:first-child:hover {
      color: #000;
    } //使用伪类
    .box:first-letter {
      color: #000;
    } //使用伪元素
    .box:first-letter:hover {
      color: #000;
    } //错误写法
    

    CSS 伪类 (Pseudo-classes)

    1. 伪类与类优先级相同,伪元素与标签优先级相同。顺便说一下优先级怎么判断,一般是 !important > 行内样式> ID 选择器 > 类选择器 > 标签 > 通配符 > 继承 > 浏览器默认属性。还有一个简单的计算方法,内联样式表的权值为 1000,ID 选择器的权值为 100,Class 类选择器的权值为 10,HTML 标签选择器的权值为 1,权值实际并不是按十进制计算的,用数字表示只是说明思想,我们可以把选择器中规则对应做加法,比较权值大小,权值越大优先级越高,如果权值相同前面的样式会被后面的覆盖。

# 93.根据条件获取递归树中过的某一节点

const orgTreeData = {
  nodeId: '19A5B',
  nodeName: '预分析用户:1313.85万人',
  nodeType: 'root',
  expand: true,
  dataCnt: 13138494,
  children: [
    {
      nodeId: '19A5B_19A5C',
      nodeName: '客户状态',
      nodeType: 'tag',
      children: [{}]
    }
  ]
};
const findParentNodeById = (data, nodeId) => {
  //设置结果
  let result;
  if (!data) return; //如果data传空,直接返回
  for (let i = 0; i < data.children.length; i++) {
    let item = data.children[i];
    //找到id相等的则返回父节点
    if (item.nodeId == nodeId) return (result = data);
    else if (item.children && item.children.length > 0) {
      //如果有子集,则把子集作为参数重新执行本方法
      result = findParentNodeById(item, nodeId);
      //关键,千万不要直接return本方法,不然即使没有返回值也会将返回return,导致最外层循环中断,直接返回undefined,要有返回值才return才对
      if (result) return result;
    }
  }
  //如果执行循环中都没有return,则在此return
  return result;
};

# 94.JavaScript this 的指向;箭头函数的 this 指向

JavaScript 深入之史上最全--5 种 this 绑定全面解析

# 95.Promise / setTimeout 的执行顺序;实际考察知识点:对「事件队列 / 宏任务 / 微任务」的了解

Promise 是微任务,会优先于 setTimeout 执行

# 96.执行代码求输出,并说明为什么,严格模式下输出有变化吗,为什么

var a = function() {
  this.b = 3;
};
var c = new a();
a.prototype.b = 9;
var b = 7;
a();

console.log(b);
console.log(c.b);

this

解析

  1. 执行a();, 没有显式指定调用方式,在非严格模式下,则指向 windows,因此函数表达式a里面的this指向windows, 即 this.b = 3 -> window.b = 3;
  2. 执行 c = new a();, c 就是 a 的实例,所以 this.b = 3 -> c.b = 3;
  3. 执行 a.prototype.b = 9;, 根据原型链规则,就是 c.__proto__ -> a.prototype;

# 97.给定一个升序整型数组[0,1,2,4,5,7,13,15,16],找出其中连续出现的数字区间,输出为["0->2","4->5","7","13","15->16"]

function summaryRanges(arr) {
  //TODO
}

# 98.请实现以下的函数,可以批量请求数据,所有的 URL 地址在 urls 参数中,同时可以通过 max 参数控制请求的并发度,当所有请求结束之后,需要执行 callback 回调函数。发请求的函数可以直接使用 fetch 即可

function sendRequest(urls: sring[], max: number, callback: () => void) {
  //TODO
}

同上

# 99.弹性盒子中 flex: 0 1 auto 表示什么意思

三个参数分别对应的是 flex-grow, flex-shrink 和 flex-basis,默认值为 0 1 auto。

  1. flex-grow 属性定义项目的放大比例,默认为 0,即如果存在剩余空间,也不放大。
  2. flex-shrink 属性定义了项目的缩小比例,默认为 1,即如果空间不足,该项目将缩小。
  3. flex-basis 属性定义了在分配多余空间之前,项目占据的主轴空间(main size)(设置了宽度跟宽度走,没设置宽度跟内容实际宽度走)。

# 100.Promise 相关

(1)promise 的链式调用怎么中断? 返回一个既不成功也不失败的 promise, 相当于是返回pending状态的 Promise 对象

return new Promise(resolve,reject) => {});

(2)promise 如何取消

  • Promise.race 竞速方法
let p1 = new Promise((resolve, reject) => {
  resolve('ok1');
});

let p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('ok2');
  }, 10);
});

Promise.race([p2, p1])
  .then(result => {
    console.log(result); //ok1
  })
  .catch(error => {
    console.log(error);
  });
  • Promise 链中抛出一个错误时,错误信息沿着链路向后传递,直至被捕获。

利用这一特性能跳过链中被捕获前的函数的调用,直至链路终点。

Promise.resolve()
  .then(() => {
    console.log('ok1');
    throw 'throw error1';
  })
  .then(
    () => {
      console.log('ok2');
    },
    err => {
      // 捕获错误
      console.log('err->', err);
    }
  )
  .then(() => {
    // 该函数将被调用
    console.log('ok3');
    throw 'throw error3';
  })
  .then(() => {
    // 错误捕获前的函数不会被调用
    console.log('ok4');
  })
  .catch(err => {
    console.log('err->', err);
  });

(3)Promise.prototype.finally的使用场景

  • 隐藏 AJAX 请求的 loading 效果
  • 关掉打开的文件
  • 不管操作完成的状态是成功还是失败都想要记录日志

等等这些情况下,就需要用 finally

(4)手写一个 promise 代码地址

# 101.打开 url 后的流程

细说浏览器输入 URL 后发生了什么

# 102.怎么解决内存泄漏。还有内存回收的有哪几种方式

JavaScript 内存泄露的 4 种方式及如何避免

# 二面

# 1.实现一个字符串反转:输入:www.toutiao.com.cn 输出:cn.com.toutiao.www

要求:1.不使用字符串处理函数 2.空间复杂度尽可能小

/**
 * @param {string} s
 * @return {string}
 */
var reverseWords = function(s) {
  var l = '';
  var l1 = '';
  for (var i = 0; i < s.length; i++) {
    if (s[i] != ' ') {
      l = s[i] + l;
    } else {
      l1 = l1 + l + ' ';
      l = '';
    }
  }
  return l1 + l;
  // return s.split("").reverse().join("").split(" ").reverse().join(" ")
};

# 2.不借助变量,交换两个数

# 1. 算术交换

针对的是 Number,或者类型可以转换成数字的变量类型

function swap(a, b) {
  a = a + b;
  b = a - b;
  a = a - b;
}

通过算术运算过程中的技巧,可以巧妙地将两个值进行互换。但是,有个缺点就是变量数据溢出。因为 JavaScript 能存储数字的精度范围是 -2^532^53。所以,加法运算,会存在溢出的问题。

# 2. 异或运算

^ 按位异或 若参加运算的两个二进制位值相同则为 0,否则为 1

此算法能够实现是由异或运算的特点决定的,通过异或运算能够使数据中的某些位翻转,其他位不变。这就意味着任意一个数与任意一个给定的值连续异或两次,值不变.

a = a ^ b;
b = a ^ b;
a = a ^ b;

# 3. ES6 的解构(真香~)

[a, b] = [b, a];

# 3.观察者模式 vs 发布-订阅模式,说说区别

# 观察者模式的概念

观察者模式模式,属于行为型模式的一种,它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主体对象在状态变化时,会通知所有的观察者对象,使他们能够自动更新自己。

# 发布订阅者模式的概念

发布-订阅模式,消息的发送方,叫做发布者(publishers),消息不会直接发送给特定的接收者,叫做订阅者。意思就是发布者和订阅者不知道对方的存在。需要一个调度中心媒介,叫做信息中介,它将订阅者和发布者串联起来,它过滤和分配所有输入的消息。换句话说,发布-订阅模式用来处理不同系统组件的信息交流,即使这些组件不知道对方的存在。

pub

我们把这些差异快速总结一下:

观察者模式中,观察者是知道Subject的,Subject一直保持对观察者进行记录。然而,在发布订阅模式中,发布者和订阅者不知道对方的存在。它们只有通过消息代理(调度中心)进行通信。

在发布订阅模式中,组件是松散耦合的,正好和观察者模式相反。

观察者模式大多数时候是同步的,比如当事件触发,Subject就会去调用观察者的方法。而发布-订阅模式大多数时候是异步的(使用消息队列)。

观察者模式需要在单个应用程序地址空间中实现,而发布-订阅更像交叉应用模式。

# 4. 与项目无绝对相关的问答题

# 5.http 网络协议

HTTP 灵魂之问,巩固你的 HTTP 知识体系

# 6.tcp 为什么是可靠的

TCP 协议为什么可靠?

# 7.solid 原则

设计模式之 SOLID 原则

# 8.柯里化

是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接收余下的参数,而且返回结果的新函数的技术。

简单点说,就是把接收多个参数的函数执行转换成一个个接收并执行的函数

示例代码如下:

const isType = (type, value) => Object.prototype.toString.call(value) === `[object ${type}]`;

const currying = (fn, arr = []) => {
  const len = fn.length;
  return (...args) => {
    let concatArgs = [...arr, ...args];
    if (concatArgs.length < len) {
      // 如果参数还没有收集玩,就继续curry参数
      return currying(fn, concatArgs); // 递归不停的产生函数
    } else {
      // 否则直接执行函数并返回结果
      return fn(...concatArgs);
    }
  };
};

const isArray = currying(isType)('Array', []);
const isString = currying(isType)('String', '');
const isNumber = currying(isType)('Number', '1');

console.log(`isArray`, isArray); // isArray true
console.log(`isString`, isString); // isString true
console.log(`isNumber`, isNumber); // isNumber false

# 9.css 单行和多行截断

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>css处理超出文本截断问题的两种情况(多行或者单行)</title>
    <style>
      /* 单行的简单处理方式 */
      .words {
        width: 400px;

        /*超过部分不显示*/
        overflow: hidden;
        /*超过部分用点点表示*/
        text-overflow: ellipsis;
        /*不换行*/
        white-space: nowrap;
      }

      /* 规定行数的截断处理方式 */
      .words {
        width: 400px;

        text-overflow: ellipsis;
        /*有些示例里需要定义该属性,实际可省略*/
        display: -webkit-box;
        -webkit-line-clamp: 2;
        /*规定超过两行的部分截断*/
        -webkit-box-orient: vertical;
        overflow: hidden;
        word-break: break-all;
        /*在任何地方换行*/
      }
    </style>
  </head>

  <body>
    <div class="words">
      For a long time it seemed to me that life was about to begin , real life. But, there was
      always some obsacle in the way, something to be gotten through first, some unfinnished
      business, time still to be served or a debt to be paid. Then life would begin.
    </div>
  </body>
</html>

# 10.手写一个 redux middleware

Redux 中间件

# 11.ssr(ssr csr 混合怎么处理)

React SSR 详解【近 1W 字】+ 2 个项目实战

# 12.TS 常见问题整理(60 多个,持续更新 ing)

TS 常见问题整理(60 多个,持续更新 ing)

# 三轮

# 1.什么是微前端

了解什么是微前端

# 2.浅拷贝深拷贝实现

js 浅拷贝与深拷贝的区别和实现方式

# 3. redux 和 mobx 的区别

redux、mobx、concent 特性大比拼, 看后生如何对局前辈

# 4.长列表渲染

性能优化之长列表渲染——时间分片和虚拟列表

# 5.redis 数据结构和实现

Redis 数据结构底层实现

# 其他

  • js 实现依赖注入
  • 接口攻击的方式和防御措施
  • js 多线程如何共享大的数据
  • 二叉树 ,diff 算法,
  • 页面渲染原理,
  • 图像算法 事件循环

# 最后

文中若有不准确或错误的地方,欢迎指出,有兴趣可以的关注下Github,一起学习呀~~

Last Updated: 2020/9/7 下午8:45:37