从一段代码说起
javascript
const arr = [1, 2, 3];
arr.push(4); // push 方法从哪来?
console.log(arr); // [1, 2, 3, 4]
你有没有想过,一个普通的数组 arr,明明我们只给了它三个数字,它为什么会自带 push、map、filter 这些方法?这些方法藏在哪里?答案就藏在 JavaScript 的原型链机制里。
原型是什么
简单来说,原型就是一个普通的对象,它是其他对象的"样板间"或"公共仓库"。
当我们创建一个数组时,JavaScript 会悄悄做一件事:给这个数组对象添加一个内部引用(在浏览器中叫 __proto__),指向 Array.prototype 这个原型对象。而 push、pop 这些方法,就定义在这个原型对象上。
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 引擎会按以下顺序查找:
- 先查自己:对象自身有没有这个属性?
- 再查原型 :没有就去
__proto__指向的原型对象里找 - 层层往上:还没有就继续往原型的原型找
- 最终返回 :找到就返回,找到
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 世界的家族族谱。
