JavaScript 创建自定义类型的 7 种模式与 ES6 Class 原理

作者:半夏时间:2026-10-22 20:33:45浏览:197

本文深入解析 JavaScript 中创建自定义类型的七种常用模式,包括工厂、构造函数、原型、组合、动态原型、寄生及稳妥构造函数模式。通过对比各模式的优缺点,揭示 ES6 Class 语法糖背后的组合使用构造函数模式本质,帮助开发者根据实际需求选择最佳实践。

JavaScript 自定义类型创建模式概述

本文详细介绍 JavaScript 中构建自定义类型的常用模式,供读者参考与学习。

尽管 ES6 引入了 class 语法,看似让人可以忽略 ES5 时代的旧有模式,但随着深入学习的进行,理解这些基础模式的重要性愈发凸显。

本文将阐述七种构建自定义类型的常用模式:工厂模式、构造函数模式、原型模式、组合使用构造函数模式、动态原型模式、寄生构造函数模式以及稳妥构造函数模式。文章将逐一提供示例代码,分析其利弊,辅助读者根据具体场景选择最合适的构建方式。

最后,文章将指出 ES6 中的 class 语法本质上依然基于组合使用构造函数模式来创建自定义类型。

工厂模式

1. 工厂模式

首先展示工厂模式的示例代码:


function createPerson(name, age, job){
  var o = new Object();      // 创建对象
  o.name = name;         // 赋予对象细节
  o.age = age;          // 赋予对象细节
  o.job = job;          // 赋予对象细节
  o.sayName = function(){     // 赋予对象细节
    alert(this.name);
  };
  return o;            // 返回该对象
}
var person1 = createPerson("Nicholas", 29, "Software Engineer");
var person2 = createPerson("Greg", 27, "Doctor");

优点:有效解决了创建多个相似对象的问题;

缺点:未能解决对象识别问题(即无法判断对象的具体类型),且对象方法无法实现复用。

构造函数模式

2. 构造函数模式


function Person(name, age, job){
  this.name = name;    // 对象的所有细节全部挂载在 this 对象下面
  this.age = age;
  this.job = job;
  this.sayName = function(){
    alert(this.name);
  };
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");

讨论构造函数模式时,必须涉及 new 操作符。以下分析 new 操作符的具体执行过程:

① 创建一个新对象; ② 将构造函数内部的 this 指向该新创建的对象,同时将函数的 prototype 引用挂载到新对象的原型链上; ③ 执行函数体内的代码,即把属性和方法挂载到新对象上; ④ 隐式返回新创建的对象。

优点:解决了对象识别的问题;

缺点:自定义类型的方法每次都会创建新的函数实例,导致无法复用。若将所有方法函数移至父级作用域,虽实现了复用,但方法只能在父级作用域的特定类型中调用,这使父级作用域名不副实,且自定义引用类型缺乏封装性。

原型模式

3. 原型模式


function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
  alert(this.name);
};
var person1 = new Person();
person1.sayName();   //"Nicholas"
var person2 = new Person();
person2.sayName();   //"Nicholas"
alert(person1.sayName == person2.sayName);   //true

理解要点如下:

① 每当创建一个新函数,系统便会根据特定规则为该函数创建一个 prototype 属性,该属性指向函数的原型对象。 ② 默认情况下,所有原型对象自动获得一个 constructor 属性,该属性包含一个指向 prototype 属性所在函数的指针。原型中的其他方法则继承自 Object。 ③ 调用构造函数创建新实例后,该实例内部将包含一个指针 [[prototype]](内部属性),指向构造函数的原型对象。 ④ 调用构造函数创建新实例后,实例环境(即构造函数)会针对原型对象上的非引用类型属性,在构造函数中自动构建相应的实例环境属性。这意味着,后续由构造函数创建的实例,其非引用类型属性均源自构造函数中的实例环境属性。

为减少冗余输入并更好地在视觉上封装原型功能,常见做法是使用包含所有属性和方法的对象字面量来重写整个原型对象,示例如下:


function Person(){
}
Person.prototype = {
  name : "Nicholas",
  age : 29,
  job: "Software Engineer",
  sayName : function () {
    alert(this.name);
  }
};

这种写法的本质是完全重写了默认的 prototype 对象,因此 constructor 属性变为新对象的 constructor 属性(指向 Object 构造函数),不再指向 Person 函数。尽管此时 instanceOf 操作符仍能返回正确结果。

若 constructor 属性至关重要,可按如下方式将其重置为适当值:


function Person(){
}
Person.prototype = {
  constructor : Person,
  name : "Nicholas",
  age : 29,
  job: "Software Engineer",
  sayName : function () {
    alert(this.name);
  }
};

注意,以这种方式重设 constructor 属性会导致其 [[Enumerable]] 特性被设置为 true。默认情况下,原生 constructor 属性是不可枚举的。因此,若使用兼容 ECMAScript 5 的 JavaScript 引擎,可尝试使用 Object.defineProperty() 方法:


function Person(){
}
Person.prototype = {
  name : "Nicholas",
  age : 29,
  job : "Software Engineer",
  sayName : function () {
    alert(this.name);
  }
};
//重设构造函数,只适用于 ECMAScript 5 兼容的浏览器
Object.defineProperty( Person.prototype, "constructor", {
  enumerable: false,
  value: Person
});

注意,重写原型对象会切断新原型与已有对象实例之间的联系;这些实例引用的仍是最初的原型。

优点:解决了自定义类型方法的函数复用问题。

缺点:

① 无法为构造函数传递初始化参数; ② 原型模式中实现了包含引用类型值属性的共享,这意味着若一个实例修改了该引用类型值,所有实例的该属性均会被修改!!!

组合使用构造函数模式和原型模式

4. 组合使用构造函数模式和原型模式

在组合使用构造函数模式和原型模式中,构造函数模式用于定义实例属性,而原型模式用于定义方法和共享属性,同时支持向构造函数传递参数。示例代码如下:


function Person(name, age, job){
  this.name = name;
  this.age = age;
  this.job = job;
  this.friends = ["Shelby", "Court"];
}
Person.prototype = {
  sayName : function(){
    alert(this.name);
  }
}
Object.defineProperty( Person.prototype, "constructor", {
  enumerable: false,
  value: Person
);
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
person1.friends.push("Van");
alert(person1.friends);  //"Shelby,Count,Van"
alert(person2.friends);  //"Shelby,Count"
alert(person1.friends === person2.friends);   //false
alert(person1.sayName === person2.sayName);   //true

优点:支持向构造函数传递初始化参数;该复用的复用,不该复用的不复用。 缺点:封装性不佳,构造函数和原型分别独立于父级作用域进行声明。

动态原型模式

5. 动态原型模式(推荐)

该模式将所有信息封装在构造函数中,通过构造函数实现原型初始化(仅在必要时),同时保留了构造函数和原型的优点。示例代码如下:


function Person(name, age, job){
  //属性
  this.name = name;
  this.age = age;
  this.job = job;
  //方法
  if (typeof this.sayAge != "function"){   // 此处应该永远去判断新添加的属性和方法
    Person.prototype.sayName = function(){
      alert(this.name);
    };
    Person.prototype.sayAge = function(){
      alert(this.age);
    };
  }
}
var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName();

if 语句可检查初始化后应存在的任何属性或方法——无需检查每个属性和方法的冗长 if 语句;只需检查其中一个即可。

注意,使用动态原型模式时,不能使用对象字面量重写原型。前文已解释,若在已有实例的情况下重写原型,将切断新原型与现有实例之间的联系。

优点:封装性极佳;还可使用 instanceOf 操作符确定其类型。 缺点:无。

寄生构造函数模式

6. 寄生构造函数模式

除使用 new 操作符并将使用的包装函数称为构造函数外,该模式与工厂模式完全一致。代码如下:


function Person(name, age, job){
  var o = new Object();
  o.name = name;
  o.age = age;
  o.job = job;
  o.sayName = function(){
    alert(this.name);
  };
  return o;
}
var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName();    //"Nicholas"

在使用 new 操作符时,若构造函数不返回值,默认返回新对象实例。通过在构造函数末尾添加 return 语句,可重写调用构造函数时返回的值。

缺点:未解决对象识别问题(即无法判断对象类型),不能依赖 instanceOf 操作符来确定对象类型;对象方法未实现复用。

稳妥构造函数模式

7. 稳妥构造函数模式

首先了解稳妥对象:指没有公共属性,且其方法不引用 this 的对象。稳妥对象适用于禁止使用 this 和 new 的安全环境,或防止数据被其他应用程序(如 Mashup 程序)篡改的场景。稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:一是新创建对象的实例方法不引用 this;二是不使用 new 操作符调用构造函数。示例代码如下:


function Person(name, age, job){
  var o = new Object();    //创建要返回的对象
  //可以在这里定义私有变量和函数
  o.sayName = function(){   //添加方法
    alert(name);
  };
  return o;    //返回对象
}
var friend = Person("Nicholas", 29, "Software Engineer");
friend.sayName(); //"Nicholas"

其原理是利用闭包,保留对私有变量和私有方法的引用。

优点:无法通过其他方法访问传入构造函数中的原始数据。 缺点:未解决对象识别问题(即无法判断对象类型),不能依赖 instanceOf 操作符来确定对象类型;对象方法未实现复用。

ES6 Class 语法原理

8. ES6 中的 class

此处以 class 实例为例展开讲解:


class Parent {
  name = "qck";
  sex = "male";
  //实例变量
  sayHello(name){
    console.log('qck said Hello!',name);
  }
  constructor(location){
   this.location = location;
  }
}

查看该代码经 babel 编译后的 _createClass 函数:


var _createClass = function () {
  function defineProperties(target, props) {
    for (var i = 0; i < props.length; i++) {
      var descriptor = props[i];
      // 对属性进行数据特性设置
      descriptor.enumerable = descriptor.enumerable || false; // enumerable设置
      descriptor.configurable = true;             // configurable设置
      if ("value" in descriptor) descriptor.writable = true; // 如果有value,那么可写
      Object.defineProperty(target, descriptor.key, descriptor); // 调用defineProperty() 进行属性设置
    }
  }
  return function (Constructor, protoProps, staticProps) {
    // 设置到第一个 Constructor 的 prototype 中
    if (protoProps) defineProperties(Constructor.prototype, protoProps);
    // 设置 Constructor 的 static 类型属性
    if (staticProps) defineProperties(Constructor, staticProps);
    return Constructor;
  };
}();

首先,该方法为自执行函数,接收三个参数:构造函数本身、需添加到构造函数原型对象的方法或属性、需添加到构造函数的静态属性对象。从该函数可见,class 在创建自定义类型时应用了原型模式。

观察编译后如何调用 _createClass:


var Parent = function () {   // 这里是自执行函数
  _createClass(Parent, [{   // Parent的实例方法,通过修改Parent.prototype来完成
    key: "sayHello",
    value: function sayHello(name) {
      console.log('qck say Hello!', name);
    }
  }]);
  function Parent(location) {   //在Parent构造函数中添加实例属性
    _classCallCheck(this, Parent);
    this.name = "qck";
    this.sex = "male";
    this.location = location;
  }
  return Parent;
}();
function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}

_createClass 的调用位置证实了前述观点——确实应用了原型模式:class 上的方法实际上是通过修改该类(即函数)的 prototype 来完成的。

通过返回的构造函数可以发现:实例属性仍通过构造函数方式添加。

最后,查看 _classCallCheck 方法,它实际上是一层校验,确保实例对象属于特定类型。

综上所述,ES6 中的 class 仅是语法糖,其本质仍是用组合使用构造函数模式创建自定义类型,这也是学习上述知识的原因所在。

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

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

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

作者最新文章