未找到匹配的笔记

1.28 - 原型链、继承的多种实现方式

JavaScript闭包面试

JavaScript 原型链与继承深度解析

目录


核心概念

1. 三个关键对象属性

__proto__ (隐式原型)

  • 每个对象都有的内部属性
  • 指向创建该对象的构造函数的 prototype
  • 实现原型链查找的关键

prototype (显式原型)

  • 只有函数才有的属性
  • 是一个对象,包含可被实例共享的属性和方法
  • constructor 属性指回函数本身

constructor (构造器)

  • prototype 对象上的属性
  • 指向创建该原型对象的构造函数
function Person(name) {
  this.name = name;
}

const person = new Person('张三');

console.log(person.__proto__ === Person.prototype); // true
console.log(Person.prototype.constructor === Person); // true
console.log(person.constructor === Person); // true (通过原型链查找)

2. 关系图解

person 实例
  ↓ __proto__
Person.prototype
  ↓ __proto__
Object.prototype
  ↓ __proto__
null

原型链机制

1. 属性查找规则

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

  1. 对象自身是否有该属性
  2. 对象的 __proto__ (即构造函数的 prototype)
  3. __proto____proto__ (继续向上查找)
  4. 直到 Object.prototype
  5. 最后是 null,返回 undefined
function Animal(name) {
  this.name = name;
}

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

const dog = new Animal('旺财');

// 属性查找过程
console.log(dog.name);        // 自身属性: "旺财"
console.log(dog.eat);         // 原型属性: function
console.log(dog.toString);    // Object.prototype 上的方法
console.log(dog.notExist);    // undefined (查找到 null)

2. 原型链的本质

原型链实际上是通过 __proto__ 连接起来的一条查找链:

function Parent() {
  this.parentProp = 'parent';
}

Parent.prototype.getParent = function() {
  return this.parentProp;
};

function Child() {
  this.childProp = 'child';
}

// 建立原型链
Child.prototype = new Parent();
Child.prototype.constructor = Child;

const child = new Child();

// 原型链结构
console.log(child.__proto__ === Child.prototype);                    // true
console.log(child.__proto__.__proto__ === Parent.prototype);         // true
console.log(child.__proto__.__proto__.__proto__ === Object.prototype); // true
console.log(child.__proto__.__proto__.__proto__.__proto__);          // null

3. instanceof 原理

instanceof 通过原型链判断对象是否是某个构造函数的实例:

function myInstanceof(obj, constructor) {
  let proto = obj.__proto__;
  const prototype = constructor.prototype;
  
  while (proto) {
    if (proto === prototype) return true;
    proto = proto.__proto__;
  }
  
  return false;
}

console.log(myInstanceof(child, Child));   // true
console.log(myInstanceof(child, Parent));  // true
console.log(myInstanceof(child, Object));  // true

继承的多种实现方式

1. 原型链继承

实现原理:将子类的原型指向父类的实例

function Parent() {
  this.name = 'parent';
  this.hobbies = ['reading', 'coding'];
}

Parent.prototype.getName = function() {
  return this.name;
};

function Child() {
  this.age = 18;
}

// 继承
Child.prototype = new Parent();
Child.prototype.constructor = Child;

const child1 = new Child();
const child2 = new Child();

console.log(child1.getName()); // "parent"
child1.hobbies.push('gaming');
console.log(child2.hobbies);   // ["reading", "coding", "gaming"]

优点

  • 简单易实现
  • 可以访问父类原型上的方法

缺点

  • 引用类型属性被所有实例共享
  • 无法向父类构造函数传参
  • 不能实现多继承

2. 构造函数继承(经典继承)

实现原理:在子类构造函数中调用父类构造函数

function Parent(name) {
  this.name = name;
  this.hobbies = ['reading', 'coding'];
}

Parent.prototype.getName = function() {
  return this.name;
};

function Child(name, age) {
  // 继承父类属性
  Parent.call(this, name);
  this.age = age;
}

const child1 = new Child('小明', 18);
const child2 = new Child('小红', 20);

child1.hobbies.push('gaming');
console.log(child1.hobbies); // ["reading", "coding", "gaming"]
console.log(child2.hobbies); // ["reading", "coding"]
console.log(child1.getName); // undefined (无法继承原型方法)

优点

  • 解决了引用类型共享问题
  • 可以向父类传参
  • 可以实现多继承(多次 call)

缺点

  • 无法继承父类原型上的方法
  • 每次创建实例都会创建一遍方法,内存浪费

3. 组合继承(原型链 + 构造函数)

实现原理:结合原型链继承和构造函数继承

function Parent(name) {
  this.name = name;
  this.hobbies = ['reading', 'coding'];
}

Parent.prototype.getName = function() {
  return this.name;
};

function Child(name, age) {
  // 第二次调用 Parent
  Parent.call(this, name);  // 继承属性
  this.age = age;
}

// 第一次调用 Parent
Child.prototype = new Parent();  // 继承方法
Child.prototype.constructor = Child;

const child1 = new Child('小明', 18);
const child2 = new Child('小红', 20);

child1.hobbies.push('gaming');
console.log(child1.hobbies);     // ["reading", "coding", "gaming"]
console.log(child2.hobbies);     // ["reading", "coding"]
console.log(child1.getName());   // "小明"

优点

  • 结合了两种继承的优点
  • 可以继承原型方法
  • 引用类型不共享
  • 可以传参

缺点

  • 调用了两次父类构造函数,浪费性能
  • 子类原型上会有一份多余的父类实例属性

4. 原型式继承

实现原理:基于已有对象创建新对象(Object.create 的原理)

function object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}

const parent = {
  name: 'parent',
  hobbies: ['reading', 'coding'],
  getName: function() {
    return this.name;
  }
};

const child1 = object(parent);
const child2 = object(parent);

child1.name = '小明';
child1.hobbies.push('gaming');

console.log(child1.name);        // "小明"
console.log(child2.name);        // "parent"
console.log(child2.hobbies);     // ["reading", "coding", "gaming"]

// ES5 原生方法
const child3 = Object.create(parent);
console.log(child3.getName());   // "parent"

优点

  • 不需要构造函数,可以快速创建对象

缺点

  • 引用类型属性被所有实例共享
  • 无法传参

5. 寄生式继承

实现原理:在原型式继承基础上,增强对象

function createChild(original) {
  const clone = Object.create(original);
  
  // 增强对象
  clone.sayHello = function() {
    console.log('Hello!');
  };
  
  return clone;
}

const parent = {
  name: 'parent',
  hobbies: ['reading']
};

const child = createChild(parent);
child.sayHello();  // "Hello!"

优点

  • 可以在创建时增强对象

缺点

  • 方法无法复用,每次创建都会创建一遍方法
  • 引用类型共享问题依然存在

6. 寄生组合式继承(最优方案)

实现原理:通过寄生方式继承父类原型,避免调用两次父类构造函数

function inheritPrototype(child, parent) {
  // 创建父类原型的副本
  const prototype = Object.create(parent.prototype);
  // 修正 constructor
  prototype.constructor = child;
  // 设置子类原型
  child.prototype = prototype;
}

function Parent(name) {
  this.name = name;
  this.hobbies = ['reading', 'coding'];
}

Parent.prototype.getName = function() {
  return this.name;
};

function Child(name, age) {
  Parent.call(this, name);  // 只调用一次
  this.age = age;
}

// 继承原型
inheritPrototype(Child, Parent);

Child.prototype.getAge = function() {
  return this.age;
};

const child1 = new Child('小明', 18);
console.log(child1.getName());  // "小明"
console.log(child1.getAge());   // 18

为什么最优?

  1. 只调用一次父类构造函数
  2. 原型链保持不变
  3. 能够正常使用 instanceofisPrototypeOf()
  4. 避免在子类原型上创建多余属性

7. ES6 Class 继承

实现原理:语法糖,底层依然基于原型链

class Parent {
  constructor(name) {
    this.name = name;
    this.hobbies = ['reading', 'coding'];
  }
  
  getName() {
    return this.name;
  }
  
  static staticMethod() {
    return 'static method';
  }
}

class Child extends Parent {
  constructor(name, age) {
    super(name);  // 必须调用 super
    this.age = age;
  }
  
  getAge() {
    return this.age;
  }
  
  // 重写父类方法
  getName() {
    return `Child: ${super.getName()}`;
  }
}

const child = new Child('小明', 18);
console.log(child.getName());           // "Child: 小明"
console.log(Child.staticMethod());      // "static method"
console.log(child instanceof Parent);   // true

Class 继承的特点

  1. super 关键字调用父类构造函数
  2. 子类必须在 constructor 中调用 super,否则无法使用 this
  3. 静态方法也会被继承
  4. 内部方法不可枚举

与寄生组合继承的对比

// ES6 Class 本质
class Child extends Parent {}

// 等价于
function Child() {
  Parent.call(this);
}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;

// 额外:静态属性继承
Object.setPrototypeOf(Child, Parent);

实战应用与面试要点

1. 手写 new 操作符

function myNew(constructor, ...args) {
  // 1. 创建一个新对象,原型指向构造函数的 prototype
  const obj = Object.create(constructor.prototype);
  
  // 2. 执行构造函数,绑定 this
  const result = constructor.apply(obj, args);
  
  // 3. 如果构造函数返回对象,则返回该对象,否则返回新对象
  return result instanceof Object ? result : obj;
}

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

const person = myNew(Person, '张三');
console.log(person.name);  // "张三"

2. 手写 Object.create

function myCreate(proto) {
  function F() {}
  F.prototype = proto;
  return new F();
}

const parent = { name: 'parent' };
const child = myCreate(parent);
console.log(child.__proto__ === parent);  // true

3. 判断对象属性来源

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

Person.prototype.age = 18;

const person = new Person('张三');

// 判断自有属性
console.log(person.hasOwnProperty('name'));  // true
console.log(person.hasOwnProperty('age'));   // false

// 判断属性是否在对象上(包括原型链)
console.log('name' in person);  // true
console.log('age' in person);   // true

// 判断属性是否在原型上
function hasPrototypeProperty(obj, name) {
  return !obj.hasOwnProperty(name) && (name in obj);
}

console.log(hasPrototypeProperty(person, 'age'));  // true

4. 完整的继承实现(寄生组合式)

// 工具函数
function extend(Child, Parent) {
  const F = function() {};
  F.prototype = Parent.prototype;
  Child.prototype = new F();
  Child.prototype.constructor = Child;
  Child.super = Parent.prototype;  // 保存父类引用
}

// 父类
function Animal(name) {
  this.name = name;
  this.sleep = function() {
    console.log(`${this.name} 正在睡觉`);
  };
}

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

// 子类
function Dog(name, color) {
  Animal.call(this, name);
  this.color = color;
}

// 继承
extend(Dog, Animal);

Dog.prototype.bark = function() {
  console.log('汪汪汪!');
};

const dog = new Dog('旺财', '黑色');
dog.eat('骨头');   // "旺财 正在吃 骨头"
dog.bark();       // "汪汪汪!"
dog.sleep();      // "旺财 正在睡觉"

5. 面试高频问题

Q1: prototype__proto__ 的区别?

  • prototype 是函数的属性,指向一个对象,用于存放共享属性和方法
  • __proto__ 是对象的属性,指向创建该对象的构造函数的 prototype
  • 所有函数都有 prototype,所有对象都有 __proto__

Q2: 原型链的终点是什么?

Object.prototype.__proto__ === null  // true

Q3: 如何获取对象的原型?

// 方法一:ES6 标准方法
Object.getPrototypeOf(obj)

// 方法二:非标准但广泛支持
obj.__proto__

// 方法三:通过 constructor
obj.constructor.prototype

Q4: 为什么要修正 constructor?

function Parent() {}
function Child() {}

Child.prototype = new Parent();

// 不修正的话
console.log(new Child().constructor === Parent);  // true (错误!)

// 修正后
Child.prototype.constructor = Child;
console.log(new Child().constructor === Child);   // true (正确)

Q5: Class 继承与寄生组合继承的区别?

  1. Class 必须使用 new 调用
  2. Class 内部方法不可枚举
  3. Class 有暂时性死区,不存在变量提升
  4. Class 支持静态方法继承
// Class 不能直接调用
class Parent {}
Parent();  // TypeError

// 函数可以
function Parent() {}
Parent();  // 正常执行

6. 性能优化建议

// ❌ 不好的做法:每次创建实例都创建方法
function Person(name) {
  this.name = name;
  this.sayName = function() {
    console.log(this.name);
  };
}

// ✅ 好的做法:方法放在原型上共享
function Person(name) {
  this.name = name;
}

Person.prototype.sayName = function() {
  console.log(this.name);
};

7. 实战场景:插件系统

// 基础插件类
class Plugin {
  constructor(options = {}) {
    this.options = options;
  }
  
  init() {
    throw new Error('子类必须实现 init 方法');
  }
  
  destroy() {
    console.log('插件已销毁');
  }
}

// 具体插件
class TooltipPlugin extends Plugin {
  init() {
    console.log('Tooltip 插件初始化', this.options);
  }
  
  show() {
    console.log('显示提示');
  }
}

const tooltip = new TooltipPlugin({ position: 'top' });
tooltip.init();    // "Tooltip 插件初始化 {position: 'top'}"
tooltip.show();    // "显示提示"
tooltip.destroy(); // "插件已销毁"

总结

继承方式对比表

继承方式优点缺点使用场景
原型链继承简单引用类型共享、无法传参不推荐
构造函数继承解决引用共享、可传参无法继承原型方法不推荐
组合继承功能完整调用两次父类构造函数可用但不最优
原型式继承简单快速引用类型共享简单对象复制
寄生式继承可增强对象方法无法复用特殊场景
寄生组合式完美实现相对复杂推荐
ES6 Class语法简洁、功能强大需要转译支持旧浏览器强烈推荐

核心要点

  1. 原型链是 JavaScript 继承的基础
  2. __proto__ 连接起整条原型链
  3. 寄生组合式继承是 ES5 最优方案
  4. ES6 Class 是现代开发首选
  5. 理解原理比记忆语法更重要

学习建议

  1. 手写实现各种继承方式
  2. 理解每种方式的适用场景
  3. 在实际项目中灵活运用
  4. 关注性能和可维护性
  5. 深入理解原型链查找机制