你是否曾好奇过,为什么[].push()
能直接调用?为什么你能随意给String
添加自定义方法?这一切的背后,都藏着JavaScript中一个既神秘又强大的机制------原型(prototype)。今天,咱们就来扒一扒这个对象世界里的"族谱"和"共享仓库"。
一、显示原型:构造函数的"共享仓库"
首先,咱们得认识一位重要角色------显示原型(prototype)。你可以把它理解成构造函数的一个"共享仓库"。
javascript
// 定义一个汽车构造函数
function Car(color) {
this.color = color; // 每个实例都有自己的颜色
}
// 在原型上"存放"共享的属性和方法
Car.prototype.name = 'su7-Ultra';
Car.prototype.run = function() {
console.log(`${this.color}的${this.name}跑起来了!`);
};
const car1 = new Car('橙色');
const car2 = new Car('绿色');
console.log(car1.name); // 'su7-Ultra'
car2.run(); // '绿色的su7-Ultra跑起来了!'
你发现了吗?虽然car1
和car2
是不同的实例,但它们都能访问到Car.prototype
上的name
和run
。这就是原型的神奇之处------把公共的属性和方法放在原型上,避免每个实例都重复创建一份,既省内存又方便维护。
值得注意的是,实例对象无法修改或删除原型上的属性和方法,只能"借用"或者"覆盖"(在自己身上重新定义同名属性)。
二、隐式原型:实例对象的"寻根指针"
说完了构造函数的"共享仓库",咱们再看看实例对象身上的"寻根指针"------隐式原型(proto)。
每个JavaScript对象都天生带着一个__proto__
属性,它指向创建这个对象的构造函数的prototype
。用代码表示就是:
javascript
car1.__proto__ === Car.prototype; // true
这就解释了为什么实例能访问到原型上的属性------当V8引擎查找对象的属性时,会先在对象自身查找,如果找不到,就会顺着__proto__
去原型上找。
三、new的原理:对象诞生的"五步曲"
既然说到实例和原型的关系,咱们就不得不聊聊new
关键字这个"接生婆"是怎么工作的。创建一个实例其实分五步:
- 搭空屋子 :创建一个空对象
{}
- 认干爹 :让构造函数的
this
指向这个空对象 - 装修屋子:执行构造函数中的代码(给空对象添加属性)
- 认祖归宗 :把空对象的
__proto__
指向构造函数的prototype
- 抱回家:返回这个"装修好"的对象
咱们可以手动模拟这个过程:
javascript
function Person() {
this.name = '张三';
}
// 手动实现new的效果
function myNew(constructor) {
const obj = {}; // 1. 创建空对象
constructor.call(obj); // 2. 绑定this并执行构造函数
obj.__proto__ = constructor.prototype; // 4. 设置原型链
return obj; // 5. 返回对象
}
const p1 = myNew(Person);
const p2 = new Person();
// p1和p2的结构基本一样
四、原型链:对象世界的"族谱图"
了解了原型的基本概念,咱们再来看看更宏大的"原型链"。
当V8引擎查找一个对象的属性时,如果在对象自身找不到,就会顺着__proto__
去原型上找;如果原型上也找不到,就会继续顺着原型的__proto__
往上找,直到找到null
为止。这个层层查找的链路,就是原型链。
咱们通过一个经典的例子来理解:
javascript
// 爷爷构造函数
function GrandParent() {
this.name = '爷爷';
this.card = 'visa';
}
// 爸爸构造函数
function Parent() {
this.lastName = '张';
}
Parent.prototype = new GrandParent(); // 爸爸的原型指向爷爷的实例
// 儿子构造函数
function Child() {
this.name = '张三';
this.age = 18;
}
Child.prototype = new Parent(); // 儿子的原型指向爸爸的实例
const child = new Child();
console.log(child.lastName); // '张' (从爸爸那里继承)
console.log(child.card); // 'visa' (从爷爷那里继承)
console.log(child.toString()); // '[object Object]' (从Object那里继承)
在这个例子中,child
的原型链是这样的: child -> Child.prototype(Parent实例) -> Parent.prototype(GrandParent实例) -> GrandParent.prototype -> Object.prototype -> null
这就是为什么几乎所有JavaScript对象都能调用toString()
、hasOwnProperty()
等方法------因为它们都在Object.prototype
这个"老祖宗"身上!
五、没有原型的对象:"跳出三界外"
不过,JavaScript里也有一些"特立独行"的对象,它们没有原型。通过Object.create(null)
可以创建这样一个"跳出三界外"的对象:
javascript
const obj1 = {}; // 有原型,__proto__指向Object.prototype
const obj2 = Object.create(null); // 没有原型,__proto__是undefined
console.log(obj1.toString); // 有这个方法
console.log(obj2.toString); // undefined,找不到这个方法
这种对象适合用作纯粹的"哈希表",因为它不会受到原型链上属性的干扰。
写在最后
原型系统是JavaScript的核心机制之一,它既实现了对象间的属性共享,又构建了一套灵活的继承体系。理解原型,就像拿到了一把钥匙,能帮你打开JavaScript对象世界的大门。
下次再看到Array.prototype.forEach
这样的代码时,不妨想想:哦,原来这就是在给数组的"共享仓库"添加工具呢!
希望这篇文章能让你对JavaScript原型有更深刻的理解。如果你有任何疑问,欢迎在评论区留言讨论!
小贴士:虽然
__proto__
在浏览器中被广泛支持,但在实际开发中,更推荐使用Object.getPrototypeOf()
和Object.setPrototypeOf()
来操作隐式原型哦!