关于 js 原型链继承的问题(头大),请求支援

2020-07-06 22:29:02 +08:00
 rzWack

片段 1: let Foo = function Foo(){}; Foo.prototype= {x:1};
let FooSon = new Foo(); console.log(FooSon.x); //输出:1 Foo.prototype.x =2; console.log(FooSon.x); //输出:2

片段 2: let Con = function Con(){} Con.prototype.x =2; let ConSon = new Con(); console.log(ConSon.x); //输出:2 Con.prototype= {x:1};
console.log(ConSon.x); //输出:2

let QonSon = new Qon(); console.log(QonSon.x); //输出:1 =>引用了最新原型

问题是看不懂为什么先 prototyp={},再 prototype.x 方式时候后面的实例能够继承到最新的原型,反过来就不可以?这里面牵涉到了什么高深知识点,万能的 v 友久久我。

3372 次点击
所在节点    JavaScript
33 条回复
darknoll
2020-07-07 09:00:27 +08:00
应该是原型对象指向了新的对象,此时 QonSon.__proto__ !== Con.prototype
fxxwor99LVHTing
2020-07-07 09:33:46 +08:00
你的代码我没看懂,建议发代码前最好整理下格式,

推荐看下《 JavaScript 高级程序设计》第 6 章 的 第 3 节。
js 的原型链继承其实并没有什么魔法。
0
js 里面的每一个函数都有一个叫做 prototype 的属性,即原型属性,这个属性指向的对象即为这个函数的原型对象,原型对象就是普普通通的对象而已
1
js 里面的一个对象可以通过 new 操作符 作用于一个‘构造函数’得到,‘构造函数’与普通的函数并没有区别,只是概念上 /功能上的区分
2
由‘构造函数’创建得到的对象,其内部会自动有一个指向其‘构造函数’原型对象的指针,也就是说‘构造函数’和其创建的对象都会有一个属性指向‘原型对象’(原型对象仅仅只是一个普通的对象,它由‘构造函数’的 prototype 属性来指定或进行修改),也就是说 js 里面的每一个对象内部都有一个指针指向一个原型对象,不论这个对象是由 自定义的‘构造函数’ 或是 对象字面量 或是 new Object() 等等方式创建的,每一个对象都会关联一个原型对象。 注意:‘构造函数’ 和 由其创建的对象 是分别指向‘原型对象’的,如果先使用‘构造函数’创建了一个对象 a,再去修改该‘构造函数’的 prototype 属性,将其指向一个新的对象,则这个修改对于对象 a 是不可见的,因为该操作只是切断了‘构造函数’和最初的那个原型对象的链接,而 a 对象还是引用着最初的那个原型对象,a 对象并没有被影响,如果直接修改最初的那个原型对象,例如给它添加一个属性,那个这个操作对于 a 对象是可见的,因为是同一个对象。
3
既然每一个对象内部都有一个指针指向一个原型对象(该原型对象由其‘构造函数’的 prototype 属性决定或进行修改), 而且原型对象也是一个普普通通的对象,那么完全可以再将‘原型对象’设置为由某个‘构造函数’创建的对象,而这个对象内部同样有一个指针指向另一个原型对象,以此类推,便形成了一个‘原型链’,注意:这里有一个递归定义,即 原型对象可以使用类似当前对象的创建方式来进行创建,即 当前对象会有一个指针指向原型对象,而原型对象只是一个简单的对象(可以类别当前对象)它也会有一个指针指向另一个原型对象,而另一个原型对象也只是一个简单的对象,它也会有一个指针指向另另一个原型对象,,,以此类推,便形成了原型链继承。
JerryY
2020-07-07 09:45:00 +08:00
@rzWack 你指的是第二个栗子吧,画个图就知道了。你创建对象后,那个对象的__proto__指向的是最原始的,接着你修改原型对象,是不会对之前的造成影响的。你之后创建的对象才会受到影响。
justfindu
2020-07-07 09:53:28 +08:00
这你就需要理解一下实例和原类的区别.
PineappleBeers
2020-07-07 10:14:11 +08:00
这里面有几个知识点,我就一一列出来,也正好给自己复习一下。
1 、引用类型与基本类型。
2 、js 取值。
3 、原型链的继承。

先了解一下引用类型和基本类型最大的区别:
1 、操作基本类型是直接操作内存空间里面的值。
2 、操作引用类型一般是操作内存地址,将内存地址赋值给别的变量。

function Fn() {};
Fn.prototype = {x: 1};
let fn1 = new Fn(), fn2 = new Fn();
fn1; //Fn(){}
fn1.x; //1
fn2.x; //1

这里 new 有四步操作
①let fn1 = undefined;
Object.create({}); //新建一个匿名对象,没有赋值
②{}.__proto__ = Fn.prototype;
{}.__proto__.constructor = Fn;
③{}.call(Fn);
④fn1 = {}; //将匿名对象赋值

然后是取值问题,fn1.x 取值会先从 fn1 这个变量里面去查找有无 x 这个值,没有的话就从它的原型链__proto__上面去查找。很显然,fn1 这个变量自身上没有值,而它的__proto__是等于 Fn.prototype 的,所以它最终找到 x = 1 ;

fn1.x = 2;
fn1.x; //2;
fn2.x //1;
fn1; //Fn() {x: 2}
fn2; //Fn() {}

当 fn1.x = 2 时,相当于给 fn1 这个变量自身加了个{x:2}的对象,而上面看到 fn1 与 fn2 是两个匿名对象,除了__proto__指向同一个 Fn.prototype 外,他们自己的内存空间是不干扰的。所以 fn1.x 不能影响 fn2.x,他们这个 x 取值的位置都不同,一个是自身,一个是原型链。

Fn.prototype.x = 3;
fn1.x; //2
fn2.x; //3
fn1.__proto__.x; //3

这里也就清楚了,如果 Fn 的 prototype 直接修改 x,也就相当于在{x: 1}(原)这块内存空间中直接修改 x 的值。而 fn1.__proto__与 fn2.__proto__指向的还是{x: 1}(原)这块内存空间,所以从 fn1.__proto__和 fn2.__proto__取 x 都变成了 3 。

Fn.prototype = {x: 4};
fn1.__proto__.x; //3
fn2.x; //3

上面提到过,引用类型赋值,变量指向的是内存地址,由于 fn1.__proto__与 fn2.__proto__在声明的时候已经指向了原来{x: 1}(后 x 改为了 3 )这块内存空间,所以 Fn.prototype 指向了新的内存空间也就与 fn1.__proto__和 fn2.__proto__无关了。
soulmt
2020-07-07 10:30:05 +08:00
其实你还是没理解 原型 和原型链, 跟其他人一样,建议你先看一遍红宝书,理解原型,原型链,继承,还有引用再回头想想
首先先说片段 2
你在第一次 new 的时候 ConSon.__proto__ === Con.prototype
所以在你 Con.prototype= {x:1};之前
修改 Con.prototype 和修改 ConSon.__proto__效果是一样的,
但是你 Con.prototype= {x:1};之后
ConSon.__proto__ 所指的 prototype 其实没变
这其实就相当于
let a = {a:1}
let b = a;
a = {c:2};
这段代码的结果就是 a 是{c: 2} b 是{a:1}
与上述同理 你改了 prototype 之后 只能说 Con.prototype 确实变了,但是 ConSon.__proto__其实没变, 所以不会有变化,只有你重新 new 才会用新的 prototype 去初始化
不知道你好不好理解, 这是个好问题,之前确实没有这么思考过。
ChanKc
2020-07-07 10:31:55 +08:00
@Mutoo 你的文章排版和图片很不错,我尤其喜欢代码块的字体
但是 instanceof 的部分不太对,可能是标准有发生的变化

foo instanceof Foo 可以替换成 Foo[Symbol.hasInstance](foo)

其中[Symbol.hasInstance]是 Function.prototype 上的方法

所以除了考虑“追溯”原型链,还要看 instanceof 右值是不是一个函数
Mutoo
2020-07-07 10:58:35 +08:00
@ChanKc 15 年写的文章,标准应该是有变化
liberty1900
2020-07-07 11:59:25 +08:00
@Jirajine 老哥稳,这篇文章才读了没几段,就豁然开朗. 这个问题一句话就能解释, "F.prototype only used at new F time", 也就是 prototype 在 new 的时候已经确定,之后对 F.prototype 的改动不会对已经 new 过的对象产生影响,只会对未来 new 的对象生效
liberty1900
2020-07-07 12:03:21 +08:00
@liberty1900 我这里说的改动不是类似 F.prototype.x = 1 这种引用改动,而是 F.prototype = {x: 1}这种 reassign
chenliangngng
2020-07-07 12:46:12 +08:00
JavaScript 中有一种奇怪的行为一直在被无耻地滥用,那就是模仿类------《 You don't know JavaScript 》
rzWack
2020-07-07 15:54:33 +08:00
看了诸位大佬的解释和推荐的链接,我觉得我懂了。现在说一下我的理解:

对于片段 1,先 prototype={..} 覆盖了原有(默认)原型,再 prototype.x=.. 时,是基于 prototype={..} 的地址引用改动,无论怎么修改 FooSon.__proto__ 指向的 Foo.prototype 都是 Foo.prototype={..}这一步的地址,所以 FooSon 能够动态获取到原型链上的值。用官方一点的说法是: “继承原型属性的实例总是能够获取最新值” 。

对于片段 2,先 prototype.x =.. 是基于原有(默认)原型上的引用改动,再 prototype={..}覆盖原型,此时地址发生了变化。对于 CooSon.__proto__ 恒指向的是 初始化时原型(的地址),而不是覆盖后的地址导致后续无论怎么修改 Coo.prototype, CooSon.x 值恒定不变。用官方一点的说法是: “用新对象替换 prototype 属性不会更新以前的实例” 。

而在后面实例化的 QonSon 实例化时,QonSon.__proto__ 指向的是在 QonSon 实例化之前的 Coo.prototype 的最新值,所以 QonSon.x =1 。引用 29 楼大佬 @liberty1900 的话,即实例化对象的 __proto__ 属性的值(个人更倾向于内存空间的地址)在 new 的时候已近确定。后实例化的对象自然继承的就是原型链上最后一次修改的值。
如果理解有误,希望大家指正一下。
xoyimi
2020-07-07 16:22:40 +08:00
跟着复习了一下,有了新的进步,耐思

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/687713

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX