如果以前問我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指向,這使用call、apply方法都行:
// 子類
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.prototype和Sup.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繼承
接下來我們使用ES6的class關鍵字來實作上面的例子:
// 父類
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__ === SupES5:
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}歲`)
}
在ES6的class里super有兩種含義,當做函式呼叫的話它代表父類的建構式,只能在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編譯后的代碼來總結了ES5和ES6繼承的5個區別,可能還有一些其他的,有興趣可以自行了解,
關于class的詳細資訊可以看這篇繼承class繼承,
示例代碼在https://github.com/wanglin2/es5-es5-inherit-example,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/296365.html
標籤:JavaScript
下一篇:js原生搜索框自動搜索
