前言
最近在牛客上收集了一些字节的面试题,下面是整理的面经。
面经一
问题一:本地存储方式的区别和应用场景
cookie
http
协议是无状态的协议,会话结束了也就终止了联系,为了能在下次发送请求可以直接让服务器端知道是谁,于是cookie
就诞生了。
特点:
- 本质上是一段存储在本地不超过
4kb
的小型文本 - 内部以键值对的方式来存储(在chrome开发者面板的
Application
这一栏可以看到)
常见字段:
Expries
用于设置 cookie 的过期时间
Expires=Wed, 21 Oct 2015 07:28:00 GMT
Max-Age
用于设置在 Cookie 失效之前需要经过的秒数(优先级比Expires
高)
Max-Age=604800
Cookie
的SameSite
属性- strict模式,完全禁止第三方请求携带,完全遵守同源策略
- lax模式,get提交的时候可以携带
- none模式,自动携带
domain
属性用于限制 Cookie 的作用域,只有在指定的域名下才能够使用该 Cookie。
Domain=example.com
path
属性则用于限制 Cookie 的生效路径,只有在指定的路径下才能够使用该 Cookie。
Path=/api
secure
:一个布尔值,表示是否只在 HTTPS 连接时发送 Cookie。http-only
:一个布尔值,表示是否禁止通过 JavaScript 访问 Cookie,从而提高安全性。name
:Cookie 的名称,通常是一个字符串。value
:Cookie 的值,可以是一个字符串或其他类型的数据。
所以cookie
最开始的作用并不是为了缓存而设计出来,只是借用了cookie
的特性实现缓存。
怎么设置和删除?
// 设置 Cookie
document.cookie = "username=john; expires=Thu, 18 Dec 2043 12:00:00 GMT; path=/";
// 读取 Cookie
function getCookie(name) {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.startsWith(name + '=')) {
return cookie.substring(name.length + 1);
}
}
return '';
}
const username = getCookie('username');
// 删除 Cookie
// 最常用的方法就是给`cookie`设置一个过期的事件,这样`cookie`过期后会被浏览器删除
function deleteCookie(name) {
document.cookie = name + '=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/';
}
deleteCookie('username');
localStorage
HTML5
新方法,IE8及以上浏览器都兼容。
localStorage
是一种用于在客户端(浏览器)中存储数据的 Web API,可以用于长期存储非敏感数据,例如用户的个人偏好、应用程序状态等。
特点:
- 持久化的本地存储,除非主动删除,否则永远不会过期。
- 在同一域名中,存储的信息是共享的。
- 当本页操作(新增、修改、删除)了
localStorage
的时候,本页面不会触发storage
事件,但是别的页面会触发storage
事件。通过window.addEventListener('storage', listener)
方法注册一个事件监听器,其中listener
是用于处理storage
事件的回调函数,也就是说本页改变localStorage
不会触发这个这个事件,也不会执行回调函数。 - 大小:5M(跟浏览器厂商有关系)。
- 只存在客户端,默认不参与与服务端的通信。这样就很好地避免了 Cookie 带来的性能问题和安全问题。
- 接口封装。通过
localStorage
暴露在全局,并通过它的setItem
和getItem
等方法进行操作,非常方便。
下面再看看关于localStorage
的使用:
设置
localStorage.setItem('username','cfangxu');
获取
localStorage.getItem('username')
获取键名
localStorage.key(0) //获取第一个键名
删除
localStorage.removeItem('username')
一次性清除所有存储
localStorage.clear()
localStorage
也不是完美的,它有两个缺点:
- 无法像
Cookie
一样设置过期时间 - 只能存入字符串,无法直接存对象
localStorage.setItem('key', {name: 'value'});
console.log(localStorage.getItem('key')); // '[object, Object]'
sessionStorage
sessionStorage
和 localStorage
使用方法基本一致,唯一不同的是生命周期,一旦页面(会话)关闭,sessionStorage
将会删除数据。
应用场景
- 可以用它对表单信息进行维护,将表单信息存储在里面,可以保证页面即使刷新也不会让之前的表单信息丢失。
- 可以用它存储本次浏览记录。如果关闭页面后不需要这些记录,用
sessionStorage
就再合适不过了。
IndexedDB
indexedDB
是运行在浏览器中的非关系型数据库,IndexDB
的一些重要特性,除了拥有数据库本身的特性,比如支持事务
,存储二进制数据
,还有这样一些特性需要格外注意:
虽然 Web Storage
对于存储较少量的数据很有用,但对于存储更大量的结构化数据来说,这种方法不太有用。IndexedDB
提供了一个解决方案。
优点:
- 储存量理论上没有上限
- 所有操作都是异步的,相比
LocalStorage
同步操作性能更高,尤其是数据量较大时 - 原生支持储存
JS
的对象 - 是个正经的数据库,意味着数据库能干的事它都能干
缺点:
- 操作非常繁琐
- 本身有一定门槛
关于indexedDB
的使用基本使用步骤如下:
- 打开数据库并且开始一个事务
- 创建一个
object store
- 构建一个请求来执行一些数据库操作,像增加或提取数据等。
- 通过监听正确类型的
DOM
事件以等待操作完成。 - 在操作结果上进行一些操作(可以在
request
对象中找到)
关于使用indexdb
的使用会比较繁琐,大家可以通过使用Godb.js
库进行缓存,最大化的降低操作难度。
区别
关于cookie
、sessionStorage
、localStorage
三者的区别主要如下:
- 存储大小:
cookie
数据大小不能超过4k
,sessionStorage
和localStorage
虽然也有存储大小的限制,但比cookie
大得多,可以达到5M或更大 - 有效时间:
localStorage
存储持久数据,浏览器关闭后数据不丢失除非主动删除数据;sessionStorage
数据在当前浏览器窗口关闭后自动删除;cookie
设置的cookie
过期时间之前一直有效,即使窗口或浏览器关闭 - 数据与服务器之间的交互方式,
cookie
的数据会自动的传递到服务器,服务器端也可以写cookie
到客户端;sessionStorage
和localStorage
不会自动把数据发给服务器,仅在本地保存
问题二:cookie的字段
参考问题一对cookie
的描述
问题三:从url输入到页面显示的具体过程
-
用户输入url并回车
-
浏览器进程检查url,组装协议,构成完整的url
-
浏览器进程通过进程间通信(IPC)把url请求发送给网络进程
-
网络进程接收到url请求后检查本地缓存是否缓存了该请求资源,如果有则将该资源返回给浏览器进程
-
如果没有,网络进程向web服务器发起http请求(网络请求),请求流程如下:
- 进行DNS解析,获取服务器ip地址,端口
- 利用ip地址和服务器建立tcp连接
- 构建请求头信息
- 发送请求头信息
- 服务器响应后,网络进程接收响应头和响应信息,并解析响应内容
-
网络进程解析响应流程;
- 检查状态码,如果是301/302,则需要重定向,从Location自动中读取地址,重新进行第4步,如果是200,则继续处理请求。
- 200响应处理: 检查响应类型Content-Type,如果是字节流类型,则将该请求提交给下载管理器,该导航流程结束,不再进行 后续的渲染,如果是html则通知浏览器进程准备渲染进程准备进行渲染。
-
准备渲染进程
- 浏览器进程检查当前url是否和之前打开的渲染进程根域名是否相同,如果相同,则复用原来的进程,如果不同,则开启新的渲染进程
-
传输数据、更新状态
- 渲染进程准备好后,浏览器向渲染进程发起“提交文档”的消息,渲染进程接收到消息和网络进程建立传输数据的“管道”
- 渲染进程接收完数据后,向浏览器发送“确认提交”
- 浏览器进程接收到确认消息后更新浏览器界面状态:安全、地址栏url、前进后退的历史状态、更新web页面
问题四:渲染流程
-
渲染进程接受完数据之后,先把html内容转换为DOM树(document)
-
渲染引擎将html内容里的css内容转换为styleSheets(document.styleSheets),还会把属性值标准化,比如rem转换为px,计算DOM节点的样式,把不显示在页面的DOM去掉。设置为display:none的节点,会存在在DOM树里。
-
创建布局树,计算元素、节点的布局信息
-
对布局树进行分层,生成分层树。页面元素是按照嵌套关系组织的,生成分层树,使得渲染更加高效
-
对每一个图层生成绘制列表,并提交到合成线程中。绘制列表只是用来记录绘制顺序和绘制指令的列表,际上绘制操作是由渲染引擎中的合成线程来完成的。
-
合成线程将图层会分成图块,并在光栅化线程池中将图块转换成位图。合成线程会将图层划分为图块(tile),这些图块的大小通常是
256x256
或者512x512
,合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图.位图(bitmap),也叫做光栅图或像素图,例如,当浏览器将网页中的SVG
图片或Canvas
元素渲染成屏幕上可视的元素时,会使用光栅化技术将其转换为位图。位图可以直接在屏幕上显示,而不需要进行额外的计算,因此它们是一种非常高效的图像呈现方式。栅格化过程都会使用GPU
来加速生成,使用GPU
生成位图的过程叫快速栅格化,或者GPU
栅格化,生成的位图被保存在GPU
内存中。这就涉及到了跨进程操作。 -
合成线程发送绘制图块命令 DrawQuad 给浏览器进程。
-
浏览器进程根据 DrawQuad 消息生成页面,并显示到显示器上。浏览器进程里面有一个叫
viz
的组件,用来接收合成线程发过来的DrawQuad
命令,然后根据DrawQuad
命令,将其页面内容绘制到内存中,最后再将内存的绘制内容显示在屏幕上。
CSS
的transform
属性可以用来对元素进行平移、旋转、缩放等变换。由于transform
只涉及到视觉呈现的变化,而不会引起文档流的改变,因此在使用transform
实现动画效果时,可以避开重排和重绘阶段,从而提高页面性能。
问题五:深拷贝浅拷贝
概念
浅拷贝:
创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
深拷贝:
将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象。
JSON.parse(JSON.stringify())拷贝的缺点
- 无法处理特殊类型数据
JSON.stringify()
函数只能序列化 JavaScript 中的基本数据类型和数组、对象等结构化数据类型,这是JSON 数据格式所支持的数据类型,不能序列化函数、日期、正则表达式等特殊类型的数据。因此,在进行深拷贝时,如果原始对象中包含这些特殊类型的数据,拷贝后这些数据会被忽略或转换成字符串格式。
// 拷贝函数
const obj = {
functionName: function() {
console.log('Hello, world!');
}
};
console.log(JSON.stringify(obj)); // 输出 '{}'
// 拷贝日期类型数据
// 将日期类型转换为字符串类型后再进行序列化
const obj = {
date: new Date()
};
console.log(JSON.stringify(obj)); // 输出 '{"date":"2021-09-30T02:45:00.000Z"}'
// 正则表达式类型数据
const obj = {
regex: /hello/g
};
console.log(JSON.stringify(obj)); // 输出 '{"regex":{}}'
- 不能处理循环引用
如果原始对象包含循环引用,即一个对象的某个属性值指向了该对象自身,如 { a: { b: { c: null } } }
中的 c
属性,JSON.stringify()
函数无法将其序列化为 JSON 字符串,会报错。因此,在进行深拷贝时,如果原始对象中存在循环引用,拷贝操作会产生错误。
- 效率较低
JSON.stringify()
和 JSON.parse()
都需要将 JavaScript 对象或数组转换成 JSON 字符串和 JavaScript 对象或数组,这样的操作可能会比较耗时,尤其是对于大型对象或数组时,效率会更加低下。因此,如果需要频繁进行深拷贝操作,使用 JSON.parse(JSON.stringify())
不是最优的选择。
考虑循环引用的深拷贝
function deepClone(obj, hash = new WeakMap()) {
// 如果是null我就不进行拷贝操作
if (obj === null) return obj;
// 可能是对象或者普通的值
if (typeof obj !== "object") return obj;
// 是对象的话就要进行深拷贝
if (hash.get(obj)) return hash.get(obj);
// 使用了原对象的构造方法,保留对象原型上的数据
let cloneObj = new obj.constructor();
// 找到的是所属类原型上的constructor,而原型上的 constructor指向的是当前类本身
hash.set(obj, cloneObj);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
// 实现一个递归拷贝
cloneObj[key] = deepClone(obj[key], hash);
}
}
return cloneObj;
}
可以拷贝Map、Set数据类型
function deepClone(obj, hash = new WeakMap()) {
if (obj instanceof Date) return new Date(obj);
// if (obj instanceof RegExp) return new RegExp(obj);
if (obj === null || typeof obj !== 'object') return obj;
if (hash.has(obj)) return hash.get(obj);
let cloneObj;
const Constructor = obj.constructor;
switch (Constructor) {
case Object:
cloneObj = new Constructor();
break;
case Array:
cloneObj = new Constructor();
break;
case Map:
cloneObj = new Constructor();
for (const [key, value] of obj.entries()) {
cloneObj.set(deepClone(key, hash), deepClone(value, hash));
}
break;
case Set:
cloneObj = new Constructor();
for (const value of obj.values()) {
cloneObj.add(deepClone(value, hash));
}
break;
default:
cloneObj = new Constructor();
break;
}
hash.set(obj, cloneObj);
for (let prop in obj) {
if (Object.prototype.hasOwnProperty.call(obj, prop)) {
cloneObj[prop] = deepClone(obj[prop], hash);
}
}
return cloneObj;
}
问题六:git reset 和 git revert
问题七:水平垂直居中(如果父元素宽高自适应)
问题八: align-content 和 justify-content的区别
align-content
属性作用于多行的 Flexbox 容器,用于控制多行子元素的垂直对齐方式;而 justify-content
属性作用于单行的 Flexbox 容器,用于控制单行子元素水平对齐方式。
问题九:移动端适配的方法
- 使用CSS3的媒体查询(@media)来适配不同屏幕大小。
在CSS中使用@media查询可以根据屏幕大小调整不同样式。通常情况下,我们会根据屏幕宽度设置断点,并在特定宽度下应用不同的样式。
例如,在屏幕宽度小于768px时,我们可以将页面字体大小设置为16px;而在屏幕宽度大于等于768px时,页面字体大小设置为18px。
@media (min-width: 768px) {
body {
font-size: 18px;
}
}
@media (max-width: 767px) {
body {
font-size: 16px;
}
}
- 使用JavaScript根据设备像素比(devicePixelRatio)进行适配
由于不同设备的像素密度不同,如果只是简单地按照像素计算,可能会导致页面在高像素密度的设备上显示过小。而使用设备像素比(devicePixelRatio)来计算,可以根据设备像素密度调整视口的缩放比例,从而实现适配。
例如: 获取设备像素比为3,并计算出缩放比例为1/3,将元素宽度和高度都放大3倍,以达到适配高像素密度设备的目的。
var scale = 1 / window.devicePixelRatio;
document.querySelector('meta[name="viewport"]')
.setAttribute(
'content',
'width=device-width, initial-scale=' + scale + ', maximum-scale=' + scale + ',
user-scalable=no'
);
- 使用viewport meta标签来设置视口大小并控制页面缩放。
在移动端开发中,通常会设置viewport的width为设备宽度,并禁止用户缩放页面。这样可以保证页面展示效果的稳定性。
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
- 使用rem或者em作为单位进行适配,同时配合根据不同尺寸屏幕动态计算字体大小等方法。
rem和em都是相对长度单位,可以根据页面根元素(html)的字体大小进行调整。我们可以将页面字体大小设置为基准大小,然后根据不同屏幕尺寸动态调整字体大小。
html {
font-size: 16px; /* 假设基准像素为16px */
}
@media (max-width: 767px) {
html {
font-size: 14px;
}
}
问题十: 可以用flex能完成移动端适配吗
可以,flex 布局可以方便地实现响应式设计。
流程
使用flex布局进行移动端适配的具体流程可以分为以下几步:
- 设置 meta 标签,控制页面缩放和视口大小。通常建议设置viewport的 width=device-width,initial-scale=1.0,user-scalable=no。
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
-
确定设计稿尺寸,并根据设计稿尺寸计算出基准像素。比如,设计稿宽度为 750px,则基准像素为 1/75,即 10px。
-
在根元素(html)中设置 font-size,用于控制 rem 的大小。一般情况下可以设置为基准像素大小。同时,建议设置一个最小值,避免在屏幕过小时字体过小。
/* 假设基准像素为10px */
html {
font-size: 10px;
min-width: 320px; /* 避免字体过小 */
}
- 使用 rem 作为长度单位,并根据视觉稿中的尺寸换算成 rem。比如,设计稿中某一元素的宽度为 100px,则应设置其宽度为 10rem。
/* 假设设计稿中某一元素的宽度为100px */
.element {
width: 10rem;
}
- 使用 flex 布局,并根据子元素所占比例进行设置。使用 flex-wrap 属性设置是否换行,使用 align-items 和 justify-content 控制子元素在交叉轴和主轴上的对齐方式。
.container {
display: flex;
flex-wrap: wrap;
}
.item {
flex: 1; /* 自适应宽度 */
min-width: 100px; /* 设置最小宽度 */
margin-right: 10px; /* 设置间距 */
margin-bottom: 10px;
}
在实际开发中,还要注意屏幕尺寸、像素密度、字体大小等因素,选择合适的方案实现移动端适配。
问题十一:怎么在高像素设备上显示 1px
- transform的scaleY属性
- meta viewport 设置scale的值
- SVG
- Canvas
问题十二:flexible 适配的原理
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Document</title>
<style>
/* 假设设计稿宽度为750px,在750px屏幕下,1rem=100px */
html {
font-size: 100px;
}
/* 其他CSS样式 */
.box {
width: 6rem;
height: 3rem;
background-color: #f00;
margin: 1rem auto;
}
</style>
</head>
<body>
<div class="box"></div>
<!-- flexible核心代码,设置根元素的font-size值 -->
<script>
(function () {
var width = document.documentElement.clientWidth // 获取屏幕宽度
var ratio = 750 / width // 计算出设计稿的比例
var fontSize = 100 * ratio // 将比例设置为根元素的font-size值
document.documentElement.style.fontSize = fontSize + 'px'
window.onresize = function () {
var width = document.documentElement.clientWidth // 监听屏幕尺寸变化
var ratio = 750 / width
var fontSize = 100 * ratio
document.documentElement.style.fontSize = fontSize + 'px'
}
})()
</script>
</body>
</html>
问题十三:react
问题十四:手写 new 函数
function myNew(constructor, ...args) {
// 创建一个空对象,并将它的原型指向构造函数的 prototype 属性
// 它的作用是以指定对象为原型创建一个新的对象,新对象会继承原型对象的所有属性和方法
const obj = Object.create(constructor.prototype);
// 调用构造函数,并将 this 绑定到新创建的对象上
const result = constructor.apply(obj, args);
// 如果构造函数返回了一个对象,则直接返回该对象;否则返回新创建的对象
return (typeof result === 'object' && result !== null) ? result : obj;
}
问题十五:场景代码题
function find(obj, str) {
var arr = str.split('.'); // 将传入的字符串按照"."切割成数组
var pointer = obj; // 初始化指针变量为传入的对象本身
for (var i = 0; i < arr.length; i++) {
var key = arr[i];
if (pointer.hasOwnProperty(key)) { // 判断当前指针变量所指的对象中是否包含该属性
pointer = pointer[key]; // 将指针变量指向该属性对应的对象
} else {
return undefined; // 如果不存在该属性,返回undefined
}
}
return pointer; // 循环结束时,指针变量所指的对象即为所要查找的属性值
}
// 示例
var obj = {a:{b:{c:1}}};
console.log(find(obj,'a.b.c')); // 1
console.log(find(obj,'a.d.c')); // undefined
问题十六
剑指 Offer II 105. 岛屿的最大面积 - 力扣(LeetCode)
面经二
问题一: 三次握手为什么是三次,两次不行吗?
对应到 TCP 的三次握手,也是需要确认: 请求能力、接受能力、对方请求能力和对方接受能力
一是三次握手可以确认服务器端有没有发送能力、对方接受能力。
二是当只需要两次握手就可以建立连接时,对于发了 SYN 报文想握手,但是这个包滞留在了当前的网络中迟迟没有到达,TCP 以为这是丢了包,于是重传,两次握手建立好了连接。但是连接关闭后,如果这个滞留在网路中的包到达了服务端呢?这时候由于是两次握手,服务端只要接收到然后发送相应的数据包,就默认建立连接,但是现在客户端已经断开了。
所以两次握手会请求超时导致的脏连接,带来了连接资源的浪费。无法确认服务器端有没有发送能力、对方接受能力。而三次握手可以防止超时,同时保证信息对等。
问题二: 四次挥手为什么是四次,三次不行吗?
- 第四次挥手,主动关闭端等待2MSL是必须的
- 所以也就是说把服务端将
ACK
和FIN
的发送合并为一次挥手 - 这个时候长时间的延迟可能会导致客户端误以为
FIN
没有到达客户端 - 从而让客户端不断的重发
FIN
问题三: 强缓存和协商缓存
问题四: 跨域
问题五: 箭头函数和普通函数的区别
- 语法不同:箭头函数使用箭头(=>)而不是关键字function来声明,这使得它们的语法更加紧凑、简洁。
- this 值不同:箭头函数中的this值不同于普通函数在相同情况下的this值。箭头函数的this值由外层作用域(即定义时的作用域)决定,而不是函数被调用时的作用域。这意味着箭头函数中的this指向永远不会改变,而普通函数的this值可能会随着调用方式的不同而改变。
- 没有自己的arguments对象:箭头函数不能使用自己的arguments对象,但是可以通过扩展运算符...来访问传递给函数的参数。
- 不能作为构造函数:箭头函数不能用作构造函数,因为它们没有自己的this值和原型对象。如果尝试使用new关键字实例化一个箭头函数,将会抛出TypeError错误。
- 没有 prototype 属性:箭头函数没有prototype属性,因为它们不能用作构造函数,因此也就不需要原型对象。普通函数则具有原型对象,并且可以在此原型对象上添加方法和属性。
- 不能使用 yield 关键字:箭头函数不能使用yield关键字来定义生成器函数,但是普通函数可以。
问题六: 虚拟 DOM 和 diff 算法
了解 diff 算法可以去看看图理解双端diff算法 - 掘金 (juejin.cn)
- 什么是虚拟 DOM
虚拟 DOM 是指将页面结构转化成一个JavaScript
对象,在内存中维护一份虚拟 DOM 树,通过对比前后两个虚拟 DOM 树的差异,最小化地更新真实 DOM。
- 为什么使用虚拟 DOM
由于真实 DOM 操作非常昂贵,并且直接影响到应用程序的性能和用户体验。因此,虚拟 DOM 提供了一种更高效的方式来操作 DOM,它可以利用 JavaScript 引擎更高效地更新 DOM,并且可以避免频繁的重绘、回流等操作,从而提高应用程序的性能。
- 什么是 diff 算法
diff 算法是比较两个虚拟 DOM 树的差异,找出需要更新的节点,并将这些节点更新到真实 DOM 上的算法。Vue.js 内部使用的是优化算法,可以快速地确定需要更新的节点,以最小化更新的时间和效果。
- diff 算法如何工作
当数据改变时,Vue.js 首先创建一个新的虚拟 DOM 树,并将其与之前的虚拟 DOM 树进行比较。diff 算法是一种递归算法,它会逐层遍历节点,比较两个虚拟 DOM 树中的每个节点,找出需要更新的节点。在比较节点时,Vue.js 会使用一些启发式算法来优化比较性能。
- diff 算法的优化
Vue.js 内部使用了大量的优化策略来提高 diff 算法的性能,其中最重要的包括:
- 只对同级组件进行比较
- 根据节点的关键属性进行比较
- 对列表中的节点采用就地复用策略
这些优化策略可以显著提高 Vue.js 应用程序的性能,并使其在渲染大型数据集时更具可扩展性。
问题七:判断链表是否有环,不能用本地IDE,自己定义数据结构
// 定义链表节点类
class Node {
constructor(val) {
this.val = val;
this.next = null;
}
}
// 定义链表类
class LinkedList {
constructor() {
this.head = null;
}
// 尾部添加新节点
append(val) {
const newNode = new Node(val);
if (!this.head) {
this.head = newNode;
} else {
let current = this.head;
while (current.next) {
current = current.next;
}
current.next = newNode;
}
}
// 判断链表是否有环
hasCycle() {
if (!this.head || !this.head.next) {
return false;
}
let slow = this.head;
let fast = this.head.next;
while (slow !== fast) {
if (!fast || !fast.next) {
return false;
}
slow = slow.next;
fast = fast.next.next;
}
return true;
}
}
// 测试代码
const list = new LinkedList();
list.append(1);
list.append(2);
list.append(3);
list.append(4);
list.append(5);
console.log(list.hasCycle()); // 输出 false
// 创建一个有环的链表
const node3 = new Node(3);
const node2 = new Node(2);
const node1 = new Node(1);
node1.next = node2;
node2.next = node3;
node3.next = node1;
const list2 = new LinkedList();
list2.head = node1;
console.log(list2.hasCycle()); // 输出 true
// 判断链表是否有环(使用哈希表)
function hasCycle(head) {
const hashSet = new Set();
let current = head;
while (current) {
if (hashSet.has(current)) {
return true;
}
hashSet.add(current);
current = current.next;
}
return false;
}
// 测试代码
const node3 = {val: 3, next: null};
const node2 = {val: 2, next: node3};
const node1 = {val: 1, next: node2};
node3.next = node1;
console.log(hasCycle(node1)); // 输出 true
const node5 = {val: 5, next: null};
const node4 = {val: 4, next: node5};
console.log(hasCycle(node4)); // 输出 false
// 判断链表是否有环(暴力解法)
function hasCycle(head) {
let current = head;
while (current) {
let checkNode = current.next;
while (checkNode) {
if (current === checkNode) {
return true;
}
checkNode = checkNode.next;
}
current = current.next;
}
return false;
}
// 测试代码
const node3 = {val: 3, next: null};
const node2 = {val: 2, next: node3};
const node1 = {val: 1, next: node2};
node3.next = node1;
console.log(hasCycle(node1)); // 输出 true
const node5 = {val: 5, next: null};
const node4 = {val: 4, next: node5};
console.log(hasCycle(node4)); // 输出 false
问题八:最长递增子序列
问题九:在 es5 中如何实现 const
var MY_CONST = {};
Object.defineProperty(MY_CONST, "MY_CONSTANT", {
value: "my constant value",
writable: false,// writable 属性设置为 false 表示该常量不可写
enumerable: true,// enumerable 属性设置为 true 表示该常量可以被枚举
configurable: false // configurable 属性设置为 false 表示该常量不可再被重新配置或删除
});
console.log(MY_CONST.MY_CONSTANT); // 输出 "my constant value"
MY_CONST.MY_CONSTANT = "new value"; // 没有报错,但不会更改 MY_CONSTANT 的值
console.log(MY_CONST.MY_CONSTANT); // 输出 "my constant value"
问题十: object.defineProproty 和 proxy 的区别
Object.defineProproty
该方法可以在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。
语法
Object.defineProperty(obj, prop, descriptor)
参数
obj: 要在其上定义属性的对象。
prop: 要定义或修改的属性的名称。
descriptor: 将被定义或修改的属性的描述符。
函数的第三个参数 descriptor 所表示的属性描述符有两种形式:数据描述符和存取描述符。
这就意味着你可以:
Object.defineProperty({}, "num", {
value: 1,
writable: true,
enumerable: true,
configurable: true
});
也可以:
var value = 1;
Object.defineProperty({}, "num", {
get : function(){
return value;
},
set : function(newValue){
value = newValue;
},
enumerable : true,
configurable : true
});
两者均具有以下两种键值:
configurable
当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,也能够被删除。默认为 false。
enumerable
当且仅当该属性的 enumerable 为 true 时,该属性才能够出现在对象的枚举属性中。默认为 false。
数据描述符同时具有以下可选键值:
value
该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined。
writable
当且仅当该属性的 writable 为 true 时,该属性才能被赋值运算符改变。默认为 false。
存取描述符同时具有以下可选键值:
get
一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。该方法返回值被用作属性值。默认为 undefined。
set
一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。该方法将接受唯一参数,并将该参数的新值分配给该属性。默认为 undefined。
descriptor
这个字段是必须的,如果不进行任何配置,你可以这样:
var obj = Object.defineProperty({}, "num", {});
console.log(obj.num); // undefined
Proxy
下面是 Proxy 的几个主要特点:
- 拦截器(handler):Proxy 对象的第一个参数是一个目标对象,第二个参数是一个拦截器对象,其中拦截器对象包含一些方法,用来拦截底层操作。
- 可代理的操作:Proxy 可以代理目标对象的各种操作,例如读取属性、写入属性、函数调用等。
- 自定义处理逻辑:在拦截器对象中,我们可以定义自己的处理逻辑,并在底层操作被触发时执行相应的处理逻辑。
下面通过代码示例来进一步说明 Proxy 的用法:
const person = {
name: 'Tom',
age: 18
};
const handler = {
get(target, key) {
console.log(`get ${key}`);
return target[key];
},
set(target, key, value) {
console.log(`set ${key} to ${value}`);
target[key] = value;
return true;
}
};
const proxyPerson = new Proxy(person, handler);
proxyPerson.name; // 触发 getter
proxyPerson.age = 20; // 触发 setter
下面的例子是拦截第一个字符为下划线的属性名,不让它被 for of 遍历。
let target = {
_bar: 'foo',
_prop: 'bar',
prop: 'baz'
};
let handler = {
ownKeys (target) {
return Reflect.ownKeys(target).filter(key => key[0] !== '_');
}
};
let proxy = new Proxy(target, handler);
for (let key of Object.keys(proxy)) {
console.log(target[key]);
}
// "baz"
问题十一:用对象实现 Map 数据类型?
function Map() {
this.data = {};
}
Map.prototype.set = function(key, value) {
this.data[JSON.stringify(key)] = value;
};
Map.prototype.get = function(key) {
return this.data[JSON.stringify(key)];
};
Map.prototype.delete = function(key) {
delete this.data[JSON.stringify(key)];
};
Map.prototype.clear = function() {
this.data = {};
};
Map.prototype.has = function(key) {
return this.data.hasOwnProperty(JSON.stringify(key));
};
Map.prototype.forEach = function(callback) {
for (let prop in this.data) {
callback(JSON.parse(prop), this.data[prop]);
}
};
Map.prototype.keys = function() {
let keys = [];
for (let prop in this.data) {
keys.push(JSON.parse(prop));
}
return keys;
};
Map.prototype.values = function() {
let values = [];
for (let prop in this.data) {
values.push(this.data[prop]);
}
return values;
};
Map.prototype.entries = function() {
let entries = [];
for (let prop in this.data) {
entries.push([JSON.parse(prop), this.data[prop]]);
}
return entries;
};
问题十二:算法题1
实现一个定时器函数myTimer(fn, a, b),
让fn执行,
第一次执行是a毫秒后,
第二次执行是a+b毫秒后,
第三次是a+2b毫秒,
第N次执行是a+Nb毫秒后
要求:
1、白板手撕
2、myTimer要有返回值,并且返回值是一个函数,调用该函数,可以让myTimer停掉
解答:
function myTimer(fn, a, b) {
let timerId;
let count = 0;
function schedule() {
const delay = a + count * b;
timerId = setTimeout(() => {
fn();
count++;
schedule();
}, delay);
}
schedule();
return function() {
clearTimeout(timerId);
}
}
问题十三:算法题2
写一个构造函数Foo,该函数每个实例为一个对象,形如{id:N},其中N表示第N次调用得到的。
要求:
1、不能使用全局变量
2、直接调用Foo()也会返回实例化的对象
3、实例化的对象必须是Foo的实例
解答:
const Foo = (function() {
let count = 0;
function Foo() {
if (!(this instanceof Foo)) {
return new Foo();
}
count++;
this.id = count;
}
return Foo;
})();
const foo1 = new Foo();
console.log(foo1); // { id: 1 }
const foo2 = new Foo();
console.log(foo2); // { id: 2 }
const foo3 = Foo();
console.log(foo3); // { id: 3 }