原型链继承实例共享数据修改问题的本质

最近在B站看了几个 js 继承的视频,一般第一个讲的都是原型链继承,基本就没有说对的。

原型链继承

话不多说,先上代码:

js 复制代码
function Parent() {
    this.a = 123;
}

Parent.prototype.b = function() {
    console.log('方法 b');
}

function Child() {

}

Child.prototype = new Parent();

由以上代码可知,通过将子构造函数的原型 指向父构造函数的实例对象,实现了原型链继承。

通过这种方式,Child的实例对象们,可以通过原型链访问到父类的所有属性和方法。

原型链继承存在的问题:

当子构造函数只有一个实例对象时,这种方法是没有什么问题的。

当有多个实例对象时,会存在某一个实例修改数据,其他实例的数据也会跟着改变的问题。

但是,这个问题只存在于,对引用数据类型中的数据的修改,原始数据类型的修改和引用的修改都不会出现这个问题。

什么意思?让我们来看下代码:

1、当实例修改原始数据类型时

js 复制代码
const c1 = new Child();
const c2 = new Child();
c1.a = '456';
console.log(c2.a);

// 控制台打印
// 123

可见c1修改a并没有影响c2

2、当实例修改引用数据类型的引用

js 复制代码
// 我们为 Parent 添加一个 引用数据类型 arr
function Parent() {
    this.a = '123';
    this.arr = [1, 2, 3];
}

const c1 = new Child();
const c2 = new Child();
c1.arr = ['a', 'b', 'c'];
console.log(c2.arr);

//控制台打印
// [1, 2, 3]

可见c1修改arr的引用,并没有影响到c2

3、当实例修改引用数据类型中的数据时

js 复制代码
// 我们为 Parent 添加一个 引用数据类型 arr
function Parent() {
    this.a = '123';
    this.arr = [1, 2, 3];
}

const c1 = new Child();
const c2 = new Child();
c1.arr.push(4);
console.log(c2.arr);

//控制台打印
// [1, 2, 3, 4]

哎,这就有问题了,c1修改引用数据类型中的数据,影响了c2

原型链继承问题的本质:

变量的查找方式:

我们都知道,JS 在执行过程中,是到作用域(词法环境) 中去查找变量。

查找的方式有两种:

  • LHS
  • RHS

因为LHSRHS是相对于赋值号=左右两侧来说的,所以我们可以简单理解为:

  • LHS:当遇到赋值操作时,执行LHS查询
  • RHS:当遇到读取值操作时,执行RHS查询

举例:

js 复制代码
let a = 1;  // 对 a 赋值 1,这是一个 LHS

let a = b; // 先 RHS 获取 b 的值,再 LHS 赋值给 a

function fn(x) {
  ...
}
fn(1);   // 当实参传递给形参,也是 LHS

console.log(c); // 首先 RHS 查找对象console,然后 RHS 查找函数 log,然后 RHS 查找变量 c,最后输出 

通过以上几个简单的例子,我们应该清楚了什么是 LHS、RHS

PS:LHS 查询实际上是执行一个 [[PUT]]隐式函数,RHS 查询是执行 [[GET]]隐式函数

查找流程:

对于变量的查找,就是沿着作用域链查找,跟以下讲的原型链查找是不一样的,这里我们不多赘述。

我们着重讲对于对象属性的查找(原型链查找)。

RHS 的查找流程

以上述代码中的片段为例:

js 复制代码
c1.a
  • 首先,在 c1 本身查找,如果找到就ok了,如果没有,沿着原型链找
  • c1 原型链指向 Child 的原型(c1.__proto__ ==> Child.prototype
  • 如果 Child.prototype 有就ok,没有继续沿着原型链找
  • 因为Child.prototype = new Parent() ,所以在这里我们找到了 a,值为 '123'
  • 如果直到最后的 Object.prototype = null,还没有找到,则返回 undefined

以上 RHS 查询是比较简单的,我们就直接写出来了

LHS查询是比较麻烦一点的,下面我们用一张来演示

LHS 的查找流程

以上述代码中的片段为例:

js 复制代码
c1.a = '456';

上图为 LHS 查询对象属性的流程

根据上图,我们再来看原型链继承中的 3 种实例修改数据的情况:

1、当实例修改原始数据类型时

js 复制代码
const c1 = new Child();
const c2 = new Child();
c1.a = '456';
console.log(c2.a);

// 控制台打印
// 123

LHS,c1 对象本身是没有 a 属性的,因此我们沿着原型链找,最终找到了 a,又因为 a 是一个原始数据类型 (因为 this.a = '123'),因此,我们不使用原型链找到的这个 a,而是在我 c1 对象本身创建一个属性 a,然后赋值为 '456'。

不信我们看控制台输出(c1对象):

原本,c1 对象本身是没有任何属性,但是执行了 c1.a = '456'后,由于 LHS 查询机制,在 c1 对象本身创建了 a = '456'

2、当实例修改引用数据类型的引用

js 复制代码
// 我们为 Parent 添加一个 引用数据类型 arr
function Parent() {
    this.a = '123';
    this.arr = [1, 2, 3];
}

const c1 = new Child();
const c2 = new Child();
c1.arr = ['a', 'b', 'c'];
console.log(c2.arr);

//控制台打印
// [1, 2, 3]

LHS,c1 没有 arr,沿着原型链找,最终找到了 arr,因为 arr 是一个引用数据类型 ,又因为我们是直接修改引用本身 ,因此,我们覆盖

不信看控制台输出(c1对象):

3、当实例修改引用数据类型中的数据时

js 复制代码
// 我们为 Parent 添加一个 引用数据类型 arr
function Parent() {
    this.a = '123';
    this.arr = [1, 2, 3];
}

const c1 = new Child();
const c2 = new Child();
c1.arr.push(4);
console.log(c2.arr);

//控制台打印,    注意:c2 被影响
// [1, 2, 3, 4]

LHS,同以上流程,找到了 arr,因为 arr 是一个引用数据类型,且是对引用数据类型中数据的修改,那我们就直接修改原型链上找到的这个 arr

也就是说,c1 修改了父类原型中的数据,导致 c2 被影响

以上,就是原型链继承,实例共享数据修改问题的本质原因。

仅仅一个原型链继承就写了这么多,还有好多知识没有扩展出来仔细讲解。

第一次写文章,必然有很多很多问题和不足,希望掘友多多指教,感谢

相关推荐
一颗花生米。13 分钟前
深入理解JavaScript 的原型继承
java·开发语言·javascript·原型模式
学习使我快乐0117 分钟前
JS进阶 3——深入面向对象、原型
开发语言·前端·javascript
bobostudio199518 分钟前
TypeScript 设计模式之【策略模式】
前端·javascript·设计模式·typescript·策略模式
勿语&1 小时前
Element-UI Plus 暗黑主题切换及自定义主题色
开发语言·javascript·ui
一路向前的月光6 小时前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js
长路 ㅤ   6 小时前
vue-live2d看板娘集成方案设计使用教程
前端·javascript·vue.js·live2d
Fan_web6 小时前
jQuery——事件委托
开发语言·前端·javascript·css·jquery
Jiaberrr7 小时前
Element UI教程:如何将Radio单选框的圆框改为方框
前端·javascript·vue.js·ui·elementui
安冬的码畜日常9 小时前
【D3.js in Action 3 精译_029】3.5 给 D3 条形图加注图表标签(上)
开发语言·前端·javascript·信息可视化·数据可视化·d3.js
太阳花ˉ9 小时前
html+css+js实现step进度条效果
javascript·css·html