原型链
JavaScript的原型链是一种基于原型继承的机制,它允许对象从其他对象继承属性和方法。在JavaScript中,几乎所有的对象都是通过原型继承来实现的。要理解原型链,首先需要了解以下几个关键概念:
- 原型(Prototype): 每个JavaScript对象都有一个内部属性
[[Prototype]]
(通常通过__proto__
属性访问),它指向创建该对象时使用的构造函数的原型对象。这个原型对象本身也可能有一个原型,这样就形成了一个链式结构,即原型链。 - 构造函数的原型属性: 每个构造函数都有一个
prototype
属性,它是一个对象,包含了通过该构造函数创建的所有对象实例共享的属性和方法。当你尝试访问一个对象的属性或方法时,如果该对象本身没有这个属性或方法,JavaScript会沿着原型链向上查找,直到找到为止。 - 属性查找: 当代码尝试访问一个对象的属性时,JavaScript首先在对象本身查找这个属性。如果找不到,它会沿着原型链向上查找,直到找到该属性或到达原型链的末端(通常是一个空对象)。如果还没有找到,这个属性访问会返回
undefined
。 - 方法继承: 方法也可以通过原型链进行继承。当你调用一个对象的方法时,JavaScript首先在对象本身查找这个方法。如果找不到,它会沿着原型链向上查找,直到找到这个方法或到达原型链的末端。
- 原型链的末端: 原型链的末端通常是
Object.prototype
,因为所有的JavaScript对象都是Object
的实例。Object.prototype
本身有一个指向null
的__proto__
属性,这意味着null
是原型链的终点。
例子:
// 定义一个构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
// 在原型对象上定义一个方法
Person.prototype.sayHello = function() {
console.log('Hello, my name is ' + this.name);
};
// 创建一个实例对象
let person = new Person('Alice', 30);
// 调用实例对象的方法
person.sayHello(); // 输出: Hello, my name is Alice
// 在实例对象上查找属性
console.log(person.name); // 输出: Alice
// 在实例对象上查找方法
console.log(person.hasOwnProperty('sayHello')); // 输出: false
在这个示例中,我们定义了一个构造函数 Person
,并在它的原型对象上定义了一个方法 sayHello
。当我们创建一个 Person
类的实例 person
时,person
对象会继承 Person.prototype
上的方法。当调用 person.sayHello()
时,JavaScript 引擎会在 person
对象上查找 sayHello
方法,但并没有找到,然后它会顺着原型链向上查找,最终在 Person.prototype
上找到了这个方法并执行。同样地,当我们在实例对象 person
上查找属性 name
时,它首先在 person
对象上找到了该属性,如果没有找到则继续沿着原型链向上查找。
组合继承
组合继承是JavaScript中的一种继承模式,它旨在结合原型链继承和构造函数继承的优点,以实现代码的复用和结构的清晰。组合继承允许你创建一个类(子类)的实例,同时继承另一个类(父类)的属性和方法,并且能通过父类的构造函数初始化子类实例的状态。
组合继承的工作流程通常如下:
- 创建父类实例: 通过调用父类的构造函数来创建一个新的父类实例。这样做可以确保父类构造函数中定义的任何属性或方法都能被添加到子类实例中。
- 链接原型: 将父类的原型对象链接到子类的构造函数中。这通常是通过设置子类构造函数的原型为父类实例来实现的。这样,子类的实例就可以访问父类的原型上定义的所有方法。可以使用
Object.getPrototype()
获取指定对象的原型。原型是对象继承属性和方法的源头。当你尝试访问一个对象的属性或方法时,如果该对象本身没有这个属性或方法,JavaScript 引擎会沿着原型链向上查找,直到找到该属性或方法,或者到达原型链的末端(null
)。 - 复制属性: 如果需要,可以通过某种方式(如
Object.assign
)将父类实例的属性复制到子类实例中。这样可以确保子类实例拥有父类实例的所有属性,而不仅仅是方法。 - 修正构造函数指针: 由于
Object.create
或手动设置原型链的方式不会改变子类构造函数内部的this
指向,因此需要手动设置子类构造函数的[[Prototype]]
(即prototype
属性)中的constructor
属性指向正确的子类构造函数。
function SuperType(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
};
function SubType(name, age) {
// 继承属性
SuperType.call(this, name); // 调用父类构造函数
// 这里可以添加子类特有的属性
this.age = age;
}
// 链接原型
SubType.prototype = Object.create(SuperType.prototype);
// 修正构造函数指针
SubType.prototype.constructor = SubType;
// 继承方法
SubType.prototype.sayAge = function() {
console.log(this.age);
};
let instance = new SubType('Kimi', 30);
instance.sayName(); // 输出: Kimi
instance.sayAge(); // 输出: 30
在这个示例中,SubType
通过组合继承从SuperType
继承了属性和方法。首先,通过SuperType.call(this, name)
调用父类构造函数来初始化子类的属性。然后,通过Object.create(SuperType.prototype)
创建一个新的对象,并将这个对象设置为SubType.prototype
,从而让SubType
的实例可以访问SuperType
原型上定义的方法。最后,修正子类的constructor
属性指向SubType
,确保instanceof
操作符能正确判定实例的类型。
组合继承作为一种在JavaScript中实现继承的模式,相比于仅使用原型链继承,有以下几个好处:
- 避免属性重复: 在使用原型链继承时,如果你在父类的构造函数中定义了一些属性,那么通过原型链继承创建的所有子类实例都会共享这些属性。这意味着对一个实例的属性修改可能会影响到其他实例。组合继承通过使用构造函数调用来初始化每个子类实例的属性,避免了这种属性共享的问题。
- 清晰的实例属性和原型属性分离: 组合继承允许你清晰地区分实例属性(通过构造函数设置)和原型属性(通过原型链设置)。这样,你可以确保实例的属性是独立的,而共享的方法和属性则放在原型上。
- 更好的原型链完整性: 在组合继承中,子类的原型是一个父类的实例,这意味着子类的原型链保持了完整性。这样,当你使用
instanceof
操作符时,你可以正确地识别对象的类型,因为子类的原型链正确地指向了父类。 - 方法重写: 组合继承允许子类重写父类的方法。因为父类的方法是通过原型链继承的,所以子类可以简单地通过在子类构造函数或原型上定义同名方法来重写它们。
- 利用父类构造函数逻辑: 通过在子类构造函数中调用父类构造函数,组合继承允许你复用父类构造函数中的逻辑,例如初始化设置或者验证参数等。
- 支持多级继承: 组合继承可以很好地支持多级继承,即子类可以继承自一个继承了其他类的父类。这种层叠的继承结构可以帮助你构建复杂的对象层次结构。
基于原型链和组合继承想对象添加新方法
当需要添加新的函数时,除了直接更改代码外,需要添加到prototype上而不是直接在该类上添加。
-
通过构造函数的原型添加 如果你想让通过某个构造函数创建的所有对象实例都获得一个新方法,你可以在构造函数的
prototype
对象上添加这个新方法。function Person(name) { this.name = name; } // 在构造函数的原型上添加新方法 Person.prototype.introduce = function() { console.log('My name is ' + this.name); }; let person1 = new Person('Alice'); person1.introduce();// 输出: My name is Alice
通过这种方式添加的方法,所有
Person
的实例都会继承introduce
方法。 -
使用
Object.assign
方法Object.assign
方法可以用来将一个或多个源对象的所有可枚举属性复制到目标对象。你可以使用这个方法来给对象添加一个新方法。let myObject = { name: 'Kimi' }; // 使用 Object.assign 添加新方法 Object.assign(myObject, { sayHello: function() { console.log('Hello, my name is ' + this.name); } }); myObject.sayHello();// 输出: Hello, my name is Kimi
-
使用
Object.defineProperty
方法Object.defineProperty
方法允许你精确地添加或修改对象的属性,并设置属性的描述符。使用这个方法添加的方法将不可枚举,并且可以设置方法的属性特性。let myObject = { name: 'Kimi' }; // 使用 Object.defineProperty 添加新方法 Object.defineProperty(myObject, 'greet', { value: function() { console.log('Hello, my name is ' + this.name); }, writable: true, enumerable: false, configurable: true }); myObject.greet();// 输出: Hello, my name is Kimi
实例讲解
构建一个基类 Animal
和一个子类 Dog
。其中,基类 Animal
包含了一个构造函数和一个原型方法,用于创建动物对象并输出动物的名字。子类 Dog
继承了基类 Animal
,并在其原型上添加了一个新方法 bark()
,用于模拟狗的叫声,并通过属性特性的方式给狗对象添加了颜色属性。最后,创建了一个 Dog
类的实例,并测试了继承的属性和方法。
function Animal(name) {
this.name = name;
}
// 在原型链上定义一个新方法
Animal.prototype.sayName = function() {
console.log('My name is ' + this.name);
};
// 定义子类 Dog
function Dog(name, breed) {
// 调用父类构造函数,并继承属性
Animal.call(this, name);
this.breed = breed;
}
// 使用 Object.create 函数继承父类原型上的方法
Dog.prototype = Object.create(Animal.prototype);
// 修正子类构造函数指向
Dog.prototype.constructor = Dog;
// 在子类原型上定义一个新方法
Dog.prototype.bark = function() {
console.log('Woof!');
};
// 使用 Object.defineProperty 函数定义属性特性
Object.defineProperty(Dog.prototype, 'color', {
get: function() {
return this._color;
},
set: function(value) {
if (value === 'brown' || value === 'black' || value === 'white') {
this._color = value;
} else {
console.error('Invalid color!');
}
}
});
// 创建 Dog 类的实例
let myDog = new Dog('Buddy', 'Labrador');
myDog.sayName(); // 输出: My name is Buddy
myDog.bark(); // 输出: Woof!
// 测试属性特性
myDog.color = 'brown';
console.log(myDog.color); // 输出: brown
myDog.color = 'red'; // 输出: Invalid color!
在这段代码中,涉及了以下原型链和组合继承的知识点:
- 原型链:
- 在
Animal
基类的原型链上定义了一个方法sayName()
,用于输出动物的名字。 - 使用
Object.create
继承了Animal
基类原型上的方法,用于创建Dog
子类的原型链。
- 在
- 组合继承:
- 在
Dog
子类的构造函数中,使用Animal.call(this, name)
实现了对基类构造函数的调用,继承了基类的属性。 - 使用
Object.create
继承了Animal
基类原型上的方法,实现了对基类原型方法的继承。 - 使用
Dog.prototype.constructor = Dog
修正了子类的构造函数指向,确保正确标识子类的构造函数。 - 使用**
Object.defineProperty
** 函数定义属性特性为子类本身
- 在
总结
- 原型链:每个 JavaScript 对象都有一个原型链,它是一种对象到对象的链式结构,用于实现对象间的继承和属性查找。当访问一个对象的属性或方法时,JavaScript 引擎会先在当前对象上查找,如果找不到,则会沿着原型链向上查找,直到找到为止。通过原型链,可以实现对象的属性和方法的共享,减少内存消耗,提高代码的效率。
- 组合继承:组合继承是一种实现继承的方式,结合了原型链和构造函数的特点。它通过在子类构造函数内部调用父类构造函数,实现对父类属性的继承;同时,通过将子类的原型对象指向父类的实例,实现对父类原型方法的继承。这样,子类既继承了父类的属性,又继承了父类原型上的方法,实现了完整的继承关系。
实践作业
设计一个简单的类继承关系,包括一个基类 Person
和两个子类 Student
和 Teacher
。其中,基类 Person
包含了姓名和年龄两个属性,以及一个方法 introduce()
,用于输出人的介绍。子类 Student
继承了基类 Person
,并添加了一个新的属性 grade
表示学生的年级,以及一个方法 study()
,用于输出学生的学习行为。子类 Teacher
同样继承了基类 Person
,并添加了一个新的属性 subject
表示教师的科目,以及一个方法 teach()
,用于输出教师的教学行为。
- 使用构造函数定义基类
Person
,接受两个参数name
和age
。 - 在基类
Person
的原型链上定义一个方法introduce()
,用于输出姓名和年龄。 - 使用组合继承定义子类
Student
和Teacher
。 - 子类
Student
添加一个构造函数,接受三个参数name
、age
和grade
。 - 子类
Teacher
添加一个构造函数,接受三个参数name
、age
和subject
。 - 在子类
Student
的原型上定义一个方法study()
,用于输出学生的学习行为。 - 在子类
Teacher
的原型上定义一个方法teach()
,用于输出教师的教学行为。 - 创建一个
Student
类的实例,并测试其方法和属性。 - 创建一个
Teacher
类的实例,并测试其方法和属性。
参考答案
// 定义基类 Person
function Person(name, age) {
this.name = name;
this.age = age;
}
// 在基类 Person 的原型链上定义方法 introduce()
Person.prototype.introduce = function() {
console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`);
};
// 定义子类 Student,继承自基类 Person
function Student(name, age, grade) {
Person.call(this, name, age); // 调用基类构造函数,继承属性
this.grade = grade;
}
// 在子类 Student 的原型上定义方法 study()
Student.prototype.study = function() {
console.log(`I'm a student in grade ${this.grade}, and I'm studying hard.`);
};
// 定义子类 Teacher,继承自基类 Person
function Teacher(name, age, subject) {
Person.call(this, name, age); // 调用基类构造函数,继承属性
this.subject = subject;
}
// 在子类 Teacher 的原型上定义方法 teach()
Teacher.prototype.teach = function() {
console.log(`I'm a teacher of ${this.subject}, and I'm teaching.`);
};
// 创建 Student 类的实例,并测试
let student = new Student('Alice', 20, 'Grade 10');
student.introduce(); // 输出: Hello, my name is Alice and I'm 20 years old.
student.study(); // 输出: I'm a student in grade Grade 10, and I'm studying hard.
// 创建 Teacher 类的实例,并测试
let teacher = new Teacher('Bob', 30, 'Math');
teacher.introduce(); // 输出: Hello, my name is Bob and I'm 30 years old.
teacher.teach(); // 输出: I'm a teacher of Math, and I'm teaching.