原型链与 class 语法糖

第二编 · 第四章:原型链与 class 语法糖的深入分析


从一个问题开始

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

Person.prototype.sayHello = function() {
  return 'Hello, I am ' + this.name;
};

const alice = new Person('Alice');
const bob = new Person('Bob');

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

为什么 alicebob 能共享 sayHello 方法?为什么 alice.__proto__ 等于 Person.prototype

答案在原型链。


原型是什么

JavaScript 的对象有一个内部属性 [[Prototype]],在浏览器环境里暴露为 __proto__。这个属性指向另一个对象,这就是原型。

当访问对象的某个属性时,如果对象本身没有这个属性,JavaScript 引擎会沿着原型链向上查找,直到找到或者到达原型链的末端(null)。

const parent = { name: 'parent' };
const child = Object.create(parent);
child.age = 10;

console.log(child.name);    // 'parent',从 parent 原型上找到
console.log(child.age);     // 10,自己的属性
console.log(child.toString()); // 从 Object.prototype 找到

Object.create(parent) 创建一个新对象,它的原型指向 parent


构造函数和原型

在 JavaScript 里,构造函数(用 new 调用的函数)有一个 prototype 属性,指向一个对象。当用 new 创建实例时,实例的 __proto__ 会指向构造函数的 prototype

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

Foo.prototype.greet = function() {
  return 'Hello, ' + this.name;
};

const foo = new Foo('foo');

// 关键关系:
foo.__proto__ === Foo.prototype // true
Foo.prototype.constructor === Foo // true

Foo.prototype 是一个普通对象,它有一个 constructor 属性指向 Foofoo 实例通过 __proto__ 指向 Foo.prototype,所以能访问 Foo.prototype.greet

这就是为什么所有实例能共享方法:方法定义在构造函数的 prototype 上,所有实例共享同一个原型对象。


原型链是什么

原型链是多个原型对象通过 __proto__ 连接形成的链式结构。

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

Animal.prototype.speak = function() {
  return '...';
};

function Dog(name) {
  Animal.call(this, name);
}

// 设置原型链:Dog.prototype.__proto__ = Animal.prototype
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

Dog.prototype.bark = function() {
  return 'Woof!';
};

const dog = new Dog('Rex');

console.log(dog.name);     // 'Rex',自己的属性
console.log(dog.bark());   // 'Woof!',Dog.prototype 上的方法
console.log(dog.speak());   // '...',Animal.prototype 上的方法
console.log(dog.toString()); // Object.prototype 上的方法

dog 的原型链是:dogDog.prototypeAnimal.prototypeObject.prototypenull

当访问 dog.speak() 时,JavaScript 引擎先在 dog 上找,没找到;然后在 dog.__proto__(也就是 Dog.prototype)上找,没找到;然后在 Dog.prototype.__proto__(也就是 Animal.prototype)上找到了。


new 操作符到底做了什么

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

const person = new Person('Tom');

new Person('Tom') 做了以下事情:

  1. 创建一个新对象:{}
  2. 设置新对象的原型:新对象.__proto__ = Person.prototype
  3. 执行构造函数:Person.call(新对象, 'Tom')
  4. 返回新对象(如果构造函数没有显式返回一个对象)

关键点是:构造函数的 this 被绑定到新创建的对象,所以 this.name = name 会给新对象添加属性。

如果构造函数显式返回了一个对象,那个对象会代替默认返回的新对象。如果返回的不是对象,new 会忽略返回值,仍然返回新对象。


ES6 class 是什么

ES6 的 class 语法是原型继承的语法糖。它没有引入新的继承模型,只是让原型继承的写法更清晰。

class Person {
  constructor(name) {
    this.name = name;
  }

  sayHello() {
    return 'Hello, I am ' + this.name;
  }

  static createAnonymous() {
    return new Person('Anonymous');
  }
}

class Student extends Person {
  constructor(name, grade) {
    super(name);
    this.grade = grade;
  }

  introduce() {
    return 'I am ' + this.name + ', grade ' + this.grade;
  }
}

const student = new Student('Alice', 'A');
console.log(student.sayHello());   // 'Hello, I am Alice'
console.log(student.introduce());  // 'I am Alice, grade A'
console.log(Student.createAnonymous()); // 静态方法的调用

class 语法的特点:

  • constructor 是构造函数
  • 方法定义在 ClassName.prototype
  • static 方法定义在类本身而不是原型上
  • extends 设置原型链:Student.prototype.__proto__ = Person.prototype
  • super() 调用父类构造函数

实际上,class 的实现和原型继承完全一致,只是语法更简洁:

// 上面的 class 等价于:
function Person(name) {
  this.name = name;
}
Person.prototype.sayHello = function() {
  return 'Hello, I am ' + this.name;
};
Person.createAnonymous = function() {
  return new Person('Anonymous');
};

原型链的末端

所有对象的原型链最终都会到达 Object.prototype,它的原型是 null

console.log(Object.prototype.__proto__); // null

Object.prototype 上有一些重要的方法,如 toStringvalueOfhasOwnProperty

hasOwnProperty 用于检查一个属性是不是对象自己的(而不是继承来的):

const obj = { own: true };
console.log(obj.hasOwnProperty('own'));           // true
console.log(obj.hasOwnProperty('toString'));     // false,继承来的
console.log(Object.prototype.hasOwnProperty.call(obj, 'toString')); // 检查继承属性的正确方式

原型链和属性查找

属性查找沿着原型链向上,时间复杂度是 O(n)。但这个 n 通常很小,不是性能问题。

属性遮蔽(Property Shadowing)发生在对象有自己的属性和原型链上同名的属性时:

const parent = { value: 'parent' };
const child = Object.create(parent);
child.value = 'child';

console.log(child.value);  // 'child',自己的属性遮蔽了原型上的
console.log(parent.value); // 'parent',不受影响

删除属性时,只删除对象自己的属性,不会影响原型上的:

delete child.value;
console.log(child.value); // 'parent',现在访问到原型上的了

这一章想说的

原型链是 JavaScript 实现继承的核心机制。每个对象都有 __proto__ 指向一个原型对象,形成链式查找结构。当访问属性时,如果对象本身没有,会沿着原型链向上找。

构造函数通过 prototype 属性定义实例能共享的方法,new 操作符把实例的原型连接到构造函数的 prototype 上。

ES6 的 class 语法是原型继承的语法糖,底层实现完全一致。classextends 设置原型链,super 调用父类构造函数,static 定义类级别的方法。