讀《三元-JS靈魂之問》總結,給本身的一份原生JS補給(上)

前言

你盼世界,我盼望你無bug。Hello 你們好!我是霖呆呆!javascript

幾個月前看過一遍三元大佬的《(建議收藏)原生JS靈魂之問, 請問你能接得住幾個?》系列,當時是利用上下班公交的時間刷的。說下那時的感覺吧,有些知識點還真不知道,就感受好牛批,確實有一種被靈魂拷問的感受。最最可怕的是那時候尚未意識到本身的基礎這麼差,只是死記着一些零散的知識點,沒幾天可能就忘了,總想着何時好好理一下本身的知識體系卻一直沒有付出行動。html

在某一個點上,多是被某篇文章刺激的,忽然讓個人心態發生了很大的改變。那種感受怎麼形容呢...就像是我覺得本身都懂,可是我還不知道本身不懂,而後我還每天期盼着明天會更好。有點以爲本身是井底之蛙吧。等我真正認清了本身以後才知道了沉澱這個詞的重要性。當我帶着'爲何會這樣?'、'還能夠怎樣?'、'若是這樣會怎樣?'的問題來回顧以前的一些知識,我發現本身要補充的真的還有不少...前端

這篇文章原本是本身近期再讀《三元-JS靈魂之問》作的一些筆記,可是發現越記越多...也所以引出了我寫的一系列文章,好比JS類型轉換系列JS繼承系列this等等。這裏對三元提到的一些題作一些補充說明,使它們變得更適合初中級的小夥伴閱讀吧,同時也是對本身這階段學習的一個鞏固,有寫的不對的地方還請各位大佬指出。vue

寫了一兩年的掘金還沒破Lv4,看來我要放大招了。在此立個flag,升Lv4後爆女裝"呆妹",把"她"放到下篇文章安排一波。已經很卑微了...java

(秉承着對原做者神三元的感謝之情寫的,還請三元的19177位粉絲不要誤會呀,我本身也是他的一名小粉絲...)git

霖呆呆的知識體系

全部文章均被收入gitHub「niubility-coding-js」中。github

第一補: JS類型基礎

1. '1'.toString()爲何能夠調用,1.toString()卻不行?

咱們知道若是在代碼中使用:面試

'1'.toString()
// 或者是
true.toString()
複製代碼

都是能夠正常調用的,這是由於toString它是Object.prototype上的方法,任何能訪問到Object原型的元素均可以調用它。數組

而在此處,對於'1'.toString()至關因而作了一層轉換,將其轉爲了一個"對象",這樣就能夠調用toString()方法了。瀏覽器

也就是這樣:

var s = new Object('1');
s.toString();
s = null;
複製代碼
  • 建立Object實例,將s變爲了String{"1"}對象
  • 調用Object.prototype上的實例方法toString()
  • 用完以後當即銷燬這個實例

這一部分三元分析的已經挺多了,我主要是想補充一下1.toString()爲何就不行。

當咱們在代碼中試圖使用1.toString(),發現編輯器已經報錯不容許咱們這樣作了。

最開始會有這麼奇怪的想法是由於咱們都忽視了一件事,那就是.它也是屬於數字裏的一部分啊 😂。

好比1.21.3。因此當你想要使用1.toString()的時候,JavaScript的解釋器會把它做爲數字的一部分,這樣就至關於(1.)toString了,很顯然這是一段錯誤的代碼。

既然這樣的話,若是我還給代碼一個.是否是就能夠了,因而我嘗試了一下:

console.log(1.1.toString())
複製代碼

發現它居然能正常打印出來:

"1.1"
複製代碼

這也就再次證實了1.toString()會將.歸給1所屬,而不是歸給toString()

固然若是你用的一個變量來承載這個數字的話也是能夠的:

var num = 1;
console.log(num.toString()) // "1"

// 或者
console.log((1).toString()) // "1"
複製代碼

2. 爲何可使用new Number卻不能使用new Symbol?

var num = new Number(1) // Number{1}
var str = new String('1') // String{'1'}
var bol = new Boolean(true) // Boolean{true}

var symbol = new Symbol(1) // TypeError
複製代碼

向上面這種使用new Number、new String等建立的基本數據類型被稱之爲:圍繞原始數據類型建立一個顯式包裝器對象

通俗點說就是:用new來建立基本類型的包裝類。

而這種作法在ES6以後就不被支持了,從new Symbol(1)報錯就能夠看出來,如今使用的是不帶new的方式:var symbol = Symbol(1),因此它做爲構造函數來講是不完整的。

可是由於歷史遺留的緣由,new Number仍然能夠這樣用,不過並不推薦。

若是你真的想建立一個 Symbol 包裝器對象 (Symbol wrapper object),你可使用 Object() 函數:

var sym = Symbol(1)
console.log(typeof sym) // "symbol"
var symObj = Object(sym)
console.log(typeof symObj) // "object"
複製代碼

第二補:JS類型檢測

1. instanceof可否判斷基本數據類型?

什麼意思呢 🤔️?

正常來講,instanceof是用來判斷某個對象的原型鏈上是否可以查找到某個構造函數的原型對象。

來看看通俗點的簡介:

a instanceof B

實例對象a instanceof 構造函數B

檢測a的原型鏈(__proto__)上是否有B.prototype,有則返回true,不然返回false

那麼它能夠用來判斷基本數據類型嗎?

也就是說我定義了一個var num = 1,我能夠用instanceof來判斷它是一個number類型的變量嗎?

若是你試圖這樣寫:

var num = 1;
console.log(num instanceof Number) // false
複製代碼

發現結果是false

此時你能夠用Symbol.hasInstance來實現一個自定義instanceof的行爲。

首先想一想咱們是要實現一個什麼功能?

a instanceof B
複製代碼

左側的a是一個變量,而右側的B是一個構造函數,而class的本質也是一個構造函數。

因此在這個需求中,咱們能夠定義一個叫作MyNumber的類,在其裏面封裝一層,暴露一個靜態方法用來判斷數據類型:

class MyNumber {
    static [Symbol.hasInstance](instance) {
        return typeof instance === 'number'
    }
}
var num = 1;
console.log(num instanceof MyNumber) // true
複製代碼
  • 在類MyNumber中定義了一個名爲Symbol.hasInstance的靜態方法
  • 這個方法接收的是一個實例對象instance
  • 返回值爲typeof判斷是不是number類型

這裏比較難理解的就是Symbol.hasInstance了,第一次接觸它也不知道它是個啥 😂。找了一波MDN 上對它的介紹:

用於判斷某對象是否爲某構造器的實例。

而後試着寫了幾個案例,發現也不用把它想的那麼複雜,你就簡單理解,當咱們在使用instanceof的時候,可以自定義右側構造函數(類)它的instanceof驗證方式就能夠了。

就像是上面👆那個案例同樣,我在MyNumber重寫了靜態方法Symbol.hasInstance,讓它的驗證方式變成type instance === 'number'

那麼爲何是靜態方法呢(也就是在方法前面加上static),經過閱讀《🔥【何不三連】比繼承家業還要簡單的JS繼承題-封裝篇(牛刀小試)》咱們知道,靜態方法是掛載在MyNumber這個類上的方法,所以咱們甚至能夠把下面的內容換一種寫法:

console.log(num instanceof MyNumber) // true
// 換成:
console.log(MyNumber[Symbol.hasInstance](num)) // true
複製代碼

看到了吧,它其實就是一個方法名而已,而這個方法由於是靜態的,在MyNumber上的,所以咱們能夠用MyNumber[Symbol.hasInstance]這種方式調用。

想一想,若是沒有static這個關鍵字呢?

沒有static的話,定義在類裏的方法就至關因而掛載到類的原型對象上,那麼若是咱們想要使用它,一種就是直接用MyNumber.prototype調用,還有一種就是使用new MyNumber()生成一個實例來調用:

class MyNumber {
    [Symbol.hasInstance](instance) { // 沒有 static
        return typeof instance === 'number'
    }
}
var num = 1

console.log(num instanceof new MyNumber()) // true
console.log(num instanceof MyNumber.prototype) // true

// 轉化爲:
console.log(MyNumber.prototype[Symbol.hasInstance](num)) // true
console.log(new MyNumber()[Symbol.hasInstance](num)) // true
複製代碼

因此如今回過頭來看看:

class MyNumber {
    static [Symbol.hasInstance](instance) {
        return typeof instance === 'number'
    }
}
var num = 1;
console.log(num instanceof MyNumber) // true
複製代碼

是否是就好理解多了呢?

那麼假如我如今想要你實現一個用instanceof判斷是否是數組的類MyArray,該如何去寫呢?

思考🤔...

唔...上答案:

class MyArray {  
  static [Symbol.hasInstance](instance) {
    return Array.isArray(instance);
  }
}
console.log([] instanceof MyArray); // true
複製代碼

2. instanceof的查找路線?

上面咱們說到了instanceof是用來判斷某個對象的原型鏈上是否可以查找到某個構造函數的原型對象。

而且是會沿着原型鏈一層一層的向上查找,直到到達原型鏈的末位。

那這個過程具體是怎樣的呢?讓咱們來看一個例子🌰:

function Parent () {
  this.name = 'parent'
}
function Child () {
  this.sex = 'boy'
}
Child.prototype = new Parent()
var child1 = new Child()

console.log(child1 instanceof Child)
console.log(child1 instanceof Parent)
console.log(child1 instanceof Object)
複製代碼

結果爲:

true
true
true
複製代碼

這裏其實用到了原型鏈繼承,Chind繼承於Parent,並且三個構造函數的原型對象都存在於child1的原型鏈上。

也就是說,左邊的child1它會向它的原型鏈中不停的查找,看有沒有右邊那個構造函數的原型對象。

例如child1 instanceof Child的查找順序:

child1 -> child1.__proto__ -> Child.prototype
複製代碼

child1 instanceof Parent的查找順序:

child1 -> child1.__proto__ -> Child.prototype
-> Child.prototype.__proto__ -> Parent.prototype
複製代碼

還不理解?

不要緊,我還有大招:

我在上面👆原型鏈繼承的思惟導圖上加了三個查找路線。

被⭕️標記的一、二、3分別表明的是Child、Parent、Object的原型對象。

好滴,一張圖簡潔明瞭。之後再碰到instanceof這種東西,按照我圖上的查找路線來查找就能夠了 😁 ~

3. isPropertypeOf()有什麼做用?

既然說到了instanceof,那麼就不得不提一下isPrototypeOf這個方法了。

它屬於Object.prototype上的方法,這點你能夠將Object.prototype打印在控制檯中看看。

isPrototypeOf()的用法和instanceof相反。

它是用來判斷指定對象object1是否存在於另外一個對象object2的原型鏈中,是則返回true,不然返回false

例如仍是上面👆這道題,咱們將要打印的內容改一下:

function Parent () {
  this.name = 'parent'
}
function Child () {
  this.sex = 'boy'
}
Child.prototype = new Parent()
var child1 = new Child()

console.log(Child.prototype.isPrototypeOf(child1))
console.log(Parent.prototype.isPrototypeOf(child1))
console.log(Object.prototype.isPrototypeOf(child1))
複製代碼

這裏輸出的依然是三個true

true
true
true
複製代碼

判斷的方式只要把原型鏈繼承instanceof查找思惟導圖這張圖反過來查找便可。

更多關於instanceOf的內容能夠戳這裏👇:

💦【何不三連】作完這48道題完全弄懂JS繼承(1.7w字含辛整理-返璞歸真)

4. Object.is()和===的區別?

  • 對於+0-0的判斷不一樣
  • 對於NaNNaN的判斷不一樣
console.log(+0 === -0) // true
console.log(Object.is(+0, -0)) // false
console.log(NaN === NaN) // false
console.log(Object.is(NaN, NaN)) // true
複製代碼

其實不須要特地的去記,你只須要理解,Object.is()===的基礎上修復了這些特殊狀況的失誤。

也就是說:+0-0本就該不相等的,這點從1 / +0Infinity1 / -0-Infinity上能夠看出。

(可是+00是沒有區別的)

NaN表示的都是非數字,因此應該是相等的。

所以Object.is()的內部實際上是作了一些修復的處理:

function is (x, y) {
  if (x === y) {
    return x !== 0 || 1 / x === 1 / y;
  } else {
    return x !== x && y !== y;
  }
}
複製代碼

這裏能夠怎樣理解呢 🤔️?

  • x === y的時候,就是用來處理+0, -0, 0的特殊狀況。

在這個判斷中,首先會判斷出x和y是否是都是+0, -0, 0中的其中一個(由於咱們知道不管是這三個中的哪個和0進行全等比較,結果都會是true,因此x !== 0就會是false),而根據||的短路原則,前面一項爲false,那麼最終的結果取決於後面一項。

因此x !== 0至關因而把x, y+0, -0, 0的狀況推給了1 / x === 1 / y,用它的結果來決定最後的結果。

(在三元的原文中這段判斷是這樣寫的return x !== 0 || y !== 0 || 1 /x === 1 / y,比我這裏多了一個|| y !== 0。經評論區小夥伴matteokjh提示,以爲能夠把|| y !== 0的判斷省略掉,另外MDN上對它的polyfill也是沒有這一步的,我想了一下:由於能進入到x === y裏,那麼若是x !== 0 成立的話,那麼y !== 0確定也成立了,貌似是能夠省去。另外我把省去的代碼執行了一下,和沒有省去時的執行結果同樣的,因此我感受可行。)

並且咱們又知道1 / +0Infinity1 / -0-Infinity,因此若是x, y+0或者-0的時候是不相等的,以此來實現Object.is(+0, -0)的結果爲false

  • x !== y的時候,就是用來處理NaN, NaN的特殊狀況。

由於咱們知道NaN !== NaN的,那麼若是xy都不和它們本身相等的話,說明兩個都是NaN了,而若是都是NaN的話,Object.is(NaN, NaN)的結果爲true

第三補:toString()方法的妙用

1. toString()存在於哪裏?

在此以前,我翻了不少關於toString()的資料,大多都是介紹了它的用法,可是它真正存在於哪裏呢?

可能比較常見的一種說法是它存在於Object的原型對象中,也就是Object.prototype上,那麼對於基本數據類型,Number、String、Boolean、 Symbol、BigInt呢?它們自身有這個方法嗎?或者它們的原型對象上有嗎?

本着一探到底的精神,我打印出了NumberNumber.prototype

console.log(Number)
console.log(Number.prototype)
複製代碼

而後我發現了幾件事:

  • Number只是一個構造函數,打印出來顯示的會是源代碼
  • Number.prototype上確實也有toString()
  • Number.prototype.__proto__也就是Object.prototype上也有toString()

而後我又試了一下String、Boolean、Symbol發現結果也和上面同樣。

其實不難理解,看過《💦【何不三連】作完這48道題完全弄懂JS繼承(1.7w字含辛整理-返璞歸真)》的小夥伴都知道,全部對象的原型鏈到最後都會指向Object.prototype,算是都"繼承"了Object的對象實例,所以都能使用toString()方法,可是對於不一樣的內置對象爲了能實現更適合自身的功能需求,都會重寫該方法,因此你能夠看到Number.prototype上也會有該方法。

因此咱們能夠先得出第一個結論:

  • 除了null、undefined之外的其它數據類型(基本數據類型+引用數據類型),它們構造函數的原型對象上都有toString()方法
  • 基本數據類型構造函數原型對象上的toString()會覆蓋Object原型對象上的toString()方法

(固然,等你看到後面你就會發現這種說法其實並不太準確,可是大多數時候咱們都只是關心誰能夠用它,而不是它存在於哪裏)

2. 誰能夠調用toString()?

這個問題,其實在上面👆已經給出答案了,全部對象除了null、undefined之外的任何值均可以調用toString()方法,一般狀況下它的返回結果和String同樣。

其實這裏,咱們最容易搞混的就是StringtoString

以前老是爲了將某個類型轉爲字符串胡亂的用這兩個屬性。

  • String是一個相似於Function這樣的對象,它既能夠當成對象來用,用它上面的靜態方法,也能夠當成一個構造函數來用,建立一個String對象
  • toString它是除了null、undefined以外的數據類型都有的方法,一般狀況下它的返回結果和String同樣。

3. 如何用toString()判斷某個數據的具體類型?

可能你們看的比較多的一種用法是這樣的:

Object.prototype.toString.call({ name: 'obj' }) // '[object Object]'
複製代碼

先來點硬知識,Object.prototype.toString這個方法會根據這個對象的[[class]]內部屬性,返回由 "[object " 和 class 和 "]" 三個部分組成的字符串。

啥意思?[[class]]內部屬性是個啥 🤔️?

這裏你還真別想多,你就按字面意思來理解它就行了,想一想,class 英文單詞的意思->

那好,我就認爲它表明的是一類事物就好了。

就好比

  • 數組是一類,它的[[class]]Array
  • 字符串是一類,它的[[class]]String
  • arguments是一類,它的[[class]]Arguments

另外,關於[[class]]的種類是很是多的,你也不須要記住所有,只須要知道一些經常使用的,基本的,好理解的就能夠了。

因此回到Object.prototype.toString.call()這種調用方式來,如今你能夠理解它的做用了吧,它可以幫助咱們準確的判斷某個數據類型,也就是辨別出是數組仍是數字仍是函數,仍是NaN。😊

另外鑑於它的返回結果是"[object Object]"這樣的字符串,並且前面的"[object ]"這八個字符串都是固定的(包括"t"後面的空格),因此咱們是否是能夠封裝一個方法來只拿到"Object"這樣的字符串呢?

很簡單,上代碼:

function getClass (obj) {
    let typeString = Object.prototype.toString.call(obj); // "[object Object]"
    return typeString.slice(8, -1);
}
複製代碼

能夠看到,我給這個函數命名爲getClass,這也就呼應了它本來的做用,是爲了拿到對象的[[class]]內部屬性。

另外,在拿到了"[object Object]"字符串以後,是用了一個.slice(8, -1)的字符串截取功能,去除了前八個字符"[object ]"和最後一個"]"

如今讓咱們來看看一些常見的數據類型吧:

function getClass(obj) {
    let typeString = Object.prototype.toString.call(obj); // "[object Array]"
    return typeString.slice(8, -1);
}
console.log(getClass(new Date)) // Date
console.log(getClass(new Map)) // Map
console.log(getClass(new Set)) // Set
console.log(getClass(new String)) // String
console.log(getClass(new Number)) // Number
console.log(getClass(true)) // Boolean
console.log(getClass(NaN)) // Number
console.log(getClass(null)) // Null
console.log(getClass(undefined)) // Undefined
console.log(getClass(Symbol(42))) // Symbol
console.log(getClass({})) // Object
console.log(getClass([])) // Array
console.log(getClass(function() {})) // Function
console.log(getClass(document.getElementsByTagName('p'))) // HTMLCollection

console.log(getClass(arguments)) // Arguments
複製代碼

"霖呆呆,這麼多,這是人乾的事嗎?"

"性平氣和,記住一些經常使用的就好了..."

"啪!"

4. toString.call()與typeof的區別?

好滴👌,經過剛剛的學習,咱們瞭解到了,toString.call這種方式是爲了獲取某個變量更加具體的數據類型。

咦~說到數據類型,咱們原來不是有一個typeof嗎?它和toString.call()又啥區別?

首先幫你們回顧一下typeof它的顯示規則:

  • 對於原始類型來講(也就是number、string這種),除了null均可以顯示正確的類型
  • null由於歷史版本的緣由被錯誤的判斷爲了"object"
  • 對於引用類型來講(也就是object、array這種),除了函數都會顯示爲"object"
  • 函數會被顯示爲function

因此呀,typeof的缺點很明顯啊,我如今有一個對象和一個數組,或者一個日期對象,我想要仔細的區分它,用typeof確定是不能實現的,由於它們獲得的都是"object"

因此,採用咱們封裝的getClass()顯然是一個很好的選擇。

5. 不一樣類型的數據調用toString()會怎麼樣?

在不一樣的數據類型調用toString()會有什麼不一樣呢?

這裏我主要是分爲兩大塊來講:

  • 基本數據類型調用
  • 引用類型調用

5.1 基本數據類型調用toString

對於基本數據類型來調用它,超級簡單的,你就想着就是把它的原始值換成了字符串而已:

console.log('1'.toString()) // '1'
console.log(1.1.toString()) // '1.1'
console.log(true.toString()) // 'true'
console.log(Symbol(1).toString()) // 'Symbol(1)'
console.log(10n.toString()) // '10'
複製代碼

因此對於基本數據類型:

  • 原始數據類型調用時,把它的原始值換成了字符串

5.2 引用類型調用toString

比較難的部分是引用類型調用toString(),並且咱們知道引用類型根據[[class]]的不一樣是分了不少類的,好比有ObjectArrayDate等等。

那麼不一樣類之間的toString()是否也不一樣呢 🤔️?

沒錯,不一樣版本的toString主要是分爲:

  • 數組的toString方法是將每一項轉換爲字符串而後再用","鏈接
  • 普通的對象(好比{name: 'obj'}這種)轉爲字符串都會變爲"[object Object]"
  • 函數(class)、正則會被轉爲源代碼字符串
  • 日期會被轉爲本地時區的日期字符串
  • 原始值的包裝對象調用toString會返回原始值的字符串
  • 擁有Symbol.toStringTag內置屬性的對象在調用時會變爲對應的標籤"[object Map]"

(Symbol.toStringTag屬性下一題會問到)

例如🌰:

console.log([].toString()) // ""
console.log([1, 2].toString()) // "1,2"

console.log({}.toString()) // "[object Object]"
console.log({name: 'obj'}.toString()) // "[object Object]"

console.log(class A {}.toString()) // "class A {}"
console.log(function () {}.toString()) // "function () {}"
console.log(/(\[|\])/g.toString()) // "/(\[|\])/g1"
console.log(new Date().toString()) // "Fri Mar 27 2020 12:33:16 GMT+0800 (中國標準時間)"

console.log(new Object(true).toString()) // "true"
console.log(new Object(1).toString()) // "1"
console.log(new Object(BigInt(10)).toString()) // "10"

console.log(new Map().toString()) // "[object Map]"
console.log(new Set().toString()) // "[object Set]"
複製代碼

6. 瞭解Symbol.toStringTag嗎?

《Symbol.toStringTag》上是這樣描述它的:

Symbol.toStringTag公知的符號是在建立對象的默認字符串描述中使用的字符串值屬性。它由該Object.prototype.toString()方法在內部訪問。

看不懂不要緊,你這樣理解就能夠了,它其實就是決定了剛剛咱們提到全部數據類型中[[class]]這個內部屬性是什麼。

好比數字,咱們前面獲得的[[class]]Number,那我就能夠理解爲數字這個類它的Symbol.toStringTag返回的就是Number

只不過在以前咱們用到的Number、String、Boolean中並無Symbol.toStringTag這個內置屬性,它是在咱們使用toString.call()調用的時候纔將其辨別返回。

而剛剛咱們剛剛在第五問看到的new Map(),讓咱們把它打印出來看看。

console.log(new Map())
複製代碼

能夠看到Symbol.toStringTag它是確確實實存在於Map.prototype上的,也就是說它是Map、Set內置的一個屬性,所以當咱們直接調用toString()的時候,就會返回"[object Map]"了。

額,咱們是否是就能夠這樣理解呢?

  • 沒有Symbol.toStringTag內置屬性的類型在調用toString()的時候至關因而String(obj)這樣調用轉換爲相應的字符串
  • Symbol.toStringTag內置屬性的類型在調用toString()的時候會返回相應的標籤(也就是"[object Map]"這樣的字符串)

咱們經常使用的帶有Symbol.toStringTag內置屬性的對象有:

console.log(new Map().toString()) // "[object Map]"
console.log(new Set().toString()) // "[object Set]"
console.log(Promise.resolve().toString()) // "[object Promise]"
複製代碼

而它最主要的功能就是和Symbol.hasInstance同樣,能夠容許咱們自定義標籤。

(Symbol.hasInsance的做用是自定義instanceof的返回值)

什麼是自定義標籤呢 🤔️?

也就是說,假如咱們如今建立了一個類,而且用toString.call()調用它的實例對象是會有以下結果:

class Super {}
console.log(Object.prototype.toString.call(new Super())) // "[object Object]"
複製代碼

很好理解,由於產生的new Super()是一個對象嘛,因此打印出的會是"[object Object]"

可是如今有了Symbol.toStringTag以後,咱們能夠改後面的"Object"

好比我重寫一下:

class Super {
  get [Symbol.toStringTag] () {
    return 'Validator'
  }
}
console.log(Object.prototype.toString.call(new Super())) // "[object Validator]"
複製代碼

這就是Symbol.toStringTag的厲害之處,它可以容許咱們自定義標籤。

可是有一點要注意了,Symbol.toStringTag重寫的是new Super()這個實例對象的標籤,而不是重寫Super這個類的標籤,也就是說這裏有區別的:

class Super {
  get [Symbol.toStringTag] () {
    return 'Validator'
  }
}
console.log(Object.prototype.toString.call(Super)) // "[object Function]"
console.log(Object.prototype.toString.call(new Super())) // "[object Validator]"
複製代碼

由於Super它自己仍是一個函數,只有Super產生的實例對象纔會用到咱們的自定義標籤。

總結一下Symbol.toStringTag

  • 它是某些特定類型的內置屬性,好比Map、Set、Promise
  • 主要做用是能夠容許咱們自定義標籤,修改Object.prototype.toString.call()的返回結果

更多關於toStringSymbol.toStringTag的內容能夠戳這裏👇:

【精】從206個console.log()徹底弄懂數據類型轉換的前世此生(上)

第四補:JS類型轉換

由於類型轉換算是讓人比較頭疼的一部分,因此對於這一塊我也專門寫了系列文章,基本上覆蓋了面試可能會問到的知識點,傳送門:

【精】從206個console.log()徹底弄懂數據類型轉換的前世此生(上)

【精】從206個console.log()徹底弄懂數據類型轉換的前世此生(下)

(下篇寫的挺好的沒人看難受😣)

1. 說一下valueOf()的基本用法

  • 基本數據類型調用,返回調用者本來的值
  • 非日期對象的其它引用類型調用valueOf()默認是返回它自己
  • 而日期對象會返回一個1970 年 1 月 1 日以來的毫秒數(相似於1585370128307)。

例子🌰:

console.log('1'.valueOf()) // '1'
console.log(1.1.valueOf()) // 1.1

console.log([].valueOf()) // []
console.log({}.valueOf()) // {}
console.log(['1'].valueOf()) // ['1']
console.log(function () {}.valueOf()) // ƒ () {}
console.log(/(\[|\])/g.valueOf()) // /(\[|\])/g
console.log(new Date().valueOf()) // 1585370128307
複製代碼

2. ToPrimitive的具體轉換流程?

當咱們在將對象轉換爲原始類型或者進行==比較的時候,會調用內置的ToPrimitive函數。

好比:

console.log(String({})) // 對象轉字符串,結果爲 "[object Object]"
console.log(Number([1, 2])) // 對象轉數字,結果爲 NaN

console.log([] == ![]) // true
複製代碼

以上結果的由來都通過了ToPrimitive函數。

先讓咱們來看看它的函數語法:

ToPrimitive(input, PreferredType?)
複製代碼

參數:

  • 參數一:input,表示要處理的輸入值
  • 參數二:PerferredType,指望轉換的類型,能夠看到語法後面有個問號,表示是非必填的。它只有兩個可選值,NumberString

而它對於傳入參數的處理是比較複雜的,讓咱們來看看流程圖:

根據流程圖,咱們得出了這麼幾個信息:

  1. 當不傳入 PreferredType 時,若是 input 是日期類型,至關於傳入 String,不然,都至關於傳入 Number。
  2. 若是是 ToPrimitive(obj, Number),處理步驟以下:
  • 若是 obj 爲 基本類型,直接返回
  • 不然,調用 valueOf 方法,若是返回一個原始值,則 JavaScript 將其返回。
  • 不然,調用 toString 方法,若是返回一個原始值,則 JavaScript 將其返回。
  • 不然,JavaScript 拋出一個類型錯誤異常。
  1. 若是是 ToPrimitive(obj, String),處理步驟以下:
  • 若是 obj爲 基本類型,直接返回
  • 不然,調用 toString 方法,若是返回一個原始值,則 JavaScript 將其返回。
  • 不然,調用 valueOf 方法,若是返回一個原始值,則 JavaScript 將其返回。
  • 不然,JavaScript 拋出一個類型錯誤異常。

(總結來源《冴羽-JavaScript深刻之頭疼的類型轉換(上)》)

上面👆的圖其實只是看着很複雜,細心的小夥伴可能會發現,在圖裏紅框裱起來的地方,只有toString()valueOf()方法的執行順序不一樣而已。

若是 PreferredType 是 String 的話,就先執行 toString()方法

若是 PreferredType 是 Number 的話,就先執行 valueOf()方法

(霖呆呆建議你先本身在草稿紙上將這幅流程圖畫一遍)

2.1 對象轉字符串

來點例子鞏固一下:

console.log(String({})) // "[object Object]"
複製代碼

對於這個簡單的轉換咱們能夠把它換成toPrimitive的僞代碼看看:

toPrimitive({}, 'string')
複製代碼

OK👌,來回顧一下剛剛的轉換規則:

  1. input{},是一個引用類型,PerferredTypestring
  2. 因此調用toString()方法,也就是{}.toString()
  3. {}.toString()的結果爲"[object Object]",是一個字符串,爲基本數據類型,而後返回,到此結束。

哇~

是否是一切都說得通了,好像不難吧 😁。

沒錯,當使用String()方法的時候,JS引擎內部的執行順序確實是這樣的,不過有一點和剛剛提到的步驟不同,那就是最後返回結果的時候,其實會將最後的基本數據類型再轉換爲字符串返回。

也就是說上面👆的第三步咱們得拆成兩步來:

  1. {}.toString()的結果爲"[object Object]",是一個字符串,爲基本數據類型
  2. 將這個"[object Object]"字符串再作一次字符串的轉換而後返回。(由於"[object Object]"已是字符串了,因此原樣返回,這裏看不出有什麼區別)

將最後的結果再轉換爲字符串返回這一步,其實很好理解啊。你想一想,我調用String方法那就是爲了獲得一個字符串啊,你要是給我返回一個number、null啊什麼的,那不是隔壁老王乾的事嘛~

2.2 對象轉數字

剛剛咱們說了對象轉字符串也就是toPrimitive(object, 'string')的狀況,

那麼對象轉數字就是toPrimitive(object, 'number')

區別就是轉數字會先調用valueOf()後調用toString()

例子🌰:

console.log(Number({}))
console.log(Number([]))
console.log(Number([0]))
console.log(Number([1, 2]))

console.log(Number(new Date()))
複製代碼

對於Number({})

  • 傳入的是一個對象{},所以調用valueOf()方法,該方法在題7.1中已經提到過了,它除了日期對象的其它引用類型調用都是返回它自己,因此這裏仍是返回了對象{}
  • valueOf()返回的值仍是對象,因此繼續調用toString()方法,而{}調用toString()的結果爲字符串"[object Object]",是一個基本數據類型
  • 獲得基礎數據類型了,該要返回了,不過在這以前還得將它在轉換爲數字才返回,那麼"[object Object]"轉爲數字爲NaN,因此結果爲NaN

對於Number([])

  • 傳入的是一個數組[],所以調用valueOf()方法,返回它自身[]
  • []繼續調用toString()方法,而空數組轉爲字符串是爲""
  • 最後再將空字符串""轉爲數字0返回

對於Number([0])

  • 由於[0]轉爲字符串是爲"0",最後在轉爲數字0 返回

對於Number([1, 2])

  • 傳入的是一個數組[1, 2],因此調用valueOf()方法返回的是數組自己[1,2]
  • 因此繼續調用toString()方法,此時被轉換爲了"1,2"字符串
  • "1,2"字符串最後被轉爲數字爲NaN,因此結果爲NaN

對於Number(new Date())

  • 傳入的是一個日期類型的對象new Date(),所以調用valueOf(),在題目7.2中已經說了,日期類型調用valueOf()是會返回一個毫秒數
  • 毫秒數爲數字類型,也就是基本數據類型,那麼直接返回(其實還有一步轉爲數字類型的過程),因此結果爲1585413652137

結果:

console.log(Number({})) // NaN
console.log(Number([])) // 0
console.log(Number([0])) // 0
console.log(Number([1, 2])) // NaN

console.log(Number(new Date())) // 1585413652137
複製代碼

3. 數組轉換爲字符串爲何會用","鏈接?

咱們都知道,當數組在進行轉字符串的時候,會把裏面的每一項都轉爲字符串而後再進行","拼接返回。

那麼爲何會有","拼接這一步呢?難道toString()在調用的時候還會調用join()方法嗎?

爲了驗證個人想法💡,我作了一個實驗,重寫了一個數組的join()方法:

var arr = [1, 2]
arr['join'] = function () {
  let target = [...this]
  return target.join('~')
}
console.log(String(arr))
複製代碼

重寫的join函數中,this表示的就是調用的這個數組arr

而後將返回值改成"~"拼接,結果答案居然是:

"1~2"
複製代碼

也就是說在String(arr)的過程當中,它確實是隱式調用了join方法。

可是當咱們重寫了toString()以後,就不會管這個重寫的join了:

var arr = [1, 2]
arr['toString'] = function () {
  let target = [...this]
  return target.join('*')
}
arr['join'] = function () {
  let target = [...this]
  return target.join('~')
}
console.log(String(arr)) // "1*2"
複製代碼

能夠看出toString()的優先級仍是比join()高的。

如今咱們又能夠得出一個結論:

對象若是是數組的話,當咱們不重寫其toString()方法,在轉換爲字符串類型的時候,默認實現就是將調用join()方法的返回值做爲toString()的返回值。

4. 知道Symbol.toPrimitive嗎?它有什麼用?

當咱們在進行對象轉原始值的時候,會隱式調用內部的ToPrimitive方法,按照前面說的那種方式進行轉換。

Symbol.toPrimitive屬性是能幫助咱們重寫toPrimitive,以此來更改轉換的結果。

讓咱們來看看它的總結:

  • 若是重寫了某個對象或者構造函數中的toString、valueOf、Symbol.toPrimitive方法,Symbol.toPrimitive的優先級是最高的
  • 如果Symbol.toPrimitive函數返回的值不是基礎數據類型(也就是原始值),就會報錯
  • Symbol.toPrimitive接收一個字符串參數hint,它表示要轉換到的原始值的預期類型,一共有'number'、'string'、'default'三種選項
  • 使用String()調用時,hint'string';使用Number()時,hint'number'
  • hint參數的值從開始調用的時候就已經肯定了

來看個例子🌰:

var b = {
  toString () {
    console.log('toString')
    return '1'
  },
  valueOf () {
    console.log('valueOf')
    return [1, 2]
  },
  [Symbol.toPrimitive] (hint) {
    console.log('symbol')
    if (hint === 'string') {
      console.log('string')
      return '1'
    }
    if (hint === 'number') {
      console.log('number')
      return 1
    }
    if (hint === 'default') {
      console.log('default')
      return 'default'
    }
  }
}
console.log(String(b))
console.log(Number(b))
複製代碼

這道題重寫了toString、valueOf、Symbol.toPrimitive三個屬性,經過上面👆的題目咱們已經知道了只要有Symbol.toPrimitive在,前面兩個屬性就被忽略了,因此咱們不用管它們。

而對於Symbol.toPrimitive,我將三種hint的狀況都寫上了,若是按照個人設想的話,在調用String(b)的時候應該是要打印出string的,調用Number(b)打印出number,結果也正如我所預想的同樣:

'string'
'1'
'number'
1
複製代碼

5. ==在進行比較時的類型轉換?

其實在實際中咱們被考的比較多的可能就是用==來比較判斷兩個不一樣類型的變量是否相等。

而全等===的狀況比較簡單,通常不太會考,由於全等的條件就是:若是類型相等值也相等才認爲是全等,並不會涉及到類型轉換。

可是==的狀況就相對複雜了,先給你們看幾個比較眼熟的題哈:

console.log([] == ![]) // true
console.log({} == true) // false
console.log({} == "[object Object]") // true
複製代碼

怎樣?這幾題是否是常常看到呀 😁,下面就讓咱們一個一個來看。

首先,咱們仍是得清楚幾個概念,這個是硬性規定的,不看的話咱無法繼續下去啊。

當使用==進行比較的時候,會有如下轉換規則(判斷規則):

  1. 兩邊類型若是相同,值相等則相等,如 2 == 3確定是爲false的了
  2. 比較的雙方都爲基本數據類型:
  • 如果一方爲null、undefined,則另外一方必須爲null或者undefined才爲true,也就是null == undefinedtrue或者null == nulltrue,由於undefined派生於null
  • 其中一方爲String,是的話則把String轉爲Number再來比較
  • 其中一方爲Boolean,是的話則將Boolean轉爲Number再來比較
  1. 比較的一方有引用類型:
  • 將引用類型遵循ToNumber的轉換形式來進行比較(實際上它的hintdefault,也就是toPrimitive(obj, 'default'),可是default的轉換規則和number很像)
  • 兩方都爲引用類型,則判斷它們是否是指向同一個對象

在一些文章中,會說道:

若是其中一方爲Object,且另外一方爲String、Number或者Symbol,會將Object轉換成字符串,再進行比較

(摘自《神三元-(建議收藏)原生JS靈魂之問, 請問你能接得住幾個?(上)》中的3. == 和 ===有什麼區別?)

這樣認爲其實也能夠,由於想一想toPrimitive(obj, 'number')的過程:

  • 如果輸入值爲引用數據類型,則先調用valueOf()方法
  • 如果valueOf()方法的返回值是基本數據類型則直接返回,若不是則繼續調用toString()
  • 如果調用toString()的返回值是基本數據類型則返回,不然報錯。

能夠看到,首先是會執行valueOf()的,可是引用類型執行valueOf()方法,除了日期類型,其它狀況都是返回它自己,也就是說執行完valueOf()以後,仍是一個引用類型而且是它自己。那麼咱們是否是就能夠將valueOf()這一步給省略掉,認爲它是直接執行toString()的,這樣作起題來也快了不少。

6. 幾種一元運算符的類型轉換?

對於幾種經常使用運算符的類型轉換:

  1. -、*、/、%這四種都會把符號兩邊轉成數字來進行運算
  2. +因爲不只是數字運算符,仍是字符串的鏈接符,因此分爲兩種狀況:
  • 兩端都是數字則進行數字計算(一元正號+b這種狀況至關於轉換爲數字)
  • 有一端是字符串,就會把另外一端也轉換爲字符串進行鏈接

對象的+號類型轉換:

  • 對象在進行+號字符串鏈接的時候,toPrimitive的參數hintdefault,可是default的執行順序和number同樣都是先判斷有沒有valueOf,有的話執行valueOf,而後判斷valueof後的返回值,如果是引用類型則繼續執行toString。(相似題4.54.6)
  • 日期在進行+號字符串鏈接的時候,優先調用toString()方法。(相似題4.7)
  • 一元正號是轉換其餘對象到數值的最快方法,也是最推薦的作法,由於 它不會對數值執行任何多餘操做

7. 你會幾種讓if(a == 1 && a == 2 && a == 3)條件成立的辦法?

這道題相信你們看的不會少,除了重寫valueOf()你還會哪些解法呢?

解法一:重寫valueOf()

這個解法是利用了:當對象在進行==比較的時候實際是會先執行valueOf(),如果valueOf()的返回值是基本數據類型就返回,不然仍是引用類型的話就會繼續調用toString()返回,而後判斷toString()的返回值,如果返回值爲基本數據類型就返回,不然就報錯。

如今valueOf()每次返回的是一個數字類型,因此會直接返回。

// 1
var a = {
  value: 0,
  valueOf () {
    return ++this.value
  }
}
if (a == 1 && a == 2 && a == 3) {
  console.log('成立')
}
複製代碼

解法二:重寫valueOf()toString()

var a = {
  value: 0,
  valueOf () {
    return {}
  },
  toString () {
    return ++this.value
  }
}
if (a == 1 && a == 2 && a == 3) {
  console.log('成立')
}
複製代碼

原理就是解法一的原理,只不過用到了當valueOf()的返回值是引用類型的時候會繼續調用toString()

這裏你甚至均可以不用重寫valueOf(),由於除了日期對象其它對象在調用valueOf()的時候都是返回它自己。

也就是說你也能夠這樣作:

var a = {
  value: 0,
  toString () {
    return ++this.value
  }
}
if (a == 1 && a == 2 && a == 3) {
  console.log('成立')
}
複製代碼

解法三:重寫Symbol.toPrimitive

想一想是否是還能夠用Symbol.toPrimitive來解呢?

結合題3.10咱們知道,當對象在進行==比較的時候,Symbol.toPrimitive接收到的參數hint"defalut",那麼咱們只須要這樣重寫:

var a = {
  value: 0,
  [Symbol.toPrimitive] (hint) {
    if (hint === 'default') {
      return ++this.value
    }
  }
}
if (a == 1 && a == 2 && a == 3) {
  console.log('成立')
}
複製代碼

這樣結果也是能夠的。

解法四:定義class並重寫valueOf()

固然你還能夠用class來寫:

class A {
  constructor () {
    this.value = 0
  }
  valueOf () {
    return ++this.value
  }
}
var a = new A()
if (a == 1 && a == 2 && a == 3) {
  console.log('成立')
}
複製代碼

解法五:利用數組轉爲字符串會隱式調用join()

什麼 ? 還有別的解法嗎?並且我看解法五的題目有點沒看懂啊。

讓咱們回過頭去看看題4.3,那裏提到了當數組在進行轉字符串的時候,調用toString()的結果其實就是調用join的結果。

那和這道題有什麼關係?來看看答案:

let a = [1, 2, 3]
a['join'] = function () {
  return this.shift()
}
if (a == 1 && a == 2 && a == 3) {
  console.log('成立')
}
複製代碼

由於咱們知道,對象若是是數組的話,當咱們不重寫其toString()方法,在轉換爲字符串類型的時候,默認實現就是將調用join()方法的返回值做爲toString()的返回值。

因此這裏咱們重寫了ajoin方法,而此次重寫作了兩件事情:

  1. 將數組a執行a.shift()方法,咱們知道這會影響原數組a的,將第一項去除
  2. 將剛剛去除的第一項返回回去

因此當咱們在執行a == 1這一步的時候,因爲隱式調用了a['join']方法,因此會執行上面👆說的那兩件事情,後面的a == 2a == 3同理。

解法六:定義class繼承Array並重寫join()

對於解法五咱們一樣能夠用class來實現

class A extends Array {
  join = this.shift
}
var a = new A(1, 2, 3)
if (a == 1 && a == 2 && a == 3) {
  console.log('成立')
}
複製代碼

這種寫法比較酷🆒,可是第一次看可能不太能懂。

  • 首先A這個類經過extends繼承於Array,這樣經過new A建立的就是一個數組
  • 而後A重寫了join方法,join = this.shift就至關因而join = function () { return this.shift() }
  • 這樣當每次調用a == xxx的時候,都會隱式調用咱們自定義的join方法,執行和解法五同樣的操做。

8. 讓if (a === 1 && a === 2 && a === 3)條件成立?

這道題看着和上面那道有點像,不過這裏判斷的條件是全等的。

咱們知道全等的條件:

  1. 左右兩邊的類型要相等,若是類型不相等則直接返回false,這點和==不一樣,==會發生隱式類型轉換
  2. 再判斷值相不相等

而對於上面👆一題的解法咱們都是利用了==會發生隱式類型轉換這一點,顯然若是再用它來解決這道題是不能實現的。

想一想當咱們在進行a === xxx判斷的時候,實際上就是調用了a這個數據而已,也就是說咱們要在調用這個數據以前,作一些事情,來達到咱們的目的。

不知道這樣說有沒有讓你想到些什麼 🤔️?或許你和呆呆同樣會想到Vue大名鼎鼎的數據劫持 😁。

想一想在Vue2.x中不就是利用了Object.defineProperty()方法從新定義data中的全部屬性,那麼在這裏咱們一樣也能夠利用它來劫持a,修改a變量的get屬性。

var value = 1;
Object.defineProperty(window, "a", {
  get () {
    return this.value++;
  }
})
if (a === 1 && a === 2 && a === 3) {
  console.log('成立')
}
複製代碼

這裏實際就作了這麼幾件事情:

  • 使用Object.defineProperty()方法劫持全局變量window上的屬性a
  • 當每次調用a的時候將value自增,並返回自增後的值

(其實我還想着用Proxy來進行數據劫持,代理一下window,將它用new Proxy()處理一下,可是對於window對象好像沒有效果...)

解法二

怎麼辦 😂,一碰到這種題我又想到了數組...

var arr = [1, 2, 3];
Object.defineProperty(window, "a", {
  get () {
    return this.arr.shift()
  }
})
if (a === 1 && a === 2 && a === 3) {
  console.log('成立')
}
複製代碼

中了shift()的毒...固然,這樣也是能夠實現的。

解法三

還有就是EnoYao大佬那裏看來的騷操做:

原文連接:juejin.im/post/5e66dc…

var aᅠ = 1;
var a = 2;
var ᅠa = 3;
if (aᅠ == 1 && a == 2 && ᅠa == 3) {
  console.log("成立");
}
複製代碼

說來慚愧...a的先後隱藏的字符我打不來 😂...

9. 控制檯輸入{}+[]會怎樣?

這道有趣的題是從LINGLONG的一篇《【js小知識】[]+ {} =?/{} +[] =?(關於加號的隱式類型轉換)》那裏看來的。

(PS: pick一波玲瓏,這位小姐姐的文章寫的都挺好的,不過熱度都不高,你們能夠支持一下呀 😁)

OK👌,來看看題目是這樣的:

在控制檯(好比瀏覽器的控制檯)輸入:

{}+[]
複製代碼

的結果會是什麼 🤔️?

咦~這道題應該很簡單吧,根據前面的類型轉換原則,+兩邊都轉換爲字符串,{}轉爲"[object Object]"[]轉爲"",拼接的結果就是:

console.log({}+[]) // "[object Object]"
複製代碼

可是注意這裏的題目,是要在控制檯輸出哦。

此時我把這段代碼在控制檯輸出結果發現答案居然和預期的不同:

{}+[]
0
複製代碼

也就是說{}被忽略了,直接執行了+[],結果爲0

知道緣由的我眼淚掉了下來,原來它和以前提到的1.toString()有點像,也是由於JS對於代碼解析的緣由,在控制檯或者終端中,JS會認爲大括號{}開頭的是一個空的代碼塊,這樣看着裏面沒有內容就會忽略它了。

因此只執行了+[],將其轉換爲0

若是咱們換個順序的話就不會有這種問題:

(圖片來源:juejin.im/post/5e6055…)

爲了證明這一點,咱們能夠把{}當成空對象來調用一些對象的方法,看會有什麼效果:

(控制檯或者終端)

{}.toString()
複製代碼

如今的{}依舊被認爲是代碼塊而不是一個對象,因此會報錯:

Uncaught SyntaxError: Unexpected token '.'
複製代碼

解決辦法能夠用一個()將它擴起來:

({}).toString
複製代碼

不過這東西在實際中用的很少,我能想到的一個就是在項目中(好比我用的vue),而後定義props的時候,若是其中一個屬性的默認值你是想要定義爲一個空對象的話,就會用到:

props: {
    target: {
        type: Object,
        default: () => ({})
    }
}
複製代碼

第五補:八種JS繼承

對於JS繼承我也寫了一個系列「封裝|繼承|多態」,這裏是傳送門:

具體的例子還有題目在文章中都已經說的很清楚了,這裏我就只列舉一下各個繼承的優缺點以及僞代碼。

1. 原型鏈繼承

僞代碼:

Child.prototype = new Parent()
複製代碼

思惟導圖:

優勢:

  • 繼承了父類的模板,又繼承了父類的原型對象

缺點:

  • 若是要給子類的原型上新增屬性和方法,就必須放在Child.prototype = new Parent()這樣的語句後面
  • 沒法實現多繼承(由於已經指定了原型對象了)
  • 來自原型對象的全部屬性都被共享了,這樣若是不當心修改了原型對象中的引用類型屬性,那麼全部子類建立的實例對象都會受到影響
  • 建立子類時,沒法向父類構造函數傳參數

2. 構造繼承

僞代碼:

function Child () {
    Parent.call(this, ...arguments)
}
複製代碼

思惟導圖:

優勢:

  • 解決了原型鏈繼承中子類實例共享父類引用對象的問題,實現多繼承,建立子類實例時,能夠向父類傳遞參數

缺點:

  • 構造繼承只能繼承父類的實例屬性和方法,不能繼承父類原型的屬性和方法
  • 實例並非父類的實例,只是子類的實例
  • 沒法實現函數複用,每一個子類都有父類實例函數的副本,影響性能

3. 組合繼承

僞代碼:

// 構造繼承
function Child () {
  Parent.call(this, ...arguments)
}
// 原型鏈繼承
Child.prototype = new Parent()
// 修正constructor
Child.prototype.constructor = Child
複製代碼

思惟導圖:

實現方式:

  • 使用原型鏈繼承來保證子類能繼承到父類原型中的屬性和方法
  • 使用構造繼承來保證子類能繼承到父類的實例屬性和方法

優勢:

  • 能夠繼承父類實例屬性和方法,也可以繼承父類原型屬性和方法
  • 彌補了原型鏈繼承中引用屬性共享的問題
  • 可傳參,可複用

缺點:

  • 使用組合繼承時,父類構造函數會被調用兩次
  • 而且生成了兩個實例,子類實例中的屬性和方法會覆蓋子類原型(父類實例)上的屬性和方法,因此增長了沒必要要的內存。

4. 寄生組合繼承

僞代碼:

// 構造繼承
function Child () {
  Parent.call(this, ...arguments)
}
// 原型式繼承
Child.prototype = Object.create(Parent.prototype)
// 修正constructor
Child.prototype.constructor = Child
複製代碼

思惟導圖:

寄生組合繼承算是ES6以前一種比較完美的繼承方式吧。

它避免了組合繼承中調用兩次父類構造函數,初始化兩次實例屬性的缺點。

因此它擁有了上述全部繼承方式的優勢:

  • 只調用了一次父類構造函數,只建立了一份父類屬性
  • 子類能夠用到父類原型鏈上的屬性和方法
  • 可以正常的使用instanceOfisPrototypeOf方法

5. 原型式繼承

僞代碼:

var child = Object.create(parent)
複製代碼

實現方式:

該方法的原理是建立一個構造函數,構造函數的原型指向對象,而後調用 new 操做符建立實例,並返回這個實例,本質是一個淺拷貝。

ES5以後能夠直接使用Object.create()方法來實現,而在這以前就只能手動實現一個了(如題目6.2)。

優勢:

  • 再不用建立構造函數的狀況下,實現了原型鏈繼承,代碼量減小一部分。

缺點:

  • 一些引用數據操做的時候會出問題,兩個實例會公用繼承實例的引用數據類
  • 謹慎定義方法,以避免定義方法也繼承對象原型的方法重名
  • 沒法直接給父級構造函數使用參數

6. 寄生式繼承

僞代碼:

function createAnother (original) {
    var clone = Object.create(original);; // 經過調用 Object.create() 函數建立一個新對象
    clone.fn = function () {}; // 以某種方式來加強對象
    return clone; // 返回這個對象
}
複製代碼

實現方式:

  • 原型式繼承的基礎上再封裝一層,來加強對象,以後將這個對象返回。

優勢:

  • 再不用建立構造函數的狀況下,實現了原型鏈繼承,代碼量減小一部分。

缺點:

  • 一些引用數據操做的時候會出問題,兩個實例會公用繼承實例的引用數據類
  • 謹慎定義方法,以避免定義方法也繼承對象原型的方法重名
  • 沒法直接給父級構造函數使用參數

7. 混入方式繼承

僞代碼:

function Child () {
    Parent.call(this)
    OtherParent.call(this)
}
Child.prototype = Object.create(Parent.prototype)
Object.assign(Child.prototype, OtherParent.prototype)
Child.prototype.constructor = Child
複製代碼

思惟導圖:

8. class中的繼承

僞代碼:

class Child extends Parent {
    constructor (...args) {
        super(...args)
    }
}
複製代碼

ES6中的繼承:

  • 主要是依賴extends關鍵字來實現繼承,且繼承的效果相似於寄生組合繼承
  • 使用了extends實現繼承不必定要constructorsuper,由於沒有的話會默認產生並調用它們
  • extends後面接着的目標不必定是class,只要是個有prototype屬性的函數就能夠了

ES5繼承和ES6繼承的區別:

  • ES5中的繼承(例如構造繼承、寄生組合繼承) ,實質上是先創造子類的實例對象this,而後再將父類的屬性和方法添加到this上(使用的是Parent.call(this))。
  • 而在ES6中卻不是這樣的,它實質是先創造父類的實例對象this(也就是使用super()),而後再用子類的構造函數去修改this

參考文章

知識無價,支持原創。

參考文章:

後語

你盼世界,我盼望你無bug。這篇文章就介紹到這裏。

因爲開篇已經說了太多話了這裏就不說了🙊。

喜歡霖呆呆的小夥還但願能夠關注霖呆呆的公衆號 LinDaiDai 或者掃一掃下面的二維碼👇👇👇.

我會不定時的更新一些前端方面的知識內容以及本身的原創文章🎉

你的鼓勵就是我持續創做的主要動力 😊.

相關推薦:

《全網最詳bpmn.js教材》

《【建議改爲】讀完這篇你還不懂Babel我給你寄口罩》

《【建議星星】要就來45道Promise面試題一次爽到底(1.1w字用心整理)》

《【建議👍】再來40道this面試題酸爽繼續(1.2w字用手整理)》

《【何不三連】比繼承家業還要簡單的JS繼承題-封裝篇(牛刀小試)》

《【何不三連】作完這48道題完全弄懂JS繼承(1.7w字含辛整理-返璞歸真)》

《【精】從206個console.log()徹底弄懂數據類型轉換的前世此生(上)》

相關文章
相關標籤/搜索