构造函数与ES6 class之间的一点微妙区别

前几天去面试,面试官提出了这么一个问题:有A和B两个类,a和b分别继承前者,如何让b也继承A。

那时候我可是一脸懵逼,想了下回答了b = Object.create(A.prototype),面试官不置可否,当时我心里窃喜,以为蒙对了。回家之后越想越不对,于是有了这篇文章。

基础代码

为了可以愉快地console.log东西,需要先以以下代码作为基础:

class A {
  a() {
    console.log('a');
  }
}

class B {
  b() {
    console.log('b');
  }
}

let a = new A();
let b = new B();

挺简单的对吧?

Object.create()

首先试下自己的想法,输入b = Object.create(A.prototype),看看会有什么结果:

结果很残酷,整个b都继承A了,其实文档写的蛮清楚的:Object.create() 方法使用指定的原型对象和其属性创建了一个新的对象。因而b = Object.create(A.prototype)约等于b.__proto__ = A.prototype,都是看文档囫囵吞枣的锅啊!

(写完文章后,发现其实为了答题的话,想到一个黑科技:b.__proto__.__proto__ = Object.create(A.prototype)b.bb.a都能打印出来,但我看起来真的好奇怪,求dalao打醒。)

for in 遍历

既然Object.create()此路不通,那么我换一个,我遍历A.prototype,再一个个添加到b.__proto__里面总可以了吧?于是我喜滋滋地写下了以下代码:

for(let key in A.prototype){
    b.__proto__[key] = A.prototype[key]
}
console.log(b.a());

之后脸就被打得啪啪响了,Chrome报错:Uncaught TypeError: b.a is not a function,这是什么鬼?为毛b.a不是函数,再三检查不是语法错误之后,我在b.__proto__[key] = A.prototype[key]前多加了一句console.log(key),结果是什么都没打印出来。然而,当我打印console.log(A.prototype)时,明显是一个对象,里面有key为a!

解释

When you have eliminated the impossible, whatever remains, however improbable, must be the truth.

造成这样的结果,只有一个原因,A.prototype.a不可枚举,因而for in不能遍历。不过我怎么觉得以前是可以的,于是我在Chrome中输入以下代码:

function C() {

}

C.prototype.c = function() {
  console.log('c')
}

for(let key in C.prototype){
  console.log(key);
}

结果是能打印出a的,这就尴尬了,这到底是怎么一回事呢?难道ES6的class语法糖,它的prototype里的方法是不可枚举,而原来构造函数prototype里的方法确实可以枚举的?赶紧验证一下:

cDescriptor = Object.getOwnPropertyDescriptor(c,'c');
aDescriptor = Object.getOwnPropertyDescriptor(a,'a');

打印出来,发现cDescriptor的enumerablefalse,而aDescriptor的enumerabletrue,所以,原因一目了然,ES6中的class,它的方法默认是不可枚举的,而构造函数默认是可枚举的。细想一下,这其实是个优化,让你在遍历时候,不需要再判断hasOwnProperty了。(后面查资料发现,其实阮一峰老师的文章早就介绍过了,当时看的时候不认真啊!)

解决问题

那么,那道面试题应该如何解呢?如果使用构造函数,那倒是可以for in遍历塞进去,但如果是ES6的class呢?
查阅文档发现,使用Object.getOwnPropertyNames()方法,就可以拿到我所需要的prototype对象的key,代码如下:

let keys = Object.getOwnPropertyNames(A.prototype);
for(let key of keys){
    if(key !== 'constructor'){
        b.__proto__[key] = A.prototype[key];
    }
}

如此一来,b.ab.b都能打印出来了!

小结与思考

总的来是,是复习了一些prototype相关API,希望对大家有帮助。
另外,如此解题,我感觉是十分古怪的,因为b的constructor指向B,然而它却有B.prototype没有的方法,现在代码简单是无所谓,然而复杂起来的时候,debug绝对是个深坑。希望有dalao指导如何优雅的实现。