你了解 javascript 原型链继承的设计思想吗?

背景

1994年,网景公司(Netscape)发布了历史上第一个比较成熟的网络浏览器,但它只能浏览,不具备与用户互动的能力,浏览器无法对用户填写的内容进行校验,只能发送到服务器端进行判断,这不仅浪费服务器资源且耗时较长对用户体验不佳。设计者Brendan Eich创建了JavaScript这门语言,该语言设计的初衷是解决类似表单验证这种简单的操作。而关于"继承"的实现,他选择了基于原型的继承模型

new运算符

C++和Java使用new命令时,都会调用"类"的构造函数(constructor)。在Javascript语言中设计者做了简化的设计,new命令后面跟的不是类,而是构造函数。

js 复制代码
function Animal(name) {
  this.name = name;
  // 定义一个公共属性
  this.category = '动物类';
}

// 生成两个实例对象
const dog = new Animal('dog');
const cat = new Animal('cat');

dog.category = '犬类';
console.log(dog.category); // 犬类
console.log(cat.category); // 动物类

用构造函数生成实例对象,无法共享属性和方法。两个实例对象中的category属性是相互独立的,修改其中一个,另一个并不受影响。因此,为节省资源、实现数据共享,引入了prototype属性。

prototype

每个JavaScript对象(null除外)在创建时就会与之关联另一个对象,这个关联的对象就被称为该对象的原型。原型本身也是一个对象,它包含可以被原对象共享的属性和方法。

所有实例对象需要共享的属性和方法,都放在 prototype 对象中;而不需要共享的属性和方法,则放在构造函数中。

上面的例子, 使用 prototype 改写:

js 复制代码
function Animal(name) {
  this.name = name;
}

// 将公共属性定义在prototype对象中
Animal.prototype.category = '动物类';

const dog = new Animal('dog');
const cat = new Animal('cat');

// 更改公共属性
Animal.prototype.category='animal';

console.log(dog.category); // animal
console.log(cat.category); // animal

原型链

原型链是JavaScript中实现继承属性查找的一种机制。

构造函数、原型和实例之间的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,实例都包含一个原型对象的内部指针。

那么,假设我们让原型对象等于另一个构造函数的实例,结果会怎样?显然,此时原型对象中将包含一个指向另一个原型的指针,另一个原型对象中也包含着一个指向另一个原型对象的指针,直至终点null,而构成实例与原型之间关系的这条链,就是所谓的 原型链

js 复制代码
const a = { a: 1 };
// a ---> Object.prototype ---> null

const b = Object.create(a);
// b ---> a ---> Object.prototype ---> null

const c = Object.create(b);
// c ---> b ---> a ---> Object.prototype ---> null

const d = Object.create(null);
// d ---> null(d 是一个直接以 null 为原型的对象)

原型搜索机制:当你尝试访问一个对象的属性或方法时,如果该对象本身没有这个属性或方法,JavaScript解释器就会去该对象的原型对象中查找。如果原型对象也没有这个属性或方法,解释器会继续沿着这个原型链向上查找,直到找到这个属性/方法,或者到达原型链的顶端 null

1、默认的原型

原型最常见的用途是实现继承。当你创建一个函数对象时,JavaScript解释器会为这个函数添加一个prototype属性,指向一个原型对象。然后,当你通过这个构造函数来创建新对象时,这个新对象的内部原型(通常通过[[Prototype]]属性访问)将会指向构造函数的 prototype 属性引用的那个对象。

js 复制代码
var obj = {};
obj.toString(); // [object Object]

我们使用字面量的方式创建一个新的对象,为什么可以直接调用toString()、valueOf()等默认方法呢?

实际上,obj通过原型链的方式继承了object.prototype, 当调用 obj.toString()的时候其实是调用了保存在object.prototype中的方法。

默认原型都会包含一个内部指针, 指向 object.prototype。这就是所有自定义类型都会继承 toString()、valueOf()等默认方法的根本原因

2、原型和实例的关系

我们可以通过以下两种方法确定原型与实例之间的关系

  • instanceof
js 复制代码
dog instanceof Object; // true
dog instanceof Animal; // true

我们可以说: dog 是 object 的实例, 也可以说 dog 是Animal 的实例。

  • isPrototypeof()

只要是原型链中出现过的原型,都可以说是该原型链所派生的实例的原型

js 复制代码
Object.prototype.isPrototypeOf(dog);  //true
Animal.prototype.isPrototypeOf(dog);  //true

3、给原型添加方法的顺序很重要!

  • 给原型添加方法的代码一定要放在替换原型的语句之后

当你替换一个构造函数的原型对象时(例如,通过直接赋值一个新对象给构造函数的 prototype 属性),这个操作会切断原有原型对象与构造函数之间的链接,同时建立一个新的原型对象与构造函数之间的链接。如果在这个操作之前,你已经给原有的原型对象添加了一些方法,这些方法不会自动转移到新的原型对象上。所以这些方法对于通过新原型创建的实例来说是不可用的。

举个例子:

js 复制代码
function Parent() {
  this.parentValue = 1;
}

Parent.prototype.getParentValue = function() {
  return this.parentValue;
}

function Child() {
  this.childValue = 2;
}

// 给原型添加方法
Child.prototype.getChildValue = function() {
  return this.childValue;
}
Child.prototype.getParentValue = function() {
  return 3;
}

// 替换原型
Child.prototype = new Parent();
Child.prototype.constructor = Child;

const instance = new Child();
console.log(instance.getParentValue()); // 1
console.log(instance.getChildValue()); // instance.getChildValue is not a function

上面例子,先添加方法后替换原型,导致我们在 Child 原型对象中添加的方法 getChildValue、getParentValue都不可用。遵循原型搜索机制,找到在Parent的原型对象中定义的getParentValue,返回结果1;而在原型链中并不存在getChildValue的定义,故报错。

js 复制代码
function Parent() {
  this.parentValue = 1;
}

Parent.prototype.getParentValue = function() {
  return this.parentValue;
}

function Child() {
  this.childValue = 2;
}

Child.prototype = new Parent();
Child.prototype.constructor = Child;

// 添加新方法
Child.prototype.getChildValue = function() {
  return this.childValue;
}
// 重写方法
Child.prototype.getParentValue = function() {
  return 3;
}
const instance = new Child();
console.log(instance.getChildValue()); // 2
console.log(instance.getParentValue()); // 3

在这个例子中我们先替换了原型再添加方法,重写了getParentValue方法,Child.prototype中定义的方法可用并返回了结果。Parent.prototype中定义的getParentValue会被遮蔽。

  • 通过原型链实现继承时, 不能使用对象字面量创建原型方法,因为这样做会重写原型链
js 复制代码
function Parent() {
  this.parentValue = 1;
}

Parent.prototype.getParentValue = function() {
  return this.parentValue;
}

function Child() {
  this.childValue = 2;
}

Child.prototype = new Parent();
Child.prototype.constructor = Child;

// 使用字面量的方式添加新方法,会重写原型链,导致上面的继承无效
Child.prototype = {
  getChildValue: function() {
    return this.childValue;
  }
}

const instance = new Child();
console.log(instance.getParentValue()); // instance.getParentValue is not a function

4、原型链继承存在的问题

js 复制代码
function Parent() {
  this.colors = ['red', 'green', 'blue'];
}

function Child() {}

Child.prototype = new Parent();
Child.prototype.constructor = Child;

const instance1 = new Child();
instance1.colors.push('orange');
console.log(instance1.colors); // ['red', 'green', 'blue', 'orange']

const instance2 = new Child();
console.log(instance2.colors); // ['red', 'green', 'blue', 'orange']
  • 问题1:引用类型会被共享
  • 问题2:没办法在不影响所有对象实例的情况下,给父类构造函数传递参数

性能

原型链上较深层的属性的查找时间可能会对性能产生负面影响;尝试访问不存在的属性始终会遍历整个原型链。某些情况下,我们可能只需要遍历实例自身的属性即可,此时,我们可以使用 hasOwnPropertyObject.hasOwn 方法进行判断。

参考文章

Javascript继承机制的设计思想

继承与原型链

相关推荐
weixin_382395238 小时前
为小工厂量身打造:本地部署的物料管理系统带缺料计算
前端·制造
__zRainy__8 小时前
解决pnpm v10+不自动构建
前端·pnpm·工程化
猫猫不是喵喵.8 小时前
Vue3 Props 属性
前端·javascript·vue.js
醉城夜风~9 小时前
CSS元素显示模式(display)
前端·css
AI大模型-小华10 小时前
Codex 三方充值快速入门指南
java·前端·数据库·chatgpt·ai编程·codex·chatgpt pro
做前端的娜娜子12 小时前
同一链接实现 PC Web 与移动 H5 自适应
前端·掘金·金石计划
小帅不太帅12 小时前
架构没变、规模没变,DeepSeek V4 Flash 正式版凭什么暴涨 47 分?
前端·aigc·deepseek
jarvisuni13 小时前
DeepSeekFlash前端依旧拉垮,而且变慢了很多!
前端·javascript·算法
卷福同学14 小时前
AI编程出海第二步:验证关键词能否做站
前端·人工智能·后端