JavaScript 面向对象编程(一):封装

作者:纸飞机

时间:2026-10-26

浏览:71

本文详解 JavaScript 面向对象编程中的封装概念,对比原始对象模式、构造函数模式与 Prototype 模式的优劣。重点解析如何通过 prototype 属性解决内存浪费问题,并介绍 isPrototypeOf、hasOwnProperty 及 in 运算符在验证原型关系中的应用。

文章目录

学习 JavaScript 时,最具挑战性的部分通常被认为是对象(Object)模型。

由于 JavaScript 的对象模型设计独特,与其他主流编程语言存在显著差异,初学者往往难以快速掌握其核心机制。

以下整理自个人学习笔记,旨在帮助读者理解这一部分,主要参考了《面向对象的 JavaScript》与《JavaScript 高级程序设计(第二版)》。

JavaScript 对象模型概述

JavaScript 对象模型概述

《Javascript高级程序设计(第二版)》(Professional JavaScript for Web Developers, 2nd Edition)

这两本著作均为 JavaScript 领域的经典读物,强烈建议读者阅读。

本系列笔记共分为三部分:第一部分聚焦于“封装”(Encapsulation),后续的第二部分和第三部分将分别探讨“继承”(Inheritance)的不同场景。

============================

JavaScript 面向对象编程(一):封装

作者:阮一峰

JavaScript 对象模型概述

JavaScript 是一种基于对象的编程语言,开发者遇到的绝大多数实体均可视为对象。然而,由于其语法体系中缺乏传统的 class(类)定义,它并不完全符合传统意义上面向对象编程(OOP)的标准。

若要将“属性”(property)与“方法”(method)封装为单一对象,甚至基于原型对象创建实例对象,应如何实现?

生成对象的原始模式

1. 生成对象的原始模式

假设我们将“猫”定义为一个对象,它包含“名字”和“颜色”两个属性。

代码如下:

 
var Cat = {
name : '',
color : ''
  } 

基于上述原型对象,我们需要创建两个具体的实例对象。


代码如下:

 
var cat1 = {}; // 创建一个空对象
cat1.name = "大毛"; // 按照原型对象的属性赋值
cat1.color = "黄色";
var cat2 = {};
cat2.name = "二毛";
cat2.color = "黑色";

这实现了最基础的封装,但存在两个明显缺陷:首先,若需生成大量实例,代码编写将极为繁琐;其次,实例与原型之间缺乏显式的联系标识。


2. 原始模式的改进

原始模式的改进

我们可以编写一个函数来消除代码重复问题。


代码如下:

 
function Cat(name,color){
return {
name:name,
color:color
    } 
  } 

生成实例对象的过程,实质上等同于调用该函数。


代码如下:

 
var cat1 = Cat("大毛","黄色");
var cat2 = Cat("二毛","黑色");

此方法仍未能解决 cat1 与 cat2 之间缺乏内在联系的问题,无法体现它们源自同一原型对象的事实。


3. 构造函数模式

构造函数模式

为解决从原型对象生成实例的问题,JavaScript 引入了构造函数(Constructor)模式。


构造函数本质上是一个普通函数,但其内部使用了 this 变量。通过 new 运算符调用构造函数,即可生成实例,此时 this 变量会绑定到具体的实例对象上。


例如,猫的原型对象可改写为如下形式:


代码如下:

 
function Cat(name,color){
this.name=name;
this.color=color;
  } 

现在,我们可以直接生成实例对象。


代码如下:

 
var cat1 = new Cat("大毛","黄色");
var cat2 = new Cat("二毛","黑色");
alert(cat1.name); // 大毛
alert(cat1.color); // 黄色

此时,cat1 和 cat2 会自动拥有一个 constructor 属性,该属性指向其对应的构造函数。


代码如下:

 
alert(cat1.constructor == Cat); //true
alert(cat2.constructor == Cat); //true

JavaScript 还提供 instanceof 运算符,用于验证原型对象与实例对象之间的继承关系。


代码如下:

 
alert(cat1 instanceof Cat); //true
alert(cat2 instanceof Cat); //true

4. 构造函数模式的问题

构造函数模式的问题

尽管构造函数模式实用,但它存在内存浪费的问题。


假设我们为 Cat 对象添加一个固定属性“type”(种类)和一个方法 eat(吃老鼠),原型对象 Cat 的定义将变为:


代码如下:

 
function Cat(name,color){
this.name = name;
this.color = color;
this.type = "猫科动物";
this.eat = function(){alert("吃老鼠");};
  } 

采用相同的方法生成实例:


代码如下:

 
var cat1 = new Cat("大毛","黄色");
var cat2 = new Cat ("二毛","黑色");
alert(cat1.type); // 猫科动物
cat1.eat(); // 吃老鼠

表面上看似乎无误,但实际存在重大弊端:每个实例对象的 type 属性和 eat() 方法都包含完全相同的内容。每次生成新实例时,都会重复创建这些内容,占用额外内存,既低效又不环保。


alert(cat1.eat == cat2.eat); //false


能否让 type 属性和 eat() 方法仅在内存中生成一次,并使所有实例指向该内存地址?答案是肯定的。


5. Prototype模式

Prototype 模式

JavaScript 规定,每个构造函数都拥有一个 prototype 属性,该属性指向另一个对象。该对象的所有属性和方法,均会被构造函数的实例所继承。


这意味着,我们可以将那些不变的属性和方法,直接定义在 prototype 对象上。


代码如下:

 
function Cat(name,color){
this.name = name;
this.color = color;
  } 
Cat.prototype.type = "猫科动物";
Cat.prototype.eat = function(){alert("吃老鼠")};

随后生成实例。


代码如下:

 
var cat1 = new Cat("大毛","黄色");
var cat2 = new Cat("二毛","黑色");
alert(cat1.type); // 猫科动物
cat1.eat(); // 吃老鼠

此时,所有实例的 type 属性和 eat() 方法均指向同一内存地址(即 prototype 对象),从而显著提高了运行效率。


alert(cat1.eat == cat2.eat); //true


6. Prototype模式的验证方法

Prototype 模式的验证方法

6.1 isPrototypeOf()


该方法用于判断某个 prototype 对象与特定实例之间的关系。


代码如下:

 
alert(Cat.prototype.isPrototypeOf(cat1)); //true
alert(Cat.prototype.isPrototypeOf(cat2)); //true

6.2 hasOwnProperty()


每个实例对象均具备 hasOwnProperty() 方法,用于判断某一属性是本地属性还是继承自 prototype 对象的属性。


代码如下:

 
alert(cat1.hasOwnProperty("name")); // true
alert(cat1.hasOwnProperty("type")); // false

6.3 in运算符


in 运算符可用于判断实例是否包含特定属性,无论该属性是本地定义还是继承所得。


代码如下:

 
alert("name" in cat1); // true
alert("type" in cat1); // true

此外,in 运算符还可用于遍历对象的所有属性。


for(var prop in cat1) { alert("cat1["+prop+"]="+cat1[prop]); }


未完待续,请继续阅读本系列第二部分《构造函数的继承》和第三部分《非构造函数的继承》。



(完)

实用工具推荐

电子包浆

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

使用 ›
图片压缩

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

使用 ›
相关文章