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

Javascript 之奇技淫巧 - 如何模拟实现 `super` 访问父类方法的功能(与小学三年级的 ChatGPT 一起学 JS 编程)

  •  
  •   riceball · 2023-04-01 09:13:15 +08:00 · 1538 次点击
    这是一个创建于 602 天前的主题,其中的信息可能已经有所发展或是发生改变。

    心智为 9 岁的 ChatGPT 大约相当于我们小学 3 年级的学生,所以让我们与小学 3 年级的 ChatGPT 一起来深入学习编程吧.

    下面是对 ChatGPT 的第一次提问:

    你知道吗?编写一个 js 函数模拟实现 super 访问父类方法的功能有几种方法?哪一种方法从语法上最接近原生super?

    要求:该函数的调用方式要从语法上尽可能的接近原生super的调用方式,实现尽量简洁明了. 其中约定实现的函数名以及函数签名(返回值根据具体实现方式而定)为: getSuper(instance: Object)

    ChatGPT 的第一次回答

    实现 super 访问父类方法的功能有以下几种方法:

    1. 使用 Object.getPrototypeOfObject.getOwnPropertyDescriptor 方法来获取父类的属性描述符,从而获取父类方法,并使用 Function.prototype.call 方法来调用该方法:

      function getSuper(instance) {
        const proto = Object.getPrototypeOf(instance);
        const descriptor = Object.getOwnPropertyDescriptor(proto, arguments.callee.caller.name);
        if (descriptor) {
          const superFunc = descriptor.value;
          return function(...args) {
            return superFunc.call(instance, ...args);
          };
        }
      }
      
    2. 使用原型链 Object.getPrototypeOf()Reflect.getPrototypeOf() 都可以获取原型对象,但是 Reflect.getPrototypeOf() 可以在 Proxy 对象上获取正确的原型对象,因此使用 Reflect.getPrototypeOf() 可以增强代码的兼容性。具体实现代码如下:

      function getSuper(instance) {
        const prototype = Reflect.getPrototypeOf(instance);
        return function(methodName, ...args) {
          const parentPrototype = Reflect.getPrototypeOf(prototype);
          const parentMethod = parentPrototype[methodName];
          if (typeof parentMethod === 'function') {
            return parentMethod.call(instance, ...args);
          }
        }
      }
      
    3. 使用 ES6 的 Proxy 对象来代理子类实例的属性访问和方法调用。当子类实例的属性或方法不存在时,通过代理对象访问父类实例的属性或方法:

      function getSuper(instance) {
        return new Proxy(instance, {
          get(target, prop) {
            if (prop in target) {
              return target[prop];
            } else if (prop in Object.getPrototypeOf(target)) {
              const superProp = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(target), prop);
              if (superProp.hasOwnProperty('value')) {
                return (...args) => superProp.value.call(target, ...args);
              } else {
                return superProp.get.call(target);
              }
            }
          }
        });
      }
      
      class Parent {
        constructor() {
          this.parentProperty = 'parent property';
        }
        parentMethod() {
          return 'parent method';
        }
      }
      
      class Child extends Parent {
        constructor() {
          super();
          this.childProperty = 'child property';
        }
        childMethod() {
          const superMethod = getSuper(this).parentMethod;
          return `child method calling parent method: ${superMethod()}`;
        }
      }
      
      const childInstance = new Child();
      console.log(childInstance.childMethod()); // Output: "child method calling parent method: parent method"
      

    样例类:

    class Root {
      constructor(name) {
        this.name = name;
      }
      hi(n) {
        console.log(`hi ${n} ${this.name} from Root`);
      }
    }
    
    class A extends Root {
      hi() {
        console.log(`hi from A`)
        super.hi("here") //replace this
    }
    
    let a = new A('Mike')
    a.hi('here') // print 'hi here from A' and 'hi Mike from Root'
    

    引导提问 1

    ChatGPT, 你之前的回答中各个方法中都存在问题,思考下.

    那么,如何实现 super 访问父类方法的功能,你们怎么看?

    3 条回复    2023-04-01 20:40:13 +08:00
    riceball
        1
    riceball  
    OP
       2023-04-01 20:12:22 +08:00
    ### 请说出方法一中的问题和缺陷

    > 首先,该函数只能在函数内部被调用,即在子类方法中使用 `getSuper(this)(...args)` 调用父类方法。如果在函数外部调用该函数,则无法获取到正确的调用者函数名。
    >
    > 其次,该函数只能用于访问父类方法,而不能用于访问父类属性。如果父类属性与子类属性同名,则无法通过该函数访问父类属性。
    >
    > 最后,该函数使用了 `arguments.callee.caller`,这是一个非标准的特性,在严格模式下可能会被禁用。

    补充:函数表达式也不存在`arguments`, 而且目前 JS 几乎都是在严格模式下,所以该方案基本无用.

    ### 那么方法二中的问题和缺陷

    > 该实现方式可以实现模拟 `super` 访问父类方法的功能,但是有一些问题和限制。
    >
    > 首先,该实现方式使用了 `Reflect.getPrototypeOf` 方法获取实例的原型对象。这种方式可以正确地获取实例的原型对象,但在一些特殊情况下可能会出现问题,比如当实例的原型链中存在 Proxy 对象时。
    >
    > 其次,该实现方式只能在子类方法内部调用 `getSuper` 函数,且需要手动传入父类方法名。这样就需要手动维护父类方法名,容易出现错误。
    >
    > 最后,该实现方式没有考虑多层继承的情况。如果子类的父类也继承自其他类,则可能无法正确地访问到最终的父类方法。


    ### 那么方法三呢


    > 该实现方式可以实现模拟 `super` 访问父类方法的功能,但是也有一些问题和限制。
    >
    > 首先,该实现方式使用了 Proxy 对象来代理实例对象,以拦截属性的访问。这种方式可能会带来性能上的损失,特别是在大规模的应用中。
    >
    > 其次,该实现方式只能用于访问父类属性或方法,而不能用于访问父类的私有属性或方法。
    >
    > 最后,该实现方式可能会存在多层继承的情况。如果子类的父类也继承自其他类,则可能无法正确地访问到最终的父类方法。

    补充: 当子类重载了该方法,那么该实现无法访问到父类方法,需要修改,总体来说 ChatGPT 想得过于复杂

    ### 最后,什么是多层继承的情况

    多层继承是指一个子类从另一个子类继承,而这个子类又从其它父类继承。举例说明:

    ```javascript
    class A {
    m() {console.log('A')}
    }
    class B extends A {
    m() {console.log('B'); getSuper(this).m()}
    }
    class C extends B {
    m() {console.log('C'); getSuper(this).m()}
    }

    如果不支持多重继承,那么`B.prototype.m()`中`getSuper()`就会出错.
    chnwillliu
        2
    chnwillliu  
       2023-04-01 20:29:33 +08:00 via Android
    super 是静态绑定,有些场景是模拟不了的。

    class A {
    m() {console.log('A')}
    }
    class B extends A {
    m() {console.log('B'); super.m()}
    }

    b = new B()
    m = b.m;
    m(); // 方法可以脱离原来的主体,super 照样拿得到。this 不行
    riceball
        3
    riceball  
    OP
       2023-04-01 20:40:13 +08:00
    @chnwillliu 虽然 super 可以这样,但是实例方法不是这样用的,如果不需要`this`,那么应该定义为 static 方法或者函数.
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2518 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 10:36 · PVG 18:36 · LAX 02:36 · JFK 05:36
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.