前一陣面試,過程當中發現問到一些很基礎的問題時候,本身並不能很流暢的回答出來。或者遇到一些基礎知識的應用,因爲對這些點理解的不是很深刻,拿着筆竟然什麼都寫不出來,因而有了回顧一下這些基礎知識的想法。javascript
首先就是面試中常常會問到的,JS是怎麼實現繼承的,其實問到繼承,面試官想問的可能仍是你對JS面向對象的理解吧。java
這一部分的主要參考資料:《JavaScript高級程序設計》、《JavaScript設計模式》
若是有什麼錯誤的地方,也但願看到這篇文章的小夥伴給我指出來,謝謝 ^_^面試
Javascript是一種基於對象(object-based)的語言,你遇到的全部東西幾乎都是對象。
一個簡單的對象建立:編程
var People = { name : "eavan", age : 24, getName : function(){ alert(this.name); //eavan } }
使用的時候就能夠用People.name,獲取People這個對象的name屬性,或者是People.getName()來獲得People的name值。
另外一種對象建立方式:設計模式
var People = new Object(); People.name = "eavan"; People.age = 24; People.getName = function(){ alert(this.name); }
這裏用到了new,就順便提一下在使用new的時候發生了什麼,其實在使用new的時候,大體能夠認爲作了這三件事,看下面的代碼:編程語言
var People = {}; //咱們建立了一個空對象People People.__proto__ = Object.prototype; //咱們將這個空對象的__proto__成員指向了Object函數對象prototype成員對象 Object.call(People); //咱們將Object函數對象的this指針替換成People,而後再調用Object函數
簡單來講就是對一些屬性的隱藏域暴露,好比私有屬性、私有方法、共有屬性、共有方法、保護方法等等。而js也能實現私有屬性、私有方法、共有屬性、共有方法等等這些特性。函數
像java這樣的面向對象的編程語言通常會有一個類的概念,從而實現封裝。而javascript中沒有類的概念,JS中實現封裝主要仍是靠函數。this
首先聲明一個函數保存在一個變量裏面。而後在這個函數(類)的內部經過對this變量添加屬性或者方法來實現對類添加屬相或者方法。prototype
var Person = function(){ var name = "eavan"; //私有屬性 function checkName(){}; //私有方法 this.myName = "gaof"; //對象共有屬性 this.myFriends = ["aa","bb","cc"]; this.copy = function(){} //對象共有方法 this.getName = function(){ //構造器方法 return name; }; }
純構造函數封裝數據的問題是:對像this.copy = function(){}這種方法的建立,其實在執行的時候大可沒必要綁定到特定的對象上去,將其定義到全局變量上也是同樣的,並且其過程至關於實例化了一個Function,也大可沒必要實例化這麼多其實幹同一件事的方法。而這個小問題的解決能夠用原型模式來解決。設計
在每建立一個函數的時候,都會生成一個prototype屬性,這個屬性指向函數的原型對象。而其是用來包含特定類型的全部實例共享的屬性和方法。因此,直接添加在原型中的實例和方法,就會被全部實例所共享。
一樣仍是上面的Person的例子,咱們能夠爲其原型添加新的屬性和方法。
Person.isChinese = true; //類的靜態共有屬性(對象不能訪問) Person.prototype.sex = "man" ; //類的共有屬性 Person.prototype.frends = ["gao","li","du"]; Person.prototype.isBoy = function(){}; //類的共有方法
原型封裝數據的問題:對綁定在prototype上的引用類型的變量,因爲被全部對象所共有,其中某一個對象對該數據進行修改,當別的對象訪問該數據的時候,所訪問到的值就是被修改後的。
好比以下代碼:
var person1 = new Person(); person1.frends.push("dd"); console.log(person1.frends); //["gao", "li", "du", "dd"] var person2 = new Person(); person2.frends.push("ee"); console.log(person2.frends); //["gao", "li", "du", "dd", "ee"]
本來但願對person1和person2的friends屬性分別添加新的內容,結果兩者的friends屬性竟然是「公用」的!
綜上,最多見的方式應該是組合使用構造函數和原型模式,構造函數用於定義實例屬性,原型模式用於定義方法和共享的屬性。
每一個類有三部分構成:第一部分是構造函數內,供實例對象化複製用。第二部分是構造函數外,直接經過點語法添加,供類使用,實例化對象訪問不到。第三部分是類的原型中,實例化對象能夠經過其原型鏈間接訪問到,也是爲全部實例化對象所共用。
在說到對象實例的屬性的時候,咱們有一個問題,就是在訪問一個屬性的時候,這個屬性是屬於實例,仍是屬於這個實例的原型的呢?
好比仍是上面的例子,咱們爲person2實例增長一個sex屬性,這時候訪問person2的sex屬性時,獲得的是咱們增長的值。說明爲對象實例添加一個屬性的時候,這個屬性就會屏蔽原型對象中保存的同名屬性。
person2.sex = "woman"; console.log(person1.sex); //man console.log(person2.sex); //woman
這個時候咱們可使用hasOwnProperty()方法來檢測一個屬性是存在於實例中,仍是存在於原型中。若是實例中有這個屬性,hasOwnProperty()會返回true,而hasOwnProperty()並不會感知到原型中的屬性。因此能夠用這個方法檢測屬性究竟是存在於實例中仍是原型中。
console.log(person1.hasOwnProperty("sex")); //原型中的屬性,返回false console.log(person2.hasOwnProperty("sex")); //實例中的屬性,返回true
ECMAScript中描述了原型鏈的概念,並將原型鏈做爲實現繼承的主要方法。其基本思想是利用原型讓一個引用類型繼承另外一個引用類型的屬性和方法。
以下代碼:
function Super(){ this.val = true; this.arr = ["a"]; } function Sub(){ //... } Sub.prototype = new Super(); var sub = new Sub(); console.log(sub.val) //true
以上代碼定義了Super和Sub兩個類型,繼承的核心就一句話:Sub.prototype = new Super() 將父類的一個實例賦給子類的原型。這樣子類就可以使用父類實例所擁有的方法和父類原型中的方法。
這種狀況要想給子類添加本身的方法或者是覆蓋父類中某個方法的時候,必定要在放在替換原型語句後面。不然寫在原型上的方法都會丟失。
並且在給子類添加新方法的時候,不能使用字面量的方式添加新方法,這樣會致使繼承無效。
如:
Sub.prototype = new Super(); Sub.prototype = { //錯誤的方式 getVal : function(){ //... } }
以上代碼剛剛把Super的實例賦給原型,緊接着又將原信替換成一個對象字面量,致使如今原型包含的是一個Object的實例,並不是Super的實例,所以原型鏈被切斷了,Sub和Super已經沒有關係了。
原型鏈的問題:
最主要的問題有兩個:一是因爲引用類型的原型屬性會被全部實例所共享,因此經過原型鏈繼承時,原型變成了另外一個類型的實例,原先的實例屬性也就變成了如今的原型屬性,以下代碼:
function Super(){ this.friends = ["peng","gao"]; } function Sub(){ //... } Sub.prototype = new Super(); var sub1 = new Sub(); var sub2 = new Sub(); sub1.friends.push("du"); console.log(sub2.friends); //["peng", "gao", "du"]
這個例子說明的就是上面的問題,子類的全部實例共享了父類中的引用類型屬性。
原型鏈繼承的另外一個問題是在建立子類行的實例的時候,無法向父類的構造函數傳遞參數。
具體實現:
function Super(){ this.val = true; this.arr = ["a"]; } function Sub(){ Super.call(this); } var sub = new Sub(); console.log(sub.val) //true
這種模式這是解決了原型鏈繼承中出現的兩個問題,它能夠傳遞參數,也沒有了子類共享父類引用屬性的問題。
但這種模式也有他的問題,那就是在父類原型中定義的方法,實際上是對子類不可見的。
既然上述的兩種方式各有各自的侷限性,將它倆整合到一塊兒是否是會好一點呢,因而就有了組合繼承。
function Super(){ this.val = true; this.arr = ["a"]; } function Sub(){ Super.call(this); //{2} } Sub.prototype = new Super(); //{1} Sub.prototype.constructor = Sub; //{3} var sub = new Sub(); console.log(sub.val) //true
組合繼承還有一個要注意的地方:
在代碼{3}處,將子類原型的constructor屬性指向子類的構造函數。由於若是不這麼作,子類的原型是父類的一個實例,因此子類原型的constructor屬性就丟失了,他會順着原型鏈繼續往上找,因而就找到了父類的constructor因此它指向的實際上是父類。
這種繼承方式是使用最多的一種方式。
這種繼承方式解決了上兩種方式的缺點,不會出現共享引用類型的問題,同時父類原型中的方法也被繼承了下來。
若是要提及有什麼缺點咱們發現,在執行代碼{1}時,Sub.prototype會獲得父類型的val和arr兩個屬性。他們是Super的實例屬性,只不過如今在Sub的原型上。而代碼{2}處,在建立Sub實例的時候,調用Super的構造函數,又會在新的對象上建立屬性val和arr,因而,這兩個屬性就屏蔽了原型中兩個同名屬性。
對於上面的問題,咱們也有解決辦法,不是在子類原型中多了一份父類的屬性和方法麼,那我原型中就只要父類原型中的屬性和方法,這裏咱們引入了一個方法:
function inheritObject(obj){ var F = function(){}; F.prototype = obj; return new F(); }
這個方法建立了一個對象臨時性的構造函數,而後將傳入的對象做爲這個構造函數的原型,最後返回這個臨時類型的一個新實例。
咱們能夠設想,若是用這個方法拷貝一份父類的原型屬性給子類,是否是就避免了上面提到的子類原型中多了一份父類構造函數內的屬性。看以下代碼:
function Super(){ this.val = 1; this.arr = [1]; } Super.prototype.fun1 = function(){}; Super.prototype.fun2 = function(){}; function Sub(){ Super.call(this); } var p = inheritObject(Super.prototype); //{1} p.constructor = Sub; //{2} Sub.prototype = p; //{3} var sub = new Sub();
基本思路就是:沒必要爲了指定子類型的原型而調用父類的夠着函數,咱們須要的無非就是父類原型的一個副本而已。本質上就是複製出父類的一個副本,而後再將結果指定給子類型的原型。
所謂多態,就是同一個方法的多種調用方式,在javascript中,經過arguments對象對傳入的參數作判斷就能夠實現多種調用方式。
例子:
function Add(){ function zero(){ return 10; } function one(num){ return 10 + num; } function two(num1, num2){ return num1 + num2; } this.add = function(){ var arg = arguments, len = arg.length; switch (len){ case 0: return zero(); case 1: return one(arg[0]); case 2: return two(arg[0], arg[1]); } } } var A = new Add(); console.log(A.add()); //10 console.log(A.add(5)); //15 console.log(A.add(6, 7)); //13