在 JavaScript 的世界里,万物皆对象,而每个对象都有一个与之关联的原型对象,这就构成了原型链的基础。原型链,简单来说,是一个由对象的原型相互连接形成的链式结构 。每个对象都有一个内部属性[[Prototype]](在大多数浏览器中可以通过__proto__属性访问,不过__proto__是非标准属性,更推荐使用Object.getPrototypeOf()方法来获取原型),它指向该对象的原型对象。
以一个简单的例子来说明,我们创建一个构造函数Person:
function Person(name) {
this.name = name;
}
Person.prototype.sayName = function() {
console.log('My name is'+ this.name);
};
let person1 = new Person('Alice');
在这个例子中,person1是Person构造函数创建的实例对象。person1的__proto__属性指向Person.prototype,而Person.prototype也是一个对象,它同样有自己的__proto__属性,指向Object.prototype,Object.prototype的__proto__则为null,这就形成了一条完整的原型链:person1 -> Person.prototype -> Object.prototype -> null。
当我们访问person1的属性或方法时,比如调用person1.sayName(),JavaScript 引擎会首先在person1自身上查找是否有sayName方法。由于person1自身并没有定义sayName方法,引擎就会沿着原型链向上查找,在Person.prototype中找到了sayName方法,于是就执行该方法。如果在Person.prototype中也没有找到,就会继续向上在Object.prototype中查找,直到找到该属性或方法,或者到达原型链的顶端(null)。如果一直到原型链顶端都没有找到,就会返回undefined。
继承机制基础
JavaScript 基于原型链的继承机制是其实现代码复用和对象层次化结构的核心方式。简单来说,通过将一个对象的原型设置为另一个对象,新对象就可以继承原型对象的属性和方法。
继续以上面的Person构造函数为例,我们创建一个新的构造函数Student,让Student继承Person:
function Student(name, grade) {
Person.call(this, name);
this.grade = grade;
}
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;
Student.prototype.sayGrade = function() {
console.log('My grade is'+ this.grade);
};
let student1 = new Student('Bob', 10);
在这段代码中,首先在Student构造函数内部通过Person.call(this, name)调用了Person构造函数,这一步的作用是让Student实例能够继承Person构造函数中定义的属性,比如name。然后,通过Student.prototype = Object.create(Person.prototype)将Student.prototype的原型设置为Person.prototype,这样Student的实例就可以继承Person.prototype上的属性和方法,比如sayName方法。最后,重新设置Student.prototype.constructor为Student,以确保构造函数的指向正确。
通过这样的方式,student1既拥有自己特有的属性grade和方法sayGrade,又继承了Person的属性name和方法sayName,实现了对象间的属性和方法继承,充分体现了 JavaScript 基于原型链继承机制的灵活性和强大之处。
现有继承方式剖析
原型链继承
原型链继承是 JavaScript 中最基本的继承方式,它通过将子类的原型指向父类的实例,从而实现子类对父类属性和方法的继承。下面是一个简单的示例:
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function() {
console.log(this.name +'makes a sound.');
};
function Dog(name, breed) {
this.breed = breed;
this.name = name;
}
Dog.prototype = new Animal();
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function() {
console.log(this.name +'barks.');
};
let myDog = new Dog('Buddy', 'Golden Retriever');
在这个例子中,Dog.prototype = new Animal();这行代码将Dog的原型设置为Animal的一个实例,这样Dog的实例就可以通过原型链访问到Animal原型上的属性和方法,比如speak方法。
优点:
- 简单直观:实现方式简单,易于理解,通过原型链的机制,自然地实现了属性和方法的继承。
- 共享方法:父类原型上的方法可以被所有子类实例共享,节省内存空间,提高代码复用性。例如,多个Dog实例都可以调用speak方法,而不需要在每个实例中都创建该方法的副本。
缺点:
- 引用类型属性共享问题:由于子类实例共享父类原型上的属性,对于引用类型的属性,一个子类实例对其进行修改,会影响到其他子类实例。比如,如果Animal原型上有一个friends属性,是一个数组,当一个Dog实例向friends数组中添加元素时,其他Dog实例的friends数组也会发生变化。
- 无法向父类构造函数传参:在创建子类实例时,无法直接向父类构造函数传递参数,这在很多情况下会限制代码的灵活性。例如,我们无法在创建Dog实例时,直接为Animal构造函数中的name属性赋值。
借用构造函数继承
借用构造函数继承,也称为经典继承,是通过在子类构造函数中使用call或apply方法调用父类构造函数,从而实现子类对父类实例属性的继承。示例如下:
function Animal(name) {
this.name = name;
this.species = 'Animal';
}
function Dog(name, breed) {
Animal.call(this, name);
this.breed = breed;
}
let myDog = new Dog('Max', 'Poodle');
在上述代码中,Animal.call(this, name);这行代码在Dog构造函数的作用域内调用了Animal构造函数,使得Dog实例拥有了Animal构造函数中定义的属性,如name和species。
优点:
- 解决引用类型属性共享问题:每个子类实