CCClass 进阶参考

相比其它 JavaScript 的类型系统,CCClass 的特别之处在于功能强大,能够灵活的定义丰富的元数据。CCClass 的技术细节比较丰富,你可以在开发过程中慢慢熟悉。本文档将列举它的详细用法,阅读前需要先掌握 使用 cc.Class 声明类型

术语

  • CCClass:使用 cc.Class 声明的类。
  • 原型对象:调用 cc.Class 时传入的字面量参数。
  • 实例成员:包含“成员变量”和“成员方法”。
  • 静态成员:包含“静态变量”和“类方法”。
  • 运行时:项目脱离编辑器独立运行时,或者在模拟器和浏览器里预览的时候。
  • 序列化:解析内存中的对象,将它的信息编码为一个特殊的字符串,以便保存到硬盘上或传输到其它地方。

原型对象参数说明

所有原型对象的参数都可以省略,用户只需要声明用得到的部分即可。

  1. cc.Class({
  2. // 类名,用于序列化
  3. // 值类型:String
  4. name: "Character",
  5. // 基类,可以是任意创建好的 cc.Class
  6. // 值类型:Function
  7. extends: cc.Component,
  8. // 构造函数
  9. // 值类型:Function
  10. ctor: function () {},
  11. // 属性定义(方式一,直接定义)
  12. properties: {
  13. text: ""
  14. },
  15. // 属性定义(方式二,使用 ES6 的箭头函数,详见下文)
  16. properties: () => ({
  17. text: ""
  18. }),
  19. // 实例方法
  20. print: function () {
  21. cc.log(this.text);
  22. },
  23. // 静态成员定义
  24. // 值类型:Object
  25. statics: {
  26. _count: 0,
  27. getCount: function () {}
  28. },
  29. // 提供给 Component 的子类专用的参数字段
  30. // 值类型:Object
  31. editor: {
  32. disallowMultiple: true
  33. }
  34. });

类名

类名可以是任意字符串,但不允许重复。可以使用 cc.js.getClassName 来获得类名,使用 cc.js.getClassByName 来查找对应的类。对在项目脚本里定义的组件来说,序列化其实并不使用类名,因此那些组件不需要指定类名。对其他类来说,类名用于序列化,如果不需要序列化,类名可以省略。

构造函数

通过 ctor 定义

CCClass 的构造函数使用 ctor 定义,为了保证反序列化能始终正确运行,ctor 不允许定义构造参数

开发者如果确实需要使用构造参数,可以通过 arguments 获取,但要记得如果这个类会被序列化,必须保证构造参数都缺省的情况下仍然能 new 出对象。

通过 ctor 定义

ctorctor 一样,但是允许定义构造参数,并且不会自动调用父构造函数,因此用户可以自行调用父构造函数。ctor 不是标准的构造函数定义方式,如果没有特殊需要请一律使用 ctor 定义。

判断类型

判断实例

需要做类型判断时,可以用 JavaScript 原生的 instanceof

  1. var Sub = cc.Class({
  2. extends: Base
  3. });
  4. var sub = new Sub();
  5. cc.log(sub instanceof Sub); // true
  6. cc.log(sub instanceof Base); // true
  7. var base = new Base();
  8. cc.log(base instanceof Sub); // false

判断类

使用 cc.isChildClassOf 来判断两个类的继承关系:

  1. var Texture = cc.Class();
  2. var Texture2D = cc.Class({
  3. extends: Texture
  4. });
  5. cc.log(cc.isChildClassOf(Texture2D, Texture)); // true

两个传入参数都必须是类的构造函数,而不是类的对象实例。如果传入的两个类相等,isChildClassOf 同样会返回 true。

成员

实例变量

在构造函数中定义的实例变量不能被序列化,也不能在 属性检查器 中查看。

  1. var Sprite = cc.Class({
  2. ctor: function () {
  3. // 声明实例变量并赋默认值
  4. this.url = "";
  5. this.id = 0;
  6. }
  7. });
如果是私有的变量,建议在变量名前面添加下划线 _ 以示区分。

实例方法

实例方法请在原型对象中声明:

  1. var Sprite = cc.Class({
  2. ctor: function () {
  3. this.text = "this is sprite";
  4. },
  5. // 声明一个名叫 "print" 的实例方法
  6. print: function () {
  7. cc.log(this.text);
  8. }
  9. });
  10. var obj = new Sprite();
  11. // 调用实例方法
  12. obj.print();

静态变量和静态方法

静态变量或静态方法可以在原型对象的 statics 中声明:

  1. var Sprite = cc.Class({
  2. statics: {
  3. // 声明静态变量
  4. count: 0,
  5. // 声明静态方法
  6. getBounds: function (spriteList) {
  7. // ...
  8. }
  9. }
  10. });

上面的代码等价于:

  1. var Sprite = cc.Class({ ... });
  2. // 声明静态变量
  3. Sprite.count = 0;
  4. // 声明静态方法
  5. Sprite.getBounds = function (spriteList) {
  6. // ...
  7. };

静态成员会被子类继承,继承时会将父类的静态变量浅拷贝给子类,因此:

  1. var Object = cc.Class({
  2. statics: {
  3. count: 11,
  4. range: { w: 100, h: 100 }
  5. }
  6. });
  7. var Sprite = cc.Class({
  8. extends: Object
  9. });
  10. cc.log(Sprite.count); // 结果是 11,因为 count 继承自 Object 类
  11. Sprite.range.w = 200;
  12. cc.log(Object.range.w); // 结果是 200,因为 Sprite.range 和 Object.range 指向同一个对象

如果你不需要考虑继承,私有的静态成员也可以直接定义在类的外面:

  1. // 局部方法
  2. function doLoad (sprite) {
  3. // ...
  4. };
  5. // 局部变量
  6. var url = "foo.png";
  7. var Sprite = cc.Class({
  8. load: function () {
  9. this.url = url;
  10. doLoad(this);
  11. };
  12. });

继承

父构造函数

请注意,不论子类是否有定义构造函数,子类实例化前父类的构造函数都会被自动调用。

  1. var Node = cc.Class({
  2. ctor: function () {
  3. this.name = "node";
  4. }
  5. });
  6. var Sprite = cc.Class({
  7. extends: Node,
  8. ctor: function () {
  9. // 子构造函数被调用前,父构造函数已经被调用过,所以 this.name 已经被初始化过了
  10. cc.log(this.name); // "node"
  11. // 重新设置 this.name
  12. this.name = "sprite";
  13. }
  14. });
  15. var obj = new Sprite();
  16. cc.log(obj.name); // "sprite"

因此你不需要尝试调用父类的构造函数,否则父构造函数就会重复调用。

  1. var Node = cc.Class({
  2. ctor: function () {
  3. this.name = "node";
  4. }
  5. });
  6. var Sprite = cc.Class({
  7. extends: Node,
  8. ctor: function () {
  9. Node.call(this); // 别这么干!
  10. this._super(); // 也别这么干!
  11. this.name = "sprite";
  12. }
  13. });
在一些很特殊的情况下,父构造函数接受的参数可能和子构造函数无法兼容。这时开发者就只能自己手动调用父构造函数并且传入需要的参数,这时应该将构造函数定义在 ctor 中。

重写

所有成员方法都是虚方法,子类方法可以直接重写父类方法:

  1. var Shape = cc.Class({
  2. getName: function () {
  3. return "shape";
  4. }
  5. });
  6. var Rect = cc.Class({
  7. extends: Shape,
  8. getName: function () {
  9. return "rect";
  10. }
  11. });
  12. var obj = new Rect();
  13. cc.log(obj.getName()); // "rect"

和构造函数不同的是,父类被重写的方法并不会被 CCClass 自动调用,如果你要调用的话:

方法一:使用 CCClass 封装的 this._super

  1. var Shape = cc.Class({
  2. getName: function () {
  3. return "shape";
  4. }
  5. });
  6. var Rect = cc.Class({
  7. extends: Shape,
  8. getName: function () {
  9. var baseName = this._super();
  10. return baseName + " (rect)";
  11. }
  12. });
  13. var obj = new Rect();
  14. cc.log(obj.getName()); // "shape (rect)"

方法二:使用 JavaScript 原生写法:

  1. var Shape = cc.Class({
  2. getName: function () {
  3. return "shape";
  4. }
  5. });
  6. var Rect = cc.Class({
  7. extends: Shape,
  8. getName: function () {
  9. var baseName = Shape.prototype.getName.call(this);
  10. return baseName + " (rect)";
  11. }
  12. });
  13. var obj = new Rect();
  14. cc.log(obj.getName()); // "shape (rect)"
如果你想实现继承的父类和子类都不是 CCClass,只是原生的 JavaScript 构造函数,你可以用更底层的 API cc.js.extend 来实现继承。

属性

属性是特殊的实例变量,能够显示在 属性检查器 中,也能被序列化。

属性和构造函数

属性不用在构造函数里定义,在构造函数被调用前,属性已经被赋为默认值了,可以在构造函数内访问到。如果属性的默认值无法在定义 CCClass 时提供,需要在运行时才能获得,你也可以在构造函数中重新给属性赋默认值。

  1. var Sprite = cc.Class({
  2. ctor: function () {
  3. this.img = LoadImage();
  4. },
  5. properties: {
  6. img: {
  7. default: null,
  8. type: Image
  9. }
  10. }
  11. });

不过要注意的是,属性被反序列化的过程紧接着发生在构造函数执行之后,因此构造函数中只能获得和修改属性的默认值,还无法获得和修改之前保存(序列化)的值。

属性参数

所有属性参数都是可选的,但至少必须声明 default, get, set 参数中的其中一个。

default 参数

default 用于声明属性的默认值,声明了默认值的属性会被 CCClass 实现为成员变量。默认值只有在第一次创建对象的时候才会用到,也就是说修改默认值时,并不会改变已添加到场景里的组件的当前值。

当你在编辑器中添加了一个组件以后,再回到脚本中修改一个默认值的话,属性检查器 里面是看不到变化的。因为属性的当前值已经序列化到了场景中,不再是第一次创建时用到的默认值了。如果要强制把所有属性设回默认值,可以在 属性检查器 的组件菜单中选择 Reset。

default 允许设置为以下几种值类型:

  • 任意 number, string 或 boolean 类型的值
  • nullundefined
  • 继承自 cc.ValueType 的子类,如 cc.Vec2, cc.Colorcc.Rect 的实例化对象:
  1. properties: {
  2. pos: {
  3. default: new cc.Vec2(),
  4. }
  5. }
  • 空数组 [] 或空对象 {}
  • 一个允许返回任意类型值的 function,这个 function 会在每次实例化该类时重新调用,并且以返回值作为新的默认值:
  1. properties: {
  2. pos: {
  3. default: function () {
  4. return [1, 2, 3];
  5. },
  6. }
  7. }

visible 参数

默认情况下,是否显示在 属性检查器 取决于属性名是否以下划线 _ 开头。如果以下划线开头,则默认不显示在 属性检查器,否则默认显示。

如果要强制显示在 属性检查器,可以设置 visible 参数为 true:

  1. properties: {
  2. _id: { // 下划线开头原本会隐藏
  3. default: 0,
  4. visible: true
  5. }
  6. }

如果要强制隐藏,可以设置 visible 参数为 false:

  1. properties: {
  2. id: { // 非下划线开头原本会显示
  3. default: 0,
  4. visible: false
  5. }
  6. }

serializable 参数

指定了 default 默认值的属性默认情况下都会被序列化,序列化后就会将编辑器中设置好的值保存到场景等资源文件中,并且在加载场景时自动还原之前设置好的值。如果不想序列化,可以设置serializable: false

  1. temp_url: {
  2. default: "",
  3. serializable: false
  4. }

type 参数

default 不能提供足够详细的类型信息时,为了能在 属性检查器 显示正确的输入控件,就要用 type 显式声明具体的类型:

  • 当默认值为 null 时,将 type 设置为指定类型的构造函数,这样 属性检查器 才知道应该显示一个 Node 控件。
  1. enemy: {
  2. default: null,
  3. type: cc.Node
  4. }
  • 当默认值为数值(number)类型时,将 type 设置为 cc.Integer,用来表示这是一个整数,这样属性在 属性检查器 里就不能输入小数点。
  1. score: {
  2. default: 0,
  3. type: cc.Integer
  4. }
  • 当默认值是一个枚举(cc.Enum)时,由于枚举值本身其实也是一个数字(number),所以要将 type 设置为枚举类型,才能在 属性检查器 中显示为枚举下拉框。
  1. wrap: {
  2. default: Texture.WrapMode.Clamp,
  3. type: Texture.WrapMode
  4. }

url 参数

如果属性是用来访问 Raw Asset 资源的 url,为了能在 属性检查器 中选取资源,或者能正确序列化,你需要指定 url 参数:

  1. texture: {
  2. default: "",
  3. url: cc.Texture2D
  4. },

可参考 获取和加载资源: Raw Asset

override 参数

所有属性都将被子类继承,如果子类要覆盖父类同名属性,需要显式设置 override 参数,否则会有重名警告:

  1. _id: {
  2. default: "",
  3. tooltip: "my id",
  4. override: true
  5. },
  6. name: {
  7. get: function () {
  8. return this._name;
  9. },
  10. displayName: "Name",
  11. override: true
  12. }

更多参数内容请查阅 属性参数

属性延迟定义

如果两个类相互引用,脚本加载阶段就会出现循环引用,循环引用将导致脚本加载出错:

  • Game.js
  1. var Item = require("Item");
  2. var Game = cc.Class({
  3. properties: {
  4. item: {
  5. default: null,
  6. type: Item
  7. }
  8. }
  9. });
  10. module.exports = Game;
  • Item.js
  1. var Game = require("Game");
  2. var Item = cc.Class({
  3. properties: {
  4. game: {
  5. default: null,
  6. type: Game
  7. }
  8. }
  9. });
  10. module.exports = Item;

上面两个脚本加载时,由于它们在 require 的过程中形成了闭环,因此加载会出现循环引用的错误,循环引用时 type 就会变为 undefined。因此我们提倡使用以下的属性定义方式:

  • Game.js
  1. var Game = cc.Class({
  2. properties: () => ({
  3. item: {
  4. default: null,
  5. type: require("Item")
  6. }
  7. })
  8. });
  9. module.exports = Game;
  • Item.js
  1. var Item = cc.Class({
  2. properties: () => ({
  3. game: {
  4. default: null,
  5. type: require("Game")
  6. }
  7. })
  8. });
  9. module.exports = Item;

这种方式就是将 properties 指定为一个 ES6 的箭头函数(lambda 表达式),箭头函数的内容在脚本加载过程中并不会同步执行,而是会被 CCClass 以异步的形式在所有脚本加载成功后才调用。因此加载过程中并不会出现循环引用,属性都可以正常初始化。

箭头函数的用法符合 JavaScript 的 ES6 标准,并且 Creator 会自动将 ES6 转义为 ES5,用户不用担心浏览器的兼容问题。

你可以这样来理解箭头函数:

  1. // 箭头函数支持省略掉 `return` 语句,我们推荐的是这种省略后的写法:
  2. properties: () => ({ // <- 箭头右边的括号 "(" 不可省略
  3. game: {
  4. default: null,
  5. type: require("Game")
  6. }
  7. })
  8. // 如果要完整写出 `return`,那么上面的写法等价于:
  9. properties: () => {
  10. return {
  11. game: {
  12. default: null,
  13. type: require("Game")
  14. }
  15. }; // <- 这里 return 的内容,就是原先箭头右边括号里的部分
  16. }
  17. // 我们也可以不用箭头函数,而是用普通的匿名函数:
  18. properties: function () {
  19. return {
  20. game: {
  21. default: null,
  22. type: require("Game")
  23. }
  24. };
  25. }

GetSet 方法

在属性中设置了 get 或 set 以后,访问属性的时候,就能触发预定义的 get 或 set 方法。

get

在属性中设置 get 方法:

  1. properties: {
  2. width: {
  3. get: function () {
  4. return this.__width;
  5. }
  6. }
  7. }

get 方法可以返回任意类型的值。这个属性同样能显示在 属性检查器 中,并且可以在包括构造函数内的所有代码里直接访问。

  1. var Sprite = cc.Class({
  2. ctor: function () {
  3. this.__width = 128;
  4. cc.log(this.width); // 128
  5. },
  6. properties: {
  7. width: {
  8. get: function () {
  9. return this.__width;
  10. }
  11. }
  12. }
  13. });

请注意:

  • 设定了 get 以后,这个属性就不能被序列化,也不能指定默认值,但仍然可附带除了 default, serializable 外的大部分参数。
  1. width: {
  2. get: function () {
  3. return this.__width;
  4. },
  5. type: cc.Integer,
  6. tooltip: "The width of sprite"
  7. }
  • get 属性本身是只读的,但返回的对象并不是只读的。用户使用代码依然可以修改对象内部的属性,例如:
  1. var Sprite = cc.Class({
  2. ...
  3. position: {
  4. get: function () {
  5. return this._position;
  6. },
  7. }
  8. ...
  9. });
  10. var obj = new Sprite();
  11. obj.position = new cc.Vec2(10, 20); // 失败!position 是只读的!
  12. obj.position.x = 100; // 允许!position 返回的 _position 对象本身可以修改!

set

在属性中设置 set 方法:

  1. width: {
  2. set: function (value) {
  3. this._width = value;
  4. }
  5. }

set 方法接收一个传入参数,这个参数可以是任意类型。

set 一般和 get 一起使用:

  1. width: {
  2. get: function () {
  3. return this._width;
  4. },
  5. set: function (value) {
  6. this._width = value;
  7. },
  8. type: cc.Integer,
  9. tooltip: "The width of sprite"
  10. }
如果没有和 get 一起定义,则 set 自身不能附带任何参数。和 get 一样,设定了 set 以后,这个属性就不能被序列化,也不能指定默认值。

editor 参数

editor 只能定义在 cc.Component 的子类。

  1. cc.Class({
  2. extends: cc.Component,
  3. editor: {
  4. // 允许当前组件在编辑器模式下运行。
  5. // 默认情况下,所有组件都只会在运行时执行,也就是说它们的生命周期回调在编辑器模式下并不会触发。
  6. //
  7. // 值类型:Boolean
  8. // 默认值:false
  9. executeInEditMode: false,
  10. // requireComponent 参数用来指定当前组件的依赖组件。
  11. // 当组件添加到节点上时,如果依赖的组件不存在,引擎将会自动将依赖组件添加到同一个节点,防止脚本出错。
  12. // 该选项在运行时同样有效。
  13. //
  14. // 值类型:Function (必须是继承自 cc.Component 的构造函数,如 cc.Sprite)
  15. // 默认值:null
  16. requireComponent: null,
  17. // 脚本生命周期回调的执行优先级。
  18. // 小于 0 的脚本将优先执行,大于 0 的脚本将最后执行。
  19. // 该优先级只对 onLoad, onEnable, start, update 和 lateUpdate 有效,对 onDisable 和 onDestroy 无效。
  20. //
  21. // 值类型:Number
  22. // 默认值:0
  23. executionOrder: 0,
  24. // 当本组件添加到节点上后,禁止同类型(含子类)的组件再添加到同一个节点,
  25. // 防止逻辑发生冲突。
  26. //
  27. // 值类型:Boolean
  28. // 默认值:false
  29. disallowMultiple: false,
  30. // menu 用来将当前组件添加到组件菜单中,方便用户查找。
  31. //
  32. // 值类型:String (如 "Rendering/Camera")
  33. // 默认值:""
  34. menu: "",
  35. // 当设置了 "executeInEditMode" 以后,playOnFocus 可以用来设定选中当前组件所在的节点时,
  36. // 编辑器的场景刷新频率。
  37. // playOnFocus 如果设置为 true,场景渲染将保持 60 FPS,如果为 false,场景就只会在必要的时候进行重绘。
  38. //
  39. // 值类型:Boolean
  40. // 默认值:false
  41. playOnFocus: false,
  42. // 自定义当前组件在 **属性检查器** 中渲染时所用的网页 url。
  43. //
  44. // 值类型:String
  45. // 默认值:""
  46. inspector: "",
  47. // 自定义当前组件在编辑器中显示的图标 url。
  48. //
  49. // 值类型:String
  50. // 默认值:""
  51. icon: "",
  52. // 指定当前组件的帮助文档的 url,设置过后,在 **属性检查器** 中就会出现一个帮助图标,
  53. // 用户点击将打开指定的网页。
  54. //
  55. // 值类型:String
  56. // 默认值:""
  57. help: "",
  58. }
  59. });

继续前往 属性参数参考