JS 数组与对象遍历方法全解析 2024-12-06

在 JS 语言中我们经常会遇到数组与对象的遍历场景,而且 JS 语言的灵活性导致我们不知道采用哪种写法会更好。本文做个总结:

1 数组的遍历

1.1 for 循环遍历

使用最基本的 for 循环来遍历数组,可以自定义循环的起始点、终止条件和步进值。
在 JavaScript 中,for 循环是一种常见的遍历数组的方式。例如:

const fruits = ["apple", "banana", "cherry", "orange"];
for(let i = 0; i < fruits.length; i++){
    console.log(fruits[i]);
}

1.2 forEach 方法

除了 for 循环可以遍历数组外,JS 中还有一个方法也可以遍历数组:forEach ()。使用数组的 forEach 方法,它接受一个回调函数,对数组的每个元素执行该函数。
但此方法只支持 IE8 以上的浏览器。forEach () 方法需要一个函数作为参数。像 forEach () 中传入的函数,虽然由我们创建但不由我们调用,因此成为回调函数。数组中有几个元素,函数就会执行几次,每次执行时,浏览器会将遍历到的元素以实参的形式传递进来,所以我们可以定义形参,来读取这些内容。

var arr = ["中文", "英文", "日文", "法文"];
arr.forEach(function(a, b){ // 传入两个形参
    console.log(1); // 会输出 4 次,因为 arr 里有 4 个元素
    console.log(a); // 输出 arr 里的每个元素
    console.log(b); // 输出 arr 里每个元素的索引
})

forEach () 方法迭代数组中的元素并为每个元素执行一次预定义函数。下面说明了 forEach () 方法的语法。

Array.forEach(callback [, thisArg]);

forEach () 方法有两个参数:

  • 回调
    forEach () 方法用于在每个元素上执行的回调函数。回调接受以下参数:
    currentElement:是当前正在处理的数组元素。
    index:当前元素在数组中的索引。
    array:调用 forEach () 方法的数组。
    索引和数组是可选的。

  • thisArg
    thisArg 是执行回调时用作 this 的值。
    请注意,forEach () 函数返回 undefined,因此,它不像 filter ()、map ()、some ()、every () 和 sort () 方法。
    与 for 循环相比,forEach () 方法的一个限制是,我们不能使用 break 或 continue 语句来控制循环。我们要终止 forEach () 方法中的循环,必须在回调函数内抛出异常。

1.3 for…of 循环

for-of循环,ES6 新增特性。有了 forEach 来遍历数组,似乎变得简洁了许多。

var arr = ["中文", "英文", "日文", "法文"];
for(var value of arr){
    console.log(value);
}

注意for of不仅可以遍历数组,还可以:
1、遍历字符串
2、遍历类数组对象
3、支持 Map 和 Set 对象遍历。

for of 遍历 map 对象举例:

var data = ["中文", "英文", "日文", "法文"];
for(const[index, item]of new Map(data.map((item, i)=>[i, item]))){
  console.log(index, item)
}

1.4 map 方法

map 方法可以对数组中的每个元素执行一个函数,并返回一个新的数组。
map 和 forEach 几乎很像,但是也有它的不同之处,比如:
注意:map 是表示映射的,也就是一一对应,遍历完成之后会返回一个新的数组,但是不会修改原来的数组。

var a1 = ['a', 'b', 'c'];
var a2 = a1.map(function(item, key, arr) {
    return item.toUpperCase();
});
console.log(a1);// ['a','b','c'];
console.log(a2); //['A','B','C'];

item:必填参数,数组中正在处理的当前元素。
index:可选参数,数组中正在处理的当前元素的索引。
arr:可选参数,方法被调用的数组。也就是当前元素属于的数组对象。

map 方法会给原数组中的每个元素都按顺序调用一次 callback 函数。callback 每次执行后的返回值(包括 undefined)组合起来形成一个新数组。 callback 函数只会在有值的索引上被调用;那些从来没被赋过值或者使用 delete 删除的索引则不会被调用。
callback 函数会被自动传入三个参数:数组元素,元素索引,原数组本身。
map 不修改调用它的原数组本身(当然可以在 callback 执行时改变原数组)。

注意:使用 map 方法处理数组时,数组元素的范围是在 callback 方法第一次调用之前就已经确定了。在 map 方法执行的过程中:原数组中新增加的元素将不会被 callback 访问到;若已经存在的元素被改变或删除了,则它们的传递到 callback 的值是 map 方法遍历到它们的那一时刻的值;而被删除的元素将不会被访问到。

1.5 filter 方法

filter 方法用于根据条件筛选出符合条件的数组元素,返回一个新数组。filter 有过滤的意思,也就说它就是一个过滤器。
注意:filter 它是遍历每一个元素,用每一个元素去匹配,如果返回 true,就会返回一个次数,最后将所有符合添加的全部选出。

var a1 =[1,2,3,4,5,6];
var a2 = a1.filter(function(item) {
    return item <= 3;
});
console.log(a2); //[1,2,3];

1.6 reduce 方法

reduce 方法用于对数组中的所有元素进行累积操作,返回一个累积值。
reduce 从左到右依次遍历,一般用来做加减乘除运算使用。reduce () 方法接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值。下面举例:

var a1 =[1, 2, 3];
var total = a1.reduce(function(first, second) {
    return first + second;
}, 0);
console.log(total) // 6

注意:就是 return first+second 其实相当于 return first+=second;也就是说每次的 first 是上一次的和就是 function {} 后面的参数(值为0),如果有值那么第一次加载的时候 first = 0;second = 1;如果没有值,first =1,second = 2;如果后面的参数是个字符串,那么就是会是字符串拼接。

1.7 some 和 every 方法

some 方法用于检查数组中是否至少有一个元素满足条件,而 every 方法用于检查数组中是否所有元素都满足条件。
some () 遍历数组,只要有一个以上的元素满足条件就返回 true,否则返回 false,退出循环。

arr.some(callback(element[, index[, array]])[, thisArg])

举例:

function isNumber(value){
    return typeof value == 'number';
}
var a1 =[1, 2, 3];
console.log(a1.every(isNumber)); // true
var a2 =[1, '2', 3];
console.log(a2.every(isNumber)); // false

1.8 for…in 循环

for...in 循环用于遍历对象的属性,但也可以用于遍历数组。不过,不推荐在数组上使用 for...in,因为它会包括数组的原型属性。
for...in 循环主要用于遍历对象的可枚举属性(包括继承的属性)。然而,它也可以遍历数组,但此时它遍历的是数组的索引(作为字符串),而不是数组的值。举例:

const array = ['apple', 'banana', 'orange'];
for (const key in array) {
    console.log(key); // 输出:'0', '1', '2'(字符串形式的索引)
    console.log(array[key]); // 输出数组的值
}

2 对象的遍历

2.1 Object.keys()

Object.keys () 方法会返回一个给定对象的自身 (不含继承的) 可枚举属性 (不含 Symbol 属性)组成的数组,数组中属性名的排列顺序和使用 for...in 循环遍历该对象时返回的顺序一致。它可以传入对象、字符串或数组等不同类型的数据,返回不同的结果。例如:传入对象时,返回包含对象可枚举属性和方法的数组;传入字符串时,返回索引值;传入数组时,也返回索引值
举例如下:

// 创建一个示例对象
const person = {
  name: 'John',
  age: 30,
  city: 'New York'
};

// 使用Object.keys()获取对象的属性名数组
const keys = Object.keys(person);

// 遍历属性名数组并输出属性名和对应的值
keys.forEach(key => {
  console.log(`${key}: ${person[key]}`);
});
// "name: John"
//  "age: 30"
// "city: New York"

2.2 for in 循环

遍历对象自身的和继承的可枚举属性 (不含 Symbol 属性),使用时需注意过滤原型链上的属性。 for...in 是用来枚举对象的属性的,它遍历的是对象所有可遍历(enumerable)的属性,会跳过不可遍历的属性,不仅遍历对象自身的属性,还遍历继承的属性。每次循环都会将对象中存在的一个属性名赋值给变量,一直到对象中所有的属性都被枚举一遍为止。在多人协作开发项目时,可能会因为同事在代码里或引入的插件里对 Object.prototype 进行操作,导致遍历对象时出现意想不到的属性。可以利用 es5 的 Object.defineProperty 方法为属性添加限制,设置 enumerable 属性值为 false,让特定属性不可枚举,从而避免在遍历中出现。

2.2.1 基本示例

let myObject = {
       name: "Alice",
       age: 30,
       hobby: "Reading"
   };
for (let key in myObject) {
       console.log(key + ": " + myObject[key]);
   }
// "name: Alice"
// "age: 30"
// "hobby: Reading"

在这个例子中,for - in循环会遍历myObject对象的每一个可枚举属性。在每次循环中,key变量会被赋值为当前属性的名称,然后通过 myObject[key]可以访问该属性的值。所以这个循环会输出对象myObject的所有属性名和对应的值。

2.2.2 遍历包含方法的对象

let person = {
       firstName: "Bob",
       lastName: "Smith",
       getFullName: function () {
           return this.firstName + " " + this.lastName;
       }
   };
for (let property in person) {
       console.log(property + ": " + person[property]);
}
//  "firstName: Bob"
// "lastName: Smith"
// 'getFullName: function () {
//           return this.firstName + " " + this.lastName;
//       }'

在这里,for - in循环不仅会遍历对象的属性(firstName和lastName),还会遍历方法(getFullName)。当遍历到方法时,它会输出方法的代码内容。

2.2.3 继承情况下的遍历

当涉及对象继承时,for - in循环会遍历对象自身的可枚举属性以及从原型链继承的可枚举属性。

 let animal = {
   type: "Mammal"
};
let dog = Object.create(animal);
dog.breed = "Labrador";
for (let key in dog) {
    console.log(key + ": " + dog[key]);
}
// "breed: Labrador"
// "type: Mammal"

在这个例子中,dog对象通过Object.create()从animal对象继承了type属性,并且自身有breed属性。for - in循环会遍历dog对象的breed属性和从animal对象继承的type属性,输出它们的名称和对应的值。
需要注意的是,在实际应用中,如果只想遍历对象自身的属性,而不包括继承的属性,可以使用Object.hasOwnProperty()方法来检查属性是否是对象自身的属性。例如:

let animal = {
   type: "Mammal"
};
let dog = Object.create(animal);
dog.breed = "Labrador";
for (let key in dog) {
   if (dog.hasOwnProperty(key)) {
    console.log(key + ": " + dog[key]);
   }
}
// "breed: Labrador"

2.3 Object.getOwnPropertyNames(obj)

Object.getOwnPropertyNames ()方法返回一个数组,包含对象自身的所有属性名(包括不可枚举属性但不包括 Symbol 值作为名称的属性)组成的数组。可以通过数组的 forEach 方法来遍历这个数组,获取对象的属性名和对应的值。

2.3.1 基本示例

let myObject = {
    name: "Eve",
    age: 28,
    [Symbol("secret")]: "hidden value"
};
let propertyNames = Object.getOwnPropertyNames(myObject);
for (let i = 0; i < propertyNames.length; i++) {
    let propertyName = propertyNames[i];
    console.log(propertyName + ": " + myObject[propertyName]);
}
// "name: Eve"
// "age: 28"

在这个例子中,Object.getOwnPropertyNames(myObject)返回一个包含对象myObject所有自身属性名的数组(不包括Symbol属性)。然后通过一个for循环遍历这个数组,对于每个属性名,通过myObject[propertyName]获取属性值并打印出来。在这个例子中,会打印出name和age这两个属性的名称和对应的值。

2.3.2 遍历包含不可枚举属性的对象

let anotherObject = {};
Object.defineProperty(anotherObject, "id", {
    value: 123,
    enumerable: false
});
anotherObject.name = "Frank";
let anotherObjectPropertyNames = Object.getOwnPropertyNames(anotherObject);
console.log(anotherObjectPropertyNames);
// Array ["id", "name"]

这里通过Object.defineProperty定义了一个不可枚举属性id,并添加了一个可枚举属性name。Object.getOwnPropertyNames会返回包含id和name的数组,因为它返回的是对象自身的所有属性名,不管是否可枚举。如果使用Object.keys,它只会返回name,因为Object.keys只返回可枚举的属性名。

2.3.3 与函数对象结合使用

对于函数对象,Object.getOwnPropertyNames也很有用。

function myFunction() {
    this.x = 10;
}
myFunction.prototype.y = 20;
let functionObject = new myFunction();
let functionObjectPropertyNames = Object.getOwnPropertyNames(functionObject);
console.log(functionObjectPropertyNames);
// Array ["x"]

在这里,functionObject是通过构造函数myFunction创建的。Object.getOwnPropertyNames会返回包含x的数组,因为x是对象functionObject自身的属性,而不会返回y,因为y是在原型myFunction.prototype上定义的属性。

2.4 Reflect.ownKeys(obj)

Reflect.ownKeys () 方法返回一个数组,包含对象自身所有属性名组成的数组,包括不可枚举的属性和 Symbol 属性。这个方法在 ES2015 新增,是一种获取对象所有属性的强大方式。

2.4.1 基本示例

let myObject = {
    name: "Grace",
    age: 35,
    [Symbol("secret")]: "This is a secret"
};
let keys = Reflect.ownKeys(myObject);
keys.forEach(key => {
    if (typeof key === "string") {
        console.log(`String property: ${key} - Value: ${myObject[key]}`);
    } else if (typeof key === "symbol") {
        console.log(`Symbol property: ${key.toString()} - Value: ${myObject[key]}`);
    }
});
//"String property: name - Value: Grace"
//"String property: age - Value: 35"
// "Symbol property: Symbol(secret) - Value: This is a secret"

在这个例子中,Reflect.ownKeys(myObject)返回一个包含对象myObject所有自身属性的数组,不管属性名是字符串还是Symbol,也不管是否可枚举。然后使用forEach循环遍历这个数组。在循环中,根据属性名的类型(字符串或Symbol),分别打印出属性名和对应的属性值。

2.4.2 包含不可枚举属性的对象

let nonEnumerableObject = {};
Object.defineProperty(nonEnumerableObject, "id", {
    value: 100,
    enumerable: false
});
nonEnumerableObject.name = "Henry";
let nonEnumerableKeys = Reflect.ownKeys(nonEnumerableObject);
console.log(nonEnumerableKeys);
// Array ["id", "name"]

这里通过Object.defineProperty定义了一个不可枚举属性id,并添加了一个可枚举属性name。Reflect.ownKeys会返回包含id和name的数组,因为它返回对象自身的所有属性,不受可枚举性的限制。

2.4.3 与类和原型链相关的示例

class MyClass {
    constructor() {
        this.instanceProperty = "Instance value";
    }
}
MyClass.prototype.classProperty = "Class value";
let myInstance = new MyClass();
let instanceKeys = Reflect.ownKeys(myInstance);
console.log(instanceKeys);
// > Array ["instanceProperty"]

在这个例子中,myInstance是MyClass的一个实例。Reflect.ownKeys(myInstance)会返回包含instanceProperty的数组,因为它只返回对象自身的属性,而不会返回在原型MyClass.prototype上定义的classProperty。这有助于我们清晰地分离对象自身的属性和从原型继承的属性。

2.5 Object.values()

Object.values ()方法返回一个数组,只包含可枚举的属性值,不包括原型链上的属性。这个方法可以用于获取对象自身可枚举属性的值,方便对对象的属性值进行操作。

2.5.1 基本示例

const user = {
    name: "Lucy",
    age: 22,
    city: "London"
};
const values = Object.values(user);
values.forEach((value, index) => {
    console.log(`属性值 ${value} 对应的索引是 ${index}`);
});
// "属性值 Lucy 对应的索引是 0"
// "属性值 22 对应的索引是 1"
// "属性值 London 对应的索引是 2"

首先,Object.values(user)会获取对象user所有可枚举属性的值,返回一个数组[ "Lucy", 22, "London" ]。然后使用forEach方法遍历这个数组,value代表每个属性值,index是其在数组中的索引位置。这样就可以输出每个属性值以及它在属性值数组中的索引。

2.5.2 结合函数对象使用

const mathOperations = {
    add: function (a, b) {
        return a + b;
    },
    subtract: function (a, b) {
        return a - b;
    },
    multiply: function (a, b) {
        return a * b;
    }
};
const operationValues = Object.values(mathOperations);
operationValues.forEach((operation) => {
    console.log(`操作函数: ${operation.name}`);
});
// "操作函数: add"
// "操作函数: subtract"
// "操作函数: multiply"

这里Object.values(mathOperations)获取对象mathOperations中函数属性的值,也就是函数本身。返回一个数组后,通过forEach遍历这个数组,operation代表每个函数。我们通过operation.name来输出每个函数的名称,如add、subtract、multiply

2.5.3 遍历嵌套对象

const nestedObject = {
    outerProp1: "value1",
    outerProp2: {
        innerProp1: "value2",
        innerProp2: "value3"
    }
};
const nestedValues = Object.values(nestedObject);
nestedValues.forEach((value) => {
    if (typeof value === "object") {
        console.log("嵌套对象的值:", value);
        const innerValues = Object.values(value);
        innerValues.forEach((innerValue) => {
            console.log("嵌套对象内部的值:", innerValue);
        });
    } else {
        console.log("非嵌套对象的值:", value);
    }
});
// "非嵌套对象的值:" "value1"
// "嵌套对象的值:" Object { innerProp1: "value2", innerProp2: "value3" }
// "嵌套对象内部的值:" "value2"
// "嵌套对象内部的值:" "value3"

首先,Object.values(nestedObject)返回一个包含outerProp1的值和outerProp2这个对象的数组。然后在forEach循环中,对于非对象类型的值(如outerProp1的值)直接输出。对于对象类型的值(如outerProp2),再使用Object.values获取其内部属性的值并输出,这样就可以遍历嵌套对象中的所有属性值。

2.6 Object.entries()

Object.entries () 方法返回一个数组,包含给定对象自身可枚举属性的 [key, value]对。这个数组的排列顺序和使用 for...in 循环遍历该对象时返回的顺序一致,但与 for-in 不同的是,它不会枚举原型链上的属性。

2.6.1 基本示例

const book = {
    title: "JS",
    author: "John Doe",
    year: 2023
};
const entries = Object.entries(book);
entries.forEach(([key, value]) => {
    console.log(`属性名: ${key}, 属性值: ${value}`);
});
// "属性名: title, 属性值: JS"
// "属性名: author, 属性值: John Doe"
// "属性名: year, 属性值: 2023"

Object.entries(book)返回一个二维数组,其中每个子数组包含两个元素:属性名和对应的属性值。

2.6.2 与数组的 map 方法结合使用

const car = {
    brand: "Toyota",
    model: "Corolla",
    color: "Blue"
};
const newArray = Object.entries(car).map(([key, value]) => {
    return `${key}: ${value}`;
});
console.log(newArray);
// Array ["brand: Toyota", "model: Corolla", "color: Blue"]

这里,Object.entries(car)获取汽车对象car的属性名 - 属性值对数组。然后使用map方法对这个数组进行转换,将每个属性名和属性值组合成一个格式化的字符串(如brand: Toyota),最后将这些字符串存储在newArray中并打印出来。

2.7 Object.getOwnPropertySymbols()

Object.getOwnPropertySymbols () 方法返回一个数组,包含对象自身的所有 Symbol 属性键。这个方法可以用于获取对象的 Symbol 属性,方便对这些特殊类型的属性进行操作。

2.7.1 基本示例

const myObject = {
    name: "Tom",
    [Symbol("id")]: 123
};
const symbols = Object.getOwnPropertySymbols(myObject);
symbols.forEach((symbol) => {
    console.log(`Symbol属性: ${symbol.toString()},值为: ${myObject[symbol]}`);
});
// "Symbol属性: Symbol(id),值为: 123"

在这个例子中,Object.getOwnPropertySymbols(myObject)返回一个包含对象myObject所有自身Symbol属性的数组。这里只有一个Symbol("id")属性。然后通过forEach循环遍历这个数组,对于每个Symbol属性,通过myObject[symbol]获取其对应的值,并打印出Symbol属性的字符串表示和它的值。

2.7.2 结合普通属性的对象

const anotherObject = {
    age: 30,
    [Symbol("secret")]: "This is a secret"
};
const symbolsOfAnotherObject = Object.getOwnPropertySymbols(anotherObject);
console.log("Symbol属性列表:");
symbolsOfAnotherObject.forEach((symbol) => {
    console.log(symbol.toString());
});
console.log("普通属性列表:");
const propertyNames = Object.keys(anotherObject);
propertyNames.forEach((propertyName) => {
    console.log(propertyName);
});
//  "Symbol属性列表:"
// "Symbol(secret)"
// "普通属性列表:"
// "age"

这里Object.getOwnPropertySymbols(anotherObject)获取对象的Symbol属性数组,然后分别打印出这些Symbol属性的字符串表示。接着使用Object.keys获取普通属性数组并打印出普通属性的名称,这样就可以清晰地分开查看对象的Symbol属性和普通属性。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容