筆者最近在對(duì)原生JS的知識(shí)做系統(tǒng)梳理,因?yàn)槲矣X(jué)得JS作為前端工程師的根本技術(shù),學(xué)再多遍都不為過(guò)。打算來(lái)做一個(gè)系列,一共分三次發(fā),以一系列的問(wèn)題為驅(qū)動(dòng),當(dāng)然也會(huì)有追問(wèn)和擴(kuò)展,內(nèi)容系統(tǒng)且完整,對(duì)初中級(jí)選手會(huì)有很好的提升,高級(jí)選手也會(huì)得到復(fù)習(xí)和鞏固。敬請(qǐng)大家關(guān)注!
第一篇: JS數(shù)據(jù)類型之問(wèn)——概念篇
1.JS原始數(shù)據(jù)類型有哪些?引用數(shù)據(jù)類型有哪些?
在 JS 中,存在著 7 種原始值,分別是:
- boolean
- null
- undefined
- number
- string
- symbol
- bigint
引用數(shù)據(jù)類型:
對(duì)象Object(包含普通對(duì)象-Object,數(shù)組對(duì)象-Array,正則對(duì)象-RegExp,日期對(duì)象-Date,數(shù)學(xué)函數(shù)-Math,函數(shù)對(duì)象-Function)
2.說(shuō)出下面運(yùn)行的結(jié)果,解釋原因。
function test(person) {
person.age = 26
person = {
name: 'hzj',
age: 18
}
return person
}
const p1 = {
name: 'fyq',
age: 19
}
const p2 = test(p1)
console.log(p1) // -> ?
console.log(p2) // -> ?
結(jié)果:
p1:{name: “fyq”, age: 26}
p2:{name: “hzj”, age: 18}
原因: 在函數(shù)傳參的時(shí)候傳遞的是對(duì)象在堆中的內(nèi)存地址值,test函數(shù)中的實(shí)參person是p1對(duì)象的內(nèi)存地址,通過(guò)調(diào)用person.age = 26確實(shí)改變了p1的值,但隨后person變成了另一塊內(nèi)存空間的地址,并且在最后將這另外一份內(nèi)存空間的地址返回,賦給了p2。
3.null是對(duì)象嗎?為什么?
結(jié)論: null不是對(duì)象。
解釋: 雖然 typeof null 會(huì)輸出 object,但是這只是 JS 存在的一個(gè)悠久 Bug。在 JS 的最初版本中使用的是 32 位系統(tǒng),為了性能考慮使用低位存儲(chǔ)變量的類型信息,000 開(kāi)頭代表是對(duì)象然而 null 表示為全零,所以將它錯(cuò)誤的判斷為 object 。
4.'1'.toString()為什么可以調(diào)用?
其實(shí)在這個(gè)語(yǔ)句運(yùn)行的過(guò)程中做了這樣幾件事情:
var s = new Object('1');
s.toString();
s = null;
第一步: 創(chuàng)建Object類實(shí)例。注意為什么不是String ? 由于Symbol和BigInt的出現(xiàn),對(duì)它們調(diào)用new都會(huì)報(bào)錯(cuò),目前ES6規(guī)范也不建議用new來(lái)創(chuàng)建基本類型的包裝類。
第二步: 調(diào)用實(shí)例方法。
第三步: 執(zhí)行完方法立即銷毀這個(gè)實(shí)例。
整個(gè)過(guò)程體現(xiàn)了基本包裝類型
的性質(zhì),而基本包裝類型恰恰屬于基本數(shù)據(jù)類型,包括Boolean, Number和String。
參考:《JavaScript高級(jí)程序設(shè)計(jì)(第三版)》P118
5.0.1+0.2為什么不等于0.3?
0.1和0.2在轉(zhuǎn)換成二進(jìn)制后會(huì)無(wú)限循環(huán),由于標(biāo)準(zhǔn)位數(shù)的限制后面多余的位數(shù)會(huì)被截掉,此時(shí)就已經(jīng)出現(xiàn)了精度的損失,相加后因浮點(diǎn)數(shù)小數(shù)位的限制而截?cái)嗟亩M(jìn)制數(shù)字在轉(zhuǎn)換為十進(jìn)制就會(huì)變成0.30000000000000004。
6.如何理解BigInt?
什么是BigInt?
BigInt是一種新的數(shù)據(jù)類型,用于當(dāng)整數(shù)值大于Number數(shù)據(jù)類型支持的范圍時(shí)。這種數(shù)據(jù)類型允許我們安全地對(duì)
大整數(shù)
執(zhí)行算術(shù)操作,表示高分辨率的時(shí)間戳,使用大整數(shù)id,等等,而不需要使用庫(kù)。
為什么需要BigInt?
在JS中,所有的數(shù)字都以雙精度64位浮點(diǎn)格式表示,那這會(huì)帶來(lái)什么問(wèn)題呢?
這導(dǎo)致JS中的Number無(wú)法精確表示非常大的整數(shù),它會(huì)將非常大的整數(shù)四舍五入,確切地說(shuō),JS中的Number類型只能安全地表示-9007199254740991(-(253-1))和9007199254740991((253-1)),任何超出此范圍的整數(shù)值都可能失去精度。
console.log(999999999999999); //=>10000000000000000
同時(shí)也會(huì)有一定的安全性問(wèn)題:
9007199254740992 === 9007199254740993; // → true 居然是true!
如何創(chuàng)建并使用BigInt?
要?jiǎng)?chuàng)建BigInt,只需要在數(shù)字末尾追加n即可。
console.log( 9007199254740995n ); // → 9007199254740995n
console.log( 9007199254740995 ); // → 9007199254740996
另一種創(chuàng)建BigInt的方法是用BigInt()構(gòu)造函數(shù)、
BigInt("9007199254740995"); // → 9007199254740995n
簡(jiǎn)單使用如下:
10n + 20n; // → 30n
10n - 20n; // → -10n
+10n; // → TypeError: Cannot convert a BigInt value to a number
-10n; // → -10n
10n * 20n; // → 200n
20n / 10n; // → 2n
23n % 10n; // → 3n
10n ** 3n; // → 1000n
const x = 10n;
++x; // → 11n
--x; // → 9n
console.log(typeof x); //"bigint"
值得警惕的點(diǎn)
BigInt不支持一元加號(hào)運(yùn)算符, 這可能是某些程序可能依賴于 + 始終生成 Number 的不變量,或者拋出異常。另外,更改 + 的行為也會(huì)破壞 asm.js代碼。
因?yàn)殡[式類型轉(zhuǎn)換可能丟失信息,所以不允許在bigint和 Number 之間進(jìn)行混合操作。當(dāng)混合使用大整數(shù)和浮點(diǎn)數(shù)時(shí),結(jié)果值可能無(wú)法由BigInt或Number精確表示。
10 + 10n; // → TypeError
- 不能將BigInt傳遞給Web api和內(nèi)置的 JS 函數(shù),這些函數(shù)需要一個(gè) Number 類型的數(shù)字。嘗試這樣做會(huì)報(bào)TypeError錯(cuò)誤。
Math.max(2n, 4n, 6n); // → TypeError
- 當(dāng) Boolean 類型與 BigInt 類型相遇時(shí),BigInt的處理方式與Number類似,換句話說(shuō),只要不是0n,BigInt就被視為truthy的值。
if(0n){//條件判斷為false
}
if(3n){//條件為true
}
元素都為BigInt的數(shù)組可以進(jìn)行sort。
BigInt可以正常地進(jìn)行位運(yùn)算,如|、&、<<、>>和^
瀏覽器兼容性
caniuse的結(jié)果:
其實(shí)現(xiàn)在的兼容性并不怎么好,只有chrome67、firefox、Opera這些主流實(shí)現(xiàn),要正式成為規(guī)范,其實(shí)還有很長(zhǎng)的路要走。
我們期待BigInt的光明前途!
第二篇: JS數(shù)據(jù)類型之問(wèn)——檢測(cè)篇
1. typeof 是否能正確判斷類型?
對(duì)于原始類型來(lái)說(shuō),除了 null 都可以調(diào)用typeof顯示正確的類型。
typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
但對(duì)于引用數(shù)據(jù)類型,除了函數(shù)之外,都會(huì)顯示"object"。
typeof [] // 'object'
typeof {} // 'object'
typeof console.log // 'function'
因此采用typeof判斷對(duì)象數(shù)據(jù)類型是不合適的,采用instanceof會(huì)更好,instanceof的原理是基于原型鏈的查詢,只要處于原型鏈中,判斷永遠(yuǎn)為true
const Person = function() {}
const p1 = new Person()
p1 instanceof Person // true
var str1 = 'hello world'
str1 instanceof String // false
var str2 = new String('hello world')
str2 instanceof String // true
2. instanceof能否判斷基本數(shù)據(jù)類型?
能。比如下面這種方式:
class PrimitiveNumber {
static [Symbol.hasInstance](x) {
return typeof x === 'number'
}
}
console.log(111 instanceof PrimitiveNumber) // true
如果你不知道Symbol,可以看看MDN上關(guān)于hasInstance的解釋。
其實(shí)就是自定義instanceof行為的一種方式,這里將原有的instanceof方法重定義,換成了typeof,因此能夠判斷基本數(shù)據(jù)類型。
3. 能不能手動(dòng)實(shí)現(xiàn)一下instanceof的功能?
核心: 原型鏈的向上查找。
function myInstanceof(left, right) {
//基本數(shù)據(jù)類型直接返回false
if(typeof left !== 'object' || left === null) return false;
//getProtypeOf是Object對(duì)象自帶的一個(gè)方法,能夠拿到參數(shù)的原型對(duì)象
let proto = Object.getPrototypeOf(left);
while(true) {
//查找到盡頭,還沒(méi)找到
if(proto == null) return false;
//找到相同的原型對(duì)象
if(proto == right.prototype) return true;
proto = Object.getPrototypeof(proto);
}
}
測(cè)試:
console.log(myInstanceof("111", String)); //false
console.log(myInstanceof(new String("111"), String));//true
4. Object.is和===的區(qū)別?
Object在嚴(yán)格等于的基礎(chǔ)上修復(fù)了一些特殊情況下的失誤,具體來(lái)說(shuō)就是+0和-0,NaN和NaN。
源碼如下:
function is(x, y) {
if (x === y) {
//運(yùn)行到1/x === 1/y的時(shí)候x和y都為0,但是1/+0 = +Infinity, 1/-0 = -Infinity, 是不一樣的
return x !== 0 || y !== 0 || 1 / x === 1 / y;
} else {
//NaN===NaN是false,這是不對(duì)的,我們?cè)谶@里做一個(gè)攔截,x !== x,那么一定是 NaN, y 同理
//兩個(gè)都是NaN的時(shí)候返回true
return x !== x && y !== y;
}
第三篇: JS數(shù)據(jù)類型之問(wèn)——轉(zhuǎn)換篇
1. [] == ![]結(jié)果是什么?為什么?
解析:
== 中,左右兩邊都需要轉(zhuǎn)換為數(shù)字然后進(jìn)行比較。
[]轉(zhuǎn)換為數(shù)字為0。
![] 首先是轉(zhuǎn)換為布爾值,由于[]作為一個(gè)引用類型轉(zhuǎn)換為布爾值為true,
因此![]為false,進(jìn)而在轉(zhuǎn)換成數(shù)字,變?yōu)?。
0 == 0 , 結(jié)果為true
2. JS中類型轉(zhuǎn)換有哪幾種?
JS中,類型轉(zhuǎn)換只有三種:
- 轉(zhuǎn)換成數(shù)字
- 轉(zhuǎn)換成布爾值
- 轉(zhuǎn)換成字符串
轉(zhuǎn)換具體規(guī)則如下:
注意"Boolean 轉(zhuǎn)字符串"這行結(jié)果指的是 true 轉(zhuǎn)字符串的例子
[圖片上傳失敗...(image-ce018a-1572492788692)]
3. == 和 ===有什么區(qū)別?
===叫做嚴(yán)格相等,是指:左右兩邊不僅值要相等,類型也要相等,例如'1'===1的結(jié)果是false,因?yàn)橐贿吺莝tring,另一邊是number。
==不像===那樣嚴(yán)格,對(duì)于一般情況,只要值相等,就返回true,但==還涉及一些類型轉(zhuǎn)換,它的轉(zhuǎn)換規(guī)則如下:
- 兩邊的類型是否相同,相同的話就比較值的大小,例如1==2,返回false
- 判斷的是否是null和undefined,是的話就返回true
- 判斷的類型是否是String和Number,是的話,把String類型轉(zhuǎn)換成Number,再進(jìn)行比較
- 判斷其中一方是否是Boolean,是的話就把Boolean轉(zhuǎn)換成Number,再進(jìn)行比較
- 如果其中一方為Object,且另一方為String、Number或者Symbol,會(huì)將Object轉(zhuǎn)換成字符串,再進(jìn)行比較
console.log({a: 1} == true);//false
console.log({a: 1} == "[object Object]");//true
4. 對(duì)象轉(zhuǎn)原始類型是根據(jù)什么流程運(yùn)行的?
對(duì)象轉(zhuǎn)原始類型,會(huì)調(diào)用內(nèi)置的[ToPrimitive]函數(shù),對(duì)于該函數(shù)而言,其邏輯如下:
- 如果Symbol.toPrimitive()方法,優(yōu)先調(diào)用再返回
- 調(diào)用valueOf(),如果轉(zhuǎn)換為原始類型,則返回
- 調(diào)用toString(),如果轉(zhuǎn)換為原始類型,則返回
- 如果都沒(méi)有返回原始類型,會(huì)報(bào)錯(cuò)
var obj = {
value: 3,
valueOf() {
return 4;
},
toString() {
return '5'
},
[Symbol.toPrimitive]() {
return 6
}
}
console.log(obj + 1); // 輸出7
5. 如何讓if(a == 1 && a == 2)條件成立?
其實(shí)就是上一個(gè)問(wèn)題的應(yīng)用。
var a = {
value: 0,
valueOf: function() {
this.value++;
return this.value;
}
};
console.log(a == 1 && a == 2);//true
第四篇: 談?wù)勀銓?duì)閉包的理解
什么是閉包?
紅寶書(shū)(p178)上對(duì)于閉包的定義:閉包是指有權(quán)訪問(wèn)另外一個(gè)函數(shù)作用域中的變量的函數(shù),
MDN 對(duì)閉包的定義為:閉包是指那些能夠訪問(wèn)自由變量的函數(shù)。
(其中自由變量,指在函數(shù)中使用的,但既不是函數(shù)參數(shù)arguments也不是函數(shù)的局部變量的變量,其實(shí)就是另外一個(gè)函數(shù)作用域中的變量。)
閉包產(chǎn)生的原因?
首先要明白作用域鏈的概念,其實(shí)很簡(jiǎn)單,在ES5中只存在兩種作用域————全局作用域和函數(shù)作用域,當(dāng)訪問(wèn)一個(gè)變量時(shí),解釋器會(huì)首先在當(dāng)前作用域查找標(biāo)示符,如果沒(méi)有找到,就去父作用域找,直到找到該變量的標(biāo)示符或者不在父作用域中,這就是作用域鏈
,值得注意的是,每一個(gè)子函數(shù)都會(huì)拷貝上級(jí)的作用域,形成一個(gè)作用域的鏈條。 比如:
var a = 1;
function f1() {
var a = 2
function f2() {
var a = 3;
console.log(a);//3
}
}
在這段代碼中,f1的作用域指向有全局作用域(window)和它本身,而f2的作用域指向全局作用域(window)、f1和它本身。而且作用域是從最底層向上找,直到找到全局作用域window為止,如果全局還沒(méi)有的話就會(huì)報(bào)錯(cuò)。就這么簡(jiǎn)單一件事情!
閉包產(chǎn)生的本質(zhì)就是,當(dāng)前環(huán)境中存在指向父級(jí)作用域的引用。還是舉上面的例子:
function f1() {
var a = 2
function f2() {
console.log(a);//2
}
return f2;
}
var x = f1();
x();
這里x會(huì)拿到父級(jí)作用域中的變量,輸出2。因?yàn)樵诋?dāng)前環(huán)境中,含有對(duì)f2的引用,f2恰恰引用了window、f1和f2的作用域。因此f2可以訪問(wèn)到f1的作用域的變量。
那是不是只有返回函數(shù)才算是產(chǎn)生了閉包呢?、
回到閉包的本質(zhì),我們只需要讓父級(jí)作用域的引用存在即可,因此我們還可以這么做:
var f3;
function f1() {
var a = 2
f3 = function() {
console.log(a);
}
}
f1();
f3();
讓f1執(zhí)行,給f3賦值后,等于說(shuō)現(xiàn)在f3擁有了window、f1和f3本身這幾個(gè)作用域的訪問(wèn)權(quán)限
,還是自底向上查找,最近是在f1
中找到了a,因此輸出2。
在這里是外面的變量f3存在著父級(jí)作用域的引用
,因此產(chǎn)生了閉包,形式變了,本質(zhì)沒(méi)有改變。
閉包有哪些表現(xiàn)形式?
明白了本質(zhì)之后,我們就來(lái)看看,在真實(shí)的場(chǎng)景中,究竟在哪些地方能體現(xiàn)閉包的存在?
- 返回一個(gè)函數(shù)。剛剛已經(jīng)舉例。
- 作為函數(shù)參數(shù)傳遞
var a = 1;
function foo(){
var a = 2;
function baz(){
console.log(a);
}
bar(baz);
}
function bar(fn){
// 這就是閉包
fn();
}
// 輸出2,而不是1
foo();
- 在定時(shí)器、事件監(jiān)聽(tīng)、Ajax請(qǐng)求、跨窗口通信、Web Workers或者任何異步中,只要使用了回調(diào)函數(shù),實(shí)際上就是在使用閉包。
以下的閉包保存的僅僅是window和當(dāng)前作用域。
// 定時(shí)器
setTimeout(function timeHandler(){
console.log('111');
},100)
// 事件監(jiān)聽(tīng)
$('#app').click(function(){
console.log('DOM Listener');
})
- IIFE(立即執(zhí)行函數(shù)表達(dá)式)創(chuàng)建閉包, 保存了
全局作用域window
和當(dāng)前函數(shù)的作用域
,因此可以全局的變量。
var a = 2;
(function IIFE(){
// 輸出2
console.log(a);
})();
如何解決下面的循環(huán)輸出問(wèn)題?
for(var i = 1; i <= 5; i ++){
setTimeout(function timer(){
console.log(i)
}, 0)
}
為什么會(huì)全部輸出6?如何改進(jìn),讓它輸出1,2,3,4,5?(方法越多越好)
因?yàn)閟etTimeout為宏任務(wù),由于JS中單線程eventLoop機(jī)制,在主線程同步任務(wù)執(zhí)行完后才去執(zhí)行宏任務(wù),因此循環(huán)結(jié)束后setTimeout中的回調(diào)才依次執(zhí)行,但輸出i的時(shí)候當(dāng)前作用域沒(méi)有,往上一級(jí)再找,發(fā)現(xiàn)了i,此時(shí)循環(huán)已經(jīng)結(jié)束,i變成了6。因此會(huì)全部輸出6。
解決方法:
1、利用IIFE(立即執(zhí)行函數(shù)表達(dá)式)當(dāng)每次for循環(huán)時(shí),把此時(shí)的i變量傳遞到定時(shí)器中
for(var i = 1;i <= 5;i++){
(function(j){
setTimeout(function timer(){
console.log(j)
}, 0)
})(i)
}
2、給定時(shí)器傳入第三個(gè)參數(shù), 作為timer函數(shù)的第一個(gè)函數(shù)參數(shù)
for(var i=1;i<=5;i++){
setTimeout(function timer(j){
console.log(j)
}, 0, i)
}
3、使用ES6中的let
for(let i = 1; i <= 5; i++){
setTimeout(function timer(){
console.log(i)
},0)
}
let使JS發(fā)生革命性的變化,讓JS有函數(shù)作用域變?yōu)榱藟K級(jí)作用域,用let后作用域鏈不復(fù)存在。代碼的作用域以塊級(jí)為單位,以上面代碼為例:
// i = 1
{
setTimeout(function timer(){
console.log(1)
},0)
}
// i = 2
{
setTimeout(function timer(){
console.log(2)
},0)
}
// i = 3
...
因此能輸出正確的結(jié)果。
第五篇: 談?wù)勀銓?duì)原型鏈的理解
1.原型對(duì)象和構(gòu)造函數(shù)有何關(guān)系?
在JavaScript中,每當(dāng)定義一個(gè)函數(shù)數(shù)據(jù)類型(普通函數(shù)、類)時(shí)候,都會(huì)天生自帶一個(gè)prototype屬性,這個(gè)屬性指向函數(shù)的原型對(duì)象。
當(dāng)函數(shù)經(jīng)過(guò)new調(diào)用時(shí),這個(gè)函數(shù)就成為了構(gòu)造函數(shù),返回一個(gè)全新的實(shí)例對(duì)象,這個(gè)實(shí)例對(duì)象有一個(gè)proto屬性,指向構(gòu)造函數(shù)的原型對(duì)象。
2.能不能描述一下原型鏈?
JavaScript對(duì)象通過(guò)prototype指向父類對(duì)象,直到指向Object對(duì)象為止,這樣就形成了一個(gè)原型指向的鏈條, 即原型鏈。
- 對(duì)象的 hasOwnProperty() 來(lái)檢查對(duì)象自身中是否含有該屬性
- 使用 in 檢查對(duì)象中是否含有某個(gè)屬性時(shí),如果對(duì)象中沒(méi)有但是原型鏈中有,也會(huì)返回 true
第六篇: JS如何實(shí)現(xiàn)繼承?
第一種: 借助call
function Parent1(){
this.name = 'parent1';
}
function Child1(){
Parent1.call(this);
this.type = 'child1'
}
console.log(new Child1);
這樣寫(xiě)的時(shí)候子類雖然能夠拿到父類的屬性值,但是問(wèn)題是父類原型對(duì)象中一旦存在方法那么子類無(wú)法繼承。那么引出下面的方法。
第二種: 借助原型鏈
function Parent2() {
this.name = 'parent2';
this.play = [1, 2, 3]
}
function Child2() {
this.type = 'child2';
}
Child2.prototype = new Parent2();
console.log(new Child2());
看似沒(méi)有問(wèn)題,父類的方法和屬性都能夠訪問(wèn),但實(shí)際上有一個(gè)潛在的不足。舉個(gè)例子:
var s1 = new Child2();
var s2 = new Child2();
s1.play.push(4);
console.log(s1.play, s2.play);
可以看到控制臺(tái):
[圖片上傳失敗...(image-e83e3d-1572492788692)]
明明我只改變了s1的play屬性,為什么s2也跟著變了呢?很簡(jiǎn)單,因?yàn)閮蓚€(gè)實(shí)例使用的是同一個(gè)原型對(duì)象。
那么還有更好的方式么?
第三種:將前兩種組合
function Parent3 () {
this.name = 'parent3';
this.play = [1, 2, 3];
}
function Child3() {
Parent3.call(this);
this.type = 'child3';
}
Child3.prototype = new Parent3();
var s3 = new Child3();
var s4 = new Child3();
s3.play.push(4);
console.log(s3.play, s4.play);
可以看到控制臺(tái):
[圖片上傳失敗...(image-2e3c13-1572492788692)]
之前的問(wèn)題都得以解決。但是這里又徒增了一個(gè)新問(wèn)題,那就是Parent3的構(gòu)造函數(shù)會(huì)多執(zhí)行了一次(Child3.prototype = new Parent3();)。這是我們不愿看到的。那么如何解決這個(gè)問(wèn)題?
第四種: 組合繼承的優(yōu)化1
function Parent4 () {
this.name = 'parent4';
this.play = [1, 2, 3];
}
function Child4() {
Parent4.call(this);
this.type = 'child4';
}
Child4.prototype = Parent4.prototype;
這里讓將父類原型對(duì)象直接給到子類,父類構(gòu)造函數(shù)只執(zhí)行一次,而且父類屬性和方法均能訪問(wèn),但是我們來(lái)測(cè)試一下:
var s3 = new Child4();
var s4 = new Child4();
console.log(s3)
[圖片上傳失敗...(image-413287-1572492788692)]
子類實(shí)例的構(gòu)造函數(shù)是Parent4,顯然這是不對(duì)的,應(yīng)該是Child4。
第五種(最推薦使用): 組合繼承的優(yōu)化1
function Parent5 () {
this.name = 'parent5';
this.play = [1, 2, 3];
}
function Child5() {
Parent5.call(this);
this.type = 'child5';
}
Child5.prototype = Object.create(Parent5.prototype);
Child5.prototype.constructor = Child5;
這是最推薦的一種方式,接近完美的繼承,它的名字也叫做寄生組合繼承。
ES6的extends被編譯后的JavaScript代碼
ES6的代碼最后都是要在瀏覽器上能夠跑起來(lái)的,這中間就利用了babel這個(gè)編譯工具,將ES6的代碼編譯成ES5讓一些不支持新語(yǔ)法的瀏覽器也能運(yùn)行。
那最后編譯成了什么樣子呢?
function _possibleConstructorReturn (self, call) {
// ...
return call && (typeof call === 'object' || typeof call === 'function') ? call : self;
}
function _inherits (subClass, superClass) {
// ...
//看到?jīng)]有
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;
}
var Parent = function Parent () {
// 驗(yàn)證是否是 Parent 構(gòu)造出來(lái)的 this
_classCallCheck(this, Parent);
};
var Child = (function (_Parent) {
_inherits(Child, _Parent);
function Child () {
_classCallCheck(this, Child);
return _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).apply(this, arguments));
}
return Child;
}(Parent));
核心是_inherits函數(shù),可以看到它采用的依然也是第五種方式————寄生組合繼承方式,同時(shí)證明了這種方式的成功。不過(guò)這里加了一個(gè)Object.setPrototypeOf(subClass, superClass),這是用來(lái)干啥的呢?
答案是用來(lái)繼承父類的靜態(tài)方法。這也是原來(lái)的繼承方式疏忽掉的地方。
追問(wèn): 面向?qū)ο蟮脑O(shè)計(jì)一定是好的設(shè)計(jì)嗎?
不一定。從繼承的角度說(shuō),這一設(shè)計(jì)是存在巨大隱患的。
從設(shè)計(jì)思想上談?wù)劺^承本身的問(wèn)題
假如現(xiàn)在有不同品牌的車,每輛車都有drive、music、addOil這三個(gè)方法。
class Car{
constructor(id) {
this.id = id;
}
drive(){
console.log("wuwuwu!");
}
music(){
console.log("lalala!")
}
addOil(){
console.log("哦喲!")
}
}
class otherCar extends Car{}
現(xiàn)在可以實(shí)現(xiàn)車的功能,并且以此去擴(kuò)展不同的車。
但是問(wèn)題來(lái)了,新能源汽車也是車,但是它并不需要addOil(加油)。
如果讓新能源汽車的類繼承Car的話,也是有問(wèn)題的,俗稱"大猩猩和香蕉"的問(wèn)題。大猩猩手里有香蕉,但是我現(xiàn)在明明只需要香蕉,卻拿到了一只大猩猩。也就是說(shuō)加油這個(gè)方法,我現(xiàn)在是不需要的,但是由于繼承的原因,也給到子類了。
繼承的最大問(wèn)題在于:無(wú)法決定繼承哪些屬性,所有屬性都得繼承。
當(dāng)然你可能會(huì)說(shuō),可以再創(chuàng)建一個(gè)父類啊,把加油的方法給去掉,但是這也是有問(wèn)題的,一方面父類是無(wú)法描述所有子類的細(xì)節(jié)情況的,為了不同的子類特性去增加不同的父類,代碼勢(shì)必會(huì)大量重復(fù)
,另一方面一旦子類有所變動(dòng),父類也要進(jìn)行相應(yīng)的更新,代碼的耦合性太高
,維護(hù)性不好。
那如何來(lái)解決繼承的諸多問(wèn)題呢?
用組合,這也是當(dāng)今編程語(yǔ)法發(fā)展的趨勢(shì),比如golang完全采用的是面向組合的設(shè)計(jì)方式。
顧名思義,面向組合就是先設(shè)計(jì)一系列零件,然后將這些零件進(jìn)行拼裝,來(lái)形成不同的實(shí)例或者類。
function drive(){
console.log("wuwuwu!");
}
function music(){
console.log("lalala!")
}
function addOil(){
console.log("哦喲!")
}
let car = compose(drive, music, addOil);
let newEnergyCar = compose(drive, music);
代碼干凈,復(fù)用性也很好。這就是面向組合的設(shè)計(jì)方式。
參考出處: