阿里雲最近在作活動,低至2折,有興趣能夠看看:promotion.aliyun.com/ntms/yunpar…javascript
在理解繼承以前,須要知道 js 的三個東西:html
咱們知道 JS 有對象,好比前端
var obj = { name: 'obj' }
複製代碼
咱們經過控制檯把obj 打印出來:java
咱們會發現 obj
已經有幾個屬性(方法)了。那麼問題來了:valueOf/toString/constructor
是怎麼來?咱們並無給 obj.valueOf
賦值呀。git
上面這個圖有點難懂,手畫一個示意圖:github
咱們發現控制檯打出來的結果是:面試
如今回到咱們的問題:obj 爲何會擁有 valueOf / toString / constructor 這幾個屬性?瀏覽器
答案: 這跟 __proto__有關 。bash
當咱們「讀取」 obj.toString 時,JS 引擎會作下面的事情:app
看看 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.toStrin
g 和 obj2.toString
的行爲不一樣怎麼作呢? 直接賦值就行了:
obj.toString = function(){ return '新的 toString 方法' }
複製代碼
小結
[讀]屬性時會沿着原型鏈搜索
[新增]屬性時不會去看原型鏈
你可能遇到過這樣的 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 了
複製代碼
小結:
咱們聲明一個士兵,具備以下屬性:
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
。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(){ /*護臉*/ }
}
複製代碼
繼承的本質就是上面的講的原型鏈
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
方法,因此這種只能繼承父類的實例屬性和方法,不能繼承原型屬性/方法。
/**
* 藉助原型鏈實現繼承
*/
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 一次。
function 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
*/
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 原型鏈?][1] [this 的值究竟是什麼?一次說清楚][2] [JS 的 new 究竟是幹什麼的?][3]
你的點贊是我持續分享好東西的動力,歡迎點贊!
乾貨系列文章彙總以下,以爲不錯點個Star,歡迎 加羣 互相學習。
我是小智,公衆號「大遷世界」做者,對前端技術保持學習愛好者。我會常常分享本身所學所看的乾貨,在進階的路上,共勉!
關注公衆號,後臺回覆福利,便可看到福利,你懂的。
每次整理文章,通常都到2點才睡覺,一週4次左右,挺苦的,還望支持,給點鼓勵
[1]: zhuanlan.zhihu.com/p/23090041?… [2]: zhuanlan.zhihu.com/p/23804247 [3]: zhuanlan.zhihu.com/p/23987456?… [4]: www.fundebug.com/?utm_source… [5]: /img/bVbraR4