概要:
一、繼承的原理
二、繼承的幾種方式
三、繼承的應用場景
什么是繼承?
繼承:子類可以使用父類的所有功能,並且對這些功能進行擴展。繼承的過程,就是從一般到特殊的過程。
要了解JS繼承必須首先要了解this的指向,原型prototype、構造器constructor、原型鏈_proto_;
第一:關於this的指向問題:
// "use strict" //嚴格模式(strict mode)即在嚴格的條件下運行,在嚴格模式下,很多正常情況下不會報錯的問題語句,將會報錯並阻止運行 //this是什么? JS關鍵字,在JS中具有特殊意義,代表一個空間地址 //this的指向,是在函數被調用的時候確定的。也就是執行上下文被創建的時候確定的 // 一、初始情況下 // 全局作用域下的this指向:即this和window指向同一個堆內存 this == window; //true this === window; //true // console.log("init," + this) //二、函數和對象里的this的指向 var x = "x"; function test() { var x = 1; // this.x = 2; console.log(this); console.log(this ? this.x : ''); } //直接調用
// 嚴格模式下,如果調用者函數,被某一個對象所擁有,那么該函數在調用時,內部的this指向該對象;如果函數獨立調用,那么該函數內部的this,則指向undefined。
test(); // 非嚴格模式下調用相當於window.test(), this指向window;嚴格模式下this等於undefine
var y = 2; var Arrow = () => { y = 3; console.log("arrow," + this); console.log(this ? "arrow," + this.y : ''); }; Arrow(); //箭頭函數條用時指向上下文this指向window,嚴格模式下也是指向window console.log("------------------------函數自調用------------------------") //賦值調用 var obj = { x: 1, fn: test, fns: { fn: test, x: "FNC" }, y: 3, Arrow: Arrow, fnArrow: { Arrow: Arrow, y: 5 } } var fn = obj.fn; //定義變量相當於在window對象添加一個屬性,window下調用給,他的上層對象就是window,所以this指向window fn(); // 調用的是window.fn(), this指向window。嚴格模式下為undefined obj.fn() //相當於window.obj.fn();this指向上層對象:obj;嚴格模式下也是指向上層對象 obj.fns.fn(); //this指向上層對象:fns;嚴格模式下也是指向上層對象 console.log("------------------------對象里面調用函數------------------------") obj.Arrow(); //嚴格模式下this指向window obj.fnArrow.Arrow(); //嚴格模式下this指向window console.log("------------------------對象里面調用箭頭函數------------------------") //三、構造函數的this指向,構造函數在new實例化的過程中改變了this的指向,所以this指向當前函數的實例對象 function Structure() { this.user = "二營長"; console.log(this) } var StructureArrow = () => { console.log(this) } var a = new Structure(); console.log(a.user); //二營長 console.log("type," + Object.prototype.toString.call(StructureArrow)) // var sa = new StructureArrow(); //StructureArrow is not a constructor; new關鍵字只能實例化有prototype的函數對象,所以這里拋出異常 // 這里之所以對象a可以點出函數Structure里面的user是因為new關鍵字可以改變this的指向,將這個this指向對象a, // 為什么我說a是對象,因為用了new關鍵字就是創建一個對象實例, // 我們這里用變量a創建了一個Structure的實例(相當於復制了一份Structure到對象a里面),此時僅僅只是創建,並沒有執行, // 而調用這個函數Structure的是對象a,那么this指向的自然是對象a,那么為什么對象a中會有user, // 因為你已經復制了一份Structure函數到對象a中,用了new關鍵字就等同於復制了一份。//四、箭頭函數的this指向 console.log("------------------------箭頭函數的this指向------------------------") var ar = 0; function arrows() { var ar = 1; var f = () => { console.log(this); console.log(this ? this.ar : ''); } return f(); } var arrowsobj = { ar: 2, fn: arrows, fn1: () => { console.log(this); //箭頭函數將this指向當前環境上下文,即this指向全局環境中的this,即window console.log(this ? this.ar : ''); //0 箭頭函數將this指向當前環境上下文,即this指向全局環境中的this,即window }, fn2: function() { setTimeout(() => { console.log(this); //指向arrowsobj console.log(this ? this.ar : ''); //2 }, 0) }, fnc: { fn1: () => { console.log(this); //箭頭函數將this指向當前環境上下文,即this指向全局環境中的this,即window console.log(this ? this.ar : ''); //0 箭頭函數將this指向當前環境上下文,即this指向全局環境中的this,即window } } }; arrows(); //非嚴格模式下為this.ar 為0;嚴格模式下報錯 arrowsobj.fn(); //this.ar 為2;嚴格模式下報錯箭頭函數將this指向當前環境上下文,即this指向test中的this,即arrowsobj // arrowsfn(); arrowsobj.fn1(); arrowsobj.fn2(); arrowsobj.fnc.fn1(); console.log("------------------------事件里面的this指向------------------------") //五、事件里面的this指向 //事件綁定 var btn = document.querySelector("body"); btn.onclick = function() { console.log(this) // 調用的是btn.onclick, this指向body } //事件監聽 var btns = document.querySelector("html") btns.onclick = function() { var timers = setTimeout(() => { console.log(this) //上下文對象是body }, 1000); } // this指向window // 全局作用域下的this都是:即this和window指向同一個堆內存; // 自執行函數中的this都是 // 函數作為參數里面的this一般都是 // 回調函數中的this指向; // 遞歸函數中的this指向; // this不是window // 函數執行的時候判斷函數前面是否有點,如果有點,前面是誰this就是;如果沒有點this就是; // 給當前元素某個事件綁定方法,元素觸發事件,事件中的; // 構造函數中的; // 通過call apply bind可以改變this指向,指向誰就是誰; // 箭頭函數中沒有this;
第二、prototype(JS對象)
javascript中的每個對象都有prototype屬性,Javascript中對象的prototype屬性的解釋是:返回對象類型原型的引用。
每一個構造函數都有一個屬性叫做原型。這個屬性非常有用:為一個特定類聲明通用的變量或者函數。
你不需要顯式地聲明一個prototype屬性,因為在每一個構造函數中都有它的存在。
在JavaScript中,prototype對象是實現面向對象的一個重要機制。
每個函數就是一個對象(Function),函數對象都有一個子對象 prototype對象,類是以函數的形式來定義的。prototype表示該函數的原型,也表示一個類的成員的集合。
第三、constructor (構造器,指向創建自己的那個構造函數)
在JavaScript中,每個具有原型的對象都會自動獲得constructor屬性。除了arguments、Error、Global、Math、RegExp、Regular Expression、Enumerator等一些特殊對象之外,其他所有的JavaScript內置對象都具備constructor屬性。例如:Array、Boolean、Date、Function、Number、Object、String等。所有主流瀏覽器均支持該屬性
第四、原型鏈_proto_(每一個對象都有一個_proto_屬性,這個屬性指向一個對象,這個對象是原型對象)
由__proto__組成的鏈條叫做原型鏈 (訪問一個對象的屬性時,先在基本屬性中查找,如果沒有,再沿着_proto_這條鏈向上找,這就是原型鏈。
// 在js中萬物皆對象,對象又分為兩種:普通對象(Object)和函數對象(Function)。 // prototype:每一個函數都有一個prototyp屬性 這個屬性指向一個對象 這個對象叫做原型對象; //constructor:構造器,指向創建自己的那個構造函數 // __proto__:每一個對象都有一個_proto_屬性,這個屬性指向一個對象,這個對象是原型對象 // 原型對象里面有2個屬性:constructor,__proto__ // 任何對象都具有隱式原型屬性(__proto__),只有函數對象有顯式原型屬性(prototype)。 // 原型鏈:由__proto__組成的鏈條叫做原型鏈 (訪問一個對象的屬性時,先在基本屬性中查找,如果沒有,再沿着_proto_這條鏈向上找,這就是原型鏈。) function fn() { console.log("fn") } console.log(fn); console.dir(fn); // 一.函數對象和一般對象 var Literal = function() { console.log("Literal") } console.log(Literal) //輸出函數自身,字符串 console.dir(Literal) //輸出對象 var f = new Function("arg", "statement"); console.dir(f); //注意:由上面兩條得出,字面量函數和關鍵字函數都擁有這兩個特殊的屬性__proto__和prototype var newLiteral = new Literal(); console.log(newLiteral) //輸出對象 console.dir(newLiteral) //輸出對象 //new關鍵字的構造函數,相當於實例化一個對象,將實例的函數重新拷貝一份,所以只有__proto__ var obj = { fn: function() { console.log("2") }, //擁有__proto__和prototype newfn: new fn(), //new關鍵字的構造函數,相當於實例化一個對象,將實例的函數重新拷貝一份,所以只有__proto__ obj: {}, //__proto__ arr: [], //__proto__ x: '6', type: null }; console.log(obj); // new一個對象時,會經歷以下幾個步驟(摘自javascript高級程序設計): // (1)創建一個對象; // (2)將構造函數的作用域賦值給新對象(因此this就指向了這個新對象); // (3)執行構造函數中的代碼(為這個新對象添加屬性); // (4)返回新對象 // 創建了一個全新的對象。 // 這個對象會被執行 [[Prototype]](也就是 proto)鏈接。 // 生成的新對象會綁定到函數調用的 this。 // 通過 new創建的每個對象將最終被 [[Prototype]]鏈接到這個函數的 prototype對象上。 // 如果函數沒有返回對象類型 Object(包含 Functoin, Array, Date, RegExg, Error),那么 new表達式中的函數調用會自動返回這個新的對象。 Object.prototype.__proto__ == null Object.__proto__ == Functon.prototype Function.protype.__proto__ == Object.prototype Function.__proto__ == Function.prototype // 原型鏈都指向了null對象,也就是正常的結束,不會死循環
他們之間的關系圖:
二、繼承的幾種方式
1.原型鏈繼承(類式繼承): 將父函數的實例繼承給子函數prototype原型屬性;
2.借用構造函數繼承: 使用父類的構造函數來增強子類實例,等於是復制父類的實例屬性給子類(跟原型沒有任何關系)
3.組合式繼承: 結合了原型鏈繼承和構造函數繼承兩種模式的優點,傳參和復用,在子類構造函數中執行父類構造函數,在子類
原型上實例化父類。(最常用)
4.原型式繼承 : 封裝一個函數,該函數返回一個不含實例屬性的對象,然后對這個對象逐步增強(逐步添加實例屬性)
5.寄生繼承:創建對象-增強-返回該對象,這樣的過程叫做寄生式繼承(寄生式繼承和原型式繼承是緊密相關的一種思路。寄生式繼承就是給原型式繼承穿了個馬甲而已)
6.寄生組合式繼承:通過寄生方式,砍掉了子類原型對象上多余的那份父類實例屬性,這樣,在調用兩次父類的構造函數的時候,就不會初始化兩次實例方法/屬性,避免了組合繼承的缺點(最理想)
7.class類繼承:consuctor的super方法;
第一、類式繼承(原型鏈繼承)
//一、類式繼承(原型鏈繼承) //聲明父類 function Parent() { var sex = "man"; //私有屬性 var height = [178]; //私有引用屬性 function sleep() { console.log("sleep") } //私有函數(引用屬性) this.books = ["javascript", "css", "html"]; this.ParentInfo = true; //實例屬性 this.height = [179]; //實例引用屬性 this.sleep = function() {}; //實例函數(引用屬性) this.name = "父類"; } Parent.prototype.getParentBaseinfo = function() { return this.ParentInfo; } Parent.addClass = "human"; //私有屬性,子類繼承后會在Child.prototype.__proto__.constructor里面 //聲明子類 // console.dir(Parent); function Child() { this.ChildInfo = false; return this; } // Parent(); //繼承父類 Child.prototype = new Parent(); Child.prototype.getChildBaseinfo = function() { return this.ChildInfo; } console.dir(Child); // 繼承父類之后,子類可以使用父類的實例屬性以及父類的原型屬性 console.log(Child.prototype.getParentBaseinfo(), Child.prototype.__proto__.constructor.addClass) // Child.prototype.__proto__.constructor==Parent console.log(new Parent()) // 缺點1、由於子類通過其原型prototype對父類實例化,繼承了父類,所以說父類的共有屬性要是引用類型,就會在之類中被所有實例共用 //因此一個子類的實例更改子類原型從父類構造函數中繼承來的屬性就會直接影響到其他子類 //如下所述 var instance1 = deepClone(new Child()); var instance2 = new Child(); console.log(instance1, instance2, instance2.books) //["javascript", "css", "html"] instance1.books.push("設計模式"); console.log(instance1, instance2.books) // ["javascript", "css", "html", "設計模式"] // 解決方案:深拷貝=>但是每次實例化的時候都需要進行深拷貝比較麻煩 function deepClone(initalObj, finalObj) { var obj = finalObj || {}; for (var i in initalObj) { var prop = initalObj[i]; // 避免相互引用對象導致死循環,如initalObj.a = initalObj的情況 if (prop === obj) { continue; } if (typeof prop === 'object') { obj[i] = (prop.constructor === Array) ? [] : {}; arguments.callee(prop, obj[i]); } else { obj[i] = prop; } } return obj; } // 缺點2,無法向父類構造函數傳遞參數,由於子類實現繼承是靠其原型prptotype對父類的實例化實現的,因此創建父類的時候,是無法向父類傳遞參數的,
//因此在實例化父類的時候, // 也無法對父類構造函數內的屬性進行初始化 console.log("--------------------傳遞參數示例---------------------") function Person(name, age, job) { this.name = name; this.age = age; this.ob = job; } function Man(age) { this.name = age; } var m = new Man('Anthony', 27, 'PE'); m.prototype = new Person('thony', 27, 'PE'); var v = new Man('maker', 32, 'TW'); v.prototype = new Person('Alice', 21, 'VN'); console.dir(m); console.dir(v); //優缺點 //1.優點: // 從已有的對象衍生新的對象,不需要創建自定義類型 // 2.缺點 // (1).新實例無法向父類構造函數傳參( // 並不是語法上不能實現對構造函數的參數傳遞,而是這樣做不符合面向對象編程的規則:對象(實例)才是屬性的擁有者。 // 如果在子類定義時就將屬性賦了值,就變成了類擁有屬性,而不是對象擁有屬性了。) // (2).原型引用屬性會被所有實例所共享,因為是整個父類對象充當子類的原型對象,所以這個缺陷無法避免、 // (3).無法實現代碼的復用 // (4).繼承單一 //類的原型對象的作用就是為類的原型添加共有的方法,打包類不能直接訪問這些屬性和方法,必須通過原型prototype來訪問。而我們實例化一個函數的時候,
//新創建的對象復制了父類構造 // 函數的屬性和方法並且將原型__proto__指向父元素的原型對象,這樣就擁有了父類原型對象上的屬性與方法。
二、借用構造函數繼承
//聲明父類 function Parent(id) { this.books = ['JavaScript', 'html', 'css']; this.id = id || ''; // this.showBooks = function() { // console.log(this.books); // } } function Monther(id) { // this.books = ['UI', 'JAVA']; this.id = id || ''; this.hobby = ['draw', 'movie'] } //父類聲明原型方法 Parent.prototype.showBooks = function() { console.log(this.books); } //聲明子類 function Child(id) { console.log("call改變原實例的this指向", this) Parent.call(this, id); //call改變父類this作用域名的指向 Monther.call(this, id); //call改變母類this作用域名的指向,但是相同屬性后者會覆蓋前者 } var test1 = new Child(11); var test2 = new Child(12); test1.books.push("設計模式"); // console.dir(Parent) // console.dir(Monther) console.log("---------------------輸出測試實例1----------------------") console.log(test1); console.log(test1.books); console.log(test1.id); console.log("---------------------輸出測試實例2----------------------") console.log(test2); console.log(test2.books); console.log(test2.id); // test1.showBooks(); //Parent.call(this,id)是構造函數式的精髓,由於call這個方法可以更改函數的作用環境, // 因此在子類中,對Parent調用這個方法,就是將子類的變量在父類中執行一遍, // 由於父類中是給this綁定屬性的,因此子類自然就繼承了父類的共有屬性。由於這種類型的繼承沒有涉及prototype, // 所以父類的原型方法自然就不會被子類繼承。 //如果想要繼承父類的原型方法就必須綁定在this上面,這樣創建出來的每一個實例都會單獨擁有一份而不能共用, // 這樣就違背了代碼復用的原則。 //為了綜合之前兩種模式的有點於是有了組合式繼承
//三、組合式繼承//聲明父類 function Parent(name) { this.books = ['JavaScript', 'html', 'css']; this.name = name; // this.showBooks =function(){ // console.log(this.books); // } } //父類聲明原型共有方法 Parent.prototype.getName = function() { console.log(this.name); } //聲明子類 function Child(name, time) { //構造函數式繼承父類name屬性 Parent.call(this, name); //call改變父類this作用於的指向,從父類拷貝一份父類的實例屬性給子類作為子類的實例屬性 this.time = time; } Child.prototype = new Parent(); //創建父類實例作為子類的原型 ,此時這個父類實例就又有了一份實例屬性,但這份會被第一次拷貝來的實例屬性屏蔽掉 Child.prototype.getTime = function() { console.log(this.time) } //在子類構造函數中執行父類構造函數,在子類原型上實例化父類就是組合模式,這樣就融合了類式繼承和構造函數的優點,並且過濾掉其缺點。 var fn = new Child('js book', '2018-12-14'); console.dir(fn) fn.books.push("設計模式"); console.log(fn.books); //["JavaScript", "html", "css", "設計模式"] fn.getName(); fn.getTime(); var fnc = new Child('css book', '2019-10-24'); console.log(fnc.books); // ["JavaScript", "html", "css"] fnc.getName(); //css book fnc.getTime(); //2019-10-24
//原型式繼承 function inheritObject(o) { //聲明一個過渡函數對象 function F() {} //過渡對象的原型繼承父對象 F.prototype = o; //返回過渡對象的一個實例,該實例的原型繼承了父對象 return new F(); } var book = { name: "js book", alikebook: ["css book", "html book"] }; var newBook = inheritObject(book); newBook.name = "node book"; newBook.alikebook.push("jquery book"); var otherBook = inheritObject(book); otherBook.name = "flash book"; otherBook.alikebook.push("flash book"); console.log("-------------------輸出newBook---------------------------") console.dir(newBook) console.log(newBook.name) //node book console.log(newBook.alikebook) //["css book", "html book", "jquery book", "flash book"] console.log("-------------------輸出otherBook---------------------------") console.dir(otherBook) console.log(otherBook.name) //flash book console.log(otherBook.alikebook) // ["css book", "html book", "jquery book", "flash book"] console.log("-------------------輸出book對象屬性---------------------------") console.log(book.name) //js book console.log(book.alikebook) //["css book", "html book", "jquery book", "flash book"] //原型繼承,跟類繼承一樣,父類對象book中的值被復制,引用類型的屬性被共用
function inheritObject(o) { //聲明一個過渡函數對象 function F() {} //過渡對象的原型繼承父對象 F.prototype = o; //返回過渡對象的一個實例,該實例的原型繼承了父對象 return new F(); } //聲明函數對象 var book = { name: "JS Book", alineBooks: ["Css Book", "Html Book"] }; function Parent() { var sex = "man"; //私有屬性 var height = [178]; //私有引用屬性 function sleep() { console.log("sleep") } //私有函數(引用屬性) this.books = ["javascript", "css", "html"]; this.ParentInfo = true; //實例屬性 this.height = [179]; //實例引用屬性 this.sleep = function() {}; //實例函數(引用屬性) this.alineBooks = ["Css Book", "Html Book"] this.name = "父類"; } Parent.prototype.getParentBaseinfo = function() { return this.ParentInfo; } var par = new Parent() function createBook(obj) { var o = new inheritObject(obj); // console.log(o) o.getName = function() { console.log(name); }; return o; } var getBook = createBook(par); //函數生命呢之后可以添加其他屬性 getBook.setname = function() { this.name = "Java Book" } var getnewBook = createBook(par); getBook.name = "Node Book"; getBook.alineBooks.push("PDF Book") console.log("-----------------------getBook----------------") console.dir(getBook) console.log(getBook.name); console.log(getBook.alineBooks); console.log("-----------------------getnewBook----------------") console.dir(getnewBook) console.log(getnewBook.alineBooks) console.log("-----------------------book----------------") console.dir(par) // console.log(book.name); // console.log(book.alineBooks);
//寄生組合式繼承(寄生式+原型:通過借用函數來繼承屬性,通過原型鏈的混成形式來繼承方法) function inheritObject(o) { //聲明一個過渡函數對象 function F() {} //過渡對象的原型繼承父對象 F.prototype = o; //返回過渡對象的一個實例,該實例的原型繼承了父對象 return new F(); } function inheritPrototype(Child, Parent) { //復制一份父類的原型副本保存在變量中 var p = inheritObject(Parent.prototype); //修正因為重寫子類的原型導致子類的constructor屬性被修改 p.constructor = Child; //設置子類的原型 Child.prototype = p; } //定義父類 function Parent(name) { this.name = name; this.colors = ["red", "blue", "green"]; } //定義父類的原型方法 Parent.prototype.getName = function() { console.log(this.name); } function Child(name, time) { //構造函數式繼承 Parent.call(this, name); this.time = time; } //寄生式繼承父類原型 inheritPrototype(Child, Parent); //子類新增原型方法 Child.prototype.getTime = function() { console.log(this.time); }; //創建兩個測試方法 var test1 = new Child("js book", "2018-01-02"); var test2 = new Child("css book", "2018-01-03"); test1.colors.push("black") test2.getName() //css book test2.getTime() //2018-01-03 console.dir("-----------------test1--------------------") console.dir(test1) console.log(test1.colors); //["red", "blue", "green", "black"] console.dir("-----------------test2--------------------") console.dir(test2) console.log(test2.colors); // ["red", "blue", "green"]
class Parent{ //屬性 constructor(name,age){ this.name = name; this.age = age; } eat(){ console.log('111') } show(){ console.log('222') } } //ES6的繼承 class Man extends Parent{ constructor(beard,name,age){ super(name,age)//super調用父類的構造方法! this.beard = beard; } work(){} } var p2 = new Man(10,"張家輝",40); var p1 = new Man(10,"古天樂",41); console.log(p1,p2) //優缺點,代碼簡潔,但是有兼容性問題
三、JS繼承的應用場景
JS繼承的話主要用於面向對象的變成中,試用場景的話還是以單頁面應用或者JS為主的開發里,因為如果只是在頁面級的開發中很少會用到JS繼承的方式,與其說繼承,還不如直接寫個函數來的簡單直接有效一些。
想用繼承的話最好是那種主要以JS為主開發的大型項目,比如說單頁面的應用或者寫JS框架,前台的所有東西都用JS來完成,整個站的跳轉,部分邏輯,數據處理等大部分使用JS來做,這樣面向對象的編程才有存在的價值和意義
為什么要繼承:通常在一般的項目里不需要,因為應用簡單,但你要用純js做一些復雜的工具或框架系統就要用到了,比如webgis、或者js框架如jquery、ext什么的,不然一個幾千行代碼的框架不用繼承得寫幾萬行,甚至還無法維護