最近在從新看js基礎,索性就將繼承、閉包、原型鏈這三個原生js中比較重要的點寫篇文章總結一下。本身明白理解是一回事,寫了文章讓別人看明白是另一回事,經過講述,本身也能進步。
JS
的做者Brendan Eich
在設計這門編程語言時,只是爲了讓這門語言做爲瀏覽器與網頁互動的工具。他以爲這門語言只須要能完成一些簡單操做就夠了,好比判斷用戶是否填寫了表單。
基於簡易語言的設計初衷,做者以爲JS
不須要有相似java
等面嚮對象語言所擁有的「繼承」機制。可是考慮到JS
中一切皆對象(全部的數據類型均可以用對象來表示),必須有一種機制,把全部的對象聯繫起來,實現相似的「繼承」機制。
不一樣於大部分面嚮對象語言,ES6
以前並無引入類(class
)的概念,JS
並不是經過類而是經過構造函數來建立實例,使用prototype
原型模型來實現「繼承」。
在 JavaScript
裏,構造函數一般是用來實現實例的,JavaScript
沒有類的概念,可是有特殊的構造函數。構造函數本質上是個普通函數,充當類的角色,主要用來建立實例,並初始化實例,即爲實例成員變量賦初始值。java
構造函數和普通函數的區別在於,構造函數應該遵循如下幾點規範:golang
new
關鍵字來進行調用;this
指向的是新建立的實例;return
表達式,通常狀況下,會隱式地返回 this
,也就是新建立的對象,若是想要使用顯式的返回值,則顯式的返回值必須是對象,不然依然返回實例。構造函數是用來建立實例的
// 步驟1:新建構造函數 function Person(name) { this.name = name; this.sayName = function() { console.log(this.name); } } // 步驟2:建立實例 var person = new Person('yang');
此時,以下圖所示,針對步驟1,當構造函數被建立時,會在內存空間新建一個對象,構造函數內有一個屬性 prototype
會指向這個對象的存儲空間,這個對象稱爲構造函數的原型對象。
針對步驟2,以下圖所示,person
是經過 Person
構造函數建立的實例,在 person
內部將包含一個指針(內部屬性),指向構造函數的原型對象,這個指針稱爲 [[prototype]]
。
目前,大部分瀏覽器都支持 __proto__
這個屬性來訪問構造函數的原型對象,就像這裏,person.__proto__
指向 Person.prototype
的對象存儲空間。
由上面示例圖知道,實例 person
若是訪問原型對象,須要使用 __proto__
這個屬性。
事實上,__proto__
是一個訪問器屬性(由一個 getter
函數和一個 setter
函數構成),但做爲訪問 [[prototype]]
的屬性,它是一個不被推薦的屬性, JavaScript
規範中規定,這個屬性僅在瀏覽器環境下才能使用。[[prototype]]
是內部的並且是隱藏的,當須要訪問內部 [[prototype]]
時,可使用如下現代方法:編程
// 返回對象 `obj` 的 `[[prototype]]`。 Object.getPrototypeOf(obj); // 將對象 `obj` 的 `[[prototype]]` 設置爲 `proto`。 Object.setPrototypeOf(obj, proto) // 利用給定的 `proto` 做爲 `[[prototype]]` 和屬性描述符(可選)來建立一個空對象。 Object.create(proto[, descriptors])
在默認狀況下,全部的原型對象都會自動得到一個 constructor
的屬性,這個屬性包含一個指向 prototype
所在函數的指針,即 constructor
屬性會指向構造函數自己。
此外,Person.prototype
指向的位置是一個對象,也包含有內部 [[prototype]]
指針,這個指針指向的是 Object.prototype
,是一個對象。這個關係表示,Person.prototype
是由 Object
做爲構造函數建立的。
須要注意的是,原型是能夠被改寫的。可是 JavaScript
中對其作了規定,只能夠被改寫成對象,若是改寫成其餘值(空值 null
也不行),會自動被忽略,會讓原型鏈下一級來替換這個被改寫的原型。瀏覽器
- 屬性公用化:原型能夠存儲一些默認屬性和方法,而且在各個不一樣的實例中能夠共享使用;
- 繼承:在子類構造函數中借用父類構造函數,再經過原型來繼承父類的原型屬性和方法,模擬繼承的效果;
- 節省存儲空間:結合第1點,公用的屬性和方法多了,對應須要的存儲空間也減小了。
// 第一步 新建構造函數 function Person(name) { this.name = name; this.age = 18; this.sayName = function() { console.log(this.name); } } // 第二步 建立實例 1 var person1 = new Person('1號'); // 第三步 建立實例2 var person2 = new Person('2號'); // 結果均爲 true person1.__proto__ === Person.prototype; person2.__proto__ === Person.prototype; // 1號 2號 console.log(person1.name, person2.name); // 18 18 console.log(person1.age, person2.age);
JavaScript
中,萬物皆對象(全部的數據類型均可以用對象來表示),對象與對象之間存在關係,並非孤立存在的,對象之間的繼承關係,在JavaScript
中實例對象經過內部屬性[[prototype]]
指向父類對象的原型空間,直到指向瀏覽器實現的內部對象Object
爲止,Object
的內部屬性[[prototype]]
爲null
,這樣就造成了一個原型指向的鏈條,這個鏈條稱爲原型鏈。
當訪問對象的屬性時,會先在對象自身屬性中查找,若是有則直接返回使用,若是沒有則會順着原型鏈指向繼續尋找(不斷查找內部屬性 [[prototype]]),直到尋找瀏覽器內置對象的原型,若是依然沒有找到,則返回 undefined。
須要注意的是,原型鏈中訪問器屬性和數據屬性在讀寫上是有區別的(點擊瞭解訪問器屬性和數據屬性)。若是在原型鏈上某一級設置了訪問器屬性(假設爲 age
),則讀取 age
時,直接按訪問器屬性設置的值返回;寫入時也是以訪問器屬性爲最優先級。在數據屬性的讀寫上,讀取時,會按照原型鏈屬性查找進行查找;寫入時,直接寫入當前對象,若原型鏈中有相同屬性,會被覆蓋。
能夠結合如下代碼來對原型鏈進行分析:網絡
// 第一步 新建構造函數 function Person(name) { this.name = name; this.age = 18; this.sayName = function() { console.log(this.name); } } // 第二步 建立實例 var person = new Person('person'); 複製代碼
根據以上代碼,能夠獲得下面的圖示:閉包
第一步中,新建 Person
的構造函數,此時原型空間被建立;第二步中,經過 new
構造函數生成實例 person
,person
的 [[prototype]]
會指向原型空間。app
不少人容易忽視的是瀏覽器對於下面的處理,這裏 Person.prototype.__proto__
指向內置對象,由於 Person.prototype
是個對象,默認是由 Object
函數做爲類建立的,而 Object.prototype
爲內置對象。異步
而 Person.__proto__
指向內置匿名函數 anonymous
,由於 Person
是個函數對象,默認由 Function
做爲類建立,而 Function.prototype
爲內置匿名函數 anonymous
。編程語言
這裏還須要注意一個點,Function.prototype
和 Function.__proto__
同時指向內置匿名函數 anonymous
,這樣原型鏈的終點就是 null
,而不用擔憂原型鏈查找會陷入死循環中。函數
一、 藉助call
function Parents(age, live) { this.name = '藉助call方式實現繼承' this.age = age this.live = live } function Child() { Parents.call(this, ...arguments) } let child = new Child(18, true) console.log('child: ', child)
缺點:這樣寫的時候子類雖然可以拿到父類的屬性值, 可是問題是父類原型對象中一旦存在方法那麼子類沒法繼承。
二、藉助原型鏈
function Parents1(age) { this.name = "藉助原型鏈實現繼承" this.age = age } function Child1() { this.type = 'Child1' } Child1.prototype = new Parents1() let child1 = new Child1() console.log("child1: ", child1.name)
缺點:改變實例的屬性會影響到父類的屬性,由於共用一個原型對象(引用類型)
三、 將前兩中組合(組合式繼承)
function Parents2(age) { this.name = '藉助組合式實現繼承' this.age = age this.arr = [1, 2, 3] } function Child2() { this.type = 'Child2' Parents2.call(this, ...arguments) } Child2.prototype = new Parents2() let child2 = new Child2(12) let anthorChild2 = new Child2(13) child2.arr.push(4) console.log('child2: ', child2) console.log('anthorChild2: ', anthorChild2)
缺點:這種繼承的問題 那就是Parent2的構造函數會多執行了一次(Child2.prototype = new Parent2();)
四、組合繼承的優化
function Parents3(age) { this.age = age this.name = '組合繼承的優化1' } function Child3() { Parents.call(this, ...arguments) this.type = 'Child3' } // 這裏讓將父類原型對象直接給到子類,父類構造函數只執行一次, // 並且父類屬性和方法均能訪問 Child2.prototype = Parents3.prototype
缺點:子類實例的構造函數是Parent3,顯然這是不對的,應該是Child3。
五、寄生組合式繼承
function Parents4(age) { this.age = age this.name = '寄生組合式繼承' } function Child4() { Parents.apply(this, [...arguments]) this.type = 'Child4' } Child4.prototype = Object.create(Parents4.prototype) Child4.prototype.constructor = Child4
這是最推薦的一種方式, 接近完美的繼承, 它的名字也叫作寄生組合繼承。
六、ES6的extends
它用的就是寄生組合式繼承,可是加了一個Object.setPrototypeOf(subClass, superClass)
是用來繼承父類的靜態方法。這也是原來的繼承方式疏忽掉的地方。
擴展:面向對象繼承的問題,沒法決定繼承哪些屬性, 全部屬性都得繼承。
例如:不一樣的車有不一樣的功能
function drive(){ console.log("發動"); } function music() { console.log("音樂") } function addOil() { console.log("加油") } // compose是一個組合各類方法的方法 // 普通汽車 let car = compose(drive, music, addOil); // 新能源 let newEnergyCar = compose(drive, music);
閉包是指有權訪問另一個函數做用域中的變量的函數(紅寶書)
閉包是指那些可以訪問自由變量的函數。(MDN)其中自由變量, 指在函數中使用的, 但既不是函數參數arguments也不是函數的局部變量的變量,其實就是另一個函數做用域中的變量。)
提及閉包,就必需要說說做用域,ES5種只存在兩種做用域:一、函數做用域。二、全局做用域
當訪問一個變量時, 解釋器會首先在當前做用域查找標示符,若是沒有找到, 就去父做用域找, 直到找到該變量的標示符或者不在父做用域中, 這就是做用域鏈,每個子函數都會拷貝上級的做用域, 造成一個做用域的鏈條。
let a = 1; function f1() { var a = 2 function f2() { var a = 3; console.log(a); //3 } }
在這段代碼中, f1的做用域指向有全局做用域(window) 和它自己,而f2的做用域指向全局做用域(window)、 f1和它自己。並且做用域是從最底層向上找, 直到找到全局做用域window爲止,若是全局尚未的話就會報錯。閉包產生的本質就是, 當前環境中存在指向父級做用域的引用。
function f2() { var a = 2 function f3() { console.log(a); //2 } return f3; } var x = f2(); x();
這裏x會拿到父級做用域中的變量, 輸出2。 由於在當前環境中,含有對f3的引用, f3偏偏引用了window、 f3和f3的做用域。 所以f3能夠訪問到f2的做用域的變量。那是否是隻有返回函數纔算是產生了閉包呢?回到閉包的本質,只須要讓父級做用域的引用存在便可。
var f4; function f5() { var a = 2 f4 = function () { console.log(a); } } f5(); f4();
讓f5執行,給f4賦值後,等於說如今f4擁有了window、f5和f4自己這幾個做用域的訪問權,仍是自底向上查找,最近是在f5中找到了a,所以輸出2。在這裏是外面的變量f4存在着父級做用域的引用, 所以產生了閉包,形式變了,本質沒有改變。
var b = 1; function foo() { var b = 2; function baz() { console.log(b); } bar(baz); } function bar(fn) { // 這就是閉包 fn(); } // 輸出2,而不是1 foo(); // 如下的閉包保存的僅僅是window和當前做用域。 // 定時器 setTimeout(function timeHandler() { console.log('111'); }, 100) // 事件監聽 // document.body.click(function () { // console.log('DOM Listener'); // }) // 當即執行函數 var c = 2; (function IIFE() { // 輸出2 console.log(c); })();
for (var i = 1; i <= 5; i++) { setTimeout(function timer() { console.log(i) }, 0) } // 6 6 6 6 6 6 // 爲何會所有輸出6? 如何改進, 讓它輸出1, 2, 3, 4, 5?
解析:
// 一、利用IIFE(當即執行函數表達式)當每次for循環時,把此時的i變量傳遞到定時器中 for (var i = 0; i < 5; i++) { (function (j) { setTimeout(() => { console.log(j) }, 1000); })(i) } // 二、給定時器傳入第三個參數, 做爲timer函數的第一個函數參數 for (var i = 0; i < 5; i++) { setTimeout(function (j) { console.log(j) }, 1000, i); } // 三、使用ES6中的let // let使JS發生革命性的變化, 讓JS有函數做用域變爲了塊級做用域, // 用let後做用域鏈不復存在。 代碼的做用域以塊級爲單位, for (let i = 1; i <= 5; i++) { setTimeout(function timer() { console.log(i) }, 2000) }
以上部份內容來源與本身複習時的網絡查找,也主要用於我的學習,至關於記事本的存在,暫不列舉連接文章。若是有做者看到,能夠聯繫我將原文連接貼出。