如何理解JavaScript中的原型链?

原型链是JavaScript实现继承和属性查找的机制,通过对象的[[Prototype]]链接形成链条,当访问属性时会沿链向上查找直至找到或到达null。原型(prototype)是函数特有的属性,指向实例共享方法的原型对象;原型链则是由__proto__连接构成的查找路径,二者共同实现对象间的方法共享与继承。利用原型链可优化内存、实现继承并提升性能,ES6的class本质仍是基于原型链的语法糖,提供更清晰的继承写法但底层机制不变。

如何理解javascript中的原型链?

JavaScript中的原型链,简单来说,就是一套对象间继承属性和方法的机制。当你试图访问一个对象的某个属性或方法时,如果它本身没有,JavaScript引擎就会沿着一条由

__proto__

(或内部的

[[Prototype]]

)连接起来的链条向上查找,直到找到该属性或者到达链的末端(

null

)。这条查找路径,就是我们所说的原型链。它让不同的对象能够共享同一套行为,避免了重复定义,也使得JavaScript的继承模式显得既灵活又有些“不走寻常路”。

理解原型链,我觉得就像理解一棵家族树,但不是从上往下看,而是从你自身往上追溯你的祖先。每个对象都有一个“父母”对象(它的原型),如果父母没有,就去找爷爷奶奶(父母的原型),以此类推,直到找到“始祖”(

Object.prototype

)或者发现没人有这个属性。

解决方案

要深入理解原型链,我们得从几个核心概念入手。首先是每个JavaScript对象内部都有一个

[[Prototype]]

属性,它指向这个对象的原型。在旧的浏览器环境或为了方便调试,我们常常通过

__proto__

这个非标准但广泛实现的属性来访问它。而对于函数(它们也是对象),还有一个

prototype

属性,这可不是一回事。

prototype

属性指向的是一个对象,这个对象会作为通过该函数构造出来的实例的

[[Prototype]]

。听起来有点绕?别急,我们慢慢捋。

我们通常创建对象的方式,其实都在默默地利用原型链。

立即学习“Java免费学习笔记(深入)”;

1. 对象字面量:当你写

{}

new Object()

时,你创建的对象会默认继承

Object.prototype

const myObj = {};console.log(myObj.__proto__ === Object.prototype); // trueconsole.log(myObj.toString()); // "[object Object]" - 这个方法就来自Object.prototype
myObj

本身并没有

toString

方法,但它能调用,就是因为JavaScript引擎沿着

myObj.__proto__

找到了

Object.prototype

上的

toString

2. 构造函数:这是JavaScript实现“类”或“蓝图”的传统方式。

function Person(name) {    this.name = name;}// 在Person.prototype上添加方法,所有Person实例共享Person.prototype.sayHello = function() {    console.log(`Hello, my name is ${this.name}`);};const alice = new Person('Alice');console.log(alice.name); // 'Alice'alice.sayHello(); // 'Hello, my name is Alice'// 关键来了:alice的原型是Person.prototypeconsole.log(alice.__proto__ === Person.prototype); // true// Person.prototype的原型是Object.prototypeconsole.log(Person.prototype.__proto__ === Object.prototype); // true// Object.prototype的原型是null,链的终点console.log(Object.prototype.__proto__ === null); // true

alice.sayHello()

被调用时,引擎先在

alice

对象自身找

sayHello

,没找到。接着,它会沿着

alice.__proto__

找到

Person.prototype

,在这里找到了

sayHello

方法并执行。这就是原型链在实际工作中的一个经典体现。

3.

Object.create()

这个方法允许你显式地指定一个新对象的原型。

const animal = {    eats: true,    walk() {        console.log("Animal walks.");    }};const rabbit = Object.create(animal); // rabbit的原型是animalrabbit.jumps = true;rabbit.walk(); // "Animal walks."console.log(rabbit.__proto__ === animal); // trueconsole.log(animal.__proto__ === Object.prototype); // true
rabbit

对象自身没有

walk

方法,但它能通过原型链找到

animal

对象上的

walk

方法。

原型链的魔力在于它实现了属性和方法的共享,这意味着内存效率更高,因为方法只存储在一个地方(原型对象上),所有实例都通过原型链去访问它。同时,它也是JavaScript实现“继承”的基石。

JavaScript原型(prototype)和原型链(prototype chain)之间究竟有什么关联与区别

这确实是初学者最容易混淆的地方,也是我当年踩过不少坑的地方。要我说,它们俩是紧密相连但又有着明确分工的两个概念。

原型(

prototype

):这个词在JavaScript里通常特指函数对象上的一个特殊属性。记住,只有函数(包括构造函数和类)才会有这个

prototype

属性。它是一个普通的对象,我们称之为“原型对象”。这个原型对象的作用非常关键:当你用这个函数作为构造器(比如

new MyFunction()

)创建实例时,新创建的实例的

[[Prototype]]

(也就是

__proto__

)就会指向这个

MyFunction.prototype

对象。

举个例子,

Array.prototype

就是

Array

构造函数的原型对象。所有数组实例,比如

[1, 2, 3]

,它们的

__proto__

都指向

Array.prototype

,因此它们都能访问

Array.prototype

上定义的

push

pop

等方法。

原型链(

prototype chain

):原型链则是一个机制,是当JavaScript引擎在查找对象的属性或方法时,所遵循的一条路径。这条路径是由对象的

[[Prototype]]

(或

__proto__

)属性串联起来的。每个对象都有一个

[[Prototype]]

,它指向其原型对象,而这个原型对象也可能有自己的

[[Prototype]]

,这样一层一层向上,就形成了一条链。这条链的终点通常是

null

(即

Object.prototype

[[Prototype]]

)。

所以,关联在于:原型(

prototype

属性所指向的原型对象)是构成原型链上的一个个节点。 每个对象的

__proto__

指向的,就是它在原型链上的“上一站”——它的原型对象。

区别在于:

prototype

是一个属性,存在于函数对象上,指向一个对象。它的作用是为通过该函数构造的实例提供共享的属性和方法。原型链 是一种查找机制/结构,由一系列对象通过

__proto__

(内部的

[[Prototype]]

)连接而成。它的作用是实现对象的继承和属性查找。

你可以把

prototype

想象成一个工厂的“蓝图库”,里面存放着所有新产品(实例)应该具备的共享部件。而原型链则是当你拿到一个产品(实例)后,发现某个部件没有,就去它的“蓝图库”找,如果蓝图库里也没有,就去蓝图库的“蓝图库”找,直到找到或者放弃,这个查找过程就是原型链。

在实际开发中,我们如何有效利用JavaScript原型链来优化代码结构和性能?

原型链在实际开发中的应用远不止于理论,它确实能帮助我们写出更优雅、更高效的代码。我个人觉得,最直接和最常见的应用场景就是共享方法和属性,这直接关系到内存和性能。

1. 共享方法,减少内存开销:这是原型链最核心的优势之一。想象一下,如果你有1000个

User

对象,每个对象都有一个

greet()

方法。如果这个方法直接定义在每个实例上:

function UserBad(name) {    this.name = name;    this.greet = function() { // 每个实例都会有自己的greet方法副本        console.log(`Hi, I'm ${this.name}`);    };}const user1 = new UserBad('Alice');const user2 = new UserBad('Bob');// user1.greet !== user2.greet // 它们是不同的函数对象

这样,每个

UserBad

实例都会在内存中存储一个

greet

函数的副本,如果方法很多,实例也很多,内存消耗会非常大。

而利用原型链,我们可以将方法定义在构造函数的

prototype

对象上:

function UserGood(name) {    this.name = name;}UserGood.prototype.greet = function() { // 所有实例共享同一个greet方法    console.log(`Hi, I'm ${this.name}`);};const userA = new UserGood('Charlie');const userB = new UserGood('David');// userA.greet === userB.greet // 它们引用的是同一个函数对象

这样,

greet

方法只在

UserGood.prototype

上存在一份,所有

UserGood

的实例都通过原型链来访问它。这显著减少了内存占用,尤其是在处理大量对象时,性能提升非常明显。

2. 实现继承:虽然ES6有了

class

语法糖,但其底层依然是原型链。通过原型链,我们可以实现经典的“原型式继承”。

function Animal(name) {    this.name = name;}Animal.prototype.eat = function() {    console.log(`${this.name} eats.`);};function Dog(name, breed) {    Animal.call(this, name); // 继承Animal的属性    this.breed = breed;}// 核心:让Dog的原型继承Animal的原型// 避免直接赋值Animal.prototype,那样会覆盖Dog.prototype,导致所有Dog实例的原型都指向Animal.prototype// Object.create() 是一个很好的方式来创建一个新对象,并指定其原型Dog.prototype = Object.create(Animal.prototype);Dog.prototype.constructor = Dog; // 修复constructor指向Dog.prototype.bark = function() {    console.log(`${this.name} barks!`);};const myDog = new Dog('Buddy', 'Golden Retriever');myDog.eat(); // Buddy eats. (来自Animal.prototype)myDog.bark(); // Buddy barks! (来自Dog.prototype)

这里,

myDog

实例的原型链是:

myDog

->

Dog.prototype

->

Animal.prototype

->

Object.prototype

->

null

。这样

Dog

实例就能访问

animal

原型上的方法了。

3. 扩展内置对象(需谨慎):虽然不推荐大规模修改内置对象的原型(因为它可能导致命名冲突和难以调试的问题),但在某些特定且受控的环境下,为内置对象添加一些辅助方法确实能提高开发效率。

// 示例:给Array.prototype添加一个求和方法// 实际项目中应避免直接修改内置原型,除非你完全控制环境if (!Array.prototype.sum) { // 检查是否已存在,避免重复定义    Array.prototype.sum = function() {        return this.reduce((acc, current) => acc + current, 0);    };}const numbers = [1, 2, 3, 4, 5];console.log(numbers.sum()); // 15

这样,所有数组实例都能直接调用

sum()

方法,非常方便。但再次强调,这种做法应极度谨慎。

通过合理利用原型链,我们能够构建出模块化、可维护且高效的JavaScript代码,这对于大型应用尤其重要。

ES6中的

class

语法糖,它背后和JavaScript原型链的工作机制有何异同?

ES6引入的

class

关键字,无疑让JavaScript的面向对象编程看起来更“传统”了,它提供了更清晰、更简洁的语法来创建构造函数和处理继承。然而,这只是一个语法糖(syntactic sugar)。它并没有引入新的继承机制,其底层依然是基于我们刚才讨论的原型链机制。

相同点:

基于原型链的继承:

class

的继承机制,即

extends

关键字,本质上还是通过操作原型链来实现的。当你写

class Dog extends Animal {}

时,JavaScript引擎会在幕后将

Dog.prototype

[[Prototype]]

设置为

Animal.prototype

,从而建立了原型链上的继承关系。

class Animal {    constructor(name) {        this.name = name;    }    eat() {        console.log(`${this.name} eats.`);    }}class Dog extends Animal {    constructor(name, breed) {        super(name); // 调用父类的构造函数        this.breed = breed;    }    bark() {        console.log(`${this.name} barks!`);    }}const myDog = new Dog('Buddy', 'Golden Retriever');myDog.eat(); // Buddy eats.myDog.bark(); // Buddy barks!// 底层原理:console.log(Dog.prototype.__proto__ === Animal.prototype); // trueconsole.log(myDog.__proto__ === Dog.prototype); // true

可以看到,

Dog.prototype

确实继承自

Animal.prototype

,这正是原型链在起作用。

方法共享:

class

中定义的所有实例方法(非静态方法),都会被放置在类的

prototype

对象上,从而实现所有实例共享这些方法,节省内存。这与传统构造函数将方法定义在

Function.prototype

上是完全一致的。

constructor

属性:

class

中的

constructor

方法,其实就是传统构造函数。它在创建实例时被调用,

this

指向新创建的实例。

不同点(主要是语法和一些细节行为):

语法更清晰: 这是最显而易见的区别。

class

语法避免了手动操作

prototype

Object.create()

来设置继承,让代码看起来更像传统的面向对象语言,更容易理解和维护。

super

关键字: 在子类的构造函数中,必须先调用

super()

才能使用

this

super()

实际上就是调用了父类的构造函数。在方法中,

super

可以用来调用父类的方法。这是

class

特有的语法糖,在传统原型继承中,你需要手动调用

Parent.call(this, ...)

Parent.prototype.method.call(this, ...)

class

是函数: 尽管看起来像一个特殊的结构,但

class

本身仍然是一个函数。

class MyClass {}console.log(typeof MyClass); // "function"

不过,

class

函数与普通函数有一些关键区别:

不能被

new

调用: 普通函数如果不用

new

调用,

this

可能指向全局对象;

class

必须用

new

调用。没有

arguments

对象:

class

内部没有

arguments

对象。严格模式:

class

内部的代码默认运行在严格模式下,即使你没有在文件开头声明

"use strict"

静态方法和属性:

class

提供了

static

关键字来定义静态方法和属性,这些方法和属性直接属于类本身,而不是类的实例,也无法通过实例访问。

class Utils {    static add(a, b) {        return a + b;    }}console.log(Utils.add(1, 2)); // 3// const util = new Utils();// util.add(1, 2); // TypeError: util.add is not a function

在传统原型链中,静态方法通常是直接作为构造函数的属性来添加的,比如

Function.staticMethod = ...

私有字段(提案中或已实现): 现代JavaScript的

class

语法还引入了私有字段(如

#privateField

),这在传统原型继承中是难以直接实现的(通常需要闭包或

Symbol

来模拟)。

总结来说,

class

是JavaScript语言在语法层面为了迎合更广泛的编程范式而做出的改进,它让开发者能够以更直观的方式使用原型链的强大功能。但无论你使用

class

还是传统的构造函数加原型,理解原型链的底层机制始终是深入掌握JavaScript面向对象编程的关键。它就像是冰山下面的部分,虽然不总被直接看到,但支撑着整个上层结构。

以上就是如何理解JavaScript中的原型链?的详细内容,更多请关注创想鸟其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/1521644.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
JS 浏览器渲染原理 – 从解析 HTML 到 Composite 层的完整流程
上一篇 2025年12月20日 14:21:20
将扁平对象转换为嵌套结构:基于路径分割键的JavaScript实现
下一篇 2025年12月20日 14:21:30

相关推荐

  • PHP匿名函数怎么用_PHP匿名函数使用场景分析

    PHP匿名函数是无名函数,可作为回调或赋值给变量,常用在数组处理、事件回调、逻辑封装等场景,支持use引入外部变量及fn短语法,结合bindTo可访问对象私有成员。 PHP匿名函数,也叫闭包函数(Closure),是一种没有名称的函数,通常作为回调使用或赋值给变量。它在实际开发中非常灵活,尤其适合用…

    2026年9月22日
    100
  • 中国联通正式获得开展 eSIM 手机运营服务商用试验的批复

    感谢网友 会弹琴的九号、学士 的线索投递! 10月13日,三大运营商官方微信号相继发布消息,宣告eSIM服务进入新阶段。其中,中国联通于当日上午10:00率先发布推文《抢约!联通eSIM来了!》,动作迅速,展现出强烈的市场积极性;中国移动在傍晚19:29发布《中国移动全面上线eSIM手机办理》;而中…

    2026年9月22日
    200
  • 为什么建议手动定义Java序列化ID

    手动定义serialVersionUID可确保序列化兼容性,避免因类结构变化导致反序列化失败。Java默认生成的ID依赖类名、字段等信息,编译环境或代码微小改动均使其改变,易引发InvalidClassException。显式声明后,可在兼容性变更时主动控制ID更新,保留原ID则允许旧版本读取新对象…

    2026年9月22日
    200
  • mysql怎么使用全文索引 mysql创建全文索引的配置方法

    mysql怎么使用全文索引 mysql创建全文索引的配置方法mysql怎么使用全文索引 mysql创建全文索引的配置方法mysql怎么使用全文索引 mysql创建全文索引的配置方法mysql怎么使用全文索引 mysql创建全文索引的配置方法

    mysql使用全文索引的核心是让数据库像搜索引擎一样理解并高效检索文本内容。1. 创建全文索引:可在建表时或之后通过alter table语句为char、varchar或text字段添加fulltext索引;2. 使用match against查询:支持自然语言模式(自动过滤停用词并按相关性排序)和…

    2026年9月22日 用户投稿
    100
  • VSCode如何通过调试变量监视列表批量追踪数据变化 VSCode变量监视列表批量追踪的新颖技巧​

    VSCode如何通过调试变量监视列表批量追踪数据变化 VSCode变量监视列表批量追踪的新颖技巧​VSCode如何通过调试变量监视列表批量追踪数据变化 VSCode变量监视列表批量追踪的新颖技巧​VSCode如何通过调试变量监视列表批量追踪数据变化 VSCode变量监视列表批量追踪的新颖技巧​VSCode如何通过调试变量监视列表批量追踪数据变化 VSCode变量监视列表批量追踪的新颖技巧​

    vscode中高效批量追踪数据变化的关键是将监视列表用作表达式求值器,而非仅添加单一变量;2. 可在监视列表中添加复杂对象路径(如user.profile.address.city)、计算表达式(如(a + b) * c)、函数调用(如calculatetotal(items))或条件判断(如myv…

    2026年9月22日 用户投稿
    000
  • 在Java中如何统计List中元素出现次数

    答案是使用Map或Stream API统计List元素频次最高效。通过HashMap手动遍历统计,或用Java 8的Stream结合groupingBy和counting()实现简洁计数,Collections.frequency适用于小数据量但性能较差,推荐Stream方式兼顾性能与可读性。 在J…

    2026年9月22日
    900
  • 如何设置Linux服务超时参数 systemd服务超时配置

    如何设置Linux服务超时参数 systemd服务超时配置如何设置Linux服务超时参数 systemd服务超时配置如何设置Linux服务超时参数 systemd服务超时配置如何设置Linux服务超时参数 systemd服务超时配置

    systemd服务超时参数调整方法包括:1.使用systemctl show查看timeoutstartsec、timeoutstopsec、timeoutsec字段获取当前配置;2.通过systemctl edit编辑unit文件设置timeoutstartsec、timeoutstopsec或t…

    2026年9月22日 用户投稿
    000
  • 360浏览器如何切换极速模式

    在浏览网页时,想要获得更流畅、更快速的上网体验,许多用户都希望将360浏览器切换至极速模式。那么具体该如何操作呢?以下是几种简单有效的方法。 方法一:通过地址栏图标一键切换 打开360浏览器后,留意地址栏右侧,会看到一个闪电图标和一个书本图标的组合。其中,闪电代表极速模式,书本则代表兼容模式。只需点…

    2026年9月22日
    100
  • c盘清理工具哪个好用_好用的C盘清理工具推荐与使用评测

    推荐C盘清理方案:系统自带工具如磁盘清理、存储感知和手动清%temp%目录安全可靠,适合日常维护;第三方工具CCleaner、金舟Windows优化大师、风云C盘清理大师和全能C盘清理专家提供一键深度清理,操作便捷且误删率低;空间分析工具WizTree、SpaceSniffer和TreeSize可可…

    2026年9月22日
    000
  • mysql安装完如何诊断 mysql慢查询分析与优化方法

    要解决 mysql 慢查询问题,首先要开启慢查询日志,其次使用 mysqldumpslow 分析日志,再通过 explain 查看执行计划,最后根据常见优化建议改进 sql 和索引。具体步骤如下:一、修改配置文件或动态开启慢查询日志,并设置阈值和路径;二、使用 mysqldumpslow 工具分析慢…

    2026年9月22日
    100
  • 主板供电相数对CPU超频稳定性的影响:14相 vs. 20相实测

    20相供电主板在超频下表现更稳,实测显示其VRM温度更低、电压波动更小、性能输出更一致,尤其适合极限超频和高负载场景,而14相供电配合优质用料也能满足主流超频需求,普通用户无需盲目追求高相数。 主板供电相数直接影响CPU在高负载和超频状态下的电压稳定性和温度控制。很多人在选择主板时会看到“14相”或…

    2026年9月22日
    200
  • PHP如何实现视频留言评论_PHP实现视频留言评论功能

    答案:通过数据库设计、前端表单、后端处理和评论展示四步实现PHP视频留言功能。1. 创建comments表存储信息;2. 构建表单提交昵称与评论;3. 用add_comment.php接收并存入数据库;4. 在页面读取并安全输出评论,防止XSS。 要实现视频留言评论功能,PHP可以结合前端页面、数据…

    2026年9月22日
    000
  • Java中如何区分逻辑错误和系统异常

    系统异常是程序运行中由JVM抛出的RuntimeException,如空指针、数组越界,会导致程序中断并打印堆栈;逻辑错误是程序语法正确但结果不符预期,如条件写反、循环次数错误,不会崩溃但行为异常。两者区别在于是否抛出异常、是否中断执行及调试方式不同,需通过防御性编程、单元测试和日志调试加以防范。 …

    2026年9月22日
    000
  • mysql安装后怎么建表 mysql创建数据表的详细步骤

    mysql安装后怎么建表 mysql创建数据表的详细步骤mysql安装后怎么建表 mysql创建数据表的详细步骤mysql安装后怎么建表 mysql创建数据表的详细步骤mysql安装后怎么建表 mysql创建数据表的详细步骤

    安装完 mysql 后,建表的关键在于先创建数据库并选择使用,然后通过 create table 语句定义表结构。1. 创建数据库:使用 create database mydatabase; 创建数据库;2. 使用数据库:通过 use mydatabase; 选择当前操作的数据库;3. 建表语法:…

    2026年9月22日 用户投稿
    200
  • 夸克浏览器电脑网页版访问入口 夸克官网主页链接地址

    夸克浏览器电脑网页版访问入口是https://www.quark.cn/,用户可直接在浏览器地址栏输入该链接访问,其界面采用极简设计并集成智能搜索、网盘服务与跨设备同步等功能。 立即进入“☞☞☞☞☞点击夸克资源网(永久免费)入口☜☜☜☜☜”; 立即进入“☞☞☞☞☞点击夸克浏览器电脑网页版访问入口☜☜…

    2026年9月22日
    500
  • Spring Boot 应用中的单元测试、Mockito 和集成测试:最佳实践

    第一段引用上面的摘要: 本文旨在帮助初学者理解在 Spring Boot 应用中何时以及如何使用 JUnit、Mockito 和集成测试。我们将探讨这些测试框架在 Controller、Service 和 Repository 层中的应用,并提供示例说明何时使用 Mockito 模拟对象,以及何时使…

    2026年9月22日
    000
  • PHP一键环境如何设置网站域名解析_本地域名解析配置

    配置本地域名需修改hosts文件并设置Web服务器虚拟主机。1. 在hosts添加“127.0.0.1 myproject.test”实现域名解析;2. Apache中编辑httpd-vhosts.conf添加VirtualHost指向项目目录,确保加载该配置文件后重启服务;3. Nginx中在vh…

    2026年9月22日
    700
  • mysql如何输入变量值 mysql交互式代码输入步骤详解

    mysql如何输入变量值 mysql交互式代码输入步骤详解mysql如何输入变量值 mysql交互式代码输入步骤详解mysql如何输入变量值 mysql交互式代码输入步骤详解mysql如何输入变量值 mysql交互式代码输入步骤详解

    在mysql命令行中交互式输入变量值可通过预处理语句或用户自定义变量实现。1. 使用预处理语句时,先用prepare定义含占位符的sql语句,再通过set设置变量值,最后用execute执行并传参,完成后需deallocate释放资源;2. 使用用户自定义变量时,直接通过set赋值并在sql语句中引…

    2026年9月22日 用户投稿
    100
  • Karate框架中处理带方括号和日期范围的GET请求参数

    本文旨在解决Karate框架中构建包含复杂、带方括号(如filters[start_date])及日期范围的GET请求参数时遇到的URL编码问题。通过对比直接定义查询对象和使用param关键字的方法,详细阐述了如何正确地构造URL,确保参数格式符合预期,从而有效进行API测试。 1. 问题背景与挑战…

    2026年9月22日
    000
  • RAID 0阵列对NVMe SSD性能的提升与数据安全风险分析

    RAID 0通过多NVMe SSD并行提升读写性能,理论速度翻倍且显著优化高负载响应,但无冗余导致任一硬盘故障即全阵列崩溃,数据恢复极难,仅建议用于可接受高风险的临时工作或性能优先场景,并必须配合外部备份。 raid 0通过将数据条带化分布在多个存储设备上,理论上可提升读写性能。在搭配nvme ss…

    用户投稿 2026年9月22日
    200

发表回复

登录后才能评论
关注微信