172  
查询码:00000433
javascript 数组知识点(培训)
作者: 何书亮 于 2020年06月04日 发布在分类 / FM组 / FM_App 下,并于 2020年06月04日 编辑
javascript 数组

数组

JavaScript的 Array 可以包含任意数据类型,并通过索引来访问每个元素。

要取得 Array 的长度,直接访问 length 属性:
var arr = [ 1 , 2 , 3.14 , 'Hello' , null, true];
arr.length; // 6
请注意 ,直接给 Array length 赋一个新的值会导致 Array 大小的变化:
var arr = [ 1 , 2 , 3 ];
arr.length; // 3
arr.length = 6 ;
arr; // arr变为[1, 2, 3, undefined, undefined, undefined]
arr.length = 2 ;
arr; // arr变为[1, 2]
Array 可以通过索引把对应的元素修改为新的值,因此,对 Array 的索引进行赋值会直接修改这个 Array
var arr = [ 'A' , 'B' , 'C' ];
arr[ 1 ] = 99 ;
arr; // arr现在变为['A', 99, 'C']
请注意 ,如果通过索引赋值时,索引超过了范围,同样会引起 Array 大小的变化:
var arr = [ 1 , 2 , 3 ];
arr[ 5 ] = 'x' ;
arr; // arr变为[1, 2, 3, undefined, undefined, 'x']
大多数其他编程语言不允许直接改变数组的大小,越界访问索引会报错。然而,JavaScript的 Array 却不会有任何错误。在编写代码时,不建议直接修改 Array 的大小,访问索引时要确保索引不会越界。

indexOf

与String类似, Array 也可以通过 indexOf() 来搜索一个指定的元素的位置:
var arr = [ 10 , 20 , '30' , 'xyz' ];
arr.indexOf( 10 ); // 元素10的索引为0
arr.indexOf( 20 ); // 元素20的索引为1
arr.indexOf( 30 ); // 元素30没有找到,返回-1
arr.indexOf( '30' ); // 元素'30'的索引为2
注意了,数字 30 和字符串 '30' 是不同的元素。

slice

slice() 就是对应String的 substring() 版本,它截取 Array 的部分元素,然后返回一个新的 Array
var arr = [ 'A' , 'B' , 'C' , 'D' , 'E' , 'F' , 'G' ];
arr.slice( 0 , 3 ); // 从索引0开始,到索引3结束,但不包括索引3: ['A', 'B', 'C']
arr.slice( 3 ); // 从索引3开始到结束: ['D', 'E', 'F', 'G']
注意到 slice() 的起止参数包括开始索引,不包括结束索引。
如果不给 slice() 传递任何参数,它就会从头到尾截取所有元素。利用这一点,我们可以很容易地复制一个 Array
var arr = [ 'A' , 'B' , 'C' , 'D' , 'E' , 'F' , 'G' ];
var aCopy = arr.slice();
aCopy; // ['A', 'B', 'C', 'D', 'E', 'F', 'G']
aCopy === arr; // false

push和pop

push() Array 的末尾添加若干元素, pop() 则把 Array 的最后一个元素删除掉:
var arr = [ 1 , 2 ];
arr.push( 'A' , 'B' ); // 返回Array新的长度: 4
arr; // [1, 2, 'A', 'B']
arr.pop(); // pop()返回'B'
arr; // [1, 2, 'A']
arr.pop(); arr.pop(); arr.pop(); // 连续pop 3次
arr; // []
arr.pop(); // 空数组继续pop不会报错,而是返回undefined
arr; // []

unshift和shift

如果要往 Array 的头部添加若干元素,使用 unshift() 方法, shift() 方法则把 Array 的第一个元素删掉:
var arr = [ 1 , 2 ];
arr.unshift( 'A' , 'B' ); // 返回Array新的长度: 4
arr; // ['A', 'B', 1, 2]
arr.shift(); // 'A'
arr; // ['B', 1, 2]
arr.shift(); arr.shift(); arr.shift(); // 连续shift 3次
arr; // []
arr.shift(); // 空数组继续shift不会报错,而是返回undefined
arr; // []

sort

sort() 可以对当前 Array 进行排序,它会直接修改当前 Array 的元素位置,直接调用时,按照默认顺序排序:
var arr = [ 'B' , 'C' , 'A' ];
arr.sort();
arr; // ['A', 'B', 'C']
能否按照我们自己指定的顺序排序呢?完全可以,我们将在后面的函数中讲到。

reverse

reverse() 把整个 Array 的元素给掉个个,也就是反转:
var arr = [ 'one' , 'two' , 'three' ];
arr.reverse();
arr; // ['three', 'two', 'one']

splice

splice() 方法是修改 Array 的“万能方法”,它可以从指定的索引开始删除若干元素,然后再从该位置添加若干元素:
var arr = [ 'Microsoft' , 'Apple' , 'Yahoo' , 'AOL' , 'Excite' , 'Oracle' ];
// 从索引2开始删除3个元素,然后再添加两个元素:
arr.splice( 2 , 3 , 'Google' , 'Facebook' ); // 返回删除的元素 ['Yahoo', 'AOL', 'Excite']
arr; // ['Microsoft', 'Apple', 'Google', 'Facebook', 'Oracle']
// 只删除,不添加:
arr.splice( 2 , 2 ); // ['Google', 'Facebook']
arr; // ['Microsoft', 'Apple', 'Oracle']
// 只添加,不删除:
arr.splice( 2 , 0 , 'Google' , 'Facebook' ); // 返回[],因为没有删除任何元素
arr; // ['Microsoft', 'Apple', 'Google', 'Facebook', 'Oracle']

concat

concat() 方法把当前的 Array 和另一个 Array 连接起来,并返回一个新的 Array
var arr = [ 'A' , 'B' , 'C' ];
var added = arr.concat([ 1 , 2 , 3 ]);
added; // ['A', 'B', 'C', 1, 2, 3]
arr; // ['A', 'B', 'C']
请注意 concat() 方法并没有修改当前 Array ,而是返回了一个新的 Array
实际上, concat() 方法可以接收任意个元素和 Array ,并且自动把 Array 拆开,然后全部添加到新的 Array 里:
var arr = [ 'A' , 'B' , 'C' ];
arr.concat( 1 , 2 , [ 3 , 4 ]); // ['A', 'B', 'C', 1, 2, 3, 4]

join

join() 方法是一个非常实用的方法,它把当前 Array 的每个元素都用指定的字符串连接起来,然后返回连接后的字符串:
var arr = [ 'A' , 'B' , 'C' , 1 , 2 , 3 ];
arr.join( '-' ); // 'A-B-C-1-2-3'
如果 Array 的元素不是字符串,将自动转换为字符串后再连接。

对于数组,除了 map() reduce filter() sort() 这些方法可以传入一个函数外, Array 对象还提供了很多非常实用的高阶函数。

every

every() 方法可以判断数组的所有元素是否满足测试条件。
例如,给定一个包含若干字符串的数组,判断所有字符串是否满足指定的测试条件:
'use strict';

 Run

find

find() 方法用于查找符合条件的第一个元素,如果找到了,返回这个元素,否则,返回 undefined
'use strict';

 Run

findIndex

findIndex() find() 类似,也是查找符合条件的第一个元素,不同之处在于 findIndex() 会返回这个元素的索引,如果没有找到,返回 -1
'use strict';

 Run

forEach

forEach() map() 类似,它也把每个元素依次作用于传入的函数,但不会返回新的数组。 forEach() 常用于遍历数组,因此,传入的函数不需要返回值:
'use strict';

 Run

ES6数组新特性

扩展运算符
console . log ( . . . [ 1 , 2 , 3 ] )
// 1 2 3
console . log ( 1 , . . . [ 2 , 3 , 4 ] , 5 )
// 1 2 3 4 5
[ . . . document . querySelectorAll ( 'div' ) ]

替代函数的 apply 方法

由于扩展运算符可以展开数组,所以不再需要 apply 方法,将数组转为函数的参数了。
// ES5 的写法
function f ( x , y , z ) {
// ...
}
var args = [ 0 , 1 , 2 ] ;
f . apply ( null , args ) ;

// ES6的写法
function f ( x , y , z ) {
// ...
}
let args = [ 0 , 1 , 2 ] ;
f ( . . . args ) ;
下面是扩展运算符取代 apply 方法的一个实际的例子,应用 Math.max 方法,简化求出一个数组最大元素的写法。
// ES5 的写法
Math . max . apply ( null , [ 14 , 3 , 77 ] )

// ES6 的写法
Math . max ( . . . [ 14 , 3 , 77 ] )

// 等同于
Math . max ( 14 , 3 , 77 ) ;

扩展运算符的应用

(1)复制数组
数组是复合的数据类型,直接复制的话,只是复制了指向底层数据结构的指针,而不是克隆一个全新的数组。
const a1 = [ 1 , 2 ] ;
const a2 = a1 ;

a2 [ 0 ] = 2 ;
a1 // [2, 2]
上面代码中, a2 并不是 a1 的克隆,而是指向同一份数据的另一个指针。修改 a2 ,会直接导致 a1 的变化。
ES5 只能用变通方法来复制数组。
const a1 = [ 1 , 2 ] ;
const a2 = a1 . concat ( ) ;

a2 [ 0 ] = 2 ;
a1 // [1, 2]
上面代码中, a1 会返回原数组的克隆,再修改 a2 就不会对 a1 产生影响。
扩展运算符提供了复制数组的简便写法。
const a1 = [ 1 , 2 ] ;
// 写法一
const a2 = [ . . . a1 ] ;
// 写法二
const [ . . . a2 ] = a1 ;
(2)合并数组
扩展运算符提供了数组合并的新写法。
const arr1 = [ 'a' , 'b' ] ;
const arr2 = [ 'c' ] ;
const arr3 = [ 'd' , 'e' ] ;

// ES5 的合并数组
arr1 . concat ( arr2 , arr3 ) ;
// [ 'a', 'b', 'c', 'd', 'e' ]
// ES6 的合并数组
[ . . . arr1 , . . . arr2 , . . . arr3 ]
// [ 'a', 'b', 'c', 'd', 'e' ]
不过,这两种方法都是浅拷贝,使用的时候需要注意。
const a1 = [ { foo : 1 } ] ;
const a2 = [ { bar : 2 } ] ;

const a3 = a1 . concat ( a2 ) ;
const a4 = [ . . . a1 , . . . a2 ] ;

a3 [ 0 ] === a1 [ 0 ] // true
a4 [ 0 ] === a1 [ 0 ] // true
上面代码中, a3 a4 是用两种不同方法合并而成的新数组,但是它们的成员都是对原数组成员的引用,这就是浅拷贝。如果修改了引用指向的值,会同步反映到新数组。
3)与解构赋值结合
扩展运算符可以与解构赋值结合起来,用于生成数组。
// ES5
a = list [ 0 ] , rest = list . slice ( 1 )
// ES6
[ a , . . . rest ] = list
下面是另外一些例子。
const [ first , . . . rest ] = [ 1 , 2 , 3 , 4 , 5 ] ;
first // 1
rest // [2, 3, 4, 5]
const [ first , . . . rest ] = [ ] ;
first // undefined
rest // []
const [ first , . . . rest ] = [ "foo" ] ;
first // "foo"
rest  // []
如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错。
const [ . . . butLast , last ] = [ 1 , 2 , 3 , 4 , 5 ] ;
// 报错
const [ first , . . . middle , last ] = [ 1 , 2 , 3 , 4 , 5 ] ;
// 报错
(4)字符串
扩展运算符还可以将字符串转为真正的数组。
[ . . . 'hello' ]
// [ "h", "e", "l", "l", "o" ]

Array.from()  §  

Array.from 方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。
下面是一个类似数组的对象, Array.from 将它转为真正的数组。
let arrayLike = {
    '0' : 'a' ,
    '1' : 'b' ,
    '2' : 'c' ,
    length : 3
} ;

// ES5的写法
var arr1 = [ ] . slice . call ( arrayLike ) ; // ['a', 'b', 'c']
// ES6的写法
let arr2 = Array . from ( arrayLike ) ; // ['a', 'b', 'c']
只要是部署了 Iterator 接口的数据结构, Array.from 都能将其转为数组。
Array . from ( 'hello' )
// ['h', 'e', 'l', 'l', 'o']
let namesSet = new Set ( [ 'a' , 'b' ] )
Array . from ( namesSet ) // ['a', 'b']
Array.from 还可以接受第二个参数,作用类似于数组的 map 方法,用来对每个元素进行处理,将处理后的值放入返回的数组。
Array . from ( arrayLike , x = > x * x ) ;
// 等同于
Array . from ( arrayLike ) . map ( x = > x * x ) ;

Array . from ( [ 1 , 2 , 3 ] , ( x ) = > x * x )
// [1, 4, 9]

Array.of()

Array.of 方法用于将一组值,转换为数组。
Array . of ( 3 , 11 , 8 ) // [3,11,8]
Array . of ( 3 ) // [3]
Array . of ( 3 ) . length // 1
这个方法的主要目的,是弥补数组构造函数 Array() 的不足。因为参数个数的不同,会导致 Array() 的行为有差异。
Array ( ) // []
Array ( 3 ) // [, , ,]
Array ( 3 , 11 , 8 ) // [3, 11, 8]
上面代码中, Array 方法没有参数、一个参数、三个参数时,返回结果都不一样。只有当参数个数不少于 2 个时, Array() 才会返回由参数组成的新数组。参数个数只有一个时,实际上是指定数组的长度。
Array.of 基本上可以用来替代 Array() new Array() ,并且不存在由于参数不同而导致的重载。它的行为非常统一。
Array . of ( ) // []
Array . of ( undefined ) // [undefined]
Array . of ( 1 ) // [1]
Array . of ( 1 , 2 ) // [1, 2]
Array.of 总是返回参数值组成的数组。如果没有参数,就返回一个空数组。
Array.of 方法可以用下面的代码模拟实现。
function ArrayOf ( ) {
  return [ ] . slice . call ( arguments ) ;
}

数组实例的 copyWithin()

数组实例的 copyWithin() 方法,在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。也就是说,使用这个方法,会修改当前数组。
Array . prototype . copyWithin ( target , start = 0 , end = this . length )
它接受三个参数。
  • target(必需):从该位置开始替换数据。如果为负值,表示倒数。
  • start(可选):从该位置开始读取数据,默认为 0。如果为负值,表示从末尾开始计算。
  • end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示从末尾开始计算。
这三个参数都应该是数值,如果不是,会自动转为数值。
[ 1 , 2 , 3 , 4 , 5 ] . copyWithin ( 0 , 3 )
// [4, 5, 3, 4, 5]
上面代码表示将从 3 号位直到数组结束的成员(4 和 5),复制到从 0 号位开始的位置,结果覆盖了原来的 1 和 2。
下面是更多例子。
// 将3号位复制到0号位
[ 1 , 2 , 3 , 4 , 5 ] . copyWithin ( 0 , 3 , 4 )
// [4, 2, 3, 4, 5]
// -2相当于3号位,-1相当于4号位
[ 1 , 2 , 3 , 4 , 5 ] . copyWithin ( 0 , - 2 , - 1 )
// [4, 2, 3, 4, 5]
// 将3号位复制到0号位
[ ] . copyWithin . call ( { length : 5 , 3 : 1 } , 0 , 3 )
// {0: 1, 3: 1, length: 5}
// 将2号位到数组结束,复制到0号位
let i32a = new Int32Array ( [ 1 , 2 , 3 , 4 , 5 ] ) ;
i32a . copyWithin ( 0 , 2 ) ;
// Int32Array [3, 4, 5, 4, 5]
// 对于没有部署 TypedArray 的 copyWithin 方法的平台
// 需要采用下面的写法
[ ] . copyWithin . call ( new Int32Array ( [ 1 , 2 , 3 , 4 , 5 ] ) , 0 , 3 , 4 ) ;
// Int32Array [4, 2, 3, 4, 5]

数组实例的 find() 和 findIndex()

数组实例的 find 方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为 true 的成员,然后返回该成员。如果没有符合条件的成员,则返回 undefined
[ 1 , 4 , - 5 , 10 ] . find ( ( n ) = > n < 0 )
// -5
上面代码找出数组中第一个小于 0 的成员。
[ 1 , 5 , 10 , 15 ] . find ( function ( value , index , arr ) {
  return value > 9 ;
} ) // 10
上面代码中, find 方法的回调函数可以接受三个参数,依次为当前的值、当前的位置和原数组。
数组实例的 findIndex 方法的用法与 find 方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回 -1
[ 1 , 5 , 10 , 15 ] . findIndex ( function ( value , index , arr ) {
  return value > 9 ;
} ) // 2
这两个方法都可以接受第二个参数,用来绑定回调函数的 this 对象。
function f ( v ) {
  return v > this . age ;
}
let person = { name : 'John' , age : 20 } ;
[ 10 , 12 , 26 , 15 ] . find ( f , person ) ;    // 26
上面的代码中, find 函数接收了第二个参数 person 对象,回调函数中的 this 对象指向 person 对象。
另外,这两个方法都可以发现 NaN ,弥补了数组的 indexOf 方法的不足。
[ NaN ] . indexOf ( NaN )
// -1
[ NaN ] . findIndex ( y = > Object . is ( NaN , y ) )
// 0
上面代码中, indexOf 方法无法识别数组的 NaN 成员,但是 findIndex 方法可以借助 Object.is 方法做到。

数组实例的 fill()

fill 方法使用给定值,填充一个数组。
[ 'a' , 'b' , 'c' ] . fill ( 7 )
// [7, 7, 7]
new Array ( 3 ) . fill ( 7 )
// [7, 7, 7]
上面代码表明, fill 方法用于空数组的初始化非常方便。数组中已有的元素,会被全部抹去。
fill 方法还可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置。
[ 'a' , 'b' , 'c' ] . fill ( 7 , 1 , 2 )
// ['a', 7, 'c']
上面代码表示, fill 方法从 1 号位开始,向原数组填充 7,到 2 号位之前结束。
注意,如果填充的类型为对象,那么被赋值的是同一个内存地址的对象,而不是深拷贝对象。
let arr = new Array ( 3 ) . fill ( { name : "Mike" } ) ;
arr [ 0 ] . name = "Ben" ;
arr
// [{name: "Ben"}, {name: "Ben"}, {name: "Ben"}]
let arr = new Array ( 3 ) . fill ( [ ] ) ;
arr [ 0 ] . push ( 5 ) ;
arr
// [[5], [5], [5]]

数组实例的 entries(),keys() 和 values()

ES6 提供三个新的方法—— entries() keys() values() ——用于遍历数组。它们都返回一个遍历器对象(详见《Iterator》一章),可以用 for...of 循环进行遍历,唯一的区别是 keys() 是对键名的遍历、 values() 是对键值的遍历, entries() 是对键值对的遍历。
for ( let index of [ 'a' , 'b' ] . keys ( ) ) {
  console . log ( index ) ;
}
// 0
// 1
for ( let elem of [ 'a' , 'b' ] . values ( ) ) {
  console . log ( elem ) ;
}
// 'a'
// 'b'
for ( let [ index , elem ] of [ 'a' , 'b' ] . entries ( ) ) {
  console . log ( index , elem ) ;
}
// 0 "a"
// 1 "b"
如果不使用 for...of 循环,可以手动调用遍历器对象的 next 方法,进行遍历。
let letter = [ 'a' , 'b' , 'c' ] ;
let entries = letter . entries ( ) ;
console . log ( entries . next ( ) . value ) ; // [0, 'a']
console . log ( entries . next ( ) . value ) ; // [1, 'b']
console . log ( entries . next ( ) . value ) ; // [2, 'c']

数组实例的 includes()

Array.prototype.includes 方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的 includes 方法类似。ES2016 引入了该方法。
[ 1 , 2 , 3 ] . includes ( 2 )     // true
[ 1 , 2 , 3 ] . includes ( 4 )     // false
[ 1 , 2 , NaN ] . includes ( NaN ) // true
该方法的第二个参数表示搜索的起始位置,默认为 0 。如果第二个参数为负数,则表示倒数的位置,如果这时它大于数组长度(比如第二个参数为 -4 ,但数组长度为 3 ),则会重置为从 0 开始。
[ 1 , 2 , 3 ] . includes ( 3 , 3 ) ; // false
[ 1 , 2 , 3 ] . includes ( 3 , - 1 ) ; // true
没有该方法之前,我们通常使用数组的 indexOf 方法,检查是否包含某个值。
if ( arr . indexOf ( el ) ! == - 1 ) {
// ...
}
indexOf 方法有两个缺点,一是不够语义化,它的含义是找到参数值的第一个出现位置,所以要去比较是否不等于 -1 ,表达起来不够直观。二是,它内部使用严格相等运算符( === )进行判断,这会导致对 NaN 的误判。
[ NaN ] . indexOf ( NaN )
// -1
includes 使用的是不一样的判断算法,就没有这个问题。
[ NaN ] . includes ( NaN )
// true
下面代码用来检查当前环境是否支持该方法,如果不支持,部署一个简易的替代版本。
const contains = ( ( ) = >
  Array . prototype . includes
    ? ( arr , value ) = > arr . includes ( value )
    : ( arr , value ) = > arr . some ( el = > el === value )
) ( ) ;
contains ( [ 'foo' , 'bar' ] , 'baz' ) ; // => false
另外,Map 和 Set 数据结构有一个 has 方法,需要注意与 includes 区分。
  • Map 结构的 has 方法,是用来查找键名的,比如 Map.prototype.has(key) WeakMap.prototype.has(key) Reflect.has(target, propertyKey)
  • Set 结构的 has 方法,是用来查找值的,比如 Set.prototype.has(value) WeakSet.prototype.has(value)

数组实例的 flat(),flatMap()

数组的成员有时还是数组, Array.prototype.flat() 用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数据没有影响。
[ 1 , 2 , [ 3 , 4 ] ] . flat ( )
// [1, 2, 3, 4]
上面代码中,原数组的成员里面有一个数组, flat() 方法将子数组的成员取出来,添加在原来的位置。
flat() 默认只会“拉平”一层,如果想要“拉平”多层的嵌套数组,可以将 flat() 方法的参数写成一个整数,表示想要拉平的层数,默认为1。
[ 1 , 2 , [ 3 , [ 4 , 5 ] ] ] . flat ( )
// [1, 2, 3, [4, 5]]
[ 1 , 2 , [ 3 , [ 4 , 5 ] ] ] . flat ( 2 )
// [1, 2, 3, 4, 5]
上面代码中, flat() 的参数为2,表示要“拉平”两层的嵌套数组。
如果不管有多少层嵌套,都要转成一维数组,可以用 Infinity 关键字作为参数。
[ 1 , [ 2 , [ 3 ] ] ] . flat ( Infinity )
// [1, 2, 3]
如果原数组有空位, flat() 方法会跳过空位。
[ 1 , 2 , , 4 , 5 ] . flat ( )
// [1, 2, 4, 5]
flatMap() 方法对原数组的每个成员执行一个函数(相当于执行 Array.prototype.map() ),然后对返回值组成的数组执行 flat() 方法。该方法返回一个新数组,不改变原数组。
// 相当于 [[2, 4], [3, 6], [4, 8]].flat()
[ 2 , 3 , 4 ] . flatMap ( ( x ) = > [ x , x * 2 ] )
// [2, 4, 3, 6, 4, 8]
flatMap() 只能展开一层数组。
// 相当于 [[[2]], [[4]], [[6]], [[8]]].flat()
[ 1 , 2 , 3 , 4 ] . flatMap ( x = > [ [ x * 2 ] ] )
// [[2], [4], [6], [8]]
上面代码中,遍历函数返回的是一个双层的数组,但是默认只能展开一层,因此 flatMap() 返回的还是一个嵌套数组。
flatMap() 方法的参数是一个遍历函数,该函数可以接受三个参数,分别是当前数组成员、当前数组成员的位置(从零开始)、原数组。
arr . flatMap ( function callback ( currentValue [ , index [ , array ] ] ) {
// ...
} [ , thisArg ] )
flatMap() 方法还可以有第二个参数,用来绑定遍历函数里面的 this
end 


 推荐知识

 历史版本

修改日期 修改人 备注
2020-06-04 10:10:38[当前版本] 何书亮 创建版本

  目录
    知识分享平台 -V 4.8.7 -wcp