下面我将为你详细讲解JS原型链的完整攻略。
JS 原型链
JS 原型链是 JS 中实现继承的重要机制之一,它可以让我们避免代码冗余,提高代码的可维护性。在学习原型链之前,我们先了解一下 JS 中的构造函数和对象。
构造函数和对象
在 JS 中,我们可以通过构造函数来创建新的对象,其方法如下:
function Person(name) {
this.name = name;
}
const person1 = new Person('Tom');
const person2 = new Person('John');
在上面的代码中,Person
就是一个构造函数。通过 new
关键字我们可以用这个构造函数来创建对象。其实这里的 Person
就是一个对象原型(也可以称之为类),它包含着一些属性和方法,这些属性和方法可以被 person1
和 person2
继承和访问。当我们使用 new
加上构造函数时,JS 会自动帮我们完成以下操作:
- 在内存中新建一个对象;
- 将新建的对象的
__proto__
属性指向构造函数的prototype
对象; - 将
this
指向新建的对象; - 执行构造函数的方法,将属性和方法添加到新建的对象上;
- 返回新建的对象。
原型链的基本概念
在 JS 中,每个对象都有一个隐藏的属性 __proto__
,我们可以通过 Object.getPrototypeOf(object)
来获取对象的原型。原型对象也有自己的原型,如果一个对象的原型不是 Object.prototype
,那么它的原型就是其原型对象的原型。这样就形成了一个链式结构,我们称之为原型链。如果在当前对象上找不到某个属性或方法,JS 引擎会沿着原型链依次向上查找,直到找到或者到达原型链尾部,这个过程称为原型链查找。
以下是一个示例:
function Person(name) {
this.name = name;
}
Person.prototype.sayHi = function() {
console.log(`Hi, my name is ${this.name}.`);
};
const person1 = new Person('Tom');
const person2 = new Person('John');
person1.sayHi(); // Hi, my name is Tom.
person2.sayHi(); // Hi, my name is John.
console.log(person1.__proto__ === Person.prototype); // true
console.log(person2.__proto__ === Person.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
在上面的代码中,Person.prototype
就是 person1
和 person2
的原型。当我们调用 person1.sayHi()
时,JS 引擎会沿着原型链依次向上查找,首先在 person1
对象上查找 sayHi
方法,找不到的话就到其原型 Person.prototype
上查找,如果还是找不到就继续往上查找,直到找到或者到达原型链尾部。最后找到 sayHi
方法后,JS 引擎会运行该方法并传入执行上下文中的 this
对象,这里指的就是 person1
。
构造函数的原型
在 JS 中,每个构造函数都有一个原型对象,该对象可以被该构造函数创建的所有实例共享。我们可以通过在构造函数上添加属性和方法,来让创建出来的实例共享这些属性和方法。例如:
function Person(name) {
this.name = name;
}
Person.prototype.sayHi = function() {
console.log(`Hi, my name is ${this.name}.`);
};
const person1 = new Person('Tom');
const person2 = new Person('John');
Person.prototype.nickName = 'XXX'; // 在 Person 构造函数的原型对象上添加属性 nickName
console.log(person1.nickName); // XXX
console.log(person2.nickName); // XXX
在上面示例中,我们在 Person.prototype
上添加了一个属性 nickName
。这样在后面创建的 person1
和 person2
对象上就都能访问这个属性了。这个属性只需要在构造函数的原型对象上添加一次就可以,不需要每个实例对象都添加一次。这也是 JS 中实现继承的重要机制之一。
继承
在 JS 中,我们可以通过原型链来实现继承。对于一个子类(或者继承类)来说,它应该也是一个构造函数,我们可以通过 call
或者 apply
方法在子类的构造函数中调用父类的构造函数,来继承父类的属性。同时我们还需要让子类的原型对象继承父类的原型对象,以便子类实例也能继承父类的方法。以下是一个示例:
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHi = function() {
console.log(`Hi, my name is ${this.name} and I am ${this.age} years old.`);
};
function Student(name, age, grade) {
Person.call(this, name, age);
this.grade = grade;
}
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;
const tom = new Student('Tom', 18, 3);
tom.sayHi(); // Hi, my name is Tom and I am 18 years old.
在上面示例中,我们定义了两个构造函数 Person
和 Student
。Person
可以创建一个拥有 name
和 age
属性的对象,并且有一个 sayHi
方法。Student
是 Person
的子类,它继承了 Person
的属性,并新添加了一个 grade
属性。在 Student
构造函数中,我们通过 call
方法来调用 Person
的构造函数,以便在创建 Student
实例对象时,能够同时设置 name
和 age
属性。同时我们还将 Student
的原型对象设置为 Person.prototype
的一个新实例,这里我们使用了 Object.create
方法。这样就可以实现让 Student
继承 Person
的方法和属性了。
示例说明
示例1:通过原型链实现继承
在之前的继承示例中,我们通过 Student.prototype = Person.prototype
来让 Student
继承 Person
的方法。但是这种方式有一个问题,那就是当我们修改 Student.prototype
时,会同时修改 Person.prototype
。这个问题跟 JavaScript
的对象引用有关。解决这个问题的方法是让 Student.prototype
指向一个新的对象,这个新的对象的原型指向 Person.prototype
。
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHi = function() {
console.log(`Hi, my name is ${this.name} and I am ${this.age} years old.`);
};
function Student(name, age, grade) {
Person.call(this, name, age);
this.grade = grade;
}
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;
const tom = new Student('Tom', 18, 3);
tom.sayHi(); // Hi, my name is Tom and I am 18 years old.
在上面的示例中,我们使用 Object.create
方法将 Student.prototype
的原型设置为一个新创建的对象,这个新的对象的原型指向了 Person.prototype
。这样就可以实现 Student
继承 Person
的方法了,当我们修改 Student.prototype
时,不会影响到 Person.prototype
。
示例2:修改原型链
在 JS 中我们可以通过 Object.defineProperty
(也可以使用 Object.prototype.__defineGetter__
和 Object.prototype.__defineSetter__
)方法向一个对象中添加一个属性,同时还可以设置属性的性质(描述符)。其中 writable
、enumerable
和 configurable
分别表示属性是否可写、是否可枚举和是否可配置。我们还可以通过 Object.defineProperty
方法修改一个属性的值和性质,这样就可以实现对原型链的修改。
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHi = function() {
console.log(`Hi, my name is ${this.name} and I am ${this.age} years old.`);
};
const tom = new Person('Tom', 18);
const john = new Person('John', 20);
console.log(tom.__proto__ === Person.prototype); // true
console.log(john.__proto__ === Person.prototype); // true
Object.defineProperty(Person.prototype, 'gender', {
value: 'Male',
writable: false,
enumerable: true,
configurable: false
});
console.log(tom.gender); // Male
console.log(john.gender); // Male
Person.prototype.gender = 'Female'; // 无法修改,因为 writable 为 false
console.log(tom.gender); // Male
console.log(john.gender); // Male
delete Person.prototype.gender; // 无法删除,因为 configurable 为 false
console.log(tom.gender); // Male
console.log(john.gender); // Male
在上面的示例中,我们通过 Object.defineProperty
方法向 Person.prototype
中添加了一个属性 gender
,这个属性的值为 Male
,同时不允许修改和删除,因为 writable
和 configurable
都设置为了 false
。最后输出 tom.gender
和 john.gender
都为 Male
,这是因为 gender
属性属于 Person.prototype
,所以它是在原型链中被共享的。在最后尝试修改 Person.prototype.gender
的值,由于 writable
为 false
,所以修改失败,再次尝试删除 Person.prototype.gender
,由于 configurable
为 false
,删除失败。这样就保证了原型链中共享的属性不会被修改或者删除。
本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:小白谈谈对JS原型链的理解 - Python技术站