原型链 在 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...

相关推荐
大前端爱好者1 小时前
React 19 新特性详解
前端
随云6321 小时前
WebGL编程指南之着色器语言GLSL ES(入门GLSL ES这篇就够了)
前端·webgl
寻找09之夏2 小时前
【Vue3实战】:用导航守卫拦截未保存的编辑,提升用户体验
前端·vue.js
多多米10053 小时前
初学Vue(2)
前端·javascript·vue.js
柏箱3 小时前
PHP基本语法总结
开发语言·前端·html·php
新缸中之脑3 小时前
Llama 3.2 安卓手机安装教程
前端·人工智能·算法
hmz8563 小时前
最新网课搜题答案查询小程序源码/题库多接口微信小程序源码+自带流量主
前端·微信小程序·小程序
看到请催我学习3 小时前
内存缓存和硬盘缓存
开发语言·前端·javascript·vue.js·缓存·ecmascript
blaizeer4 小时前
深入理解 CSS 浮动(Float):详尽指南
前端·css
编程老船长4 小时前
网页设计基础 第一讲:软件分类介绍、工具选择与课程概览
前端