彻底理解JS中ES5原型和原型链与ES6类和继承妈妈再也不用怕被面试官问到JS中类的继承。设计原型和原型链的目的 Jav
妈妈再也不用怕被面试官问到JS中类的继承
设计原型和原型链的目的
JavaScript 设计之初是为了在浏览器中提供一种简单的脚本语言来处理网页上的动态内容。为了实现这一目标,JavaScript 强调了动态性和灵活性:
- 动态性:JavaScript 允许在运行时修改对象的属性和方法,这种灵活性使得原型链成为一个自然的选择。你可以在运行时动态地添加、修改或删除对象的属性和方法,而这种能力在其他一些面向对象语言中是有限的。
- 灵活性:JavaScript 的原型继承机制允许开发者以一种非常灵活的方式来创建和扩展对象,这种机制支持了动态属性和方法的修改和继承,这在早期编程语言中并不常见。
当然这种简单还体现在类型约束上,随着用户体量的增大,应用面积的增大,项目越来越复杂,也又有了TypeScript进行类型约束。
原型链(Prototype Chain)是 JavaScript 语言中独特的继承机制,它允许对象通过其原型链继承其他对象的属性和方法。与传统的基于类的继承机制相比,原型链的设计有以下特点:
- 原型对象:每个 JavaScript 对象都有一个原型对象,这个原型对象本身也是一个对象,形成链式结构。这样,属性的查找不仅仅在对象本身进行,还会沿着原型链进行查找。
- 简化的继承机制:相对于基于类的继承机制,原型链提供了一种更简单的方式来实现继承。没有严格的类的概念,继承通过修改对象的原型链来实现。
在 JavaScript 中,隐式和显式主要体现在对象属性和方法的访问与修改上:
- 隐式:当访问一个对象的属性时,如果该对象没有这个属性,JavaScript 会沿着原型链查找。这个过程是隐式的,即开发者不需要显式地指定这个查找过程。
- 显式:当创建对象时,可以显式地定义属性和方法,这些属性和方法会直接挂在对象本身上,不会影响到原型链。
底层的实现是由链表实现的
JavaScript 的原型和原型链机制在底层的实现涉及到如何管理对象及其继承关系。虽然不同的 JavaScript 引擎(如 V8、SpiderMonkey、JavaScriptCore)有各自的实现方式,但原理大致相同。这里,我们以 V8 引擎(Chrome 和 Node.js 使用的引擎)为例,简要介绍底层实现的基本概念。
1. 对象和原型
在 JavaScript 中,每个对象都有一个内部属性 [[Prototype]]
,指向另一个对象,这个对象就是该对象的原型。V8 引擎中的实现可以简化为以下几个步骤:
- 对象创建:当你创建一个对象时,比如使用
new Object()
,V8 引擎会为这个对象分配一个内存区域,并将其内部的[[Prototype]]
属性设置为Object.prototype
。 - 原型链:原型链是由一系列对象通过
[[Prototype]]
属性连接起来的。V8 引擎通过维护这些对象的指针来实现原型链。
2. 原型链查找
当访问一个对象的属性或方法时,V8 引擎会沿着原型链查找:
- 查找过程:V8 首先在对象自身的属性中查找,如果找不到,就会沿着
[[Prototype]]
指针查找,直到找到该属性或到达原型链的末端(即null
)。 - 性能优化:为了提高性能,V8 引擎会缓存查找结果,并在内存中保持原型链的优化结构,以减少查找时间。
3. 原型和原型链的实现细节
- 内存结构:在 V8 引擎中,对象的原型链被表示为一个链表结构。每个对象都有一个指向其原型对象的指针,这样形成了一个链式结构。V8 内部使用复杂的数据结构来优化原型链的访问和管理。
- 原型继承:当一个对象继承另一个对象时,V8 引擎会将子对象的
[[Prototype]]
指向父对象。这个关系在内存中被维护为对象指针的链表结构,支持动态的属性访问和继承。
4. 具体实现(V8 引擎)
以下是一些具体实现的概念:
JSObject
类:在 V8 中,对象通常由JSObject
类表示。JSObject
内部维护指向其原型的指针,以及对象自身的属性。Map
和Set
:V8 使用Map
和Set
等数据结构来优化原型链的查找和管理。例如,属性查找可以利用哈希表来加速。Lookup
和Get
操作:V8 中的Lookup
操作负责在原型链上查找属性,而Get
操作则处理属性的读取。V8 引擎对这些操作进行了高度优化,以提高性能。
5. 原型链的动态修改
在 JavaScript 中,你可以动态地修改对象的原型链,比如使用 Object.setPrototypeOf
。V8 引擎支持这种动态修改,但为了性能,通常会将这种修改的开销降到最低,并且在某些情况下可能会限制原型链的修改能力。
原型和原型链的具体解释和好坏处区别
1. 原型和原型链的解释
原型
- 定义:每个 JavaScript 对象都有一个内部属性
[[Prototype]]
,这个属性指向另一个对象,这个对象就是该对象的原型。 - 作用:当你访问对象的属性或方法时,JavaScript 会首先在对象自身查找,如果找不到,就会沿着原型链查找。
原型链
- 定义:原型链是由一系列对象通过
[[Prototype]]
属性连接起来的链式结构。最终链的末端是null
,这是Object.prototype
的原型。 - 作用:通过原型链,实现了对象之间的继承和共享属性/方法。
2. 例子
// 定义一个父类对象
const Animal = {
speak() {
console.log('Animal speaks');
}
};
// 定义一个子类对象,继承自 Animal
const Dog = Object.create(Animal);
Dog.bark = function() {
console.log('Dog barks');
};
// 创建 Dog 的实例
const myDog = Object.create(Dog);
myDog.name = 'Buddy';
// 访问属性和方法
console.log(myDog.name); // 输出 'Buddy'
myDog.speak(); // 输出 'Animal speaks' (继承自 Animal)
myDog.bark(); // 输出 'Dog barks' (自身的属性)
解释:
Animal
是一个普通对象,具有speak
方法。Dog
使用Object.create(Animal)
创建,Dog
的原型指向Animal
,因此Dog
继承了Animal
的speak
方法。myDog
使用Object.create(Dog)
创建,myDog
的原型链是myDog -> Dog -> Animal
。- 访问
myDog.name
从myDog
对象自身获取。 - 访问
myDog.speak()
时,JavaScript 首先在myDog
对象中查找,找不到就沿着原型链查找,最终找到Animal
对象上的speak
方法。 - 访问
myDog.bark()
时,直接在myDog
对象上找到bark
方法。
3. 原型链的好处
- 属性共享:原型链允许不同对象共享同一份属性和方法。例如,所有从
Animal
继承的对象都可以访问speak
方法,避免了重复定义。 - 动态继承:通过原型链,子对象可以动态地继承父对象的属性和方法。这种动态性使得对象间的关系非常灵活。
- 节省内存:由于方法和属性是在原型对象上定义的,而不是每个实例上定义的,这样可以节省内存。所有实例共享相同的方法,减少了内存使用。
4. 原型链的坏处
- 性能开销:访问属性时,需要沿着原型链查找,这会增加查找的开销,尤其是当原型链非常长时。
- 复杂性:当原型链变得复杂时,可能会导致调试困难。比如,如果原型链上有多个层级的对象,追踪某个属性的来源可能会变得棘手。
- 可能的修改影响:如果你在原型链的某个位置修改了属性或方法,这些修改会影响所有继承该原型链的对象。这种全局影响有时可能导致意外的副作用。
- 性能优化:一些 JavaScript 引擎可能会对原型链的性能进行优化,但对于一些操作,例如动态修改原型链,可能会导致性能下降。
__proto__
和 prototype
的具体解释
__proto__
和 prototype
是 JavaScript 中与原型链和继承相关的两个重要概念,它们在不同的上下文中起着不同的作用。
1. __proto__
- 定义:
__proto__
是每个 JavaScript 对象内部属性的一个非标准(但广泛支持的)访问方式,用于访问该对象的原型对象。它实际上是一个指向对象原型的指针。 - 用途:通过
__proto__
可以获取或设置一个对象的原型。它在调试或手动操作原型链时非常有用。
示例:
const person = {
name: 'Alice',
greet() {
console.log(`Hello, my name is ${this.name}`);
}
};
const student = {
grade: 'A'
};
// 设置 student 的原型为 person
student.__proto__ = person;
// 访问原型属性
console.log(student.name); // 输出 'Alice'
student.greet(); // 输出 'Hello, my name is Alice'
注意:
__proto__
是一个非标准的属性,虽然大多数现代浏览器支持它,但不推荐在生产代码中使用。- 在现代 JavaScript 中,推荐使用
Object.getPrototypeOf()
和Object.setPrototypeOf()
方法来操作对象的原型。
2. prototype
- 定义:
prototype
是函数对象的一个属性,用于定义该函数创建的对象的原型。换句话说,prototype
用于指定所有通过该构造函数创建的对象的共享属性和方法。 - 用途:当定义一个构造函数时,
prototype
属性可以用来添加方法和属性,这些方法和属性会被所有通过该构造函数创建的实例所共享。
示例:
// 构造函数
function Person(name) {
this.name = name;
}
// 在 prototype 上定义方法
Person.prototype.greet = function() {
console.log(`Hello, my name is ${this.name}`);
};
// 创建实例
const alice = new Person('Alice');
const bob = new Person('Bob');
alice.greet(); // 输出 'Hello, my name is Alice'
bob.greet(); // 输出 'Hello, my name is Bob'
解释:
Person.prototype
定义了一个greet
方法,这个方法对所有Person
的实例都是共享的。Person
函数的原型对象(即Person.prototype
)作为Person
实例的[[Prototype]]
。
总结
__proto__
:是对象的内部属性,指向对象的原型。用于访问或修改原型链,但不推荐在生产环境中使用,因为它是非标准的。建议使用Object.getPrototypeOf()
和Object.setPrototypeOf()
。prototype
:是构造函数的一个属性,用于定义构造函数创建的所有实例的共享属性和方法。它是实现 JavaScript 继承和共享的标准机制。
示意图:
Constructor Function (Person)
|
|-- prototype
|
|-- greet() (shared method)
Instance (alice)
|
|-- name
|
|-- __proto__ (指向Person的原型)
|
|-- greet()
JS中的类和继承
ES6 引入了类(class
)和继承(extends
)的语法糖,使得在 JavaScript 中定义和使用对象更加直观和面向对象。虽然 ES6 的类是基于原型链的,但它提供了一种更易于理解和使用的语法。
1. ES6 类的定义
ES6 的 class
关键字用于定义类。一个类包含构造函数(constructor
)和方法。类定义的基本语法如下:
class Person {
// 构造函数
constructor(name, age) {
this.name = name;
this.age = age;
}
// 方法
greet() {
console.log(`Hello, my name is ${this.name}`);
}
}
// 创建实例
const person = new Person('Alice', 30);
person.greet(); // 输出 'Hello, my name is Alice'
解释:
constructor
是一个特殊的方法,用于初始化类的新实例。它会在使用new
关键字创建对象时自动调用。- 方法
greet
定义在类的原型上,所有实例共享这个方法。
2. ES6 类的继承
ES6 的类允许子类继承父类的属性和方法,使用 extends
关键字实现。这使得创建继承关系变得更加直观和简洁。
示例:
// 父类
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a noise.`);
}
}
// 子类
class Dog extends Animal {
constructor(name, breed) {
super(name); // 调用父类的构造函数
this.breed = breed;
}
speak() {
super.speak(); // 调用父类的方法
console.log(`${this.name} barks.`);
}
}
// 创建实例
const dog = new Dog('Rex', 'Golden Retriever');
dog.speak();
// 输出:
// Rex makes a noise.
// Rex barks.
解释:
extends
关键字表示Dog
类继承自Animal
类。super
关键字用于调用父类的构造函数和方法。在Dog
的构造函数中,super(name)
调用父类的构造函数。super.speak()
在Dog
的speak
方法中调用父类的speak
方法。
3. 类的静态方法
ES6 类支持静态方法,这些方法是直接在类本身上调用的,而不是在类的实例上调用。
class MathUtils {
static add(a, b) {
return a + b;
}
}
// 调用静态方法
console.log(MathUtils.add(3, 4)); // 输出 7
解释:
static
关键字定义静态方法,静态方法不能在类的实例上调用,只能通过类名调用。这和Java中static思路一模一样。
4. 类的 getter 和 setter
ES6 类支持 getter 和 setter 方法,用于定义对象属性的访问和设置行为。
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
get area() {
return this.width * this.height;
}
set area(value) {
this.width = Math.sqrt(value);
this.height = Math.sqrt(value);
}
}
const rect = new Rectangle(4, 5);
console.log(rect.area); // 输出 20
rect.area = 36;
console.log(rect.width); // 输出 6
console.log(rect.height); // 输出 6
解释:
get
定义了一个 getter 方法,用于计算并返回属性值。set
定义了一个 setter 方法,用于设置属性值并更新相关属性。
5. 类的原型与实例
尽管 ES6 类语法提供了更简洁的面向对象编程方式,其底层实现仍基于原型链。这意味着类实例仍然是通过原型链继承父类的方法和属性的。
class Person {
constructor(name) {
this.name = name;
}
greet() {
console.log(`Hello, my name is ${this.name}`);
}
}
const p1 = new Person('Alice');
const p2 = new Person('Bob');
// 验证原型
console.log(p1.__proto__ === Person.prototype); // 输出 true
console.log(p2.__proto__ === Person.prototype); // 输出 true
总结
- ES6 类 提供了更直观的面向对象编程方式,简化了类和继承的定义。
- 继承 使用
extends
和super
关键字实现,支持子类继承父类的属性和方法。 - 静态方法 和 getter/setter 提供了类的额外功能,使得类更加灵活和强大。
- 尽管 ES6 类语法简化了面向对象编程,但其底层实现仍基于原型链,类的实例通过原型链继承父类的方法和属性。
JS中的继承
- 原型链继承 -- 子类的实例之间会共享从父类继承到的引用类型从而互相影响
- 构造函数继承 -- 无法继承父类原型上的属性
- 组合继承 -- 父类需要执行两遍
- 原生式继承 -- 多个实例之间继承的引用类型的数据是共享的
- 寄生式继承 -- xxxx
- 寄生组合继承 -- 最优雅的
- es6中的继承 class
其中的具体解释和案例
-
原型链继承 (Prototype Chain Inheritance) :
- 原理:通过将子类的原型对象设置为父类的实例,从而实现继承。
- 特点:子类实例和父类实例共享同一个原型对象。因此,如果在子类中修改原型属性,会影响所有子类实例。
- 问题:由于共享原型对象,子类实例之间会相互影响,可能导致难以追踪的错误。无法继承父类构造函数中的属性。
function Parent() { this.name = 'parent'; } function Child() { this.age = 18; } Child.prototype = new Parent(); const child = new Child(); console.log(child.name); // 'parent'
-
构造函数继承 (Constructor Inheritance) :
- 原理:在子类构造函数中调用父类构造函数,使用
call
或apply
方法。 - 特点:继承了父类构造函数中的属性,但无法继承父类原型上的属性和方法。
- 问题:每次创建子类实例时,都会调用父类构造函数,可能导致重复代码和性能问题。
function Parent(name) { this.name = name; } function Child(name, age) { Parent.call(this, name); this.age = age; } const child = new Child('child', 18); console.log(child.name); // 'child' console.log(child.age); // 18
- 原理:在子类构造函数中调用父类构造函数,使用
-
组合继承 (Combination Inheritance) :
- 原理:结合原型链继承和构造函数继承。先使用构造函数继承,再使用原型链继承。
- 特点:继承了父类构造函数中的属性和原型上的方法。
- 问题:父类构造函数被调用了两次(一次在构造函数继承中,一次在原型链继承中),可能会影响性能。
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; } Child.prototype = new Parent(); Child.prototype.constructor = Child; const child = new Child('child', 18); child.sayName(); // 'child'
-
原生式继承 (Augmented Prototype Inheritance) :
- 原理:通过直接将子类原型设置为父类实例,并在子类原型上添加构造函数。
- 特点:与原型链继承类似,但子类实例之间共享引用类型的数据。
- 问题:子类实例共享父类原型的引用类型数据,可能导致意外的副作用。
function Parent() { this.colors = ['red', 'blue']; } function Child() {} Child.prototype = new Parent(); Child.prototype.constructor = Child; const child1 = new Child(); const child2 = new Child(); child1.colors.push('green'); console.log(child2.colors); // ['red', 'blue', 'green']
-
寄生式继承 (Parasitic Inheritance) :
- 原理:创建一个继承自父类的副本,然后在副本上添加新的属性和方法。
- 特点:在副本上进行更改,能创建具有新特性的对象。
- 问题:和原型链继承类似,无法继承父类构造函数中的属性。
function createChild(Parent) { function Child() { Parent.call(this); } Child.prototype = Object.create(Parent.prototype); Child.prototype.constructor = Child; return Child; } function Parent() {} Parent.prototype.sayHello = function() { console.log('Hello'); }; const Child = createChild(Parent); const child = new Child(); child.sayHello(); // 'Hello'
-
寄生组合继承 (Parasitic Combination Inheritance) :
- 原理:结合了寄生式继承和组合继承的优点。先使用寄生式继承生成原型链,然后使用组合继承来构造实例。
- 特点:避免了组合继承中父类构造函数被调用两次的问题。
- 优势:较为优雅地解决了继承中的性能和功能问题。
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; } function inheritPrototype(child, parent) { const prototype = Object.create(parent.prototype); prototype.constructor = child; child.prototype = prototype; } inheritPrototype(Child, Parent); const child = new Child('child', 18); child.sayName(); // 'child'
-
ES6中的继承 (ES6 Class Inheritance) :
- 原理:使用
class
关键字定义类,通过extends
关键字实现继承。 - 特点:语法简洁,支持更直观的继承机制。
- 优势:符合现代JavaScript标准,易于理解和使用。
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('child', 18); child.sayName(); // 'child'
- 原理:使用
这些继承方式逐步演化,使得JavaScript的面向对象编程更加灵活和强大。从最初的原型链继承到ES6的class
,每种继承方式都有其适用场景和优缺点。
回味JS中继承的不断发展,你也能感受到JS这门语言的不断成熟的过程。
转载自:https://juejin.cn/post/7401403660246286390