JavaScript类工厂设计:原型、继承与实现模式详解

作者:闭包旅人

时间:2026-10-29

浏览:606

本文深入解析JavaScript中类与继承的实现机制,从原型链、特权方法到私有属性,详细对比了拷贝继承与原型继承的优劣。重点介绍如何利用Object.create及自定义类工厂简化子类创建,并结合P.js等库展示现代JavaScript框架中的类设计最佳实践。

文章目录

JavaScript类工厂概述

JavaScript中类与继承概念的引入,标志着该语言已具备支撑大规模开发的能力。早期ECMAScript 4曾尝试引入类、模块等特性,但因特性过多导致争议而被否决,类的支持最终延后至ES6。尽管目前JavaScript尚无原生意义上的“类”,但通过模拟类实现的方式,类工厂曾长期作为主流框架的标准配置。本章将介绍多种类的实现方案,帮助开发者在构建或选择框架时,找到适合自身需求的类设计风格。

JavaScript对类的支持机制

1. JavaScript对类的支持机制

在其他面向对象语言中,类的实例通常通过构造函数配合new操作符生成。JavaScript作为有意模仿Java的语言,同样提供了new操作符,且所有函数均可作为构造器使用。构造函数与普通方法在语法上并无本质区别。为了构建如Node、Element、HTMLElement等丰富的浏览器生态,JavaScript借鉴了原型(Prototype)机制以实现方法或属性的共享。每个函数都拥有一个特殊的Prototype属性,当通过new操作符创建“实例”时,该实例将拥有原型对象的所有成员,从而实现所有实例共享一组方法或属性。JavaScript所谓的“类”,本质上是通过修改Prototype对象来区分原生对象与其他自定义“类”。例如,浏览器中的Node类基于Object修改而来,Element基于Node,HTMLElement基于Element,以此类推。在实际业务中,开发者可以创建自己的类以实现代码重用与共享。


  function A(){

  }
  A.prototype = {
    aa:"aa",
    method:function(){
    }
  };
  var a = new A;
  var b = new A;
  console.log(a.aa === b.aa);
  console.log(a.method === b.method)

通常,定义在原型上的方法称为原型方法,所有实例共享这些方法。这种方式既有优势也有局限。为了实现实例间的差异化,JavaScript允许在构造器内部直接定义方法,这被称为特权方法(Privileged Method)。若定义的是属性,则称为特权属性。特权方法或属性为每个实例创建独立的副本,互不影响。因此,最佳实践通常是将操作数据的方法放在原型上以实现共享,而将私有属性放在特权属性中。若希望属性完全私有,可将其定义在函数体的作用域内,这样外部无法直接访问,成为名副其实的私有属性。


  function A() {
    var count = 0;
    this.aa = "aa";
    this.method = function() {
      return count;
    }
    this.obj = {}
  }
  A.prototype = {
    aa:"aa",
    method:function(){

    }
  };
  var a = new A;
  var b = new A;
  console.log(a.aa === b.aa);//true 由于aa的值为基本类型,比较值
  console.log(a.obj === b.obj) //false 引用类型,每次进入函数体都要重新创建,因此都不一样。
  console.log(a.method === b.method); //false

特权方法或属性仅遮蔽了原型中同名的方法或属性。如果删除特权方法,程序将回退到访问同名的原型方法或属性。


  delete a.method;
  delete b.method;
  console.log(a.method === A.prototype.method);//true
  console.log(a.method === b.method); //true

用Java术语来说,原型方法与特权方法均属于实例方法。Java中还存在类方法(Class Method)与类属性,在JavaScript中模拟这一概念也非常简单,只需直接将方法定义在函数对象上即可。


  A.method2 = function(){} //类方法
  var c = new A;
  console.log(c.method2); //undefined

接下来探讨继承的实现。前文提到,原型对象中包含什么,其实例就拥有什么,无论是后期添加的属性还是整个原型对象的替换。如果将子类的prototype对象置换为另一个类的原型,子类即可轻松获得该类的所有一般成员。


  function A() {};
  A.prototype = {
    aaa : 1
  }
  function B() {};
  B.prototype = A.prototype;
  var b = new B;
  console.log(b.aaa); //=> 1;
  A.prototype.bb = 2;
  console.log(b.bb) //=> 2;

由于多个类引用同一个原型对象,修改A类的原型也会同步修改B类的原型。因此,不能简单地将一个原型对象赋值给两个不同的类。针对这一问题,有两种解决思路:

方法1:通过for...in循环将父类的原型成员逐一赋值给子类的原型。 方法2:子类的原型不直接由父类获得,而是先将父类的原型赋值给一个中间函数,然后将该中间函数的实例作为子类的原型。

方法1通常涉及实现类似mixin的功能,有些书籍称之为“拷贝继承”。其优点是简单直接,缺点是无法通过instanceof进行类型验证。Prototype.js库中的extend方法即用于实现此功能。


  function extend (des, source) { //des = destination
    for (var property in source)
      des[property] = source[property];
    return des;
  }

方法2则在原型对象上做文章,因此被称为“原型继承”。以下是经典实现范本:


  function A() {};
  A.prototype = {
    aa:function(){
      alert(1)
    }
  }
  function bridge() {

  };
  bridge.prototype = A.prototype;

  function B() {}
  B.prototype = new bridge();

  var a = new A;
  var b = new B;

  console.log(a == b) //false 证明成功分开原型
  console.log(A.prototype == B.prototype) //true 子类共享父类的原型方法
  console.log(a.aa === b.aa); //为父类动态添加新的方法
  A.prototype.bb = function () {
    alert(2)
  }
  //true,继承父类的方法
  B.prototype.cc = function (){
    alert(3)
  }
  //false 父类未必有子类的new实例
  console.log(a.cc === b.cc)
  //并且它能够正常通过javascript自带的验证机制instanceof
  console.log(b instanceof A) ;//true
  console.log(b instanceof B) ; //true

方法2能够通过instanceof验证。ES5内置了Object.create方法来实现原型继承,若不考虑其第二个参数,它大致等价于以下代码:


  Object.create = function (o) {
    function F() {}
    F.prototype = o;
    return new F();
  }

上述方法要求传入一个父类的原型作为参数,并返回子类的原型对象。

然而,上述实现仍遗漏了一些关键点:子类不仅继承父类的成员,还应拥有自己的特有成员。此外,单纯的原型继承并未让子类继承父类的实例成员与特权成员。这些都需要手动添加,例如类成员可通过上述extend方法添加,特权成员则可在子类构造器中通过apply方法实现。


  function inherit(init, Parent, proto){
    function Son(){
      Parent.apply(this, argument); //先继承父类的特权成员
      init.apply(this, argument); //在执行自己的构造器
    }
  }
  //由于Object.create是我们伪造的,因此避免使用第二个参数
  Son.prototype = Object.create(Parent.prototype,{});
  Son.prototype.toString = Parent.prototype.toString; //处理IEbug
  Son.prototype.valueOf = Parent.prototype.valueOf; //处理IEbug
  Son.prototype.constructor = Son; //确保构造器正常指向,而不是Object
  extend(Son, proto) ;//添加子类的特有的原型成员
  return Son;

下面进行一组实验,测试实例的属性回溯机制。当访问对象的某个属性时,JavaScript引擎会先查找其特权成员,若存在同名属性则返回;若不存在,则查找原型对象;若仍不存在,则继续向上查找父类的原型……我们尝试临时修改实例的原型,观察其属性值的变化。


  function A(){

  }
  A.prototype = {
    aa:1
  }
  var a = new A;
  console.log(a.aa) ; //=>1

  //将它的所有原型都替换掉
  A.prototype = {
    aa:2
  }
  console.log(a.aa); //=>1

  //于是我们想到每个实例都有一个constructor方法,指向其构造器
  //而构造器上面正好有我们的原型,javascript引擎是不是通过该路线回溯属性呢
  function B(){

  }
  B.prototype = {
    aa:3
  }
  a.constructor = B;
  console.log(a.aa) //1 表示不受影响

因此,类的实例必然通过另一条通道进行属性回溯。查阅ECMA规范可知,每个对象内部都有一个[[prototype]]属性,它保存着创建该实例时构造函数所引用的Prototype对象。在标准浏览器及IE11中,这一内部属性暴露为__proto__属性供外部访问。因此,只要不直接修改__proto__,无论代码如何变动,a.aa始终稳定返回1。

再来看看new操作符执行过程中发生了什么:

1. 创建了一个空对象instance。 2. 将instance.__proto__指向instanceClass.prototype。 3. 将构造函数内部的this绑定为instance。 4. 执行构造函数内的代码。 5. 判断构造函数是否有返回值:若无返回值,则默认返回undefined;若返回值为复合数据类型(对象),则直接返回该对象;否则返回this。 由此产生了上述实验结果。


  function A(){
    console.log(this.__proto__.aa); //1
    this.aa = 2
  }
  A.prototype = {aa:1}
  var a = new A;
  console.log(a.aa)
  a.__proto__ = {
    aa:3
  }
  console.log(a.aa) //=>2
  delete a. aa; //删除特权属性,暴露原型链上的同名属性
  console.log(a.aa) //=>3

有了__proto__属性,我们可以将原型继承的设计简化。仍以之前的例子为基础进行修改试验:


  function A() {}
  A.prototype = {
    aa:1
  }
  function bridge() {}
  bridge.prototype = A.prototype;

  function B(){}
  B.prototype = new bridge();
  B.prototype.constructor = B;
  var b = new B;
  B.prototype.cc = function(){
    alert(3)
  }
  //String.prototype === new String().__proto__ => true
  console.log(B.prototype.__proto__ === A.prototype) //true
  console.log(b.__proto__ == B.prototype); //true 
  console.log(b.__proto__.__proto__ === A.prototype); //true 得到父类的原型对象

由于b.__proto__.constructor指向B,而B的原型源自bridge,且bridge.prototype = A.prototype。反过来,若在定义时设置B.prototype.__proto__ = A.prototype,即可轻松实现两个类之间的继承关系。

__proto__属性已被纳入ES6规范,因此可以大胆使用。

各种类工厂的实现方案

2. 各种类工厂的实现方案

上一节演示了多种继承方式的实现,但代码较为零散。我们期望提供一个专门的方法,用户只需传入相应参数或遵循简单的格式,即可创建一个类,特别是子类。

主流框架的类工厂往往依赖其庞杂的工具函数集,而一个精巧的类工厂代码量不过百行左右。

一个相当精巧的库:P.js

https://github.com/jiayi2/pjs

使用版:https://github.com/jiayi2/factoryjs

这是一个相当精巧的库,特别是在调用父类同名方法时,它直接将父类的原型暴露给用户,连_super变量也无需额外定义。


  var P = (function(prototype, ownProperty, undefined) {
 return function P(_superclass /* = Object */, definition) {
  // handle the case where no superclass is given
  if (definition === undefined) {
   definition = _superclass;
   _superclass = Object;
  }

  // C is the class to be returned.
  //
  // When called, creates and initializes an instance of C, unless
  // `this` is already an instance of C, then just initializes `this`;
  // either way, returns the instance of C that was initialized.
  //
  // TODO: the Chrome inspector shows all created objects as `C`
  //    rather than `Object`. Setting the .name property seems to
  //    have no effect. Is there a way to override this behavior?
  function C() {
   var self = this instanceof C ? this : new Bare;
   self.init.apply(self, arguments);
   return self;
  }

  // C.Bare is a class with a noop constructor. Its prototype will be
  // the same as C, so that instances of C.Bare are instances of C.
  // `new MyClass.Bare` then creates new instances of C without
  // calling .init().
  function Bare() {}
  C.Bare = Bare;

  // Extend the prototype chain: first use Bare to create an
  // uninitialized instance of the superclass, then set up Bare
  // to create instances of this class.
  var _super = Bare[prototype] = _superclass[prototype];
  var proto = Bare[prototype] = C[prototype] = C.p = new Bare;

  // pre-declaring the iteration variable for the loop below to save
  // a `var` keyword after minification
  var key;

  // set the constructor property on the prototype, for convenience
  proto.constructor = C;

  C.extend = function(def) { return P(C, def); }

  return (C.open = function(def) {
   if (typeof def === 'function') {
    // call the defining function with all the arguments you need
    // extensions captures the return value.
    def = def.call(C, proto, _super, C, _superclass);
   }

   // ...and extend it
   if (typeof def === 'object') {
    for (key in def) {
     if (ownProperty.call(def, key)) {
      proto[key] = def[key];
     }
    }
   }

   // if no init, assume we're inheriting from a non-Pjs class, so
   // default to using the superclass constructor.
   if (!('init' in proto)) proto.init = _superclass;

   return C;
  })(definition);
 }

 // as a minifier optimization, we've closured in a few helper functions
 // and the string 'prototype' (C[p] is much shorter than C.prototype)
})('prototype', ({}).hasOwnProperty);

我们尝试创建一个类:


  var Dog = P (function(proto, superProto){
    proto.init = function(name) { //构造函数
      this.name = name;
    }
    proto.move = function(meters){ //原型方法
      console.log(this.name + " moved " + meters + " m.")
    }
  });
  var a = new Dog("aaa")
  var b = new Dog("bbb"); //无实例变化
  a.move(1);
  b.move(2);

在当前环境下,我们可以尝试创建更简洁的定义方式:


  var Animal = P (function(proto, superProto){
    proto.init = function(name) { //构造函数
      this.name = name;
    }
    proto.move = function(meters){ //原型方法
      console.log(this.name + " moved " + meters + " m.")
    }
  });
  var a = new Animal("aaa")
  var b = new Animal("bbb"); //无实例变化
  a.move(1);
  b.move(2);
  //...............
  var Snake = P (Animal, function(snake, animal){
    snake.init = function(name, eyes){
      animal.init.call(this, arguments); //调运父类构造器
      this.eyes = 2;
    }
    snake.move = function() {
      console.log('slithering...');
      animal.move.call(this, 5); //调运父类同名方法
    }
  });
  var s = new Snake("snake", 1);
  s.move();
  console.log(s.name);
  console.log(s.eyes);

私有属性演示:由于私有属性在函数体内集中定义,因此既安全又可靠。


  var Cobra = P (Snake, function(cobra){
    var age = 1;//私有属性
    //这里还可以编写私有方法
    cobra.glow = function(){ //长大
      return age++;
    }
  });
  var c = new Cobra("cobra");
  console.log(c.glow()); //1
  console.log(c.glow()); //2
  console.log(c.glow()); //3

以上所述即为本文的全部内容,希望读者能够从中受益。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章