V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐关注
Meteor
JSLint - a JavaScript code quality tool
jsFiddle
D3.js
WebStorm
推荐书目
JavaScript 权威指南第 5 版
Closure: The Definitive Guide
rzWack
V2EX  ›  JavaScript

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

  •  2
     
  •   rzWack · 2020-07-06 22:29:02 +08:00 · 3492 次点击
    这是一个创建于 1605 天前的主题,其中的信息可能已经有所发展或是发生改变。

    片段 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 友久久我。

    33 条回复    2020-07-07 16:22:40 +08:00
    rpxwa
        1
    rpxwa  
       2020-07-06 22:40:08 +08:00 via iPhone   ❤️ 1
    可以看出 prototype 本质上是一个指针。

    我给了你一张纸条,纸条上写着我家的地址,这个地址就叫指针。

    当我把我家玻璃砸碎,你通过这个地址来到我家,你就能看到玻璃已被砸碎。

    然后我把你手里的纸条撕碎,给了你一张新纸条,纸条上写着我刚买的毛坯房地址。

    现在我把我原来家的玻璃修好。

    你顺着纸条找过来,目光所及没有修好的玻璃,只有空空如也的房子。
    JerryY
        2
    JerryY  
       2020-07-06 22:45:45 +08:00
    建议先看看红宝书再来思考这个问题。这里给一点参考,prototype={}已经改写了原型对象了,这时候再 prototype.x 是在这个改写后的对象上的修改,这里不是你说的`继承到最新的原型`;反过来的话,就更好理解了,因为你先 prototype.x 赋值,再改掉这个 prototype 的整个对象,这时候原型链已经“丢失”了。
    ChanKc
        3
    ChanKc  
       2020-07-06 22:51:04 +08:00
    想了一下
    原型链其实继承的是对象
    Foo.prototype= {x:1}; 这里是表示 new Foo 创建的对象要继承于匿名的{x:1}对象
    Foo.prototype= {x:2}; 这时候就继承于另外一个新创建的匿名对象{x:2}了,不过因为原来的{x:1}还能通过你之前 new 的 Foo 的原型来获取,所以不会被回收
    ChanKc
        4
    ChanKc  
       2020-07-06 22:55:40 +08:00
    @ChanKc
    修正
    原来的{x:1}还能通过 Object.getPrototypeOf 获取
    Kaciras
        5
    Kaciras  
       2020-07-06 23:00:41 +08:00
    不知道你能不能理解这个最基本的引用问题:

    let A = { x: 1 };
    let B = A;
    A.x = 2;

    请问现在 B.x = ?

    let A = { x: 2 };
    let B = A;
    A = { x: 1 };

    请问现在 B.x = ?

    你的例子就是上面的复杂版,没有高深的知识点,只是“原型链”三个字把你吓着了。

    我是这么理解原型链的,就两条:
    1.实例化时,自动给实例添加一个__proto__的属性,其值为类(或函数)的 prototype 属性
    2.从对象查找属性时,如果找不到则自动往__proto__上递归查找

    let ConSon = new Con(); // ConSon.__proto__ = Con.prototype
    Con.prototype= {x:1}; // Con.prototype 重新赋值,但是 ConSon.__proto__ 仍指向旧的值
    ChanKc
        6
    ChanKc  
       2020-07-06 23:02:00 +08:00
    另外本质上,new 可以大致等同于以下操作
    let foo = Object.create(Foo.prototype);
    Foo.call(foo)

    这里就只是用了一些 Foo 的原型链指向的一个对象,然后再拿 Foo 作为普通函数调用一下。可以说构造完的对象和 Foo 这个函数没什么关系了
    rzWack
        7
    rzWack  
    OP
       2020-07-06 23:03:58 +08:00
    @JerryY 嗯嗯,这个我理解了。但是这里还有个奇怪的问题,在先 prototype.x 赋值,再通过 prototype={..} 覆盖掉当前实例的原型后,再使用 prototype.x 赋值。此时输出的值仍然还是第一次 prototype.x 的值。这里是不符合前面 “ 再 prototype.x 是在这个改写后的对象上的修改 ”的逻辑的呀
    rzWack
        8
    rzWack  
    OP
       2020-07-06 23:15:59 +08:00
    @Kaciras 第一个是引用,值跟着变;第二个不太懂,用浏览器输出了一下 B.x=2,猜想 A={..}的时候重写地址发生了改变,B 还是指向原来 A 的地址,所以还是 2
    ChanKc
        9
    ChanKc  
       2020-07-06 23:19:40 +08:00
    @rzWack 索性这样,单纯只考虑原型链的话
    我把 foo = new Foo()替换成

    foo = {}
    Object.setPrototypeOf(foo, Foo.prototype)
    Foo.call(foo)

    接下来我把 Foo.prototype 换个写法

    let obj = {x:1}
    Foo.prototype = obj
    foo = {}
    Object.setPrototypeOf(foo, Foo.prototype)
    Foo.call(foo)

    然后我等价代换一下

    let obj = {x:1}
    Foo.prototype = obj
    foo = {}
    Object.setPrototypeOf(foo, obj)
    Foo.call(foo)

    问此时 foo 这个对象和 Foo.prototype 还有什么关系
    ChanKc
        10
    ChanKc  
       2020-07-06 23:20:23 +08:00
    @rzWack 你的猜想是对的
    hurrytospring
        11
    hurrytospring  
       2020-07-06 23:21:28 +08:00
    理解一下把 new 的过程实现一遍清楚了
    Jirajine
        12
    Jirajine  
       2020-07-06 23:23:36 +08:00 via Android
    看这个,图文并茂深入浅出
    https://javascript.info/function-prototype
    Kaciras
        13
    Kaciras  
       2020-07-06 23:29:42 +08:00
    @rzWack 你的猜想是对的,把 A 换成 Con.prototype,B 换成 ConSon.__proto__ 就是你的问题了
    shuangya
        14
    shuangya  
       2020-07-06 23:43:58 +08:00
    rzWack
        15
    rzWack  
    OP
       2020-07-06 23:53:53 +08:00 via Android
    @ChanKc 这里只看懂到
    Object.setPrototypeOf(foo, obj) => foo.prototype=obj 。Foo.call(foo)这一步不懂,大佬能给解释一下不😂
    Pyrex23
        16
    Pyrex23  
       2020-07-07 00:38:25 +08:00 via iPhone
    @rzWack 调用了构造函数 Foo,并把 this 指向实例 foo 。这一步是在初始化属性
    cs419
        17
    cs419  
       2020-07-07 07:05:19 +08:00   ❤️ 1
    let Foo = function Foo(){};
    Foo.prototype= {x:1};

    let s01 = new Foo();
    s01.__proto__.y=2;
    console.log(s01.__proto__ == Foo.prototype);
    let s02 = new Foo();
    console.log(s02.y);

    Foo.prototype = {z:3};
    console.log(s01.__proto__ == s02.__proto__);
    console.log(s01.__proto__ == Foo.prototype);
    Mutoo
        18
    Mutoo  
       2020-07-07 07:53:58 +08:00
    分享一下我的笔记:JS 原型拓扑
    https://blog.mutoo.im/2015/01/topology-of-javascript-prototype/

    画重点:
    当 let foo = new Foo() 的时候,伪代码相当于
    let foo = {__proto: Foo.prototype };
    之后修改 Foo.prototype 指向另一个 {} 时,不影响已经创建的 foo 的 __proto 指向原来的 prototype
    ChanKc
        19
    ChanKc  
       2020-07-07 08:22:43 +08:00 via Android
    @rzWack Foo.call(foo)就是“让 Foo 函数中的 this 指向 foo,并在这个情况下调用 Foo 函数”
    ChanKc
        20
    ChanKc  
       2020-07-07 08:54:02 +08:00 via Android
    @rzWack setPrototypeOf 等于楼里其他人说的调用__proto__的 setter 。我不用__proto__是因为在最新的 ecmascript 标准里不推荐用
    darknoll
        21
    darknoll  
       2020-07-07 09:00:27 +08:00
    应该是原型对象指向了新的对象,此时 QonSon.__proto__ !== Con.prototype
    fxxwor99LVHTing
        22
    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
        23
    JerryY  
       2020-07-07 09:45:00 +08:00
    @rzWack 你指的是第二个栗子吧,画个图就知道了。你创建对象后,那个对象的__proto__指向的是最原始的,接着你修改原型对象,是不会对之前的造成影响的。你之后创建的对象才会受到影响。
    justfindu
        24
    justfindu  
       2020-07-07 09:53:28 +08:00
    这你就需要理解一下实例和原类的区别.
    PineappleBeers
        25
    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
        26
    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
        27
    ChanKc  
       2020-07-07 10:31:55 +08:00
    @Mutoo 你的文章排版和图片很不错,我尤其喜欢代码块的字体
    但是 instanceof 的部分不太对,可能是标准有发生的变化

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

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

    所以除了考虑“追溯”原型链,还要看 instanceof 右值是不是一个函数
    Mutoo
        28
    Mutoo  
       2020-07-07 10:58:35 +08:00
    @ChanKc 15 年写的文章,标准应该是有变化
    liberty1900
        29
    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
        30
    liberty1900  
       2020-07-07 12:03:21 +08:00
    @liberty1900 我这里说的改动不是类似 F.prototype.x = 1 这种引用改动,而是 F.prototype = {x: 1}这种 reassign
    chenliangngng
        31
    chenliangngng  
       2020-07-07 12:46:12 +08:00 via Android
    JavaScript 中有一种奇怪的行为一直在被无耻地滥用,那就是模仿类------《 You don't know JavaScript 》
    rzWack
        32
    rzWack  
    OP
       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
        33
    xoyimi  
       2020-07-07 16:22:40 +08:00
    跟着复习了一下,有了新的进步,耐思
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5440 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 27ms · UTC 07:24 · PVG 15:24 · LAX 23:24 · JFK 02:24
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.