深入解析JavaScript原型链——继承的本质

6 阅读2分钟

JavaScript原型链:继承的本质

我们先整理一下,通过构造函数可以创建一个实例对象,那么实例对象如何使用构造函数里的属性和方法呢?

  • 构造函数里有:一个属性protoType,属性值是一个对象,里边放着可以共享的属性和方法
  • 实例对象里有: 一个指针__proto__,指向构造函数的protoType属性的属性值,所以通过指针就可以访问构造函数的属性和方法。但是最好不要使用这个属性,因为它不是规范中规定的。ES5 中新增了一个Object.getPrototypeOf()方法,可以通过这个方法来获取对象的原型;一个constructor属性,指回构造函数本身

当访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性,这个原型对象又会有自己的原型,于是就这样一直找下去,也就是原型链的概念。原型链的尽头一般来说都是 Object.prototype 所以这就是新建的对象为什么能够使用 toString() 等方法的原因。

一切从对象开始

在JavaScript中,几乎一切都是对象。当你创建一个数组、函数或对象字面量时,它们都和一个隐藏的链接相关联——这个链接指向另一个对象,我们称之为“原型”。

const person = {
  name: '小明',
  greet() {
    console.log(`你好,我是${this.name}`);
  }
};

console.log(person.toString()); // [object Object]

这里有个有趣的现象:person对象并没有定义toString方法,但它却可以调用。这是因为它沿着原型链找到了这个方法。

原型链的核心机制

每个JavaScript对象都有一个内部属性[[Prototype]](在浏览器中通常通过__proto__访问),它指向另一个对象。当访问一个对象的属性时,JavaScript引擎会执行以下步骤:

  1. 在对象自身的属性中查找
  2. 如果没找到,沿着[[Prototype]]链向上查找
  3. 如果找到原型链末端(null)还没找到,返回undefined
const animal = {
  eat: true
};

const rabbit = {
  jump: true,
  __proto__: animal  // 设置原型
};

console.log(rabbit.jump); // true(自身属性)
console.log(rabbit.eat);  // true(原型上的属性)
console.log(rabbit.toString); // function(原型链更深处的属性)

函数与构造函数的特殊地位

函数在JavaScript中扮演着特殊角色。每个函数都有一个prototype属性,但这个属性并不是函数的原型,而是当这个函数作为构造函数时,创建的新对象的原型。

function Dog(name) {
  this.name = name;
}

Dog.prototype.bark = function() {
  console.log(`${this.name}汪汪叫`);
};

const dog1 = new Dog('旺财');
const dog2 = new Dog('来福');

dog1.bark(); // 旺财汪汪叫
dog2.bark(); // 来福汪汪叫
console.log(dog1.bark === dog2.bark); // true(共享同一个方法)

这里的关键关系是:

  • dog1dog2的内部[[Prototype]]指向Dog.prototype
  • Dog.prototype本身也是一个对象,它的内部[[Prototype]]指向Object.prototype
  • Object.prototype的内部[[Prototype]]null,这是原型链的终点

原型的动态性

原型关系是动态的。即使对象已经创建,修改原型仍然会影响所有现有实例:

Dog.prototype.run = function() {
  console.log(`${this.name}跑起来了`);
};

dog1.run(); // 旺财跑起来了(即使dog1已经创建)

这种动态性为JavaScript带来了极大的灵活性,但也需要留意性能影响——原型链上的属性查找比自身属性稍慢。

constructor属性的奇妙作用

每个原型对象都有一个constructor属性,指回构造函数本身:

console.log(Dog.prototype.constructor === Dog); // true
console.log(dog1.constructor === Dog); // true(通过原型链找到)

这个属性可以用来创建新对象:

const dog3 = new dog1.constructor('小花');

原型链与继承的实现

利用原型链,我们可以实现继承:

function Animal(name) {
  this.name = name;
}

Animal.prototype.eat = function() {
  console.log(`${this.name}正在吃东西`);
};

function Cat(name, color) {
  Animal.call(this, name); // 调用父构造函数
  this.color = color;
}

// 建立继承关系
Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat; // 修复constructor指向

Cat.prototype.meow = function() {
  console.log(`${this.name}喵喵叫`);
};

const kitty = new Cat('小白', '白色');
kitty.eat(); // 小白正在吃东西(继承的方法)
kitty.meow(); // 小白喵喵叫(自己的方法)

原型链的查找性能与优化

虽然原型链很强大,但使用时需要注意:

  1. 属性屏蔽:如果对象自身有属性,就不会查找原型链
kitty.name = '小黑'; // 屏蔽原型链上的name
  1. 检查属性:使用hasOwnProperty区分自身属性和原型属性
console.log(kitty.hasOwnProperty('name')); // true
console.log(kitty.hasOwnProperty('eat'));  // false
  1. 性能考量:原型链越长,查找越慢。保持原型链的合理深度很重要。

现代JavaScript中的原型操作

ES6引入了更清晰的操作方式:

// 获取原型
Object.getPrototypeOf(kitty);

// 设置原型
const proto = { x: 10 };
const obj = {};
Object.setPrototypeOf(obj, proto);

// 更推荐的对象创建方式
const newObj = Object.create(proto);