Class
基本语法
概述
- 在日常开发中,我们经常需要创建许多相同类型的对象,例如用户(
users
)、商品(goods
)或者任何其他东西new function
可以帮助我们实现这种需求
- 但在现代
JavaScript
中,还有一个更高级的“类(class
)”构造方式,它引入许多非常棒的新功能,这些功能对于面向对象编程很有用
语法
- 基本语法
1 2 3 4 5 6 7 8 |
class MyClass { // class 方法 constructor() { ... } method1() { ... } method2() { ... } method3() { ... } ... } |
- 然后使用
new MyClass()
来创建具有上述列出的所有方法的新对象new
会自动调用constructor()
方法,因此我们可以在constructor()
中初始化对象
- 当
new User("John")
被调用:- 一个新对象被创建
constructor
使用给定的参数运行,并将其赋值给this.name
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class User { constructor(name) { this.name = name; } sayHi() { alert(this.name); } } // 用法: let user = new User("John"); user.sayHi(); |
什么是 class
?
- 在
JavaScript
中,类是一种函数
1 2 3 4 5 6 7 |
class User { constructor(name) { this.name = name; } sayHi() { alert(this.name); } } // 佐证:User 是一个函数 alert(typeof User); // function |
class User {...}
构造实际上做了如下的事儿:- 创建一个名为
User
的函数,该函数成为类声明的结果
该函数的代码来自于constructor
方法(如果我们不编写这种方法,那么它就被假定为空) - 存储类中的方法,例如
User.prototype
中的sayHi
- 创建一个名为
- 当
new User
对象被创建后,当我们调用其方法时,它会从原型中获取对应的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class User { constructor(name) { this.name = name; } sayHi() { alert(this.name); } } // class 是一个函数 alert(typeof User); // function // ...或者,更确切地说,是 constructor 方法 alert(User === User.prototype.constructor); // true // 方法在 User.prototype 中,例如: alert(User.prototype.sayHi); // sayHi 方法的代码 // 在原型中实际上有两个方法 alert(Object.getOwnPropertyNames(User.prototype)); // constructor, sayHi |
不仅仅是语法糖
- 人们常说
class
是一个语法糖(旨在使内容更易阅读,但不引入任何新内容的语法)- 因为我们实际上可以在不使用
class
的情况下声明相同的内容: - 这个定义的结果与使用类得到的结果基本相同
- 因此,这确实是将
class
视为一种定义构造器及其原型方法的语法糖的理由
- 因为我们实际上可以在不使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// 用纯函数重写 class User // 1. 创建构造器函数 function User(name) { this.name = name; } // 函数的原型(prototype)默认具有 "constructor" 属性, // 所以,我们不需要创建它 // 2. 将方法添加到原型 User.prototype.sayHi = function() { alert(this.name); }; // 用法: let user = new User("John"); user.sayHi(); |
- 尽管,它们之间存在着重大差异:
- 首先,通过
class
创建的函数具有特殊的内部属性标记[[IsClassConstructor]]: true
因此,它与手动创建并不完全相同 - 类方法不可枚举
类定义将"prototype"
中的所有方法的enumerable
标志设置为false
- 类总是使用
use strict
在类构造中的所有代码都将自动进入严格模式
- 首先,通过
1 2 3 4 5 6 |
class User { constructor() {} } alert(typeof User); // function User(); // Error: Class constructor User cannot be invoked without 'new' |
1 2 3 4 5 |
class User { constructor() {} } alert(User); // class User { ... } |
类表达式
- 就像函数一样,类可以在另外一个表达式中被定义,被传递,被返回,被赋值等
1 2 3 4 5 |
let User = class { sayHi() { alert("Hello"); } }; |
- 类似于命名函数表达式(
Named Function Expressions
),类表达式可能也应该有一个名字- 如果类表达式有名字,那么该名字仅在类内部可见:
1 2 3 4 5 6 7 8 9 10 11 |
// “命名类表达式(Named Class Expression)” // (规范中没有这样的术语,但是它和命名函数表达式类似) let User = class MyClass { sayHi() { alert(MyClass); // MyClass 这个名字仅在类内部可见 } }; new User().sayHi(); // 正常运行,显示 MyClass 中定义的内容 alert(MyClass); // error,MyClass 在外部不可见 |
- 我们甚至可以动态地“按需”创建类,就像这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function makeClass(phrase) { // 声明一个类并返回它 return class { sayHi() { alert(phrase); } }; } // 创建一个新的类 let User = makeClass("Hello"); new User().sayHi(); // Hello |
Getters/setters
- 就像对象字面量,类可能包括
getters/setters
,计算属性(computed properties
)等 - 这是一个使用
get/set
实现user.name
的示例:- 从技术上来讲,这样的类声明可以通过在
User.prototype
中创建getters
和setters
来实现
- 从技术上来讲,这样的类声明可以通过在
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
class User { constructor(name) { // 调用 setter this.name = name; } get name() { return this._name; } set name(value) { if (value.length < 4) { alert("Name is too short."); return; } this._name = value; } } let user = new User("John"); alert(user.name); // John user = new User(""); // Name is too short. |
计算属性名称 […]
- 这里有一个使用中括号
[...]
的计算方法名称示例:
1 2 3 4 5 6 7 8 9 |
class User { ['say' + 'Hi']() { alert("Hello"); } } new User().sayHi(); |
Class
字段
- “类字段”是一种允许添加任何属性的语法
- 例如,让我们在
class User
中添加一个name
属性:
- 例如,让我们在
1 2 3 4 5 6 7 8 9 |
class User { name = "John"; sayHi() { alert(`Hello, ${this.name}!`); } } new User().sayHi(); // Hello, John! |
- 类字段的重要区别在于,它们会被挂在实例对象上,而非
User.prototype
上:
1 2 3 4 5 6 7 |
class User { name = "John"; } let user = new User(); alert(user.name); // John alert(User.prototype.name); // undefined |
- 也可以在赋值时使用更复杂的表达式和函数调用:
1 2 3 4 5 6 |
class User { name = prompt("Name, please?", "John"); } let user = new User(); alert(user.name); // John |
- 使用类字段制作绑定方法
JavaScript
中的函数具有动态的this
。它取决于调用上下文- 因此,如果一个对象方法被传递到某处,或者在另一个上下文中被调用,则
this
将不再是对其对象的引用 - 例如,此代码将显示
undefined
: - 这个问题被称为“丢失
this
”
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Button { constructor(value) { this.value = value; } click() { alert(this.value); } } let button = new Button("hello"); setTimeout(button.click, 1000); // undefined |
- 有两种可以修复它的方式:
- 传递一个包装函数,例如
setTimeout(() => button.click(), 1000)
- 将方法绑定到对象,例如在
constructor
中
- 传递一个包装函数,例如
- 类字段提供了另一种非常优雅的语法:
- 类字段
click = () => {...}
是基于每一个对象被创建的,在这里对于每一个Button
对象都有一个独立的方法,在内部都有一个指向此对象的this
- 我们可以把
button.click
传递到任何地方,而且this
的值总是正确的 - 在浏览器环境中,它对于进行事件监听尤为有用
- 类字段
1 2 3 4 5 6 7 8 9 10 11 12 |
class Button { constructor(value) { this.value = value; } click = () => { alert(this.value); } } let button = new Button("hello"); setTimeout(button.click, 1000); // hello |
类继承
概述
- 类继承是一个类扩展另一个类的一种方式
- 因此,我们可以在现有功能之上创建新功能
“extends
” 关键字
- 假设我们有
class
Animal
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Animal { constructor(name) { this.speed = 0; this.name = name; } run(speed) { this.speed = speed; alert(`${this.name} runs with speed ${this.speed}.`); } stop() { this.speed = 0; alert(`${this.name} stands still.`); } } let animal = new Animal("My animal"); |
- 然后我们想创建另一个
class Rabbit
:- 因为
rabbit
是animal
,所以 classRabbit
应该是基于class
Animal
的,可以访问animal
的方法,以便rabbit
可以做“一般”动物可以做的事儿 - 在内部,关键字
extends
使用了很好的旧的原型机制进行工作 - 它将
Rabbit.prototype.[[Prototype]]
设置为Animal.prototype
- 所以,如果在
Rabbit.prototype
中找不到一个方法,JavaScript
就会从Animal.prototype
中获取该方法
- 因为
1 2 3 4 5 6 7 8 9 10 |
class Rabbit extends Animal { hide() { alert(`${this.name} hides!`); } } let rabbit = new Rabbit("White Rabbit"); rabbit.run(5); // White Rabbit runs with speed 5. rabbit.hide(); // White Rabbit hides! |
注意1
- 在
extends
后允许任意表达式- 类语法不仅允许指定一个类,在
extends
后可以指定任意表达式 - 例如,一个生成父类的函数调用:
- 类语法不仅允许指定一个类,在
1 2 3 4 5 6 7 8 9 |
function f(phrase) { return class { sayHi() { alert(phrase); } }; } class User extends f("Hello") {} new User().sayHi(); // Hello |
重写方法
- 默认情况下,所有未在
class Rabbit
中指定的方法均从class Animal
中直接获取 - 但是如果我们在
Rabbit
中指定了我们自己的方法,例如stop()
,那么将会使用它:
1 2 3 4 5 6 |
class Rabbit extends Animal { stop() { // ……现在这个将会被用作 rabbit.stop() // 而不是来自于 class Animal 的 stop() } } |
- 然而通常,我们不希望完全替换父类的方法,而是希望在父类方法的基础上进行调整或扩展其功能
- 我们在我们的方法中做一些事儿,但是在它之前或之后或在过程中会调用父类方法
Class
为此提供了"super"
关键字- 执行
super.method(...)
来调用一个父类方法 - 执行
super(...)
来调用一个父类constructor
(只能在我们的constructor
中)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
class Animal { constructor(name) { this.speed = 0; this.name = name; } run(speed) { this.speed = speed; alert(`${this.name} runs with speed ${this.speed}.`); } stop() { this.speed = 0; alert(`${this.name} stands still.`); } } class Rabbit extends Animal { hide() { alert(`${this.name} hides!`); } stop() { super.stop(); // 调用父类的 stop this.hide(); // 然后 hide } } let rabbit = new Rabbit("White Rabbit"); rabbit.run(5); // White Rabbit runs with speed 5. rabbit.stop(); // White Rabbit stands still. White Rabbit hides! |
注意2
- 箭头函数没有
super
- 如果被访问,它会从外部函数获取。例如:
1 2 3 4 5 |
class Rabbit extends Animal { stop() { setTimeout(() => super.stop(), 1000); // 1 秒后调用父类的 stop } } |
- 箭头函数中的
super
与stop()
中的是一样的,所以它能按预期工作。如果我们在这里指定一个“普通”函数,那么将会抛出错误:
1 2 |
// 意料之外的 super setTimeout(function() { super.stop() }, 1000); |
重写constructor
- 对于重写 constructor 来说,则有点棘手
- 到目前为止,
Rabbit
还没有自己的constructor
- 根据 规范,如果一个类扩展了另一个类并且没有
constructor
,那么将生成下面这样的“空”constructor
:
- 到目前为止,
1 2 3 4 5 6 |
class Rabbit extends Animal { // 为没有自己的 constructor 的扩展类生成的 constructor(...args) { super(...args); } } |
- 正如我们所看到的,它调用了父类的
constructor
,并传递了所有的参数。如果我们没有写自己的constructor
,就会出现这种情况- 现在,我们给
Rabbit
添加一个自定义的constructor
- 得到了一个报错,是什么地方出错了?
- 继承类的
constructor
必须调用super(...)
,并且一定要在使用this
之前调用 - 为什么呢
- 现在,我们给
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class Animal { constructor(name) { this.speed = 0; this.name = name; } // ... } class Rabbit extends Animal { constructor(name, earLength) { this.speed = 0; this.name = name; this.earLength = earLength; } // ... } // 不工作! let rabbit = new Rabbit("White Rabbit", 10); // Error: this is not defined. |
- 在
JavaScript
中,继承类(所谓的“派生构造器”,英文为 “derived constructor
”)的构造函数与其他函数之间是有区别的- 派生构造器具有特殊的内部属性
[[ConstructorKind]]:"derived"
- 这是一个特殊的内部标签
- 该标签会影响它的
new
行为:
当通过new
执行一个常规函数时,它将创建一个空对象,并将这个空对象赋值给this
但是当继承的constructor
执行时,它不会执行此操作。它期望父类的constructor
来完成这项工作 - 因此,派生的
constructor
必须调用super
才能执行其父类(base
)的constructor
,否则this
指向的那个对象将不会被创建。并且我们会收到一个报错
- 派生构造器具有特殊的内部属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
class Animal { constructor(name) { this.speed = 0; this.name = name; } // ... } class Rabbit extends Animal { constructor(name, earLength) { super(name); this.earLength = earLength; } // ... } // 现在可以了 let rabbit = new Rabbit("White Rabbit", 10); alert(rabbit.name); // White Rabbit alert(rabbit.earLength); // 10 |
重写类字段
- 我们不仅可以重写方法,还可以重写类字段
- 不过,当我们在父类构造器中访问一个被重写的字段时,有一个诡异的行为,这与绝大多数其他编程语言都很不一样
- 这里,
Rabbit
继承自Animal
,并且用它自己的值重写了name
字段 - 因为
Rabbit
中没有自己的构造器,所以Animal
的构造器被调用了 - 当父类构造器在派生的类中被调用时,它会使用被重写的方法。但对于类字段并非如此
Rabbit
是派生类,里面没有constructor()
。正如先前所说,这相当于一个里面只有super(...args)
的空构造器- 所以,
new Rabbit()
调用了super()
,因此它执行了父类构造器,并且(根据派生类规则)只有在此之后,它的类字段才被初始化 - 在父类构造器被执行的时候,
Rabbit
还没有自己的类字段,这就是为什么Animal
类字段被使用了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class Animal { name = 'animal'; constructor() { alert(this.name); // (*) } } class Rabbit extends Animal { name = 'rabbit'; } new Animal(); // animal new Rabbit(); // animal |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class Animal { showName() { // 而不是 this.name = 'animal' alert('animal'); } constructor() { this.showName(); // 而不是 alert(this.name); } } class Rabbit extends Animal { showName() { alert('rabbit'); } } new Animal(); // animal new Rabbit(); // rabbit |
- 这里为什么会有这样的区别呢?
- 实际上,原因在于字段初始化的顺序
- 类字段是这样初始化的:
- 对于基类(还未继承任何东西的那种),在构造函数调用前初始化
- 对于派生类,在
super()
后立刻初始化
深入:内部探究和 [[HomeObject]]
- 在下面的例子中,
rabbit.__proto__ = animal
。现在让我们尝试一下:在rabbit.eat()
我们将会使用this.__proto__
调用animal.eat()
:- 在
(*)
这一行,我们从原型(animal
)中获取eat
,并在当前对象的上下文中调用它 - 注意,
.call(this)
在这里非常重要,因为简单的调用this.__proto__.eat()
将在原型的上下文中执行eat
,而非当前对象 - 代码确实按照了期望运行:我们获得了正确的
alert
- 在
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
let animal = { name: "Animal", eat() { alert(`${this.name} eats.`); } }; let rabbit = { __proto__: animal, name: "Rabbit", eat() { // 这就是 super.eat() 可以大概工作的方式 this.__proto__.eat.call(this); // (*) } }; rabbit.eat(); // Rabbit eats. |
- 现在,让我们在原型链上再添加一个对象。我们将看到这件事是如何被打破的:
- 代码无法再运行了
- 在
(*)
和(**)
这两行中,this
的值都是当前对象(longEar
) - 这是至关重要的一点:所有的对象方法都将当前对象作为
this
,而非原型或其他什么东西 - 因此,在
(*)
和(**)
这两行中,this.__proto__
的值是完全相同的:都是rabbit
。它们俩都调用的是rabbit.eat
,它们在不停地循环调用自己,而不是在原型链上向上寻找方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
let animal = { name: "Animal", eat() { alert(`${this.name} eats.`); } }; let rabbit = { __proto__: animal, eat() { // ...bounce around rabbit-style and call parent (animal) method this.__proto__.eat.call(this); // (*) } }; let longEar = { __proto__: rabbit, eat() { // ...do something with long ears and call parent (rabbit) method this.__proto__.eat.call(this); // (**) } }; longEar.eat(); // Error: Maximum call stack size exceeded |
[[HomeObject]]
- 为了提供解决方法,
JavaScript
为函数添加了一个特殊的内部属性:[[HomeObject]]
- 当一个函数被定义为类或者对象方法时,它的
[[HomeObject]]
属性就成为了该对象 - 然后
super
使用它来解析(resolve
)父原型及其方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
let animal = { name: "Animal", eat() { // animal.eat.[[HomeObject]] == animal alert(`${this.name} eats.`); } }; let rabbit = { __proto__: animal, name: "Rabbit", eat() { // rabbit.eat.[[HomeObject]] == rabbit super.eat(); } }; let longEar = { __proto__: rabbit, name: "Long Ear", eat() { // longEar.eat.[[HomeObject]] == longEar super.eat(); } }; // 正确执行 longEar.eat(); // Long Ear eats. |
方法并不是“自由”的
- 正如我们之前所知道的,函数通常都是“自由”的,并没有绑定到
JavaScript
中的对象- 正因如此,它们可以在对象之间复制,并用另外一个
this
调用它
- 正因如此,它们可以在对象之间复制,并用另外一个
[[HomeObject]]
的存在违反了这个原则,因为方法记住了它们的对象[[HomeObject]]
不能被更改,所以这个绑定是永久的
- 在
JavaScript
语言中[[HomeObject]]
仅被用于super
- 所以,如果一个方法不使用
super
,那么我们仍然可以视它为自由的并且可在对象之间复制 - 但是用了
super
再这样做可能就会出错
- 所以,如果一个方法不使用
- 下面是复制后错误的
super
结果的示例:- 调用
tree.sayHi()
显示 “I’m an animal
”。这绝对是错误的 - 原因很简单:
- 在
(*)
行,tree.sayHi
方法是从rabbit
复制而来。也许我们只是想避免重复代码? - 它的
[[HomeObject]]
是rabbit
,因为它是在rabbit
中创建的。没有办法修改[[HomeObject]]
tree.sayHi()
内具有super.sayHi()
。它从rabbit
中上溯,然后从animal
中获取方法
- 调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
let animal = { sayHi() { alert(`I'm an animal`); } }; // rabbit 继承自 animal let rabbit = { __proto__: animal, sayHi() { super.sayHi(); } }; let plant = { sayHi() { alert("I'm a plant"); } }; // tree 继承自 plant let tree = { __proto__: plant, sayHi: rabbit.sayHi // (*) }; tree.sayHi(); // I'm an animal (?!?) |
方法,不是函数属性
[[HomeObject]]
是为类和普通对象中的方法定义的- 但是对于对象而言,方法必须确切指定为
method()
,而不是"method: function()"
- 这个差别对我们来说可能不重要,但是对
JavaScript
来说却非常重要
- 但是对于对象而言,方法必须确切指定为
- 在下面的例子中,使用非方法(
non-method
)语法进行了比较。未设置[[HomeObject]]
属性,并且继承无效:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
let animal = { eat: function() { // 这里是故意这样写的,而不是 eat() {... // ... } }; let rabbit = { __proto__: animal, eat: function() { super.eat(); } }; rabbit.eat(); // 错误调用 super(因为这里没有 [[HomeObject]]) |
静态属性和静态方法
概述
- 我们还可以为整个类分配一个方法。这样的方法被称为 静态的(
static
) - 在一个类的声明中,它们以
static
关键字开头,如下所示:
1 2 3 4 5 6 7 |
class User { static staticMethod() { alert(this === User); } } User.staticMethod(); // true |
- 这实际上跟直接将其作为属性赋值的作用相同:
1 2 3 4 5 6 7 |
class User { } User.staticMethod = function() { alert(this === User); }; User.staticMethod(); // true |
- 在
User.staticMethod()
调用中的this
的值是类构造器User
自身(“点符号前面的对象”规则)- 通常,静态方法用于实现属于整个类,但不属于该类任何特定对象的函数
- 例如,我们有对象
Article
,并且需要一个方法来比较它们 - 通常的解决方案就是添加
Article.compare
静态方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class Article { constructor(title, date) { this.title = title; this.date = date; } static compare(articleA, articleB) { return articleA.date - articleB.date; } } // 用法 let articles = [ new Article("HTML", new Date(2019, 1, 1)), new Article("CSS", new Date(2019, 0, 1)), new Article("JavaScript", new Date(2019, 11, 1)) ]; articles.sort(Article.compare); alert( articles[0].title ); // CSS |
- 另一个例子是所谓的“工厂”方法
- 比如说,我们需要通过多种方式来创建一篇文章:
- 通过用给定的参数来创建(
title
,date
等)。 - 使用今天的日期来创建一个空的文章。
- 其它方法
- 第一种方法我们可以通过
constructor
来实现。对于第二种方式,我们可以创建类的一个静态方法来实现- 现在,每当我们需要创建一个今天的文章时,我们就可以调用
Article.createTodays()
再说明一次,它不是一个文章的方法,而是整个class
的方法
- 现在,每当我们需要创建一个今天的文章时,我们就可以调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class Article { constructor(title, date) { this.title = title; this.date = date; } static createTodays() { // 记住 this = Article return new this("Today's digest", new Date()); } } let article = Article.createTodays(); alert( article.title ); // Today's digest |
注意
- 静态方法不适用于单个对象
- 静态方法可以在类上调用,而不是在单个对象上
1 2 |
// ... article.createTodays(); /// Error: article.createTodays is not a function |
静态属性
- 这是一个最近添加到
JavaScript
的特性 - 静态的属性也是可能的,它们看起来就像常规的类属性,但前面加有
static
:- 这等同于直接给
Article
赋值:
- 这等同于直接给
1 2 3 4 5 |
class Article { static publisher = "Levi Ding"; } alert( Article.publisher ); // Levi Ding |
1 |
Article.publisher = "Levi Ding"; |
继承静态属性和方法
- 静态属性和方法是可被继承的
- 例如,下面这段代码中的
Animal.compare
和Animal.planet
是可被继承的,可以通过Rabbit.compare
和Rabbit.planet
来访问:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
class Animal { static planet = "Earth"; constructor(name, speed) { this.speed = speed; this.name = name; } run(speed = 0) { this.speed += speed; alert(`${this.name} runs with speed ${this.speed}.`); } static compare(animalA, animalB) { return animalA.speed - animalB.speed; } } // 继承于 Animal class Rabbit extends Animal { hide() { alert(`${this.name} hides!`); } } let rabbits = [ new Rabbit("White Rabbit", 10), new Rabbit("Black Rabbit", 5) ]; rabbits.sort(Rabbit.compare); rabbits[0].run(); // Black Rabbit runs with speed 5. alert(Rabbit.planet); // Earth |
本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!