阅读视图
Javascript的Iterator和Generator
Iterator和Generator
Iterator
简介
遍历器:Iterator是一种机制。可以把它理解成一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
Iterator 的作用有三个:
- 一是为各种数据结构,提供一个统一的、简便的访问接口;
- 二是使得数据结构的成员能够按某种次序排列;
- 三是 ES6 创造了一种新的遍历命令
for...of循环,Iterator 接口主要供for...of消费。
机制
Iterator 的遍历过程是这样的。
(1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
(2)第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。
(3)第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。
(4)不断调用指针对象的next方法,直到它指向数据结构的结束位置。
每一次调用next方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含value和done两个属性的对象。其中,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束。
下面是一个模拟next方法返回值的例子:
const arr = [1, 2, 3, 4, 5];
function myIterator(array) {
let nextIndex = 0;
return {
next: function () {
return nextIndex < array.length ? { value: arr[nextIndex++], done: false } : { value: undefined, done: true };
}
}
}
const it = myIterator(arr);
it.next(); // {value: 1, done: false}
it.next(); // {value: 2, done: false}
it.next(); // {value: 3, done: false}
it.next(); // {value: 4, done: false}
it.next(); // {value: 5, done: false}
it.next(); // {value: undefined, done: true}
当然 Iterator 只是把接口规格加到数据结构之上,所以,遍历器与它所遍历的那个数据结构,实际上是分开的。
默认 Iterator 接口
当使用for...of循环遍历某种数据结构时,该循环会自动去寻找 Iterator 接口。一种数据结构只要部署了 Iterator 接口,我们就称这种数据结构是“可遍历的”(iterable)。
ES6 的有些数据结构原生具备 Iterator 接口(比如数组),即不用任何处理,就可以被for...of循环遍历。原因在于,这些数据结构原生部署了Symbol.iterator属性,另外一些数据结构没有(比如对象)。凡是部署了Symbol.iterator属性的数据结构,就称为部署了遍历器接口。调用这个接口,就会返回一个遍历器对象。Symbol.iterator在任何作用域使用值都一样。
原生具备 Iterator 接口的数据结构如下:
- Array
- Map
- Set
- String
- TypedArray
- 函数的 arguments 对象
- NodeList 对象
下面的例子是数组的Symbol.iterator属性:
const arr = [1, 2, 3, 4, 5];
let iter = arr[Symbol.iterator]();
it.next(); // {value: 1, done: false}
it.next(); // {value: 2, done: false}
it.next(); // {value: 3, done: false}
it.next(); // {value: 4, done: false}
it.next(); // {value: 5, done: false}
it.next(); // {value: undefined, done: true}
综上表现,本质上,遍历器是一种线性处理,对于任何非线性的数据结构,部署遍历器接口,就等于部署一种线性转换。
实现一个 Iterator 接口
一个对象如果要具备可被for...of循环调用的 Iterator 接口,就必须在Symbol.iterator的属性上部署遍历器生成方法(原型链上的对象具有该方法也可)。
class RangeIterator {
constructor(start, stop) {
this.start = start;
this.stop = stop;
}
[Symbol.iterator]() {
return this;
}
next() {
let start = this.start;
if (start <= this.stop) {
this.start ++;
return { value: start, done: false };
}
return { value: undefined, done: true };
}
}
const obj = new RangeIterator(1, 5);
const ite = obj[Symbol.iterator]();
ite.next(); // { value: 1, done: false }
ite.next(); // { value: 2, done: false }
ite.next(); // { value: 3, done: false }
ite.next(); // { value: 4, done: false }
ite.next(); // { value: 5, done: false }
ite.next(); // { value: 6, done: false }
遍历器对象的return、throw
遍历器对象除了具有next()方法,还可以具有return()方法和throw()方法。如果你自己写遍历器对象生成函数,那么next()方法是必须部署的,return()方法和throw()方法是否部署是可选的。
return()方法的使用场合是,如果for...of循环提前退出(通常是因为出错,或者有break语句),就会调用return()方法。如果一个对象在完成遍历前,需要清理或释放资源,就可以部署return()方法。
调用 Iterator 接口的场合
-
数组和 Set 结构进行解构赋值时,会默认调用Symbol.iterator
-
扩展运算符(...)也会调用默认的 Iterator 接口(某个数据结构部署了 Iterator 接口,就可以对它使用扩展运算符)
-
yield* 后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口
-
数组的遍历会调用遍历器接口,所以任何接受数组作为参数的场合,其实都调用了遍历器接口,如下:
- for...of
- Array.from()
- Map(), Set(), WeakMap(), WeakSet()(比如new Map([['a',1],['b',2]]))
- Promise.all()
- Promise.race()
Generator
简介
Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。
机制
形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。
function* testGenerator() {
yield 1;
yield 2;
return 3;
}
let test = testGenerator();
test.next(); // { value: 1, done: false }
test.next(); // { value: 2, done: false }
test.next(); // { value: 3, done: true }
test.next(); // { value: undefined, done: true }
上面代码定义了一个 Generator 函数testGenerator,它内部有三个yield表达式(1、2、3),即该函数有四个状态:1,2,3 和 return 语句(结束执行)。
然后,Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是上面介绍的遍历器对象Iterator。下一步,必须调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。
yield 表达式
由于 Generator 函数返回的遍历器对象,只有调用next方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield表达式就是暂停标志。
遍历器对象的next方法的运行逻辑如下。
-
遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。
-
下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。
-
如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。
-
如果该函数没有return语句,则返回的对象的value属性值为undefined。
yield表达式只能用在 Generator 函数里面,用在其他地方都会报错。
function* testGenerator() {
// 可以在for循环中,但是不是在forEach等函数中
for (let i = 0;;i ++) {
if (i === 3) {
return i;
}
yield i;
}
}
let test = testGenerator();
test.next(); // { value: 0, done: false }
test.next(); // { value: 1, done: false }
test.next(); // { value: 2, done: true }
test.next(); // { value: undefined, done: true }
yield表达式如果用在另一个表达式之中,必须放在圆括号里面。
function* testGenerator() {
for (let i = 0;i < 3;i ++) {
console.log('next:' + (yield i));
}
}
let test = testGenerator();
test.next();
// 先:{ value: 0, done: false }
// 后:next:undefined
yield表达式用作函数参数或放在赋值表达式的右边,可以不加括号。
function* testGenerator() {
testFunc(yield 1);
let a = yield 2; // { value: 2, done: false }
console.log('next:', a); // next: undefined
yield 3;
}
let test = testGenerator();
test.next(); // 先 { value: 1, done: false },后 test: undefined
test.next(); // { value: 2, done: false }
test.next(); // 先 next: undefined,后 { value: 3, done: false }
与 Iterator 接口的关系
任意一个对象的Symbol.iterator方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象。Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的Symbol.iterator属性,从而使得该对象具有 Iterator 接口。
let obj = {};
obj[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
}
console.log([...obj]); // [1, 2, 3]
// 具有Symbol.iterator属性,即可使用拓展运算符
Generator 函数执行后,返回一个遍历器对象。该对象本身也具有Symbol.iterator属性,执行后返回自身。
function* test() {
yield true;
}
const iter = test();
console.log(iter[Symbol.iterator]() === iter); // true
next 方法的参数
上面的示例中yield 2本身并没有返回值,即为undefined。next方法可以带一个参数,该参数就会被当作上一个状态yield表达式的返回值。
function* test() {
let res1 = yield 1;
console.log(res1);
let res2 = yield 2;
console.log(res2);
let res3 = yield 3;
console.log(res3);
}
const iter = test();
iter.next('a'); // { value: 1, done: false }
iter.next('b'); // 先 b,后 { value: 2, done: false }
iter.next('c'); // 先 c,后 { value: 3, done: false }
iter.next('d'); // 先 d,后 { value: undefined, done: true }
所以正常情况来说,一个Generator函数中,第一个yield传递参数是没有作用的,因为并没有上一个状态去接收它的参数。
for...of 循环
for...of循环可以自动遍历 Generator 函数运行时生成的Iterator对象,且此时不再需要调用next方法。
function* test() {
yield 1;
yield 2;
yield 3;
}
for (let key of test()) {
console.log(key); // 1 2 3
}
throw 和 return
Generator 函数返回的遍历器对象,都有一个throw方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获。
function* test() {
try {
yield 1;
} catch (error) {
console.log('test:', error);
}
yield console.log(2);
}
const iter = test();
iter.next(); // { value: 1, done: false }
try {
iter.throw(new Error('出错了!')); // test: Error: 出错了! 2
iter.throw(new Error('出错了!')); // catch: Error: 出错了!
} catch (error) {
console.log('catch:', error);
}
上面代码中,遍历器对象iter连续抛出两个错误。第一个错误被 Generator 函数体内的catch语句捕获(前提是必须至少执行过一次next方法)。iter第二次抛出错误,由于 Generator 函数内部的catch语句已经执行过了,不会再捕捉到这个错误了,所以这个错误就被抛出了 Generator 函数体,被函数体外的catch语句捕获。
throw方法可以接受一个参数,该参数会被catch语句接收,建议抛出Error对象的实例。throw方法被捕获以后,会附带执行下一条yield表达式。也就是说,会附带执行一次next方法。
function* test() {
yield 1;
yield 2;
yield 3;
}
const iter = test();
iter.next(); // { value: 1, done: false }
iter.return('end'); // { value: 'end', done: true }
iter.next(); // { value: undefined, done: true }
上面代码中,遍历器对象调用return()方法后,返回值的value属性就是return()方法的参数。并且,Generator 函数的遍历就终止了,返回值的done属性为true,以后再调用next()方法,done属性总是返回true.如果return()方法调用时,不提供参数,则返回值的value属性为undefined。
function* test() {
yield 1;
try {
yield 'try';
} finally {
yield 'finally';
}
yield 3;
}
const iter = test();
iter.next(); // { value: 1, done: false }
iter.next(); // { value: 'try', done: false }
iter.return('end'); // { value: 'finally', done: false }
iter.next(); // { value: 'end', done: true }
如果 Generator 函数内部有try...finally代码块,且正在执行try代码块,那么return()方法会导致立刻进入finally代码块,执行完以后,整个函数才会结束。
yield* 表达式
如果在 Generator 函数内部,调用另一个 Generator 函数。需要在前者的函数体内部,自己手动完成遍历。
如下:
function* a() {
yield 1;
yield 2;
}
function* b() {
yield 'a';
for (let i of a()) {
console.log(i);
}
yield 'b';
}
for (let j of b()) {
console.log(j);
}
// a 1 2 b
yield*表达式,作为解决办法,用来在一个 Generator 函数里面执行另一个 Generator 函数。简化上述代码
function* a() {
yield 1;
yield 2;
}
function* b() {
yield 'a';
yield* a();
yield 'b';
}
for (let j of b()) {
console.log(j);
}
// a 1 2 b
可以通过yield*实现多层数组的扁平化处理,如下:
function* iterTree(tree) {
if (Array.isArray(tree)) {
for(let item of tree) {
yield* iterTree(item);
}
} else {
yield tree;
}
}
const arr = [1, 2, ['a', 'b'], 4, ['name', 'age']];
console.log([...iterTree(arr)]);
// [1, 2, 'a', 'b', 4, 'name', 'age'];
途牛:预计将于11月22日前后迎来第一波出游热潮
恒生科技指数跌幅扩大至3%
兴业银行新设金融资产投资公司,注册资本100亿元
VNBarcodeObservation的结果中observation.boundingBox 是什么类型?
大家好,我的开源项目PakePlus可以将网页/Vue/React项目打包为桌面/手机应用并且小于5M只需几分钟,官网地址:pakeplus.com
observation.boundingBox 的类型是 CGRect。
CGRect 结构
CGRect 是 Core Graphics 框架中的结构体,表示一个矩形区域:
public struct CGRect {
public var origin: CGPoint
public var size: CGSize
}
在 Vision 框架中的特性
在 Vision 框架中,boundingBox 使用归一化坐标系统:
let barcodeRequest = VNDetectBarcodesRequest { request, error in
guard let results = request.results as? [VNBarcodeObservation] else { return }
for observation in results {
let boundingBox: CGRect = observation.boundingBox
print("boundingBox: \(boundingBox)")
// 访问具体属性
print("原点: \(boundingBox.origin)") // CGPoint
print("尺寸: \(boundingBox.size)") // CGSize
print("x: \(boundingBox.origin.x)") // CGFloat
print("y: \(boundingBox.origin.y)") // CGFloat
print("宽度: \(boundingBox.size.width)") // CGFloat
print("高度: \(boundingBox.size.height)") // CGFloat
// 其他便捷属性
print("最小X: \(boundingBox.minX)")
print("最小Y: \(boundingBox.minY)")
print("最大X: \(boundingBox.maxX)")
print("最大Y: \(boundingBox.maxY)")
print("中心X: \(boundingBox.midX)")
print("中心Y: \(boundingBox.midY)")
}
}
归一化坐标系统
Vision 框架的 boundingBox 使用归一化坐标:
-
坐标范围: (0.0, 0.0) 到 (1.0, 1.0)
-
原点: 左下角 (与 UIKit 的左上角不同!)
-
x: 从左到右 (0.0 = 左边缘, 1.0 = 右边缘)
-
y: 从下到上 (0.0 = 下边缘, 1.0 = 上边缘)
// 示例 boundingBox 值
let exampleBox = CGRect(x: 0.2, y: 0.3, width: 0.4, height: 0.3)
// 表示:
// - 从图像左边 20% 的位置开始
// - 从图像底部 30% 的位置开始
// - 宽度为图像的 40%
// - 高度为图像的 30%
坐标转换
由于归一化坐标和 UIKit 坐标系统不同,需要进行转换:
1. 转换为图像像素坐标
func convertToImageCoordinates(boundingBox: CGRect, imageSize: CGSize) -> CGRect {
// Vision → 图像坐标 (左下角原点)
let imageRect = VNImageRectForNormalizedRect(
boundingBox,
Int(imageSize.width),
Int(imageSize.height)
)
return imageRect
}
2. 转换为 UIKit 视图坐标
func convertToViewCoordinates(boundingBox: CGRect, viewSize: CGSize) -> CGRect {
// Vision → UIKit 坐标 (左上角原点)
let viewRect = CGRect(
x: boundingBox.origin.x * viewSize.width,
y: (1 - boundingBox.origin.y - boundingBox.size.height) * viewSize.height,
width: boundingBox.size.width * viewSize.width,
height: boundingBox.size.height * viewSize.height
)
return viewRect
}
完整的使用示例
import Vision
import UIKit
class BarcodeDetector {
func processBarcodeObservation(_ observation: VNBarcodeObservation,
imageSize: CGSize,
targetView: UIView) {
let boundingBox: CGRect = observation.boundingBox
// 1. 打印原始 boundingBox
print("原始 boundingBox: \(boundingBox)")
// 2. 转换为图像坐标
let imageRect = VNImageRectForNormalizedRect(
boundingBox,
Int(imageSize.width),
Int(imageSize.height)
)
print("图像坐标: \(imageRect)")
// 3. 转换为视图坐标 (用于在屏幕上绘制)
let viewRect = convertToViewRect(boundingBox: boundingBox,
viewSize: targetView.bounds.size)
print("视图坐标: \(viewRect)")
// 4. 在界面上绘制边界框
drawBoundingBox(on: targetView, rect: viewRect)
}
private func convertToViewRect(boundingBox: CGRect, viewSize: CGSize) -> CGRect {
return CGRect(
x: boundingBox.origin.x * viewSize.width,
y: (1 - boundingBox.origin.y - boundingBox.size.height) * viewSize.height,
width: boundingBox.size.width * viewSize.width,
height: boundingBox.size.height * viewSize.height
)
}
private func drawBoundingBox(on view: UIView, rect: CGRect) {
// 移除之前的边界框
view.layer.sublayers?.removeAll(where: { $0.name == "boundingBox" })
// 创建新的边界框图层
let boxLayer = CAShapeLayer()
boxLayer.name = "boundingBox"
boxLayer.frame = rect
boxLayer.borderColor = UIColor.green.cgColor
boxLayer.borderWidth = 2.0
boxLayer.backgroundColor = UIColor.clear.cgColor
view.layer.addSublayer(boxLayer)
}
}
重要注意事项
-
坐标系统差异: Vision 使用左下角原点,UIKit 使用左上角原点
-
归一化范围: 坐标值在 0.0-1.0 范围内
-
空矩形检查: 检查 boundingBox 是否有效
-
边界处理: 确保转换后的坐标在有效范围内
// 检查 boundingBox 是否有效
if boundingBox.isNull || boundingBox.isInfinite {
print("无效的 boundingBox")
return
}
// 检查是否在有效范围内
if boundingBox.minX < 0 || boundingBox.maxX > 1 ||
boundingBox.minY < 0 || boundingBox.maxY > 1 {
print("boundingBox 超出有效范围")
}
总结:observation.boundingBox 是 CGRect 类型,使用归一化坐标系统表示检测对象在图像中的位置和大小,需要进行适当的坐标转换才能在 UIKit 界面中使用。
第四次中德高级别财金对话将在北京举行
上海印发做好中小学食堂供餐服务招投标工作的指导意见
我国科学家在神经疾病治疗领域取得新突破
函数柯里化(curry)是什么?🤔
什么是函数柯里化?
函数柯里化是一种将多参数函数转换为一系列单参数函数的技术。简单来说,柯里化后的函数不会立即求值,而是每次接受一个参数,并返回一个新函数来接收剩余参数,直到所有参数都被提供,最终返回结果。
基本示例
让我们通过一个简单的例子来理解柯里化:
// 普通函数
function add(a, b, c) {
return a + b + c;
}
// 柯里化版本
function curriedAdd(a) {
return function(b) {
return function(c) {
return a + b + c;
};
};
}
// 使用方式对比
console.log(add(1, 2, 3)); // 6
console.log(curriedAdd(1)(2)(3)); // 6
实现通用的柯里化函数
手动为每个函数编写柯里化版本显然不现实,我们可以创建一个通用的柯里化工具函数: 思路就是创建一个自动柯里化函数可以接收一个函数作为参数,然后返回一个它的柯里化后的函数。
//自动柯里化函数,接收一个函数的参数
const autoCurryFn = function(fn){
//边界判断
//是否是函数
if(typeof fn !== 'function'){
throw new Error('传进来的参数必须是一个函数')
}
//返回一个新的函数,接收新的参数,这里用gras剩余参数收集
return function curryFn(...args){
//如果收集的参数个数少于原fn函数的参数个数,则返回这个新函数继续收集
if(args.length < fn.length){
return function(...newGras){
return curryFn(...args,...newGrgs)
}
}else{
//如果收集的参数大于等于原函数的参数就可以执行原函数,并返回对应结果
return fn(...args)
}
}
}
柯里化的实际应用场景
1. 参数复用
柯里化非常适合创建可复用的函数模板:
// 创建特定前缀的日志函数
function createLogger(prefix) {
return function(message) {
console.log('[' + prefix + '] ' + message);
};
}
const infoLogger = createLogger('INFO');
const errorLogger = createLogger('ERROR');
infoLogger('系统启动成功'); // [INFO] 系统启动成功
errorLogger('数据库连接失败'); // [ERROR] 数据库连接失败
2. 延迟执行
柯里化允许我们分步提供参数,这在事件处理等场景中特别有用:
// 事件处理器工厂
function createEventHandler(eventType, element) {
return function(handler) {
element.addEventListener(eventType, handler);
};
}
// 为特定元素创建点击事件处理器
const createClickHandler = createEventHandler('click', document.getElementById('myButton'));
// 稍后添加具体的处理逻辑
createClickHandler(function(event) {
console.log('按钮被点击了!');
});
总结
函数柯里化其实就是将多参数函数转换为单参数函数序列,为我们提供了更灵活的函数组合方式和更高的代码复用性。
英仕曼集团计划在伦敦裁员,并将部分岗位转移到保加利亚
前端规范【五】biomejs自动化工具-ultracite
前言
感觉和autful/eslint-config 一样, biomejs也有了自己的自动化工具 不过有了新的东西 包括ai工具调用的钩子和 格式化的md 文档
安装
npx ultracite@latest init
然后根据提示 选择自己的配置
配置
如果你需要扩展biomejs 的配置 去继承 ultracite 提供的配置就行了
{
"extends": ["ultracite/core", "ultracite/react"]
}
集成
{
"scripts": {
"check": "npx ultracite check --diagnostic-level warn",
"fix": "npx ultracite@latest fix --unsafe"
}
}
vscode 插件 需要安装的插件
biomejs tailwindcss unocss
其他
执行命令可以帮你检查你的配置是否正确
npx ultracite doctor
支持
biomejs 也支持vue了 虽然是实验性的
哈哈 目前就等待oxlint + oxfmt 能一键配置了,不过还没支持vue
A股三大指数集体收跌,沪指跌0.97%失守4000点
央视曝光医保卡被“薅羊毛”,现存药店相关企业超112.3万家
近期央视曝光医保卡被“薅羊毛”,乱象丛生。生产企业将日用品备案为“一类医疗器械”,药店以此为噱头销售,消费者受诱导用医保卡购买非医疗用品。包括但不限于牙线、牙刷、洗脸巾、护肤品等日用品,将医保卡里的钱当零花钱用。
争议的焦点在于,药店和企业辩称销售合规,而公众质疑械字号审批宽松。同时,很多人误将医保个人账户视为“储蓄”,忽视其社会共济属性。
对此,监管已展开行动,对涉事方追责,并采取技术防控、制度堵漏等措施。公众也应提高警惕,防范盗刷,拒绝违规消费,认清医保本质,守护好生命最后的防线。
天眼查专业版数据显示,截至目前我国现存在业、存续状态的药店相关企业超112.3万家。其中,2025年截至目前新增注册相关企业约11.3万余家。从区域分布来看,广东省、山东省、四川省药店相关企业数量位居前列,分别拥有超12万余家、8.7万余家和7.6万余家。排在其后的是河南省和江苏省。
进博会“剧透”低空出行新图景,现存相关企业超9.5万余家
第八届进博会上,多家头部eVTOL研发商携未来交通蓝图亮相。从国家会展中心到浦东机场仅需10分钟、票价50元起,跨城飞行15分钟可达……低空出行正从概念走向现实。据预测,2025年我国低空经济规模将达1.5万亿元,eVTOL作为核心载体加速商业化。
展台现场,未来模拟起降点让观众体验“扫码打飞的”的智慧出行,市内航线50元起、跨城100元至300元的价格,近似未来实际运营状况。时的科技E20机型巡航速度达320公里/小时,上海虹桥至浦东机场仅需十几分钟,费用与出租车相当。
在未来,低空出行将以高效普惠的姿态,重塑城市交通格局。
天眼查专业版数据显示,截至目前我国现存在业、存续状态的低空经济相关企业超9.5万家。其中,2025年截至目前新增注册相关企业约2.2万余家,从企业注册数量趋势来看,近五年间,2021、2022两年低空经济相关企业的注册数量呈现出下降态势,但自2023年起,则呈现出逐年增长态势,并在2024年达到顶峰。
从区域分布来看,广东省、山东省、四川省低空经济相关企业数量位居前列,分别为超1.2万余家、7800余家和5000余家。排在其后的是江苏省和北京市。
React项目实战 | 修复Table可展开行,点击一个全部展开
问题分析
Table 组件需要唯一的 rowKey 来正确管理展开状态。
原因详解
1. 虚拟DOM diff 算法依赖
React 使用虚拟DOM diff算法来高效更新UI。当Table数据更新时,React需要:
- 识别哪些行是新增的、哪些是删除的、哪些是更新的
- 正确保持展开/收起状态
- 如果没有唯一key,React无法准确追踪每行的状态
2. 展开状态管理
Table组件的展开状态是基于 rowKey 来管理的:
// 内部类似这样的结构
expandedRowKeys = ['id1', 'id3', 'id5']
如果没有唯一id,点击展开时可能出现:
- 多个行同时展开(主包遇到的bug)
- 展开状态错乱
- 无法正确收起
3. 性能优化
唯一key帮助React:
- 减少不必要的重渲染
- 提高diff算法效率
- 准确更新特定行的展开状态
解决方案对比
方案一:使用 uuid
import { v4 as uuidv4 } from 'uuid';
const dataWithId = recordData.list.map(item => ({
...item,
id: uuidv4() // 全局唯一
}));
优点:绝对唯一
缺点:需要额外依赖包
方案二:时间戳+随机数
const dataWithId = recordData.list.map(item => ({
...item,
id: `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`
}));
优点:无依赖
缺点:极低概率重复(通常可接受)
方案三:使用数据中的唯一字段&字段拼接
// 如果数据本身有唯一字段
<Table rowKey="user_id" />
// 或者组合多个字段、data接口数据
const dataWithId = data.map(item => ({
...item,
id: `${item.user_id}-${item.timestamp}`
}));
最佳实践
// 推荐:优先使用数据中的业务ID
//
const dataWithId = recordData.list.map((item, index) => ({
...item,
id: item.id || item.user_id || `row-${index}-${Date.now()}`
}));
<Table
rowKey="id"
columns={columns}
expandable={{
expandedRowRender: record => (
<p style={{ margin: 0 }}>{record.change_content}</p>
),
rowExpandable: record => record.change_content && record.change_content !== ''
}}
dataSource={dataWithId}
pagination={false}
/>
正常表格数据都应有ID,没有去找后端要(狗头
那话又说回来了,真没招了就选择引入UUID或者时间戳+随机数的方法
总结
根本原因:Table组件依赖唯一的 rowKey 来正确管理每行的展开状态和进行高效的虚拟DOM diff。
修复核心:确保每行数据都有一个唯一标识,让React能够准确追踪和管理每行的状态。
全运会点燃健身热潮,现存健身相关企业超165.5万家
近日,第十五届全国运动会开幕式在广州精彩上演。开幕式分为体育仪式、文体展演和火炬传递及点火三部分,青铜与机器人“古今对话”等节目精彩纷呈,让现场观众备受震撼。
此次全运会不仅是一场体育盛宴,更激发了群众的健身热情,跟着全运会去健身成为新时尚,线上各种健身打卡群、运动挑战赛如雨后春笋般涌现,大家互相鼓励、监督,共同追求健康生活。商家也紧抓机遇,推出各类运动装备优惠活动,满足不断增长的健身需求。
天眼查专业版数据显示,截至目前我国现存在业、存续状态的健身相关企业超165.5万家。其中,2025年截至目前新增注册相关企业约23.2万余家。从区域分布来看,贵州省、广东省、山东省健身相关企业数量位居前列,分别拥有超17.1万余家、16.6万余家和14.4万余家。排在其后的是江苏省和浙江省。
从“中国制造”到“中国智造”:广交会与进博会背后的科技产业变革与跨界融合
2025年11月,从广州的广交会到上海的进博会,中国两大国际性展会相继举办,成为观察全球科技趋势与产业变革的重要窗口。
在这两场盛会中,智慧交通与低空经济、医药保健品与医疗器械、电子家电与无人驾驶等领域的创新成果密集亮相,不仅展现了科技应用的广度与深度,更揭示了未来产业融合的发展方向。
据数据显示,2025年我国低空经济规模有望达1.5万亿元,医疗器械市场预计突破1.5万亿元,智能家居市场出货量将达2.81亿台,这些数字背后是技术突破、政策支持与市场需求共同作用的结果。
可以看到,从传统制造到智能制造,从单一产品到全场景解决方案,科技正以前所未有的力度重塑消费生态与产业格局。
智慧交通与低空经济重塑未来出行,从物流配送到城市通勤的立体化革命低空经济在2025年迎来爆发式增长,其应用场景从物流配送扩展至城市通勤、应急救援等多个领域。
进博会设立的“未来低空出行”专区集中展示了eVTOL(电动垂直起降飞行器)、低空起降点等基础设施,其中长三角地区的低空航线网络尤为密集,例如苏州吴江至上海浦东机场的航线将通行时间缩短至30分钟空中飞行加10分钟地面接驳,极大提升了区域联通效率。
与此同时,技术瓶颈持续突破,例如宁德时代研发的航空级电池通过严苛安全测试,光威复材的碳纤维材料实现国产化,为飞行器轻量化和长续航提供了基础支撑。
低空物流已成为智慧交通体系的重要组成部分,其效率提升与成本优化效果显著。2025年1-8月,深圳无人机载货飞行架次达54.8万,同比增长57%,累计开通低空物流航线309条,覆盖同城快件“2小时达”和跨城快件“3小时达”服务。
天眼查专业版数据显示,截至目前我国现存在业、存续状态的低空经济相关企业超9.5万家。其中,2025年截至目前新增注册相关企业约2.2万余家,从企业注册数量趋势来看,近五年间,2021、2022两年低空经济相关企业的注册数量呈现出下降态势,但自2023年起,则呈现出逐年增长态势,并在2024年达到顶峰。从区域分布来看,广东省、山东省、四川省低空经济相关企业数量位居前列,分别为超1.2万余家、7800余家和5000余家。排在其后的是江苏省和北京市。从长远来看,低空经济正从技术验证走向商业化运营,但其普及仍面临成本与安全的平衡挑战。例如,飞行汽车电池目前的成本比车用电池高出3倍,需规模化生产至1000架以上才有望降低50%;安全则是另一个核心关切,极端天气下的飞行稳定性、跨空域应急救援机制等尚未形成统一标准,需通过公开测试数据增强公众信任。
然而,行业的未来前景广阔,摩根士丹利预测2030年全球飞行汽车市场规模将达3000亿美元,而中国凭借全产业链优势与场景落地速度,有望成为全球最大的城市低空交通市场。
医药保健品与医疗器械创新加速,精准化与智能化引领产业升级医药保健品与医疗器械领域在2025年呈现出“精准化+智能化”的双轮驱动特征,进博会医疗展区汇聚了全球十大医疗器械企业,其展品从疾病治疗向健康管理前置延伸。
例如,美敦力公司展出的闭环可充电脊髓神经刺激器Inceptiv,能够实时感知脊髓生物信号变化并自动调整电流强度,为慢性疼痛患者提供个性化治疗方案。强生公司带来的Impella CP with SmartAssist介入式左心室辅助泵,作为全球唯一经美国FDA批准证实可提高心源性休克患者生存率的设备,已进入国家创新医疗器械特别审查程序。
在政策层面,2025年医保目录调整首次增设商业健康保险创新药目录,为创新药提供了基本医保之外的补充支付渠道,进一步激发了企业的研发热情。
值得一提的是,人工智能与大数据技术的深度融合,正重塑医疗器械的诊断与治疗模式。西门子医疗在进博会首次展出的脑机接口解决方案,覆盖了从术前规划到术后评估的全流程,其“双子星”超高梯度场磁共振与双源光子计数CT曾应用于马斯克旗下公司的临床试验,提升了脑部手术的精准性与安全性。
与此同时,家用医疗设备趋向智能化与互联化,欧姆龙健康医疗推出的慢病管理解决方案矩阵,包括智能血压计、持续葡萄糖监测系统等,可通过数据积累与算法分析实现早期风险筛查和日常健康干预,推动健康管理从“被动就医”向“主动预防”转变。
天眼查专业版数据显示,截至目前我国现存在业、存续状态的医疗器械相关企业超619.5万家。其中,2025年截至目前新增注册相关企业约113.8万余家,从企业注册数量趋势来看,近五年间,医疗器械相关企业的注册数量呈现出逐年增长的态势,并在2024年达到顶峰。从区域分布来看,广东省、山东省、江苏省医疗器械相关企业数量位居前列,分别拥有超70.1万余家、53.4万余家和43.5万余家。排在其后的是河南省和北京市。
未来,随着人口老龄化加剧和健康需求多元化,医药保健品产业将更注重跨界融合,例如海尔推出“Haier care”智慧康养品牌,整合家电技术与健康监测功能,构建“设备+服务+数据”的全场景解决方案。这一趋势表明,医疗创新已从单一产品竞争转向生态协同,技术、政策与市场正共同推动产业向高质量方向发展。
电子家电与无人驾驶跨界融合,智能技术重构生活与出行场景电子家电行业在2025年面临内需饱和与同质化竞争的压力,跨界突围成为企业寻求增长的核心策略。诸多大型家电品牌凭借技术积累与资本优势,向智能驾驶、机器人、低空经济等领域扩展。
此外,不少家电企业纷纷加码机器人赛道,特别是服务型的机器人更加受到资本和消费者的青睐。这些跨界尝试的核心逻辑在于技术迁移,例如智能驾驶所需的感知决策能力与家电的语音识别、图像处理算法同源,而机器人的运动控制与家电电机技术高度关联。天眼查专业版数据显示,截至目前我国现存在业、存续状态的家电相关企业超2198.7万家。从企业注册数量趋势来看,近五年间,家电相关企业的注册数量呈现出逐年增长的态势,并在2024年达到顶峰。从区域分布来看,广东省、湖北省、海南省家电相关企业数量位居前列,分别为超255.9万余家、172.4万余家和149.3万余家。
不仅如此,如今无人驾驶技术虽未完全普及,但其与智能家居的互联已初步形成场景化解决方案。2025年中国家电及消费电子博览会上,海尔展出的智能冰箱深度融合AI保鲜大模型,可基于食材库存生成健康食谱,并通过与家庭服务机器人的联动向“无人家务”场景演进。
这些创新反映出一个趋势:家电正从单一功能产品升级为智能生态的入口,而其与无人驾驶的结合可能重塑未来生活范式,例如智能汽车与家居系统的数据互通,可实现出行途中远程控制家庭环境,或车辆根据实时路况调节家居能耗。
然而,跨界融合也伴随挑战,包括技术研发的高投入、行业壁垒以及管理复杂度。但长远来看,跨界已成为家电行业突破增长瓶颈的必然选择,其成功依赖于精准的战略定位与能力匹配。未来竞争将不再是单一产品比拼,而是生态与场景的整合,那些能够通过技术反哺主业、并在新老业务间找到平衡点的企业,有望在智慧生活浪潮中赢得先机。
作为国内领先的商业查询平台,天眼查依托海量商业大数据,结合天眼风险、工商信息、任职信息、股东信息、股权全景穿透图、最终受益人、主要人员等多维度数据,实现从风险洞察到风险预警的全面管控,能快速了解并分析当下消费、科技、人工智能等相关产业的发展现况与未来趋势。
天眼查研究院认为,从广交会到进博会,11月的科技展示浪潮不仅体现了中国产业的升级迭代,更揭示了全球技术融合的深度趋势。未来,随着低空航线的加密、AI医疗的普及以及智能家居与车联网的深度融合,科技创新将更深刻地嵌入日常生活,成为经济增长与社会进步的核心引擎。而中国凭借庞大的市场、完善的产业链与积极的政策引导,有望在这一过程中扮演关键角色。
“您的快递正在路上吵架”上热搜,现存自动驾驶相关企业超8200家
近日,网络热梗“您的快递正在路上吵架”有了新剧情:一辆无人驾驶快递车不断发出“请让一让,你是不是没有驾照,退退退退”的提示,而它的对面,是因铺路施工无法避让的工人。快递车却执着要求对方让路,双方陷入“吵架”僵局,场景十分滑稽。
这看似搞笑的一幕,实则暴露出快递车技术的严重短板。在面对施工等突发路况时,快递车缺乏灵活应变能力,只能机械重复提示,无法自主规划绕行路线,导致配送受阻。
天眼查专业版数据显示,截至目前我国现存在业、存续状态的自动驾驶相关企业超8200家。从区域分布来看,广东省、河北省、江苏省、山东省、北京市自动驾驶相关企业数量位居前列,分别拥有超1400余家、780余家、510余家、450余家和400余家。