前端面试题详解整理104|interface和type的区别,TCP,加密过程,基于umi修改的配置手写useQuery,eslint手写事件总线,观察者模式,

108 阅读14分钟

字节前端二面

拷问项目

interface和type的区别?使用场景?

在 TypeScript 中,interfacetype 都用来描述对象的形状或者定义自定义类型,但它们之间有一些区别:

  1. 语法

    • interface:使用 interface 关键字定义,可以描述对象、函数、类等。
    • type:使用 type 关键字定义,用于定义自定义类型。
  2. 可扩展性

    • interface:可以声明多次同名的 interface,并且它们会自动合并。
    • type:无法声明多次同名的 type,如果重复定义同名 type,会报错。
  3. 适用范围

    • interface:主要用于描述对象的形状,也可以用于描述函数类型、类类型等。
    • type:主要用于定义自定义类型,包括联合类型、交叉类型、字面量类型等。
  4. 扩展性

    • interface:可以通过 extends 关键字扩展其他 interface
    • type:可以通过联合类型和交叉类型进行组合,但无法直接使用 extends 来扩展其他 type

使用场景:

  • 如果需要描述对象的形状或者定义对象的类型,通常使用 interface
  • 如果需要定义自定义类型,包括联合类型、交叉类型等,通常使用 type
  • 在实际项目中,根据需要选择合适的方式来定义类型,有时候也可以结合使用 interfacetype 来达到更灵活的定义。

TCP是如何保证传输的稳定性和可靠性的?

TCP(传输控制协议)通过以下几种机制来保证传输的稳定性和可靠性:

  1. 确认和重传机制:TCP 使用确认和重传机制来确保数据的可靠传输。发送端发送数据后,会等待接收端的确认,如果一定时间内没有收到确认,则会认为数据丢失,触发重传机制重新发送数据。

  2. 序列号和确认号:TCP 在每个数据包中都包含了序列号和确认号。序列号用于标识数据包的顺序,确认号用于确认接收到的数据包。接收端根据序列号和确认号来检查数据包的完整性和顺序,从而保证数据的正确性和完整性。

  3. 流量控制:TCP 使用流量控制机制来控制数据的发送速率,防止发送方发送数据过快导致接收方无法及时处理。接收端通过 TCP 窗口大小来告诉发送端可以接收的数据量,发送端根据窗口大小调整发送速率,从而保持传输的平稳和稳定。

  4. 拥塞控制:TCP 使用拥塞控制机制来避免网络拥塞和丢包现象。当网络拥塞时,发送端会根据丢包情况和网络负载情况调整发送速率,避免造成更严重的拥塞。

  5. 超时重传:TCP 设置了超时时间,在规定的超时时间内没有收到确认,则会触发超时重传机制,重新发送数据。这样可以确保即使数据包丢失或者延迟到达,也能及时进行重传,提高传输的可靠性。

总的来说,TCP 通过以上机制来保证传输的稳定性和可靠性,使得数据能够在不可靠的网络环境中安全地传输。

为什么要进行四次挥手?

Https的加密过程?

TCP(Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层协议。它保证传输的稳定性和可靠性通过以下几个方面:

  1. 序列号和确认应答机制:TCP 使用序列号和确认应答机制来保证数据的可靠传输。发送方给每个数据包分配一个序列号,接收方收到数据后会发送确认应答,指明已经成功接收到的最后一个数据包的序列号。如果发送方未收到确认应答或收到了重复的确认应答,就会重新发送数据。

  2. 重传机制:如果发送方在超时时间内未收到确认应答,或者收到了重复的确认应答,就会认为数据丢失或损坏,会重新发送数据。这个机制确保了数据的可靠性。

  3. 滑动窗口:TCP 使用滑动窗口来进行流量控制和拥塞控制。接收方会告诉发送方它还有多少可用的缓冲区,发送方根据这个信息调整发送的数据量,避免发送方发送速度过快导致接收方无法处理。

  4. 连接管理:TCP 使用三次握手建立连接和四次挥手关闭连接来保证连接的可靠性和稳定性。

至于为什么要进行四次挥手,主要是为了确保双方都能安全地关闭连接。在四次挥手过程中,客户端和服务器各自发送 FIN 和 ACK,用来通知对方自己已经没有数据要发送,并且确认对方的 FIN 消息。这样可以避免出现连接被意外关闭或者数据丢失的情况。

HTTPS(Hypertext Transfer Protocol Secure)是基于 HTTP 的加密传输协议,它通过在传输层加入 SSL/TLS 协议来实现数据的加密和身份认证。HTTPS 的加密过程如下:

  1. 建立连接:客户端向服务器发起连接请求,并且请求建立 HTTPS 连接。

  2. 协商密钥:在建立连接时,客户端和服务器会协商加密算法和密钥的使用方式。这通常包括选择对称加密算法和非对称加密算法,生成会话密钥等。

  3. 证书验证:服务器会向客户端发送自己的证书,证书中包含了服务器的公钥和其他相关信息。客户端会验证服务器的证书是否有效,包括验证证书的签名、证书是否过期、证书是否与域名匹配等。

  4. 加密通信:一旦客户端验证了服务器的证书,它就会生成一个随机的会话密钥,并且使用服务器的公钥进行加密,然后发送给服务器。服务器使用自己的私钥解密得到会话密钥,然后使用会话密钥对通信数据进行加密。

  5. 安全通信:一旦建立了安全通信通道,客户端和服务器之间的通信就会使用会话密钥进行加密和解密,保证数据的安全性和完整性。

整个过程中,HTTPS 通过 SSL/TLS 协议实现了加密通信和身份认证,保护了数据的安全性和用户的隐私。

抓包的流程是什么?

证书的作用是什么?

抓包是指通过特定的工具或软件来捕获计算机网络上的数据包,并分析其中的内容。抓包的流程通常包括以下几个步骤:

  1. 选择抓包工具:首先需要选择适合的抓包工具,常用的抓包工具有 Wireshark、Fiddler、Charles 等。

  2. 配置抓包工具:根据需要配置抓包工具,包括选择网络接口、设置过滤条件等。

  3. 开始抓包:启动抓包工具,开始捕获网络上的数据包。

  4. 分析数据包:对捕获到的数据包进行分析,包括查看数据包的头部信息、查看数据包的内容、分析数据包的协议等。

  5. 过滤和重放:根据需要对数据包进行过滤或者重放,以便进一步分析或者模拟网络请求。

证书的作用是用来证明某个实体的身份或者信息的合法性。在网络通信中,证书通常用于身份验证和数据加密。具体来说,证书的作用包括以下几个方面:

  1. 身份验证:证书可以用来证明某个实体的身份,比如网站的服务器。在 HTTPS 协议中,服务器会使用证书来证明自己的身份,客户端通过验证证书的有效性来确认连接的安全性和合法性。

  2. 数据加密:证书中包含了公钥,可以用来进行数据加密。在 HTTPS 协议中,客户端和服务器会通过协商加密算法和密钥的方式来使用证书中的公钥进行加密通信,保护数据的安全性。

  3. 数据完整性验证:证书中包含了数字签名,可以用来验证数据的完整性。客户端可以使用证书中的公钥来验证数据的签名,以确保数据在传输过程中没有被篡改。

总的来说,证书在网络通信中起着非常重要的作用,可以保障通信的安全性和可靠性。

用useContext和useReducer模拟实现redux

import React, { createContext, useContext, useReducer } from 'react';

// 初始状态
const initialState = {
  count: 0,
};

// 创建一个上下文
const StateContext = createContext();
const DispatchContext = createContext();

// 定义 reducer 函数,根据不同的 action 类型来更新状态
const reducer = (state, action) => {
  switch (action.type) {
    case 'increment':
      return { ...state, count: state.count + 1 }; // 增加 count
    case 'decrement':
      return { ...state, count: state.count - 1 }; // 减少 count
    default:
      throw new Error('Unknown action type'); // 未知的 action 类型
  }
};

// 定义 Provider 组件,它提供了状态和 dispatch 的上下文
export const StateProvider = ({ children }) => {
  const [state, dispatch] = useReducer(reducer, initialState); // 使用 useReducer 来管理状态

  return (
    <StateContext.Provider value={state}>
      <DispatchContext.Provider value={dispatch}>
        {children}
      </DispatchContext.Provider>
    </StateContext.Provider>
  );
};

// 自定义 hook,方便在组件中使用状态和 dispatch
export const useStateContext = () => useContext(StateContext);
export const useDispatchContext = () => useContext(DispatchContext);
import React from 'react';
import { StateProvider, useStateContext, useDispatchContext } from './redux'; // 引入自定义的 Redux

// 计数器组件
const Counter = () => {
  const state = useStateContext(); // 获取状态
  const dispatch = useDispatchContext(); // 获取 dispatch 函数

  // 处理增加计数的函数
  const handleIncrement = () => {
    dispatch({ type: 'increment' }); // 调用 dispatch 函数,并传入 action 对象
  };

  // 处理减少计数的函数
  const handleDecrement = () => {
    dispatch({ type: 'decrement' }); // 调用 dispatch 函数,并传入 action 对象
  };

  return (
    <div>
      <h1>Count: {state.count}</h1> // 显示当前的计数
      <button onClick={handleIncrement}>Increment</button> // 点击按钮触发增加计数的函数
      <button onClick={handleDecrement}>Decrement</button> // 点击按钮触发减少计数的函数
    </div>
  );
};

// 应用组件,包裹计数器组件
const App = () => {
  return (
    <StateProvider>
      <Counter /> // 使用计数器组件
    </StateProvider>
  );
};

export default App;

以上代码创建了一个简单的 Redux 状态管理系统。StateProvider 组件提供了状态和 dispatch 的上下文,Counter 组件中使用 useStateContext 获取状态,使用 useDispatchContext 获取 dispatch 函数,并根据需要调用 dispatch 函数来更新状态。

要使用 useContextuseReducer 模拟实现 Redux,你可以创建一个上下文来存储应用的状态,并使用 useReducer 来处理状态的变化。以下是一个简单的示例:

import React, { createContext, useContext, useReducer } from 'react';

// 初始状态
const initialState = {
  count: 0,
};

// 创建一个上下文
const StateContext = createContext();
const DispatchContext = createContext();

// 定义 reducer 函数
const reducer = (state, action) => {
  switch (action.type) {
    case 'increment':
      return { ...state, count: state.count + 1 };
    case 'decrement':
      return { ...state, count: state.count - 1 };
    default:
      throw new Error('Unknown action type');
  }
};

// 定义 Provider 组件
export const StateProvider = ({ children }) => {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <StateContext.Provider value={state}>
      <DispatchContext.Provider value={dispatch}>
        {children}
      </DispatchContext.Provider>
    </StateContext.Provider>
  );
};

// 自定义 hook,方便在组件中使用状态和 dispatch
export const useStateContext = () => useContext(StateContext);
export const useDispatchContext = () => useContext(DispatchContext);

使用示例:

import React from 'react';
import { StateProvider, useStateContext, useDispatchContext } from './redux';

const Counter = () => {
  const state = useStateContext();
  const dispatch = useDispatchContext();

  const handleIncrement = () => {
    dispatch({ type: 'increment' });
  };

  const handleDecrement = () => {
    dispatch({ type: 'decrement' });
  };

  return (
    <div>
      <h1>Count: {state.count}</h1>
      <button onClick={handleIncrement}>Increment</button>
      <button onClick={handleDecrement}>Decrement</button>
    </div>
  );
};

const App = () => {
  return (
    <StateProvider>
      <Counter />
    </StateProvider>
  );
};

export default App;

在这个示例中,StateProvider 组件提供了一个状态和 dispatch 的上下文,通过 useStateContextuseDispatchContext 自定义 hook 可以在任何子组件中使用。然后,Counter 组件中可以通过 useStateContext 获取状态,通过 useDispatchContext 获取 dispatch 函数,并通过 dispatch 函数来触发状态的更新。

实现一个自定义hook useHover,当鼠标hover到元素上时触发传入的事件

要实现一个自定义的 useHover Hook,可以利用 React 的 useStateuseEffect 钩子来监听鼠标的进入和离开事件。以下是一个简单的实现示例:

import React, { useState, useEffect } from 'react';

const useHover = (onHover) => {
  const [isHovered, setIsHovered] = useState(false);

  const handleMouseEnter = () => {
    setIsHovered(true);
  };

  const handleMouseLeave = () => {
    setIsHovered(false);
  };

  useEffect(() => {
    const element = document.getElementById('hover-element'); // 替换为你需要监听的元素的 ID 或者其他选择器
    if (element) {
      element.addEventListener('mouseenter', handleMouseEnter);
      element.addEventListener('mouseleave', handleMouseLeave);
    }

    return () => {
      if (element) {
        element.removeEventListener('mouseenter', handleMouseEnter);
        element.removeEventListener('mouseleave', handleMouseLeave);
      }
    };
  }, []);

  useEffect(() => {
    if (isHovered && onHover) {
      onHover();
    }
  }, [isHovered]);

  return isHovered;
};

export default useHover;

使用示例:

import React from 'react';
import useHover from './useHover';

const HoverComponent = () => {
  const handleHover = () => {
    console.log('Hovered!');
  };

  const isHovered = useHover(handleHover);

  return (
    <div id="hover-element">
      {isHovered ? 'Hovered!' : 'Not hovered!'}
    </div>
  );
};

export default HoverComponent;

在这个示例中,useHover 自定义 Hook 接受一个回调函数 onHover,用于处理鼠标悬停事件。它返回一个布尔值 isHovered,表示元素是否被悬停。当元素被悬停时,onHover 回调函数被触发。

作者:会飞的佩格斯
链接:www.nowcoder.com/discuss/527…
来源:牛客网

字节懂车帝前端二面 8.28

深挖项目

不同路由实现原理区别

react-query原理,手写实现uesQuery

不同路由实现原理的区别通常取决于使用的技术和框架。以下是一些常见的路由实现原理及其区别:

  1. 客户端路由

    • Hash 路由:基于 URL 中的哈希部分(#)来实现路由,通过监听 hashchange 事件来响应路由变化。优点是兼容性好,缺点是 URL 中带有哈希符号,不够美观。
    • History API 路由:基于浏览器的 History API 来实现路由,通过 pushStatereplaceState 方法来改变 URL,可以实现更加友好的 URL。优点是 URL 更美观,缺点是需要服务器端支持。
  2. 服务器端路由

    • 基于文件路径的路由:通过服务器端配置,将不同的 URL 映射到不同的文件或处理程序上,然后由服务器端处理路由逻辑并返回相应的内容。
    • 基于路由框架的路由:使用诸如 Express.js、Koa.js 等路由框架,在服务器端定义路由规则和处理程序,根据 URL 路径来匹配路由并执行相应的处理逻辑。

React-Query 是一个 React 应用中用于数据获取和管理的库,其原理是通过使用 React 的 Context API 和自定义 Hooks 来管理数据的获取和状态。它的核心思想是将数据请求和状态统一管理,以提供更好的数据缓存和更新机制,从而简化数据管理的复杂度。

要手写实现 useQuery,你需要实现以下几个关键功能:

  1. 数据获取:使用 useEffect 钩子来触发数据请求,并使用异步函数来获取数据。
  2. 数据状态管理:使用 React 的 useState 钩子来管理数据的加载状态、数据和错误状态。
  3. 数据缓存:使用 React 的 Context API 来创建一个全局的数据缓存,以便在多个组件中共享数据。
  4. 数据更新:使用 useEffect 钩子来监听依赖变化,并在依赖变化时重新触发数据请求。

以下是一个简单的示例代码,展示了如何手写实现 useQuery

import React, { useState, useEffect, createContext, useContext } from 'react';

const DataContext = createContext();

export const useQuery = (getData) => {
  const [data, setData] = useState(null);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      setIsLoading(true);
      try {
        const result = await getData();
        setData(result);
      } catch (error) {
        setError(error);
      }
      setIsLoading(false);
    };

    fetchData();
  }, []);

  return { data, isLoading, error };
};

export const DataProvider = ({ children }) => {
  return (
    <DataContext.Provider value={useQuery}>
      {children}
    </DataContext.Provider>
  );
};

export const useData = () => {
  return useContext(DataContext);
};

在上面的示例中,useQuery 自定义 Hook 负责发起数据请求,并返回数据、加载状态和错误状态。DataProvider 组件使用 React 的 Context API 创建一个数据提供者,以便在整个应用中共享数据。useData 自定义 Hook 负责在组件中使用数据。

webpack怎么配置,eslint用了哪些规则,git hook实现代码commit前检验怎么只测增量代码。

Webpack配置

Webpack 是一个模块打包工具,用于将各种资源(例如 JavaScript、CSS、图片等)打包成静态资源。要配置Webpack,你需要创建一个名为 webpack.config.js 的配置文件,然后在其中定义Webpack的配置选项。以下是一个简单的Webpack配置示例:

const path = require('path');

module.exports = {
  entry: './src/index.js', // 入口文件
  output: { // 输出配置
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  module: { // 模块配置
    rules: [
      {
        test: /\.js$/, // 使用正则匹配要处理的文件
        exclude: /node_modules/, // 排除 node_modules 目录
        use: 'babel-loader' // 使用 babel-loader 处理 JavaScript 文件
      },
      {
        test: /\.css$/, // 使用正则匹配要处理的文件
        use: ['style-loader', 'css-loader'] // 使用 style-loader 和 css-loader 处理 CSS 文件
      },
      {
        test: /\.(png|jpg|gif)$/, // 使用正则匹配要处理的文件
        use: 'file-loader' // 使用 file-loader 处理图片文件
      }
    ]
  },
  plugins: [
    // 插件配置
    // 可以在这里配置各种插件,例如 HtmlWebpackPlugin、MiniCssExtractPlugin 等
  ]
};

上面的配置文件中定义了入口文件、输出配置、模块配置和插件配置等内容,你可以根据具体项目需求进行相应的配置。

ESLint规则

ESLint 是一个用于检查 JavaScript 代码规范的工具,它可以帮助你发现和修复代码中的潜在问题。在 ESLint 配置文件(例如 .eslintrc.js)中,你可以定义一系列规则,用于检查代码的格式、风格和质量。例如:

module.exports = {
  // 指定代码运行的环境
  env: {
    browser: true,
    es2021: true
  },
  // 指定扩展的规则配置
  extends: [
    'eslint:recommended',
    'plugin:react/recommended'
  ],
  // 自定义规则
  rules: {
    'no-console': 'warn', // 禁止使用 console.log,将警告输出
    'no-unused-vars': 'error' // 禁止定义未使用的变量,将错误输出
  }
};

在上面的配置中,我们定义了两个规则:禁止使用 console.log(警告级别)和禁止定义未使用的变量(错误级别)。

Git Hook实现代码Commit前检验

Git Hook 是 Git 提供的一种机制,允许你在执行特定 Git 命令时自动触发一些操作。通过在项目的 .git/hooks 目录下创建相应的 hook 脚本文件,你可以实现在代码 Commit 前检验代码,例如运行测试、Lint 检查等操作。

要实现代码 Commit 前只检验增量代码,你可以借助 Git 提供的一些命令来判断即将提交的文件是否是新增或修改过的文件。例如,你可以使用 git diff --cached --name-only 命令来获取即将提交的文件列表,然后根据文件列表进行检查。

下面是一个示例的 Git Hook 脚本,用于在代码 Commit 前运行 ESLint 检查:

#!/bin/sh

# 获取即将提交的文件列表
files=$(git diff --cached --name-only)

# 运行 ESLint 检查
eslint ${files}

在上面的脚本中,${files} 表示即将提交的文件列表,通过 eslint ${files} 命令来运行 ESLint 检查。你可以根据需要修改脚本,实现其他检查逻辑。将该脚本保存为 .git/hooks/pre-commit 文件,并赋予执行权限,即可在代码 Commit 前运行该脚本进行代码检查。

观察者模式,手写实现一个事件总线,怎么用。

观察者模式是一种设计模式,用于对象之间的一对多依赖关系。在观察者模式中,一个对象(称为主题或可观察对象)维护一组依赖于它的其他对象(称为观察者),当主题的状态发生变化时,它会通知所有的观察者,使它们能够自动更新自己。

下面是一个简单的手写实现事件总线(EventBus)的示例:

class EventBus {
  constructor() {
    // 存储事件和对应的回调函数
    this.events = {};
  }

  // 注册事件监听器
  on(event, callback) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(callback);
  }

  // 触发事件
  emit(event, ...args) {
    const callbacks = this.events[event];
    if (callbacks) {
      callbacks.forEach(callback => {
        callback(...args);
      });
    }
  }

  // 移除事件监听器
  off(event, callback) {
    const callbacks = this.events[event];
    if (callbacks) {
      this.events[event] = callbacks.filter(cb => cb !== callback);
    }
  }
}

// 创建事件总线实例
const eventBus = new EventBus();

// 注册事件监听器
eventBus.on('update', data => {
  console.log('Received update event:', data);
});

// 触发事件
eventBus.emit('update', { message: 'Hello, EventBus!' });

// 移除事件监听器
eventBus.off('update');

在上面的示例中,我们定义了一个 EventBus 类,它具有 onemitoff 三个方法,分别用于注册事件监听器、触发事件和移除事件监听器。你可以通过实例化 EventBus 类来创建一个事件总线,并使用 on 方法注册事件监听器,然后使用 emit 方法触发事件,最后使用 off 方法移除事件监听器。

使用事件总线时,你可以在应用程序的任何地方注册事件监听器,并在其他地方触发相应的事件,从而实现不同模块之间的通信。

做了哪些基于umi修改的配置

对于基于 Umi 的项目,你可能会根据项目需求进行一些配置修改。以下是一些常见的基于 Umi 修改的配置:

  1. 路由配置:通过修改 config/routes.ts 文件来定义路由配置,包括路由路径、组件路径以及路由配置项等。

  2. 代理配置:通过修改 config/config.ts 文件来配置代理规则,实现前端请求的转发。

  3. 主题配置:通过修改 config/config.ts 文件来配置项目的主题样式,包括颜色、字体等。

  4. 环境变量配置:通过修改 config/config.ts 文件来配置环境变量,例如不同环境下的接口地址等。

  5. 插件配置:通过修改 config/config.ts 文件来配置 Umi 插件,例如 antd 插件、dva 插件等。

  6. 构建配置:通过修改 config/config.ts 文件来配置项目的构建配置,例如打包输出目录、打包文件名等。

  7. 自定义 webpack 配置:通过修改 config/config.ts 文件中的 chainWebpack 方法来添加自定义的 webpack 配置,例如添加自定义 loader、plugin 等。

  8. 布局配置:通过修改 config/layout.tsx 文件来配置项目的布局结构,包括顶部导航、侧边栏菜单等。

这些是一些常见的基于 Umi 修改的配置,根据具体项目需求,你可能会进行更多的配置修改来满足项目的功能和性能要求。

一个钟,时间费在我手写上

8.30早上 寄

作者:boscograb
链接:www.nowcoder.com/discuss/525…
来源:牛客网

字节提前批前端二面()45min

  1. 自我介绍(5min)
  2. 聊实习项目 (15min)
  3. 手撕(10min):模板字符串替换(实现功能,但是面试官说不是想考察的点,应该用正则+字符串替换实现,由于正则忘了,就只讲了下替换的思想,面试官就让下一题了)

理解了,让我用正则表达式来替换模板字符串中的变量值。假设模板字符串中的变量格式为 ${variable},可以使用正则表达式来匹配这种格式的变量,并将其替换为对应的值。以下是一个示例实现:

function replaceTemplate(template, values) {
    // 定义正则表达式匹配模板字符串中的变量
    const regex = /\${(.*?)}/g;

    // 使用 replace 方法替换模板字符串中的变量
    return template.replace(regex, (match, variable) => {
        // 将匹配到的变量作为 key 在 values 对象中查找对应的值
        return values[variable.trim()] || match;
    });
}

// 示例用法
const template = 'Hello, ${name}! Today is ${day}.';
const values = {
    name: 'John',
    day: 'Monday'
};

const result = replaceTemplate(template, values);
console.log(result); // 输出: Hello, John! Today is Monday.

在上面的示例中,replaceTemplate 函数接受模板字符串和一个对象作为参数,使用正则表达式匹配模板字符串中的变量,并通过在对象中查找对应的值来替换变量。

  1. 手撕(10min):两个数组合并改编题(不难,A了)
  2. 事件循环打印顺序题(5min)(需要自己讲解下,A了)

总共只有45min,而且聊实习项目感觉更多是了解做了什么事情,只有个别让详细讲解,不知道是不是KPI啊!希望能约三面吧!

作者:不努力怎么幸运
链接:www.nowcoder.com/discuss/513…
来源:牛客网