最新的字节校招面经(详细答案)

1,661 阅读15分钟

前言

学校面试交流群里,传出了第一个字节校招offer!字节, 爱在初秋! 大佬给了面经,我们一起看看,沾沾喜气。

image.png

码字不易 如果觉得本文有帮助 记得点赞三连哦 十分感谢!

一面

1. 自我介绍

可以看介绍公式

2. JS有哪些基础类型

有 7种, 分别是:StringNumberBooleanUndefinedNullSymbolBigInt

3. 不同类型数据存储在什么位置?

不同类型的数据存储在不同的位置,主要包括栈(Stack)和堆(Heap)。

原始类型是按值传递的,并且存储在栈(Stack)中

引用类型是按引用传递的,实际的数据存储在堆(Heap)中,而栈中存储的是指向堆中数据的引用(指针)。

4. 数组在其他语言里会把它当作基本类型,JS里为什么数组是Object?

在其他语言中,数组通常被视为基本类型,具有固定长度和连续存储。而在 JavaScript 中,数组是对象,因为 JavaScript 是动态类型语言,允许数组动态增减元素、添加属性和方法。数组继承自 Object 原型,具备对象的灵活性和丰富的内置方法,如 pushpopmap等,这使得数组操作更加方便和强大。当然也是为了满足js 原型面向对象的需要。

5. 讲讲垃圾回收

垃圾回收(Garbage Collection, GC) 是指自动管理内存的过程,释放不再使用的内存,防止内存泄漏。

常见的垃圾回收算法

  • 标记-清除(Mark and Sweep) :从根对象开始,标记所有可达对象,然后清除未标记的对象。

  • 引用计数(Reference Counting) :跟踪每个对象的引用次数,当引用次数为零时,释放该对象。

例子

  • Event Loop 同步任务和微任务执行完毕,执行栈清空时,垃圾回收有机会运行
  • 组件卸载生命周期时回收一些耗时、巨大的对象。
  • 闭包自由变量不会回收
  • WeakMapWeakSet 通过弱引用存储对象,不会阻止垃圾回收,从而帮助避免内存泄漏,确保不再使用的对象可以被及时释放。
  • 设置为null,手动垃圾回收。

6. typeof Null 返回什么?

NaN(Not-a-Number)是一个特殊的数值,表示无效的数值,比如分母为0...

所以, 返回值是'number'

面试官一直追问,是否确定,补充回答:

确定。但 NaN 有一些独特的性质,比如 NaN !== NaN。我们通常使用 isNaNNumber.isNaN 来检查一个值是否是 NaN

7. 0.1 + 0.2 === 0.3 吗?

因为采用了IEEE754码制,十进制浮点数无法完全精确转换为二进制浮点数。

QQ_1728615205768.png

0.1 (十进制) = 0.00011001100110011001100110011001100110011001100110011... (二进制)

8. == 判断

== 判断是值相等就相等,类型不同没关系(类型转换)。而===既要值相等,又要类型一致,更严格,建议用后者。

== 操作符在进行相等性判断时,会进行类型转换,使两个操作数的类型相同后再进行比较。

  • nullundefined

如果一个操作数是 null 或 undefined,并且另一个操作数也是 null 或 undefined,则结果为 true。否则,结果为 false

console.log(null == undefined); // true console.log(null == null); // true console.log(undefined == undefined); // true console.log(null == 0); // false
  • 布尔值

如果一个操作数是布尔值,布尔值会被转换为数字(true 转换为 1false 转换为 0)。

console.log(true == 1); // true console.log(false == 0); // true console.log(true == '1'); // true console.log(false == '0'); // true console.log(false == ''); // true
  • 字符串和数字

如果一个操作数是字符串,另一个操作数是数字,字符串会被转换为数字。

console.log('1' == 1); // true console.log('0' == 0); // true console.log('10' == 10); // true console.log('10' == '10'); // true console.log('10' == 11); // false console.log('10px' == 10); // false
  • 对象和非对象

如果一个操作数是对象,另一个操作数是非对象,对象会被转换为原始值(使用 valueOf 或 toString 方法)。

console.log([] == false); // true console.log([0] == false); // true console.log(['0'] == false); // true console.log([1] == true); // true console.log({ valueOf: () => 1 } == 1); // true console.log({ toString: () => '1' } == 1); // true

null == 0 这会怎么转换呢?

null == 0 的类型转换规则

根据 == 操作符的类型转换规则,null == 0 的转换过程如下:

  1. 检查 nullundefined

    • 如果一个操作数是 null 或 undefined,并且另一个操作数也是 null 或 undefined,则结果为 true
    • 否则,结果为 false

    null == 0 中,一个操作数是 null,另一个操作数是 0。由于 0 不是 nullundefined,因此不满足这条规则。

  2. 检查布尔值

    • 如果一个操作数是布尔值,布尔值会被转换为数字(true 转换为 1false 转换为 0)。

    null == 0 中,没有布尔值,因此不适用这条规则。

  3. 检查字符串和数字

    • 如果一个操作数是字符串,另一个操作数是数字,字符串会被转换为数字。

    null == 0 中,没有字符串,因此不适用这条规则。

  4. 检查对象和非对象

    • 如果一个操作数是对象,另一个操作数是非对象,对象会被转换为原始值(使用 valueOf 或 toString 方法)。

    null == 0 中,null 不是对象,因此不适用这条规则。

由于 null0 之间的类型转换规则都不适用,null == 0 的结果为 false

9. Symbol 的出现是为了解决什么问题?

Symbol 的出现是为了创建唯一标识符,解决对象属性名冲突的问题,确保不同模块或库之间的属性名不会相互干扰。

比如:

vue-routeruseRouter hook功能使用了Symbol来标记唯一值。

const ROUTER_KEY = Symbol('router');
function useRouter() {
  const router = inject(ROUTER_KEY);
  if (!router) {
    throw new Error('useRouter must be used within a router setup.');
  }
  return router;
}

Symbol 是唯一的,即使使用相同的描述符创建 Symbol,它们仍然是不同的值。这确保了属性的唯一性,避免了意外覆盖。在多人协作的大型项目中非常有用。

10. interface 、 type 区别?

interfacetype 在 TypeScript 中都用于类型定义,但它们有一些关键的区别:

  1. 使用范围

   - interface 主要用于定义对象类型的形状。

   - type 可以用于任何类型的定义,包括联合类型、元组等。   

// 基本数据类型
type StringOrNumber = string | number;
// 联合数据类型
type User = { name: string; age: number } | null;
// 元祖类型
type Point = [number, number];
   const point1: Point= [0, 0];
// 函数类型
type GreetFunction = (name: string) => void;
   const greet: GreetFunction = name => console.log(`Hello, ${name}!`);
  1. 继承

   - interface 支持嵌套和混合(intersection)继承。

     ```typescript

     interface A {

       a: number;

     }

     interface B {

       b: string;

     }

     // 混合接口

     interface C extends A, B {}

     type D = A & B; // 类似于接口的混合类型定义

     ```

   - type 不支持嵌套继承,但可以进行联合类型和交叉类型的定义。

  1. 泛型

   - interface 可以用于定义泛型接口。

     ```typescript

     interface I {

       value: T;

     }

     type T = { value: T };

     ```

   - type 也可以用于定义泛型类型别名。 4. 兼容性

   - interface 更符合面向对象编程的思维,适用于描述类的继承关系。

   - type 更灵活,可以更广泛地用于各种类型的定义。

  1. 可读性和语义化

   - 使用 interface 时,接口名称通常更具描述性,如 UserInterface

   - 使用 type 时,类型别名名称通常更简洁,如 UserId.

总结起来:

  • 如果你需要定义一个对象的形状或接口结构,使用 interface

  • 如果你需要进行联合类型或其他类型的复杂定义,使用 type

11. omit、pick、partial有什么用?

这个就有点深了, 面试官是不想让我过一面吗?

  • 首先先来介绍omit
`omit<T, K>` 用于从类型 `T` 中排除属性 `K`interface Person {
  id: number;
  name: string;
  email: string;
}
type PersonWithoutEmail = Omit<Person, 'email'>; // { id: number; name: 
string }
  • pick

pick<T, K> 用于从类型 T 中提取指定的属性 K


interface Person {
  id: number;
  name: string;
  email: string;
}
type PersonName = Pick<Person, 'name'>; // { name: string }

在这个例子中,PersonName 类型从 Person 中提取了 name 属性。

  • partial Partial<T> 用于将类型 T 的所有属性标记为可选(即,可以是 undefined)。 它的主要用途是使对象的某些或全部属性变为可选。

interface Person {
  id: number;
  name: string;
  email: string;
}
type PartialPerson = Partial<Person>; // { id?: number; name?: string; 
email?: string }

在这个例子中,PartialPerson 类型将 idnameemail 属性都标记为可选属性。

12. 返回boolean的函数如何通过is关键字给ts编辑器提供类型推断?

在 TypeScript 中,你可以使用 is 关键字来创建类型守卫,并在编辑器中提供更丰富的类型信息。具体来说,你可以定义一个类型守卫函数,该函数返回 boolean 并用于判断某个对象是否符合特定的类型。

以下是一个示例,展示如何通过 is 关键字为 TypeScript 编辑器提供类型推断:


interface Person {

  name: string;

  age: number;

}
interface Animal {

  species: string;

  sound: () => void;

}
function isPerson(obj: Person | Animal): obj is Person {

  return (obj as Person).name !== undefined;

}

function isAnimal(obj: Person | Animal): obj is Animal {

  return (obj as Animal).sound !== undefined;

}
const person = { name: "Alice", age: 30 };
const animal = { species: "Dog", sound: () => console.log("Woof!") };
if (isPerson(person)) {
  // TypeScript 知道 `person` 是 `Person` 类型

  console.log(person.name); // IntelliSense 提供 `name` 属性

} else if (isAnimal(animal)) {

  // TypeScript 知道 `animal` 是 `Animal` 类型

  animal.sound(); // IntelliSense 提供 `sound` 方法
}

在这个示例中:

  1. 定义了两个接口 PersonAnimal

  2. 创建了两个类型守卫函数 isPersonisAnimal,它们分别用于判断传入的对象是否为 PersonAnimal 类型。

  3. if 语句中使用这些类型守卫来推断对象的类型,并在控制台输出相应的属性或方法。

13. for of 原理, 什么是迭代器和异步迭代器

for...of 通过迭代器协议来遍历可迭代对象,比如数组、字符串、Map、Set 等。

  1. 迭代器协议(Iterator Protocol) :for...of 循环依赖于对象实现的迭代器协议。一个对象要支持 for...of,它必须实现 Symbol.iterator 方法,该方法返回一个迭代器对象。

  2. 迭代器对象:迭代器对象必须有一个 next() 方法,next() 方法返回一个对象,包含两个属性:value:当前迭代的值。done:布尔值,表示是否迭代完成。

  3. 工作流程

    for...of 循环会调用可迭代对象的 Symbol.iterator 方法,获取迭代器。

    然后循环调用迭代器的 next() 方法,取出每一项的 value,直到 done 为 true。

14. Tailwind 优点

  • 快速开发

TailwindCss 原子类,让我们几乎不怎么需要写css。

  • 可定制

tailwind.config.js,可以轻松地调整颜色、间距、字体、主题定制等

module.exports = {
  theme: {
    extend: {
      colors: {
        primary: {
          light: '#f0f0f0',
          DEFAULT: '#d0d0d0',
          dark: '#a0a0a0',
        },
        secondary: {
          light: '#e0e0e0',
          DEFAULT: '#c0c0c0',
          dark: '#909090',
        },
      },
    },
  },
  variants: {
    extend: {},
  },
  plugins: [],
};
  • 响应式设计

Tailwind 提供了丰富的响应式类,可以轻松地为不同的屏幕尺寸定义样式。

smmdlgxl2xl 这些代表不同尺寸的设备。

14. 有用过别的css 预编译器吗?

有的, Stylus & Sass。

他们有以下css 不具备的功能, 让css 变得强大。

  • 变量

当然 css 4 支持变量了。

  • 嵌套

tab 缩进嵌套, 支持css模块化

  • 选择器继承

&

  • mixin

函数化的 css复用

15. 跨域以及解决方法

  • 1. JSONP

    客户端定义一个回调函数, 通过 <script> 标签请求服务器资源,并在 URL 中指定回调函数名,服务器返回一个 JavaScript 脚本,该脚本调用客户端定义的回调函数,并传入数据。 <script> 标签加载资源并不跨域,但只能用于GET请求

    1. CORS(Cross-Origin Resource Sharing)

    CORS 是一种标准的跨域解决方案,通过在服务器端设置响应头来允许跨域请求。

    服务器在响应头中添加 Access-Control-Allow-Origin Access-Control-Allow-Methods, Access-Control-Allow-Headers, 如果请求方法是 PUTDELETECONNECTOPTIONSTRACEPATCH 等, 或头部信息包含**Authorization**等,会先发送一个OPTION 预检请求。

    1. Nginx 代理
    1. WebSocket
    1. PostMessage:适用于窗口或 iframe 之间的跨域通信。

16. Cookie 上的samesite 属性

SameSite 属性用于控制 Cookie 是否随跨站请求一起发送,有三个值:StrictLaxNone,分别表示严格不随跨站请求发送、大部分跨站请求不发送和始终发送。

17. Vite 为什么比别的构建工具快

Vite 采用即时编译技术,在开发模式下实现零构建延迟,直接在浏览器中运行经过打 包的代码,显著提高了开发效率和用户体验。

比如: index.html 是这么引入的。

<script type="module" src="...js"></script>

快相对于webpack, 因为他不支持esm, 需要将所有文件按依赖关系完全打包才可以运行, 所以较慢。

18. 了解rollup吗?

Rollup 是一个 JavaScript 模块打包工具,用于构建高效的生产环境代码。 我们在学习中常接触的是vite,以及老牌的webpack

Rollup 的主要优势在于其模块树的静态分析能力和更小的输出文件体积,特别适合用于库的构建和较小项目的开发。此外,Rollup 的配置相对简单,更适合命令行使用,且在处理树 shaking 方面表现优异。

面试官在vite 外,还问了一道rollup 的题目, 看来是想在工程化上希望我们带给他惊喜。

19. 手写函数柯里化

柯里化的本质是将一个多参数函数转换为一系列单参数函数的过程


function curry(fn) {

    return function curried(...args) {

        if (args.length >= fn.length) {

            return fn.apply(this, args);

        } else {

            return function(...moreArgs) {

                return curried.apply(this, args.concat(moreArgs));
            };

        }
    };
}
// 示例用法
const add = curry((a, b, c) => a + b + c);
console.log(add(1)(2)(3)); // 输出 6

20. 手写array.flat

这是常考的数组扁平化,我参考了# 面试20个人居然没有一个写出数组扁平化?如何手写flat函数

扁平化就是将多维数组变成一维数组,不存在数组的嵌套。

  • es6 自带flat方法
const arr = [1, [2, [3, [4, 5]]], 6] 
function flatten(params) { 
    return params.flat(Infinity) 
} 
console.log(flatten(arr));
  • toString 如果数组的项全为数字,可以使用join(),toString()可以利用数组toString() 转为字符串

QQ_1729907440917.png

function flatten(arr) {
  return arr.toString().split(',').map(item =>parseFloat(item))
}
console.log(flatten(arr));
// 输出:[ 1, 2, 3, 4, 5, 6 ]
  • 使用正则替换

看到嵌套的数组,如果在字符串的角度上看就是多了很多[ 和],如果把它们替换就可以实现简单的扁平化

function flatten (arr) {
  console.log('JSON.stringify(arr)', typeof JSON.stringify(arr))
  let str= JSON.stringify(arr).replace(/(\[|\])/g, '');
  str = '[' + str + ']';
  arr = JSON.parse(str);
  return arr
}
console.log(flatten(arr))
  • 循环递归

循环 + concat + push

function flatten(arr) {
  let result = [];
  for (let i = 0; i < arr.length; i++) {
    if (Array.isArray(arr[i])) {
      result = result.concat(flatten(arr[i]));
    } else {
      result.push(arr[i])
    }
  }
  return result
}

console.log(flatten(arr));

  • reduce
const arr = [1, [[2, 3], 4],5]
const flatten = (arr, deep = 1) => {
    if (deep <= 0) return arr;
    return arr.reduce((res, curr) => res.concat(Array.isArray(curr) ? flatten(curr, deep - 1) : curr), [])
}
// function flatten (arr,deep=1) {
// return   arr.reduce((acc,val) => acc.concat(Array.isArray(val)? flatten(val,deep-1):val),[])
// }
console.log(arr, Infinity);
// 输出:[ 1, 2, 3, 4, 5, 6 ]

  • 使用堆栈 stack 避免递归
var arr1 = [1,2,3,[1,2,3,4, [2,3,4]]];
function flatten(arr) {
  const stack = [...arr];
  const res = [];
  while (stack.length) {
    // 使用 pop 从 stack 中取出并移除值
    const next = stack.pop();
    if (Array.isArray(next)) {
      // 使用 push 送回内层数组中的元素,不会改动原始输入
      stack.push(...next);
    } else {
      res.push(next);
    }
  }
  // 反转恢复原数组的顺序
  return res.reverse();
}
flatten(arr1);// [1, 2, 3, 1, 2, 3, 4, 2, 3, 4]

21. 有没有写过nodejs的服务器端编程?比如借口?

搞过koa, 基于restfulmvc,编写过前端需要的接口。 实习中,有时候需要针对后段,写一些BFF业务,可以轻松搞定。

BFF(Backend For Frontend)模式是指为特定的前端应用创建专用后端服务的一种架构设计,旨在优化API响应和提高用户体验。这种模式允许后端更直接地满足前端的具体需求,比如数据聚合、协议转换等。

22. ["1", "2"].map(parseInt)和["1", "2"].map(item => parseInt(item))区别

["1", "2"].map(parseInt)["1", "2"].map(item => parseInt(item)) 这两种方式在表面上看起来都是将数组中的字符串转换为整数,但实际上它们的行为是不同的。

["1", "2"].map(parseInt)

当你直接传递 parseInt 作为 .map() 的回调函数时,.map() 会自动为这个函数提供三个参数:当前元素、当前索引和数组本身。parseInt 函数的签名是 parseInt(string, radix?),其中 string 是要解析的字符串,而 radix 是可选的基数(默认值为10)。

因此,在这种情况下,parseInt 被调用时:

  • 第一个元素 "1" 会被传入 parseInt("1", 0),因为第一个元素的索引是0。
  • 第二个元素 "2" 会被传入 parseInt("2", 1),因为第二个元素的索引是1。

由于基数不是10(或有效的范围),这会导致非预期的结果。具体来说:

  • parseInt("1", 0) 可能返回 1(尽管基数0通常意味着自动检测基数,但在此处可能不适用)。
  • parseInt("2", 1) 会返回 NaN,因为基数1是无效的。

所以结果将是 [1, NaN] 或类似的非预期输出。

["1", "2"].map(item => parseInt(item))

在这种写法中,你使用了一个箭头函数来明确地只传递当前元素给 parseInt,忽略了索引和数组这两个额外的参数。这样,每个元素都会被正确地解析为十进制整数:

  • parseInt("1") 返回 1
  • parseInt("2") 返回 2 最终结果是一个包含整数的数组 [1, 2]

23. function A(){}; A.a = 'b' 能执行吗?

可以执行, 函数是一等对象,可以给对象添加属性。

总结

校招确实比实习难挺多的,比如js底层、ts、全栈等考察点。只有身经百战,才能经受住如此考验。佩服佩服!

下次在更新二面面试题。