ES6 Class 和 Babel 6 在 IE 10 及以下时候的一个坑

- hikerpig
#Javascript#Babel

写 ES6+ 一定逃不开 babel,也避不开调试 babel 生成的一些代码。

当输入一段 ES6 Class 代码时:

class Person {
  static baseName = 'Person'

  static speakForAll() {
    return this.baseName
  }

  speak() {
    return 'Hello'
  }
}

class Developer extends Person {
}


const myself = new Developer()
console.log(myself.speak() === 'Hello') // true

console.log(Person.speakForAll() === 'Person') // true
console.log(Developer.speakForAll() === 'Person') // true

问题

在开发常用的浏览器 Chrome 和 Firefox 里正常工作,但是在 IE10 下会报错 Uncaught TypeError: Developer.speakForAll is not a function

刨根问底

.babelrc 配置如下:

{
  "presets": ["es2015", "stage-2"],
}

看 babel 编译出的一串代码 blahblah, 重点下面说:

var _class, _temp

function _possibleConstructorReturn(self, call) {
  if (!self) {
    throw new ReferenceError(
      "this hasn't been initialised - super() hasn't been called",
    )
  }
  return call && (typeof call === 'object' || typeof call === 'function')
    ? call
    : self
}

function _inherits(subClass, superClass) {
  if (typeof superClass !== 'function' && superClass !== null) {
    throw new TypeError(
      'Super expression must either be null or a function, not ' +
        typeof superClass,
    )
  }
  subClass.prototype = Object.create(superClass && superClass.prototype, {
    constructor: {
      value: subClass,
      enumerable: false,
      writable: true,
      configurable: true,
    },
  })
  if (superClass)
    Object.setPrototypeOf
      ? Object.setPrototypeOf(subClass, superClass)
      : (subClass.__proto__ = superClass)
}

function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError('Cannot call a class as a function')
  }
}

var Person = ((_temp = _class = (function() {
  function Person() {
    _classCallCheck(this, Person)
  }

  Person.speakForAll = function speakForAll() {
    return this.baseName
  }

  Person.prototype.speak = function speak() {
    return 'Hello'
  }

  return Person
})()),
(_class.baseName = 'Person'),
_temp)

var Developer = (function(_Person) {
  _inherits(Developer, _Person)

  function Developer() {
    _classCallCheck(this, Developer)

    return _possibleConstructorReturn(this, _Person.apply(this, arguments))
  }

  return Developer
})(Person)

var myself = new Developer()
console.log(myself.speak() === 'Hello')

console.log(Person.speakForAll() === 'Person')
console.log(Developer.speakForAll() === 'Person')

关键是此段实现继承的部分:

function _inherits(subClass, superClass) {
  if (typeof superClass !== 'function' && superClass !== null) {
    throw new TypeError(
      'Super expression must either be null or a function, not ' +
        typeof superClass,
    )
  }
  subClass.prototype = Object.create(superClass && superClass.prototype, {
    constructor: {
      value: subClass,
      enumerable: false,
      writable: true,
      configurable: true,
    },
  })
  if (superClass)
    Object.setPrototypeOf
      ? Object.setPrototypeOf(subClass, superClass)
      : (subClass.__proto__ = superClass)
}

subClass.prototype 这一段比较简单,操作原型链来实现实例方法和属性的继承。顺带还用 object descriptor 重写了 constructor 这一属性,调用 myself.constructor 时才会拿到正确的值 Developer,而不是 Person

接下来的一段比较有趣。

Object.setPrototypeOf(subClass, superClass)

这个写法还是比较讨巧的,将父类的构造函数 superClass 作为子类构造函数 subClass 的原型。

知识回顾

Object.setPrototypeOf

这是个 ES2015 新提出的函数,函数签名:

Object.setPrototypeOf(obj, prototype)

对比 Object.create,可以在对象创建出来之后替换其原型。

const p1 = {}
Object.setPrototypeOf(p1, Person.prototype)
console.log(p1.speak()) // 为'Hello'

浏览器兼容性

Feature Chrome Edge Firefox IE Opera Safari
Basic Support 34 (Yes) 31 11 (Yes) 9

注意到从 IE11 才开始支持此方法。

既然第一条路行不通,那就第二条呗。

__proto__

_inherits 函数中回退到 subClass.__proto__ = superClass__proto__ 指向的是对象构造函数的 prototype,通过重设 subClass 的原型来使其获得父类构造函数上的方法(此例中是 class 上的静态方法)。

关键在于,__proto__ 是个非标准的属性,根据微软的文档,IE10 及其以下都没有支持。

Not supported in the following document modes: Quirks, Internet Explorer 6 standards, Internet Explorer 7 standards, Internet Explorer 8 standards, Internet Explorer 9 standards, Internet Explorer 10 standards. Not supported in Windows 8.

Babel 的一个 issue 中有人提过类似问题,回答是:babel 6 不考虑兼容 IE。没碰上问题算幸运,碰上问题只好自己解决。

解决方案

就这个事情来说,添加一个 polyfill 能够解决。以 这个实现 来说:

module.exports = Object.setPrototypeOf || ({__proto__:[]} instanceof Array ? setProtoOf : mixinProperties);

function setProtoOf(obj, proto) {
  obj.__proto__ = proto;
  return obj;
}

function mixinProperties(obj, proto) {
  for (var prop in proto) {
    if (!obj.hasOwnProperty(prop)) {
      obj[prop] = proto[prop];
    }
  }
  return obj;
}

先探测 Object 上是否原生支持,然后检测更改 __proto__ 是否有作用,最后回退到简单暴力的遍历赋值。