摘要:共享原型鏈現在我們還有另一個對象如圖那么和其實是同一東西,也就是。改進通過第一節可以知道,我們可以通過原型鏈來解決重復創建的問題我們先創建一個士兵原型,然后讓士兵的指向士兵原型。所以這個是原型鏈繼承的缺點,原因是和指向同一個地址即父類的。
在理解繼承之前,需要知道 js 的三個東西:
什么是 JS 原型鏈
this 的值到底是什么
JS 的new 到底是干什么的
想閱讀更多優質文章請猛戳GitHub博客,一年百來篇優質文章等著你!
一、什么是 JS 原型鏈?我們知道 JS 有對象,比如
var obj = { name: "obj" }
我們通過控制臺把obj 打印出來:
我們會發現 obj已經有幾個屬性(方法)了。那么問題來了:valueOf/toString/constructor 是怎么來?我們并沒有給 obj.valueOf 賦值呀。
上面這個圖有點難懂,手畫一個示意圖:
我們發現控制臺打出來的結果是:
obj本身有一個屬性 name (這是我們給它加的)
obj還有一個屬性叫做 __proto__(它是一個對象)
obj還有一個屬性,包括 valueOf, toString, constructor等
obj.__proto__其實也有一個叫做__proto__的屬性(console.log沒有顯示),值為 null
現在回到我們的問題:obj 為什么會擁有 valueOf / toString / constructor 這幾個屬性?
答案: 這跟 __proto__有關 。
當我們「讀取」 obj.toString 時,JS 引擎會做下面的事情:
看看 obj 對象本身有沒有 toString 屬性。沒有就走到下一步。
看看 obj.__proto__ 對象有沒有 toString 屬性, 發現 obj.__proto__ 有 toString 屬性, 于是找到了,所以 obj.toString實際就是第2步中找到的 obj.__proto__.toString。
如果 obj.__proto__沒有,那么瀏覽器會繼續查看 obj.__proto__.__proto__。
如果 obj.__proto__.__proto__ 也沒有,那么瀏覽器會繼續查看 obj.__proto__.__proto__.__proto__。
5.直到找到 toString 或者 __proto__ 為 null。
上面的過程,就是「讀」屬性的「搜索過程」。而這個「搜索過程」,是連著由 proto 組成的鏈子一直走的。這個鏈子,就叫做「原型鏈」。
共享原型鏈現在我們還有另一個對象
var obj2 = { name: "obj2" }
如圖:
那么 obj.toString 和 obj2.toString 其實是同一東西, 也就是 obj2.__proto__.toString。
說白了,我們改其中的一個 __proto__.toString ,那么另外一個其實也會變!
如果我們想讓 obj.toString 和 obj2.toString 的行為不同怎么做呢?
直接賦值就好了:
obj.toString = function(){ return "新的 toString 方法" }
小結
[讀]屬性時會沿著原型鏈搜索
[新增]屬性時不會去看原型鏈
二、 this 的值到底是什么你可能遇到過這樣的 JS 面試題:
var obj = { foo: function(){ console.log(this) } } var bar = obj.foo obj.foo() // 打印出的 this 是 obj bar() // 打印出的 this 是 window
請解釋最后兩行函數的值為什么不一樣。
函數調用JS(ES5)里面有三種函數調用形式:
func(p1, p2) obj.child.method(p1, p2) func.call(context, p1, p2) // 先不講 apply
一般,初學者都知道前兩種形式,而且認為前兩種形式「優于」第三種形式。
我們方方老師大姥說了,你一定要記住,第三種調用形式,才是正常調用形式:
func.call(context, p1, p2)
其他兩種都是語法糖,可以等價地變為 call 形式:
func(p1, p2)等價于 func.call(undefined, p1, p2);
obj.child.method(p1, p2) 等價于 obj.child.method.call(obj.child, p1, p2);
至此我們的函數調用只有一種形式:
func.call(context, p1, p2)
這樣,this 就好解釋了 this就是上面 context。
this 是你 call 一個函數時傳的 context,由于你從來不用 call 形式的函數調用,所以你一直不知道。
先看 func(p1, p2) 中的 this 如何確定:
當你寫下面代碼時 function func(){ console.log(this) } func() 等價于 function func(){ console.log(this) } func.call(undefined) // 可以簡寫為 func.call()
按理說打印出來的 this 應該就是 undefined 了吧,但是瀏覽器里有一條規則:
如果你傳的 context 就 null 或者 undefined,那么 window 對象就是默認的 context(嚴格模式下默認 context 是 undefined)
因此上面的打印結果是 window。如果你希望這里的 this 不是 window,很簡單:
func.call(obj) // 那么里面的 this 就是 obj 對象了
回到題目:
var obj = { foo: function(){ console.log(this) } } var bar = obj.foo obj.foo() // 轉換為 obj.foo.call(obj),this 就是 obj bar() // 轉換為 bar.call() // 由于沒有傳 context // 所以 this 就是 undefined // 最后瀏覽器給你一個默認的 this —— window 對象[ ] 語法
function fn (){ console.log(this) } var arr = [fn, fn2] arr[0]() // 這里面的 this 又是什么呢?
我們可以把 arr[0]( ) 想象為arr.0( ),雖然后者的語法錯了,但是形式與轉換代碼里的 obj.child.method(p1, p2) 對應上了,于是就可以愉快的轉換了:
arr[0]() 假想為 arr.0() 然后轉換為 arr.0.call(arr) 那么里面的 this 就是 arr 了
小結:
this 就是你 call 一個函數時,傳入的第一個參數。
如果你的函數調用不是 call 形式, 請將其轉換為 call 形式
三、JS 的 new 到底是干什么的?我們聲明一個士兵,具有如下屬性:
var 士兵 = { ID: 1, // 用于區分每個士兵 兵種:"美國大兵", 攻擊力:5, 生命值:42, 行走:function(){ /*走倆步的代碼*/}, 奔跑:function(){ /*狂奔的代碼*/ }, 死亡:function(){ /*Go die*/ }, 攻擊:function(){ /*糊他熊臉*/ }, 防御:function(){ /*護臉*/ } }
我們制造一個士兵, 只需要這樣:
兵營.制造(士兵)
如果需要制造 100 個士兵怎么辦呢?
循環 100 次吧: var 士兵們 = [] var 士兵 for(var i=0; i<100; i++){ 士兵 = { ID: i, // ID 不能重復 兵種:"美國大兵", 攻擊力:5, 生命值:42, 行走:function(){ /*走倆步的代碼*/}, 奔跑:function(){ /*狂奔的代碼*/ }, 死亡:function(){ /*Go die*/ }, 攻擊:function(){ /*糊他熊臉*/ }, 防御:function(){ /*護臉*/ } } 士兵們.push(士兵) } 兵營.批量制造(士兵們)
哎呀,看起來好簡單
質疑上面的代碼存在一個問題:浪費了很多內存
行走、奔跑、死亡、攻擊、防御這五個動作對于每個士兵其實是一樣的,只需要各自引用同一個函數就可以了,沒必要重復創建 100 個行走、100個奔跑……
這些士兵的兵種和攻擊力都是一樣的,沒必要創建 100 次。
只有 ID 和生命值需要創建 100 次,因為每個士兵有自己的 ID 和生命值。
改進通過第一節可以知道 ,我們可以通過原型鏈來解決重復創建的問題:我們先創建一個「士兵原型」,然后讓「士兵」的 proto 指向「士兵原型」。
var 士兵原型 = { 兵種:"美國大兵", 攻擊力:5, 行走:function(){ /*走倆步的代碼*/}, 奔跑:function(){ /*狂奔的代碼*/ }, 死亡:function(){ /*Go die*/ }, 攻擊:function(){ /*糊他熊臉*/ }, 防御:function(){ /*護臉*/ } } var 士兵們 = [] var 士兵 for(var i=0; i<100; i++){ 士兵 = { ID: i, // ID 不能重復 生命值:42 } /*實際工作中不要這樣寫,因為 __proto__ 不是標準屬性*/ 士兵.__proto__ = 士兵原型 士兵們.push(士兵) } 兵營.批量制造(士兵們)優雅?
有人指出創建一個士兵的代碼分散在兩個地方很不優雅,于是我們用一個函數把這兩部分聯系起來:
function 士兵(ID){ var 臨時對象 = {}; 臨時對象.__proto__ = 士兵.原型; 臨時對象.ID = ID; 臨時對象.生命值 = 42; return 臨時對象; } 士兵.原型 = { 兵種:"美國大兵", 攻擊力:5, 行走:function(){ /*走倆步的代碼*/}, 奔跑:function(){ /*狂奔的代碼*/ }, 死亡:function(){ /*Go die*/ }, 攻擊:function(){ /*糊他熊臉*/ }, 防御:function(){ /*護臉*/ } } // 保存為文件:士兵.js 然后就可以愉快地引用「士兵」來創建士兵了: var 士兵們 = [] for(var i=0; i<100; i++){ 士兵們.push(士兵(i)) } 兵營.批量制造(士兵們)
JS 之父看到大家都這么搞,覺得何必呢,我給你們個糖吃,于是 JS 之父創建了 new 關鍵字,可以讓我們少寫幾行代碼:
只要你在士兵前面使用 new 關鍵字,那么可以少做四件事情:
不用創建臨時對象,因為 new 會幫你做(你使用「this」就可以訪問到臨時對象);
不用綁定原型,因為new 會幫你做(new 為了知道原型在哪,所以指定原型的名字 prototype);
不用 return 臨時對象,因為 new 會幫你做;
不要給原型想名字了,因為 new 指定名字為 prototype。
這一次用 new 來寫function 士兵(ID){ this.ID = ID this.生命值 = 42 } 士兵.prototype = { 兵種:"美國大兵", 攻擊力:5, 行走:function(){ /*走倆步的代碼*/}, 奔跑:function(){ /*狂奔的代碼*/ }, 死亡:function(){ /*Go die*/ }, 攻擊:function(){ /*糊他熊臉*/ }, 防御:function(){ /*護臉*/ } } // 保存為文件:士兵.js 然后是創建士兵(加了一個 new 關鍵字): var 士兵們 = [] for(var i=0; i<100; i++){ 士兵們.push(new 士兵(i)) } 兵營.批量制造(士兵們)
new 的作用,就是省那么幾行代碼。(也就是所謂的語法糖)
注意 constructor 屬性new 操作為了記錄「臨時對象是由哪個函數創建的」,所以預先給「士兵.prototype」加了一個 constructor 屬性:
士兵.prototype = { constructor: 士兵 }
如果你重新對「士兵.prototype」賦值,那么這個 constructor 屬性就沒了,所以你應該這么寫:
士兵.prototype.兵種 = "美國大兵" 士兵.prototype.攻擊力 = 5 士兵.prototype.行走 = function(){ /*走倆步的代碼*/} 士兵.prototype.奔跑 = function(){ /*狂奔的代碼*/ } 士兵.prototype.死亡 = function(){ /*Go die*/ } 士兵.prototype.攻擊 = function(){ /*糊他熊臉*/ } 士兵.prototype.防御 = function(){ /*護臉*/ }
或者你也可以自己給 constructor 重新賦值:
士兵.prototype = { constructor: 士兵, 兵種:"美國大兵", 攻擊力:5, 行走:function(){ /*走倆步的代碼*/}, 奔跑:function(){ /*狂奔的代碼*/ }, 死亡:function(){ /*Go die*/ }, 攻擊:function(){ /*糊他熊臉*/ }, 防御:function(){ /*護臉*/ } }四、繼承
繼承的本質就是上面的講的原型鏈
1)借助構造函數實現繼承function Parent1() { this.name = "parent1"; } Parent1.prototype.say = function () {} function Child1() { Parent1.call(this); this.type = "child"; } console.log(new Child1);
打印結果:
這個主要是借用call 來改變this的指向,通過 call 調用 Parent ,此時 Parent 中的 this 是指 Child1。有個缺點,從打印結果看出 Child1并沒有say方法,所以這種只能繼承父類的實例屬性和方法,不能繼承原型屬性/方法。
2)借助原型鏈實現繼承/** * 借助原型鏈實現繼承 */ function Parent2() { this.name = "parent2"; this.play = [1, 2, 3]; } function Child2() { this.type = "child2"; } Child2.prototype = new Parent2(); console.log(new Child2); var s1 = new Child2(); var s2 = new Child2();
打印:
通過一講的,我們知道要共享莫些屬性,需要 對象.__proto__ = 父親對象的.prototype,但實際上我們是不能直接 操作__proto__,這時我們可以借用 new 來做,所以
Child2.prototype = new Parent2(); <=> Child2.prototype.__proto__ = Parent2.prototype; 這樣我們借助 new 這個語法糖,就可以實現原型鏈繼承。但這里有個總是,如打印結果,我們給 s1.play新增一個值 ,s2 也跟著改了。所以這個是原型鏈繼承的缺點,原因是 s1.__pro__ 和 s2.__pro__指向同一個地址即 父類的prototype。
/** * 組合方式 */ function Parent3() { this.name = "parent3"; this.play = [1, 2, 3]; } Parent3.prototype.say = function () { } function Child3 () { Parent3.call(this); this.type = "child3"; } Child3.prototype = new Parent3(); var s3 = new Child3(); var s4 = new Child3(); s3.play.push(4); console.log(new Child3); console.log(s3.play, s4.play)
打印:
將 1 和 2 兩種方式組合起來,就可以解決1和2存在問題,這種方式為組合繼承。這種方式有點缺點就是我實例一個對象的時, 父類 new 了兩次,一次是var s3 = new Child3()對應 Child3.prototype = new Parent3()還要new 一次。
4)組合繼承的優化1function Parent4() { this.name = "parent4"; this.play = [1, 2, 3]; } Parent4.prototype.say = function () { } function Child4() { Parent4.call(this); this.type = "child4"; } Child4.prototype = Parent4.prototype; var s5 = new Child4(); var s6 = new Child4();
這邊主要為 Child4.prototype = Parent4.prototype, 因為我們通過構造函數就可以拿到所有屬性和實例的方法,那么現在我想繼承父類的原型對象,所以你直接賦值給我就行,不用在去 new 一次父類。其實這種方法還是有問題的,如果我在控制臺打印以下兩句:
從打印可以看出,此時我是沒有辦法區分一個對象 是直接 由它的子類實例化還是父類呢?我們還有一個方法判斷來判斷對象是否是類的實例,那就是用 constructor,我在控制臺打印以下內容:
咦,你會發現它指向的是父類 ,這顯然不是我們想要的結果, 上面講過我們 prototype里面有一個 constructor, 而我們此時子類的 prototype 指向是 父類的 prototye ,而父類prototype里面的contructor當然是父類自己的,這個就是產生該問題的原因。
組合繼承的優化2/** * 組合繼承的優化2 */ function Parent5() { this.name = "parent4"; this.play = [1, 2, 3]; } Parent5.prototype.say = function () { } function Child5() { Parent5.call(this); this.type = "child4"; } Child5.prototype = Object.create(Parent5.prototype);
這里主要使用Object.create(),它的作用是將對象繼承到__proto__屬性上。舉個例子:
var test = Object.create({x:123,y:345}); console.log(test);//{} console.log(test.x);//123 console.log(test.__proto__.x);//3 console.log(test.__proto__.x === test.x);//true
那大家可能說這樣解決了嗎,其實沒有解決,因為這時 Child5.prototype 還是沒有自己的 constructor,它要找的話還是向自己的原型對象上找最后還是找到 Parent5.prototype, constructor還是 Parent5 ,所以要給 Child5.prototype 寫自己的 constructor:
Child5.prototype = Object.create(Parent5.prototype); Child5.prototype.constructor = Child5;參考
什么是 JS 原型鏈?
this 的值到底是什么?一次說清楚
JS 的 new 到底是干什么的?
你的點贊是我持續分享好東西的動力,歡迎點贊!
交流干貨系列文章匯總如下,覺得不錯點個Star,歡迎 加群 互相學習。
https://github.com/qq44924588...
我是小智,公眾號「大遷世界」作者,對前端技術保持學習愛好者。我會經常分享自己所學所看的干貨,在進階的路上,共勉!
關注公眾號,后臺回復福利,即可看到福利,你懂的。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/98038.html
摘要:這正是我們想要的太棒了毫不意外的,這種繼承的方式被稱為構造函數繼承,在中是一種關鍵的實現的繼承方法,相信你已經很好的掌握了。 你應該知道,JavaScript是一門基于原型鏈的語言,而我們今天的主題 -- 繼承就和原型鏈這一概念息息相關。甚至可以說,所謂的原型鏈就是一條繼承鏈。有些困惑了嗎?接著看下去吧。 一、構造函數,原型屬性與實例對象 要搞清楚如何在JavaScript中實現繼承,...
摘要:當然這還沒完,因為我們還有重要的一步沒完成,沒錯就是上面的第行代碼,如果沒有這行代碼實例中的指針是指向構造函數的,這樣顯然是不對的,因為正常情況下應該指向它的構造函數,因此我們需要手動更改使重新指向對象。 第一節內容:javaScript原型及原型鏈詳解(二) 第一節中我們介紹了javascript中的原型和原型鏈,這一節我們來講利用原型和原型鏈我們可以做些什么。 普通對象的繼承 ...
摘要:雖然方法定義在對象里面,但是使用方法后,將方法里面的指向了。本文都是在非嚴格模式下的情況。在構造函數內部的內的回調函數,始終指向實例化的對象,并獲取實例化對象的的屬性每這個屬性的值都會增加。否則最后在后執行函數執行后輸出的是 本篇文章主要針對搞不清this指向的的同學們!不定期更新文章都是我學習過程中積累下的經驗,還請大家多多關注我的文章以幫助更多的同學,不對的地方還望留言支持改進! ...
摘要:搞清了構造函數和原型的區別后,就可以繼續了。指向構造函數的原型對象,存在于實例與構造函數的原型對象之間。要注意的是當我們使用下面這種將整個重寫的情況時,會切斷構造函數和原型之間的聯系,也就是說不再指向了,而是指向。 前言 先說一說為什么要搞清楚JavaScript的原型,因為這就是JS的根。JavaScript雖然不是一門傳統的面向對象語言,但它有自己的類和繼承機制,最重要的就是它采用...
摘要:在創建對象不論是普通對象還是函數對象的時候,都有一個叫做的內置屬性,用于指向創建它的構造函數的原型對象,也就是。因為一個普通對象的構造函數所以原型鏈原型鏈的形成是真正是靠而非。參考文章最詳盡的原型與原型鏈終極詳解,沒有可能是。 【前端芝士樹】Javascript的原型、原型鏈以及繼承機制 前端的面試中經常會遇到這個問題,自己也是一直似懂非懂,趁這個機會整理一下 0. 為什么會出現原型和...
閱讀 2778·2021-11-19 11:30
閱讀 3064·2021-11-15 11:39
閱讀 1785·2021-08-03 14:03
閱讀 1993·2019-08-30 14:18
閱讀 2049·2019-08-30 11:16
閱讀 2160·2019-08-29 17:23
閱讀 2604·2019-08-28 18:06
閱讀 2539·2019-08-26 12:22