先修知识:当JavaScript遇上面向对象
在ES6之前,JavaScript没有真正的class
关键字,但这并不妨碍它成为一门优秀的面向对象语言。让我们先看一个经典的构造函数示例:
function Person(name, age) {
this.name = name;
this.age = age;
function greet() {
console.log(`Hello, my name is ${this.name}`);
}
}
const person1 = new Person('Alice', 30);
const person2 = new Person('Bob', 25);
此时每个实例都会创建独立的greet
方法,损耗了性能和内存。这种设计显然不够优雅,于是我们需要原型来拯救世界。
什么是原型?
在 JavaScript 中,几乎所有的对象都有一个隐藏的内部属性 [[Prototype]]
,它指向了另一个对象,称为该对象的原型。每个对象都能访问到它的原型对象上的属性和方法。
prototype 属性
每个函数(包括构造函数)都自带一个 prototype
属性,指向一个包含实例共享方法和属性的对象。构造函数通过 prototype
来定义所有实例共享的方法。
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.greet = function() {
console.log(`Hello, my name is ${this.name}`);
};
const person1 = new Person('Alice', 30);
const person2 = new Person('Bob', 25);
person1.greet(); // Hello, my name is Alice
person2.greet(); // Hello, my name is Bob
在上面的例子中,greet
方法通过 Person.prototype
被所有 Person
的实例共享。这样,person1
和 person2
不需要分别拥有自己的 greet
方法,从而节省了内存。
什么是原型链?
原型链是 JavaScript 中对象继承的机制。当我们访问一个对象的属性或方法时,如果这个对象本身没有该属性或方法,JavaScript 会沿着原型链向上查找,直到找到该属性或者到达原型链的末端(即 Object.prototype
)。
- 对象上的属性
__proto__
就相当于原型链上的一个链接点。
原型链的结构
假设我们有以下对象结构:
person1
是Person
构造函数创建的一个实例。Person.prototype
是Person
构造函数的原型对象。Object.prototype
是所有 JavaScript 对象的最终原型。
所以,person1
的原型链是这样的:
person1 -> Person.prototype -> Object.prototype -> null
当我们访问 person1.greet()
时,JavaScript 会按照以下顺序查找:
- 首先查找
person1
是否有greet
方法,没有。 - 然后查找
Person.prototype
是否有greet
方法,找到了。 - 调用
Person.prototype.greet
方法。
__proto__
和 prototype
的区别
__proto__
是每个实例对象内部的一个属性,保存着该实例对象的构造函数的prototype
prototype
是每个函数的属性,它指向构造函数的原型对象。
console.log(person1.__proto__ === Person.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
实例化过程与原型链
当我们使用 new
运算符创建一个对象时,发生的事情是:
- 创建一个新的空对象。
- 将这个对象的
__proto__
属性指向构造函数的prototype
属性。 - 执行构造函数中的代码,将属性和方法添加到对象中。
- 返回这个对象。
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.greet = function() {
console.log(`Hello, my name is ${this.name}`);
};
const person1 = new Person('Alice', 30);
// 创建过程:
console.log(person1.__proto__ === Person.prototype); // true
console.log(person1.__proto__.__proto__ === Object.prototype); // true
在上面的例子中,person1
的 __proto__
指向了 Person.prototype
,而 Person.prototype
的 __proto__
指向了 Object.prototype
,直到最后找到 null
。
如何判断属性来自原型链?
我们可以使用 hasOwnProperty
和 in
来判断一个属性是对象自身的,还是来自原型链。
hasOwnProperty
只检查对象自身的属性,不会检查原型链上的属性。in
运算符会检查对象自身和原型链上的属性。
const person = new Person('Alice', 30);
console.log(person.hasOwnProperty('name')); // true
console.log(person.hasOwnProperty('greet')); // false
console.log('greet' in person); // true
在上面的代码中,name
是 person
自己的属性,所以 hasOwnProperty('name')
返回 true
。而 greet
是通过 Person.prototype
定义的原型方法,因此 hasOwnProperty('greet')
返回 false
,但是 greet
存在于 person
的原型链中,所以 'greet' in person
返回 true
。