跟傳統面嚮對象語言比起來,js在繼承關係方面比較特別,若是第一次看恐怕會有些抓狂,偶就是這樣(又透露小白本質#=_=),從哪裏提及好呢?函數調用?php
js中函數的調用方式大體可分如下幾種:java
1. 普通函數,直接調用ios
function Hi(){ alert(233); } Hi(); var f = function(){ alert(666); }; f();
2. 做爲對象的方法調用c++
var obj = { x:1, m:function(){ alert("hello"); } }; obj.m();
3. 構造函數調用,即new 表達式chrome
var a = new Array(1,2,3);
4. 間接調用,最經常使用應該是call或apply了數組
function f(){ alert(this.name); } var obj = { name:"Apple" }; f.call(obj); // 間接調用函數f,輸出Apple
顧名思義就是非直接的調用一個函數了,這裏的f.call(obj)的至關於obj.f(),若是直接寫obj.f()確定是錯誤的,因obj對象壓根沒定義這個方法,這即是奇妙的地方:obj對象經過call間接調用了f()函數。另外一個跟call類似的函數是apply,道理相同。瀏覽器
仍是要牢記在js中除了null和undefined外都是對象,它們的基類都是Object,Object是全部對象的祖宗(比如六道仙人他媽),函數天然也是對象,並且js中全部的函數會自動繼承Function這個類(固然最終繼承自Object),在咱們寫js代碼時,環境中已經有了這些類,Function這個類裏面就定義了一些方法,其中就有call和apply,因此f才能打點這麼調。app
call與apply方法的第一個參數是調用者所在的對象,好比這裏想達到讓obj來調用f()的目的,在f()函數中要對obj對象的屬性進行引用,那麼第一個參數就傳入obj這個對象,這樣f()函數中的上下文就是(context,好比全局做用域在瀏覽器中上下文是window對象,this是對它的引用)obj這個對象,因此在f()函數中this.name引用的是obj的name屬性,這個this即是對obj的引用。函數
若是f()函數須要傳參數的話,用call這種間接調用的方式怎麼寫?這即是call與apply的不一樣之處了,假如如今f()是這樣的:f(name, age){...},那麼這兩種方式是:工具
call寫法:f.call(obj, "Li", 23);
apply寫法:f.apply(obj, ["Li", 23]);
很明顯的區別:call以單個參數形式、逗號分隔傳入,有幾個寫幾個,放在第一個對象參數後面,apply則把全部要傳入的參數統一放在一個數組中。
有時調用apply第一個對象參數沒有或沒必要要,能夠傳undefined或null,並且在全局做用域中用這種間接形式調用時,第一個參數能夠是全局對象引用this,或者直接寫undefined時,它會自動去把這個全局下文環境做爲第一個參數對象,所以咱們日常的寫的函數能夠有不少奇怪的寫法:
// 通常寫法 Math.max(1, 5, 7, 2); // 奇怪寫法 Math.max.call(Math, 1, 5, 7, 2); Math.max.call(window, 1, 5, 7, 2); Math.max.call(this, 1, 5, 7, 2); Math.max.call(undefined, 1, 5, 7, 2); Math.max.apply(Math, [1, 5, 7, 2]); // max函數可傳入不定個數參數
大概設計者想讓天下沒有難調的方法,因此出這麼一招,這樣的話,理論上任何對象的可調方法,幾乎都能被其餘無關的對象調用。
js有面向對象,卻沒有直接定義的類(第一次聽很神奇),對象的實現由構造函數加調用表達式完成
function Game(name){ this.name = name; } var g = new Game("2048");
在調用new的時候,對象已經造成,接着運行Game函數,Game函數內部的上下文就變成了這個實例化對象的環境,this即是對這個實例化對象的引用,因此Game構造函數裏邊的語句只是在對這個對象進行一下組裝,就比如:
var g = {}; g.name = "2048"; // 對象已生成,這裏對它設置一些屬性
js的繼承方式大體分兩種,第一種就是像日常那些語言(php、c++、java等),好比class A extends B{...},class A : public B{...},經過對父類的繼承,稱爲類式繼承。通常來講這種方式會在子類構造函數中調用父類構造函數(不是必須),以初始化父類對象中的一些屬性、方法等,js同理也會這樣
function A(){ this.a = "A"; } function B(){ this.b = "B"; A.call(this); // 繼承A,調用它的構造函數 } var bObj = new B(); console.log(bObj.a); // A console.log(bObj.b); // B
這裏便用到了間接調用,並且通常也會這麼寫,由於須要傳入子對象的上下文this引用。父類構造函數A是個函數對象,它的調用只是在子類加了些東西進來而已,或者說初始化父類的屬性,它的效果跟下面同樣:
function B(){ this.b = "B"; var self = this; function A(){ self.a = "A"; } // 直接定義一個函數,爲這個對象添加屬性 A(); // 調用它 } var bObj = new B(); console.log(bObj.a); // A console.log(bObj.b); // B
類式繼承就這麼簡單,調用了下就表示繼承了另外一個類,但並不經常使用。它的好處是能夠在子類實例化時直接給父類的構造函數傳參
function A(platform){ this.platform = platform; } function B(name, platform){ this.name = name; A.call(this, platform); // 傳參給父類 } var bObj = new B("2048", "ios");
第二種是原型鏈繼承,首先要大概知道原型(prototype)是什麼。在js中每個函數都關聯着一個prototype屬性,這是語言自己的機制,這個prototype會指向一個對象,這個對象存放着一些方法和屬性,若是B構造函數的prototype屬性被賦值爲A構造函數建立的實例,就完成了一次原型式繼承:B的實例繼承自A,相似下面child繼承Parent(例1)
function Parent(name){ this.name = name; } function Child(age){ this.age = age; } Child.prototype = new Parent("Li"); var child = new Child(25); console.log(child.name); // Li console.log(child.age); // 25
Child.prototype = new Parent("Li")就表示Child的實例化對象繼承了Parent這個類,prototype這種繼承形式被不少人詬病,但它確實有強大的一面。例如咱們都知道對象類型的變量是引用,引用是共享地址的,一個的屬性值改變了,另外一個的值跟着變,以下
function Game(){ this.name = "2048"; } var o1 = new Game(); console.log(o1.name); // 2048 var o2 = o1; o2.name = "gta"; console.log(o1.name); // gta
當從Game建立對象時,兩個對象的方法是徹底分開的,各有一份
function Game(){ this.name = "2048"; this.m = function(name){ this.name = name; } } var o1 = new Game(); var o2 = new Game(); console.log(o1.m == o2.m); // false,兩個對象的方法不相等 o1.m("gta"); o2.m("angry bird"); console.log(o1.name); // gta console.log(o2.name); // angry bird,o一、o2各改各的值
但在原型中定義的屬性
// 以上例爲基礎 Game.prototype.f = function(){ this.name = "unknown"; }; console.log(o1.f === o2.f); // true,o一、o2的f方法相等
即在prototype中定義的屬性,是被全部實對象共享的,普通對象的屬性,每實例化一次,它們的屬性、方法就被複制一份存到各自的對象裏面去,但prototype中定義的只有一份,由於一個構造函數只關聯一個prototype,而全部的對象從這個構造函數實例化而來。故理論上全部函數都可做爲構造函數並用做原型繼承(固然還有一點限制),他們都有prototype屬性。
那麼當具體訪問對象的某一屬性時,如何找到?好比o1.name,首先在o1對象中找,當它本身對象中沒有該屬性時,他會到它的構造函數的原型---Game.prototype所指向的對象中去找,而Game.prototype又是由另外一個構造函數設爲A,實例化獲得的對象,這個對象對應的構造函數A也有本身的prototype---A.prototype,當在Game.prototype中沒找到時,又會去A.prototype對中去找,直到最終的老祖宗Object的prototype---它是undefined,此時如還未找到就會報錯,找到了的話在哪一個裏面就用哪一個。這種一級一級的、回溯的prototype造成的鏈式結構被稱爲原型鏈,這種方式也被稱爲原型鏈繼承。
function Animal(){ this.name = 'animal'; } function Dog(){ this.type = 'dog'; } Dog.prototype = new Animal(); // 原型繼承 function Husky(){ this.weight = 2.3; } Husky.prototype = new Dog(); var o = new Husky(); // 順着原型鏈查照 console.log(o.name); console.log(o.type); console.log(o.weight);
原型的第一大優勢就是屬性(方法)只保留一份,各對象共享,節省空間,還有個優勢是能夠動態的添加屬性。上例中,o一、o2已經實例化了,在函數對象Game的prototype屬性上添加其餘值時,對象依然能夠訪問到,緣由就是先找本身對象中的屬性,再順着原型鏈找,原型中有那就用它了。
只有函數纔有prototype屬性且能夠訪問到,其餘對象實例是沒有的,但其餘對象實例有一個__proto__,聽說它是一個內在的指針,指向對象所繼承的類,不少瀏覽器都視其爲私有屬性而不可訪問,貌似火狐將其暴露了出來。在chrome的控制檯中能夠大體能夠看到這種風結構
對象的__proto__指向Object,數組的__proto__指向Array,正由於如此,咱們才能調用到一些對象的方法,好比var a = [1,2,3]; a.push(4);,由於push已經被封裝在Array裏面了,它們在調用屬性、方法時也會順着__proto__這個鏈去找。
原型或者說原型對象,是類的惟一標識,也就是說當兩個對象繼承自同一個原型對象時,它們才屬於同一個類的實例。當兩個構造函數的prototype是指向同一個對象時,由它們建立的實例纔是同類的,如何判斷對象是否是屬於某個類?可用instanceof運算符,如
o instanceof Husky
若是o繼承自Husky.prototype(注意不是Husky),則返回true。既然這樣爲什麼不直接寫 o instanceof Husky.prototype ?結果報錯了:instanceof右邊必須是個函數。另外一種方法是isPrototypeOf,如
Husky.prototype.isPrototypeOf(o)
若o爲Husky的實例化對象則返回true,Husky.prototype是o的原型。
若是觀察了chrome的控制檯打印對象的話,特別是經過new表達式生成的對象,就會發如今對象的原型__proto__所指的對象中有一個constructor屬性,這個屬性的值就是它本身的構造函數(對象)
對於一個完整的原型對象來講,它的constructor屬性是它自己的構造函數對象。因此更好的處理是這樣的
function Animal(){ this.name = "animal"; } function Dog(){ this.type = "dog"; } Dog.prototype = new Animal(); Dog.prototype.constructor = Dog; // 將原型constructor屬性指向本身的構造函數
這裏有幾個容易讓人糊塗的東西:構造函數對象、實例化對象、原型對象。梳理一下:
稍微標準點的原型式繼承不是直接B.prototype = new A(),而是封裝到函數裏面,以下
function inherit(obj){ // 返回繼承自對象obj的新實例 if(obj === null) return null; if(Object.create) // 使用ECMAScript5函數 return Object.create(obj); var t = typeof obj; if(t != "object" && t != "function") return null; function f(){} // 定義一個空函數 f.prototype = obj; return new f(); // 返回繼承原型的實例對象 } var obj = { x:5, m:function(){console.log(this.x);} }; var subObject = inherit(obj); // 子對象
這裏用到了Object.create()函數,它的第一個參數是對象,若是隻傳第一個參數如obj,它將返回指定原型爲obj對象的新對象。若是Object.create不存在,建立一個空函數,讓函數的prototype指向obj,再返回以這個空函數的實例對象,也完成了原型式繼承(有的把這個稱做寄生式繼承)。再看下例
function A(){ this.name = "A"; } function B(){ this.type = "B"; } B.prototype = inherit(A.prototype); B.prototype.constructor = B; // 將constructor指向本身的構造函數 var o = new B();
o是B的實例,B是A的子類,則需保證B的原型對象繼承自A的原型對象,若是不這樣,B的原型只繼承自Object.prototype,那麼o跟一個普通實例沒有差異。OK,這種寫法,o裏面有沒有屬性name?事實是沒有。前面在說類式繼承時,父類構造函數在子類構造函數裏面調用一下,其實至關因而給子類實例添加了屬性,因此子類實例才擁有這個屬性,這裏B的prototype只繼承自A的prototype,也不是前面的原型繼承時寫的B.prototype = new A()(這樣寫o.name是存在的),都是原型繼承,兩種不一樣寫法形成了這一差異。因此原型繼承的核心仍是在於:o的構造函數對象B的prototype原型到底包沒包含所需的屬性。
針對以上類式繼承和原型式繼承各自的優缺點,又來了個新方式:組合繼承。組合固然是兼具類式繼承和原型式繼承的優勢了。
function Parent(){ this.name = "parent"; } function Child(){ this.type = "child"; } // 先處理原型鏈 Child.prototype = new Parent(); Child.prototype.constructor = Child; // 而後添加需繼承的屬性和方法 Child.prototype.p = "2333"; Child.prototype.f = function(){ console.log("hello"); }; var o = new Child();
以上只是零散的組合先設置好原型鏈,而後手動給原型添加屬性和方法,代替類式繼承的直接傳參,以此克服弊端。若是寫的標準點,首先準備個給原型添加屬性的小函數:
// 將對象q的屬性添加到p中 extend(p, q){ if(p == null || q == null) return; if(typeof p != "object" || typeof q != "object") return; for(var prop in q) p[prop] = q[prop]; }
另外一個工具工具函數
/* * 實現子類的繼承 * @param superClass 父類構造函數對象 * @param subClass 子類構造函數對象 * @param methods 包含函數屬性的對象 * @param props 包含屬性的對象 * return function 組合繼承後的構造函數 */ function inheritClass(superClass, subClass, methods, props){ // 使子類在原型上繼承自父類 subClass.prototype = inherit(superClass.prototype); subClass.prototype.constructor = subClass; // 將所需方法添加至子類原型中 if(typeof methods == "object") extend(subClass.prototype, methods); // 將所需屬性添加至子類原型中 if(typeof props == "object") extend(subClass,.prototype, props); // 返回子類 return subClass; }
superClass是父類構造函數,subClass是子類構造函數,methods與props爲將要添加的方法和屬性的集合,以對象的方式包裝起來。借用了inherit和extend方法,inherit方法設置好原型鏈,爲了克服沒法直接傳參的弊端,將方法和屬性包裝好後,動態的添加到子類構造函數的prototype中。
如inheritClass方法添加方法和屬性時,這樣寫extend(subClass, methods),將給subClass這個函數對象添加的是私有屬性,對象打點沒法訪問,只有構造函數打點能夠訪問,在其餘語言中,靜態變量或常量相似這種形式,因此這些屬性在js中稱爲類的私有屬性。
測試代碼打印下看效果,依然在chrome控制檯
function Parent(){ this.p = "parent"; } function Child(){ this.c = "child"; } // 添加的方法對象 var mObj = { sayHello:function(){ console.log("hello") } }; // 添加的屬性對象 var pObj = { pos:"developer" }; // 實現繼承 var InheritedChild = inheritClass(Parent, Child, mObj, pObj); var obj = new InheritedChild(); console.log(obj);
console.table打印一個樹形結構,obj是對象,因此是__proto__,它關聯給本身實例化的構造函數的原型對象,表現上就是__proto__: Child,這個原型中有constructor屬性,值爲Child函數對象,還有添加進去的pos屬性和sayHello方法,這個原型又關聯這個Parent的prototype原型對象:__proto__:Parent,同理這個原型有本身的constructor屬性,並最終繼承自Object。
繼承關係上大體就是這樣,亂七八糟一堆,也許一個$.extend()方法就解決了~