原型链:JavaScript 世界的家族族谱

从一段代码说起

javascript 复制代码
const arr = [1, 2, 3];
arr.push(4);        // push 方法从哪来?
console.log(arr);   // [1, 2, 3, 4]

你有没有想过,一个普通的数组 arr,明明我们只给了它三个数字,它为什么会自带 pushmapfilter 这些方法?这些方法藏在哪里?答案就藏在 JavaScript 的原型链机制里。

原型是什么

简单来说,原型就是一个普通的对象,它是其他对象的"样板间"或"公共仓库"。

当我们创建一个数组时,JavaScript 会悄悄做一件事:给这个数组对象添加一个内部引用(在浏览器中叫 __proto__),指向 Array.prototype 这个原型对象。而 pushpop 这些方法,就定义在这个原型对象上。

javascript 复制代码
const arr = [1, 2, 3];
console.log(arr.__proto__ === Array.prototype);  // true
console.log(Array.prototype.push);                // function push() { ... }

所以当你调用 arr.push(4) 时,实际发生的事情是:先在 arr 自身找 push 方法 → 没找到 → 顺着 __proto__Array.prototype 里找 → 找到了 → 调用。

原型链:层层向上的查找链路

原型对象本身也是对象,它也有自己的原型。这样就形成了一条链,直到某个原型的原型是 null 为止。

javascript 复制代码
const arr = [1, 2, 3];

// 原型链:arr → Array.prototype → Object.prototype → null
console.log(arr.__proto__);              // Array.prototype
console.log(arr.__proto__.__proto__);    // Object.prototype
console.log(arr.__proto__.__proto__.__proto__);  // null

这就是原型链 的本质:一个对象通过 __proto__ 指针连接起来的链条,用于实现属性和方法的继承与共享

一张图胜过千言万语

复制代码
┌─────────────┐      ┌─────────────────┐      ┌─────────────────┐      ┌──────┐
│    arr      │      │  Array.prototype │      │ Object.prototype │      │ null │
│             │      │                 │      │                 │      │      │
│ 0: 1        │      │ push            │      │ toString        │      │      │
│ 1: 2        │      │ pop             │      │ hasOwnProperty  │      │      │
│ 2: 3        │      │ map             │      │ valueOf         │      │      │
│ length: 3   │      │ ...             │      │ ...             │      │      │
│             │      │                 │      │                 │      │      │
│ __proto__ ──┼────→ │ __proto__ ──────┼────→ │ __proto__ ──────┼────→ │      │
└─────────────┘      └─────────────────┘      └─────────────────┘      └──────┘

原型链的查找规则

当访问一个对象的属性时,JavaScript 引擎会按以下顺序查找:

  1. 先查自己:对象自身有没有这个属性?
  2. 再查原型 :没有就去 __proto__ 指向的原型对象里找
  3. 层层往上:还没有就继续往原型的原型找
  4. 最终返回 :找到就返回,找到 null 还没找到就返回 undefined
javascript 复制代码
const obj = { a: 1 };
obj.__proto__ = { b: 2 };
obj.__proto__.__proto__ = { c: 3 };

console.log(obj.a);  // 1(自身属性)
console.log(obj.b);  // 2(原型上的属性)
console.log(obj.c);  // 3(原型的原型上的属性)
console.log(obj.d);  // undefined(整条链都没有)

函数、构造函数和 prototype 属性

上面我们一直在用 __proto__,但实际开发中更常用的是函数的 prototype 属性。这两个概念容易混淆:

  • __proto__:每个对象都有的内部指针,指向它的原型
  • prototype :只有函数才有的属性,当这个函数作为构造函数(使用 new)时,prototype 会被赋值给实例对象的 __proto__
javascript 复制代码
function Person(name) {
  this.name = name;
}

Person.prototype.sayHello = function() {
  console.log(`Hello, I'm ${this.name}`);
};

const alice = new Person('Alice');

console.log(alice.__proto__ === Person.prototype);  // true
console.log(Person.prototype.__proto__ === Object.prototype);  // true

实际应用:方法共享与性能优化

原型链最大的价值在于共享 。想象一下,如果每个数组实例都拷贝一份 push 方法,内存消耗会非常恐怖。通过原型链,所有数组共享同一个 push 方法,既节省内存,又能统一更新。

javascript 复制代码
// 给所有数组添加一个自定义方法
Array.prototype.last = function() {
  return this[this.length - 1];
};

const arr1 = [1, 2, 3];
const arr2 = ['a', 'b', 'c'];

console.log(arr1.last());  // 3
console.log(arr2.last());  // 'c'
// 只需定义一次,所有数组实例都能用

原型链继承

基于原型链,JavaScript 实现了自己的继承模式:

javascript 复制代码
function Animal(name) {
  this.name = name;
}
Animal.prototype.speak = function() {
  console.log(`${this.name} makes a sound`);
};

function Dog(name, breed) {
  Animal.call(this, name);  // 继承实例属性
  this.breed = breed;
}
// 设置原型链实现继承
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

// 添加子类自己的方法
Dog.prototype.bark = function() {
  console.log('Woof!');
};

const buddy = new Dog('Buddy', 'Golden Retriever');
buddy.speak();  // Buddy makes a sound(来自 Animal)
buddy.bark();   // Woof!(来自 Dog)

几个需要知道的关键点

1. 原型链的终点是 null

Object.prototype.__proto__ === null,链条到此为止。

2. 属性屏蔽

如果自身和原型链上都有同名属性,自身属性会"屏蔽"原型链上的:

javascript 复制代码
const obj = { toString: 'hello' };
console.log(obj.toString);  // 'hello',不会去找 Object.prototype.toString

3. hasOwnProperty 的重要性

这个方法可以判断属性是对象自身的,还是从原型链上继承的:

javascript 复制代码
const arr = [1, 2, 3];
console.log(arr.hasOwnProperty('push'));   // false(push 来自原型)
console.log(arr.hasOwnProperty('length')); // true(length 是自身的)

4. 性能注意事项

属性查找沿着原型链越深,性能损耗越大。频繁跨多层原型链访问属性时,可以考虑缓存到局部变量。

现代 JavaScript 与原型链

ES6 的 class 语法本质上是原型链的语法糖:

javascript 复制代码
// 现代写法
class Animal {
  constructor(name) {
    this.name = name;
  }
  speak() {
    console.log(`${this.name} makes a sound`);
  }
}

// 编译后还是原型链那套
// Animal.prototype.speak = function() { ... }

总结

原型链是 JavaScript 最核心的特性之一,它回答了"对象的属性和方法到底从哪来"这个基本问题。理解原型链,你就能理解:

  • 为什么所有对象都能用 toString()
  • 为什么数组有一堆好用的方法?
  • JavaScript 的继承到底是怎么工作的?
  • 如何给内置类型添加自定义方法?

记住这个最简单的模型:每个对象都有个隐藏的 __proto__ 指针,指向它的原型对象,而原型对象也有自己的 __proto__,就这样连成一条链,直到 null。属性查找,就沿着这条链一路往上找。

这就是原型链,JavaScript 世界的家族族谱。


立即进入

相关推荐
清水白石00843 分钟前
Python 编程实战全景:从基础语法到插件架构、异步性能与工程最佳实践
开发语言·python·架构
Mh1 小时前
我决定写一个 3D 地球仪来记录下我要去的地方
前端·javascript·动效
. . . . .3 小时前
ref、useRef 和 forwardRef
前端·javascript·react.js
Halo_tjn3 小时前
Java 基于字符串相关知识点
java·开发语言·算法
梦想的颜色3 小时前
java 利用redis来限制用户频繁点击
java·开发语言
报错小能手3 小时前
Swift 并发 Combine响应式框架
开发语言·ios·swift
柚子8164 小时前
break跳出语句块的神奇技巧
前端·javascript
万法若空4 小时前
C++ <memory> 库全方位详解
开发语言·c++
代码中介商4 小时前
C++ 类型转换深度解析:static_cast、dynamic_cast、const_cast、reinterpret_cast
开发语言·c++
青小莫4 小时前
C++之string(OJ练习)
开发语言·c++·stl