JavaScript权威指南(个人笔记):(五)类和模块

在JavaScript中,类的实现是基于原型继承机制的。如果两个实例都从同一个原型对象上继承了属性,我们说他们是同一个类的实例。

如果两个对象继承自同一个原型,往往意味着(但不是绝对)他们是由同一个构造函数创建并初始化的。

定义类是模块开发和重用代码的有效方法之一

 

类和原型

在JavaScript中,类的所有实例对象都从同一个原型对象上继承属性。因此,原型对象是类的唯一标识

例子:一个简单的JavaScript类,实现一个能表示值的范围的类

// 这个工厂方法返回一个新的“范围对象”

function range(from, to) {

  // 使用inherit()函数来创建对象,这个对象继承自在下面定义的原型对象

  // 原型对象作为函数的一个属性存储,并定义所有“范围对象”所共享的方法(行为)

  let r = inherit(range.methods);

  // 存储新的“范围对象”的起始位置和结束位置(状态)

  // 这两个属性是不可继承的,每个对象都拥有唯一的属性

  r.from = from;

  r.to = to;

  // 返回这个新创建的对象

  return r;

}

range.methods = {

  // 如果x在范围内,则返回true,否则返回false

  // 这个方法可以比较数字的范围

  includes: function (x) { return this.from <= x && x <= this.to; }

};

let r = range(1, 3);  // 创建一个范围对象

r.includes(2);  // => true: 2 在这个范围内

注意:这种方法不常用!

 

类和构造函数

调用构造函数的一个重要特征是,构造函数的prototype属性被用作新对象的原型。这意味着通过同一个构造函数创建的所有对象都继承自同一个对象,因此他们都是同一个类的成员。

例子:对上一个例子进行修改,使用构造函数代替工厂函数

// 这是一个构造函数,用以初始化新创建的“范围对象”

// 注意,这里并没有创建并返回一个对象,仅仅是初始化

function Range(from, to) {

  // 存储“范围对象”的起始位置和结束位置(状态)

  // 这两个属性是不可继承的,每个对象都拥有唯一的属性

  this.from = from;

  this.to = to;

}

// 所有的“范围对象”都继承自这个对象

// 注意,属性的名字必须是“prototype”

Range.prototype = {

  // 如果x在范围内,则返回true,否则返回false

  // 这个方法可以比较数字的范围

  includes: function (x) { return this.from <= x && x <= this.to; }

};

let r = new Range(1, 3);  // 创建一个范围对象

r.includes(2);  // => true: 2 在这个范围内

以上两个例子对比,可以发现函数名称变了。常规的编程约定:从某种意义上来说,定义构造函数既是定义类,并且类名首字母要大写。而普通的函数和方法都是首字母小写。

 

构造函数和类的标识

原型对象是类的唯一标识:当且仅当两个对象继承自同一个原型对象时,他们才是属于同一个类的实例。

构造函数是类的公共标识。

而初始化对象的状态的构造函数则不能作为类的标识,两个构造函数的prototype属性可能指向同一个原型对象。那么这两个构造函数创建的实例是属于同一个类的。

r instanceof Range  // 如果r继承自Range.prototype, 则返回true

实际上instance运算符不会检查对象r是否是Range()构造函数初始化而来,而会检查对象r是否继承自Range.prototype

 

constructor属性

在“类和构造函数”的例子中,将Range.prototype定义为一个新对象。其实没有必要新创建一个对象,用单个对象直接量的属性就可以方便的定义原型上的方法。

任何JavaScript函数都可以用作构造函数,并且调用构造函数是需要用到一个prototype属性的。因此基本每个函数都拥有一个prototype属性。

prototype属性的值是一个对象,这个对象包含唯一一个不可枚举属性constructor。constructor属性的值是一个函数对象。

例子:

let F = function () { };    // 这是一个函数对象

let p = F.prototype;    // 这是F相关联的原型对象

let c = p.constructor;   // 这是与原型相关联的函数

c === F;       // =>true:对于任意函数F.prototype.constructor == F

可以看到构造函数的原型中存在预先定义好的constructor属性。由于构造函数是类的“公共标识”(原型是唯一的标识),因此这个constructor属性为对象提供了类。

例子:

let o = new F();     // 创建类F的一个对象o

o.constructor === F;    // =>true,constructor属性指代这个类(但是上面Range的例子这个是不行的,下面会解释到)

 

因为Range.prototype被重写了,不包含constructor属性。因此Range类的实例r也不含有constructor属性。

我们可以通过补救措施来修正这个问题,显示的给原型添加一个构造函数:

Range.prototype = {

  constructor: Range,  // 显示设置构造函数反向引用

  includes: function (x) { return this.from <= x && x <= this.to; }

};

另一种常用的解决办法是使用预定义的原型对象,预定义的原型对象包含constructor属性,然后依次给原型对象添加方法:

// 扩展预定义的Range.prototype对象,而不重写

// 这样就自动创建Range.prototype.constructor属性

Range.prototype.includes = function (x) {

  return this.from <= x && x <= this.to;

};

 

类的扩充

JavaScript中基于原型的继承机制是动态的:对象从其原型继承属性,如果创建对象之后原型的属性发生改变,也会影响到继承这个原型的所有实例对象。

 

instanceof运算符

如果o继承自c.prototype,则表达式o instanceof c 的值为true。这里的继承可以不是直接继承:如果o所继承的对象继承自另一个对象,后一个对象继承自c.prototype,这个表达式的运算结果也是true。

尽管instanceof运算符的右操作数是构造函数,但是计算过程中实际上是检测了对象的继承关系,而不是检测创建对象的构造函数。

 

私有状态

使用闭包来封装类的状态的类,一定会比不使用封装的状态变量的等价类运行速度更慢,并占用更多内存。

 

定义子类

JavaScript对象可以从类的原型对象中继承属性(通常继承的是方法)。

例子:

B.prototype = inherit(A.prototype);  // 子类派生自父类

B.prototype.constructor = B;  // 重载继承来的constructor属性

这两行代码是在JavaScript中创建子类的关键。如果不这样做,原型对象仅仅是一个普通对象,它只继承自Object.prototype,这意味着你的类和你所有的类一样是Object的子类

posted @ 2020-11-26 16:57  或许从前  阅读(162)  评论(0)    收藏  举报