JavaScript原型:对象世界的"族谱"与"共享仓库"

你是否曾好奇过,为什么[].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跑起来了!'

你发现了吗?虽然car1car2是不同的实例,但它们都能访问到Car.prototype上的namerun。这就是原型的神奇之处------把公共的属性和方法放在原型上,避免每个实例都重复创建一份,既省内存又方便维护

值得注意的是,实例对象无法修改或删除原型上的属性和方法,只能"借用"或者"覆盖"(在自己身上重新定义同名属性)。

二、隐式原型:实例对象的"寻根指针"

说完了构造函数的"共享仓库",咱们再看看实例对象身上的"寻根指针"------隐式原型(proto)

每个JavaScript对象都天生带着一个__proto__属性,它指向创建这个对象的构造函数的prototype。用代码表示就是:

javascript 复制代码
car1.__proto__ === Car.prototype; // true

这就解释了为什么实例能访问到原型上的属性------当V8引擎查找对象的属性时,会先在对象自身查找,如果找不到,就会顺着__proto__去原型上找

三、new的原理:对象诞生的"五步曲"

既然说到实例和原型的关系,咱们就不得不聊聊new关键字这个"接生婆"是怎么工作的。创建一个实例其实分五步:

  1. 搭空屋子 :创建一个空对象{}
  2. 认干爹 :让构造函数的this指向这个空对象
  3. 装修屋子:执行构造函数中的代码(给空对象添加属性)
  4. 认祖归宗 :把空对象的__proto__指向构造函数的prototype
  5. 抱回家:返回这个"装修好"的对象

咱们可以手动模拟这个过程:

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()来操作隐式原型哦!

相关推荐
页面魔术几秒前
无虚拟dom怎么又流行起来了?
前端·javascript·vue.js
胡gh几秒前
如何聊懒加载,只说个懒可不行
前端·react.js·面试
Double__King4 分钟前
巧用 CSS 伪元素,让背景图自适应保持比例
前端
Mapmost5 分钟前
【BIM+GIS】BIM数据格式解析&与数字孪生适配的关键挑战
前端·vue.js·three.js
一涯6 分钟前
写一个Chrome插件
前端·chrome
鹧鸪yy13 分钟前
认识Node.js及其与 Nginx 前端项目区别
前端·nginx·node.js
跟橙姐学代码14 分钟前
学Python必须迈过的一道坎:类和对象到底是什么鬼?
前端·python
汪子熙16 分钟前
浏览器里出现 .angular/cache/19.2.6/abap_test/vite/deps 路径究竟说明了什么
前端·javascript·面试
Benzenene!17 分钟前
让Chrome信任自签名证书
前端·chrome