Skip to content

JavaScript原型与继承

JavaScript是一种基于原型的语言,不同于传统的基于类的继承,它使用原型链来实现对象之间的继承关系。本文将深入探讨JavaScript中的原型与继承机制。

原型基础

理解原型

在JavaScript中,每个对象都有一个内部属性[[Prototype]](在大多数浏览器中可以通过__proto__访问),它指向该对象的原型。原型本身也是一个对象,因此它也有自己的原型,这样就形成了一个原型链。

javascript
const person = {
  name: "张三",
  age: 30
};

// 访问对象的原型
console.log(Object.getPrototypeOf(person)); // Object.prototype
console.log(person.__proto__); // Object.prototype(不推荐使用)

// 检查原型关系
console.log(Object.prototype.isPrototypeOf(person)); // true

原型链

当我们尝试访问一个对象的属性时,JavaScript引擎会先在对象自身查找,如果找不到,就会沿着原型链向上查找,直到找到该属性或到达原型链的末端(null)。

javascript
const animal = {
  eat: function() {
    console.log("动物在进食");
  }
};

const dog = {
  bark: function() {
    console.log("汪汪汪");
  }
};

// 设置dog的原型为animal
Object.setPrototypeOf(dog, animal);

dog.bark(); // "汪汪汪" - 在dog对象上找到
dog.eat();  // "动物在进食" - 在dog的原型(animal)上找到

// 原型链查找
console.log("toString" in dog); // true - 在Object.prototype上找到
console.log(dog.hasOwnProperty("bark")); // true - 是dog自己的属性
console.log(dog.hasOwnProperty("eat")); // false - 是从原型继承的

构造函数与原型

在JavaScript中,构造函数是用来创建对象的特殊函数。每个构造函数都有一个prototype属性,它指向一个对象,这个对象会成为通过该构造函数创建的实例的原型。

javascript
// 构造函数
function Person(name, age) {
  this.name = name;
  this.age = age;
}

// 在原型上添加方法
Person.prototype.greet = function() {
  console.log(`你好,我是${this.name},今年${this.age}岁`);
};

// 创建实例
const person1 = new Person("张三", 30);
const person2 = new Person("李四", 25);

person1.greet(); // "你好,我是张三,今年30岁"
person2.greet(); // "你好,我是李四,今年25岁"

// 验证原型关系
console.log(person1.__proto__ === Person.prototype); // true
console.log(person2.__proto__ === Person.prototype); // true
console.log(Person.prototype.constructor === Person); // true

原型属性与实例属性

原型上的属性和方法被所有实例共享,而实例属性则是每个实例独有的。

javascript
function Car(make, model) {
  this.make = make;
  this.model = model;
  this.speed = 0;
}

// 原型方法 - 所有实例共享
Car.prototype.accelerate = function(speed) {
  this.speed += speed;
  console.log(`${this.make} ${this.model}的速度增加到${this.speed}km/h`);
};

// 原型属性 - 所有实例共享
Car.prototype.manufacturer = "通用汽车";

const car1 = new Car("别克", "君威");
const car2 = new Car("雪佛兰", "科鲁兹");

car1.accelerate(50); // "别克 君威的速度增加到50km/h"
car2.accelerate(40); // "雪佛兰 科鲁兹的速度增加到40km/h"

console.log(car1.manufacturer); // "通用汽车"
console.log(car2.manufacturer); // "通用汽车"

// 修改原型属性会影响所有实例
Car.prototype.manufacturer = "上汽集团";
console.log(car1.manufacturer); // "上汽集团"
console.log(car2.manufacturer); // "上汽集团"

// 但修改实例属性不会影响其他实例
car1.color = "红色";
console.log(car1.color); // "红色"
console.log(car2.color); // undefined

继承模式

JavaScript提供了多种实现继承的方式,以下是几种常见的继承模式。

原型链继承

原型链继承是最基本的继承方式,通过将子类的原型设置为父类的实例来实现。

javascript
// 父类构造函数
function Animal(name) {
  this.name = name;
  this.colors = ["黑色", "白色"];
}

// 父类方法
Animal.prototype.eat = function() {
  console.log(`${this.name}正在进食`);
};

// 子类构造函数
function Dog(name, breed) {
  Animal.call(this, name); // 调用父类构造函数
  this.breed = breed;
}

// 设置原型链,实现继承
Dog.prototype = new Animal();
// 修复constructor指向
Dog.prototype.constructor = Dog;

// 添加子类特有方法
Dog.prototype.bark = function() {
  console.log(`${this.name}汪汪叫`);
};

// 创建实例
const dog = new Dog("小黑", "拉布拉多");
dog.eat(); // "小黑正在进食"
dog.bark(); // "小黑汪汪叫"
console.log(dog.colors); // ["黑色", "白色"]
console.log(dog.breed); // "拉布拉多"

// 检查继承关系
console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // true

原型链继承的问题:

  1. 父类的引用类型属性会被所有子类实例共享
  2. 创建子类实例时,无法向父类构造函数传参

构造函数继承

构造函数继承通过在子类构造函数中调用父类构造函数来实现属性继承。

javascript
function Animal(name) {
  this.name = name;
  this.colors = ["黑色", "白色"];
}

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

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

// 添加子类方法
Dog.prototype.bark = function() {
  console.log(`${this.name}汪汪叫`);
};

const dog1 = new Dog("小黑", "拉布拉多");
const dog2 = new Dog("小白", "金毛");

// 实例属性独立
dog1.colors.push("棕色");
console.log(dog1.colors); // ["黑色", "白色", "棕色"]
console.log(dog2.colors); // ["黑色", "白色"]

// 但无法继承父类原型上的方法
// dog1.eat(); // 错误:dog1.eat is not a function

构造函数继承的问题:

  1. 无法继承父类原型上的方法
  2. 每个子类实例都会创建父类方法的副本,导致内存浪费

组合继承

组合继承结合了原型链继承和构造函数继承的优点。

javascript
function Animal(name) {
  this.name = name;
  this.colors = ["黑色", "白色"];
}

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

function Dog(name, breed) {
  // 第一次调用父类构造函数
  Animal.call(this, name);
  this.breed = breed;
}

// 设置原型链
Dog.prototype = new Animal(); // 第二次调用父类构造函数
Dog.prototype.constructor = Dog;

// 添加子类方法
Dog.prototype.bark = function() {
  console.log(`${this.name}汪汪叫`);
};

const dog1 = new Dog("小黑", "拉布拉多");
const dog2 = new Dog("小白", "金毛");

dog1.colors.push("棕色");
console.log(dog1.colors); // ["黑色", "白色", "棕色"]
console.log(dog2.colors); // ["黑色", "白色"]

dog1.eat(); // "小黑正在进食"
dog1.bark(); // "小黑汪汪叫"

组合继承的问题:

  1. 父类构造函数被调用两次,导致子类实例和子类原型上都有父类的实例属性

寄生组合继承

寄生组合继承是最理想的继承方式,它解决了组合继承的问题。

javascript
function Animal(name) {
  this.name = name;
  this.colors = ["黑色", "白色"];
}

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

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

// 创建一个空函数作为中介
function F() {}
F.prototype = Animal.prototype;
// 让子类原型指向父类原型的一个实例
Dog.prototype = new F();
Dog.prototype.constructor = Dog;

// 或者使用Object.create简化上面的步骤
// Dog.prototype = Object.create(Animal.prototype);
// Dog.prototype.constructor = Dog;

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

const dog = new Dog("小黑", "拉布拉多");
dog.eat(); // "小黑正在进食"
dog.bark(); // "小黑汪汪叫"
console.log(dog.colors); // ["黑色", "白色"]

ES6 类继承

ES6引入了class语法,使继承变得更加简洁和易于理解,但底层仍然是基于原型的。

javascript
class Animal {
  constructor(name) {
    this.name = name;
    this.colors = ["黑色", "白色"];
  }
  
  eat() {
    console.log(`${this.name}正在进食`);
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name); // 调用父类构造函数
    this.breed = breed;
  }
  
  bark() {
    console.log(`${this.name}汪汪叫`);
  }
  
  // 重写父类方法
  eat() {
    console.log(`${this.name}在啃骨头`);
    // 调用父类方法
    super.eat();
  }
}

const dog = new Dog("小黑", "拉布拉多");
dog.eat(); // "小黑在啃骨头" 然后 "小黑正在进食"
dog.bark(); // "小黑汪汪叫"
console.log(dog.colors); // ["黑色", "白色"]
console.log(dog.breed); // "拉布拉多"

// 检查继承关系
console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // true

高级原型技术

对象创建

JavaScript提供了多种创建对象的方法,每种方法都有其特定的用途。

javascript
// 1. 对象字面量
const obj1 = {
  name: "张三",
  greet() {
    console.log(`你好,${this.name}`);
  }
};

// 2. 构造函数
function Person(name) {
  this.name = name;
}
Person.prototype.greet = function() {
  console.log(`你好,${this.name}`);
};
const obj2 = new Person("李四");

// 3. Object.create()
const proto = {
  greet() {
    console.log(`你好,${this.name}`);
  }
};
const obj3 = Object.create(proto);
obj3.name = "王五";

// 4. ES6类
class User {
  constructor(name) {
    this.name = name;
  }
  greet() {
    console.log(`你好,${this.name}`);
  }
}
const obj4 = new User("赵六");

// 调用方法
obj1.greet(); // "你好,张三"
obj2.greet(); // "你好,李四"
obj3.greet(); // "你好,王五"
obj4.greet(); // "你好,赵六"

原型污染

原型污染是一种安全风险,发生在修改内置对象原型时。

javascript
// 危险:修改Object.prototype
Object.prototype.customMethod = function() {
  console.log("这是一个自定义方法");
};

// 现在所有对象都有这个方法
const obj = {};
obj.customMethod(); // "这是一个自定义方法"

// 可能导致意外行为
for (const key in obj) {
  console.log(key); // 输出 "customMethod"
}

// 安全的替代方案
if (!Object.prototype.hasOwnProperty("customMethod")) {
  // 使用Symbol作为键名,避免命名冲突
  const customMethodSymbol = Symbol("customMethod");
  Object.defineProperty(Object.prototype, customMethodSymbol, {
    value: function() {
      console.log("这是一个安全的自定义方法");
    },
    enumerable: false // 不可枚举,不会出现在for...in循环中
  });
  
  obj[customMethodSymbol](); // "这是一个安全的自定义方法"
}

多重继承与混入

JavaScript不直接支持多重继承,但可以通过混入(Mixin)模式实现类似效果。

javascript
// 使用对象混入
const canEat = {
  eat(food) {
    console.log(`${this.name}正在吃${food}`);
  }
};

const canWalk = {
  walk() {
    console.log(`${this.name}正在走路`);
  }
};

const canSwim = {
  swim() {
    console.log(`${this.name}正在游泳`);
  }
};

// 创建一个人类
function Person(name) {
  this.name = name;
}

// 混入多个行为
Object.assign(Person.prototype, canEat, canWalk);

const person = new Person("张三");
person.eat("面包"); // "张三正在吃面包"
person.walk(); // "张三正在走路"

// 创建一个鸭子类
function Duck(name) {
  this.name = name;
}

// 混入多个行为
Object.assign(Duck.prototype, canEat, canWalk, canSwim);

const duck = new Duck("唐老鸭");
duck.eat("鱼"); // "唐老鸭正在吃鱼"
duck.walk(); // "唐老鸭正在走路"
duck.swim(); // "唐老鸭正在游泳"

使用ES6类实现混入:

javascript
// 定义混入函数
function mixinEater(Base) {
  return class extends Base {
    eat(food) {
      console.log(`${this.name}正在吃${food}`);
    }
  };
}

function mixinWalker(Base) {
  return class extends Base {
    walk() {
      console.log(`${this.name}正在走路`);
    }
  };
}

function mixinSwimmer(Base) {
  return class extends Base {
    swim() {
      console.log(`${this.name}正在游泳`);
    }
  };
}

// 基类
class Animal {
  constructor(name) {
    this.name = name;
  }
}

// 创建人类
class Person extends mixinWalker(mixinEater(Animal)) {
  constructor(name) {
    super(name);
  }
}

// 创建鸭子
class Duck extends mixinSwimmer(mixinWalker(mixinEater(Animal))) {
  constructor(name) {
    super(name);
  }
}

const person = new Person("张三");
person.eat("面包"); // "张三正在吃面包"
person.walk(); // "张三正在走路"

const duck = new Duck("唐老鸭");
duck.eat("鱼"); // "唐老鸭正在吃鱼"
duck.walk(); // "唐老鸭正在走路"
duck.swim(); // "唐老鸭正在游泳"

属性描述符

JavaScript允许我们精确控制对象属性的行为。

javascript
const person = {};

// 添加具有特定描述符的属性
Object.defineProperty(person, "name", {
  value: "张三",
  writable: true,      // 是否可修改
  enumerable: true,    // 是否可枚举
  configurable: true   // 是否可配置(删除、修改特性)
});

Object.defineProperty(person, "age", {
  value: 30,
  writable: false, // 只读属性
  enumerable: true,
  configurable: true
});

// 添加具有getter和setter的属性
let _address = "北京";
Object.defineProperty(person, "address", {
  get() {
    return _address;
  },
  set(newValue) {
    if (typeof newValue === "string") {
      _address = newValue;
    }
  },
  enumerable: true,
  configurable: true
});

console.log(person.name); // "张三"
person.name = "李四";
console.log(person.name); // "李四"

console.log(person.age); // 30
person.age = 40; // 尝试修改只读属性
console.log(person.age); // 30(在严格模式下会抛出错误)

console.log(person.address); // "北京"
person.address = "上海";
console.log(person.address); // "上海"
person.address = 123; // 非字符串值被忽略
console.log(person.address); // "上海"

原型方法与静态方法

在JavaScript中,我们可以创建原型方法(实例方法)和静态方法。

javascript
// 使用构造函数
function Person(name) {
  this.name = name;
}

// 原型方法 - 在实例上调用
Person.prototype.greet = function() {
  console.log(`你好,我是${this.name}`);
};

// 静态方法 - 在构造函数上调用
Person.create = function(name) {
  return new Person(name);
};

const person1 = new Person("张三");
person1.greet(); // "你好,我是张三"

const person2 = Person.create("李四");
person2.greet(); // "你好,我是李四"

// 使用ES6类
class User {
  constructor(name) {
    this.name = name;
  }
  
  // 原型方法
  greet() {
    console.log(`你好,我是${this.name}`);
  }
  
  // 静态方法
  static create(name) {
    return new User(name);
  }
}

const user1 = new User("王五");
user1.greet(); // "你好,我是王五"

const user2 = User.create("赵六");
user2.greet(); // "你好,我是赵六"

实际应用示例

创建可扩展的库

使用原型和继承创建可扩展的库或框架。

javascript
// 基础组件类
class Component {
  constructor(options = {}) {
    this.element = options.element || document.createElement("div");
    this.events = {};
    this.initialize(options);
  }
  
  initialize(options) {
    // 由子类实现
  }
  
  render() {
    // 由子类实现
    return this;
  }
  
  on(event, callback) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(callback);
    return this;
  }
  
  trigger(event, ...args) {
    const callbacks = this.events[event] || [];
    callbacks.forEach(callback => callback.apply(this, args));
    return this;
  }
}

// 按钮组件
class Button extends Component {
  initialize(options) {
    this.text = options.text || "按钮";
    this.element.className = "button " + (options.className || "");
    
    this.element.addEventListener("click", (e) => {
      this.trigger("click", e);
    });
  }
  
  render() {
    this.element.textContent = this.text;
    return this;
  }
  
  setText(text) {
    this.text = text;
    return this.render();
  }
}

// 使用示例
const button = new Button({
  text: "点击我",
  className: "primary"
});

button
  .on("click", function() {
    console.log("按钮被点击了");
    this.setText("已点击");
  })
  .render();

document.body.appendChild(button.element);

实现插件系统

使用原型扩展来创建插件系统。

javascript
// 主应用类
class Application {
  constructor() {
    this.plugins = {};
  }
  
  registerPlugin(name, plugin) {
    this.plugins[name] = plugin;
    // 将插件方法混入到应用实例
    if (plugin.methods) {
      Object.assign(this, plugin.methods);
    }
    // 调用插件的安装方法
    if (typeof plugin.install === "function") {
      plugin.install(this);
    }
    return this;
  }
  
  usePlugin(name, ...args) {
    const plugin = this.plugins[name];
    if (!plugin) {
      throw new Error(`插件 "${name}" 未注册`);
    }
    if (typeof plugin.execute === "function") {
      return plugin.execute.apply(this, args);
    }
    return null;
  }
}

// 创建一个日志插件
const loggerPlugin = {
  install(app) {
    console.log("日志插件已安装");
  },
  methods: {
    log(message) {
      console.log(`[LOG]: ${message}`);
    },
    error(message) {
      console.error(`[ERROR]: ${message}`);
    }
  },
  execute(level, message) {
    if (level === "error") {
      this.error(message);
    } else {
      this.log(message);
    }
  }
};

// 创建一个格式化插件
const formatterPlugin = {
  install(app) {
    console.log("格式化插件已安装");
  },
  methods: {
    formatDate(date) {
      return date.toLocaleDateString();
    },
    formatNumber(num) {
      return num.toLocaleString();
    }
  },
  execute(type, value) {
    if (type === "date" && value instanceof Date) {
      return this.formatDate(value);
    } else if (type === "number" && typeof value === "number") {
      return this.formatNumber(value);
    }
    return value;
  }
};

// 使用插件
const app = new Application();
app.registerPlugin("logger", loggerPlugin);
app.registerPlugin("formatter", formatterPlugin);

// 直接使用混入的方法
app.log("应用已启动");
app.error("发生了一个错误");

// 使用插件的execute方法
app.usePlugin("logger", "info", "这是一条信息");
const formattedDate = app.usePlugin("formatter", "date", new Date());
console.log(formattedDate); // 如:2023/5/15

// 使用混入的格式化方法
const formattedNumber = app.formatNumber(1234567.89);
console.log(formattedNumber); // 如:1,234,567.89

对象组合模式

组合模式比继承更灵活,可以在运行时动态组合对象行为。

javascript
// 创建基本对象
function createObject(state = {}) {
  return {
    ...state
  };
}

// 可以吃的行为
function canEat(state) {
  return {
    eat(food) {
      console.log(`${state.name}正在吃${food}`);
      state.energy += 10;
      return this;
    }
  };
}

// 可以睡觉的行为
function canSleep(state) {
  return {
    sleep(hours) {
      console.log(`${state.name}睡了${hours}小时`);
      state.energy += hours * 5;
      return this;
    }
  };
}

// 可以工作的行为
function canWork(state) {
  return {
    work(hours) {
      if (state.energy < hours * 3) {
        console.log(`${state.name}太累了,无法工作`);
        return this;
      }
      console.log(`${state.name}工作了${hours}小时`);
      state.energy -= hours * 3;
      return this;
    }
  };
}

// 组合对象
function createPerson(name) {
  const state = {
    name,
    energy: 100
  };
  
  return {
    ...createObject(state),
    ...canEat(state),
    ...canSleep(state),
    ...canWork(state),
    getEnergy() {
      return state.energy;
    }
  };
}

// 使用组合对象
const person = createPerson("张三");
console.log(`初始能量: ${person.getEnergy()}`); // 100

person
  .work(8)
  .eat("午餐")
  .sleep(8);

console.log(`当前能量: ${person.getEnergy()}`); // 100 - 8*3 + 10 + 8*5 = 86

总结

JavaScript的原型与继承机制是该语言最强大也是最独特的特性之一。通过理解原型链、构造函数、各种继承模式以及ES6类语法,我们可以充分利用JavaScript的面向对象特性,创建灵活、可扩展的代码。

主要要点:

  1. 原型链是JavaScript实现对象继承的基础机制
  2. 构造函数与其prototype属性一起定义了对象的结构和行为
  3. 继承可以通过多种方式实现,包括原型链继承、构造函数继承、组合继承和ES6类继承
  4. ES6类提供了更清晰的语法,但底层仍然是基于原型的
  5. 组合优于继承,在许多情况下,对象组合比类继承提供了更灵活的设计

随着JavaScript的发展,原型继承与类继承的界限变得越来越模糊,但理解原型机制仍然是掌握JavaScript的关键。无论是使用传统的原型模式还是现代的类语法,选择适合项目需求的方法才是最重要的。

用知识点燃技术的火山