引言
前言小编前几个礼拜把项目给做完了,但是关于小编写的智能健身项目,小编预计要花一天时间,预计五篇关于该项目使用的技术栈,以及碰到代码当中的问题,将会通过五篇文章将自己纯手打造出来的项目进行详细的解析,并且对于项目当中的问题进一步修改。自此,小编也是开始了BOSS直聘上的投递,今天也是面了一家公司,发现自身也有很多八股不熟,话不多说来看下面试题吧!!!
一、es6的新特性
面试官:请你说说es6的新特性?
1.var、let、const之间的区别
在es5当中使用var声明的变量存在变量提升的情况
js
console.log(a) // undefined
var a = 20
使用var,能够对一个变量进行多次声明,后面声明的变量会覆盖前面的变量声明
js
var a = 20
var a = 30
console.log(a) // 30
在函数中使用var声明变量时候,该变量是局部的
js
var a = 20
function change(){
var a = 30
}
change()
console.log(a) // 20
let是ES6新增的命令,用来声明变量,只在let命令所在的代码块内有效
js
{
let a = 10
}
console.log(a) // ReferenceError: a is not defined.
不存在变量提升
js
console.log(a) // 报错ReferenceError
let a = 2
使用let声明变量前,该变量都不可用,也就是大家常说的"暂时性死区"
js
var a = 123
if(true){
a = 'abc' // ReferenceError
let a;
}
const声明一个只读的常量,一旦声明,常量的值就不能改变
js
const a = 1
a = 3
// TypeError: Assisgnment to constant variable
如果之前用var或let声明过变量,再用const声明同样会报错
js
var a = 20
let b = 20
const a = 30
const b = 30
// 都会报错
const实际上保证的并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动
对于复杂类型的数据,变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的,并不能确保改变量的结构不变
2.ES6新增数组扩展
一、扩展运算符的应用
ES6通过扩展元素符...,好比rest参数的逆运算,将一个数组转为用逗号分隔的参数序列
js
console.log(...[1,2,3])
// 1 2 3
console.log(1, ...[2,3,4],5)
// 1 2 3 4 5
[...document.querySelectorAll('div')]
// [<div>,<div>,<div>]
主要用于函数调用的时候,将一个数组变为参数序列
js
function push(array, ...items){
array.push(...items)
}
function push(array,...items){
array.push(...items)
}
const arr = [1,2,3]
push(arr,4,5,6)
console.log(arr);
// [1, 2, 3, 4, 5, 6]
数组的合并也更为简洁了
js
const arr1 = ['a','b']
const arr2 = ['c']
const arr3 = ['d','e']
[...arr1, ...arr2, ...arr3]
// ['a','b','c','d','e']
注意:通过扩展运算符实现的是浅拷贝,修改了引用指向的值,会同步反映到新数组
二、构造函数新增的方法
- Array.from() 将对象转为真正的数组
- Array.of() 用于将一组值,转换为数组
三、实例对象新增的方法
- copyWithin() 将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组
js
[1, 2, 3, 4, 5].copyWithin(0, 3) // 将从 3 号位直到数组结束的成员(4 和 5),复制到从 0 号位开始的位置,结果覆盖了原来的 1 和 2
// [4,5,3,4,5]
- find() 用于找出第一个符合条件的数组成员 参数是一个回调函数,接受三个参数依次为当前的值、当前的位置和原数组
javascript
[1, 5, 10, 15].find(function(value, index, arr) {
return value > 9;
}) // 10
- findIndex 返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回
-1
javascript
[1, 5, 10, 15].findIndex(function(value, index, arr) {
return value > 9;
}) // 2
- fill() 使用给定值,填充一个数组
js
['a','b','c'].fill(7)
// [7,7,7]
new Array(3).fill(7)
// [7,7,7]
- entires().keys(),values() keys是对键名的遍历、values()是对键值的遍历,entries()是对键值对的遍历
js
for(let index of ['a','b'].keys()){
console.log(index);
}
// 0 1
for(let elem of ['a','b'].values()){
console.log(elem)
}
// 'a' 'b'
for(let [index,elem] of ['a','b'].entries()){
console.log(index,elem);
}
// 0 "a"
3.es6对象上新增
-super关键字 this关键字总是指向函数所在的当前对象,ES6又新增了另一个类似的关键字super,指向当前对象的原型对象
js
const name = {
foo: 'hello world'
}
const obj = {
foo: 'world',
find(){
return super.foo;
}
};
Object.setPrototypeOf(obj,proto); // 为obj设置原型对象
obj.find() // 'hello'
4.es6函数新增扩展
一、参数
- ES6允许为函数的参数设置默认值
js
function log(x, y = 'World') {
console.log(x, y);
}
console.log('Hello') // Hello World
console.log('Hello', 'China') // Hello China
console.log('Hello', '') // Hello
- 函数的形参是默认声明的,不能用let或const再次声明
二、属性
- 函数的length属性,length将返回没有指定默认值的参数个数
js
(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2
- 如果设置了默认参数不是尾参数,那么length属性也不再计入后面的参数了
js
(function (a = 0, b, c) {}).length // 0
(function (a, b = 1, c) {}).length // 1
- 返回该函数的函数名
js
var f = function () {};
// ES5
f.name // ""
// ES6
f.name // "f"
- 如果将一个具名函数赋值给一个变量,则name属性都返回这个具名函数原本的名字
js
const bar = function baz(){}
bar.name // "baz"
三、作用域
一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域。等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的。
下面例子中,y=x会形成一个单独作用域,x没有被定义,所以指向全局变量x
js
let x = 1;
function f(y = x) {
// 等同于 let y = x
let x = 2;
console.log(y);
console.log(x);
}
f() // 1 2
四、严格模式
只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错
javascript
// 报错
function doSomething(a, b = a) {
'use strict';
// code
}
// 报错
const doSomething = function ({a, b}) {
'use strict';
// code
};
// 报错
const doSomething = (...a) => {
'use strict';
// code
};
const obj = {
// 报错
doSomething({a, b}) {
'use strict';
// code
}
};
五、箭头函数
- 使用"箭头"(=>)定义函数
js
var foo = a => a
// 等同于
var f = function(v){
return v;
}
- 如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分
js
var f = () => 5;
// 等同于
var f = function () {return 5};
var sum = (num1, num2) => num1 + num2;
// 等同于
var sum = function(num1,num2){
return num1 + num2;
}
注意:函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象
二、react的虚拟dom
面试官:请你讲一下react的虚拟dom
简单来说,虚拟DOM是一个用javaScript对象来描述真实 DOM 结构的轻量级、内存中的表示。
- 状态驱动视图:在React中,UI被看作是应用状态(state)的函数。当组件的状态或属性(props)发生变化时,React需要更新视图。
- 生成新的虚拟DOM树:当状态变化发生时,React 会重新调用组件的
render
方法,生成一个新的虚拟 DOM 树。 - Diff算法:它会将新生成的虚拟 DOM 树与上一次渲染后的旧虚拟 DOM 树进行比较来找出两棵树之间的差异。通过 Diff 算法,React 能够精确地计算出需要对真实 DOM 进行的最小修改集合(操作真实DOM是相对损耗也米娜性能的)。
- 批量更新真实DOM:React会将计算出的最小更新操作批量应用到真实DOM上,从而完成界面的更新。这个过程通常是异步的,并且会进行优化。
三、useEffect
面试官:讲下react当中的useEffect
"useEffect
是 React Hooks 中最核心、最强大的 Hook 之一,它用于在函数式组件中处理副作用 。
jsx
useEffect(() => {
// 执行副作用操作
const subscription = subscribe();
const timer = setInterval(fetchData, 1000);
// 可选的清理函数(Cleanup)
return () => {
// 清理副作用,例如取消订阅、清除定时器
unsubscribe(subscription);
clearInterval(timer);
};
}, [/* 依赖数组 */]);
useEffect
的行为由它的依赖数组(Dependency Array) 控制:
- 没有依赖数组(或依赖数组为空):这个 effect 只在组件首次挂载(mount)时执行一次 。常用于初始化操作,如设置定时器、订阅事件、发送首次数据请求等。务必记得在清理函数中清除资源,防止内存泄漏。
- 有依赖数组:这个 effect 会在组件首次挂载时执行 ,并且每当依赖数组中的任意一个值发生变化时重新执行。这非常适合根据 props 或 state 的变化来同步执行操作,比如根据用户 ID 获取用户信息。
- 没有依赖数组(不推荐,除非明确需要):会在每次组件重新渲染后都执行。这通常会导致性能问题或无限循环(比如在 effect 中更新 state 导致再次渲染),应谨慎使用。
- 异步处理:
useEffect
的回调函数不能是async
函数(因为需要返回清理函数)。处理异步操作的正确方式是在内部定义一个async
函数并立即调用它。 - 执行机制:在浏览器完成渲染之后异步执行的,返回的清理函数会在组件卸载时执行,并且在下一次effect执行前也会执行。
四、如何封装一个自定义hooks
面试官:看你项目当中封装了hooks,请你讲下如何封装的
在项目当中在实现页面的瀑布流实现当中,首先在展示组件当中封装了一个WaterFall组件。并且在WaterFall组件当中封装使用了ImageCard组件实现两列式布局,在ImageCard当中为了实现图片的懒加载和滚动懒加载,为了减少页面当中的重排和重绘,使用了useIntersectionObserverhooks函数组件。使用观察者模式对用户在使用该组件当中对页面进行滑动时,对图片的进入高度和离开页面高度进行监听,只有当用户不断下拉屏幕到下一张图片时,图片才会开始加载,实现对页面当中图片的懒加载。
五、Memo、useMemo、useCallback
面试官:请你讲讲项目中如何实现项目优化
Memo (用于优化函数式组件的渲染)
如果 props 没有变化,React 就会跳过该子组件的渲染,直接复用上一次的渲染结果。在项目当中,ImageCard当中使用了memo进行性能优化,因为在用户不断滑动屏幕当中,卡片组件当中的内容不会更改,因此在该组件当中使用Memo实现性能优化。
useMemo (缓存计算结果)
只有当依赖项发生变化时才会重新计算,避免每次渲染时进行昂贵的重复计算。
jsx
// 使用 useMemo 固定随机选择的标题和描述,避免重新渲染时刷新
const { title, desc } = useMemo(() => {
if (img.title && img.desc) {
return { title: img.title, desc: img.desc }
}
const randomItem = detailFallback[Math.floor(Math.random() * detailFallback.length)]
return {
title: img.title || randomItem.title,
desc: img.desc || randomItem.desc
}
}, [img.title, img.desc])
useCallback(缓存函数的引用)
useCallback用于缓存函数引用,只有当依赖项变化时才创建新函数。这对于防止因函数引用变化导致的子组件不必要重新渲染特别有用。
jsx
import React, { useState, useEffect, useCallback } from 'react';
function ExampleComponent({ fetchData }) {
const [data, setData] = useState(null);
// 使用 useCallback 来确保 fetchData 回调函数的引用稳定性
const fetchHandler = useCallback(async () => {
const result = await fetchData();
setData(result);
}, [fetchData]); // 注意这里添加了 fetchData 作为依赖
useEffect(() => {
fetchHandler();
}, [fetchHandler]); // 只有当 fetchHandler 发生变化时才会触发 effect
return (
<div>
{data ? <p>Data: {data}</p> : <p>Loading...</p>}
</div>
);
}
六、diff算法
面试官:请你讲讲react当中的diff算法
React 的核心思想是"数据驱动视图",当组件的 state 或 props 发生变化时,React 会重新渲染组件,生成新的虚拟 DOM 树。如果每次都直接操作真实 DOM 进行全量更新,性能开销会非常大。React的diff算法基于三个核心假设,将O(n^3)的复杂度降低到O(n),从而实现高效更新。策略一:同层比较,策略二:类型比较,策略三:列表对比-key的作用。
总结 Diff 算法的三大要点
策略 | 作用 | 关键点 |
---|---|---|
同层比较 | 避免跨层级移动 | 层级变化 = 重建 |
类型比较 | 复用相同类型节点 | 类型不同 = 重建 |
列表对比 | 高效处理列表更新 | key 必须稳定、唯一 |
七、SSE
面试官:请你讲讲流式输出
流式输出是一种数据传输方式,它允许服务器在数据生成的过程中,将其分块、连续地发送给客户端,而不是等待所有数据都处理完毕后再一次性发送。持续不断地从源头(服务器)流向目的地(客户端)。一旦生成了第一个数据块就立即通过网络发送给客户端。客户端接收到第一个块后,就可以开始处理,而无需等待后续数据。而在前端实现流式输出,显著降低了首字节时间(TTFB),减少了服务器内存压力,并实现了真正的实时性。
八、jwt鉴权登录
面试官:讲讲你是如何实现注册登录的
好的,面试官,作为一名前端开发工程师,很乐意为您讲解在现代Web应用中实现注册登录功能的完整思路和关键技术点。其核心目标是安全、可靠、用户体验良好地验证用户身份并管理会话。 而我在项目当中是通过使用JWT鉴权登录的方式实现页面的登录和注册,因为它在现代SPA和API架构中非常流行。
- 状态管理:使用了轻量型的状态管理库Zustand管理用户登录状态,确保应用在任何组件都能感知用户是否已经登录。'
- 路由守卫:对于需要登录才能访问的页面,在路由层面进行拦截,在路由跳转前检查本地token和用户状态。
- API请求拦截器:使用axios拦截器,在每次发送请求前,检查是否存在Token,如果存在,自动将其添加到请求头中。拦截401状态码,清除本地存储的Token和用户状态,重定向用户到登录页,并提示"登录已过期,请重新登录"。
- HTTPS: 必须使用,防止中间人窃取密码和Token
- 前端响应:收到成功响应,解析出 JWT Token,安全存储Token,-
localStorage
: 最常用。优点是持久化,刷新页面不失效。缺点是易受 XSS 攻击 (恶意脚本可读取)。必须确保网站自身没有 XSS 漏洞。
总结
总的来说第一次参加面试还是很紧张的,特别是面试官在问第一道题目的时候,因为第一次面试很多内容都记不起来。在面试的过程中当中,跟面试官交流的情况还是很不错的,对于项目当中实现的功能自己也是能很自信的讲出来,对于面试官问的很多八股这块不会的,自己也是即时记录下来,写下文章,加深对其的理解。其实大家对于面试不用太害怕,面试官就算是又秃又强的也不要紧,大家正常回答既可。