前言
学校面试交流群里,传出了第一个字节校招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")
返回1
parseInt("2")
返回2
最终结果是一个包含整数的数组[1, 2]
。
23. function A(){}; A.a = 'b' 能执行吗?
可以执行, 函数是一等对象,可以给对象添加属性。
总结
校招确实比实习难挺多的,比如js底层、ts、全栈等考察点。只有身经百战,才能经受住如此考验。佩服佩服!
下次在更新二面面试题。