报错
let arr = [1, 2, 3]
[arr[0], arr[1]] = [arr[1], arr[0]]
console.log(arr);
每一行一定要有;, 要不然就会把第1,2行当成一行,导致报错
let arr = [1, 2, 3];
[arr[0], arr[1]] = [arr[1], arr[0]];
console.log(arr);
数组特殊情况
let arr = [1, 2, 3];
[arr[0], arr[0]] = [arr[0], arr[0]];
console.log(arr); // success
let arr = [1, 2, 3];
[arr[1], arr[0]] = [arr[0], arr[1]];
console.log(arr); // success
我当前有一个链表a, 我将a赋值给b, 这种复制是深拷贝还是浅拷贝 let a = new ListNode() let b = a
不管链表的val是基本,还是引用,都是浅拷贝
function ListNode(val, next) {
this.val = (val === undefined ? 0 : val)
this.next = (next === undefined ? null : next)
}
let a = new ListNode(1);
a.next = new ListNode(2);
a.next.next = new ListNode(3);
a.next.next.next = new ListNode(4)
a.next.next.next.next = new ListNode(5)
let b = a
let c = b
while (b) {
if (b.val === 3) {
b.next = b.next.next
break
}
b = b.next
}
console.log(a); // [1,2,3,5]
console.log(b); // [3,5]
console.log(c); // [1,2,3,5]
// 改变一个,必然影响剩下两个
对于你提到的链表节点ListNode,如果它是一个对象,那么在JavaScript中,let b = a将创建a对象的一个引用,而不是一个深拷贝。这意味着a和b将指向内存中的同一个ListNode对象。对b所做的任何修改也将反映在a上,反之亦然。
如果你需要一个真正的深拷贝,即一个完全独立的副本,其中a和b的修改不会相互影响,你需要手动实现拷贝逻辑,例如通过遍历原始链表并创建一个新的链表节点来实现。
是的,如果你直接使用 let b = a; 这样的赋值语句,那么在JavaScript中,这将创建一个浅拷贝。对于对象(包括链表节点这样的自定义对象),浅拷贝意味着两个变量(在这个例子中是 a 和 b)将引用内存中的同一个对象实例。
因此,如果你对 b 指向的链表进行修改(例如,添加、删除或修改节点),这些更改也会反映在 a 指向的链表上,因为它们实际上是同一个链表对象。反之亦然,对 a 的更改也会影响 b。
将链表拆成两部分
function ListNode(val, next) {
this.val = (val === undefined ? 0 : val)
this.next = (next === undefined ? null : next)
}
let a = new ListNode(1);
a.next = new ListNode(2);
a.next.next = new ListNode(3);
a.next.next.next = new ListNode(4)
a.next.next.next.next = new ListNode(5)
let fast = a
let slow = a
let newList = new ListNode()
let copy = newList
while (fast && fast.next) {
newList.next = new ListNode(slow.val)
newList = newList.next
fast = fast.next.next
slow = slow.next
}
newList.next = new ListNode(slow.val)
console.log(slow === fast); // false,
console.log(slow); // [3,4,5]
console.log(fast); // [5]
// 之所有打印的不一样,是因为他们虽然继承的链表相同,但是指针指向的位置不同。
console.log(copy.next); // [1,2,3]
let b = slow
b = b.next
console.log(b); //[4,5]
链表的一些情况
let a = new ListNode()
a.next = new ListNode(1) // [0, 1]。
// 因为初始化时值为0,然后0指向了1.
let b = new ListNode(-9)
b.next = a // [-9, 0, 1]
当确定某个指针g处于某个链表中,
优先 b.next = g
等价于 b.next = new ListNode(g.val)
前后端分离技术,前端一般通过ajax进行数据的请求。通过ajax去请求数据, 返回的是一个异步的数据。异步的数据会带来一些问题,比如说它不利于搜索引擎机制的搜索。同时对异步的数据进行加载的时候也会出现一些问题,比如加载速度变慢。 这种说法有问题吗
这种说法中包含一些正确的观点,但也有一些需要澄清的地方:
- 搜索引擎优化(SEO) :
-
- 确实,传统的搜索引擎优化主要针对服务器端渲染的内容。如果一个网站完全基于AJAX动态加载内容,搜索引擎可能难以索引这些内容,因为搜索引擎爬虫通常不会执行JavaScript代码来获取通过AJAX请求的内容。
- 然而,现代搜索引擎如Google已经变得更加智能,能够通过JavaScript渲染内容,但这通常需要额外的优化,比如使用服务端渲染(SSR)或预渲染(Prerendering)。
- 异步数据加载问题:
-
- AJAX请求是异步的,这意味着它们允许网页在不重新加载整个页面的情况下与服务器交换数据。这种异步性是现代Web应用程序用户体验的关键部分,因为它允许用户在不中断的情况下与页面交互。
- 异步数据加载并不直接导致加载速度变慢。实际上,由于可以仅加载需要的数据,AJAX可以减少页面加载时间,提高性能。然而,如果AJAX请求没有被正确管理,比如进行了过多的请求或请求了过多的数据,那么可能会影响性能。
- 性能问题:
-
- 加载速度变慢可能是由于多种因素造成的,包括网络延迟、服务器响应时间、前端处理数据的效率、请求的大小等。AJAX请求本身并不慢,但如果不正确地使用,比如进行了不必要的大量请求,或者没有对数据进行适当的缓存,那么可能会导致性能问题。
- 用户体验:
-
- 对于用户来说,异步数据加载可以提供更流畅的体验,因为它允许页面在后台加载数据而不干扰用户操作。然而,开发者需要确保提供适当的加载指示(如加载动画或进度条),以便用户知道数据正在加载中。
- 技术选择:
-
- 前后端分离是一种架构模式,其中前端和后端作为两个独立的系统运行,它们通过API进行通信。AJAX是前端与后端通信的一种方式,但不是唯一的方式。现代前端开发中,除了AJAX,还可以使用Fetch API、GraphQL等技术。
- 其他考虑:
-
- 除了SEO和性能,异步数据加载还需要考虑状态管理、错误处理、数据缓存和更新策略等。
nodejs
编译,构建,部署
在软件开发中,编译、构建和部署是软件开发生命周期中的三个关键步骤,它们各自承担着不同的职责:
编译(Compilation)
编译是指将源代码转换为可执行文件的过程。这个过程通常涉及到以下几个方面:
- 语言编译:将高级编程语言(如C、C++、Java等)编写的源代码转换成机器语言,生成可执行文件或字节码。
- 平台转换:确保代码能够在特定的操作系统或硬件平台上运行。
- 优化:编译器可能会执行代码优化,以提高程序的运行效率。
- 错误检查:编译过程中会检查语法错误和一些运行时错误。
对于现代Web开发,编译可能包括:
- 将ES6+ JavaScript代码转换为ES5,以便旧版浏览器可以运行。
- 将Sass或Less转换为CSS。
- 将TypeScript转换为JavaScript。
- 打包和压缩JavaScript、CSS和HTML文件。
构建(Building)
构建通常指的是将源代码和其他资源(如图像、库文件、配置文件等)打包成一种适合部署的格式的过程。构建过程可能包括以下步骤:
- 文件合并:将多个源代码文件或资源文件合并成一个或几个文件。
- 压缩:减小文件大小,以加快加载时间。
- 模块解析:处理模块和依赖关系,如JavaScript模块或Web组件。
- 编译:可能包括上述的编译步骤,如语言编译或转译。
- 构建工具:使用构建工具(如Webpack、Grunt、Gulp等)自动化构建流程。
部署(Deployment)
部署是指将构建好的应用程序或软件安装到生产环境或其他运行环境中,以便用户可以访问和使用的过程。部署涉及:
- 发布:将应用程序的可执行文件、库、配置和资源文件等发布到服务器。
- 配置:设置服务器环境,包括数据库、API端点、安全设置等。
- 启动:在服务器上启动应用程序,可能涉及到运行特定的命令或脚本。
- 测试:在生产环境中进行测试,确保应用程序在实际使用中表现正常。
- 回滚计划:准备回滚机制,以便在部署出现问题时可以快速恢复到之前的稳定版本。
持续集成/持续部署(CI/CD)
现代软件开发实践中,通常会使用持续集成和持续部署的流程来自动化编译、构建和部署的步骤:
- 持续集成(CI) :每当代码库更新时,自动运行编译和测试,以确保新代码不会破坏现有功能。
- 持续部署(CD) :在通过所有测试后,自动将代码部署到生产环境,或者至少准备自动部署。
CI/CD流程的自动化可以显著提高软件交付的速度和质量,减少人为错误,确保更快的反馈循环。
持续集成/持续部署(CI/CD)
持续集成/持续部署(CI/CD)是一种现代软件开发实践,旨在加速软件交付周期并提高软件质量。这一流程通过自动化多个开发阶段,从代码提交到产品部署,来实现快速、频繁且可靠的软件发布。下面是对CI和CD两个主要组成部分的简要说明:
持续集成(Continuous Integration, CI)
持续集成强调开发人员频繁地将代码更改合并到共享的主线(通常是主分支)上。每次代码合并后,自动化的构建和测试流程立即启动,以验证新代码是否与现有代码库兼容,并且没有引入新的错误。这包括但不限于以下步骤:
- 代码拉取:从版本控制系统中获取最新代码。
- 编译:将源代码编译成可执行的形式(如果适用)。
- 静态代码分析:检查代码质量,遵循编程规范。
- 单元测试:运行自动化单元测试,确保代码模块按预期工作。
- 集成测试:进行更高级别的测试,确保各个组件协同工作。
持续部署(Continuous Deployment, CD)
持续部署是CI的自然延伸,目标是将经过测试的代码自动部署到生产环境。一旦代码通过了CI阶段的所有测试,它就会被自动部署到生产服务器上,无需人工干预。这意味着任何通过所有自动化测试的变更都可以几乎立即到达用户手中。CD流程通常包括:
- 构建产物打包:将应用打包成适合部署的格式。
- 部署到预生产环境:首先部署到类似生产环境的预生产环境中进一步测试。
- 自动化测试(包括性能、安全等) :在预生产环境中进行更全面的测试。
- 监控和回滚策略:部署后持续监控应用状态,发现问题时能迅速回滚到前一个稳定版本。
工具与实践
实现CI/CD通常需要借助一系列工具和服务,例如:
- 自动化构建工具:Jenkins, Travis CI, CircleCI, GitLab CI/CD, GitHub Actions等。
- 代码托管平台:GitHub, GitLab, Bitbucket等,它们通常集成了CI/CD功能。
- 容器化与编排工具:Docker, Kubernetes等,用于标准化部署和扩展应用。
- 测试框架:JUnit, Mocha, Jest等,用于编写和运行自动化测试。
CI/CD不仅是一种技术实践,也是一种文化和方法论,要求团队成员紧密协作,编写可测试的代码,并且对自动化流程有深入的理解和应用。通过实施CI/CD,团队能够更快地迭代产品,更快地发现并修复问题,同时减少人工错误,提升软件交付的整体效率和质量。
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(2)
}, 0)
})
const p2 = Promise.reject(9);
const p3 = 3
console.log(Promise.race([p1, p2, p3])); // rejected, 9
console.log(Promise.all([p1, p2, p3])); // reject, 9
console.log(Promise.allSettled([p1, p2, p3])); // fulfilled, 2,9,3, 不受异步的变化
当执行同步代码时,放在微任务队列里的称为第一轮微任务。微任务会按照顺序推送到执行栈里按顺序执行,在执行微任务的时候,又出现了微任务,会将这个微任务放到微任务队列的最后面,他是所有微任务里优先级最低的,但是高于宏任务。
如下,12-13行就是
setTimeout(() => {
console.log(9999);
})
new Promise((resolve, reject) => {
console.log(1);
resolve(2)
console.log(3);
}).then(() => {
console.log(777);
}).then(() => {
console.log(5);
})
async function aa() {
console.log(8);
await bb()
console.log(999);
console.log(9990);
}
aa()
async function bb() {
console.log(10);
}
// 1, 3, 8, 10, 777, 999, 9990, 5, 9999
前端实现跨平台的手段
- HTML5/CSS3/JavaScript:这是构建网页的基础技术,它们在所有现代浏览器上都能够运行,因此可以跨不同的操作系统和设备。
- 响应式设计:通过使用媒体查询和灵活的布局,可以创建适应不同屏幕尺寸的网页,从而在手机、平板和桌面电脑上都能提供良好的用户体验。
- 框架和库:使用如React、Angular、Vue等前端框架和库可以提高开发效率,并且这些框架和库都支持跨平台开发。
- Web容器:如Electron和NW.js等,它们允许使用Web技术来开发桌面应用程序,实现一次编写,多平台运行。
- 跨平台框架:如React Native、Flutter和Apache Cordova等,它们允许开发者使用Web技术或类似Web技术的语言来开发原生移动应用。
- 渐进式Web应用(PWA) :PWA提供了一种方法,使得Web应用可以在没有网络连接的情况下运行,并且可以添加到主屏幕,提供类似原生应用的体验。
- WebAssembly:这是一种新的Web技术,允许在Web浏览器中以接近原生性能运行编译后的代码,从而扩展了Web应用的能力。
- CSS预处理器:如Sass和Less,它们提供了更强大的样式编写能力,帮助开发者创建更复杂和可维护的样式表。
node事件循环相较于浏览器事件循环有什么不同
Node.js更注重服务器端的高并发I/O操作,而浏览器则更注重用户交互和页面渲染的性能。开发者在使用这些环境时,需要根据它们的特点来优化代码和处理异步操作。
Node.js 的事件循环相较于浏览器事件循环的异同。同步,异步,宏任务和微任务顺序都一致吗
相似之处:
- 事件驱动:两者都是基于事件驱动的模型,可以处理异步事件。
- 回调函数:在异步操作完成后,两者都会调用预先定义的回调函数。
- 宏任务和微任务的概念:两者都区分了宏任务(如setTimeout和setInterval)和微任务(如Promise的.then()和MutationObserver)。
- 非阻塞I/O:Node.js和现代浏览器都支持非阻塞I/O操作,允许在等待I/O操作完成时执行其他任务。
差异之处:
- 环境:Node.js运行在服务器端,而浏览器运行在客户端。
- 事件循环的实现:Node.js使用libuv库实现事件循环,而浏览器使用自己的事件循环机制。
- I/O操作:Node.js更侧重于处理文件系统、网络请求等I/O操作,而浏览器更侧重于处理DOM操作、用户交互等。
- 线程模型:Node.js使用单线程模型处理事件循环,而浏览器使用多线程模型,JavaScript执行在主线程上,渲染和解析可能在其他线程上。
restful api
RESTful API(Representational State Transfer Application Programming Interface)是一种基于HTTP协议的轻量级架构风格,用于网络应用程序之间的互操作性。RESTful API 遵循以下原则:
- 无状态(Stateless) :每个请求从客户端到服务器都应包含所有必要的信息来理解和处理请求。服务器不会存储任何客户端请求之间的信息。
- 统一接口(Uniform Interface) :客户端和服务器之间的接口应该是统一的,使得API容易理解。
- 资源导向(Resource-Oriented) :API 应该以资源为中心,每个资源都有一个唯一的资源标识符(URI)。
- 通过HTTP方法表达行为:使用HTTP协议的GET、POST、PUT、DELETE等方法来执行资源的CRUD(创建、读取、更新、删除)操作。
- 通过状态码表达结果:使用HTTP状态码(如200、201、204、400、401、403、404、500等)来表示请求的结果。
- 支持超媒体作为应用状态的引擎(HATEOAS) :客户端可以通过服务器提供的动态链接来发现所有可用的操作和资源。
例如,一个简单的用户资源的RESTful API 可能包括以下操作:
- GET /api/users:获取所有用户列表。
- GET /api/users/1:获取ID为1的用户信息。
- POST /api/users:创建一个新用户。
- PUT /api/users/1:更新ID为1的用户信息。
- DELETE /api/users/1:删除ID为1的用户。
RESTful API 由于其简单性、可扩展性和易于使用的特点,已成为构建Web服务的主流方式之一。
用css去实现一个长比宽是4:3的div。
方法1:使用固定宽度和百分比高度
这是最简单直接的方法,你给div设置一个固定的宽度,然后使用基于宽度百分比的计算来设置高度。
.fixed-ratio-div {
width: 300px; /* 可以是任何单位,这里以像素为例 */
height: 225px; /* 宽度的3/4,保持4:3的长宽比 */
}
方法2:使用视口宽度百分比
这种方法利用视口的宽度来设置div的宽度,并计算出相应的高度以保持长宽比。
.viewport-relative-div {
width: 80vw; /* 视口宽度的80% */
height: 60vw; /* 视口宽度的60%,保持4:3的长宽比 */
}
方法3:使用CSS的padding-top技巧(padding-top的百分比相较于父元素宽度而言,他会撑起div的高度)
这种方法通过设置div的padding-top百分比来实现长宽比。这种方法在响应式设计中非常有用,因为它允许div的尺寸随其父容器的大小变化。
复制
.padding-top-div {
position: relative;
width: 100%; /* 或者其他百分比 */
padding-top: 75%; /* 宽度的75%,保持4:3的长宽比 */
overflow: hidden;
}
.padding-top-div::after {
content: '';
display: block;
}
方法5:使用CSS Grid
如果你的布局允许使用CSS Grid,那么可以创建一个网格容器,并设置网格轨道的大小来实现长宽比。
复制
.grid-div {
display: grid;
grid-template-columns: 1fr;
grid-template-rows: 1fr;
aspect-ratio: 4 / 3; /* 直接设置长宽比 */
}
函数组件和类组件的区别
函数组件
- 使用函数定义:函数组件简单地用JavaScript函数来定义,不接受this,因为它们没有实例。
- 更简洁:通常更简洁,因为没有类组件的额外语法。
- Hooks:在React 16.8及以后版本中,函数组件可以利用Hooks来使用状态(useState)和其他React特性,如生命周期函数的模拟(useEffect)。
- 无状态组件:最初设计为无状态组件,但可以通过Hooks添加状态。
- 性能优化:React可以更高效地渲染函数组件,因为它们没有与类组件相关的额外开销。
- 没有继承:不使用继承,而是使用组合,这通常更灵活。
- 较新:是React中较新的组件定义方式,被推荐用于新代码。
类组件
- 使用类定义:类组件使用JavaScript类来定义,并且继承自React.Component。
- 状态和生命周期:通过类属性(如this.state和this.props)和生命周期方法(如componentDidMount和componentDidUpdate)来管理状态和生命周期。
- 更复杂:通常比函数组件的代码量更多,更复杂。
- 必须使用 this:因为状态和生命周期方法都绑定到组件实例上,所以必须使用this来访问。
- 混入(Mixins) :在旧版本的React中,类组件可以使用混入,但在新版本中不推荐使用,因为可以使用Hooks来达到相同效果。
- 支持继承:可以继承其他类,但React官方推荐使用组合而不是继承。
- 生命周期方法:React 16.3引入了新的生命周期方法和新的API,如getDerivedStateFromProps和getSnapshotBeforeUpdate,这些在类组件中使用。
useState中传入一个函数, 传入一个非函数,两者有什么区别?
在React中使用useState时,你可以传入一个函数或一个非函数(通常是初始状态值)作为参数。这两种用法的区别在于它们如何初始化状态:
传入非函数(初始状态值)
当你向useState传入一个非函数值时,这个值将直接用作状态的初始状态。这是最常见的用法。
import React, { useState } from 'react';
function ExampleComponent() {
// 使用useState设置初始状态
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
</div>
);
}
在这个例子中,count的初始状态被设置为0。
传入一个函数
当你向useState传入一个函数时,这个函数会在渲染期间被调用,并且其返回值将用作状态的初始状态。这在你想要基于某些参数或props来计算初始状态时非常有用。
import React, { useState } from 'react';
function ExampleComponent({ initialCount }) {
// 使用useState设置初始状态,基于props
const [count, setCount] = useState(() => initialCount);
return (
<div>
<p>Count: {count}</p>
</div>
);
}
在这个例子中,count的初始状态基于props initialCount 来设置。这意味着每次initialCount改变时,状态的初始值也会相应地改变。
区别
- 性能优化:传入函数允许React在每次渲染时重新计算初始状态,这可以用于性能优化,特别是当初始状态依赖于可能会变化的值时。
- 惰性初始化:传入函数提供了一种惰性初始化状态的方式,即初始状态的计算可以推迟到组件实际渲染时进行。
- 依赖于渲染上下文:使用函数作为初始状态意味着状态的初始值可以依赖于渲染上下文,如props或其他在渲染时可用的信息。
选择使用哪种方式取决于你的具体需求。如果你的状态初始值是固定的,直接传入非函数值即可。如果你需要基于某些动态值来初始化状态,那么使用函数可能是更好的选择。
闭包是一个重要的JavaScript特性,它允许函数访问并操作定义它们的外部函数的变量。
function a() {
let num = 0
return function () {
return num + 1
}
}
const qq = a()
console.log(qq()); // 1
console.log(qq()); // 1
function a() {
let num = 0
return function () {
return ++num
}
}
const qq = a()
console.log(qq()); // 1
console.log(qq()); // 2
用js来实现useState的功能
function fn(sum) {
let state = sum
let setState = function (args) {
state = args
}
states = function () {
return state
}
return [states, setState]
}
const [state, setState] = fn(0)
setState(9)
setState(99)
console.log(states()); //99
react hooks中出现的闭包陷阱
在React Hooks中,闭包陷阱主要体现在以下几个方面:
- useState陷阱:
-
- 异步陷阱: 当在异步函数中使用useState的setter函数(如setValue)时,由于异步执行的特性,setter函数捕获的是闭包中当时的状态值。如果在异步操作完成前状态已经改变,setter函数更新的可能是过时的值,而不是期望的最新状态。
- useEffect陷阱:
-
- 过期闭包: useEffect的回调函数在组件首次渲染时创建,之后在依赖项改变或组件卸载时重新创建。如果回调中依赖于某个状态或props,而又没有将其正确地添加到依赖数组中,就可能捕获到一个过时的值,因为它是基于第一次渲染时的闭包环境。
- useCallback陷阱:
-
- 如果没有恰当地管理useCallback的依赖数组,可能会导致记忆化的回调函数持有过时的闭包状态,即使相关的依赖已经改变。
解决这些问题的方法包括:
- 确保依赖数组准确:在useEffect和useCallback中,明确列出所有外部变量作为依赖项,以确保每次这些变量变化时都能获取到最新的值并重新计算或执行。
- 使用函数式setState:对于useState,当需要基于前一个状态值进行计算时,使用函数式setState(如setValue(prevValue => prevValue + 1))可以确保总是基于最新的状态进行更新。
- 谨慎处理异步逻辑:在异步操作中使用状态更新时,确保使用正确的值,可能需要通过闭包捕获正确的状态或者使用函数式更新模式。
- 了解React的渲染机制:深入理解React如何处理函数组件渲染周期和Hooks的执行顺序,有助于避免闭包相关的错误。
随着React的不断更新,一些闭包相关的问题也得到了官方的关注,比如引入了useEvent这样的新Hook来帮助开发者更好地处理事件处理器中的闭包问题。
链表结构的定义,以及初始化。初始化不能使用.next,要不然当链表很长时,要写很多个next
function ListNode(val, next) {
this.val = (val === undefined ? 0 : val);
this.next = (next === undefined ? null : next);
}
ListNode.prototype.appends = function (val) {
// 创建一个新的ListNode实例
var newNode = new ListNode(val);
// 如果链表为空(即head为null),直接将head指向新节点
if (!this.next) {
this.next = newNode;
return;
}
// 寻找链表的最后一个节点
var current = this;
while (current.next) {
current = current.next;
}
// 将新节点接到链表末尾
current.next = newNode;
};
// 创建一个链表的头节点
let listNode1 = new ListNode();
// 向链表追加元素
listNode1.appends(1);
listNode1.appends(2);
// 为了能够查看链表的内容,我们可以添加一个打印链表的方法
ListNode.prototype.printList = function () {
var current = this;
while (current) {
console.log(current.val);
current = current.next;
}
};
// 打印链表以验证
listNode1.printList(); // 应该输出:1 2
注意
let a = new ListNode() // ListNode {val: 0, next: null}
let a = new ListNode(2) // ListNode {val: 2, next: null}
初始化链表时,不写初始值,默认val = 0
token存在哪里?
Token通常指的是一种令牌,它可以用于多种不同的场景。在计算机科学和信息技术领域,Token可以是:
- 访问令牌:用于用户身份验证,允许用户访问系统或服务。
- 加密令牌:用于加密和解密数据。
- 会话令牌:用于维护用户与服务器之间的会话状态。
- API 令牌:用于API调用,以验证请求的合法性。
Token的存储位置取决于它的用途和上下文。例如:
- 客户端:在Web应用程序中,Token可能存储在浏览器的Cookies或LocalStorage中。
- 服务器端:在服务器端,Token可能存储在数据库或内存中,用于验证和授权。
- 移动设备:在移动应用中,Token可能存储在应用的数据存储中,如Keychain(iOS)或Keystore(Android)。