1. JavaScript 中的继承是什么?
- 概念:继承是面向对象编程的核心概念之一,它允许一个对象从另一个对象获取属性和方法,从而实现代码复用。在 JavaScript 中,继承是通过原型链(prototype chain)实现的。
- 类继承和原型继承:JavaScript 最初是基于原型链的继承模式,但在 ES6 中引入了基于类(class)的语法糖,提供了更符合传统面向对象语言的继承方式。
2. 原型继承
- 原型链:每个对象都有一个内部属性
[[Prototype]]
,可以通过Object.getPrototypeOf()
或__proto__
来访问。一个对象可以通过其原型访问另一个对象的属性和方法。 prototype
属性:函数对象(尤其是构造函数)有一个prototype
属性,指向一个对象,这个对象的所有实例共享该对象的属性和方法。
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function() {
console.log(`${this.name} makes a noise.`);
}
const dog = new Animal('Dog');
dog.speak(); // "Dog makes a noise."
3.原型链继承
- 概念:原型链继承是 JavaScript 中继承的核心机制。在 JavaScript 中,每个对象都有一个
[[Prototype]]
隐式属性(也可以通过__proto__
访问),它指向另一个对象(即该对象的原型)。通过原型链,一个对象可以访问其原型对象以及更高层次原型链上的属性和方法。 - 工作原理:当你访问一个对象的属性或方法时,JavaScript 引擎首先在对象本身上查找。如果没有找到,就会沿着原型链向上查找,直到找到为止。如果到达原型链的顶端(即
null
,通常是Object.prototype
的原型为null
),仍然找不到该属性或方法,返回undefined
。 - 实现思路:通过将子类的原型对象设置为父类的实例对象,实现原型链的连接。通过这样操作后,子类就可以隐式继承父类的属性和方法了。
function Animal(name) {
this.name = name;
this.num = [1,2,3]
}
Animal.prototype.speak = function() {
console.log(`${this.name} makes a noise.`);
};
function Dog(name) {
this.name = name;
}
Dog.prototype = new Animal(); // 原型链继承
Dog.prototype.constructor = Dog;
const dog1 = new Dog('Dog1');
const dog2 = new Dog('Dog1');
dog1.num.push(4);
console.log(dog1.num)
console.log(dog2.num)
dog.speak(); // "Dog makes a noise."
在这个例子中,Dog.prototype
指向一个 Animal
实例,因此 dog
可以访问 Animal.prototype
上的 speak
方法。这就是原型链继承的典型示例。
- 缺点:但是通过原型链继承存在着一个很大的缺点。当我们分别创建Dog的实例对象dog1和dog2,通过dog1对num属性添加一个元素.
你会发现dog2
的num对象也发生了变化。子类的prototype属性是引用类型,一修改,全部改变。子类继承父类的引用类型属性都是继承了该引用类型属性的引用地址,简单来说就是子类操作的引用类型属性并不是它自己的,而是操作了父类的,虽然子类可以访问继承的引用类型属性,但是不是只属于自己的,而是所有子类共享的,只要一个子类操作了它,所有子类访问的都是被操作后的。
- 解决方法:在子类的构造函数中通过调用父类的构造函数并且改变其的this指向,将this指向子类的构造函数。 --------组合式继承
function Dog() {
Animal.call(this); // 确保每个实例都有自己的 colors 属性
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
4.组合式继承
Parent.prototype.getname = function () {
return this.name;
}
function Parent() {
this.name = 'parent';
this.age = 10;
this.arr = [1, 2, 3]
}
function Child() {
//借用构造函数继承
Parent.call(this);
this.type = 'children';
}
//原型链继承
Child.prototype = new Parent();
//手动将 Child 类的原型对象的 constructor 属性重新设置为 Child 函数本身
Child.prototype.constructor = Child;
let s1 = new Child();
let s2 = new Child();
s1.arr.push(4);
console.log(s1.arr);
console.log(s2.arr);
console.log(s1.getname());
- 缺点:调用两次构造函数造成重复继承
- 解决方法:寄生组合式继承
5.寄生组合式继承
ES6类继承的底层原理就是寄生组合式继承,是通过封装寄生组合式继承而来的。
// Shape——父类
function Shape() {
this.x = 0;
this.y = 0;
}
// 父类方法
Shape.prototype.move = function (x, y) {
this.x += x;
this.y += y;
console.info("Shape moved.");
};
// Rectangle——子类
function Rectangle() {
Shape.call(this); // 调用父类构造函数。
}
// 子类继承父类
Rectangle.prototype = Object.create(Shape.prototype, {
// 如果不将 Rectangle.prototype.constructor 设置为 Rectangle,
// 它将采用 Shape(父类)的 prototype.constructor。
// 为避免这种情况,我们将 prototype.constructor 设置为 Rectangle(子类)。
constructor: {
value: Rectangle,
enumerable: false,
writable: true,
configurable: true,
},
});
const rect = new Rectangle();
console.log("rect 是 Rectangle 类的实例吗?", rect instanceof Rectangle); // true
console.log("rect 是 Shape 类的实例吗?", rect instanceof Shape); // true
rect.move(1, 1); // 打印 'Shape moved.'
6. ES6 类继承
class
语法:ES6 引入了class
关键字,它提供了更清晰和结构化的语法来实现继承,背后依然是基于原型链。extends
和super
关键字:使用extends
关键字可以创建子类,super()
用于调用父类的构造函数或方法。
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a noise.`);
}
}
class Dog extends Animal {
age
constructor(name) {
super(name);// 调用父类的构造上函数
this.age = age
}
//同名方法,就近原则,优先使用自己的方法,无则使用父类方法
speak() {
console.log(`${this.name} barks.`);
}
}
const dog = new Dog('Dog');
dog.speak();
// 输出:
// "Dog makes a noise."
// "Dog barks."