访问修饰符允许我们更改类(或模块)成员的“可见性”和“访问权限”。这些最好通过例子来理解。
JS++有三个访问修饰符:private、protected和public。
私人会员是最不宽容的。如果一个成员被宣布为“私人”,它可以 只有 可以从声明的类或模块访问。下面是一个例子:
class Animal { private string name; string getName() { return name; // OK } } class Dog : Animal { string getName() { return name; // Error } } Animal animal = new Animal(); animal.name; // ERROR animal.getName(); // OK
受保护的成员可以从任何子类或子模块访问。下面是一个例子:
class Animal { protected string name; string getName() { return name; // OK } } class Dog : Animal { string getName() { return name; // OK } } Animal animal = new Animal(); animal.name; // ERROR animal.getName(); // OK
最后,还有“公共”访问修饰符。“public”访问修饰符是最不允许的。声明为“公共”的成员对访问没有限制,甚至可以访问 从课外 (前提是它是从类实例访问的)。下面是一个例子:
class Animal { public string name; string getName() { return name; // OK } } class Dog : Animal { string getName() { return name; // OK } } Animal animal = new Animal(); animal.name; // OK animal.getName(); // OK
访问修饰符启用 封装 封装是面向对象编程的支柱之一(正如我们在本章开头所讨论的),它指的是数据(字段)和对该数据进行操作的方法(例如方法、getter/setter等)的捆绑。简单来说: 隐藏数据 通过将字段设置为私有,并且只允许通过公共/受保护的方法、getter或setter访问它们。
JS++默认访问规则支持封装。在JS++中,字段的默认访问修饰符为“private”。所有其他类成员的默认访问修饰符为“public”。换句话说,JS++访问规则是“成员敏感的”,而通常需要用Java和C#等语言手动指定访问修饰符才能实现封装,这可能会导致代码冗长。
为什么我们需要封装?回想一下我们的getter和setter示例,我们必须定义getter和setter方法来读取和修改cat的“name”字段。假设我们的需求发生了变化,我们想在所有猫的名字前面加上“Kitty”。有了封装,我们只需要更改setter方法。相反,如果我们将字段设置为“public”,并且名称必须通过其实例直接操作,那么我们必须手动将前缀添加到实例对“name”字段的每次直接操作中。随着项目变得越来越复杂,这是不可取的。
现在我们已经对访问修饰符和封装有了明确的理解,让我们回到我们的项目。我们需要我们的“Cat”类以不同于“Animal”基类提供的方式呈现()。第一步是编辑我们的’Animal’基类,使$element字段为’protected’,以便我们的派生类(如’Cat’)可以访问该字段:
external $; module Animals { class Animal { protected var $element = $( """ <div class="animal"> <i class="icofont icofont-animal-cat"></i> </div> """ ); void render() { $("#content").append($element); } } }
接下来,让我们将render()方法恢复为“Cat”:
external $; module Animals { class Cat : Animal { string _name; Cat(string name) { _name = name; } void render() { $element.attr("title", _name); $("#content").append($element); } } }
如果你现在试图编译,你会得到一个编译错误。错误本身应该非常具有描述性:
JSPPE0252:“无效的动物。猫render()’与’void Animals’冲突。动物render()。使用不同的名称创建一个方法,或者使用“overwrite”修饰符
在本例中,我们的派生类(“Cat”)试图定义一个名为“render”的方法,但基类(“Animal”)已经有一个名为“render”的方法。因此,我们有冲突。JS++还建议我们进行修复:A)使用不同的名称创建方法,或B)使用“覆盖”修饰符。
从概念上讲,这两种方法都描述了一个概念:呈现到网页。因此,我们可能不希望用两个不同的名字来描述同一个概念。相反,我们想告诉JS++编译器这是 故意的 通过使用“覆盖”修饰符:
external $; module Animals { class Cat : Animal { string _name; Cat(string name) { _name = name; } overwrite void render() { $element.attr("title", _name); $("#content").append($element); } } }
在其他面向对象语言中,这被称为“方法隐藏”或“方法阴影”JS++这样做的原因是为了防止潜在的错误和打字错误(特别是对于更复杂的类)。如果我们有两个不同的概念,比如“Cat”在内存中呈现,而“Animal”在网页中呈现,那么在这种情况下,我们不应该有相同的方法名。
现在就编译代码。它应该成功。打开网页,你现在应该可以再次将鼠标悬停在猫身上,查看它们的名字。
在这个阶段,我们仍然有代码 复制品 .以下是“动物”render()方法:
void render() { $("#content").append($element); }
下面是我们的“Cat”render()方法:
overwrite void render() { $element.attr("title", _name); $("#content").append($element); }
你注意到重复了吗?如果我们想在以后呈现给ID为“content”的HTML元素之外的其他HTML元素呢?我们必须更改所有相关类中的渲染代码!
我们的“猫”课程“扩展”了“动物”的概念。类似地,我们的猫的render()方法通过添加HTML“title”属性“扩展”了动物的render()方法,这样我们就可以将鼠标移到上面查看名称。然而,除此之外,我们的呈现逻辑是相同的:将元素添加到ID为“content”的HTML元素中。我们可以做得更好。让我们在“猫”类中“重用”来自“动物”类的渲染代码:
external $; module Animals { class Cat : Animal { string _name; Cat(string name) { _name = name; } overwrite void render() { $element.attr("title", _name); super.render(); } } }
编译、运行并观察结果。现在,无论呈现逻辑如何变化,它都将应用于所有相关类。关键是“超级”关键字。“super”关键字指的是当前类的超类。在本例中,我们使用它来访问“Animal”类的“render”方法。如果没有“super”,我们将调用当前类的“render”方法——导致无限递归!(例如,使用“this”而不是“super”将允许您引用“Cat”类的“render”方法……但这将导致无限递归。)
到目前为止,我们已经了解了私有、受保护和公共字段和方法,但是构造函数呢?打开主管道。jspp并添加以下代码:
import Animals; Cat cat1 = new Cat("Kitty"); cat1.render(); Cat cat2 = new Cat("Kat"); cat2.render(); Animal animal = new Animal(); animal.render();
编译并运行。
哦!我们有三只猫出现在页面上。至少当你把鼠标悬停在最后一只猫身上时,它不会显示名字。然而,“动物”不是“猫”(但“猫”是“动物”)。我们之所以有三个猫图标,是因为我们的动物身上有这样的图标。jspp:
protected var $element = $( """ <div class="animal"> <i class="icofont icofont-animal-cat"></i> </div> """ );
换句话说,当我们的$element字段被初始化时,它总是被初始化为一个给我们一个cat图标的值。相反,我们可能希望在“Animal”上定义一个构造函数来参数化这个初始化。让我们换个动物。jspp,以便在构造函数中初始化此字段:
external $; module Animals { class Animal { protected var $element; protected Animal(string iconClassName) { string elementHTML = makeElementHTML(iconClassName); $element = $(elementHTML); } public void render() { $("#content").append($element); } private string makeElementHTML(string iconClassName) { string result = '<div class="animal">'; result += '<i class="icofont ' + iconClassName + '"></i>'; result += "</div>"; return result; } } }
我在所有类成员上添加了访问修饰符,以使代码更清晰。为了清晰起见,我还将HTML文本的构造分离为一个单独的函数。养成实践单一责任原则的习惯:所有班级做一件事,所有函数/方法做一件事。在上面的代码中,我们的构造函数做了一件事:初始化字段;我们的render()方法只做一件事:渲染到网页;最后,我们的“makeElementHTML”方法做了一件事:为元素生成HTML。这导致了 干净代码 ,而JS++设计时考虑到了干净的代码,所以尽量从设计中获益。
你可能注意到的另一个巧妙的技巧是使用 '
(单引号)来包装HTML字符串,如上面的代码所示。这是为了避免逃离 "
(双引号)用于环绕“makeElementHTML”方法中的HTML属性。
您可能已经注意到,所有新的访问修饰符都是不同的:受保护的构造函数、public render()和private makeElementHTML。让我们从最严格的(私人)到最不严格的(公共)。
“makeElementHTML”之所以是私有的,是因为它是一个 实施细节 .makeElementHTML的唯一用途是在我们的“动物”类中。“Cat”类无法访问方法和main。jspp无法访问该方法(通过实例化)。“Cat”类永远不需要调用“makeElementHTML”-相反,“Cat”类继承自“Animal”类。通过继承,“Cat”类将调用“Animal”构造函数。(我们将很快讨论这个问题,因为代码目前无法编译,但首先理解这些概念更重要。)因此,“Cat”类将调用“makeElementHTML” 通过 “Animal”类构造函数,但它无法访问该方法,也无法直接调用它。这样,“makeElementHTML”是“Animal”类的一个实现细节,不暴露于代码的任何其他部分。这种隐藏与其他类和代码无关的细节在面向对象编程中被称为“抽象”。
正如我们在本章开头提到的,抽象是面向对象编程(OOP)的另一个基本支柱。例如,想象一辆汽车。当你踩下汽车的油门时,你不需要知道 怎样 内燃机工作的细节。内部工作的复杂性通过一个简化的界面呈现给你:油门。通过抽象,我们使复杂系统变得简单,这是OOP的一个理想特性。
在私有“makeElementHTML”方法之后,下一个具有访问权限的代码是“受保护”构造函数。同样,“受保护”访问修饰符的限制性不如“私有”,但不如“公共”(除范围外没有访问限制)那么大。
具体来说,它是什么 意思是 使构造函数“受保护”吗?回想一下,“受保护”访问修饰符允许类中的所有成员访问,但也包括所有派生类。还记得,类的实例化执行构造函数中指定的代码。从逻辑上讲,我们可以得出结论,受保护的构造函数意味着类不能在特定上下文之外实例化。
这些具体情况是什么?显而易见的情况是,我们不能从main中实例化“Animal”。jspp。如果你现在就尝试,你会得到一个编译错误。但是,由于“protected”只能从类本身内部访问 以及所有派生类 ,我们代码中受保护构造函数的意图是将类限制为仅继承。回想一下,“Cat”类不能直接调用私有的“makeElementHTML”;此方法在运行期间通过“Animal”构造函数执行 遗产 .在继承过程中,构造函数会像在实例化中一样执行。
如果要将构造函数设置为“私有”,则基本上会阻止类的实例化和继承。(旁注:这就是JS++标准库“System.Math”类的实现方式。)记住:除字段外的所有内容的默认访问规则都是“public”。换句话说,如果我们未指定构造函数的访问修饰符,它将默认为“public”。
我们之前用来访问超类方法的“super”关键字指 例子 在实例化期间创建的超类的。当我们实例化“猫”时,我们也实例化“动物”。所有相关的施工人员将从链的底部开始执行链的上游。在我们的例子中,当“Cat”被实例化时,我们首先执行“Cat”构造函数,然后向上移动继承链,然后执行“Animal”类构造函数。(JS++使用“统一类型系统”,其中“system.Object”是所有内部类型的根,因此也将调用该类的构造函数——但前提是确定它是必需的,并且不是“死代码消除”的候选对象——但这超出了本章的范围,将在标准库章节中讨论。)
知道了构造函数在继承过程中被调用,我们现在可以解决代码中的剩余问题:您会注意到代码当前没有编译。原因是,在定义自定义构造函数时,我们已经停止使用“Animal”类的隐式默认构造函数。
我们的“动物”构造函数采用一个参数:
protected Animal(string iconClassName) { string elementHTML = makeElementHTML(iconClassName); $element = $(elementHTML); }
我们需要更改“Cat”构造函数代码,以便指定 怎样 应该调用超类构造函数。我们可以通过“super”关键字再次执行此操作。“Animal”类想知道我们想要呈现的动物类型的图标名。如果您不记得图标的名称,为了方便起见,我将其包含在“超级”通话中:
external $; module Animals { class Cat : Animal { string _name; Cat(string name) { super("icofont-animal-cat"); _name = name; } overwrite void render() { $element.attr("title", _name); super.render(); } } }
对“super”关键字的函数调用将执行超类的相关构造函数。“super”调用必须始终是第一条语句,因为在语义上,超类的构造函数将在其派生类的构造函数代码之前执行。
最后,我们需要修改main。jspp删除“Animal”类的实例化。请记住,由于我们将“Animal”构造函数设置为“protected”,因此无法从main中实例化“Animal”。jspp无论如何:
import Animals; Cat cat1 = new Cat("Kitty"); cat1.render(); Cat cat2 = new Cat("Kat"); cat2.render();
此时,您可以编译,项目应该可以成功编译。再一次,我们应该有两只猫:
最后,我们可以添加更多的动物。
狗jspp:
external $; module Animals { class Dog : Animal { string _name; Dog(string name) { super("icofont-animal-dog"); _name = name; } overwrite void render() { $element.attr("title", _name); super.render(); } } }
狗jspp非常像猫。因为狗也是需要名字的家养动物。
熊猫jspp:
external $; module Animals { class Panda : Animal { Panda() { super("icofont-animal-panda"); } } }
不像猫。jspp和狗。杰斯普,熊猫。jspp要简单得多。“Panda”类所做的就是从“Animal”继承并指定要渲染的图标。它没有名字,其render()方法与Animal完全相同,因为它不必在鼠标上方添加HTML“title”属性来显示名称。
犀牛jspp:
external $; module Animals { class Rhino : Animal { Rhino() { super("icofont-animal-rhino"); } } }
就像熊猫一样。jspp,犀牛。jspp也是一个非常简单的类。它只是从“Animal”继承而来,不需要设置或呈现名称。
最后,修改main。jspp将对新动物进行实例化:
import Animals; Cat cat1 = new Cat("Kitty"); cat1.render(); Cat cat2 = new Cat("Kat"); cat2.render(); Dog dog = new Dog("Fido"); dog.render(); Panda panda = new Panda(); panda.render(); Rhino rhino = new Rhino(); rhino.render();
像这样编译整个项目:
$ js++ src/ -o build/app.jspp.js
再一次,在所有平台(Windows、Mac和Linux)上,我们都是从命令行操作的,因此编译指令对每个人都应该是相同的。此外,完全没有必要指定“编译顺序”“猫”依赖于“动物”并不重要,因此“动物”。jspp应在Cat之前处理。jspp’。JS++会自动为您解析编译顺序,即使是最复杂的项目(例如,使用循环导入和复杂依赖项)。只需指定输入目录,让JS++递归地查找输入文件并确定编译顺序。
开放索引。网页浏览器中的html。结果应该是这样的:
确认你的两只猫有名字,你的狗有名字,但熊猫和犀牛应该有名字 不 当你把鼠标悬停在上面时,有名字。
如果一切顺利:恭喜!此时,您可能已经注意到,我们可以如下更改继承层次结构:
Animal |_ DomesticatedAnimal |_ Cat |_ Dog |_ WildAnimal |_ Panda |_ Rhino
然而,这是留给读者的练习。
现在我们已经讨论了OOP的四个基本概念中的三个:抽象、封装和继承。OOP的最后一个基本支柱是多态性,我们将在下一节介绍它。