Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
JavaScript 在編程語言界是個特殊種類,它和其他編程語言很不一樣,JavaScript 可以在運行的時候動態(tài)地改變某個變量的類型。
比如你永遠也沒法想到像isTimeout這樣一個變量可以存在多少種類型,除了布爾值true和false,它還可能是undefined、1和0、一個時間戳,甚至一個對象。
如果代碼跑異常,打開瀏覽器,開始斷點調(diào)試,發(fā)現(xiàn)InfoList這個變量第一次被賦值的時候是個數(shù)組:
[{name: 'test1', value: '11'}, {name: 'test2', value: '22'}]
過了一會竟然變成了一個對象:
{test1:'11', test2: '22'}
除了變量可以在運行時被賦值為任何類型以外,JavaScript 中也能實現(xiàn)繼承,但它不像 Java、C++、C# 這些編程語言一樣基于類來實現(xiàn)繼承,而是基于原型進行繼承。
這是因為 JavaScript 中有個特殊的存在:對象。每個對象還都擁有一個原型對象,并可以從中繼承方法和屬性。
提到對象和原型,有如下問題:
在 JavaScript 中,對象由一組或多組的屬性和值組成:
{
key1: value1,
key2: value2,
key3: value3,
}
在 JavaScript 中,對象的用途很是廣泛,因為它的值既可以是原始類型(number、string、boolean、null、undefined、bigint和symbol),還可以是對象和函數(shù)。
不管是對象,還是函數(shù)和數(shù)組,它們都是Object的實例,也就是說在 JavaScript 中,除了原始類型以外,其余都是對象。
這也就解答了問題1:JavaScript 的函數(shù)怎么也是個對象?
在 JavaScript 中,函數(shù)也是一種特殊的對象,它同樣擁有屬性和值。所有的函數(shù)會有一個特別的屬性prototype,該屬性的值是一個對象,這個對象便是我們常說的“原型對象”。
我們可以在控制臺打印一下這個屬性:
function Person(name) {
this.name = name;
}
console.log(Person.prototype);
打印結(jié)果顯示為:
可以看到,該原型對象有兩個屬性:constructor和proto。
到這里,我們仿佛看到疑惑 “2:proto和prototype到底是啥關(guān)系?”的答案要出現(xiàn)了。在 JavaScript 中,proto屬性指向?qū)ο蟮脑蛯ο?,對于函?shù)來說,它的原型對象便是prototype。函數(shù)的原型對象prototype有以下特點:
我們可以用這樣一張圖來描述prototype、proto和constructor三個屬性的關(guān)系:
從這個圖中,我們可以找到這樣的關(guān)系:
對象之所以使用廣泛,是因為對象的屬性值可以為任意類型。因此,屬性的值同樣可以為另外一個對象,這意味著 JavaScript 可以這么做:通過將對象 A 的proto屬性賦值為對象 B,即:
A.__proto__ = B
此時使用A.proto便可以訪問 B 的屬性和方法。
這樣,JavaScript 可以在兩個對象之間創(chuàng)建一個關(guān)聯(lián),使得一個對象可以訪問另一個對象的屬性和方法,從而實現(xiàn)了繼承;
以Person為例,當(dāng)我們使用new Person()創(chuàng)建對象時,JavaScript 就會創(chuàng)建構(gòu)造函數(shù)Person的實例,比如這里我們創(chuàng)建了一個叫“zhangsan”的Person:
var zhangsan = new Person("zhangsan");
上述這段代碼在運行時,JavaScript 引擎通過將Person的原型對象prototype賦值給實例對象zhangsan的proto屬性,實現(xiàn)了zhangsan對Person的繼承,即執(zhí)行了以下代碼:
//JavaScript 引擎執(zhí)行了以下代碼
var zhangsan = {};
zhangsan.__proto__ = Person.prototype;
Person.call(zhangsan, "zhangsan");
我們來打印一下zhangsan實例:
console.log(zhangsan)
結(jié)果如下圖所示:
可以看到,zhangsan作為Person的實例對象,它的proto指向了Person的原型對象,即Person.prototype。
這時,我們再補充下上圖中的關(guān)系:
從這幅圖中,我們可以清晰地看到構(gòu)造函數(shù)和constructor屬性、原型對象(prototype)和proto、實例對象之間的關(guān)系,這是很多容易混淆。根據(jù)這張圖,我們可以得到以下的關(guān)系:
那么現(xiàn)在,關(guān)于proto和prototype的關(guān)系,我們可以得到這樣的答案:
所以一個對象可通過proto訪問原型對象上的屬性和方法,而該原型同樣也可通過proto訪問它的原型對象,這樣我們就在實例和原型之間構(gòu)造了一條原型鏈。紅色線條所示:
當(dāng) JavaScript 試圖訪問一個對象的屬性時,會基于原型鏈進行查找。查找的過程是這樣的:
我們可以通過一個具體的例子,來表示基于原型鏈的對象屬性的訪問過程,在該例子中我們構(gòu)建了一條對象的原型鏈,并進行屬性值的訪問:
var o = {a: 1, b: 2}; // 讓我們假設(shè)我們有一個對象 o, 其有自己的屬性 a 和 b:
o.__proto__ = {b: 3, c: 4}; // o 的原型 o.__proto__有屬性 b 和 c:
當(dāng)我們在獲取屬性值的時候,就會觸發(fā)原型鏈的查找:
console.log(o.a); // o.a => 1
console.log(o.b); // o.b => 2
console.log(o.c); // o.c => o.__proto__.c => 4
console.log(o.d); // o.c => o.__proto__.d => o.__proto__.__proto__ == null => undefined
綜上,整個原型鏈如下:
{a:1, b:2} ---> {b:3, c:4} ---> null, // 這就是原型鏈的末尾,即 null
可以看到,當(dāng)我們對對象進行屬性值的獲取時,會觸發(fā)該對象的原型鏈查找過程。
既然 JavaScript 中會通過遍歷原型鏈來訪問對象的屬性,那么我們可以通過原型鏈的方式進行繼承。
也就是說,可以通過原型鏈去訪問原型對象上的屬性和方法,我們不需要在創(chuàng)建對象的時候給該對象重新賦值/添加方法。比如,我們調(diào)用lily.toString()時,JavaScript 引擎會進行以下操作:
由于通過原型鏈進行屬性的查找,需要層層遍歷各個原型對象,此時可能會帶來性能問題:
因此,我們在設(shè)計對象的時候,需要注意代碼中原型鏈的長度。當(dāng)原型鏈過長時,可以選擇進行分解,來避免可能帶來的性能問題。
除了通過原型鏈的方式實現(xiàn) JavaScript 繼承,JavaScript 中實現(xiàn)繼承的方式還包括經(jīng)典繼承(盜用構(gòu)造函數(shù))、組合繼承、原型式繼承、寄生式繼承,等等。
function Parent(name) {
// 私有屬性,不共享
this.name = name;
}
// 需要復(fù)用、共享的方法定義在父類原型上
Parent.prototype.speak = function() {
console.log("hello");
};
function Child(name) {
Parent.call(this, name);
}
// 繼承方法
Child.prototype = new Parent();
組合繼承模式通過將共享屬性定義在父類原型上、將私有屬性通過構(gòu)造函數(shù)賦值的方式,實現(xiàn)了按需共享對象和方法,是 JavaScript 中最常用的繼承模式。
雖然在繼承的實現(xiàn)方式上有很多種,但實際上都離不開原型對象和原型鏈的內(nèi)容,因此掌握proto和prototype、對象的繼承等這些知識,是我們實現(xiàn)各種繼承方式的前提條件。
關(guān)于 JavaScript 的原型和繼承,常常會在我們面試題中出現(xiàn)。隨著 ES6/ES7 等新語法糖的出現(xiàn),可能更傾向于使用class/extends等語法來編寫代碼,原型繼承等概念逐漸變淡。
其次JavaScript 的設(shè)計在本質(zhì)上依然沒有變化,依然是基于原型來實現(xiàn)繼承的。如果不了解這些內(nèi)容,可能在我們遇到一些超出自己認知范圍的內(nèi)容時,很容易束手無策。
端開發(fā)者丨JavaScript
實際需求中開始
要求:
此類繼承自 Date,擁有Date的所有屬性和對象
此類可以自由拓展方法
形象點描述,就是要求可以這樣:
// 假設(shè)最終的類是 MyDate,有一個getTest拓展方法
let date = newMyDate();
// 調(diào)用Date的方法,輸出GMT絕對毫秒數(shù)
console.log(date.getTime());
// 調(diào)用拓展的方法,隨便輸出什么,譬如helloworld!
console.log(date.getTest());
于是,隨手用JS中經(jīng)典的組合寄生法寫了一個繼承,然后,剛準(zhǔn)備完美收工,一運行,卻出現(xiàn)了以下的情景:
但是的心情是這樣的: 囧
以前也沒有遇到過類似的問題,然后自己嘗試著用其它方法,多次嘗試,均無果(不算暴力混合法的情況),其實回過頭來看,是因為思路新奇,憑空想不到,并不是原理上有多難。。。
于是,借助強大的搜素引擎,搜集資料,最后,再自己總結(jié)了一番,才有了本文。
正文開始前,各位看官可以先暫停往下讀,嘗試下,在不借助任何網(wǎng)絡(luò)資料的情況下,是否能實現(xiàn)上面的需求?(就以 10分鐘為限吧)
借助stackoverflow上的回答。
先看看本文最開始時提到的經(jīng)典繼承法實現(xiàn),如下:
/**
* 經(jīng)典的js組合寄生繼承
*/
functionMyDate() {
Date.apply(this, arguments);
this.abc = 1;
}
functioninherits(subClass, superClass) {
functionInner() {}
Inner.prototype = superClass.prototype;
subClass.prototype = newInner();
subClass.prototype.constructor = subClass;
}
inherits(MyDate,Date);
MyDate.prototype.getTest = function() {
returnthis.getTime();
};
let date = newMyDate();
console.log(date.getTest());
就是這段代碼?,這也是JavaScript高程(紅寶書)中推薦的一種,一直用,從未失手,結(jié)果現(xiàn)在馬失前蹄。。。
我們再回顧下它的報錯:
再打印它的原型看看:
怎么看都沒問題,因為按照原型鏈回溯規(guī)則, Date的所有原型方法都可以通過 MyDate對象的原型鏈往上回溯到。再仔細看看,發(fā)現(xiàn)它的關(guān)鍵并不是找不到方法,而是 thisisnotaDateobject.
嗯哼,也就是說,關(guān)鍵是:由于調(diào)用的對象不是Date的實例,所以不允許調(diào)用,就算是自己通過原型繼承的也不行。
首先,看看 MDN上的解釋,上面有提到,JavaScript的日期對象只能通過 JavaScriptDate作為構(gòu)造函數(shù)來實例化。
然后再看看stackoverflow上的回答:
有提到, v8引擎底層代碼中有限制,如果調(diào)用對象的 [[Class]]不是 Date,則拋出錯誤。
總的來說,結(jié)合這兩點,可以得出一個結(jié)論:要調(diào)用Date上方法的實例對象必須通過Date構(gòu)造出來,否則不允許調(diào)用Date的方法。
雖然原因找到了,但是問題仍然要解決啊,真的就沒辦法了么?當(dāng)然不是,事實上還是有不少實現(xiàn)的方法的。
首先,說說說下暴力的混合法,它是下面這樣子的:
說到底就是:內(nèi)部生成一個 Date對象,然后此類暴露的方法中,把原有 Date中所有的方法都代理一遍,而且嚴(yán)格來說,這根本算不上繼承(都沒有原型鏈回溯)。
然后,再看看ES5中如何實現(xiàn)?
// 需要考慮polyfill情況
Object.setPrototypeOf = Object.setPrototypeOf ||
function(obj, proto) {
obj.__proto__ = proto;
returnobj;
};
/**
* 用了點技巧的繼承,實際上返回的是Date對象
*/
functionMyDate() {
// bind屬于Function.prototype,接收的參數(shù)是:object, param1, params2...
vardateInst =new(Function.prototype.bind.apply(Date, [Date].concat(Array.prototype.slice.call(arguments))))();
// 更改原型指向,否則無法調(diào)用MyDate原型上的方法
// ES6方案中,這里就是[[prototype]]這個隱式原型對象,在沒有標(biāo)準(zhǔn)以前就是__proto__
Object.setPrototypeOf(dateInst,MyDate.prototype);
dateInst.abc = 1;
returndateInst;
}
// 原型重新指回Date,否則根本無法算是繼承
Object.setPrototypeOf(MyDate.prototype,Date.prototype);
MyDate.prototype.getTest = functiongetTest() {
returnthis.getTime();
};
let date = newMyDate();
// 正常輸出,譬如1515638988725
console.log(date.getTest());
一眼看上去不知所措?沒關(guān)系,先看下圖來理解:(原型鏈關(guān)系一目了然)
可以看到,用的是非常巧妙的一種做法:
正常繼承的情況如下:
newMyDate()返回實例對象 date是由 MyDate構(gòu)造的
原型鏈回溯是: date(MyDate對象)-date.__proto__-MyDate.prototype-MyDate.prototype.__proto__-Date.prototype
這種做法的繼承的情況如下:
newMyDate()返回實例對象 date是由 Date構(gòu)造的
原型鏈回溯是: date(Date對象)-date.__proto__-MyDate.prototype-MyDate.prototype.__proto__-Date.prototype
可以看出,關(guān)鍵點在于:
構(gòu)造函數(shù)里返回了一個真正的 Date對象(由 Date構(gòu)造,所以有這些內(nèi)部類中的關(guān)鍵 [[Class]]標(biāo)志),所以它有調(diào)用 Date原型上方法的權(quán)利
構(gòu)造函數(shù)里的Date對象的 [[ptototype]](對外,瀏覽器中可通過 __proto__訪問)指向 MyDate.prototype,然后 MyDate.prototype再指向 Date.prototype。
所以最終的實例對象仍然能進行正常的原型鏈回溯,回溯到原本Date的所有原型方法。
這樣通過一個巧妙的欺騙技巧,就實現(xiàn)了完美的Date繼承。不過補充一點, MDN上有提到盡量不要修改對象的 [[Prototype]],因為這樣可能會干涉到瀏覽器本身的優(yōu)化。如果你關(guān)心性能,你就不應(yīng)該在一個對象中修改它的 [[Prototype]]
當(dāng)然,除了上述的ES5實現(xiàn),ES6中也可以直接繼承(自帶支持繼承 Date),而且更為簡單:
classMyDateextendsDate{
constructor() {
super();
this.abc = 1;
}
getTest() {
returnthis.getTime();
}
}
let date = newMyDate();
// 正常輸出,譬如1515638988725
console.log(date.getTest());
對比下ES5中的實現(xiàn),這個真的是簡單的不行,直接使用ES6的Class語法就行了。而且,也可以正常輸出。
注意:這里的正常輸出環(huán)境是直接用ES6運行,不經(jīng)過babel打包,打包后實質(zhì)上是轉(zhuǎn)化成ES5的,所以效果完全不一樣。
雖然說上述ES6大法是可以直接繼承Date的,但是,考慮到實質(zhì)上大部分的生產(chǎn)環(huán)境是: ES6+Babel
直接這樣用ES6 + Babel是會出問題的。
不信的話,可以自行嘗試下,Babel打包成ES5后代碼大致是這樣的:
然后當(dāng)信心滿滿的開始用時,會發(fā)現(xiàn):
對,又出現(xiàn)了這個問題,也許這時候是這樣的⊙?⊙
因為轉(zhuǎn)譯后的ES5源碼中,仍然是通過 MyDate來構(gòu)造,而 MyDate的構(gòu)造中又無法修改屬于 Date內(nèi)部的 [[Class]]之類的私有標(biāo)志,因此構(gòu)造出的對象仍然不允許調(diào)用 Date方法(調(diào)用時,被引擎底層代碼識別為 [[Class]]標(biāo)志不符合,不允許調(diào)用,拋出錯誤)。
由此可見,ES6繼承的內(nèi)部實現(xiàn)和Babel打包編譯出來的實現(xiàn)是有區(qū)別的。(雖說Babel的polyfill一般會按照定義的規(guī)范去實現(xiàn)的,但也不要過度迷信)。
雖然上述提到的三種方法都可以達到繼承 Date的目的-混合法嚴(yán)格說不能算繼承,只不過是另類實現(xiàn)。
于是,將所有能打印的主要信息都打印出來,分析幾種繼承的區(qū)別,大致場景是這樣的:
可以參考:( 請進入調(diào)試模式)https://dailc.github.io/fe-interview/demo/extends_date.html
從上往下, 1,2,3,4四種繼承實現(xiàn)分別是:(排出了混合法)
ES6的Class大法
經(jīng)典組合寄生繼承法
本文中的取巧做法,Date構(gòu)造實例,然后更改 __proto__的那種
ES6的Class大法,Babel打包后的實現(xiàn)(無法正常調(diào)用的)
~~~~以下是MyDate們的prototype~~~~~~~~~
Date{constructor: ?, getTest: ?}
Date{constructor: ?, getTest: ?}
Date{getTest: ?, constructor: ?}
Date{constructor: ?, getTest: ?}
~~~~以下是new出的對象~~~~~~~~~
SatJan13201821:58:55GMT+0800(CST)
MyDate2{abc:1}
SatJan13201821:58:55GMT+0800(CST)
MyDate{abc:1}
~~~~以下是new出的對象的Object.prototype.toString.call~~~~~~~~~
[objectDate]
[objectObject]
[objectDate]
[objectObject]
~~~~以下是MyDate們的__proto__~~~~~~~~~
?Date() { [native code] }
? () { [native code] }
? () { [native code] }
?Date() { [native code] }
~~~~以下是new出的對象的__proto__~~~~~~~~~
Date{constructor: ?, getTest: ?}
Date{constructor: ?, getTest: ?}
Date{getTest: ?, constructor: ?}
Date{constructor: ?, getTest: ?}
~~~~以下是對象的__proto__與MyDate們的prototype比較~~~~~~~~~
true
true
true
true
看出,主要差別有幾點:
MyDate們的proto指向不一樣
Object.prototype.toString.call的輸出不一樣
對象本質(zhì)不一樣,可以正常調(diào)用的 1,3都是 Date構(gòu)造出的,而其它的則是 MyDate構(gòu)造出的
我們上文中得出的一個結(jié)論是:由于調(diào)用的對象不是由Date構(gòu)造出的實例,所以不允許調(diào)用,就算是自己的原型鏈上有Date.prototype也不行
但是這里有兩個變量:分別是底層構(gòu)造實例的方法不一樣,以及對象的 Object.prototype.toString.call的輸出不一樣(另一個 MyDate.__proto__可以排除,因為原型鏈回溯肯定與它無關(guān))。
萬一它的判斷是根據(jù) Object.prototype.toString.call來的呢?那這樣結(jié)論不就有誤差了?
于是,根據(jù)ES6中的, Symbol.toStringTag,使用黑魔法,動態(tài)的修改下它,排除下干擾:
// 分別可以給date2,date3設(shè)置
Object.defineProperty(date2,Symbol.toStringTag, {
get:function() {
returnDate;
}
});
然后在打印下看看,變成這樣了:
[objectDate]
[objectDate]
[objectDate]
[objectObject]
可以看到,第二個的 MyDate2構(gòu)造出的實例,雖然打印出來是 [objectDate],但是調(diào)用Date方法仍然是有錯誤。
此時我們可以更加準(zhǔn)確一點的確認:由于調(diào)用的對象不是由Date構(gòu)造出的實例,所以不允許調(diào)用。
而且我們可以看到,就算通過黑魔法修改 Object.prototype.toString.call,內(nèi)部的 [[Class]]標(biāo)識位也是無法修改的。(這塊知識點大概是Object.prototype.toString.call可以輸出內(nèi)部的[[Class]],但無法改變它,由于不是重點,這里不贅述)。
從上午中的分析可以看到一點:ES6的Class寫法繼承是沒問題的。但是換成ES5寫法就不行了。
所以ES6的繼承大法和ES5肯定是有區(qū)別的,那么究竟是哪里不同呢?(主要是結(jié)合的本文繼承Date來說)
區(qū)別:(以 SubClass, SuperClass, instance為例)
ES5中繼承的實質(zhì)是:(那種經(jīng)典組合寄生繼承法)
先由子類( SubClass)構(gòu)造出實例對象this
然后在子類的構(gòu)造函數(shù)中,將父類( SuperClass)的屬性添加到 this上, SuperClass.apply(this,arguments)
子類原型( SubClass.prototype)指向父類原型( SuperClass.prototype)
所以 instance是子類( SubClass)構(gòu)造出的(所以沒有父類的 [[Class]]關(guān)鍵標(biāo)志)
所以, instance有 SubClass和 SuperClass的所有實例屬性,以及可以通過原型鏈回溯,獲取 SubClass和 SuperClass原型上的方法
ES6中繼承的實質(zhì)是:
先由父類( SuperClass)構(gòu)造出實例對象this,這也是為什么必須先調(diào)用父類的 super()方法(子類沒有自己的this對象,需先由父類構(gòu)造)
然后在子類的構(gòu)造函數(shù)中,修改this(進行加工),譬如讓它指向子類原型( SubClass.prototype),這一步很關(guān)鍵,否則無法找到子類原型(注,子類構(gòu)造中加工這一步的實際做法是推測出的,從最終效果來推測)
然后同樣,子類原型( SubClass.prototype)指向父類原型( SuperClass.prototype)
所以 instance是父類( SuperClass)構(gòu)造出的(所以有著父類的 [[Class]]關(guān)鍵標(biāo)志)
所以, instance有 SubClass和 SuperClass的所有實例屬性,以及可以通過原型鏈回溯,獲取 SubClass和 SuperClass原型上的方法
以上?就列舉了些重要信息,其它的如靜態(tài)方法的繼承沒有贅述。(靜態(tài)方法繼承實質(zhì)上只需要更改下 SubClass.__proto__到 SuperClass即可)
可以看著這張圖快速理解:
有沒有發(fā)現(xiàn)呢:ES6中的步驟和本文中取巧繼承Date的方法一模一樣,不同的是ES6是語言底層的做法,有它的底層優(yōu)化之處,而本文中的直接修改_proto_容易影響性能。
ES6中在super中構(gòu)建this的好處?
因為ES6中允許我們繼承內(nèi)置的類,如Date,Array,Error等。如果this先被創(chuàng)建出來,在傳給Array等系統(tǒng)內(nèi)置類的構(gòu)造函數(shù),這些內(nèi)置類的構(gòu)造函數(shù)是不認這個this的。所以需要現(xiàn)在super中構(gòu)建出來,這樣才能有著super中關(guān)鍵的 [[Class]]標(biāo)志,才能被允許調(diào)用。(否則就算繼承了,也無法調(diào)用這些內(nèi)置類的方法)
看到這里,不知道是否對上午中頻繁提到的構(gòu)造函數(shù),實例對象有所混淆與困惑呢?這里稍微描述下。
要弄懂這一點,需要先知道 new一個對象到底發(fā)生了什么?先形象點說:
functionMyClass() {
this.abc = 1;
}
MyClass.prototype.print = function() {
console.log('this.abc:'+this.abc);
};
let instance = newMyClass();
譬如,上述就是一個標(biāo)準(zhǔn)的實例對象生成,都發(fā)生了什么呢?
步驟簡述如下:(參考MDN,還有部分關(guān)于底層的描述略去-如[[Class]]標(biāo)識位等)
構(gòu)造函數(shù)內(nèi)部,創(chuàng)建一個新的對象,它繼承自 MyClass.prototype, letinstance=Object.create(MyClass.prototype);
使用指定的參數(shù)調(diào)用構(gòu)造函數(shù) MyClass,并將 this綁定到新創(chuàng)建的對象, MyClass.call(instance);,執(zhí)行后擁有所有實例屬性
如果構(gòu)造函數(shù)返回了一個“對象”,那么這個對象會取代整個 new出來的結(jié)果。如果構(gòu)造函數(shù)沒有返回對象,那么new出來的結(jié)果為步驟1創(chuàng)建的對象。 (一般情況下構(gòu)造函數(shù)不返回任何值,不過用戶如果想覆蓋這個返回值,可以自己選擇返回一個普通對象來覆蓋。當(dāng)然,返回數(shù)組也會覆蓋,因為數(shù)組也是對象。)
結(jié)合上述的描述,大概可以還原成以下代碼(簡單還原,不考慮各種其它邏輯):
let instance = Object.create(MyClass.prototype);
let innerConstructReturn = MyClass.call(instance);
let innerConstructReturnIsObj =typeofinnerConstructReturn ==='object'||typeofinnerConstructReturn ==='function';
returninnerConstructReturnIsObj ? innerConstructReturn : instance;
注意?:普通的函數(shù)構(gòu)建,可以簡單的認為就是上述步驟。實際上對于一些內(nèi)置類(如Date等),并沒有這么簡單,還有一些自己的隱藏邏輯,譬如 [[Class]]標(biāo)識位等一些重要私有屬性。譬如可以在MDN上看到,以常規(guī)函數(shù)調(diào)用Date(即不加 new 操作符)將會返回一個字符串,而不是一個日期對象,如果這樣模擬的話會無效。
覺得看起來比較繁瑣?可以看下圖梳理:
那現(xiàn)在再回頭看看。
什么是構(gòu)造函數(shù)?
如上述中的 MyClass就是一個構(gòu)造函數(shù),在內(nèi)部它構(gòu)造出了 instance對象。
什么是實例對象?
instance就是一個實例對象,它是通過 new出來的?
實例與構(gòu)造的關(guān)系
有時候淺顯點,可以認為構(gòu)造函數(shù)是xxx就是xxx的實例。即:
let instance = newMyClass();
此時我們就可以認為 instance是 MyClass的實例,因為它的構(gòu)造函數(shù)就是它。
不一定,我們那ES5黑魔法來做示例。
functionMyDate() {
// bind屬于Function.prototype,接收的參數(shù)是:object, param1, params2...
vardateInst =new(Function.prototype.bind.apply(Date, [Date].concat(Array.prototype.slice.call(arguments))))();
// 更改原型指向,否則無法調(diào)用MyDate原型上的方法
// ES6方案中,這里就是[[prototype]]這個隱式原型對象,在沒有標(biāo)準(zhǔn)以前就是__proto__
Object.setPrototypeOf(dateInst,MyDate.prototype);
dateInst.abc = 1;
returndateInst;
}
我們可以看到 instance的最終指向的原型是 MyDate.prototype,而 MyDate.prototype的構(gòu)造函數(shù)是 MyDate,因此可以認為 instance是 MyDate的實例。
但是,實際上, instance卻是由 Date構(gòu)造的,我們可以繼續(xù)用 ES6中的 new.target來驗證。
注意?:關(guān)于 new.target, MDN中的定義是:new.target返回一個指向構(gòu)造方法或函數(shù)的引用。
嗯哼,也就是說,返回的是構(gòu)造函數(shù)。
我們可以在相應(yīng)的構(gòu)造中測試打?。?/p>
classMyDateextendsDate{
constructor() {
super();
this.abc = 1;
console.log('~~~new.target.name:MyDate~~~~');
console.log(new.target.name);
}
}
// new操作時的打印結(jié)果是:
// ~~~new.target.name:MyDate~~~~
// MyDate
然后,可以在上面的示例中看到,就算是ES6的Class繼承, MyDate構(gòu)造中打印 new.target也顯示 MyDate,但實際上它是由 Date來構(gòu)造(有著 Date關(guān)鍵的 [[Class]]標(biāo)志,因為如果不是Date構(gòu)造(如沒有標(biāo)志)是無法調(diào)用Date的方法的)。
這也算是一次小小的勘誤吧。
所以,實際上用 new.target是無法判斷實例對象到底是由哪一個構(gòu)造構(gòu)造的(這里指的是判斷底層真正的 [[Class]]標(biāo)志來源的構(gòu)造)。
再回到結(jié)論:實例對象不一定就是由它的原型上的構(gòu)造函數(shù)構(gòu)造的,有可能構(gòu)造函數(shù)內(nèi)部有著寄生等邏輯,偷偷的用另一個函數(shù)來構(gòu)造了下,當(dāng)然,簡單情況下,我們直接說實例對象由對應(yīng)構(gòu)造函數(shù)構(gòu)造也沒錯(不過,在涉及到這種Date之類的分析時,我們還是得明白)。
這一部分為補充內(nèi)容。
前文中一直提到一個概念:Date內(nèi)部的 [[Class]]標(biāo)識。
其實,嚴(yán)格來說,不能這樣泛而稱之(前文中只是用這個概念是為了降低復(fù)雜度,便于理解),它可以分為以下兩部分:
在ES5中,每種內(nèi)置對象都定義了 [[Class]] 內(nèi)部屬性的值,[[Class]] 內(nèi)部屬性的值用于內(nèi)部區(qū)分對象的種類
Object.prototype.toString訪問的就是這個[[Class]]
規(guī)范中除了通過 Object.prototype.toString,沒有提供任何手段使程序訪問此值。
而且Object.prototype.toString輸出無法被修改
而在ES5中,之前的 [[Class]] 不再使用,取而代之的是一系列的 internalslot
Internal slot 對應(yīng)于與對象相關(guān)聯(lián)并由各種ECMAScript規(guī)范算法使用的內(nèi)部狀態(tài),它們沒有對象屬性,也不能被繼承
根據(jù)具體的 Internal slot 規(guī)范,這種狀態(tài)可以由任何ECMAScript語言類型或特定ECMAScript規(guī)范類型值的值組成
通過 Object.prototype.toString,仍然可以輸出Internal slot值
簡單點理解(簡化理解),Object.prototype.toString的流程是:如果是基本數(shù)據(jù)類型(除去Object以外的幾大類型),則返回原本的slot,如果是Object類型(包括內(nèi)置對象以及自己寫的對象),則調(diào)用 Symbol.toStringTag。 Symbol.toStringTag方法的默認實現(xiàn)就是返回對象的Internal slot,這個方法可以被重寫
這兩點是有所差異的,需要區(qū)分(不過簡單點可以統(tǒng)一理解為內(nèi)置對象內(nèi)部都有一個特殊標(biāo)識,用來區(qū)分對應(yīng)類型-不符合類型就不給調(diào)用)。
JS內(nèi)置對象是這些:
Arguments,Array,Boolean,Date,Error,Function,JSON,Math,Number,Object,RegExp,String
ES6新增的一些,這里未提到:(如Promise對象可以輸出 [objectPromise]),而前文中提到的:
Object.defineProperty(date,Symbol.toStringTag, {
get:function() {
returnDate;
}
});
它的作用是重寫Symbol.toStringTag,截取date(雖然是內(nèi)置對象,但是仍然屬于Object)的 Object.prototype.toString的輸出,讓這個對象輸出自己修改后的 [objectDate]。
但是,僅僅是做到輸出的時候變成了Date,實際上內(nèi)部的 internalslot值并沒有被改變,因此仍然不被認為是Date。
其實,在判斷繼承時,沒有那么多的技巧,就只有關(guān)鍵的一點: [[prototype]]( __ptoto__)的指向關(guān)系。
譬如:
console.log(instanceinstanceofSubClass);
console.log(instanceinstanceofSuperClass);
實質(zhì)上就是:
SubClass.prototype是否出現(xiàn)在 instance的原型鏈上
SuperClass.prototype是否出現(xiàn)在 instance的原型鏈上
然后,對照本文中列舉的一些圖,一目了然就可以看清關(guān)系。有時候,完全沒有必要弄的太復(fù)雜。
覺得本文對你有幫助?請分享給更多人
前端開發(fā)者丨JavaScript
S雖然不像是JAVA那種強類型的語言,但也有著與JAVA類型的繼承屬性,那么JS中的繼承是如何實現(xiàn)的呢?
一、構(gòu)造函數(shù)繼承
在構(gòu)造函數(shù)中,同樣屬于兩個新創(chuàng)建的函數(shù),也是不相等的 function Fn(name){ this.name = name; this.show = function(){ alert(this.name); } } var obj1 = new Fn("AAA"); var obj2 = new Fn("BBB"); console.log(obj1.show==obj2.show); //false 此時可以看出構(gòu)造函數(shù)的多次創(chuàng)建會產(chǎn)生多個相同函數(shù),造成冗余太多。 利用原型prototype解決。首先觀察prototype是什么東西 function Fn(){} console.log(Fn.prototype); //constructor表示當(dāng)前的函數(shù)屬于誰 //__proto__ == [[prototype]],書面用語,表示原型指針 var fn1 = new Fn(); var fn2 = new Fn(); Fn.prototype.show = function(){ alert(1); } console.log(fn1.show==fn2.show); //ture
此時,任何一個對象的原型上都有了show方法,由此得出,構(gòu)造函數(shù)Fn.prototype身上的添加的方法,相當(dāng)于添加到了所有的Fn身上。
二、call和applay繼承
function Father(skill){ this.skill = skill; this.show = function(){ alert("我會"+this.skill); } } var father = new Father("絕世木匠"); function Son(abc){ //這里的this指向函數(shù)Son的實例化對象 //將Father里面的this改變成指向Son的實例化對象,當(dāng)相遇將father里面所有的屬性和方法都復(fù)制到了son身上 //Father.call(this,abc);//繼承結(jié)束,call適合固定參數(shù)的繼承 //Father.apply(this,arguments);//繼承結(jié)束,apply適合不定參數(shù)的繼承 } father.show() var son = new Son("一般木匠"); son.show();
三、原型鏈繼承(demo)
這個的么實現(xiàn)一個一個簡單的拖拽,a->b的一個繼承。把a的功能繼承給b。
HTML:
<div id="drag1"></div> <div id="drag2"></div>
CSS:
*{margin: 0;padding: 0;} #drag1{width: 100px;height: 100px;background: red;position: absolute;} #drag2{width: 100px;height: 100px;background: black;position: absolute;left: 500px;}
JS:
function Drag(){} Drag.prototype={ constructor:Drag, init:function(id){ this.ele=document.getElementById(id); this.cliW=document.documentElement.clientWidth||document.body.clientWidth; this.cliH=document.documentElement.clientHeight||document.body.clientHeight; var that=this; this.ele.onmousedown=function(e){ var e=event||window.event; that.disX=e.offsetX; that.disY=e.offsetY; document.onmousemove=function(e){ var e=event||window.event; that.move(e); } that.ele.onmouseup=function(){ document.onmousemove=null; } } }, move:function(e){ this.x=e.clientX-this.disX; this.y=e.clientY-this.disY; this.x=this.x<0?this.x=0:this.x; this.y=this.y<0?this.y=0:this.y; this.x=this.x>this.cliW-this.ele.offsetWidth?this.x=this.cliW-this.ele.offsetWidth:this.x; this.y=this.y>this.cliH-this.ele.offsetHeight?this.y=this.cliH-this.ele.offsetHeight:this.y; this.ele.style.left=this.x+'px'; this.ele.style.top=this.y+'px'; } } new Drag().init('drag1') function ChidrenDrag(){} ChidrenDrag.prototype=new Drag() new ChidrenDrag().init('drag2')
四、混合繼承
function Father(skill,id){ this.skill = skill; this.id = id; } Father.prototype.show = function(){ alert("我是father,這是我的技能"+this.skill); } function Son(){ Father.apply(this,arguments); } //如果不做son的原型即成father的原型,此時會報錯:son.show is not a function Son.prototype = Father.prototype; //因為,如果不讓son的原型等于father的原型,son使用apply是繼承不到原型上的方法 //但這是一種錯誤的原型繼承示例,如果使用這種方式,會導(dǎo)致修改son原型上的show方法時,會把father身上的show也修改 //內(nèi)存的堆和棧機制 Son.prototype.show = function(){ alert("我是son,這是我的技能"+this.skill); } var father = new Father("專家級鐵匠","father"); var son = new Son("熟練級鐵匠","son"); father.show(); son.show(); 上面的示例應(yīng)該修改成以下形式: 以上紅色的代碼應(yīng)改成: for(var i in Father.prototype){ Son.prototype[i] = Father.prototype[i]; } //遍歷father的原型身上的所有方法,依次拷貝給son的原型,這種方式稱為深拷貝 這種繼承方式叫做混合繼承,用到了for-in繼承,cell和apple繼承。
五、Es6的class繼承(demo)
這個demo的功能和原型鏈繼承的demo功能一樣,a->b的繼承
HTML:
<div id="drag1"></div> <div id="drag2"></div>
CSS:
*{margin: 0;padding: 0;} #drag1{width: 100px;height: 100px;background: red;position: absolute;} #drag2{width: 100px;height: 100px;background: black;position: absolute;left: 500px;}
JS:
class Drag{ constructor(id){ this.ele=document.getElementById(id); this.init(); }; init(){ var that=this; this.ele.onmousedown=function(e){ var e=event||window.event; that.disX=e.offsetX; that.disY=e.offsetY; document.onmousemove=function(e){ var e=event||window.event; that.move(e); } that.ele.onmouseup=function(){ document.onmousemove=null; that.ele.onmouseup=null; } } }; move(e){ this.ele.style.left=e.clientX-this.disX+"px"; this.ele.style.top=e.clientY-this.disY+"px"; } } new Drag("drag1"); class ExtendsDrag extends Drag{ constructor(id){ super(id); } } new ExtendsDrag("drag2")
我總結(jié)的這幾種繼承方法.兩個demo繼承的方法大家最好在編譯器上跑一下,看看。這樣才能更深刻的去理解。尤其是原型鏈的繼承,js作為一個面向?qū)ο蟮木幊陶Z言,還是很常用的。
對前端的技術(shù),架構(gòu)技術(shù)感興趣的同學(xué)關(guān)注我的頭條號,并在后臺私信發(fā)送關(guān)鍵字:“前端”即可獲取免費的架構(gòu)師學(xué)習(xí)資料
知識體系已整理好,歡迎免費領(lǐng)取。還有面試視頻分享可以免費獲取。關(guān)注我,可以獲得沒有的架構(gòu)經(jīng)驗哦!!
*請認真填寫需求信息,我們會在24小時內(nèi)與您取得聯(lián)系。