在JavaScript中,类(Class)的概念与传统的面向对象编程语言(如Java、C++)有很大不同。传统语言中的类是一种模板,用于创建具有相同属性和方法的对象实例, 而JavaScript最初并没有类的语法,而是采用基于原型(prototype)的继承机制。每个对象都可以作为另一个对象的原型,从而实现属性和方法的共享。
直到ES6(ECMAScript 2015)引入了class关键字,JavaScript才拥有了更接近传统面向对象语言的类语法,但其本质依然是基于原型的。也就是说,class只是对原型继承的一种语法糖,底层机制并没有改变。

在JavaScript中,类的本质是通过原型对象来实现属性和方法的共享。每当我们创建一个对象时,如果它继承自某个原型对象,那么这个对象就自动拥有了原型上定义的属性和方法。原型对象相当于为所有同类实例提供了一个公共的行为模板。
我们通常会先定义一个包含通用方法的原型对象,然后通过某种方式让新创建的对象继承这个原型。这样,每个实例对象都可以访问原型上的方法,实现了行为的复用和统一。这种机制使得JavaScript能够在没有传统类结构的情况下,实现面向对象编程的核心特性。
让我们通过一个简单的图书类来理解这个概念:
|// 创建一个用于继承的函数 function inherit(proto) { function EmptyConstructor() {} EmptyConstructor.prototype = proto; return new EmptyConstructor(); } // 定义一个图书工厂函数 function createBook(title, author) { // 创建一个继承自图书原型的对象 var book = inherit(createBook.methods); // 设置图书的基本信息 book.title = title; book.author = author; return book; } // 定义图书类的原型对象,包含共享的方法 createBook.methods = { // 获取图书信息 getInfo: function() { return this.title + " 作者:" + this.author; }, // 检查是否为指定作者的作品 isWrittenBy: function(author) { return this.author === author; }, // 字符串表示 toString: function() { return "《" + this.title + "》"; } }; // 使用示例 var book1 = createBook("红楼梦", "曹雪芹"); var book2 = createBook("西游记", "吴承恩"); console.log(book1.getInfo()); console.log(book1.isWrittenBy("曹雪芹")); console.log(book1.toString());
|红楼梦 作者:曹雪芹 true 《红楼梦》
这种方式虽然可行,但并不是JavaScript中定义类的惯用方法,因为它没有使用构造函数。构造函数是专门用于初始化新创建对象的函数,它与原型机制结合使用,形成了JavaScript中最常见的类定义模式。
构造函数是JavaScript中定义类的标准方式。构造函数通过new关键字调用,会自动创建新对象,因此构造函数本身只需要负责初始化对象的状态。 构造函数调用的关键特性是:构造函数的prototype属性会成为新对象的原型,这意味着所有使用同一构造函数创建的对象都继承自同一个对象,因此属于同一个类。
让我们将前面的图书类改写为使用构造函数的形式:
|// 图书构造函数 function Book(title, author) { this.title = title; this.author = author; } // 在原型上定义共享方法 Book.prototype = { getInfo: function() { return this.title + " 作者:" + this.author; }, isWrittenBy
|三国演义 作者:罗贯中 true 《三国演义》
构造函数与工厂函数的主要区别在于调用方式和命名约定。构造函数通常以大写字母开头,使用new关键字调用,而工厂函数以小写字母开头,直接调用。构造函数不需要显式创建和返回对象,这些操作由new关键字自动完成。
每个JavaScript函数都会自动获得一个prototype属性,该属性的值是一个对象,该对象包含一个不可枚举的constructor属性。 constructor属性的值就是函数对象本身。这种预定义的原型对象及其constructor属性意味着对象通常会继承一个引用其构造函数的constructor属性。
|function Student(name, grade) { this.name = name; this.grade = grade; } var student1 = new Student("小明", 85); console.log(student1.constructor === Student);
|true
如果我们像前面的例子那样直接用一个自定义对象替换原型对象,此时新的原型对象默认是没有constructor属性的。这样会导致通过实例访问constructor时,得不到原本的构造函数引用。 为了让原型对象依然正确地指向构造函数本身,我们需要手动为新原型对象添加constructor属性,这样可以保证实例的constructor属性依然指向Book构造函数,实现行为上的一致性。
|Book.prototype = { constructor: Book, // 显式设置构造函数引用 getInfo: function() { return this.title + " 作者:" + this.author; }, isWrittenBy: function(author) { return this.author === author; }, toString: function
还有一种常见的实现方式,就是直接在原型对象上逐一添加方法,而不是整体替换原型对象。这样做的好处在于,原型对象最初由JavaScript自动创建, 并且自带一个constructor属性指向构造函数本身。当我们采用逐个添加方法的方式时,这个constructor属性会被保留下来, 实例对象通过constructor属性依然能够正确引用到它们的构造函数。这种方式不仅保证了原型链的完整性,也避免了手动设置constructor属性的麻烦。
|Book.prototype.getInfo = function() { return this.title + " 作者:" + this.author; }; Book.prototype.isWrittenBy = function(author) { return this.author === author; }; Book.prototype.toString = function
虽然JavaScript与Java在类的实现上有本质差异,但我们可以模拟Java中四种类成员的概念:实例字段、实例方法、类字段和类方法。
在JavaScript中,有三个不同的对象参与类的定义,这些对象的属性表现为不同类型的类成员:
构造函数对象本身可以用来存储类字段和类方法。原型对象的属性被所有实例继承,作为实例方法使用。实例对象可以有自己的属性,作为实例字段。
让我们通过一个学生管理系统来演示这些概念:
|// 学生构造函数(定义实例字段) function Student(name, age, studentId) { this.name = name; // 实例字段 this.age = age; // 实例字段 this.studentId = studentId; // 实例字段 } // 实例方法(定义在原型上) Student.prototype.getInfo = function() { return "学号:" + this
|学号:2024001,姓名:张三,年龄:17 false 学校:光明中学 学号:毕业生,姓名:王五,年龄:22
JavaScript的继承机制依赖于原型链,这种机制具有高度的动态性。当我们通过构造函数创建一个对象实例时,这个实例会自动关联到构造函数的原型对象。 无论是在实例创建之前还是之后,只要我们为原型对象添加新的属性或方法,所有通过该构造函数创建的实例都能立即访问到这些新增的成员。
这种特性使得我们可以在程序运行过程中随时为类“扩展”功能,比如为Student类的原型添加新的方法,所有已存在和新创建的Student实例都能直接使用这些方法,无需重新创建实例或修改原有代码。
|// 为前面定义的Student类添加新方法 Student.prototype.getGrade = function() { if (this.age < 12) return "小学"; if (this.age < 15) return "初中"; if (this.age < 18) return "高中"; return "大学";
|高中 大学
在JavaScript中,内置类(如Number、String、Array、Function等)的原型对象是可以被动态扩展的。也就是说,我们可以在这些原型上添加新的方法或属性,这样所有通过这些内置类创建的实例都能直接访问到这些新增的功能。
例如,可以为Array.prototype添加一个自定义方法,所有数组实例都能使用它。同样地,String.prototype、Function.prototype等也可以被扩展。不过需要注意的是,尤其是在向Object.prototype添加方法时要非常谨慎,因为Object是所有对象的基础,修改它会影响到所有对象实例,可能导致意想不到的副作用或与第三方库产生冲突。
|// 为数组添加获取最后一个元素的方法 Array.prototype.getLast = function() { return this[this.length - 1]; }; var numbers = [1, 2, 3, 4, 5]; console.log(numbers.getLast()); // 为字符串添加首字母大写方法 String
|5 Hello world
在JavaScript中,判断一个对象属于哪种类型有多种方法。最常见的做法是使用instanceof操作符。instanceof会沿着对象的原型链向上查找,判断某个对象是否继承自指定构造函数的原型对象。如果在原型链上找到了对应的原型,instanceof就会返回true,否则返回false。通过这种方式,我们可以判断一个对象是否是某个类的实例,或者是否继承自某个父类。不过,这种检测方式也有一定的局限性,比如当对象的原型链被修改或者跨iframe、跨窗口时,instanceof的判断结果可能会不如预期。此外,JavaScript还提供了typeof和Object.prototype.toString等其他类型检测手段,但它们各自也有适用范围和限制。因此,在实际开发中,选择哪种类型检测方式需要根据具体的场景和需求来决定。
|function Vehicle(brand) { this.brand = brand; } function Car(brand, model) { this.brand = brand; this.model = model; } Car.prototype = Object.create(Vehicle.prototype); Car.prototype
|true true true
除了严格的类型检测,JavaScript社区还广泛采用“鸭子类型”的编程哲学。鸭子类型的核心思想是:如果一个对象走路像鸭子、游泳像鸭子、叫声像鸭子,那么我们就可以把它当作鸭子来对待。在编程中,这意味着我们关注对象能做什么(有哪些方法),而不是它是什么类型。
|// 定义一个检查对象是否具有指定方法的函数 function hasMethod(obj, methodName) { return typeof obj[methodName] === "function"; } // 定义一个媒体播放器接口的检查函数 function canPlayMedia(obj) { return hasMethod(obj, "play") && hasMethod(obj, "pause") && hasMethod(obj, "stop");
|true true 音乐播放中... 视频播放中...
鸭子类型的优势在于提供了更大的灵活性,允许不同类型的对象只要实现了相同的接口就可以互换使用。这种方式在JavaScript中非常常见,特别是在处理类数组对象和回调函数时。
在理解了JavaScript类的基本原理之后,进一步学习面向对象编程的相关技术变得尤为重要。通过掌握这些技术,我们不仅能够更好地组织和封装代码,还能有效提升代码的可维护性和可扩展性。
例如,合理地使用原型链可以实现方法的复用,构造函数的设计能够帮助我们灵活地创建对象实例,而封装和继承则让代码结构更加清晰。此外,理解多态和鸭子类型等概念,可以让不同对象在同一接口下协同工作,从而增强程序的灵活性。

集合在编程中扮演着非常重要的角色,它用于存储一组互不相同的元素。在 JavaScript 中,由于对象的属性名天然具有唯一性,因此我们可以利用对象来实现集合的功能。
通过将集合的每个元素作为对象的属性进行存储,我们能够方便地判断某个值是否已经存在于集合中,同时也能高效地添加和删除元素。这种方式不仅简化了集合的实现过程,还能充分发挥 JavaScript 对象的灵活性和高效性,从而让我们能够轻松地构建一个通用的集合类。
|function NumberSet() { this.values = {}; // 用对象存储集合元素 this.count = 0; // 记录元素数量 // 将所有参数添加到集合中 for (var i = 0; i < arguments.length; i++) { this.add(arguments[i]); } } // 添加元素到集合
|集合大小:5 包含3吗?true 包含2吗?false 元素:1 元素:3 元素:4 元素:5 元素:6
在JavaScript中,设计良好的类通常会实现一些具有特殊用途的转换方法。
例如,toString方法允许我们将对象以字符串的形式展现出来,这在调试或日志输出时非常有用。
valueOf方法则用于在需要原始值的场合(如算术运算或与原始类型比较时)自动调用,从而返回对象的基本表示。
toJSON方法则专门用于JSON序列化,当我们使用JSON.stringify对对象进行序列化时,toJSON会被自动调用,决定最终输出的内容。这些方法的实现能够让自定义类更好地融入JavaScript的内建机制,提升对象的可用性和表现力。
让我们为NumberSet类添加这些方法:
|// 字符串表示 NumberSet.prototype.toString = function() { var elements = []; this.forEach(function(value) { elements.push(value); }); return "{" + elements.join(", ") + "}"; }; // 转换为数组 NumberSet
|字符串表示:{10, 20, 30} 转换为数组:[10,20,30] JSON序列化:[10,20,30]
在实际开发中,我们经常会遇到需要判断两个对象是否相等,或者对一组对象进行排序的场景。为了实现这些功能,通常会在类中定义equals方法用于判断对象的内容是否相同, 而compareTo方法则用来比较两个对象的大小关系。这种做法最早源自Java语言的设计规范,但在JavaScript中同样非常实用。 通过实现equals方法,我们可以自定义对象的相等性判断逻辑,而compareTo方法则为对象的排序提供了标准依据,使得自定义类能够方便地参与到如数组排序等操作中。
|function Point(x, y) { this.x = x; this.y = y; } // 相等性比较 Point.prototype.equals = function(other) { if (other == null) return false; if (!(other instanceof Point
|p1 equals p2: true p1 equals p3: false 排序后的点: (1, 3) (2, 2) (3, 1)
封装是面向对象编程中的核心思想之一,它指的是将对象的内部状态和实现细节隐藏起来,只通过公开的方法与外部交互。这样可以保护数据不被随意修改,提高代码的安全性和可维护性。在JavaScript中,虽然传统的对象属性默认是公开的,但我们可以借助闭包机制,将某些变量限定在构造函数的作用域内,从而实现类似私有属性的效果。这种方式能够有效地将对象的内部状态与外部隔离,只允许通过特定的接口访问和操作这些私有数据。
|function BankAccount(initialBalance) { var balance = initialBalance || 0; // 私有变量 var transactionHistory = []; // 私有变量 // 公有方法,可以访问私有变量 this.getBalance = function() { return balance; }; this.deposit = function(amount) {
|账户余额:1000元 账户余额:1300元 交易记录: 存款:+500 取款:-200 直接访问balance:undefined
在实际开发中,我们经常会遇到需要根据不同的参数或条件来创建对象实例的情况。此时,单一的构造函数可能无法满足所有需求。为了解决这个问题,可以通过在构造函数的基础上增加工厂方法,或者通过模拟构造函数重载的方式,来为对象的创建提供更多灵活性。这样,无论是直接传递不同的参数,还是通过静态方法根据特定逻辑生成实例,都能够方便地实现对象的多样化创建方式。
|function Rectangle(width, height) { this.width = width; this.height = height; } // 工厂方法:创建正方形 Rectangle.createSquare = function(side) { return new Rectangle(side, side); }; // 工厂方法:从面积创建正方形 Rectangle.createSquareFromArea = function(area) { var
|矩形面积:24 正方形1面积:25,是正方形吗?true 正方形2面积:25,是正方形吗?true
继承是面向对象编程的一个重要基础,它允许我们通过已有的类创建出功能更丰富的子类。在JavaScript中,继承的实现依赖于原型链的机制。 具体来说,如果我们希望让一个类B继承自另一个类A,需要将B的原型对象(B.prototype)设置为A的原型对象(A.prototype)的一个副本。 这样,B的实例对象在查找属性或方法时,会沿着原型链逐级查找,最终能够访问到A原型上的属性和方法。 通过这种方式,子类不仅能够拥有父类的功能,还可以根据需要扩展或重写父类的方法,实现更灵活的对象行为。
让我们创建一个动物类的继承体系来演示子类的实现:
|// 基类:动物 function Animal(name, species) { this.name = name; this.species = species; } Animal.prototype.makeSound = function() { return this.name + "发出了声音"; }; Animal.prototype.getInfo = function
|动物名称:旺财,种类:犬科 旺财汪汪叫 旺财去捡球了 动物名称:咪咪,种类:猫科 咪咪喵喵叫 咪咪爬上了树 dog instanceof Dog: true dog instanceof Animal: true
在 JavaScript 的继承体系中,子类有时会重写父类的方法,但并不是完全替换父类的实现,而是希望在新方法中先调用父类的方法,再在其基础上添加或扩展功能。这种做法被称为方法链接。通过方法链接,子类能够复用父类已有的逻辑,同时根据自身需求进行增强,实现代码的灵活扩展和复用。
|function Vehicle(brand, year) { this.brand = brand; this.year = year; } Vehicle.prototype.getDescription = function() { return this.brand + " " + this.year + "年款"; }; Vehicle.prototype.
|特斯拉 2023年款,电池容量:75kWh 车辆启动,电机运行中 正在充电...
继承允许一个类自动获得另一个类的属性和方法,这在需要表达“是一种”关系时非常有用。然而,在实际开发中,继承有时会导致类之间的耦合度过高,灵活性降低,尤其是在需求变化或功能扩展时。 相比之下,组合是一种通过将其他对象作为属性包含进来,从而实现功能复用的方式。通过组合,一个对象可以拥有多个不同对象的能力, 这样不仅可以灵活地扩展功能,还能避免继承层次过深带来的复杂性。因此,在需要灵活扩展和解耦时,组合往往比继承更适合。
|// 组合示例:媒体播放器 function AudioEngine() { this.volume = 50; } AudioEngine.prototype.play = function(file) { return "播放音频文件:" + file + ",音量:" + this.volume; }; AudioEngine.prototype.setVolume = function(volume) {
|播放音频文件:music.mp3,音量:50 播放音频文件:music.mp3,音量:80 播放视频文件:movie.mp4,分辨率:1080p 播放视频文件:movie.mp4,分辨率:4K
组合的优势在于提供了更大的灵活性,避免了深层继承链可能带来的复杂性,同时使代码更容易理解和维护。在设计类的时候,我们应该根据具体情况选择继承还是组合,通常"组合优于继承"是一个好的设计原则。
Person构造函数,要求:
name和age两个参数introduce()方法,返回"我是{name},今年{age}岁"introduce()方法|// 定义构造函数 function Person(name, age) { this.name = name; this.age = age; } // 在原型上添加方法 Person.prototype.introduce = function() { return "我是" + this.name + ",今年" + this.age + "岁"; };
Car构造函数的原型上添加getInfo()方法:|function Car(brand, model) { this.brand = brand; this.model = model; } // 在这里添加原型方法 const myCar = new Car("丰田", "卡罗拉"); console.log(myCar.getInfo()); // 应该输出: 丰田 卡罗拉
|function Car(brand, model) { this.brand = brand; this.model = model; } // 在原型上添加方法 Car.prototype.getInfo = function() { return this.brand + " " + this.model; }; const myCar = new Car
Book类,要求:
title和author两个参数getInfo()方法,返回"《{title}》作者:{author}"isWrittenBy(author)方法,判断是否为指定作者的作品|// 定义Book构造函数 function Book(title, author) { this.title = title; this.author = author; } // 在原型上添加getInfo方法 Book.prototype.getInfo = function() { return "《" + this.title + "》作者:" + this.author; }; // 在原型上添加isWrittenBy方法