JavaScript面向对象程序设计详解:对象创建模式与原型链继承

作者:木棉

时间:2026-10-23

浏览:96

本文深入解析JavaScript面向对象程序设计,涵盖对象创建的五种核心设计模式:单例、工厂、构造函数、原型及混合模式,详细对比其优缺点。重点阐述ES5数据与访问器属性特性,并系统讲解原型对象、__proto__内部属性及原型链继承机制,结合Object系列方法实现属性遍历与原型操作,帮助开发者掌握JS高级编程技巧。

文章目录

导语

在之前的系列文章中,我们已经系统梳理了JavaScript的核心语法与标准库基础知识。本章将进入JavaScript高级编程的核心领域——面向对象程序设计(OOP)。本节内容旨在深化对对象数据类型的理解,详细介绍多种创建对象的设计模式,并深入剖析JavaScript独特的继承机制。

1.理解对象和面向对象的程序设计

1.1 面向对象的程序设计

面向对象编程(Object Oriented Programming,简称OOP)是一种编程思维范式。它将世界万物视为对象的集合,认为世界的运行依赖于各个对象之间的分工与协作,体现了“一切皆对象”的核心思想。在程序设计中,面向对象编程意味着编写具有特定功能的对象(模块),并通过有机的分工合作来实现系统功能。目前的模块化编程正是面向对象程序设计在实际应用中的具体体现。

1.2 理解对象

在前面的系列文章中曾提及,从数据特征的角度来看,对象是无序属性(键值对)的集合。我们可以使用字面量或构造函数的方式来创建一个最简单的对象:


var person = new Object();
person.name = "teren";
person.age = 18;
person.greet = function(){
   console.log("hello "+this.name);
}

var teren = {
 name:"teren",
 age:18,
 greet:function(){
   console.log("hello "+this.name);
 }
}

通常情况下,创建简单对象首选字面量方式。上述对象是对现实对象的一种抽象表达。

1.3 对象的属性类型

在前面的章节中,我们使用delete命令可以删除某些对象属性,但有些属性却无法删除;使用Object.keys()方法只能遍历可枚举属性。那么,对象的属性是否具有一些我们尚未了解的特性呢?ES5提供了一种仅内部使用的特性(attribute)来描述对象属性(property)的各种特征,记作[[attribute]]。在JavaScript中,这些特性不能直接访问。一个我们非常熟悉的例子是Number构造函数构造出来的实例对象:

1.理解对象和面向对象的程序设计

我们无法直接访问num.[[PrimitiveValue]],这一属性,只能通过num.valueOf()访问该值;

ES5中定义了对象属性的两种特性:数据特性和访问器特性。对象属性可以同时具备这两种特性。

数据特性定义了对象属性值的特征,一个属性值可以包括以下四个数据特性:


[[Value]]:存放属性值;
[[Writable]]:是否可写属性;
[[Enumerable]]:是否为可枚举属性;
[[Configurable]]:是否可用delete命令删除;

访问器特性定义了对象在访问属性和设置属性时调用的两个函数:getter和setter。

[[Get]]:访问属性时调用的函数; [[Set]]:设置属性时调用的函数;

下面以一个实例对象直接讲解这两个特性:


//数据特性;
var teren = {};
Object.defineProperty(teren,{
  value:"teren",
  writable:false,
  enumerable:true,
  configurable:true
})

//访问器特性;
//html
<div id="name"></div>
//js
var obj = Object.defineProperty({},"name",{
set:function(name){
 document.getElementById('name').innerHTML=name
},
get:function(){
 console.log( document.getElementById('name').innerHTML
  ) 
 },
})
obj.name = "hello world"
obj.name

[demo]

Object.defineProperties可以一次性配置对象的多个属性。

1.理解对象和面向对象的程序设计

2. 创建对象的方式

上一节我们对面向对象程序设计思想和对象有了初步理解,这一节我们将深入探讨对象的创建方式及其优缺点。

创建对象的不同方式也可以简单地称为设计模式,不同的设计模式在实际编程应用中起到不同的作用。

2.1 单例模式

单例模式旨在产生一个类的唯一实例对象,确保只有一个对象实例能够实际派上用场。

单例模式下,创建对象方式如下:


var singleton = {
 attr:1,
 method:function(){
  return this.attr
 }
}
var ex1 = singleton;
var ex2 = singleton;
ex1 === ex2//true

上述创建单例的方式:

优点:使用非常简捷; 缺点:缺乏封装,成员暴露,初始化时占用资源;

可以使用闭包方式解决这一问题:


var substance = (function(){
 var unique; 
 function init(){
  var type;  
  return {
   setType:function(t){
    return type = t;
   }
  }
 }
 return {
  getInstance:function(){
   if(!unique){
    unique = init();
   }
   return unique;
  } 
 }
})();

var Adam = substance.getInstance();
var Eve = substance.getInstance();
Adam === Eve
Adam.setType('Man')//Man

1.理解对象和面向对象的程序设计

2.2 工厂模式

单例模式只能创建单个实例对象,也就是说如果将该实例对象赋予多个变量时,会存在对象的引用问题,即修改其中一个变量会影响到另一个变量。有时我们需要创造多个结构相似的对象,只有部分属性有所区别,这时候工厂模式派上用场。

工厂模式的设计思想就是能够像工厂一样批量生产出相似属性和方法的对象,使用工厂模式能解决多个相似的问题,例如创造多个弹窗(只是标题不同)。


function person(name,age){
 var obj = new Object();
 obj.name = name;
 obj.age = age;
 obj.greet = function(){
   return "hello "+this.name;
 };
 return obj
}
var Adam = person("Adam",18);
var Eve = person("Eve",20);

上述工厂模式:

优点:能批量生产结构类似的对象;封装创建对象的细节; 缺点:未能解决对象的类型,即由哪个构造函数创建的;

2.3 构造函数模式

构造函数可以创建特定类型的对象,类似之前的Array、RegExp等原生对象都能创造特定类型的实例对象。


function Person(name,age){
 this.name = name;
 this.age = age;
 this.greet = function(){
   return "hello "+this.name;
 }
}
var p1 = new Person('Adam',18);
var p2 = new Person('Eve',20);

使用构造函数模式就能够解决实例对象由谁创建的问题。

1.理解对象和面向对象的程序设计

上述代码和工厂模式的区别在于:

1.没有显示创建新对象; 2.直接将属性和方法赋给this对象; 3.没有return语句; 4.函数名开头大写以区别普通函数; 5.使用new操作符去创建对象实例;

new操作符的原理

使用new操作符去调用函数和直接调用函数不同,其new操作符的运行函数的过程为:

创建一个新对象; 将构造函数的作用域赋给新对象并执行构造函 内的代码; 返回新对象; 使用代码表示如下:


function Person(name,age){
 this.name = name;
 this.age = age;
 this.greet = function(){
   return "hello "+this.name;
 }
}
function createPerson(name,age){
  var o = new Object();
  Person.call(o,name,age);
  return o;
}
var p1 = createPerson('Adam',18);
var p2 = createPerson('Eve',20);

使用构造函数模式创建对象的优缺点在于:

优点:能够识别对象属于的构造函数; 缺点:如果存在不同实例对象共享的属性和方法,使用构造函数模式则会浪费内存;

【注】 关于this关键字的更多知识点可以参见【what's this】; 构造函数如果不用new操作符调用和普通函数是一样的;

2.4 原型模式

每个函数都有一个prototype原型属性,这个原型属性可以部署特定类型的实例共享的属性和方法。


function Person(){}
}
Person.prototype.greet = function(){
 return "hello "+this.name;

将原来的greet函数部署在Person函数的prototype原型属性上,这样p1和p2可以共享该方法,而不像构造函数模式每创建一个实例就增加一个greet方法浪费内存。

【注】 关于原型对象的更多理解详见下一节——JavaScript的继承机制;

使用原型模式创建对象的优缺点在于:

优点:对于每个实例的共享属性和方法能够较好实现; 缺点:单独采用原型模式将无法区分不同实例的私有属性;

2.5 混合模式

混合模式,就是综合构造函数模式和原型模式的优缺点,构造函数模式部署实例的私有属性,原型模式部署实例的公有属性。


function Person(name,age){
  this.name = name;
  this.age = age;
}
Person.prototype.greet = function(){
  return "hello "+this.name;
}
var p1 = new Person("Adam",18);
var p2 = new Person("Eve",20);

混合模式是目前使用最广泛、认同度最高的一种创建自定义类型(类)的设计模式。

【注】 当然,设计模式不仅仅上述所提到,还有更加精深可以参考《设计模式》一书以及之前小羊写的一篇文章《设计模式梗概》;

3.JavaScript的继承机制

上一节我们通过创建对象的不同模式,隐式引出了原型对象的概念,这一节中我们将详细了解一下原型对象、原型链及其实现的继承机制。

前面,我们从数据特征上看,知道对象是无序属性(键值对)的集合。 现在,我们可以从面向对象的角度看,任何对象都是更为抽象的对象的实例,可以理解为类的概念。 从这个角度理解,我们现在可以重新定义一下对象和类的含义: 对象可以说是对现实事物的抽象,对象封装了属性和方法,属性值指的是对象的状态,方法指的是对象的行为; 类是提供一种模板的‘对象',它是对象的抽象; 举个简单的栗子:


function Person(name,age){
  this.name = name;
  this.age = age;
}
Person.prototype.greet = function(){
  return "hello "+this.name;
}
var p1 = new Person("Adam",18);
var p2 = new Person("Eve",20);

上述代码表明,p1和p2两个实例是对现实Adam和Eve的抽象,而“类”Person又是对2个实例的抽象,为创建相似结构的人提供标准的模板; [注]ES6之前JavaScript中没有类,在ES6中定义了类;

3.1 原型对象

在上一节的原型模式中,我们提到每个函数都有一个prototype属性,这个属性指向函数的原型对象,可以部署特定类型的实例共享的属性和方法。

更为深入理解prototype原型对象,prototype原型对象不仅可以部署特定类型的实例共享的属性和方法,而且还是实现JavaScript继承的关键。

只要创建一个新函数就会为该函数创建一个prototype属性,每个prototype属性自动获得一个constructor属性,该属性指向prototype属性所在的函数指针。

当使用构造函数创建一个实例时,该实例内部包含一个内部属性__proto__指向构造函数的原型对象。

由此,一个简单的继承便产生了; 以下面代码为例:


function Person(name,age){
  this.name = name;
  this.age = age;
}
Person.prototype.greet = function(){
  return "hello "+this.name;
}
var p1 = new Person("Adam",18);
var p2 = new Person("Eve",20);

1.理解对象和面向对象的程序设计

构造函数创建之后,自动创建一个prototype属性,prototype原型对象下有一个默认的constructor属性指向prototype属性所在的函数Person中; 在prototype原型对象上部署greet方法,实例p1的内部属性__proto__指向构造函数Person.prototype,由此继承了构造函数的原型对象上的greet方法;

【注意】

  • 实例的__proto__指向构造函数的prototype原型对象实现继承,这种联系存在于实例与构造函数的原型对象之间而不是构造函数之间;
  • 当js引擎解析对象的属性时,先会搜索对象本身的属性,如果没有则会去搜索__proto__指向的原型对象上的属性,直到找到为止,如果在对象本身定义的属性和原型对象上的具有相同属性名,则在读取该属性时,自身属性会屏蔽原型对象上的属性;

function Person(name,age){
  this.name = name;
  this.age = age;
}
Person.prototype.greet = function(){
  return "hello "+this.name;
}
var p1 = new Person("Adam",18);
p1.greet()//hello Adam;
p1.greet = function(){
  return "hello world"
}
1.理解对象和面向对象的程序设计

修改构造函数的原型对象可以直接使用点操作,效果是直接在原来的原型对象上增加属性,有时需要增加的属性太多是,点操作就显得太麻烦,可以采用重置原型对象的方法:


function Person(name,age){
  this.name = name;
  this.age = age;
}
Person.prototype = {
 constructor:Person,
 greet1:function(){},
 greet2:function(){},
 greet3:function(){}
};
var p1 = new Person("Adam",18);
Person.prototype.constructor.name//"Object"

需要注意的是,重置原型对象后,要重新为原型对象的constructor的属性指回Person构造函数; 如果不重置constructor的话,那么此时的Person.prototype是由字面量创建的对象,字面量创建的对象默认的构造函数是Object;

3.2 原型链

上面我们只定义一个构造函数,实现一次继承;如果存在多个构造函数,它们之间也存在继承关系,那么就会形成一条关于继承的原型链。


function SuperType(name,age){
  this.name = name;
  this.age = age
}
SuperType.prototype.greet = function(){
  return "hello "+this.name
}
function SubType(name,age,height){
  SuperType.call(this,name,age);
  this.height = height;
}
SubType.prototype = Object.create(SuperType.prototype);
SubType.prototype.constructor = SubType;
SubType.prototype.method = function(){return 1;}

var instance = new SubType('teren',18,180)

上面就是一个最为常用的实现多个类间继承的设计模式; 使用Object.create(SuperType.prototype)的优缺点在于:

优点:能够创建一个新的SuperType.prototype对象赋给SubType.prototype,修改SubType.prototype这个而不影响原来构造函数SuperType.prototype; 缺点:虽然拥有子类的prototype和父类的prototype值是相同的,但内存不同,从而切断子类和父类之间的类型;

1.理解对象和面向对象的程序设计

还可以使用SubType.prototype =new SuperType()实现相同效果,其优缺点在于:

优点:能够体现子类和父类的继承关系; 缺点:子类具有父类的私有属性;

1.理解对象和面向对象的程序设计

所以,一般在实际实现原型链时使用Object.create()方法,而理解原型链时使用new SuperType()方法;1.理解对象和面向对象的程序设计

3.3 与原型对象相关的方法

遍历对象属性方法 Object.keys()和Object.getOwnPropertyNames()用于遍历对象自身而不是继承的属性名,返回一个数组,其中Object.keys()只返回可枚举属性;

1.理解对象和面向对象的程序设计

in用于检查一个对象是否具有某个属性。它不区分该属性是对象自身的属性,还是继承的属性; for...in用于遍历对象的所有可枚举属性(不管是自身的还是继承的)

1.理解对象和面向对象的程序设计

如果只遍历自身的属性,可以使用如下代码:


for (var key in instance){
 if(instance.hasOwnProperty(key)){
   console.log(key)
 }
}

判断属性是否为自身的方法

Object.prototype.hasOwnProperty()返回一个布尔值,用于判断某个属性定义在对象自身,还是定义在原型链上;

1.理解对象和面向对象的程序设计

设置和获取实例对象的原型对象的方法

Object.getPropertyOf()返回一个实例对象的原型对象; Object.setPropertyOf(obj,prototype)可传两个参数,第1个为现有参数,第2个为原型对象;

1.理解对象和面向对象的程序设计

判断一个对象是否为另一个对象的原型对象

Object.prototype.isPrototypeOf()用于判断一个对象是否是另一个对象的原型;

1.理解对象和面向对象的程序设计

小结

通读本文,我们可以知道:

  • 面向对象编程时一种思维模式,它把世界的一切看做是对象的集合,世界的运作是一个个对象分工合作的结果;映射到程序设计中,就是编写各个具有特定功能的对象(模块),并将它们有机整合使程序得以运作;
  • 对象从数据特征角度看,是无序键值对的集合,对象的属性具有两种特性——数据特性和访问器特性;
  • 创建对象的不同方式可以称为设计模式,本文简单讲解了单例模式、工厂模式、构造函数模式、原型模式、混合模式等;
  • 从面向对象角度看,对象可以说是对现实事物的抽象,类是对对象的抽象;
  • 每个函数都有一个原型对象prototype,既可以部署特定类型实例的共享属性和方法,也是JavaScript实现继承的关键;
  • prototype原型对象有一个constructor属性,该属性指向prototype所在的函数指针;
  • 每当使用构造函数创建一个实例时,该实例内部包含一个内部属性__proto__指向构造函数的原型对象,由此实现简单的继承;
  • 当A构造函数是B构造函数的实例时,由此就会形成一条原型链,即 A构造函数的实例对象C的__proto__指向A构造函数的原型对象prototype,A构造函数prototype的__proto__指向B构造函数的原型对象prototype,B构造函数prototype的__proto__指向Function构造函数的prototype,Function的prototype的__proto__指向Object的prototype;
  • 与原型对象的相关方法包括:Object.keys()和Object.getPropertyNames()、for...in方法,Object.getPrototypeOf()和Object.setPrototypeOf()方法,Object.prototype.hasOwnProperty()和Object.prototype.isPrototypeOf()方法;

参考资料

《JavaScript高级程序设计(第3版)》 《JavaScript标准参考教程》——阮一峰

实用工具推荐

电子包浆

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

使用 ›
图片压缩

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

使用 ›
相关文章