前言
学校面试交流群里,传出了第一个字节校招offer!字节, 爱在初秋! 大佬给了面经,我们一起看看,沾沾喜气。
码字不易 如果觉得本文有帮助 记得点赞三连哦 十分感谢!
一面
1. 自我介绍
可以看介绍公式
2. JS有哪些基础类型
有 7种, 分别是:String、Number、Boolean、Undefined、Null、Symbol、BigInt
3. 不同类型数据存储在什么位置?
不同类型的数据存储在不同的位置,主要包括栈(Stack)和堆(Heap)。
原始类型是按值传递的,并且存储在栈(Stack)中
引用类型是按引用传递的,实际的数据存储在堆(Heap)中,而栈中存储的是指向堆中数据的引用(指针)。
4. 数组在其他语言里会把它当作基本类型,JS里为什么数组是Object?
在其他语言中,数组通常被视为基本类型,具有固定长度和连续存储。而在 JavaScript 中,数组是对象,因为 JavaScript 是动态类型语言,允许数组动态增减元素、添加属性和方法。数组继承自 Object 原型,具备对象的灵活性和丰富的内置方法,如 push、pop 和 map等,这使得数组操作更加方便和强大。当然也是为了满足js 原型面向对象的需要。
5. 讲讲垃圾回收
垃圾回收(Garbage Collection, GC) 是指自动管理内存的过程,释放不再使用的内存,防止内存泄漏。
常见的垃圾回收算法
-
标记-清除(Mark and Sweep) :从根对象开始,标记所有可达对象,然后清除未标记的对象。
-
引用计数(Reference Counting) :跟踪每个对象的引用次数,当引用次数为零时,释放该对象。
例子
Event Loop同步任务和微任务执行完毕,执行栈清空时,垃圾回收有机会运行- 组件卸载生命周期时回收一些耗时、巨大的对象。
- 闭包自由变量不会回收
WeakMap和WeakSet通过弱引用存储对象,不会阻止垃圾回收,从而帮助避免内存泄漏,确保不再使用的对象可以被及时释放。- 设置为null,手动垃圾回收。
6. typeof Null 返回什么?
NaN(Not-a-Number)是一个特殊的数值,表示无效的数值,比如分母为0...
所以, 返回值是'number'
面试官一直追问,是否确定,补充回答:
确定。但 NaN 有一些独特的性质,比如 NaN !== NaN。我们通常使用 isNaN 或 Number.isNaN 来检查一个值是否是 NaN。
7. 0.1 + 0.2 === 0.3 吗?
因为采用了IEEE754码制,十进制浮点数无法完全精确转换为二进制浮点数。
0.1 (十进制) = 0.00011001100110011001100110011001100110011001100110011... (二进制)
8. == 判断
== 判断是值相等就相等,类型不同没关系(类型转换)。而===既要值相等,又要类型一致,更严格,建议用后者。
== 操作符在进行相等性判断时,会进行类型转换,使两个操作数的类型相同后再进行比较。
null和undefined
如果一个操作数是 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 转换为 1,false 转换为 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 的转换过程如下:
-
检查
null和undefined:- 如果一个操作数是
null或undefined,并且另一个操作数也是null或undefined,则结果为true。 - 否则,结果为
false。
在
null == 0中,一个操作数是null,另一个操作数是0。由于0不是null或undefined,因此不满足这条规则。 - 如果一个操作数是
-
检查布尔值:
- 如果一个操作数是布尔值,布尔值会被转换为数字(
true转换为1,false转换为0)。
在
null == 0中,没有布尔值,因此不适用这条规则。 - 如果一个操作数是布尔值,布尔值会被转换为数字(
-
检查字符串和数字:
- 如果一个操作数是字符串,另一个操作数是数字,字符串会被转换为数字。
在
null == 0中,没有字符串,因此不适用这条规则。 -
检查对象和非对象:
- 如果一个操作数是对象,另一个操作数是非对象,对象会被转换为原始值(使用
valueOf或toString方法)。
在
null == 0中,null不是对象,因此不适用这条规则。 - 如果一个操作数是对象,另一个操作数是非对象,对象会被转换为原始值(使用
由于 null 和 0 之间的类型转换规则都不适用,null == 0 的结果为 false。
9. Symbol 的出现是为了解决什么问题?
Symbol 的出现是为了创建唯一标识符,解决对象属性名冲突的问题,确保不同模块或库之间的属性名不会相互干扰。
比如:
vue-router的useRouter 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 区别?
interface 和 type 在 TypeScript 中都用于类型定义,但它们有一些关键的区别:
- 使用范围:
- 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}!`);
- 继承:
- interface 支持嵌套和混合(intersection)继承。
```typescript
interface A {
a: number;
}
interface B {
b: string;
}
// 混合接口
interface C extends A, B {}
type D = A & B; // 类似于接口的混合类型定义
```
- type 不支持嵌套继承,但可以进行联合类型和交叉类型的定义。
- 泛型:
- interface 可以用于定义泛型接口。
```typescript
interface I {
value: T;
}
type T = { value: T };
```
- type 也可以用于定义泛型类型别名。
4. 兼容性:
- interface 更符合面向对象编程的思维,适用于描述类的继承关系。
- type 更灵活,可以更广泛地用于各种类型的定义。
- 可读性和语义化:
- 使用 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 类型将 id、name 和 email 属性都标记为可选属性。
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` 方法
}
在这个示例中:
-
定义了两个接口
Person和Animal。 -
创建了两个类型守卫函数
isPerson和isAnimal,它们分别用于判断传入的对象是否为Person或Animal类型。 -
在
if语句中使用这些类型守卫来推断对象的类型,并在控制台输出相应的属性或方法。
13. for of 原理, 什么是迭代器和异步迭代器
for...of 通过迭代器协议来遍历可迭代对象,比如数组、字符串、Map、Set 等。
-
迭代器协议(Iterator Protocol) :for...of 循环依赖于对象实现的迭代器协议。一个对象要支持 for...of,它必须实现 Symbol.iterator 方法,该方法返回一个迭代器对象。
-
迭代器对象:迭代器对象必须有一个 next() 方法,next() 方法返回一个对象,包含两个属性:value:当前迭代的值。done:布尔值,表示是否迭代完成。
-
工作流程:
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 提供了丰富的响应式类,可以轻松地为不同的屏幕尺寸定义样式。
如 sm、md、lg、xl 和 2xl 这些代表不同尺寸的设备。
14. 有用过别的css 预编译器吗?
有的, Stylus & Sass。
他们有以下css 不具备的功能, 让css 变得强大。
- 变量
当然 css 4 支持变量了。
- 嵌套
tab 缩进嵌套, 支持css模块化
- 选择器继承
&
- mixin
函数化的 css复用
15. 跨域以及解决方法
-
1. JSONP
客户端定义一个回调函数, 通过
<script>标签请求服务器资源,并在 URL 中指定回调函数名,服务器返回一个 JavaScript 脚本,该脚本调用客户端定义的回调函数,并传入数据。<script>标签加载资源并不跨域,但只能用于GET请求 -
- CORS(Cross-Origin Resource Sharing)
CORS 是一种标准的跨域解决方案,通过在服务器端设置响应头来允许跨域请求。
服务器在响应头中添加
Access-Control-Allow-Origin,Access-Control-Allow-Methods,Access-Control-Allow-Headers, 如果请求方法是PUT、DELETE、CONNECT、OPTIONS、TRACE、PATCH等, 或头部信息包含**Authorization**等,会先发送一个OPTION 预检请求。 -
- Nginx 代理
-
- WebSocket
-
- PostMessage:适用于窗口或 iframe 之间的跨域通信。
16. Cookie 上的samesite 属性
SameSite 属性用于控制 Cookie 是否随跨站请求一起发送,有三个值:Strict、Lax 和 None,分别表示严格不随跨站请求发送、大部分跨站请求不发送和始终发送。
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() 转为字符串
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, 基于restful和mvc,编写过前端需要的接口。
实习中,有时候需要针对后段,写一些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")返回1parseInt("2")返回2最终结果是一个包含整数的数组[1, 2]。
23. function A(){}; A.a = 'b' 能执行吗?
可以执行, 函数是一等对象,可以给对象添加属性。
总结
校招确实比实习难挺多的,比如js底层、ts、全栈等考察点。只有身经百战,才能经受住如此考验。佩服佩服!
下次在更新二面面试题。