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

最近在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 被影响

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

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

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

相关推荐
2401_878454533 小时前
浏览器工作原理
前端·javascript
by__csdn4 小时前
Vue3 setup()函数终极攻略:从入门到精通
开发语言·前端·javascript·vue.js·性能优化·typescript·ecmascript
Luna-player5 小时前
在前端中,<a> 标签的 href=“javascript:;“ 这个是什么意思
开发语言·前端·javascript
lionliu05195 小时前
js的扩展运算符的理解
前端·javascript·vue.js
小草cys5 小时前
项目7-七彩天气app任务7.4.2“关于”弹窗
开发语言·前端·javascript
前端一小卒7 小时前
一个看似“送分”的需求为何翻车?——前端状态机实战指南
前端·javascript·面试
syt_10137 小时前
Object.defineProperty和Proxy实现拦截的区别
开发语言·前端·javascript
长安牧笛7 小时前
儿童屏幕时间管控学习引导系统,核心功能,绑定设备,设时长与时段,识别娱乐,APP超时锁屏,推荐益智内容,生成使用报告,学习达标解锁娱乐
javascript
栀秋6668 小时前
深入浅出链表操作:从Dummy节点到快慢指针的实战精要
前端·javascript·算法
青青很轻_8 小时前
Vue自定义拖拽指令架构解析:从零到一实现元素自由拖拽
前端·javascript·vue.js