构造函数和原型
在 ES6 之前,通过构造函数和原型模拟类的实现机制,也就是 ES6 中的类
构造函数
- 本质上是一个函数,把对象中公共的属性和方法封装到函数中,用来创建对象,和
new
一起使用才有意义 - 身上的
constructor
属性指向自身 (这句话可能有问题) - new 执行四步骤:
- 在内存中创建一个空对象;
- 让构造函数的
this
指向这个空对象(将这个对象的原型设置为构造函数的 prototype 对象); - 执行构造函数里面的代码,给这个空对象添加属性和方法;
- 返回这个新对象(所以构造函数里面不需要 return )
// 构造函数
function Star(name, age) {
this.name = name;
this.age = age;
}
// 创建对象
const ldh = new Star("刘德华", 18);
静态成员和实例成员
- 构造函数中的属性和方法称为成员,可以添加;
- 实例成员是构造函数内部通过 this 添加的成员,如上方的 name,age;实例成员只能通过实例化的对象来访问,如通过 ldh 来访问实例成员;
- 静态成员是在构造函数本身上添加的成员,只能通过构造函数访问,不能通过实例化对象来访问。
// 构造函数
function Star(name, age) {
this.name = name; // 实例成员
this.age = age;
}
Star.sex = "男"; // 静态成员只能通过以下方式添加,不能直接写在构造函数里面
// 给原型对象身上添加属性或者方法,所有的实例成员都可访问到,但 Star 本身直接访问不到
Star.prototype.sing = () => {
console.log("singing");
};
原型
1️⃣ 构造函数的原型对象 prototype
构造函数存在内存浪费问题,因为函数是复杂类型,会单独开辟一个空间存储函数,这样每次调用构造函数的时候,都会为方法单独开辟一个空间存储相同的函数,造成了浪费;
为了解决内存浪费的问题,一般将公共属性 (普通类型) 定义在构造函数里面,将公共的方法 (复杂类型) 定义在构造函数的原型对象上
构造函数通过原型分配的函数是所有实例对象共享的; 每个构造函数都有一个 prototype 属性(也叫原型对象),指向另一个对象,这个对象的所有属性和方法都会被构造函数所拥有; > 可以把不变的方法定义在原型对象上,这样所有对象的实例就可以共享这些方法;
function Star(name, age) {
this.name = name;
this.age = age;
}
Star.prototype.sing = function() { console.log('唱歌ing') }; // 在构造函数的原型对象上添加方法
const ldh = new Star('刘德华', 18);
ldh.sing; // 实例化的对象都可以使用构造函数原型对象上的方法
2️⃣ 对象的原型 __proto__
每个对象都有一个属性 __proto__
指向其构造函数的 prototype 原型对象,则每个对象可以使用其构造函数的 prototype 原型对象身上的属性和方法。ldh.__proto__ === Star.prototype

constructor 属性
Star 原型对象身上有一个 constructor 属性,指向 Star 构造函数;用于记录该对象引用于哪个构造函数。
// 使用下面的方法只是在 Star 原型对象上添加属性,不会覆盖原来的原型对象
Star.prototype.sing = function () {
console.log("我会唱歌");
};
// 如果修改了原来的原型对象,给原型对象赋值的是一个对象,则必须利用 constructor 指回原来的构造函数
Star.prototype = {
constructor: Star,
sing: function () {
console.log("我会唱歌");
},
movie: function () {
console.log("我会演电影");
},
};
原型链
只要是对象,就都有 __proto__
属性,该属性指向一个原型对象
原型对象的应用
扩展内置对象的方法; 数组和字符串内置对象不能给原型对象覆盖操作,只能是用追加属性和方法的方式;
// 给数组添加(追加)自定义求和方法
Array.prototype.sum = function () {
let sum = 0;
for (let i = 0; i < this.length; i++) {
sum += this[i];
}
return sum;
};
const arr = [1, 2, 3]; // 定义数组
const sum = arr.sum(); // 调用原型对象方法
模拟继承 (构造函数+原型对象)
和上面的 Class 类进行对比
call()
方法调用这个函数,并修改函数运行时 this 的指向 fun.call(thisArg, arg1, arg2, ...)
thisArg 表示当前调用函数 this 的指向对象 arg 表示传递的其他参数
function fn(x) {
console.log(this.name, '你好啊', x);
};
const person = {
name: 'Jack'
};
fn.call(person, 666);
// 输出:Jack 你好啊 666
// apply 方法亦可,不过参数要以为数组的形式传入;bind 方法返回一个新函数,不会调用该函数
如何实现继承: 利用构造函数实现父类的属性,利用原型对象实现父类的方法; 通过 call()
把父类的 this 指向子类的 this。
// 1. 父构造函数
function Father(uname, age) {
// this 指向父构造函数的实例对象
this.uname = uname;
this.age = age;
}
Father.prototype.money = function () {
console.log(100000);
};
// 2. 子构造函数
function Son(uname, age, score) {
// 通过 call() 方法,调用父构造函数,并将其 this 指向子类(继承属性)
Father.call(this, uname, age); // 这里的 this 相当于 Son
this.score = score;
}
// 3. 原型链实现继承方法
Son.prototype = new Father(); // 子构造函数的原型对象指向父构造函数的实例对象
Son.prototype.constructor = Son; // 更改了原型对象,要利用constructor指回原来的构造函数
const son = new Son("刘德华", 18, 100);
son.money();

原型链查找的性能考虑
原型链查找虽然方便,但也需要注意性能问题:
查找效率:当访问一个对象的属性时,JavaScript 引擎会沿着原型链逐层查找,直到找到属性或到达原型链末端。如果属性在原型链的很上层,查找时间会相应增加。
内存占用:虽然原型方法共享可以节省内存,但如果实例对象过多,原型对象上的方法也会占用内存。
// 不推荐的做法:在原型链上查找不存在的属性
function Person() {}
const p = new Person();
console.log(p.someNonExistentProperty); // 会遍历整个原型链
// 推荐的做法:使用 hasOwnProperty 检查属性
if (p.hasOwnProperty('someProperty')) {
// 处理属性
}
ES6 类与原型继承的对比
ES6 的 class
语法实际上是原型继承的语法糖,但提供了更清晰的语法结构:
// ES6 类语法
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a sound.`);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
bark() {
console.log(`${this.name} barks.`);
}
}
// 等价的原型继承写法
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function() {
console.log(`${this.name} makes a sound.`);
};
function Dog(name, breed) {
Animal.call(this, name);
this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function() {
console.log(`${this.name} barks.`);
};
原型继承的常见陷阱
- 原型污染:修改原型对象会影响所有继承该原型的实例
Array.prototype.push = function() {
console.log('被修改了!');
};
// 所有数组实例的 push 方法都被改变了
- 构造函数丢失:覆盖原型对象时忘记设置 constructor
// 错误示例
Child.prototype = {
method() {}
};
// 正确示例
Child.prototype = {
constructor: Child,
method() {}
};
- 属性遮蔽:实例属性会遮蔽原型上的同名属性
function Person() {}
Person.prototype.name = 'default';
const p = new Person();
p.name = 'custom';
console.log(p.name); // 'custom'
delete p.name;
console.log(p.name); // 'default'
实际应用场景
- 框架开发:许多 JavaScript 框架使用原型继承来实现插件系统
// 简单的插件系统示例
function Plugin() {}
Plugin.prototype.init = function() {};
Plugin.prototype.destroy = function() {};
function MyPlugin() {}
MyPlugin.prototype = Object.create(Plugin.prototype);
- DOM 操作封装:创建自定义的 DOM 操作方法
Element.prototype.addClass = function(className) {
if (!this.classList.contains(className)) {
this.classList.add(className);
}
return this;
};
Element.prototype.removeClass = function(className) {
this.classList.remove(className);
return this;
};
- 工具函数库:扩展内置对象的功能
String.prototype.capitalize = function() {
return this.charAt(0).toUpperCase() + this.slice(1);
};
Array.prototype.unique = function() {
return [...new Set(this)];
};
最佳实践建议
- 优先使用 ES6 的
class
语法,它更清晰且不容易出错 - 避免直接修改内置对象的原型
- 使用
Object.create()
而不是直接赋值来设置原型 - 始终记得设置
constructor
属性 - 使用
hasOwnProperty()
检查属性是否存在 - 考虑使用组合而不是继承来实现代码复用