原型与原型链:JavaScript 的“家族关系”大揭秘

有人说JavaScript里"万物皆对象",但对象和对象之间怎么攀亲戚?今天我们就来扒一扒JS的"家族关系"------原型和原型链。看懂了它,你就理解了JS面向对象的核心,也能明白为什么一个数组能调用那么多方法。

前言

如果你第一次接触原型,可能会觉得它像个黑魔法:明明没在那个对象上定义方法,怎么就突然能用了?比如:

js 复制代码
const arr = [1, 2, 3];
arr.push(4); // 哪里来的push?

这个push方法既不是我们手动加的,也不是数组本身自带的(其实数组本身也没有,不信你console.log(arr)看看)。它是从"祖先"那里继承来的。

今天我们就来扒一扒JavaScript这个家族的族谱,看看对象们是怎么"攀亲戚"的,以及怎么利用这门亲戚关系写出优雅的代码。

一、原型是个啥?

简单来说,原型就是一个普通的对象,它被别的对象当作"备用方案"。当你访问一个对象的属性或方法时,如果这个对象自己没有,JavaScript就会去它的原型上找。如果原型上也没有,就去原型的原型上找,直到找到或者到达尽头。

这个"备用方案"的链条,就是原型链

1. 每个函数都有个prototype属性

在JavaScript里,每个函数都有一个prototype属性(箭头函数除外)。这个属性指向一个对象,当这个函数被用作构造函数(用new调用)时,创建出来的实例会继承这个prototype对象上的所有属性和方法。

js 复制代码
function Person(name) {
  this.name = name;
}

Person.prototype.sayHello = function() {
  console.log(`你好,我是${this.name}`);
};

const zhangsan = new Person('张三');
zhangsan.sayHello(); // 你好,我是张三

这里sayHello不在zhangsan自己身上,但它在Person.prototype上,zhangsan通过原型链找到了它。

2. 每个对象都有个__proto__属性

每个对象(除了null)都有一个__proto__属性(非标准,但几乎所有浏览器都实现),它指向该对象的原型(即构造函数的prototype)。

js 复制代码
console.log(zhangsan.__proto__ === Person.prototype); // true

这个__proto__就是连接实例和原型的"脐带"。

3. 构造函数也有自己的原型

构造函数本身也是对象,所以它也有__proto__。它指向Function.prototype,因为所有函数都是Function的实例。

js 复制代码
console.log(Person.__proto__ === Function.prototype); // true

二、原型链:从孙子到老祖宗

我们来看一个完整的查找链:

js 复制代码
function Animal(name) {
  this.name = name;
}
Animal.prototype.eat = function() {
  console.log(`${this.name}在吃东西`);
};

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('汪汪汪');
};

const wangcai = new Dog('旺财', '土狗');
wangcai.bark(); // 汪汪汪 (自己原型上的)
wangcai.eat();  // 旺财在吃东西 (从Animal原型继承来的)
wangcai.toString(); // [object Object] (从Object原型来的)

当调用wangcai.eat()时,查找过程是这样的:

  1. 先看wangcai自己身上有没有eat方法 → 没有
  2. wangcai.__proto__(也就是Dog.prototype)上找 → 没有
  3. Dog.prototype.__proto__(也就是Animal.prototype)上找 → 找到了eat
  4. 如果还没找到,继续往上到Animal.prototype.__proto__(也就是Object.prototype
  5. 还没找到就去Object.prototype.__proto__ → 这是null,链条结束,返回undefined

这个链条就是原型链 。它像一条家族血脉,从孙子到儿子到父亲到爷爷到祖宗,直到追溯到null

三、原型链的终点:Object.prototype

所有普通对象的原型链终点都是Object.prototypeObject.prototype本身的原型是null

js 复制代码
console.log(Object.prototype.__proto__); // null

Object.prototype上定义了一些所有对象都有的方法,比如toString()hasOwnProperty()valueOf()等。这就是为什么你的数组、函数、正则都能用这些方法。

四、如何判断属性是自己的还是继承的?

有时候我们需要知道一个属性是对象自己拥有的,还是从原型链上继承来的。这时候可以用hasOwnProperty()

js 复制代码
function Person(name) {
  this.name = name;
}
Person.prototype.age = 18;

const p = new Person('张三');
console.log(p.hasOwnProperty('name')); // true,自己的
console.log(p.hasOwnProperty('age'));  // false,继承的
console.log('age' in p);               // true,不管自己的还是继承的,只要能访问到就返回true

hasOwnProperty只检查自身属性,in操作符会检查整个原型链。

五、修改原型的影响:千万别乱动

原型是共享的,所以如果你修改了原型,所有继承自它的实例都会受影响。

js 复制代码
function Person() {}
const p1 = new Person();
const p2 = new Person();

Person.prototype.say = function() {
  console.log('hello');
};

p1.say(); // hello
p2.say(); // hello,两个实例都有了

Person.prototype.say = function() {
  console.log('world');
};

p1.say(); // world,瞬间都变了

这个特性有时候很有用(比如给内置类型添加方法),但也非常危险。尤其是在多人协作的项目里,随便修改原型可能导致难以追踪的bug。

注意 :千万不要修改内置对象的原型,比如Array.prototypeObject.prototype,除非你非常清楚自己在做什么。这会污染全局,导致不可预测的行为。

六、原型链实现继承:传统方式

在ES6的class出现之前,JS主要靠原型链实现继承。上面的Dog继承Animal就是经典写法:

js 复制代码
// 1. 定义父类构造函数
function Animal(name) {
  this.name = name;
}
Animal.prototype.eat = function() {
  console.log(`${this.name}吃东西`);
};

// 2. 定义子类构造函数
function Dog(name, breed) {
  Animal.call(this, name); // 继承属性
  this.breed = breed;
}

// 3. 继承方法
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

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

这三步是经典组合寄生继承,ES6的class语法就是它的语法糖。

七、ES6 class:原型的"糖衣"

现在写继承用class简单多了:

js 复制代码
class Animal {
  constructor(name) {
    this.name = name;
  }
  eat() {
    console.log(`${this.name}吃东西`);
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name); // 调用父类构造函数
    this.breed = breed;
  }
  bark() {
    console.log('汪汪汪');
  }
}

看着舒服多了吧?但其实它底层还是原型那一套,只是帮我们省去了手动操作prototype的麻烦。

八、常见坑点与最佳实践

1. 不要用__proto__

__proto__虽然能用,但不是标准(虽然现代浏览器都支持),而且性能也不太好。用Object.getPrototypeOf()Object.setPrototypeOf()替代。

js 复制代码
console.log(Object.getPrototypeOf(zhangsan) === Person.prototype); // true

2. 小心原型链上的属性被覆盖

如果子类实例定义了与原型同名的属性,会"遮蔽"原型上的属性。

js 复制代码
function Person() {}
Person.prototype.name = '祖先';

const p = new Person();
p.name = '自己';
console.log(p.name); // '自己',原型的被遮住了
delete p.name;
console.log(p.name); // '祖先',删除自己的,又露出来了

3. 用Object.create()创建对象

Object.create(proto)可以创建一个新对象,它的原型直接指向proto

js 复制代码
const parent = { name: '父亲' };
const child = Object.create(parent);
child.age = 10;
console.log(child.name); // '父亲',从parent继承

这是创建原型关系最简单的方式。

4. 尽量用class,少手动操作原型

现代开发中,class语法足够应对绝大多数场景,代码更清晰,不容易出错。

九、总结:原型链就是JS的"家谱"

  • 每个函数都有prototype属性(指向原型对象)
  • 每个实例都有__proto__属性(指向构造函数的prototype
  • 访问属性时,先在自身找,找不到就沿着__proto__往上找,直到null
  • 这个链条就是原型链
  • Object.prototype是链条的终点,上面定义了所有对象都有的方法
  • ES6的class是原型的语法糖,写起来更清爽

理解了原型链,你就能理解JS的继承机制,也能更高效地利用这个"家族关系"来复用代码。明天我们将在此基础上,深入讲解继承的多种实现方式,从原型链继承到ES6 class,一次性帮你理清JS继承的所有姿势。

如果你觉得这篇文章讲得清楚明白,点个赞让更多人看到。有疑问欢迎评论区留言,我们明天见!

相关推荐
谁在黄金彼岸2 小时前
Flutter应用在Windows 8上正常运行
前端
谁在黄金彼岸2 小时前
Vue项目中引入three.js并加载GLB模型流程与常见问题
前端
谁在黄金彼岸2 小时前
开发Chrome_Edge插件基本流程
前端
滴滴答答哒2 小时前
layui表格头部按钮 加入下拉选项
前端·javascript·layui
Cache技术分享2 小时前
359. Java IO API - 路径比较与处理
前端·后端
Jackson__2 小时前
OpenSpec:AI 写代码,先立规矩再动手
前端·ai编程
乌索普-2 小时前
基于vue2的简易购物车
开发语言·前端·javascript
走粥2 小时前
使用indexOf查找对象结合Pinia持久化引发的问题
开发语言·前端·javascript
北寻北爱2 小时前
前端加密解密- base64、md5、sha256、AES
前端·vue.js