你了解 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继承机制的设计思想

继承与原型链

相关推荐
阿伟来咯~26 分钟前
记录学习react的一些内容
javascript·学习·react.js
吕彬-前端31 分钟前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱34 分钟前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
guai_guai_guai43 分钟前
uniapp
前端·javascript·vue.js·uni-app
也无晴也无风雨44 分钟前
在JS中, 0 == [0] 吗
开发语言·javascript
bysking2 小时前
【前端-组件】定义行分组的表格表单实现-bysking
前端·react.js
王哲晓2 小时前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
fg_4112 小时前
无网络安装ionic和运行
前端·npm
理想不理想v2 小时前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
酷酷的阿云2 小时前
不用ECharts!从0到1徒手撸一个Vue3柱状图
前端·javascript·vue.js