先修知识:当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__是每个实例对象内部的一个属性,保存着该实例对象的构造函数的prototypeprototype是每个函数的属性,它指向构造函数的原型对象。
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。