前端基础知识

ops/2025/2/25 17:00:00/

1. 变量和常量

1.1 变量

        // 变量let name = 'Jack'let age = 20name = 'lisi'age = 18

1.2 常量

        // 常量const PI = 3.14// PI = 3.1415926 // error,常量不可重新赋值const articleList = []const user = {name: 'vue3',age: 10}

1.3 const 声明的数组和对象

因为数组和对象在 JS 中属于引用类型,对其做添加、删除等操作,并不改变其内存地址,所以可以对其进行修改

        // 数组const arr = [1, 2, 3]// 添加arr.push(4)// 删除arr.shift()console.log(arr)// 对象const obj = {name: 'lisi',age: 10}// 添加属性obj.birth = 2015console.log(obj)// 修改属性obj.age = 18console.log(obj)

运行结果:

2. 模版字符串

2.1 普通字符串

用一对单引号或双引号声明,基本上都是用单引号

        // 普通字符串let name = 'Jack'let msg = "Hello World"

2.2 模版字符串

用一对反引号声明

        // 模版字符串let name = `Jack`let msg = `Hello World`

2.3 模版字符串的优势

2.3.1 可任意换行

        let htmlStr = '<div class="hot-goods-box"><h1>热门商品</h1><p>卖爆了卖爆了卖爆了</p></div>'let htmlStr = `<div class="hot-goods-box"><h1>热门商品</h1><p>卖爆了卖爆了卖爆了</p></div>`

2.3.2 可嵌入表达式

避免了繁琐的 加号 做字符串拼接

嵌入语法:${表达式}

        let name = 'lisi'let age = 9// let str = 'My name is xxx, I am xxx years old, I am 已成年 or 未成年'// let str = 'My name is ' + name + ', I am ' + age + ' years old, I am ' + (age >= 18 ? '已成年' : '未成年')let str = `My name is ${name}, I am ${age} years old, I am ${age >= 18 ? '已成年' : '未成年'}`console.log(str)

运行结果:

3. 对象

3.1 取值

3.1.1 点取值

        const obj = {name: 'lisi',age: 18}// 点取值console.log(obj.name)console.log(obj.age)

3.1.2 中括号取值

        const obj = {name: 'lisi',age: 18}// 中括号取值console.log(obj['name'])console.log(obj['age'])

 3.1.3 特殊情况

当属性名是变量的时候,只能用中括号取值

        let x = 'name'let y = 'age'console.log(obj[x])console.log(obj[y])console.log(obj.x) // undefinedconsole.log(obj.y) // undefined

若此时还用 点取值,不会报错,但是会显示 undefined

运行结果:

3.2 简写

3.2.1 属性

当属性名和属性值的名字相同时,可以只写一个,通常会配合变量使用

        let min = 1let max = 99// 不简写const obj = {min: min,max: max}// 简写const obj = {min,max}

3.2.2 方法

        const obj = {// 不简写fn: function() {}}// 上面写法和下面写法是等价的const obj = {// 简写:连同 : 和 function 一起省略fn() {}}

4. 解构赋值

针对目标:数组 或对象

作用:让数组和对象的取值更加便捷

4.1 代码示例

1. 数组解构

        const arr = [11, 22, 33]// eg1:把 arr 中的三个元素分别赋值给变量 a, b, c// 数组下标实现:let a = arr[0]let b = arr[1]let c = arr[2]// 解构实现:let [a, b, c] = arrconsole.log(a, b, c) // 运行结果:11 22 33// eg2:把 arr 中的后两个元素分别赋值给变量 b, clet [, b, c] = arrconsole.log(b, c) // 运行结果:22 33// eg3:把 arr 中的第一个元素赋值给变量 a,第三个元素赋值给变量 clet [a, , c] = arrconsole.log(a, c) // 运行结果:11 33// eg4:把 arr 的第一个元素赋值给变量 a,剩余的全部给变量 restlet [a, ...rest] = arrconsole.log(a, rest) // 运行结果:(2) [22, 33]// eg5:把下面 arr 中的 3, 4 赋值给变量 a, bconst arr = [2, [3, 4], 5]let [, [a, b]] = arrconsole.log(a, b) // 运行结果:3 4

2. 对象解构

        const obj = {name: '胡图图',age: 3,address: '翻斗大街翻斗花园 2 号楼 1001 室'}// eg1:把 obj 的 3 个属性分别赋值给变量 name, age, address// 点赋值方式let name = obj.namelet age = obj.agelet address = obj.address// 解构方式let {age, name, address} = objconsole.log(name, age, address) // 运行结果:胡图图 3 翻斗大街翻斗花园 2 号楼 1001 室// eg2:把 obj 的 name, age 属性值赋值给变量 name, agelet {name, age} = objconsole.log(age, name) // 运行结果:3 '胡图图'//eg3:把 obj 的 name 属性值赋值给变量 name,剩余的赋值给变量 restlet {name, ...rest} = objconsole.log(name, rest)// eg4:把 obj 的 name 属性值赋值给 unamelet {name: uname} = objconsole.log(uname) // 运行结果:胡图图// eg5:把下面 obj 中的 code, message, result 的值取出来赋值给变量 code, message, listconst obj = {data: {code: 10000,message: '频道列表获取成功',result: ['HTML', 'CSS', 'JavaScript', 'Vue', 'SpringBoot']},status: 200,statusText: 'Ok'}// 解构拿到 dataconst {data} = objconsole.log(data)// 再解构 dataconst {code, message, result: list} = dataconsole.log(code, message, list)

5. 箭头函数

5.1 非箭头函数

        // 有名函数function fn() {// some code...// return 结果}// 函数表达式const fn = function() {}// 执行const result = fn()

5.2 箭头函数语法

        const fn = () => {}// 两数相加const add = (x, y) => {return x + y}const result = add(1, 1)console.log(result) // 运行结果:2

5.3 箭头函数特点

1. 当参数只有一个时,可以省略小括号

        // const log = (arg) => { // 等价于下面写法const log = arg => {console.log(arg)}log(666) // 运行结果:666log('箭头函数') // 运行结果:箭头函数

2. 当函数体只有一句话时,可以省略大括号,此时箭头函数自带 return 功能

        const add = (x, y) => {return x + y}// 上面等价于下面const add = (x, y) => x + yconsole.log(add(1, 2))

3. 当直接返回一个对象时,为了简写,需要给对象加一对小括号

        const state = () => {return {token: 'xxx',userInfo: {name: 'admin',id: 1}}}// 上面等价于下面const state = () => ({token: 'xxx',userInfo: {name: 'admin',id: 1}})console.log(state())

5.4 应用

可用于普通函数的声明,也多用于回调函数传参

        // 延时函数setTimeout(function() {console.log(666)}, 2000)// 用箭头函数简写如下setTimeout(() => {console.log(666)}, 2000)

6. 数组的重要方法

数组:用来存放相同类型的一组数,管理一组有序数据的集合

6.1 添加 push() 和 unshift()

        const arr = [11, 22, 33]// 添加const len = arr.push(44) // 尾部添加console.log(arr, len) // 运行结果:[11, 22, 33, 44] 4const len = arr.unshift(44) // 头部添加console.log(arr, len) // 运行结果:[44, 11, 22, 33] 4

6.2 删除 pop() 和 shift()

        const arr = [11, 22, 33]// 删除const last = arr.pop() // 尾部删除console.log(arr, last) // 运行结果:[11, 22] 33const first = arr.shift() // 头部删除console.log(arr, first) // 运行结果:[22, 33] 11

6.3 任意位置删除或添加 splice(startIndex, delCount, ...addItem)

  • startIndex:起始下标(从哪开始操作)
  • delCount:删除元素的个数
  • addItem:要添加的元素
        const arr = [11, 22, 33]const temp = arr.splice(1, 1) // 从下标为 1 的位置开始删除,共删除 1 个元素console.log(arr, temp) // 运行结果:[11, 33] [22]arr.splice(2, 0, 44) // 从下标为 2 的位置,删除 0 个元素,添加元素:44console.log(arr) // 运行结果:[11, 22, 44, 33]

6.4 包含 includes()

包含返回 true,否则返回 false,通常配合 if 语句做判断

        const arr = [11, 22, 33]// 包含console.log(arr.includes(33)) // trueconsole.log(arr.includes(55)) // false

6.5 遍历 forEach()

        const arr = [11, 22, 33, 44]// for 循环遍历for (let i = 0; i < arr.length; i++) {console.log(arr[i])}// forEach 循环遍历const result = arr.forEach((item, index, array) => {// item:每次遍历的元素// index:当前元素在数组中的下标// array:遍历数组本身console.log(item, index, array)})console.log(result) // 注意:forEach() 只遍历数组,只是对 for 循环的简化而已,无返回值

6.6 过滤/筛选 filter()

保留满足条件的,去掉不满足条件的

        const arr = [11, 22, 33, 44]arr.filter((item, index, array) => {// 内部会遍历数组,每遍历一次都会执行回调一次// 如果返回 true,则当前元素会保留,否则去掉return 布尔值})// 需求:留下 arr 中所有的偶数const evenArr = arr.filter((item) => {if (item % 2 === 0) { // 使用全等号(===),比 == 效率高,不会发生隐式类型转换return true} else {return false}})// 简化后:const evenArr = arr.filter((item) => {return item % 2 === 0})// 进一步简化const evenArr = arr.filter((item) => item % 2 === 0)console.log(evenArr) // 运行结果:[22, 44]// 需求:留下 arr 中大于 30 的数const filterArr = arr.filter((item) => item > 30)console.log(filterArr) //运行结果:[33, 44]

6.7 映射 map()

由一个数组得到另一个数组,二者满足两个条件:

  1. 两个数组长度一致(元素个数一致)
  2. 新数组中的每个元素可以由原数组中的每个元素一一得到
        const arr = [11, 22, 33, 44]// 语法arr.map((item, index, array) => {return 新值})// 需求:得到每个元素翻倍的新数组:[22, 44, 66, 88]const doubleArr = arr.map((item) => {return item * 2})// 简写:const doubleArr = arr.map((item) => item * 2)console.log(doubleArr) // 运行结果:[22, 44, 66, 88]// 需求:得到如下对象数组// [//     { index: 0, value: 11 },//     { index: 1, value: 22 },//     { index: 2, value: 33 },//     { index: 3, value: 44 }// ]const newArr = arr.map((value, index) => {return {index,value}})// 简写:const newArr = arr.map((value, index) => ({ index, value }))console.log(newArr)

6.8 检测每一个 every()

        const arr = [11, 22, 33, 44]// 语法// arr.every((item, index, array) => {//     // 1. 如果返回 true,说明当前元素满足条件,则继续检测下一次,若都满足条件,则最终返回 true//     // 2. 如果返回 false,说明当前元素不满足条件,立即停止检测,最终返回 false//     return 布尔值// })// 需求:判断 arr 中元素是否都为奇数const flag = arr.every((item) => item % 2 ===1)console.log(flag)// 需求:检测 arr 中的数是否都大于 10const flag = arr.every((item) => item > 10)console.log(flag)

6.9 汇总 reduce()

常用语数组求和,但不限于求和

        const arr = [11, 22, 33, 44]// 语法(有两个参数,第一个是回调函数,第二个是初始值)const result = arr.reduce((prev, item, index, array) => {return 结果}, 初始值)// 需求:遍历求和// forEach 方式:let sum = 0arr.forEach(item => {sum += item})console.log(sum)// reduce 方式:arr.reduce((prev, item, index, array) => {// prev 第一次的初始值就是 reduce 的第二个参数 0// 后面再进入回调函数,prev 的值是上一次回调函数的返回值// 若上一次回调函数无返回值,则 prev 就是 undefinedconsole.log(prev, item, index, array)return prev + item}, 0)// 简写:const sum = arr.reduce((prev, item) => prev + item, 0)console.log(sum)// 商品列表数组const goodsList = [{ id: 1, name: '篮球', num: 1 },{ id: 2, name: '玩具', num: 3 },{ id: 3, name: '书籍', num: 2 }]// 需求:求总数量const totalNum = goodsList.reduce((prev, item) => prev + item.num, 0)console.log(totalNum)

7. 对象的重要方法

Object.keys()

用来获取对象的键的数组,从而可以很灵活的遍历对象(遍历对象的每一对键值对)

        const obj = {id: 100001,name: '胡图图',age: 3,address: '翻斗大街翻斗花园 2 号楼 1001 室'}// 使用 for-in 遍历对象for (let key in obj) {// console.log(key, typeof key) // typeof 可以判断当前 key 是什么类型console.log(key, obj[key]) // tip:此处 key 是变量,只能使用 [] 取值,且不能加引号}// 使用 Object.keys() + forEach() 遍历对象Object.keys(obj).forEach((key) => {console.log(key, obj[key])})// Object.keys() 返回的是数组,我们可以对数组进行很多操作之后,再进行遍历对象,更灵活,更强大// 需求:只遍历 obj 中以字母 a 开头的属性Object.keys(obj).filter((key) => key.startsWith('a')) // 过滤掉首字母不是 a 的 key.forEach(key => {console.log(key, obj[key])})

8. 扩展运算符 ...

作用:

  1. 在解构赋值时,用于收集余下所有的
  2. 复制数组或对象
  3. 合并数组或对象

8.1 复制数组或对象

        // 复制数组或对象,复制,也叫做拷贝/克隆/备份const arr1 = [11, 22, 33]const arr2 = arr1 // 这是引用,不是复制,两者会相互影响(因为其指向的是同一个地址)arr2.push(44) // 修改了 arr2,arr1 也会被修改console.log(arr1) // 运行结果:[11, 22, 33, 44]const arr2 = [...arr1] // 把 arr1 复制一份给 arr2,二者值相同,但地址不同,相互独立arr2.push(44)console.log(arr1) // [11, 22, 33]// 复制对象const obj1 = {id: 100001,name: '胡图图',age: 3,}const obj2 = {...obj1}obj2.age = 10console.log(obj1) // 运行结果:{id: 100001, name: '胡图图', age: 3}console.log(obj2) // 运行结果:{id: 100001, name: '胡图图', age: 10}

8.2 合并数组或对象

        // 合并数组const arr1 = [1, 2, 3]const arr2 = [4, 5, 6]const arr = [...arr1, ...arr2]console.log(arr) // 运行结果:[1, 2, 3, 4, 5, 6]// 合并对象const obj1 = {name: 'Jack',height: 178}const obj2 = {height: 180,age: 18}const obj = {...obj1,...obj2 // 此处 obj2 中的 height 会覆盖掉 obj1 中的 height}console.log(obj) // 运行结果:{name: 'Jack', height: 180, age: 18}

9. 序列化和反序列化

9.1 序列化

序列化:把对象转换为 json 格式字符串

json 格式:是一种严格的对象表示法,体现在 json 格式数据的属性名必须用双引号;若存在字符串类型必须用双引号

        // 普通对象const obj = {id: 10001,name: '胡图图',age: 3}// json 格式const json = {"id": 10001,"name": "胡图图","age": 3}// 序列化语法// JSON.stringify(对象)console.log(JSON.stringify(obj))

9.2 反序列化

反序列化:把 json 字符串转换为对象

        // json 字符串const jsonStr = '{"id": 10001, "name": "胡图图", "age": 9}'// 反序列化console.log(JSON.parse(jsonStr))

10. Web 存储

10.1 介绍

Web Storage 包含如下两种机制:

1. sessionStorage 该存储区域在页面会话期间可用(即只要浏览器处于打开状态,包含页面重新加载和恢复)

  • 仅为会话存储数据,这意味着数据将一直存储到浏览器(或选项卡)关闭
  • 数据永远不会被传输到服务器
  • 存储限额大于 Cookie(最大 5MB)

2. localStorage 即使浏览器关闭并重新打开也仍然存在

  • 存储的数据没有过期日期,只能通过 JavaScript、请求浏览器缓存或本地存储的数据来清除

10.2 用法

以 localStrorage 为例,学习存、取、删

1. 存

        // 存localStorage.setItem(key:string, value:string)// eglocalStorage.setItem('uname', 'lisi')// 存对象和数组,需要序列化

2. 取

        // 取// 如果存在 key,则取出相应的数据;否则取值为 null(表示 key 不存在)localStorage.getItem(key:string)// egconsole.log(localStorage.getItem('uname'))// 取出对象和数组,需要反序列化

3. 删

        // 删localStorage.removeItem(key:string)localStorage.removeItem('uname')

4. 注意

存储对象或数组需要进行序列化和反序列化

        const obj = {id: 10001,name: '胡图图',age: 9}// 存:序列化localStorage.setItem('胡图图', JSON.stringify(obj))// 取:反序列化console.log(JSON.parse(localStorage.getItem('胡图图')))

11. Promise + Aysnc/Await

11.1 为什么需要 Promise

为了消除回调地狱

        // 需求:延迟 2 秒输出 1,完了之后延迟 1 秒输出 2,完了之后延迟 1 秒输出 3// 语法:两个参数,一个是回调函数,一个是延迟时间setTimeout(() => {console.log('setTimeout')}, 3000)// 实现setTimeout(() => {console.log(1)}, 2000)setTimeout(() => {console.log(2)}, 1000)setTimeout(() => {console.log(3)}, 1000)// 发现浏览器打印顺序为:2 3 1,这是因为 js 代码的执行顺序为 异步的// 同步代码:串行执行,前面的代码先执行,后面的代码后执行// 异步代码:并行执行,大家同时开始执行,后面的代码不会等待前面代码执行完毕才执行// 修改成以下代码可以完成需求:setTimeout(() => {console.log(1)setTimeout(() => {console.log(2)setTimeout(() => {console.log(3)}, 1000)}, 1000)}, 2000)// 上述代码存在的问题:回调套回调(又称回调地狱),代码的可读性差

为了消除回调嵌套,Promise 就应运而生了 

11.2 Promise 介绍

Promise 是一个类,用来包装异步操作,根据异步操作的结果,是成功还是失败,进而决定 Promise 是成功还是失败

Promise 支持链式调用,从而消除回调地狱

11.3 Promise 的 3 种状态

Pending: 进行中,此时 Promise 的状态还不确定
Fullfilled: 成功,状态确定了
Rejected: 失败,状态确定了

Promise 的状态

  1. 只能由 Pending -> Fullfilled,或 Pending -> Rejected
  2. Promise 的状态一旦确定,就不可改变了

11.4 基本使用

        // Promise 基本使用const p = new Promise((resolve, reject) => {// 如果异步操作成功了,需要调用 resolve(成功的数据)// 如果异步操作失败了,需要调用 reject(失败的原因)// 在这里进行包装异步代码,如:定时器、ajax 请求setTimeout(() => {// 调用 resolve(),表示当前 Promise 成功了// resolve('ok')reject('error')}, 2000)})p.then((msg) => { // 第一个回调表示 resolve(可传入参数)console.log('成功了', msg)}, (err) => { // 第二个回调表示 rejectconsole.log('失败了', err)})

 

11.5 消除上述回调地狱

        /*** 延迟输出的异步函数,用 Promise 进行封装* duration:延迟的时间* n:延迟输出的数*/function delay(duration, n) {return new Promise((resolve) => {setTimeout(() => {resolve(n)}, duration)})}// Promise 的链式调用消除回调地狱delay(2000, 1).then((n1) => {console.log(n1)return delay(1000, 2)}).then((n2) => {console.log(n2)return delay(1000, 3)}).then((n3) => {console.log(n3)})

上述代码虽然消除了回调地狱,但链式调用过长,也不利于阅读

为了继续优化,Aysnc + Await 就应运而生了 

11.6 Aysnc + Await 异步终极解决方案

        // Promise 封装延迟输出函数function delay(duration, n) {return new Promise((resolve) => {setTimeout(() => {resolve(n)}, duration)})}async function log() {// 1. 在 Promise 实例前添加 await 关键字,这样 await 的返回值就是 Promise 的 resolve 参数// 2. await 所在的函数必须被 async 修饰// 3. await 所在的函数会按照同步的方式进行代码的执行,也就是前面的 await 执行完毕了,才会执行后续的 awaitconst n1 = await delay(2000, 1)console.log(n1)const n2 = await delay(1000, 2)console.log(n2)const n3 = await delay(1000, 3)console.log(n3)}log()

12. 模块化

12.1 概述

模块:一个文件就是一个模块,模块=文件

模块化:一种代码的开发思想,是指将一个复杂程序划分为一系列独立、可互操作的模块的过程。每个模块负责特定的功能或任务,并通过定义好的接口与其他模块进行通信。简单来说,就是将代码进行分解、按功能进行管理。

不同模块之间需要通过导入导出的方式进行模块通信,提高代码的可维护性、可重用性、可测试性和可扩展性,让开发者能够更容易地处理大型 JavaScript 项目。

12.2 目录结构准备

1. 新建 12-es-module 目录

2. 命令行进入 12-es-module 目录

两种方式:

1)

2)

3. 执行 npm init,初始化得到 package.json 文件

 

4. 给 package.json 添加 "type": "module"

5. 根目录下新建 src/index.js 作为代码的入口文件

6. src 目录下新建 utils 目录

12.3 默认导出与导入

新建 utils/min.js

// 定义求两个数最小值的函数并默认导出
export default function min(m, n) {return m > n ? n : m
}// tip:默认导出 export default 在一个模块中最多出现 1 次

src/index.js

// 默认导入 min 函数
// 默认导入的变量名可以随便起,但为了语义化,建议和模块功能保持一致
import min from './utils/min.js'console.log(min(12, 45)) // 打印 12

tip:

12.4 按需导出与导入

新建 utils/math.js

// 定义求和函数并按需导出
export function add(x, y) {return x + y
}// 定义作差函数并按需导出
export function sub(x, y) {return x - y
}// tip:按需导出 export 在一个模块中可以出现多次

src/index.js

// 按需导入
// tip:按需导入的变量必须用花括号包起来,并且变量名和导出的变量名必须一致
import {add, sub} from './utils/math.js'console.log(add(33, 18)) // 51
console.log(sub(33, 18)) // 15

运行结果:


http://www.ppmy.cn/ops/161249.html

相关文章

instanceof和typeof的区别【JavaScript常见面试题】

typeof &#xff08;一元操作符&#xff09;&#xff1a; 用于判断一个变量的类型。它可以用于任何数据类型&#xff0c;并返回一个表示该类型的字符串。 console.log(typeof 42); // "number" console.log(typeof Hello); // "string" conso…

SQL:DQL数据查询语言以及系统函数(oracle)

SQL Structured Query Language&#xff0c;结构化查询语言, 是一种用于管理和操作关系数据库的标准编程语言。 sql的分类 DQL&#xff08;Data Query Language&#xff09;&#xff1a;数据查询语言 DDL&#xff08;Data Definition Language&#xff09;&#xff1a;数据…

Prompt-to-Prompt 进行图像编辑

Prompt-to-Prompt 图像编辑是一种基于注意力机制的图像编辑技术&#xff0c;它通过在输入图像和编辑目标之间建立一个双向注意力机制来实现图像编辑。这种技术可以让模型根据输入图像的内容和编辑目标的描述来进行图像编辑。 交叉注意力控制是 Prompt-to-Prompt 图像编辑中的一…

Angular 中获取 DOM 节点的几种方法

文章目录 1. 使用ViewChild获取单个 DOM 节点2. 使用ViewChildren获取多个 DOM 节点3. 使用ElementRef直接访问 DOM4. 使用Renderer2操作 DOM5. 总结 在 Angular 开发中&#xff0c;虽然框架鼓励我们通过组件和模板来操作 DOM&#xff0c;但在某些情况下&#xff0c;直接访问和…

1_安装JDK和Hadoop

一、解压jdk和hadoop安装包 下载 通过百度网盘分享的文件&#xff1a;jdk-8u172-linux-x64.tar.gz 链接&#xff1a;https://pan.baidu.com/s/1VjhdpfyqdC7ivEBIjTn8tA 提取码&#xff1a;iz25 二、配置环境变量 vi /root/.bashrc添加 #set java environment export JAVA_H…

DeepSeek安装部署笔记(一)

Ollamaopen-WebUI部署 DeepSeek安装部署笔记第一步 Ollama安装1.安装ollama&#xff1a;官网https://ollama.com/下载2.上面安装完成&#xff0c;在cmd命令行&#xff1a; 第二步 给DeepSeek添加OpenWebUI界面&#xff08;重点&#xff09;1.安装conda&#xff1a;用它来管理py…

CSS实现图片缺角效果

效果&#xff1a; 源码&#xff1a; <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>123</title…

电力通信物联网应用,国密网关守护电力数据安全

电力国密网关是用于保护电力调度数据网路由器和电力系统的局域网之间通信安全的电力专用网关机&#xff0c;主要为上下级控制系统之间的广域网通信提供认证与加密服务&#xff0c;实现数据传输的机密性、完整性。 国密算法网关功能特点 身份认证&#xff1a;对接入的设备和用户…