关于ES6中继承的问题 B继承A B.__proto__ = A?

qzuser
  • 7

先附上代码

class A{}
class B extends A{}
B.__proto__ === A //true
B.__proto__ === Function.prototype //false
typeof B //function
B.constructor === Function //true

我们知道,每个实例对象( object )都有一个私有属性(称之为 __proto__ )指向它的构造函数的原型对象(prototype

class是语法糖,本质上还是个函数,因此B实际上是个函数,可以看作通过new Function创建出来的Function对象实例

那么问题来了B.__proto__不应该等于Function.prototype吗?为什么等于A呢?这是特殊规定的吗?

评论
阅读 481
3 个回答
✓ 已被采纳

B.__proto__.__proto__ 确实是 Function.prototype ,但首先它的原型是 A ,其原型的原型才是函数原型。因为定义在 A 上的静态方法 B 也要继承。


更新:


每一个对象都有原型,但是对象的原型并不一定是对象的构造函数的 prototype 属性。

我在这里先声明一下几个术语,前端届对原型的称呼一直比较混乱:

  • 对象:JS 中一切非原始值皆对象,函数也是对象的一种
  • 原型:

    • 每个对象都有一个原型,原型本身也是一个普通的对象。
    • 使用点操作符或者 [ ] 访问属性时,会先检查对象本身的属性,如果不存在则会检查对象的原型,如果还不存在则会继续检查对象原型的原型直到原型的尽头 Object.prototype ,它没有原型,它的原型是 null,这个就是所谓的原型链。
    • 获取对象的原型可以用标准的 Object.getPrototypeOf(obj),或者非标准的 obj.__proto__, 以下我都用 Object.getPrototypeOf(obj).
    • 对象的默认 toString, valueOf 方法都来自 Object.prototype。
  • 函数:函数是一种特殊的对象,所以函数也有原型,通过 function 语句声明的函数的原型是 Function.prototype。函数的 call,apply,bind 方法都来自 Function.prototype 上。
  • 构造函数:除了箭头函数外,普通函数都可以当作构造函数用,用法就是使用 new 操作符。
  • 类:ES6 的类是一种特殊的函数,只能通过 new 操作符来使用,不能当普通函数来直接调用。
  • 函数.prototype: 函数对象还有一个特殊的属性,名字叫 prototype。这个我们还是别叫 函数的原型 了吧,因为函数作为对象看待它本身确实是有原型的,所以我们叫它函数的prototype属性,以区别于函数的原型,即:Object.getPrototypeOf(fn) vs fn.prototype.
  • new 一个构造函数发生了什么:

    • 新建一个对象
    • 把对象的原型指向构造函数的 prototype 属性
    • 把对象当 this 运行一遍构造函数
  • 什么是继承?

    • JS 的继承发生在对象之间,而非类之间,就像现实中的儿子继承爸爸,不需要先有儿子类和爸爸类,让儿子类 extends 爸爸类然后实例化。JS 中的对象直接继承另一个对象。
    • JS 的继承是使用原型来实现的。对象属性查找会检查原型链,这就是继承概念的体现。
    • ES5 加入的 Object.create API 就是把一个对象当原型来创建另一个对象之用。
  • 基于 prototype 的继承模拟基于 class 的继承:

    • 函数的 prototype 属性就是用来模拟基于 class 的继承的,否则只要能从一个对象构造另一个对象那 prototype 继承就有了。
    • 当我们 new 一个 class/函数 时,其实我们是把 class/函数的prototype 属性当原型来构造新对象的。你把方法和属性定义在函数的prototype属性上,用起来和传统的类定义方法和字段一个感觉。
    • 对象的构造器本质就是一个 constructor 属性,所以函数的 prototype.constructor 会自动回指到函数本身,以便 new 出来的对象能正确获取到构造器。

说的有点多,回到你的问题:

每一个对象都有原型,但是对象的原型并不一定是对象的构造函数的 prototype 属性。

且不说 constructor 属性并不是一个锁死的属性,JS 中的有些对象也并不一定存在一个构造器。

var father = {money: 1000,house:'big house'};
// father 对象使用对象字面量定义,本质上和 new Object 一回事
// 所以 father 是用 Object 构造的
father.constructor === Object // 没问题

var son = Object.create(father); 
son.money // 这里我们说 son 继承了 father 的所有属性
Object.getPrototypeOf(son) === father; // 毛得问题吧
son.constructor // 这玩意儿会是什么?
son.constructor === father.constructor; // 嗯,来自于父亲

如上代码展示,其实 JS 中对象的构造器不是在所有场合都合理且有意义。构造函数这个概念在基于 prototype 继承的体系里其实是不需要的,它是 JS 模拟基于类的继承加入的东西。

那么题主的问题到底出现在哪里呢?出现在这里:

class是语法糖,本质上还是个函数,因此B实际上是个函数,可以看作通过new Function创建出来的Function对象实例

class 是语法糖没错,本质是函数也没错,错在 class B 并不能看作是 new Function 构造出来的,class A 可以,但是 B 不行。因为 B extends A,B 是通过 A 构造出来的。你忘了,JS 继承的本质是对象继承。 class B extends A 这里隐含了两个 prototype 继承。 B.prototype 继承了 A.prototype, 同时 B 继承了 A。

class A {
  static p = 1;
}
class B extends A{}

B.p; // 1
Object.getPrototypeOf(B.prototype) === A.prototype; // true
Object.getPrototypeOf(B) === A; // true
//或者说用非标准属性表示 B.__proto__ === A

因为静态属性也是需要继承的,所以 B 并不是直接通过 new Function 构造出来的,B 是通过 A 作为原型构造出来的,即 B = Object.create(A); 这样 B 才能获得 A 上定义的静态属性和方法,才符合基于 class 的继承的表现。

所以,B.constructor 在这里并没有很符合实际的意义,并不存在一个函数它把 B 构造了出来。从原型的角度看待就不存在这种问题,B 用了 A 当原型,A 用了 Function.prototype 当原型,仅此而已。

class B extends A

所以 B.__proto__ === A

ES6 标准入门里是这么写的

子类的__proto__属性,表示构造函数的继承,总是指向父类。

考虑下述代码

class A {}
A.sayHi = () => 'Hi'

class B extends A {}
console.log(B, B.sayHi)

你可以发现,子类 B 继承了父类的 sayHi 静态方法,当然,这是规范的规定的,题主的困惑是由于不了解这一规定导致的。

而具体到规范的实现,在 sec-runtime-semantics-classdefinitionevaluation,主要的逻辑在

  1. If ClassHeritageopt is not present, then

...

  1. Else,
    ...
    b. Let superclassRef be the result of evaluating ClassHeritage.
    d. Let superclass be ? GetValue(superclassRef).
    g. Else,

    1. Let protoParent be ? [Get](https://tc39.es/ecma262/#sec-get-o-p)(superclass, "prototype").
    2. If [Type](https://tc39.es/ecma262/#sec-ecmascript-data-types-and-values)(protoParent) is neither Object nor Null, throw a TypeError exception.
    3. Let constructorParent be superclass.

...

  1. Let proto be OrdinaryObjectCreate(protoParent).

......

  1. Let constructorInfo be ! DefineMethod of constructor with arguments proto and constructorParent.
  2. Let F be constructorInfo.[[Closure]].

仿照规范,类似的代码,可能就是这样

class A {}
A.sayHi = () => 'Hi'

class B extends A {}
console.log(B, B.sayHi)

const C = extendClass(A)
console.log(C, C.sayHi)

function extendClass(superClass) {
  // class 继承代码
  const protoParent = superClass.prototype
  const constructorParent = superClass
  const proto = ordinaryObjectCreate(protoParent)
  const constructorInfo = defineMethod(proto, constructorParent)
  const result = constructorInfo.closure
  // 剩余的原型链继承代码...
  return result
}

function ordinaryObjectCreate(proto) {
  const o = {}
  o.__proto__ = proto
  return o
}
function ordinaryFunctionCreate(prototype) {
  return ordinaryObjectCreate(prototype)
}

function defineMethod(object, functionPrototype = Function.prototype) {
  return {
    closure: ordinaryFunctionCreate(functionPrototype)
  }
}

为了和规范保持一致,我的变量名基本都是用规范里的名称,有空的话,可以读读规范。

撰写回答

登录后参与交流、获取后续更新提醒

宣传栏