乐趣区

关于javascript:ES5的继承和ES6的继承有什么区别让Babel来告诉你

如果以前问我 ES5 的继承和 ES6 的继承有什么区别,我肯定会自信的说没有区别,不过是语法糖而已,充其量也就是写法有区别,然而当初我会伪装思考一下,而后说尽管只是语法糖,但也是有点小区别的,那么具体有什么区别呢,不要走开,下文更精彩!

本文会先回顾一下 ES5 的寄生组合式继承的实现,而后再看一下 ES6 的写法,最初依据 Babel 的编译后果来看一下到底有什么区别。

ES5:寄生组合式继承

js有很多种继承形式,比方大家耳熟能详的 原型链继承 结构继承 组合继承 寄生继承 等,然而这些或多或少都有一些不足之处,所以笔者认为咱们只有记住一种就能够了,那就是 寄生组合式继承

首先要明确继承到底要继承些什么货色,一共有三局部,一是实例属性 / 办法、二是原型属性 / 办法、三是动态属性 / 办法,咱们别离来看。

先来看一下咱们要继承的父类的函数:

// 父类
function Sup(name) {this.name = name// 实例属性}
Sup.type = '午'// 动态属性
// 静态方法
Sup.sleep =  function () {console.log(` 我在睡 ${this.type}觉 `)
}
// 实例办法
Sup.prototype.say = function() {console.log('我叫' + this.name)
}

继承实例属性 / 办法

要继承实例属性 / 办法,显著要执行一下 Sup 函数才行,并且要批改它的 this 指向,这应用 callapply 办法都行:

// 子类
function Sub(name, age) {
    // 继承父类的实例属性
    Sup.call(this, name)
    // 本人的实例属性
    this.age = age
}

能这么做的原理又是另外一道经典面试题:new 操作符都做了什么 ,很简略,就4 点:

1. 创立一个空对象

2. 把该对象的 __proto__ 属性指向Sub.prototype

3. 让构造函数里的 this 指向新对象,而后执行构造函数,

4. 返回该对象

所以 Sup.call(this)this指的就是这个新创建的对象,那么就会把父类的实例属性 / 办法都增加到该对象上。

继承原型属性 / 办法

咱们都晓得如果一个对象它自身没有某个办法,那么会去它构造函数的原型对象上,也就是 __proto__ 指向的对象上查找,如果还没找到,那么会去构造函数原型对象的 __proto__ 上查找,这样一层一层往上,也就是传说中的原型链,所以 Sub 的实例想要能拜访到 Sup 的原型办法,就须要把 Sub.prototypeSup.prototype关联起来,这有几种办法:

1. 应用Object.create

Sub.prototype = Object.create(Sup.prototype)
Sub.prototype.constructor = Sub

2. 应用__proto__

Sub.prototype.__proto__ = Sup.prototype

3. 借用两头函数

function Fn() {}
Fn.prototype = Sup.prototype
Sub.prototype = new Fn()
Sub.prototype.constructor = Sub

以上三种办法都能够,咱们再来笼罩一下继承到的 Say 办法,而后在该办法外面再调用父类原型上的 say 办法:

Sub.prototype.say = function () {console.log('你好')
    // 调用父类的该原型办法
    // this.__proto__ === Sub.prototype、Sub.prototype.__proto__ === Sup.prototype
    this.__proto__.__proto__.say.call(this)
    console.log(` 往年 ${this.age}岁 `)
}

继承动态属性 / 办法

也就是继承 Sup 函数自身的属性和办法,这个很简略,遍历一下父类本身的可枚举属性,而后增加到子类上即可:

Object.keys(Sup).forEach((prop) => {Sub[prop] = Sup[prop]
})

ES6:应用 class 继承

接下来咱们应用 ES6class关键字来实现下面的例子:

// 父类
class Sup {constructor(name) {this.name = name}
    
    say() {console.log('我叫' + this.name)
    }
    
    static sleep() {console.log(` 我在睡 ${this.type}觉 `)
    }
}
// static 只能设置静态方法,不能设置动态属性,所以须要自行添加到 Sup 类上
Sup.type = '午'
// 另外,原型属性也不能在 class 外面设置,须要手动设置到 prototype 上,比方 Sup.prototype.xxx = 'xxx'

// 子类,继承父类
class Sub extends Sup {constructor(name, age) {super(name)
        this.age = age
    }
    
    say() {console.log('你好')
        super.say()
        console.log(` 往年 ${this.age}岁 `)
    }
}
Sub.type = '懒'

能够看到一样的成果,应用 class 会简洁明了很多,接下来咱们应用 babel 来把这段代码编译回 ES5 的语法,看看和咱们写的有什么不一样,因为编译完的代码有 200 多行,所以不能一次全副贴上来,咱们先从父类开始看:

编译后的父类

// 父类
var Sup = (function () {function Sup(name) {_classCallCheck(this, Sup);

    this.name = name;
  }

  _createClass(
    Sup,
    [
      {
        key: "say",
        value: function say() {console.log("我叫" + this.name);
        },
      },
    ],
    [
      {
        key: "sleep",
        value: function sleep() {console.log("\u6211\u5728\u7761".concat(this.type, "\u89C9"));
        },
      },
    ]
  );

  return Sup;
})(); // static 只能设置静态方法,不能设置动态属性

Sup.type = "午"; // 子类,继承父类
// 如果咱们之前通过 Sup.prototype.xxx = 'xxx' 设置了原型属性,那么跟动态属性一样,编译后没有区别,也是这么设置的

能够看到是个自执行函数,外面定义了一个 Sup 函数,Sup外面先调用了一个 _classCallCheck(this, Sup) 函数,咱们转到这个函数看看:

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

instanceof运算符是用来检测左边函数的 prototype 属性是否呈现在右边的对象的原型链上,简略说能够判断某个对象是否是某个构造函数的实例,能够看到如果不是的话就抛错了,错误信息是 不能把一个类当做函数调用,这里咱们就发现第一个区别了:

区别 1:ES5 里的构造函数就是一个一般的函数,能够应用 new 调用,也能够间接调用,而 ES6 的 class 不能当做一般函数间接调用,必须应用 new 操作符调用

持续看自执行函数,接下来调用了一个 _createClass 办法:

function _createClass(Constructor, protoProps, staticProps) {if (protoProps) _defineProperties(Constructor.prototype, protoProps);
  if (staticProps) _defineProperties(Constructor, staticProps);
  return Constructor;
}

该办法接管三个参数,别离是构造函数、原型办法、静态方法(留神不蕴含原型属性和动态属性),前面两个都是数组,数组外面每一项代表一个办法对象,不论是实例办法还是原型办法,都是通过 _defineProperties 办法设置,先来看该办法:

function _defineProperties(target, props) {for (var i = 0; i < props.length; i++) {var descriptor = props[i];
    // 设置该属性是否可枚举,设为 false 则 for..in、Object.keys 遍历不到该属性
    descriptor.enumerable = descriptor.enumerable || false;
    // 默认可配置,即能批改和删除该属性
    descriptor.configurable = true;
    // 设为 true 时该属性的值能被赋值运算符扭转
    if ("value" in descriptor) descriptor.writable = true;
    Object.defineProperty(target, descriptor.key, descriptor);
  }
}

能够看到它是通过 Object.defineProperty 办法来设置原型办法和静态方法,而且 enumerable 默认为false,这就来到了第二个区别:

区别 2:ES5 的原型办法和静态方法默认是可枚举的,而 class 的默认不可枚举,如果想要获取不可枚举的属性能够应用 Object.getOwnPropertyNames 办法

接下来看子类编译后的代码:

编译后的子类

// 子类,继承父类
var Sub = (function (_Sup) {_inherits(Sub, _Sup);

  var _super = _createSuper(Sub);

  function Sub(name, age) {
    var _this;

    _classCallCheck(this, Sub);

    _this = _super.call(this, name);
    _this.age = age;
    return _this;
  }

  _createClass(Sub, [
    {
      key: "say",
      value: function say() {console.log("你好");

        _get(_getPrototypeOf(Sub.prototype), "say", this).call(this);

        console.log("\u4ECA\u5E74".concat(this.age, "\u5C81"));
      }
    }
  ]);

  return Sub;
})(Sup);

Sub.type = "懒";

同样也是一个自执行办法,把要继承的父类构造函数作为参数传进去了,进来先调用了 _inherits(Sub, _Sup) 办法,尽管 Sub 函数是在前面定义的,然而函数申明是存在晋升的,所以这里是能够失常拜访到的:

function _inherits(subClass, superClass) {
  // 被继承对象的必须是一个函数或 null
  if (typeof superClass !== "function" && superClass !== null) {throw new TypeError("Super expression must either be null or a function");
  }
  // 设置原型
  subClass.prototype = Object.create(superClass && superClass.prototype, {constructor: { value: subClass, writable: true, configurable: true}
  });
  if (superClass) _setPrototypeOf(subClass, superClass);
}

这个办法先查看了父类是否非法,而后通过 Object.create 办法设置了子类的原型,这个和咱们之前的写法是一样的,只是明天我才发现 Object.create 竟然还有第二个参数,第二个参数必须是一个对象,对象的自有可枚举属性 (即其本身定义的属性,而不是其原型链上的枚举属性) 将为新创建的对象增加指定的属性值和对应的属性描述符。

这个办法的最初为咱们揭晓了第三个区别:

区别 3:子类能够间接通过 __proto__ 找到父类,而 ES5 是指向Function.prototype

ES6:Sub.__proto__ === Sup

ES5:Sub.__proto__ === Function.prototype

为啥会这样呢,看看 _setPrototypeOf 办法做了啥就晓得了:

function _setPrototypeOf(o, p) {
    _setPrototypeOf =
        Object.setPrototypeOf ||
        function _setPrototypeOf(o, p) {
            o.__proto__ = p;
            return o;
        };
    return _setPrototypeOf(o, p);
}

能够看到这个办法把 Sub.__proto__ 设置为了 Sup,这样同时也实现了静态方法和属性的继承,因为函数也是对象,本身没有的属性和办法也会沿着__proto__ 链查找。

_inherits办法过后紧接着调用了一个 _createSuper(Sub) 办法,拉进去看看:

function _createSuper(Derived) {return function _createSuperInternal() {// ...};
}

这个函数接管子类构造函数,而后返回了一个新函数,咱们先跳到前面的子类构造函数的定义:

function Sub(name, age) {
    var _this;

    // 查看是否当做一般函数调用,是的话抛错
    _classCallCheck(this, Sub);

    _this = _super.call(this, name);
    _this.age = age;
    return _this;
}

同样是先查看了一下是否是应用 new 调用,而后咱们发现这个函数返回了一个 _this,后面介绍了new 操作符都做了什么,咱们晓得会隐式创立一个对象,并且会把函数内的 this 指向该对象,如果没有显式的指定构造函数返回什么,那么就会默认返回这个新创建的对象,而这里显然是手动指定了要返回的对象,而这个 _this 来自于 _super 函数的执行后果,_super就是后面 _createSuper 返回的新函数:

function _createSuper(Derived) {
    // _isNativeReflectConstruct 会查看 Reflect.construct 办法是否可用
    var hasNativeReflectConstruct = _isNativeReflectConstruct();
    return function _createSuperInternal() {
        // _getPrototypeOf 办法用来获取 Derived 的原型,也就是 Derived.__proto__
        var Super = _getPrototypeOf(Derived),
            result;
        if (hasNativeReflectConstruct) {
            // NewTarget === Sub
            var NewTarget = _getPrototypeOf(this).constructor;
            // Reflect.construct 的操作能够简略了解为:result = new Super(...arguments),第三个参数如果传了则作为新创建对象的构造函数,也就是 result.__proto__ === NewTarget.prototype,否则默认为 Super.prototype
            result = Reflect.construct(Super, arguments, NewTarget);
        } else {result = Super.apply(this, arguments);
        }
        return _possibleConstructorReturn(this, result);
    };
}

Super代表的是 Sub.__proto__,依据后面的继承操作,咱们晓得子类的__proto__ 指向了父类,也就是 Sup,这里会优先应用Reflect.construct 办法,相当于创立了一个父类的实例,并且这个实例的 __proto__ 又指回了 Sub.prototype,不得不说这个api 真是神奇。

咱们就不思考降级状况了,那么最初会返回这个父类的实例对象。

回到 Sub 构造函数,_this指向的就是这个通过父类创立的实例对象,为什么要这么做呢,这其实就是第四个区别了,也是最重要的区别:

区别 4:ES5 的继承,本质是先发明子类的实例对象 this,而后再执行父类的构造函数给它增加实例办法和属性(不执行也无所谓)。而 ES6 的继承机制齐全不同,本质是先发明父类的实例对象this(当然它的__proto__ 指向的是子类的prototype),而后再用子类的构造函数批改this

这就是为啥应用 class 继承在 constructor 函数里必须调用 super,因为子类压根没有本人的this,另外不能在super 执行前拜访 this 的起因也很显著了,因为调用了 super 后,this才有值。

子类自执行函数的最初一部分也是给它设置原型办法和静态方法,这个后面讲过了,咱们重点看一下实例办法编译后的后果:

function say() {console.log("你好");

    _get(_getPrototypeOf(Sub.prototype), "say", this).call(this);

    console.log("\u4ECA\u5E74".concat(this.age, "\u5C81"));
}

猜你们也忘了编译前的原函数是啥样的了,请看:

say() {console.log('你好')
    super.say()
    console.log(` 往年 ${this.age}岁 `)
}

ES6classsuper 有两种含意,当做函数调用的话它代表父类的构造函数,只能在 constructor 外面调用,当做对象应用时它指向父类的原型对象,所以 _get(_getPrototypeOf(Sub.prototype), "say", this).call(this) 这行大略相当于 Sub.prototype.__proto__.say.call(this),跟咱们最开始写的ES5 版本也差不多,然而显然在 class 的语法要简略很多。

到此,编译后的代码咱们就剖析的差不多了,不过其实还有一个区别不晓得大家有没有发现,那就是为啥要应用自执行函数,一当然是为了封装一些变量,二其实是因为第五个区别:

区别 5:class 不存在变量晋升,所以父类必须在子类之前定义

不信你把父类放到子类前面试试,不出意外会报错,你可能会感觉间接应用函数表达式也能够达到这样的成果,非也:

// 会报错
var Sub = function(){ Sup.call(this) }
new Sub()
var Sup = function(){}

// 不会报错
var Sub = function(){ Sup.call(this) }
var Sup = function(){}
new Sub()

然而 Babel 编译后的无论你在哪里实例化子类,只有父类在它之后申明都会报错。

总结

本文通过剖析 Babel 编译后的代码来总结了 ES5ES6继承的 5 个区别,可能还有一些其余的,有趣味能够自行理解。

对于 class 的详细信息能够看这篇继承 class 继承。

示例代码在 https://github.com/wanglin2/es5-es5-inherit-example。

退出移动版