第六章 JavaScript高级功能
第三节 原型与对象继承
概述
本节内容围绕JavaScript中非常核心且独特的机制——原型与对象继承展开。理解原型链和继承机制是掌握JavaScript高级编程的关键,是应对复杂应用设计和优化代码结构的基础。通过本节学习,考生将能够:
- 理解JavaScript对象的本质及原型链的构建方式
- 掌握对象继承的多种实现方法及其原理
- 深入理解构造函数、prototype属性和原型链之间的内在联系
- 解决常见的继承误区,写出更加健壮和高效的代码
- 了解ES6中class语法对继承的封装和底层原理
本节内容约3500字,结构完整,适合全国计算机等级考试二级Web程序设计备考及深入学习。
核心概念
1. 对象(Object)
JavaScript中的所有实体几乎都是对象。对象是属性和方法的集合,属性可以是基本类型,也可以是其他对象。
2. 原型(Prototype)
每个JavaScript对象都有一个内部属性[[Prototype]](在代码中可以通过__proto__访问),指向另一个对象,这个对象称为该对象的原型。
3. 原型链(Prototype Chain)
当访问一个对象的属性时,如果对象本身没有该属性,则JavaScript引擎会沿着原型链向上查找,直到找到该属性或到达原型链的顶端null。
4. 构造函数(Constructor Function)
构造函数是用来生成对象的特殊函数。通过new关键字调用构造函数时,会创建一个新对象,并将该对象的__proto__指向构造函数的prototype属性。
5. prototype属性
构造函数的prototype属性是一个对象,这个对象包含该构造函数创建的所有实例共享的属性和方法。
6. 继承(Inheritance)
继承是指一个对象可以访问另一个对象的属性和方法。JavaScript使用原型链实现继承,子对象的原型指向父对象,实现属性和方法的共享。
原理分析
JavaScript的继承机制基于原型链。每个对象都有一个内部指针指向其原型对象。查找属性时,如果本对象没有,则沿着这个指针不断查找,直到找到属性或达到原型链顶端null。
new 操作符的执行过程:
- 创建一个新对象。
- 将新对象的__proto__指向构造函数的prototype属性。
- 将构造函数内部的this指向新对象。
- 执行构造函数内部代码,给新对象添加属性。
- 返回新对象。
因此,所有通过同一构造函数创建的实例对象共享构造函数prototype上的属性和方法。
继承的实现方式主要有:
- 原型链继承
- 借用构造函数继承(经典继承)
- 组合继承(原型链+借用构造函数)
- 原型式继承
- 寄生式继承
- ES6 class继承
每种方式都基于原型链,不同之处在于具体实现细节和适用场景。
详细内容
1. JavaScript对象与原型基础
JavaScript中对象是键值对的集合。每个对象都有一个内部属性[[Prototype]],指向它的原型对象。该原型对象也有自己的原型,形成原型链,一直延伸到null。
const obj = {a: 1};
console.log(obj.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__); // null
访问obj的属性时,如果obj自身没有该属性,会去obj.__proto__中查找,依次向上查找,直到找到或返回undefined。
Object.prototype是所有普通对象的最终原型。
2. 构造函数和prototype属性
构造函数用来创建同类对象,构造函数本身有一个prototype属性,该属性是一个对象,所有实例共享该对象的属性和方法。
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
console.log('Hello, ' + this.name);
};
const p1 = new Person('Alice');
p1.sayHello(); // Hello, Alice
- p1.proto === Person.prototype
- Person.prototype.constructor === Person
3. 原型链继承
通过将子类的原型对象指向父类的实例,实现继承父类的属性和方法。
function Parent() {
this.name = 'parent';
}
Parent.prototype.sayName = function() {
console.log(this.name);
}
function Child() {}
Child.prototype = new Parent();
const child = new Child();
child.sayName(); // parent
缺点:
- 所有子类实例共享父类实例的引用属性,修改会影响其他实例。
- 无法实现多继承。
4. 借用构造函数继承
在子类构造函数中调用父类构造函数,使用call或apply改变this指向。
function Parent(name) {
this.name = name;
}
function Child(name) {
Parent.call(this, name);
}
const child1 = new Child('child1');
console.log(child1.name); // child1
优点:
- 每个实例都有自己的属性副本,避免引用共享问题。
缺点:
- 只能继承父类构造函数中的属性和方法,父类原型上的方法不能继承。
5. 组合继承(最常用)
结合原型链继承和借用构造函数继承的优点。
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue'];
}
Parent.prototype.sayName = function() {
console.log(this.name);
}
function Child(name, age) {
Parent.call(this, name); // 借用构造函数继承属性
this.age = age;
}
Child.prototype = new Parent(); // 原型链继承方法
Child.prototype.constructor = Child;
const child1 = new Child('Alice', 20);
child1.colors.push('green');
console.log(child1.colors); // ['red', 'blue', 'green']
child1.sayName(); // Alice
const child2 = new Child('Bob', 22);
console.log(child2.colors); // ['red', 'blue']
child2.sayName(); // Bob
优点:
- 解决了引用属性共享问题。
- 继承了父类原型上的方法。
6. 原型式继承
基于Object.create实现,创建一个临时构造函数,将传入对象作为新对象的原型。
const person = {
name: 'default',
sayHi: function() {
console.log('Hi, ' + this.name);
}
};
const anotherPerson = Object.create(person);
anotherPerson.name = 'Jerry';
anotherPerson.sayHi(); // Hi, Jerry
7. 寄生式继承
在原型式继承的基础上,扩展对象,增加新功能。
function createAnother(original) {
const clone = Object.create(original);
clone.sayBye = function() {
console.log('Bye, ' + this.name);
};
return clone;
}
const person2 = createAnother(person);
person2.name = 'Tom';
person2.sayHi(); // Hi, Tom
person2.sayBye(); // Bye, Tom
8. ES6 class继承
ES6引入class语法,更清晰地表达继承关系,底层仍基于原型链。
class Parent {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}
class Child extends Parent {
constructor(name, age) {
super(name);
this.age = age;
}
sayAge() {
console.log(this.age);
}
}
const c = new Child('Jack', 25);
c.sayName(); // Jack
c.sayAge(); // 25
实例分析
案例一:解决引用属性共享问题
function Parent() {
this.colors = ['red', 'green'];
}
function Child() {}
Child.prototype = new Parent();
const child1 = new Child();
child1.colors.push('blue');
console.log(child1.colors); // ['red','green','blue']
const child2 = new Child();
console.log(child2.colors); // ['red','green','blue'],意外共享
分析:
原型链继承导致所有实例共享colors数组,修改一个实例的colors会影响其他实例。
解决方案:
使用借用构造函数继承,将colors定义在构造函数内。
案例二:组合继承实现完整继承
function Parent(name) {
this.name = name;
this.colors = ['red', 'green'];
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
const child1 = new Child('Alice', 18);
child1.colors.push('blue');
console.log(child1.colors); // ['red','green','blue']
child1.sayName(); // Alice
const child2 = new Child('Bob', 20);
console.log(child2.colors); // ['red','green']
child2.sayName(); // Bob
分析:
组合继承避免了引用共享,并且继承了父类原型方法,满足大多数继承需求。
案例三:ES6 class继承的语法糖
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(this.name + ' makes a noise.');
}
}
class Dog extends Animal {
speak() {
console.log(this.name + ' barks.');
}
}
const dog = new Dog('Rex');
dog.speak(); // Rex barks.
分析:
ES6 class继承底层仍是原型链机制,但语法更加直观,易于理解和维护。
常见误区
误区:认为所有对象都有prototype属性
- 说明:只有函数才有prototype属性,普通对象没有。
- 正确做法:通过__proto__或Object.getPrototypeOf访问对象的原型。
误区:直接修改实例的__proto__属性实现继承
- 说明:直接修改__proto__性能差且不安全。
- 正确做法:使用Object.create或构造函数+prototype实现继承。
误区:原型链继承会导致引用属性共享
- 说明:引用类型属性定义在原型上,会被共享,导致意外修改。
- 正确做法:将引用属性定义在构造函数内,结合借用构造函数继承解决。
误区:忘记修正子类prototype.constructor属性
- 说明:子类原型指向父类实例后,constructor属性会指向父类。
- 正确做法:手动将constructor指回子类,保持正确的构造函数引用。
误区:ES6 class继承完全替代原型继承
- 说明:class只是语法糖,底层仍用原型链实现。
- 正确做法:理解class继承原理,灵活使用。
应用场景
构建复杂对象模型
- 通过继承实现代码复用和功能扩展,如游戏角色、UI组件等。
框架和库设计
- 利用原型继承设计组件基类,使得子类继承基本功能并拓展。
面向对象编程
- 实现封装、继承、多态等面向对象思想,提高代码可维护性。
动态扩展对象功能
- 通过原型链动态添加方法,实现插件式开发。
性能优化
- 避免在每个实例中重复定义方法,通过共享原型方法节省内存。
知识拓展
ES6 Symbol.hasInstance
- 控制instanceof行为,深入理解继承判断。
Mixin混入模式
- 多继承替代方案,多个对象属性合并到一个对象。
Proxy与Reflect
- 代理对象操作,影响继承链行为。
类的私有属性和方法
- 新的私有字段语法,封装更完善。
原型链与内存管理
- 理解原型链对垃圾回收和性能的影响。
总结回顾
本节详细介绍了JavaScript的原型与对象继承机制,重点包括:
- 对象的原型属性及原型链查找机制
- 构造函数与prototype的关系
- 各种继承方式的实现及优缺点
- 典型继承模式:原型链继承、借用构造函数继承、组合继承
- ES6 class继承的语法及其本质
- 常见误区和正确使用方法
- 多种实际应用场景示例
掌握本节内容,考生不仅能在考试中准确回答相关题目,也能在实际项目开发中设计出合理的面向对象结构,提升代码复用性和可维护性。建议结合代码实践和调试工具深入理解原型链机制,做到融会贯通。祝学习进步!