简介: 从程序角度考虑,许多 JavaScript 都基于循环和大量的 if/else 语句。在本文中,我们可了解一种更聪明的做法 — 在 JavaScript 游戏中使用面向对象来设计。本文将概述原型继承和使用 JavaScript 实现基本的面向对象的编程 (OOP)。学习如何在 JavaScript 中使用基于经典继承的库从 OOP 中获得更多的好处。本文还将介绍架构式设计模式,来展示了如何使用游戏循环、状态机和事件冒泡 (event bubbling) 示例来编写更整洁的代码。
在本文中,您将了解 JavaScript 中的 OOP,来探索原型继承模型和经典继承模型。举例说明游戏中能够从 OOP 设计的结构和可维护性中获得极大利益的模式。我们的最终目标是让每一块代码都成为人类可读的代码,并代表一种想法和一个目的,这些代码的结合超越了指令和算法的集合,成为一个精致的艺术品。
JavaScript 中的 OPP 的概述
OOP 的目标就是提供数据抽象、模块化、封装、多态性和继承。通过 OOP,您可以在代码编写中抽象化代码的理念,从而提供优雅、可重用和可读的代码,但这会消耗文件计数、行计数和性能(如果管理不善)。
过去,游戏开发人员往往会避开纯 OOP 方式,以便充分利用 CPU 周期的性能。很多 JavaScript 游戏教程采用的都是非 OOP 方式,希望能够提供一个快速演示,而不是提供一种坚实的基础。与其他游戏的开发人员相比,JavaScript 开发人员面临不同的问题:内存是非手动管理的,且 JavaScript 文件在全局的上下文环境中执行,这样一来,无头绪的代码、命名空间的冲突和迷宫式的 if/else 语句可能会导致可维护性的噩梦。为了从 JavaScript 游戏的开发中获得最大的益处,请遵循 OOP 的最佳实践,显著提高未来的可维护性、开发进度和游戏的表现能力。
原型继承
与使用经典继承的语言不同,在 JavaScript 中,没有内置的类结构。函数是 JavaScript 世界的一级公民,并且,与所有用户定义的对象类似,它们也有原型。用 new
关键字调用函数实际上会创建该函数的一个原型对象副本,并使用该对象作为该函数中的关键字 this
的上下文。清单 1 给出了一个例子。
清单 1. 用原型构建一个对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// constructor function function MyExample() { // property of an instance when used with the 'new' keyword this.isTrue = true; }; MyExample.prototype.getTrue = function() { return this.isTrue; } MyExample(); // here, MyExample was called in the global context, // so the window object now has an isTrue property—this is NOT a good practice MyExample.getTrue; // this is undefined—the getTrue method is a part of the MyExample prototype, // not the function itself var example = new MyExample(); // example is now an object whose prototype is MyExample.prototype example.getTrue; // evaluates to a function example.getTrue(); // evaluates to true because isTrue is a property of the // example instance |
依照惯例,代表某个类的函数应该以大写字母开头,这表示它是一个构造函数。该名称应该能够代表它所创建的数据结构。
创建类实例的秘诀在于综合新的关键字和原型对象。原型对象可以同时拥有方法和属性,如 清单 2 所示。
清单 2. 通过原型化的简单继承
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 |
// Base class function Character() {}; Character.prototype.health = 100; Character.prototype.getHealth = function() { return this.health; } // Inherited classes function Player() { this.health = 200; } Player.prototype = new Character; function Monster() {} Monster.prototype = new Character; var player1 = new Player(); var monster1 = new Monster(); player1.getHealth(); // 200- assigned in constructor monster1.getHealth(); // 100- inherited from the prototype object |
为一个子类分配一个父类需要调用 new
并将结果分配给子类的 prototype
属性,如 清单 3 所示。因此,明智的做法是保持构造函数尽可能的简洁和无副作用,除非您想要传递类定义中的默认值。
如果您已经开始尝试在 JavaScript 中定义类和继承,那么您可能已经意识到该语言与经典 OOP 语言的一个重要区别:如果已经覆盖这些方法,那么没有 super
或 parent
属性可用来访问父对象的方法。对此有一个简单的解决方案,但该解决方案违背了 “不要重复自己 (DRY)” 原则,而且很有可能是如今有很多库试图模仿经典继承的最重要的原因。
清单 3. 从子类调用父方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
function ParentClass() { this.color = 'red'; this.shape = 'square'; } function ChildClass() { ParentClass.call(this); // use 'call' or 'apply' and pass in the child // class's context this.shape = 'circle'; } ChildClass.prototype = new ParentClass(); // ChildClass inherits from ParentClass ChildClass.prototype.getColor = function() { return this.color; // returns "red" from the inherited property }; |
在 清单 3 中, color
和 shape
属性值都不在原型中,它们在 ParentClass
构造函数中赋值。ChildClass
的新实例将会为其形状属性赋值两次:一次作为 ParentClass
构造函数中的 “squre”,一次作为 ChildClass
构造函数中的 “circle”。将类似这些赋值的逻辑移动到原型将会减少副作用,让代码变得更容易维护。
在原型继承模型中,可以使用 原型中,它们在 ParentClass
构造函数中赋值。ChildClass
的新实例将会为其形状属性赋值两次:一次作为 ParentClass
构造函数中的 “squre”,一次作为 ChildClass
构造函数中的 “circle”。将类似这些赋值的逻辑移动到原型将会减少副作用,让代码变得更容易维护。
在原型继承模型中,可以使用 从 OOP 中获得更多的好处。本文还将介绍架构式设计模式,来展示了如何使用游戏循环、状态机和事件冒泡 (event bubbling) 示例来编写更整洁的代码。
在本文中,您将了解 JavaScript 中的 OOP,来探索原型继承模型和经典继承模型。举例说明游戏中能够从 OOP 设计的结构和可维护性中获得极大利益的模式。我们的最终目标是让每一块代码都成为人类可读的代码,并代表一种想法和一个目的,这些代码的结合超越了指令和算法的集合,成为一个精致的艺术品。
JavaScript 中的 OPP 的概述
OOP 的目标就是提供数据抽象、模块化、封装、多态性和继承。通过 OOP,您可以在代码编写中抽象化代码的理念,从而提供优雅、可重用和可读的代码,但这会消耗文件计数、行计数和性能(如果管理不善)。
过去,游戏开发人员往往会避开纯 OOP 方式,以便充分利用 CPU 周期的性能。很多 JavaScript 游戏教程采用的都是非 OOP 方式,希望能够提供一个快速演示,而不是提供一种坚实的基础。与其他游戏的开发人员相比,JavaScript 开发人员面临不同的问题:内存是非手动管理的,且 JavaScript 文件在全局的上下文环境中执行,这样一来,无头绪的代码、命名空间的冲突和迷宫式的 if/else 语句可能会导致可维护性的噩梦。为了从 JavaScript 游戏的开发中获得最大的益处,请遵循 OOP 的最佳实践,显著提高未来的可维护性、开发进度和游戏的表现能力。
原型继承
与使用经典继承的语言不同,在 JavaScript 中,没有内置的类结构。函数是 JavaScript 世界的一级公民,并且,与所有用户定义的对象类似,它们也有原型。用 new
关键字调用函数实际上会创建该函数的一个原型对象副本,并使用该对象作为该函数中的关键字 this
的上下文。清单 1 给出了一个例子。
清单 1. 用原型构建一个对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// constructor function function MyExample() { // property of an instance when used with the 'new' keyword this.isTrue = true; }; MyExample.prototype.getTrue = function() { return this.isTrue; } MyExample(); // here, MyExample was called in the global context, // so the window object now has an isTrue property—this is NOT a good practice MyExample.getTrue; // this is undefined—the getTrue method is a part of the MyExample prototype, // not the function itself var example = new MyExample(); // example is now an object whose prototype is MyExample.prototype example.getTrue; // evaluates to a function example.getTrue(); // evaluates to true because isTrue is a property of the // example instance |
依照惯例,代表某个类的函数应该以大写字母开头,这表示它是一个构造函数。该名称应该能够代表它所创建的数据结构。
创建类实例的秘诀在于综合新的关键字和原型对象。原型对象可以同时拥有方法和属性,如 清单 2 所示。
清单 2. 通过原型化的简单继承
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 |
// Base class function Character() {}; Character.prototype.health = 100; Character.prototype.getHealth = function() { return this.health; } // Inherited classes function Player() { this.health = 200; } Player.prototype = new Character; function Monster() {} Monster.prototype = new Character; var player1 = new Player(); var monster1 = new Monster(); player1.getHealth(); // 200- assigned in constructor monster1.getHealth(); // 100- inherited from the prototype object |
为一个子类分配一个父类需要调用 new
并将结果分配给子类的 prototype
属性,如 清单 3 所示。因此,明智的做法是保持构造函数尽可能的简洁和无副作用,除非您想要传递类定义中的默认值。
如果您已经开始尝试在 JavaScript 中定义类和继承,那么您可能已经意识到该语言与经典 OOP 语言的一个重要区别:如果已经覆盖这些方法,那么没有 super
或 parent
属性可用来访问父对象的方法。对此有一个简单的解决方案,但该解决方案违背了 “不要重复自己 (DRY)” 原则,而且很有可能是如今有很多库试图模仿经典继承的最重要的原因。
清单 3. 从子类调用父方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
function ParentClass() { this.color = 'red'; this.shape = 'square'; } function ChildClass() { ParentClass.call(this); // use 'call' or 'apply' and pass in the child // class's context this.shape = 'circle'; } ChildClass.prototype = new ParentClass(); // ChildClass inherits from ParentClass ChildClass.prototype.getColor = function() { return this.color; // returns "red" from the inherited property }; |
在 清单 3 中, color
和 shape
属性值都不在原型中,它们在 ParentClass
构造函数中赋值。ChildClass
的新实例将会为其形状属性赋值两次:一次作为 ParentClass
构造函数中的 “squre”,一次作为 ChildClass
构造函数中的 “circle”。将类似这些赋值的逻辑移动到原型将会减少副作用,让代码变得更容易维护。
在原型继承模型中,可以使用 /span>/div>div class="crayon-line crayon-striped-line" id="crayon-5812f2f535c54362989572-10">span class="crayon-e ">charset/span>span class="crayon-o">=/span>span class="crayon-s">"utf-8"/span>span class="crayon-o">>/span>span class="crayon-ta"></script>/span>/div>div class="crayon-line" id="crayon-5812f2f535c54362989572-11