JavaScript继承机制:从伪类到函数化模式的深度解析

作者:青柠

时间:2026-10-22

浏览:446

本文深入解析JavaScript的四种继承模式:伪类、对象说明符、原型继承及函数化构造。重点对比了基于原型的直接继承与通过构造函数模拟类继承的差异,详细阐述了函数化模式如何实现私有变量、特权方法及父类调用,并探讨了其在事件处理和防伪对象中的应用。

文章目录

本文深入探讨JavaScript继承的核心特性及其在实际开发中的应用场景,旨在为开发者提供清晰的参考与指导。

继承本质上是代码复用的重要模式。JavaScript不仅支持模拟基于类的继承结构,还具备更多表达力强的其他模式。然而,在大多数情况下,保持实现简洁往往是最佳策略。

JavaScript属于基于原型的语言,这意味着对象可以直接继承自其他对象,无需经过中间层。

1 伪类模式

JavaScript的原型机制并非让对象直接继承另一个对象,而是引入了一层间接层:通过构造函数来生成对象实例。

当函数被创建时,Function构造器生成的函数对象会执行类似以下的代码逻辑:


this.prototype = {constructor : this};

新的函数对象会新增一个名为prototype的属性,该属性是一个包含constructor属性且值指向该新函数的对象。

当使用构造器调用模式,即通过new关键字调用函数时,其执行过程如下:


Function.method('new', function (){
  var that = Object.create(this.prototype);//创建一个继承了构造器函数的原型对象的新对象
  var other = this.apply(that, arguments);//调用构造器函数,绑定 this 到新对象
  return (typeof other === 'object' && other) || that;//如果构造器函数的返回值不是对象,就直接返回这个新对象
});

我们可以定义一个构造函数,并在此基础上扩展其原型方法:


//定义构造器并扩充原型
var Mammal = function (name) {
  this.name = name;
};
Mammal.prototype.get_name = function () {
  return this.name;
};
Mammal.prototype.says = function () {
  return this.saying || '';
};

随后,通过该构造函数创建实例:


var myMammal = new Mammal('Herb the mammal');
console.log(myMammal.get_name());//Herb the mammal

接着定义另一个伪类来继承Mammal(即定义构造函数并替换其prototype属性):


var Cat = function (name) {
  this.name = name;
  this.saying = 'meow';
};
Cat.prototype = new Mammal();

进一步扩展原型以添加新方法:


Cat.prototype.purr = function (n) {
  var i, s = '';
  for (i = 0; i < n; i += 1) {
    if (s) {
      s += '-';
    }
    s += 'r';
  }
  return s;
};
Cat.prototype.get_name = function () {
  return this.says() + ' ' + this.name + ' ' + this.says();
};
var myCat = new Cat('Henrietta');
console.log(myCat.says());//meow
console.log(myCat.purr(5));//r-r-r-r-r
console.log(myCat.get_name());//meow Henrietta meow

我们利用method方法定义了inherits方法,从而封装并隐藏了上述复杂的继承细节:


/**
 * 为 Function.prototype 新增 method 方法
 * @param name 方法名称
 * @param func 函数
 * @returns {Function}
 */
Function.prototype.method = function (name, func) {
  if (!this.prototype[name])//没有该方法时,才添加
    this.prototype[name] = func;
  return this;
};
Function.method('inherits', function (Parent) {
  this.prototype = new Parent();
  return this;
});

这两个方法均返回this,从而支持链式编程风格。


var Cat = function (name) {
  this.name = name;
  this.saying = 'meow';
}.inherits(Mammal).method('purr', function (n) {
    var i, s = '';
    for (i = 0; i < n; i += 1) {
      if (s) {
        s += '-';
      }
      s += 'r';
    }
    return s;
  }).method('get_name', function () {
    return this.says() + ' ' + this.name + ' ' + this.says();
  });
var myCat = new Cat('Henrietta');
console.log(myCat.says());//meow
console.log(myCat.purr(5));//r-r-r-r-r
console.log(myCat.get_name());//meow Henrietta meow

尽管我们拥有了行为类似“类”的构造函数,但这类模式缺乏私有环境,所有属性均为公开状态,且无法直接访问父类的方法。

若在调用构造函数时遗漏new前缀,this将不会被绑定到新对象上,而是错误地绑定到全局变量。这导致新对象未被正确扩充,同时破坏了全局变量环境。

这被视为一种严重的语言设计缺陷。为降低此类错误发生的概率,约定所有构造函数均以首字母大写命名。这样,通过函数名即可识别其为构造函数。

当然,更优的策略是避免使用构造函数模式。

2 对象说明符模式

有时,构造函数需要接收大量参数,这会增加代码复杂度。因此,在编写构造函数时,使其接受一个包含配置信息的对象说明符是更好的做法:


var myObject = maker({
 first: f,
 middle: m,
 last: l
});

此时,参数可以按任意顺序传递,且构造函数能智能地为未传入的参数应用默认值,从而提升代码的可读性。

3 原型模式

基于原型的继承意味着新对象可以直接继承旧对象的属性。首先构建一个基础对象,随后可以创建更多与该对象相似的新对象。


/**
 * 原型
 */
var myMammal = {
  name: 'Herb the mammal',
  get_name: function () {
    return this.name;
  },
  says: function () {
    return this.saying || '';
  }
};
//创建新实例
var myCat = Object.create(myMammal);
myCat.name = 'Henrietta';
myCat.saying = 'meow';
myCat.purr = function (n) {
  var i, s = '';
  for (i = 0; i < n; i += 1) {
    if (s) {
      s += '-';
    }
    s += 'r';
  }
  return s;
};
myCat.get_name = function () {
  return this.says() + ' ' + this.name + ' ' + this.says();
};
console.log(myCat.says());//meow
console.log(myCat.purr(5));//r-r-r-r-r
console.log(myCat.get_name());//meow Henrietta meow

此处使用了create方法来生成新的实例:


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

4 函数化模式

前文所述的继承模式存在一个主要问题:无法保护隐私,对象的所有属性均可见。部分开发者采用伪装私有的方式,即为私有属性起一个晦涩的名称,期望其他开发者忽略它们。

实际上,有更优的解决方案:应用模块模式。

我们先构建一个用于生成对象的函数,该函数包含以下步骤: ①. 创建新对象。实现方式有四种: 【1】构建对象字面量。 【2】调用构造函数。 【3】基于已存在对象构建新实例。 【4】调用任意返回对象的函数。 ②. 定义私有实例变量与方法。 ③. 为新对象扩展特权方法,这些方法有权访问上述私有变量。 ④. 返回新对象。

函数化构造器的伪代码结构如下:


var constructor = function (spec, my){
  var that, 其他私有变量;
  my = my || {};
 //把共享的变量和函数添加到 my 中
 that = 一个新对象
 //添加给 that 的特权方法
 return that;
};

spec对象包含了构建新实例所需的所有信息,可用于访问私有变量或其他函数。 my对象为继承链中的构造函数提供了共享容器;若未传入,则会自动创建一个新的my对象。

创建特权方法的方式是:先将函数定义为私有方法,随后将其赋值给that对象:


var methodical = function (){
 ...
};
that.methodical = methodical;

这种分两步定义方式的优势在于:私有的methodical方法不受实例后续变更的影响。

现在,我们将此模式应用于mammal示例中:


var mammal = function (spec) {
  var that = {};
  that.get_name = function () {
    return spec.name;
  };
  that.says = function () {
    return spec.saying || '';
  };
  return that;
};
var myMammal = mammal({name: 'Herb'});
console.log(myMammal.get_name());//Herb
var cat = function (spec) {
  spec.saying = spec.saying || 'meow';
  var that = mammal(spec);
  that.purr = function (n) {
    var i, s = '';
    for (i = 0; i < n; i += 1) {
      if (s) {
        s += '-';
      }
      s += 'r';
    }
    return s;
  };
  that.get_name = function () {
    return that.says() + ' ' + spec.name + ' ' + that.says();
  };
  return that;
};
var myCat = cat({name: 'Henrietta'});
console.log(myCat.says());//meow
console.log(myCat.purr(5));//r-r-r-r-r
console.log(myCat.get_name());//meow Henrietta meow

函数化模式还能调用父类的方法。此处我们构建一个superior方法,该方法返回调用特定方法名的函数:


//返回调用某个方法名的函数
Object.method('superior', function (name) {
  var that = this,
    method = that[name];
  return function () {
    return method.apply(that, arguments);
  };
});

接着创建coolcat对象,其拥有能够调用父类方法的get_name函数:


var coolcat = function (spec) {
  var that = cat(spec),
    super_get_name = that.superior('get_name');
  that.get_name = function (n) {
    return 'like ' + super_get_name() + ' baby';
  };
  return that;
};
var myCoolCat = coolcat({name: 'Bix'});
console.log(myCoolCat.get_name());//like meow Bix meow baby

函数化模式具备极高的灵活性,能更好地实现封装、信息隐藏以及访问父类方法的能力。

若对象的所有状态均为私有,则称为防伪对象。该对象的属性可被替换或删除,但其内部状态不受影响。若使用函数化模式创建对象,且所有方法均不使用this或that,则该对象具有持久性,不会被外部入侵。除非存在特权方法,否则无法访问该持久性对象的内部状态。

5 事件处理函数

我们可以构建一个函数,用于为任何对象添加简单的事件处理特性。此处,我们为对象添加了on方法、fire方法以及私有的事件注册对象:


var eventuality = function (that) {
  var registry = {};
  /**
   * 触发事件
   *
   * 使用 'on' 方法注册的事件处理程序将被调用
   * @param 可以是包含事件名称的字符串,或者是一个拥有 type 属性(值为事件名称)的对象。
   */
  that.fire = function (event) {
    var array,
      func,
      handler,
      i,
      type = typeof event === 'string' ? event : event.type;
    //如果这个事件已被注册,则遍历并依序执行
    if (registry.hasOwnProperty(type)) {
      array = registry[type];
      for (i = 0; i < array.length; i += 1) {
        handler = array[i];//处理程序包含一个方法和一组可选的参数
        func = handler.method;
        if (typeof func === 'string') {//如果方法是字符串形式的名称,则寻找它
          func = this[func];
        }
        //调用它。如果处理程序包含参数,则传递过去,否则就传递事件对象
        func.apply(this, handler.parameters || [event]);
      }
    }
    return this;
  };
  /**
   * 注册一个事件
   * @param type
   * @param method
   * @param parameters
   */
  that.on = function (type, method, parameters) {
    var handler = {
      method: method,
      parameters: parameters
    };
    if (registry.hasOwnProperty(type)) {//如果已存在,就新增数组项
      registry[type].push(handler);
    } else {//新增
      registry[type] = [handler];
    }
    return this;
  };
  return that;
};

可以在任何单独对象上调用eventuality函数,从而赋予其事件处理方法。也可以在构造函数返回that之前,在构造函数内部调用它:


eventuality(that);

JavaScript的弱类型特性在此成为巨大优势,因为我们无需处理对象继承关系中的类型检查问题。

感兴趣的朋友还可以使用本站在线HTML/CSS/JavaScript代码运行工具:http://tools.jb51.net/code/HtmlJsRun测试上述代码运行结果。

更多关于JavaScript相关内容还可查看本站专题:《javascript面向对象入门教程》、《JavaScript错误与调试技巧总结》、《JavaScript数据结构与算法技巧总结》、《JavaScript遍历算法与技巧总结》及《JavaScript数学运算用法总结》

希望本文所述对大家JavaScript程序设计有所帮助。

实用工具推荐

电子包浆

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

使用 ›
图片压缩

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

使用 ›
相关文章