ECMAScript(简称ES,是JavaScript的标准规范)支持面向对象编程,通过构造函数模拟类,原型链实现继承,以及ES6引入的class语法糖简化面向对象开发。对象可通过构造函数创建,使用原型链共享方法和属性,实现继承、封装和多态等面向对象特性。
ECMAScript的面向对象技术极大提升了JavaScript的代码组织度和复用性,通过类和继承机制促进了模块化编程,增强了代码结构的清晰度与可维护性。封装、继承、多态等特性支持复杂应用开发,是构建可扩展、易管理的大型项目的基础,对提升开发效率和代码质量至关重要。
本文详细介绍了JavaScript 对象概念
、原型模式创建对象的过程
(Object 创建对象、构造函数 创建对象、原型模式 创建对象)以及机制策略
(__proto__和 prototype、constructor),继承机制
(原型链继承、构造函数继承、组合继承、寄生式继承、寄生组合式继承、ES6 Class继承)等内容。
一、JavaScript 对象
ECMAScript(JavaScript)中的对象是一种数据结构,用于存储键值对(property-value pairs),其中键(property)通常是字符串类型,值(value)可以是任意数据类型,包括其他对象。对象是JavaScript中最基本的数据结构,也是语言的核心特性之一。以下是对ECMAScript对象概念的几个关键点总结:
-
动态性:JavaScript对象是动态的,可以在运行时添加、修改或删除其属性。这为编程提供了极大的灵活性。
-
原型继承:JavaScript采用原型链继承机制。每个对象都有一个内部的
[[Prototype]]
属性(可通__proto__
访问,尽管不推荐直接操作),指向它的原型对象。当试图访问一个对象的属性或方法时,如果对象本身没有,JavaScript引擎会向上查找其原型链,直至找到或链结束。 -
构造函数:构造函数是一种特殊的函数,用于初始化新创建的对象。使用
new
关键字调用构造函数时,会创建一个空对象,并将其[[Prototype]]
链接到构造函数的prototype
属性所指向的原型对象,然后执行构造函数体内的代码以初始化该对象。 -
字面量表示法:可以直接使用对象字面量
{}
来创建并初始化一个对象,这是定义简单对象的便捷方式。 -
属性访问:可以通过
.
或[]
操作符访问对象的属性,如obj.property
或obj['property']
。 -
方法:对象的属性可以是函数,这样的属性通常称为方法。方法允许对象封装行为。
-
this关键字:在对象的方法中,
this
关键字指向调用该方法时的对象上下文。在全局作用域或非严格模式下未明确绑定的函数调用中,this
默认指向全局对象(浏览器中是window
,Node.js中是global
或globalThis
)。 -
原型对象:每个函数都有一个
prototype
属性,它指向一个对象,这个对象就是将来新建的对象的原型对象。原型对象上的属性和方法可以被其所有实例共享。
理解JavaScript的对象概念是掌握其面向对象编程的基础,对于编写高效、可维护的代码至关重要。
二、Object 创建对象
创建一个对象,然后给这个对象新建属性和方法。
var car = new Object(); //创建一个Object 对象
car.name = 'AIPHD'; //创建一个name 属性并赋值
car.color = '4AD3FF'; //创建一个color 属性并赋值
car.run = function () { //创建一个run()方法并返回值
return this.name + this.color + '运行中...';
};
alert(car.run()); //输出属性和方法的值
上述代码实例化了一个对象并定义了属性及方法,其中run()
方法内部的this
关键字即指向car
对象自身。但不足之处在于,若需生成多个相似对象,会导致代码重复且冗长。
为了解决多个相似对象实例化的冗余问题,可以采用“工厂模式”,该模式设计初衷正是为了有效减少重复代码,优化实例化过程中的资源消耗。
function createObject(name, age) { //集中实例化的函数
var obj = new Object();
obj.name = name;
obj.age = age;
obj.run = function () {
return this.name + this.age + '运行中...';
};
return obj;
}
var box1 = createObject('AIPHD', 100); //第一个实例
var box2 = createObject('AIMANT', 200); //第二个实例
alert(box1.run());
alert(box2.run()); //保持独立
尽管工厂模式缓解了重复实例化的问题,但它亦非完美之策。它引发的新挑战包括:为每个对象重复构建相同的属性和方法,导致内存使用低效;以及难以通过类型直接辨别对象函数识别问题等等。
三、构造函数 创建对象
构造函数的使用遵循一套既定的规范,旨在确保代码的高效与可维护性。这些规范主要包括:
-
命名规范:构造函数名应采用大驼峰命名法,如
Person
、Car
,以便于区分普通函数和构造函数。 -
首字母大写:遵循惯例,构造函数的名称首字母通常大写,区别于其他小写命名的普通函数。
-
使用
new
关键字:调用构造函数时,必须通过new
操作符来创建新实例。这会自动执行构造函数内的代码,并为新对象分配内存空间。 -
this
关键字:在构造函数内部,this
指代新创建的实例对象。可以使用它来给实例添加属性和方法。 -
无需显式返回值:构造函数默认返回新创建的对象实例,除非使用
return
语句明确返回一个对象,此时返回的对象将替代默认实例。 -
原型链继承:利用构造函数的
.prototype
属性,可以实现方法的共享,避免每个实例都拥有相同方法的副本,从而节省内存。
遵循这些规范,可以编写出结构清晰、易于理解且性能良好的JavaScript代码。
function Car(name, color) { //构造函数模式
this.name = name;
this.color = color;
this.run = function () {
return this.name + this.color + '运行中...';
};
}
var car1 = new Car('AIPHD', '4AD3FF'); //new Car()即可
var car2 = new Car('AIMANT', 'FFF800');
alert(car1.run());
alert(car instanceof Car); //很清晰的识别他从属于Car
构造函数创建对象的过程大致分为以下几个步骤:
-
调用构造函数:当使用
new
关键字调用一个构造函数时,JavaScript引擎首先会在内存中创建一个新的空对象。 -
绑定
this
:接着,这个新创建的空对象会被绑定到构造函数内部的this
关键字上。这意味着通过this
可以给新对象添加属性和方法。 -
执行构造函数体:构造函数的代码体开始执行。在这个阶段,可以通过
this
给新对象添加属性和方法,比如this.name = 'Alice'
。 -
初始化原型:如果构造函数中有对原型(
prototype
)的修改或扩展(例如,添加共享方法),这些更改会影响之后由该构造函数创建的所有实例。 -
返回对象:构造函数执行完毕后,如果未显式返回一个对象(或者返回
null
、undefined
),则new
操作符会自动返回最初创建的那个对象实例。如果构造函数显式返回一个对象,则返回该对象,而不是默认创建的实例。
通过这一系列步骤,构造函数不仅创建了一个新的对象实例,还为其添加了特定的属性和方法,完成了对象的初始化和配置。
注:
-
构造函数和普通函数的唯一区别,就是他们调用的方式不同。只不过,构造函数也是函数,必须用new 运算符来调用,否则就是普通函数。
-
this就是代表当前作用域对象的引用。如果在全局范围this 就代表window 对象,如果在构造函数体内,就代表当前的构造函数所声明的对象。
这种方法解决了函数识别问题,但内存消耗问题仍旧存在,且引入了新的困扰:this
关键字的上下文不确定性。具体而言,在全局环境中,若作为对象方法调用,this
正确指向Box
实例;但若以普通函数形式调用,this
则默认绑定至全局对象(在浏览器中即window
),导致this
作用域的混乱。
四、原型模式 创建对象
每个函数自带一个名为prototype
的属性,它实质上是一个对象,承载着旨在被该函数构建的所有实例共同继承的属性与方法。从逻辑角度阐释,每当通过某个构造函数生成实例时,该实例会自动指向该构造函数prototype
属性所定义的对象,作为其原型。采纳原型模式的意义,在于实现这些共有属性和方法的一次定义、多处复用,无需在每个构造实例时重复定义相同数据,而仅需将这些共享特征附加至原型上即可。
function Car() {} //声明一个构造函数
Car.prototype.name = 'AIPHD'; //在原型里添加属性
Car.prototype.color = '4AD3FF';
Car.prototype.run = function () { //在原型里添加方法
return this.name + this.color + '运行中...';
};
原型设计巧妙地缓解了内存消耗问题,同时也有效应对了this
作用域相关挑战。
在创建对象时,我们通常的做法是:将那些在生成每个新实例时需要独立初始化的属性值,放置在构造函数内部定义;而对于那些所有实例共用的方法,则倾向于将其添加到原型(prototype)上。这种方法结合了构造函数与原型的优点,即所谓的“构造函数+原型”混合模式来构建对象。
function Task(id){
this.id = id; // 属性(构造函数)
}
Task.prototype.status = "STOPPED";
Task.prototype.execute = function(args){ // 共用方法(原型)
return "execute task_"+this.id+"["+this.status+"]:"+args;
}
var task1 = new Task(1);
var task2 = new Task(2);
task1.status = "ACTIVE";
task2.status = "STARTING";
print(task1.execute("task1"));
print(task2.execute("task2"));
结果:
execute task_1[ACTIVE]:task1
execute task_2[STARTING]:task2
构造器会自动为task1,task2两个对象设置原型对象Task.prototype,这个对象被Task(在此最为构造器)的prototype属性引用,参看下图中的箭头指向。
Task作为函数实体,其隐含的proto
链结点为Function.prototype
。同时,系统内置的函数原型(Function.prototype
)其proto
指向基底的Object.prototype
。层层递进,至顶层Object.prototype
的proto
属性为空(null),标示原型链的顶层终点。
1. 原型对象
在JavaScript中,每个对象均关联一个原型对象,该对象在不同 JavaScript 引擎中的具体实现细节可能有所差异。以Firefox为例,每个对象内部含有一私有属性__proto__
,它作为一个指针,指向该对象的原型对象。
2. 原型链
原型链是JavaScript实现继承的核心机制。当试图访问一个对象的属性或方法时,如果该对象本身没有,则JavaScript引擎会向上搜索其原型对象(即__proto__
指向的对象)是否有该属性或方法。如果原型对象也没有,引擎会继续在原型的原型对象中寻找,如此层层向上,直到找到该属性或方法,或抵达原型链的末端(通常是Object.prototype的__proto__为null,标志着原型链的结束。这意味着不再有更上一层的原型可供查询,即达到了原型链的顶层)。这一连串由对象及其原型对象逐级连接形成的链式结构,即称为原型链。
2.1. __proto__和 prototype
__proto__是每个JavaScript对象(除了null)都有的一个内部属性,它指向该对象的原型对象(prototype object)。这个属性不是标准的一部分,但在大多数现代浏览器和Node.js环境中都得到了非正式的支持。正式的标准推荐使用Object.getPrototypeOf()和Object.setPrototypeOf()方法来访问和修改对象的原型。
function Animal(name) {
this.name = name; // 实例属性
}
// 给Animal构造函数的原型添加一个方法
Animal.prototype.speak = function() {
console.log("My name is " + this.name);
};
// 创建Animal的实例
let cat = new Animal("Tom");
// 访问实例的方法
cat.speak(); // 输出: My name is Tom
// 查看cat的原型链
console.log(cat.__proto__); // 输出: Animal的原型对象,其中包含speak方法
console.log(cat.__proto__ === Animal.prototype); // 输出: true,表明cat的__proto__指向Animal的prototype
console.log(Animal.prototype.__proto__ === Object.prototype) // 输出: true,表明Animal.prototype的__proto__指向创建它的函数对象(Object)的prototype
console.log(Object.prototype.__proto__) // 输出: null,Object.prototype对象也有__proto__属性,但它比较特殊,为null
// 查看Animal构造函数的prototype属性
console.log(Animal.prototype); // 显示包含speak方法的原型对象
我们把这个有__proto__串起来的直到Object.prototype.__proto__为null的链叫做原型链。
2.2. constructor
在JavaScript中,每个构造函数的prototype
对象上都有一个默认的属性constructor
,这个属性指向构造函数自身。constructor
属性的作用主要是为了方便地识别某个实例是通过哪个构造函数创建的,同时也便于从原型对象重新获取构造函数的引用。
-
自我标识:在复杂的继承结构或者原型链修改后,
constructor
可以帮助明确对象的构造函数来源,即使原型被覆盖或修改也能追溯到原始构造函数。 -
便于复用和构造:当需要根据对象类型动态创建新实例时,可以直接通过原型链上的
constructor
调用构造函数。
function Animal(name) {
this.name = name;
}
console.log(Animal.prototype.constructor === Animal); // 输出: true,表明constructor指向Animal函数
let cat = new Animal("Kitty");
console.log(cat.constructor === Animal); // 间接通过原型链,同样输出: true
// 即使修改了原型,constructor仍指向正确的构造函数
Animal.prototype = {
speak: function() {
console.log("My name is " + this.name);
}
}; // 注意:这样的直接赋值会丢失原有的constructor属性!
// 修复constructor属性
Animal.prototype.constructor = Animal;
let dog = new Animal("Rex");
console.log(dog.constructor === Animal); // 确保修复后仍为true
Function.prototype.constructor === Function //true
Object.prototype.constructor === Object //true
五、继承机制
ECMAScript(JavaScript)提供了多种继承策略,每种策略都有其特点和适用场景。以下是几种主要的继承方式的详解与示例:
1. 原型链继承
原理:通过让子类型的原型对象等于父类型的实例,使得子类型能够访问到父类型上的属性和方法。
示例:
function SuperType() {
this.superProperty = true;
}
SuperType.prototype.getSuperValue = function() {
return this.superProperty;
};
function SubType() {
this.subProperty = false;
}
// 继承SuperType
SubType.prototype = new SuperType();
// 修复构造函数引用
SubType.prototype.constructor = SubType;
SubType.prototype.getSubValue = function() {
return this.subProperty;
};
var instance = new SubType();
console.log(instance.getSuperValue()); // 输出: true
2. 构造函数继承(借用构造函数)
原理:在子类型构造函数内部通过call
或apply
方法调用父类型构造函数,为子类型实例添加属性。
示例:
function SuperType(name) {
this.name = name;
}
function SubType(name, age) {
SuperType.call(this, name); // 借用构造函数
this.age = age;
}
var instance = new SubType("Tom", 25);
console.log(instance.name); // 输出: Tom
console.log(instance.age); // 输出: 25
3. 组合继承(原型链+构造函数继承)
原理:结合原型链继承和构造函数继承的优点,既可以在子类型中继承父类型的属性和方法,又能保持每个实例的唯一性。
示例:
function SuperType(name) {
this.name = name;
}
SuperType.prototype.sayName = function() {
console.log(this.name);
};
function SubType(name, age) {
SuperType.call(this, name); // 继承属性
this.age = age;
}
// 继承方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType; // 修复构造函数引用
SubType.prototype.sayAge = function() {
console.log(this.age);
};
var instance = new SubType("Tom", 25);
instance.sayName(); // 输出: Tom
instance.sayAge(); // 输出: 25
4. 寄生式继承
原理:创建一个对象作为父类型的实例,然后为其添加额外的属性和方法,最后返回这个对象。
示例:
function createAnother(original) {
var clone = Object.create(original); // 或者使用 Object.assign({}, original) 进行浅拷贝
clone.extraMethod = function() {
console.log("Extra method");
};
return clone;
}
var original = { value: 1 };
var another = createAnother(original);
another.extraMethod(); // 输出: Extra method
5. 寄生组合式继承
原理:结合了寄生式继承和组合继承的特点,优化了组合继承中重复调用父构造函数的问题。
示例:
function inheritPrototype(subType, superType) {
var prototype = Object.create(superType.prototype); // 创建父类型的原型副本
prototype.constructor = subType; // 修正构造函数的指向
subType.prototype = prototype; // 将子类型的原型指向新创建的原型副本
}
function SuperType(name) {
this.name = name;
}
SuperType.prototype.sayName = function() {
console.log(this.name);
};
function SubType(name, age) {
SuperType.call(this, name); // 继承属性
this.age = age;
}
inheritPrototype(SubType, SuperType); // 实现继承
SubType.prototype.sayAge = function() {
console.log(this.age);
};
var instance = new SubType("Tom", 25);
instance.sayName(); // 输出: Tom
instance.sayAge(); // 输出: 25
6. ES6 Class继承
原理:ES6引入了class
关键字,使得继承更加简洁明了,背后仍然是基于原型继承机制。
示例:
class SuperType {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}
class SubType extends SuperType {
constructor(name, age) {
super(name); // 调用父类构造函数
this.age = age;
}
sayAge() {
console.log(this.age);
}
}
let instance = new SubType("Tom", 25);
instance.sayName(); // 输出: Tom
instance.sayAge(); // 输出: 25
每种继承策略各有千秋,开发者应根据实际需求选择最适合的继承方式。ES6的class
继承因其简洁易读性,逐渐成为主流选择。
在ECMAScript中运用面向对象技术,关键在于合理设计类与接口,利用ES6的class语法简化继承和封装过程。采用组合而非深度继承提高灵活性,利用 Mixins 引入多重继承特性。重视模块化,合理划分职责,利用闭包和模块模式增强封装性。适时采用原型链继承与构造函数继承,结合实际情况灵活选择,确保代码既高效又易于理解维护。