原型链 在 JavaScript 中的用武之地

面向对象编程很重要的一个方面,就是对象的继承。A 对象通过继承 B 对象,就能直接拥有 B 对象的所有属性和方法。这对于代码的复用是非常有用的。

大部分面向对象的编程语言,都是通过"类"(class)实现对象的继承。传统上,JavaScript 语言的继承不通过 class,而是通过"原型对象"(prototype)实现。

原型链的顶端:Object.prototype

在JavaScript中,Object 是一个内置对象,它有一个原型(prototype)。Object 的原型可以被所有对象继承和共享。

Object 的原型包含许多通用的方法和属性,包括:

javascript 复制代码
Object.prototype.toString()
Object.prototype.isPrototypeOf()
Object.prototype.__proto__
Object.prototype.hasOwnProperty()
Object.prototype.valueOf()

等等。

这些方法提供了对象操作和检查的基础,所有对象可以通过原型链继承这些基础操作。这一机制是在JavaScript中实现面向对象编程的基础。

下面是一个简单的例子,演示了 Object 的原型在原型链中的作用:

javascript 复制代码
// 创建一个对象
const myObject = {};

// 调用 Object 原型的方法
console.log(myObject.toString()); // 输出: [object Object]
console.log(myObject.hasOwnProperty('someProperty')); // 输出: false

在这个例子中,myObject 对象继承了 Object 原型上的 toString 和 hasOwnProperty 方法。这些方法是通过原型链在 Object 的原型上定义的,所以所有对象都可以使用它们。

什么是原型链?它是如何工作的?

对象的原型属性(_ proto_)指向它的原型,它的原型对象也有自己的原型,这样的连续指向形成一条链,即为原型链。原型链机制使得对象可以共享和继承属性和方法。

工作原理

  1. 对象创建: 当创建一个对象时,该对象自动包含一个 _ proto_ 属性,指向其构造函数的原型对象。
javascript 复制代码
function Animal() {}
const dog = new Animal();
  1. 原型对象关联: 每个构造函数都有一个 prototype 属性,它指向一个对象,即该构造函数的原型对象。
javascript 复制代码
function Animal() {}
console.log(Animal.prototype); // 输出: Animal {}
  1. 连接构造函数和实例: 当通过构造函数创建实例时,实例的 _ proto_ 指向构造函数的 prototype
javascript 复制代码
function Animal() {}
const dog = new Animal();
console.log(dog.__proto__ === Animal.prototype); // 输出: true

在这个例子中,dog 对象的 _ proto_ 指向 Animal.prototype

  1. 原型链的延续: 如果原型对象本身也有 _ proto_ ,则会形成链式关系。
javascript 复制代码
function Animal() {}
function Mammal() {}
Mammal.prototype = new Animal();

const cat = new Mammal();
console.log(cat.__proto__.__proto__ === Animal.prototype); // 输出: true

在这个例子中,cat 的原型链延续到了 Animal.prototype

实例对象、原型链、构造函数之间的关系:

原型链是如何工作的?

  • 当访问一个对象的属性或方法时,JavaScript引擎会先查找该对象本身是否包含该属性或方法。
  • 如果对象本身没有找到,引擎会沿着对象的原型链向上查找,直到找到该属性或方法或到达原型链的顶端(Object.prototype)为止。
  • 如果一直找不到,返回 undefined

这个链式查找路径,正是 instancOf 操作符的实现原理。

大致代码实现如下:

javascript 复制代码
function myInstanceof(obj, ctor) {
  let proto = Object.getPrototypeOf(obj);
  let prototype = ctor.prototype;
  while (true) {
    if (!proto) return false;
    if (proto === prototype) return true;
    proto = Object.getPrototypeOf(proto);
  }
}

创建对象时是如何应用原型链的

在 JavaScript 中创建对象的写法:

javascript 复制代码
function Animal() {}
const dog = new Animal();

可以看出,问题"创建对象时是如何应用原型链的"其实可以转化成"new"操作符做了什么。

使用 new 命令时,它后面的函数依次执行下面的步骤。

  1. 创建一个空对象,作为将要返回的对象实例。
  2. 将这个空对象的原型,指向构造函数的 prototype 属性。
  3. 将这个空对象赋值给函数内部的 this 关键字。
  4. 开始执行构造函数内部的代码。

具体代码实现大致如下:

javascript 复制代码
function objectFactory() {
  // 将 arguments 对象转为数组
  const args = [].slice.call(arguments);
  // 取出构造函数
  const constructor = args.shift();
  // 创建一个空对象,继承构造函数的 prototype 属性
  const context = Object.create(constructor.prototype);
  // 执行构造函数
  const result = constructor.apply(context, args);
  // 如果返回结果是对象,就直接返回,否则返回 context 对象
  const flag = result != null && typeof result === "object";
  return flag ? result : context;
}

实现继承时对原型链的应用

组合继承

实现

  1. 使用原型链实现对原型属性和方法的继承。
  2. 使用构造函数实现对实例属性的继承。

下面是一个简单的组合继承的示例:

javascript 复制代码
// 父类构造函数
function Animal(name) {
  this.name = name;
}

// 父类原型方法
Animal.prototype.sayName = function() {
  console.log(`I am ${this.name}`);
};

// 子类构造函数
function Dog(name, breed) {
  // 借用构造函数继承实例属性
  Animal.call(this, name);
  this.breed = breed;
}

// 子类原型继承父类
Dog.prototype = new Animal();

// 子类自己的方法
Dog.prototype.bark = function() {
  console.log('Woof!');
};

// 创建子类实例
const myDog = new Dog('Buddy', 'Golden Retriever');

// 调用父类和子类的方法
myDog.sayName(); // 输出: I am Buddy
myDog.bark();    // 输出: Woof!

优点和缺点

优点

  1. 结合了原型链继承和借用构造函数继承的优点: 可以继承原型链上的方法,同时也能够在子类构造函数中传递参数,避免了原型链继承的共享引用问题
  2. 能够利用原型链实现方法的共享: 子类通过原型链继承父类的方法,避免了在每个实例上创建相同方法的重复消耗。

缺点

  1. 调用两次父类构造函数: 在创建子类实例时,会调用两次父类构造函数。一次是通过 Animal.call(this, name) 借用构造函数实现实例属性的继承,另一次是通过 Dog.prototype = new Animal() 继承原型属性和方法。这可能会导致一些性能上的浪费。
  2. 原型链上多余的属性: 在通过 Dog.prototype = new Animal() 继承原型属性时,实际上创建了一个父类实例,而这个实例上的属性可能是子类实例不需要的,存在一定的冗余。

尽管组合继承有一些缺点,但它仍然是JavaScript中实现继承的一种常见模式。在ES6之后,也出现了更多简化的继承方式,比如 class 关键字。

寄生组合继承

  1. 避免了组合继承中多次调用父类构造函数导致的属性重复定义问题。
  2. 具有父类构造函数的属性和方法,并且原型链上继承了父类原型的方法。
javascript 复制代码
function prototype(child, parent) {
  var prototype = Object.create(parent.prototype);
  prototype.constructor = child;
  child.prototype = prototype;
}

// 使用方式
prototype(Child, Parent);

原型链深度过深可能影响性能

因为在属性查找时需要逐级遍历整个原型链。为了解决这个问题,可以将对象扁平化,例如:

javascript 复制代码
var entryObj = {
  a: {
    b: {
      c: {
        dd: "abcdd",
      },
    },
    d: {
      xx: "adxx",
    },
    e: "ae",
  },
};

// 要求转换成如下对象
var outputObj = {
  "a.b.c.dd": "abcdd",
  "a.d.xx": "adxx",
  "a.e": "ae",
};

function flat(obj, path = '', res = {}, isArray) {
  for (let [key, value] of Object.entries(obj)) {
    if (Array.isArray(value)) {
      let _key = isArray ? `${path}[${key}]` : `${path}${key}`;
      flat(value, _key, res, true);
    } else if (typeof value === 'object') {
      let _key = isArray ? `${path}[${key}].` : `${path}${key}.`;
      flat(value, _key, res, false);
    } else {
      let _key = isArray ? `${path}[${key}]` : `${path}${key}`;
      res[_key] = value;
    }
  }
  return res;
}

console.log(flat({ a: { aa: [{ aa1: 1 }] } }));

有哪些继承模式可以替代原型链?

在现代JavaScript中,除了直接写原型链继承,还有一些其他的继承模式可以使用,这些模式可以更灵活地满足特定的需求。以下是一些常见的继承模式:

  1. 类继承(ES6+的Class语法): 使用ES6引入的Class语法,通过 class 关键字创建类,使用 extends 关键字实现继承。这种方式更直观、清晰,并且可以使用 super 调用父类方法。
javascript 复制代码
class Animal {
  constructor(name) {
    this.name = name;
  }

  sayName() {
    console.log(`I am ${this.name}`);
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name);
    this.breed = breed;
  }

  bark() {
    console.log('Woof!');
  }
}

const myDog = new Dog('Buddy', 'Golden Retriever');
myDog.sayName(); // 输出: I am Buddy
myDog.bark();    // 输出: Woof!
  1. Object.create(): 使用 Object.create() 方法创建对象,并将父对象作为参数传入,可以创建一个新对象,该对象的原型链指向父对象。
javascript 复制代码
const animal = {
  sayName() {
    console.log(`I am ${this.name}`);
  }
};

const dog = Object.create(animal);
dog.name = 'Buddy';
dog.breed = 'Golden Retriever';

dog.sayName(); // 输出: I am Buddy
  1. 工厂函数: 使用工厂函数创建对象,可以在函数内部定义私有变量和方法,并返回一个带有这些私有成员的新对象。
javascript 复制代码
function createAnimal(name) {
  const sayName = () => {
    console.log(`I am ${name}`);
  };

  return {
    sayName
  };
}

function createDog(name, breed) {
  const dog = createAnimal(name);
  dog.breed = breed;

  dog.bark = () => {
    console.log('Woof!');
  };

  return dog;
}

const myDog = createDog('Buddy', 'Golden Retriever');
myDog.sayName(); // 输出: I am Buddy
myDog.bark();    // 输出: Woof!

这些继承模式在JavaScript中提供了更灵活、清晰和易于理解的方式,可以根据项目需求选择合适的模式。

参考资料

JavaScript 教程:wangdoc.com/javascript/...

ES6 入门教程:es6.ruanyifeng.com/

JavaScript深入之继承的多种方式和优缺点:github.com/mqyqingfeng...

相关推荐
崔庆才丨静觅6 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60617 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了7 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅7 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅7 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅8 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment8 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅8 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊8 小时前
jwt介绍
前端
爱敲代码的小鱼8 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax