当前位置: 首页 > news >正文

Js 中 迭代器 与 生成器

上篇讲了关于手写 Promise , 从类的设计, 构造函数设计, then 方法实现, catch 方法借助 then 方法实现, then 方法的核心优化, 包括多次调用, 链式调用等, 最后再补充了一些常用的类方法, 如 all, allSettled, any 等实现, 整体过程还是比较有难度的, 体现在状态的变更, 代码执行顺序的控制等, 也算是 js 一个重要知识点吧.

本篇则开始学习 js 中的迭代器和生成器, 只有先搞明白这俩兄弟, 才会对后续将 async ... await + Promise 有深刻认知.

认识迭代器

迭代器 (iterator) 是使用户能在容器对象上, 安全, 统一地访问容器元素的方法机制协议, 而无需关系容器内部实现细节

或者说迭代器是一种对象, 它提供了一种按顺序访问集合 (数组, 列表, 链表, 集合, 映射等) 中的每个元素的方法, 而无需让开发者了解其底层实现 (抽象接口). 它通常实现一个 next() 方法, 犯病一个包含两个属性的对象:

{value: 当前值, done: 是否遍历完成 }

它本质上是一种设计模式, 在语言中通过接口和协议规范, 在运行时表现为一个具有状态和行为的对象. 可以将它类比为一个地铁的闸机系统:

  • 设计模式: "一人一票, 顺序通过"
  • 编程规范: 所有地铁站必须刷卡
  • 接口定义: 必须实现 validate(cart) 和 open() 方法
  • 也是对象: 每个地铁站都有一台闸机
  • 数据结构: 不存数据 (乘客), 只控制访问顺序

迭代器的好处或者说应用场景在于:

  • **统一接口: **不论遍历字符串, 数组, Map, 自定义结构等, 都可以用 for...of.next() 来遍历
  • **延迟计算: **迭代器可以"按需" 生成值, 而非一次性生成所有数据 (生成器)
  • 内存提效: 适用于处理大数据流或者无限序列 (斐波那契数列等)
  • 解耦结构: 数据结构和遍历元素拆分, 让遍历元素部分由迭代器实现

js 中的迭代器

**在 js 中, 迭代器也是一个具体的对象, 这个对象需要符合迭代器协议: **

  • 迭代器协议, 定义了产生一系列值的标准方式
  • 在 js 中标准就是一个特定的 next() 方法

这个 next() 方法需要满足如下的要求:

  • 是一个无参或者仅一个参数的函数, 返回一个应当拥有如下两个属性的对象
  • done (boolean)
  • value
// 迭代器 认识, 也是一个对象const names = ['youge', 'cj', 'yaya']// 创建一个迭代器对象, 来访问 names 数组
let index = 0
const namesIterator = {next: function() {if (index < names.length) {return { done: false, value: names[index++] }} else {return { done: true, value: undefined }}}
}// 通过 迭代器的 next() 方法不断去访问数组元素
console.log(namesIterator.next())
console.log(namesIterator.next())
console.log(namesIterator.next())
console.log(namesIterator.next())
console.log(namesIterator.next())// { done: false, value: 'youge' }
// { done: false, value: 'cj' }
// { done: false, value: 'yaya' }
// { done: true, value: undefined }
// { done: true, value: undefined }

继续来封装一个生成迭代器的函数.

// 生成迭代器的函数, 返回一个对象, 包含 next()// 数组迭代器
function createArrayIterator(arr) {let index = 0return {next: function() {if (index < arr.length) {return { done: false, value: arr[index++]}} else {return { done: true, value: undefined }}}}
}const names = ['youge', 'cj', 'yaya']
const nums = [10, 20, 30]const namesIterator = createArrayIterator(names)console.log(namesIterator.next())
console.log(namesIterator.next())
console.log(namesIterator.next())
console.log(namesIterator.next())// { done: false, value: 'youge' }
// { done: false, value: 'cj' }
// { done: false, value: 'yaya' }
// { done: true, value: undefined }
// { done: true, value: undefined }// nums数组 的迭代器
const numsIterator = createArrayIterator(nums)console.log(numsIterator.next())
console.log(numsIterator.next())
console.log(numsIterator.next()) // { done: false, value: 30 }
console.log(numsIterator.next()) // { done: true, value: undefined }// 创建一个无限的迭代器
function createNumIterator() {let index = 0return {next: function() {return { done: false, value: index++ }}}
}const numIterator = createNumIterator()console.log(numIterator.next())  // 0
console.log(numIterator.next())  // 1
console.log(numIterator.next())  // 2
console.log(numIterator.next())  // 3
console.log(numIterator.next())  // 4

可迭代对象

上面认识迭代器的时候, 这个代码分为了 3个部分:

  • 创建将要被迭代的数组 (全局)
  • 创建一个对象, 里面实现了一个叫 next() 的方法来获取数组数据 (全局)
  • 创建一个全局变量 index 来控制 对象 里面的 next 方法, 来访问数组对象

现在要来思考的是, 如何将上面这有紧密联系但是有互相分开的3部分组合到一起, 即统一放在同一个对象中.

// 组合到一起const newObj {names;index;obj.next();
}

这个能组合到一起的对象, 就称为 可迭代对象

  • 该对象实现了 iterable protocol 可迭代协议
  • 该协议必须实现 @@iterator方法, 对应在代码中实现 [Symbol.iterator] 方法即可
const iterableObje= { [Symbol.iterator]: function() { return 迭代器 } }

迭代器

是一个对象, 它实现了 迭代器协议, 表现为该对象有一个 .next() 方法, 并返回 { value, done } 的结构

可迭代对象

也是一个对象, 它实现了 可迭代协议, 表现为该对象有一个 Symbol.ierator 方法, 该方法返回一个 实现了迭代器协议的对象

// 可迭代对象 与 迭代器const arr = [1, 2, 3]
console.log(arr[Symbol.iterator]) // 是个函数, arr 是可迭代对象// 数组也是对象, 内置实现了可迭代协议, 可以调用 Symbol.iterator
// 该方法返回一个迭代器, 可以用它来进行 .next() 获取元素
const iter = arr[Symbol.iterator]()console.log(iter.next())
console.log(iter.next())
console.log(iter.next())
console.log(iter.next())// { value: 1, done: false }
// { value: 2, done: false }
// { value: 3, done: false }
// { value: undefined, done: true }
对比项 可迭代对象(Iterable) 迭代器(Iterator)
本质 对象 对象
核心特征 [Symbol.iterator]() 方法 .next() 方法
协议 实现 可迭代协议 实现 迭代器协议
作用 能被 for...of...yield* 等消费 负责实际遍历,返回 {value, done}
是否可遍历 ✅ 可以被遍历 ✅ 可以被遍历(因为迭代器自己也是可迭代的)
典型例子 Array, String, Map, Set, arguments array[Symbol.iterator]() 的返回值

厘清概念之后, 现对之前的数组, 迭代器, 迭代获取数组元素这个过程进行封装, 即将 nams 数组变为可迭代对象.

// 可迭代对象const iterableObj = {names: ['youge', 'cj', 'yaya'],// Symbol.iterator 用 [] 包起来是让其作为字面量对象的 "计算属性"[Symbol.iterator]: function() {let index = 0return {next: () => { // 箭头函数无绑定this, 因此 next() 中的 this 会找到父级的 namesif (index < this.names.length) {return { done: false, value: this.names[index++]}} else {return { done: true, value: undefined}}}}}
}// 此时 iterableObj 对象是一个 可迭代对象
console.log(iterableObj[Symbol.iterator]) // 函数// 第一次调用
const iterator = iterableObj[Symbol.iterator]()
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())// 第二次调用
const iterator2 = iterableObj[Symbol.iterator]()
console.log(iterator2.next())// for ... of 可以遍历, 可迭代对象
for (const item of iterableObj) {console.log(item) // youge, cj, yaya
}

注意点:

  • Symbol.iterator 用 "[ ]" 包起来是让其作为字面量对象的 "计算属性"
  • 利用箭头函数无绑定 this, 因此 next() 中的 this 会找到父级的 names
  • for ... of 可以遍历, 可迭代对象

可见在 js 中的这个 this 的理解是非常重要的, 这里就用到了隐式调用原则嘛.

然后也解决了困扰我很久的问题: js 普通对象为啥不能用 for..of 遍历? 因为普通对象没有实现可迭代协议呀.

// 普通对象不可用 for...of 迭代 const obj = {name: 'youge',age: 18
}for (const item of obj) {console.log(item)
}// TypeError: obj is not iterable

可以理解这个 for...of 也是一个语法糖, 它做的事情就是用迭代器不断调用 .next() 获取元素, 直到 done:true 的时候, 则停止.

内置迭代器对象

像我们常用的 String, Array, Map, Set, arguments, NodeList 等原生对象, 都已实现了可迭代协议, 通过它们所产生的对象, 都是一个可迭代对象.

// 内置了`代器的对象// 字符串 
const str = "ab c"
for (const s of str) {console.log(s) // a, b, " "
}// 数组 
const arr = [1, 2]const arrIter = arr[Symbol.iterator]()
console.log(arrIter.next()) // { value: 1, done: false }
console.log(arrIter.next()) // { value: 2, done: false }
console.log(arrIter.next()) // { value: undefined, done: true}// set / map 
const set = new Set()
set.add(10)
set.add(20)console.log(set[Symbol.iterator]) // [Function: values]for (const item of set) {console.log(item) // 10, 20
}// map 
const map = new Map()
map.set('name', 'youge')
map.set(() => {}, 'cjj')for (const item of map) {console.log(item)// [ 'name', 'youge' ]// [ [Function (anonymous)], 'cjj' ]
}// 函数中的 arguments 
function foo() {for (const arg of arguments) {console.log(arg) // 1, 2, 3}
}foo(1, 2, 3)

这些内置对象, 都是可迭代对象, 原因就是它们都实现了 Symbol.iterable 方法, 它会返回迭代器, 内部实现元素的遍历啦.

可迭代对象的应用场景

  • js 语法提升: for ...of; 展开表达式; yield; 解构赋值等
  • 创建对象时: new Mapt([iterable]); new Set([iterable]), new WeakMap([iterable]) 等
  • 方法调用时: Promise.all(iterable); Promise.all(iterable); Array.from(iterable) 等
// 可迭代对象应用// 1. for...of 语法支持
const arr = [1, 2, 3]
for (const item of arr) {console.log(item) // 1, 2, 3
}// 2. 展开运算符
const iterableObj = {names: ['youge', 'cj', 'yaya'],[Symbol.iterator]: function() {let index = 0return {next: () => {if (index < this.names.length) {return { done: false, value: this.names[index++] }} else {return { done: true, value: undefined }}}}}
}const names = ['bob', 'jack']
const newNames = [...names, ...iterableObj]// [ 'bob', 'jack', 'youge', 'cj', 'yaya' ]
console.log(newNames)// ES9 新增普通对象特性, 虽不可迭代, 但要求支持展开运算符
// 猜测实现原理 for (const entry obj.entries ) {} 组装 key, value 就搞定了
const obj = { name: 'youge', age: 18 }
console.log({...obj}) // { name: 'youge', age: 18 }// 3. 解构语法
const [name1, name2] = names 
console.log(name1, name2) // bob, jackconst { name, age } = obj 
console.log(name, age) // youge, 18// 4. 创建其他对象时
const set1 = new Set(iterableObj)
const set2 = new Set(arr)// 可迭代对象, 转为数组, 如 arguments
const arr1 = Array.from(iterableObj)// 5. Promise.all(), 传普通值会自动 Promise.resolve(普通值)
Promise.all(iterableObj).then(res => {console.log("res: ", res) // res:  [ 'youge', 'cj', 'yaya' ]
})

自定义类的可迭代实现

上述通过 Array, Set, Map, String 等类, 创建出来的对象都是可迭代对象.

现在要来实现一个自己的类, 使其创建出来的对象也是可迭代的, 即在类中实现 [Symbol.iterator] 方法即可

用一个案例: 创建一个 Classroom 的类:

  • 每个教室有自己的编号, 楼层, 当前教室的学生
  • 教室可以新进来学生 (push)
  • 创建的教室对象是可迭代对象
// 自定义类 - 实现可迭代功能class Classroom {constructor(id, floor, students) {this.id = id this.floor = floor this.students = students }entry(newStudent) {this.students.push(newStudent)}
}const classroom = new Classroom("301", "3楼", ['bob', 'jack'])classroom.entry('cj')// 需求: 将教室对象都迭代出来
for (const item of classroom) {console.log(item)
}

当前是不能使用 for...of 语法的, 因为我们这个自定义的类, 没有实现可迭代协议. 则我们在对象方法上去实现这个 Symbol.iterator 的方法即可.

// 自定义类 - 实现可迭代功能class Classroom {constructor(id, floor, students) {this.id = id this.floor = floor this.students = students }entry(newStudent) {this.students.push(newStudent)}// 让对象可迭代, 添加 [Symbol.iterator] 方法[Symbol.iterator]() {let index = 0return {next: () =>  {if (index < this.students.length) {return { done: false, value: this.students[index++] }} else {return { done: true, value: undefined }}}}}
}const classroom = new Classroom("301", "3楼", ['bob', 'jack'])classroom.entry('cj')// 需求: 将教室内学生都迭代出来
for (const item of classroom) {console.log(item) // bob, jack, cj 
}

生成器

生成器 (Generator) 是一种特殊的函数, 它可以在函数执行中暂停, 之后再从暂停的地方恢复执行.

它返回一个生成器对象, 该对象既是迭代器, 也是可迭代对象

它提供了一种暂停和恢复函数执行的能力, 是实现惰性求值, 异步编程, 迭代器 等高级功能的核心工具.

// 普通函数的执行, 不能暂停function foo() {console.log(100)console.log(200)console.log(300)
}foo() // 需求: 在 打印 200 时先暂停, 晚点再往后执行

普通函数不能控制 "中途暂停", 此功能在当前知识体系下, 是不能实现的.

生成器函数

生成器函数也是一个函数, 但和普通函数有区别:

  • 需要在 function 的后面加一个符号 *
  • 可以通过 yield 关键字来控制函数执行流程
  • 返回值是一个 生成器对象, 它是一种特殊的迭代器
// 生成器函数function* foo() {console.log('函数开始执行...')console.log('第一段代码: ', 100)yieldconsole.log("第二代代码: ", 200)yieldconsole.log("第三段代码: ", 300)yield console.log('函数执行结束')
}// 调用 生成器函数时, 会返回一个生成器对象, 特殊迭代器
const generator = foo() 
// 执行第一段代码, 第一个 yield
generator.next() 
// 执行第二段代码
generator.next() 
// 执行第三段代码
generator.next() 
generator.next() // 函数执行结束

即通过 yield 来控制函数的执行顺序, 然后调用生成器函数, 返回的是一个生成器对象, 它是一个特殊的迭代器, 因此可以用它来调用 .next() 方法, 对应于 yield 的分段代码.

// 生成器函数-执行流程 // 当遇到 yield  时, 暂停函数执行
// 当遇到 return 时, 生成器停止执行   function* foo() {console.log('函数开始执行...')console.log('第一段代码: ', 100)yield 111console.log("第二代代码: ", 200)yield console.log("第三段代码: ", 300)yield 333console.log('函数执行结束')return 'over'
}// 调用 生成器函数时, 会返回一个生成器对象, 特殊迭代器
const generator = foo() // 生成器对象 的 next() 也有返回值
console.log(generator.next())
console.log(generator.next() )
console.log(generator.next() )console.log(generator.next() )// 函数执行结束// 函数开始执行...
// 第一段代码:  100
// { value: 111, done: false }// 第二代代码:  200
// { value: undefined, done: false }// 第三段代码:  300
// { value: 333, done: false }// 函数执行结束
// { value: 'over', done: true }

通过 yield 可以有返回值, 对应到 { value, done} 中. 最后可以进行 return, 值作为最后的 value, done 的值变为 true.

生成器传递参数- next()

在调用 next() 函数时, 传递参数, 则此参数会作为上一个 yield 语句的返回值, 然后在下一个yield 间进行运算

也就是说, 我们是为本次的代码块提供了一个参数值进来.

const generator = foo(5) // 生成器对象 的 next() 也有返回值
// next() 也可以传递参数 
console.log(generator.next())// 给第二个 next() 传参
console.log(generator.next(10) )

给第二段代码传参, 则先需要在 第一个段代码的 yield 的地方进行定义和接收, 然后在第二个 yield 后边处理

function* foo() {console.log('第一段代码: ', 100)const n = yield 100 * num  // 这里接收console.log("第二代代码: ", 200)const count = yield 200 * n // 这里调用
}

同样的做法, 可以给第一个 (很少见) 或者第三个都进行这样的处理:

// 生成器-next() 传递参数function* foo(num) {console.log('函数开始执行...')console.log('第一段代码: ', 100)const n = yield 100 * numconsole.log("第二代代码: ", 200)const count = yield 200 * n console.log("第三段代码: ", 300)yield 300 * count console.log('函数执行结束')return 'over'
}const generator = foo(5) // 生成器对象 的 next() 也有返回值
// next() 也可以传递参数 
console.log(generator.next())
console.log(generator.next(10) )
console.log(generator.next(20) )console.log(generator.next() )
函数开始执行...
第一段代码:  100
{ value: 500, done: false }第二代代码:  200
{ value: 2000, done: false }第三段代码:  300
{ value: 6000, done: false }函数执行结束
{ value: 'over', done: true }

生成器传递参数- return()

还有一种传参方式是通过 return() 函数, 它会直接终止生成器, 在当前代码块执行之前.

但这样的话就表示生成器函数的结束, 之后调用 next 则不会再继续生成值了.

// 生成器函数-return 函数function* foo(num) {console.log('函数开始执行...')console.log('第一段代码: ', 100)const n = yield 100 * numconsole.log("第二代代码: ", 200)const count = yield 200 * n console.log("第三段代码: ", 300)yield 300 * count console.log('函数执行结束')return 'over'
}const generator = foo(5) console.log(generator.next())
// 第二段代码, 执行 return 
console.log(generator.return(222))console.log(generator.next(20) )
console.log(generator.next())
函数开始执行...
第一段代码:  100
{ value: 500, done: false }
{ value: 222, done: true }{ value: undefined, done: true }
{ value: undefined, done: true }

则在第二段的时候, 调用 "generator.return(222)" 相当于是在第一段代码执行结束后进行 return

function* foo(num) {console.log('函数开始执行...')console.log('第一段代码: ', 100)const n = yield 100 * num// 相当于在这里终止了生成器return 222// 因为 return 了, 则后面的不会再进行 yield 新值了console.log("第二代代码: ", 200)const count = yield 200 * n // ....}

生成器抛出异常 - throw()

除了可以给生成器函数内部传参外, 也可以给生成器函数内部抛出异常.

  • 抛异常后, 可以在生成器函数中捕获异常
  • 但在 catch 语句中则不能再 yield 新值了, 但在 catch 语句外用 yield 中断语句执行可以
// 生成器函数-throw() 捕捉异常function* foo() {console.log('函数开始执行...')console.log('第一段代码: ', 100)try {yield 100 } catch (err) {console.log('捕捉到异常: ', err)}console.log("第二代代码: ", 200)const count = yield 200 console.log("第三段代码: ", 300)yield 300 * count console.log('函数执行结束')return 'over'
}const generator = foo() console.log(generator.next())
// 第二段代码, 执行 throw, 则会在第一段代码执行完后就异常了
console.log(generator.throw('err'))// 处理完异常后, 后续逻辑则正常运行
console.log(generator.next(20))
console.log(generator.next())
函数开始执行...第一段代码:  100
{ value: 100, done: false }
捕捉到异常:  err第二代代码:  200
{ value: 200, done: false }第三段代码:  300
{ value: 6000, done: false }函数执行结束
{ value: 'over', done: true }

生成器 替代 迭代器使用

生成器是一种特殊的迭代器, 则在一些情况下可以是使用生成器来代替迭代器的.

// 生成器 代替 迭代器// 迭代器
function arrayIterator(arr) {let index = 0return {next: function() {if (index < arr.length) {return {done: false, value: arr[index++]}} else {return {done: true, value: undefined}}}}
}const names = ['youge', 'cj', 'yaya']
const namesIterator = arrayIterator(names)console.log(namesIterator.next())
console.log(namesIterator.next())
console.log(namesIterator.next())// 也可以用生成器替代function* arrayGenerator(arr) {for (const item of arr) {yield item  // {value: xxx, done: false}}}const namesGenerator = arrayGenerator(names)console.log(namesGenerator.next())console.log(namesGenerator.next())

我们还可以使用 yield* 来生成一个可迭代对象.

这时候相当于是一个 yield语法糖, 会以此迭代这个可迭代对象, 每次迭代其中的一个值.

// 生成器 代替 迭代器 yield* 语法糖function* arrayGenerator(arr) {// yield* 可迭代对象yield*  arr 
}const names = ['youge', 'cj', 'yaya']
const namesGenerator = arrayGenerator(names)console.log(namesGenerator.next())console.log(namesGenerator.next())console.log(namesGenerator.next())console.log(namesGenerator.next())
{ value: 'youge', done: false }
{ value: 'cj', done: false }
{ value: 'yaya', done: false }
{ value: undefined, done: true }

来个小案例, 创建一个函数, 这个函数可以迭代一个范围内的数字

// 用生成器代替迭代器function* createRangeIterator(start, end) {let index = start while (index < end) {yield index++ }
}const rangeIterator = createRangeIterator(1, 3)
console.log(rangeIterator.next())
console.log(rangeIterator.next())
console.log(rangeIterator.next())// { value: 1, done: false }
// { value: 2, done: false }
// { value: undefined, done: true }
// { value: undefined, done: true }

同样的, 对于之前自定义类的迭代器, 也是可以改写为生成器的.

 // 自定义类 - 实现可迭代功能class Classroom {constructor(id, floor, students) {this.id = id this.floor = floor this.students = students }entry(newStudent) {this.students.push(newStudent)}// 让对象可迭代, 添加 [Symbol.iterator] 方法[Symbol.iterator]() {let index = 0return {next: () =>  {if (index < this.students.length) {return { done: false, value: this.students[index++] }} else {return { done: true, value: undefined }}}}}
}

现在可以改成这样的:

 // 自定义类 - 迭代逻辑, 用生成器替换class Classroom {constructor(id, floor, students) {this.id = id this.floor = floor this.students = students }entry(newStudent) {this.students.push(newStudent)}// 用生成器来实现 [Symbol.iterator] 方法*[Symbol.iterator]() {yield* this.students  // 妙呀!}
}
// 测试一下
const classroom = new Classroom("301", "3楼", ['bob', 'jack'])
classroom.entry('cj')// 需求: 将教室对象都迭代出来
for (const item of classroom) {console.log(item) 
}
bob
jack
cj
cj@m

在实际应用中, 能用生成器解决问题, 尽量用它, 就不用自己写迭代器的一通逻辑, 直接用这种大道至简的语法糖:

yield* 可迭代对象  // 1行顶10行

至此, 关于迭代器和生成器的基本使用部分就到这了. 整体操作难度不大, 但是概念特别多, 比如说迭代器, 迭代器对象, 可迭代对象, 生成器, 生成器函数, 生成器是特殊迭代器, 生成器可以在某些情况下替代迭代器, for ...of 的语法糖, yield* 可迭代对象等. 就还是细节很多, 理解为主, 能用就行.

http://www.sczhlp.com/news/56679/

相关文章:

  • MYSQL数据加密压缩函数应用实战指南
  • Rust 解析验证码:结合 Tesseract OCR 进行文本识别
  • 海外网站测速平台直播
  • 网站做视频好不好世界500强企业查询入口
  • 58同城建网站怎么做杭州市建设信用网站
  • 潍坊市企业网站建设做二手车那个网站会员性价比高
  • 中文字体设计网站网络平台怎么弄
  • 做国外网站收款怎么收网站数据库特点
  • 互动广告机网站建设昆明建个网站哪家便宜
  • 如何自己免费制作网站成都公司注册多少钱
  • 智能语音助手——开发物料全记录(ESP32_S3开发板+语音唤醒+语音命令+MQT+华为云)
  • js模板引擎
  • 在Ubuntu系统上安装和配置JMeter和Ant进行性能测试
  • 用 Julia 实现验证码去除干扰线的图像识别逻辑
  • 3DGS中的正向传播与反向传播
  • 石嘴山网站建设公司网页设计网站怎么放到域名里
  • 宜城网站开发北京做网站建设公司排名
  • 网站开发主要技术搞软件开发的一般学什么专业
  • 商洛做网站的公司网站建设的步骤图片过程
  • 公司做网站的费用怎么记账get_category wordpress
  • 专业做学校网站的公司安徽省建设造价管理协会网站
  • j2ee网站开发买什么书管理咨询顾问是做什么的
  • 网站风格类型为什么企业需要建设网站?
  • 合肥网站设计网站北京大兴网站建设公司咨询
  • 聊城公司网站设计网站推广宣传语
  • 大连企业建站wordpress如何把菜单
  • 2025年HR系统发展趋势:智能化、一体化与移动化
  • 好的外国设计网站推荐建app需要多少钱
  • 微信看视频打赏网站建设asp.net个人网站怎么做
  • 网站建设预估费用职业资格证培训机构加盟