《从分遗产说起:JS 原型与继承详解》
“天天开心就好”
先来讲讲概念:
原型(Prototype)
什么是原型?
原型是 JavaScript 中实现对象间共享属性和方法的机制。每个 JavaScript 对象(除了 null
)都有一个内部链接指向另一个对象,这个对象就是它的"原型"(prototype)。
继承(Inheritance)
什么是继承?
继承是面向对象编程中的一个核心概念,它允许一个对象(子对象)获取另一个对象(父对象)的属性和方法。在 JavaScript 中,继承主要通过原型链来实现。
好的现在我们明确了什么是原型,什么是继承。简单来说,原型就是一个机制,每个对象内部都一个内部链接指向他的原型。继承我的理解就是一种行为,就是像继承财产那样继承父对象的属性和方法,可谓是形容十分贴切。
原型基础
原型对象 (prototype)
- 每个函数都有一个
prototype
属性(箭头函数除外) - 这个属性指向一个对象,称为"原型对象"
- 原型对象包含可以被特定类型的所有实例共享的属性和方法
function Person() {}
Person.prototype.name = 'Default';
Person.prototype.sayHello = function() {console.log(`Hello, I'm ${this.name}`);
};
__proto__
属性
- 每个对象都有一个
__proto__
属性(现已标准化为Object.getPrototypeOf()
) - 指向创建该对象的构造函数的原型对象
const person = new Person();
console.log(person.__proto__ === Person.prototype); // true
这个很好理解了,我在这里想用c语言里面的指针来形容了。prototype就像是地址对应的数据,而_proto_就像是指向他的指针。不太恰当哈
我们经常这样说:对象的继承是通过原型链实现的。
那么什么是原型链:
什么是原型链?
原型链(Prototype Chain)是 JavaScript 中实现继承的核心机制。当访问一个对象的属性或方法时,JavaScript 引擎会沿着对象的原型链向上查找,直到找到该属性或到达原型链的末端(null
)。
原型链的构成
- 每个对象都有一个
__proto__
属性(现已标准化为Object.getPrototypeOf()
) - 每个函数都有一个
prototype
属性 - 原型链的终点是
null
原型链的工作原理
当访问一个对象的属性时:
- 首先在对象自身查找该属性
- 如果没有找到,则查找对象的
__proto__
(即其构造函数的prototype
) - 如果还没有找到,继续查找
__proto__.__proto__
,依此类推 - 直到找到该属性或到达
null
(此时返回undefined
)
function Animal(name) {this.name = name;
}
Animal.prototype.eat = function() {console.log(`${this.name} is eating`);
};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('Woof!');
};const myDog = new Dog('Buddy', 'Golden Retriever');// 原型链:
// myDog -> Dog.prototype -> Animal.prototype -> Object.prototype -> null
我们讲原型和继承就是在意js中继承这种行为是怎么实现的,就像现实中大家只在乎怎么分遗产一样!
继承实现方式
1.原型链继承
function Parent() {this.parentProperty = true;
}
Parent.prototype.getParentValue = function() {return this.parentProperty;
};function Child() {this.childProperty = false;
}
// 继承 Parent
Child.prototype = new Parent();const instance = new Child();
console.log(instance.getParentValue()); // true
问题:
- 所有子类实例共享同一个父类实例
- 无法向父类构造函数传参
2. 构造函数继承
function Parent(name) {this.name = name;
}function Child(name) {Parent.call(this, name); // 在子类构造函数中调用父类构造函数
}const child = new Child('Alice');
console.log(child.name); // 'Alice'
优点:
- 可以向父类传参
- 每个子类实例都有独立的父类属性副本
缺点:
- 无法继承父类原型上的方法
3.组合继承(最常用)
function Parent(name) {this.name = name;
}
Parent.prototype.sayName = function() {console.log(this.name);
};function Child(name, age) {Parent.call(this, name); // 第二次调用 Parentthis.age = age;
}
Child.prototype = new Parent(); // 第一次调用 Parent
Child.prototype.constructor = Child; // 修复构造函数指向const child = new Child('Alice', 25);
child.sayName(); // 'Alice'
优点:
- 结合了原型链和构造函数的优点
- 既能继承原型方法,又能保证实例属性独立
缺点:
- 父类构造函数被调用了两次
4. 原型式继承
function object(o) {function F() {}F.prototype = o;return new F();
}const parent = { name: 'Parent' };
const child = object(parent);
console.log(child.name); // 'Parent'
ES5 标准化为 Object.create()
:
const child = Object.create(parent);
5. 寄生式继承
function createAnother(original) {const clone = Object.create(original);clone.sayHi = function() {console.log('Hi');};return clone;
}
6. 寄生组合式继承(最佳实践)
function inheritPrototype(child, parent) {const prototype = Object.create(parent.prototype);prototype.constructor = child;child.prototype = prototype;
}function Parent(name) {this.name = name;
}
Parent.prototype.sayName = function() {console.log(this.name);
};function Child(name, age) {Parent.call(this, name);this.age = age;
}inheritPrototype(Child, Parent);const child = new Child('Alice', 25);
child.sayName(); // 'Alice'
优点:
- 只调用一次父类构造函数
- 原型链保持正确
- 最理想的继承方式
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;}
}const child = new Child('Alice', 25);
child.sayName(); // 'Alice'
注意:
class
本质上是语法糖,底层仍然是基于原型的继承extends
实现了寄生组合式继承- 必须在使用
this
前调用super()
继承是js中很核心的机制了,有很多中方式来实现继承,继承的好处就是我们可以直接继承父对象的方法和属性而不用自己再次定义了。用好继承可以大大提升我们的代码水平,帮助我们实现更多复杂需求。