JS++ 访问修饰符和’super’

JS++ 访问修饰符和’super’

访问修饰符允许我们改变一个类(或模块)成员的 “可见性 “和 “访问权限”。这些最好通过例子来理解。

JS++有三个访问修饰符:私有、保护和公共。

一个私有的成员是最不允许的。如果一个成员被声明为 “私有”,它只能从它被声明的类或模块中访问。这里有一个例子。

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/setters等)的捆绑。更简单地说:通过使字段私有化来隐藏你的数据,并且只通过公共/受保护的方法、获取器或设置器来访问它们。

JS++的默认访问规则实现了封装。在JS++中,字段有一个默认的访问修饰符 “private”。所有其他类成员的默认访问修饰语为 “公共”。换句话说,JS++的访问规则是 “成员敏感的”,而在像Java和C#这样的语言中,你通常需要手动指定访问修饰符来实现封装,这可能会导致冗长的代码。

为什么我们需要封装?回想一下我们的getter和setter例子,我们必须定义getter和setter方法来读取和修改我们的猫的 “name “字段。假设我们的需求发生了变化,我们想在所有的猫的名字前加上 “Kitty”。通过封装,我们只需要改变我们的setter方法。相反,如果我们把我们的字段做成 “公共 “的,并且名字必须通过它的实例直接操作,那么我们就必须在每次由实例直接操作 “名字 “字段时手动添加前缀。随着项目复杂性的增加,这将是不可取的。

现在我们已经对访问修饰符和封装有了坚定的理解,让我们回到我们的项目。我们需要我们的’猫’类以不同于’动物’基类的方式渲染()。第1步是编辑’Animal’基类,使$element字段成为’protected’,这样该字段就可以被我们的派生类(如’Cat’)访问。

external ;

module Animals
{
    class Animal
    {
        protected varelement = (
            """
            <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: void Animals.Cat.render()' 与void Animals.Animal.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);
}

这里是我们的 “猫 “渲染()方法。

overwrite void render() {
    element.attr("title", _name);("#content").append($element);
}

你注意到这个重复了吗?如果我们以后想渲染到除了ID为 “content “的那个元素之外的其他HTML元素,该怎么办?我们将不得不改变所有相关类中的渲染代码

我们的 “猫 “类 “扩展 “了 “动物 “的概念。同样,我们的 “猫 “的render()方法也 “扩展 “了 “动物 “的render()方法,增加了HTML的 “标题 “属性,这样我们就可以把鼠标放在上面,看到它的名字。然而,除此之外,我们的渲染逻辑是相同的:将元素添加到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 “关键字。super “关键字指的是当前类的超类。在本例中,我们用它来访问 “动物 “类的 “渲染 “方法。如果没有’super’,我们就会调用当前类的’render’方法–导致无限的递归(例如,使用’this’而不是’super’将允许你引用’Cat’类的’render’方法……但这将导致无限的递归)。

到目前为止,我们已经了解了私有、保护和公共字段和方法,但构造函数呢?打开main.jspp,添加以下代码。

import Animals;

Cat cat1 = new Cat("Kitty");
cat1.render();
Cat cat2 = new Cat("Kat");
cat2.render();
Animal animal = new Animal();
animal.render();

编译和运行。

呃,哦!我们已经有三只猫呈现在页面上了。至少当你把鼠标悬停在最后一只猫身上时,它没有显示名字。然而,”动物 “不是 “猫”(但 “猫 “是 “动物”)。我们之所以有三个猫咪图标,是因为我们在Animal.jspp中有这个。

protected var element =(
    """
    <div class="animal">
        <i class="icofont icofont-animal-cat"></i>
    </div>
    """
);

换句话说,当我们的$element字段被初始化时,它总是被初始化为一个值,给我们一个猫的图标。相反,我们可能想在’Animal’上定义一个构造函数,将这个初始化参数化。让我们改变Animal.jspp,使这个字段在构造函数中被初始化。

external ;

module Animals
{
    class Animal
    {
        protected varelement;

        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属性的”(双引号)。

你可能已经注意到,所有新的访问修改器都是不同的:protected constructor, public render(), and private makeElementHTML。让我们从最严格的(private)到最不严格的(public)来分析一下。

makeElementHTML “之所以是私有的,是因为它是一个实现细节。makeElementHTML’的唯一用途是在我们的’动物’类中。猫 “类不能访问该方法,main.jspp也不能访问该方法(通过实例化)。猫 “类永远不需要调用 “makeElementHTML”–相反,”猫 “类继承于 “动物 “类。通过继承,’猫’类将调用’动物’构造函数。(我们很快就会讨论这个问题,因为目前代码还不能编译,但首先要理解这些概念才是最重要的。)因此,’猫’类将通过’动物’类的构造函数调用’makeElementHTML’,但它无法访问该方法,也不能直接调用它。这样一来,’makeElementHTML’就是’动物’类的一个实现细节,没有暴露在我们代码的任何其他部分。这种隐藏与其他类和代码无关的细节的做法,在面向对象编程中被称为 “抽象”。

正如我们在本章开头提到的,抽象是面向对象编程(OOP)的另一个基本支柱。例如,想象一下一辆汽车。当你踩下汽车的油门踏板时,你不需要知道内燃机的具体工作情况。复杂的内部工作原理通过一个简化的界面呈现给你:油门踏板。通过抽象,我们使复杂的系统变得简单,这是OOP的一个理想属性。

在私有的’makeElementHTML’方法之后,下一个有访问权限的代码是’protected’构造函数。再一次,’protected’访问修饰符比’private’的限制性要小,但不像’public’那样放任自流(除了范围之外没有任何访问限制)。

具体来说,让构造函数 “受保护 “是什么意思?回顾一下,’protected’访问修饰符允许类内的所有成员访问,但也包括所有派生类。还记得吗,一个类的实例化会执行构造函数中指定的代码。从逻辑上讲,我们可以得出结论,一个受保护的构造函数意味着一个类不能在特定的上下文之外被实例化。

这些特定的上下文是什么呢?一个明显的例子是,我们不能从main.jspp中实例化’Animal’,如果你现在尝试,你会得到一个编译错误。然而,由于’protected’只能从类本身和所有派生类中访问,我们代码中的protected构造函数的意图是限制该类只能继承。回顾一下,’猫’类不能直接调用私有的’makeElementHTML’;这个方法在继承过程中通过’动物’构造函数执行。在继承过程中,构造函数就像实例化时一样被执行。

如果你把构造函数设为 “私有”,你将从本质上阻止该类的实例化和继承。(题外话:JS++标准库的’System.Math’类就是这样实现的。)记住:除了字段之外,所有东西的默认访问规则都是’public’。换句话说,如果我们没有为我们的构造函数指定访问修饰符,它就会默认为 “公共”。

我们之前用来访问超类方法的’super’关键字是指在实例化过程中创建的超类实例。当我们实例化’猫’时,我们也实例化了’动物’。所有相关的构造函数将从链的底部开始向上执行。在我们的例子中,当’猫’被实例化时,我们首先执行’猫’的构造函数,然后我们沿着继承链向上移动,接下来执行’动物’类的构造函数。(JS++使用一个 “统一类型系统”,其中’System.Object’是所有内部类型的根,所以这个类的构造函数也会被调用–但只有当它被确定为是必要的,并且不是 “死代码消除 “的候选者–但这不属于本章的范围,将在标准库章节中讨论。)

知道了构造函数在继承过程中会被调用,我们现在可以解决代码中剩下的问题:你会注意到,目前代码不能编译了。原因是我们在定义自定义构造函数时,已经停止使用’Animal’类的隐式默认构造函数。

我们的’动物’构造函数需要一个参数。

protected Animal(string iconClassName) {
    string elementHTML = makeElementHTML(iconClassName);
    element =(elementHTML);
}

我们需要改变’Cat’构造函数的代码,以便指定如何调用超类的构造函数。我们可以再次通过’super’关键字来做这件事。动物’类想要知道我们想要呈现的动物类型的图标名称。如果你不记得图标的名字,为了方便你,我在这里把它包含在’super’调用中。

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’的构造函数设为’protected’,所以无论如何我们都无法从main.jspp中实例化’Animal’。

import Animals;

Cat cat1 = new Cat("Kitty");
cat1.render();
Cat cat2 = new Cat("Kat");
cat2.render();

在这一点上,你可以进行编译,项目应该编译成功。再一次,我们应该有了这两只猫。

JS++  访问修饰符和super

最后,我们可以添加更多的动物。

Dog.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();
        }
    }
}

Dog.jspp与Cat.jspp非常相似,因为狗也是一种需要名字的驯养动物。

Panda.jspp。

external $;

module Animals
{
    class Panda : Animal
    {
        Panda() {
            super("icofont-animal-panda");
        }
    }
}

与Cat.jspp和Dog.jspp不同,Panda.jspp明显更简单。熊猫 “类所做的就是继承 “动物”,并指定要渲染的图标。它没有名字,它的render()方法和Animal的完全一样,因为它不需要在鼠标移动时添加HTML’title’属性来显示名字。

Rhino.jspp:

external $;

module Animals
{
    class Rhino : Animal
    {
        Rhino() {
            super("icofont-animal-rhino");
        }
    }
}

就像Panda.jspp,Rhino.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’应该在’猫.jspp’之前被处理。JS++会自动为你解决编译顺序问题,即使是最复杂的项目(例如,有循环导入和复杂的依赖关系)。只要指定输入目录,让Js++递归地找到输入文件并找出编译顺序。

在你的网络浏览器中打开index.html。结果应该是这样的。

JS++  访问修饰符和'super'。

验证你的两只猫有名字,你的狗有名字,但当你把鼠标悬停在熊猫和犀牛身上时,熊猫和犀牛应该没有名字。

如果一切正常的话:恭喜你!你的猫和狗都有名字了。在这一点上,你可能已经注意到我们可以改变我们的继承层次结构,如下。

    Animal
    |_ DomesticatedAnimal
        |_ Cat
        |_ Dog
    |_ WildAnimal
        |_ Panda
        |_ Rhino

然而,这是留给读者的一个练习。

我们现在已经涵盖了我们在本章介绍中讨论的OOP的四个基本概念中的三个:抽象、封装和继承。OOP的最后一个基本支柱是多态性,我们将在下一节介绍。

Python教程

Java教程

Web教程

数据库教程

图形图像教程

大数据教程

开发工具教程

计算机教程