一篇通俗易懂的關于“原型” & “this”的解釋

轉自:掘金?- i.m.t? https://juejin.cn/post/6921686794987634695
主題
今天想跟大家分享一個比較 "別扭" 的概念:“原型 & this”?。
想把這玩意兒給說清楚,大多都會感到頭大。用的時候也會遇到些尷尬的場景。就很難去整明白,這到底是個啥。
這一期,就試著將這 說個清楚,講個明白。開始~
原型
什么是?原型??帶著這個問題往下看。
原型-構造器 (constructor)
首先說到原型,那就跟對象密不可分。如果我們需要創(chuàng)建一個對象,就需要區(qū)定義一個object。那我們在開發(fā)中如何去創(chuàng)建一個對象?肯定有人會說,就是var 一個對象唄。很好你說的很對~ 確實是var 一個對象,那我如果需要兩個呢?這個時候又會說了,那就var兩個唄。很好,你又說對了~
以下是創(chuàng)建對象的方法。
code 創(chuàng)建對象
var?zhangsan?=?{
?name:'張三',
????age:20
}
var?lisi?=?{
?name:'李四',
????age:22
}
那如果我們需要創(chuàng)建100個對象呢?程序員這么懶,不會去實打實的真的給你去 var 100個對象。當然如果真去這樣做了,里面的變量也是未知的。何況如果是一個動態(tài)創(chuàng)建的,也不能去給代碼寫死不是。
好了,那這個時候,聰明的同學就已經想到了,搞一個 function 函數唄。專門生成對象,不就完事拉!
code 創(chuàng)建對象
function?User(name,?age)?{
????var?person?=?{}?//?定義一個person?對象
????person.name?=?name;?//?往對象中綁定傳參
????person.age?=?age;
????return?person?//?返回生成的新對象
}
var?zhangsan?=?User('張三',?20);
var?lisi?=?User('李四',?22);
以上的函數,就會生成你想要的任何對象,也稱之為:工廠函數?!一個專門造對象的工廠函數。
好了,那么這樣做就可以了嗎?是不是發(fā)現了什么?
對拉,js中,本身就有一種生產對象的方式啊,并且更簡單,不需要再函數中定義一個對象。只需要綁定 this 就可以了。
code 創(chuàng)建對象
function?User(name,?age)?{
??this.name?=?name;?//?這里面的this,就代表了即將生成的那個對象?,并且綁定傳參
??this.age?=?age;
}
var?zhangsan?=?new?User('張三',?20);
var?lisi?=?new?User('李四',?22);
這個時候,細心的同學已經發(fā)現了不同之處。兩個都是生成對象的函數,但是叫法就有些不同了。如果是用第二種 js 本身的函數,我們就需要用 new 關鍵字來生成對象。
code 差異
var?zhangsan?=??User('張三',?20);?//??第一種
var?zhangsan?=?new?User('張三',?20);?//??第二種
而這種需要用 new 關鍵字來叫的函數,稱之為:“構造器 constructor or 構造函數”。
而生成對象的這個過程,稱之為:實例化。“zhangsan”?可以稱之為一個對象,也可以稱之為一個?實例。
原型-proto?& prototype
好了,上一段說了構造器,那么構造器是干嘛的?就是造對象的一個函數呀。
那這一段,來說說原型中的重頭戲。先看一段代碼:
code 創(chuàng)建對象 在對象中添加一個功能屬性,可以引用自己的屬性 "greet"
function?User(name,?age)?{
??this.name?=?name;?//?這里面的this,就代表了即將生成的那個對象?,并且綁定傳參
??this.age?=?age;
??this.greet?=?function?()?{
????console.log('你好,?我是'?+?this.name?+?',我'?+?this.age?+?'歲');
??}
}
var?zhangsan?=?new?User('張三',?20);
var?lisi?=?new?User('李四',?22);
zhangsan.greet()?//?你好我是張三,我20歲
lisi.greet()?//?你好我是李四,我22歲
這個時候,用生成的對象來叫一下?greet?這個方法,一點毛病沒有。但是有沒有同學發(fā)現什么問題?細心的同學已經發(fā)現了,這兩個都分別實例了greet !
是不是有的同學有點沒理解這句話的意思?沒關系,接著看:
code 實例化后引用 greet 差異對比
zhangsan.greet?===?lisi.greet??//?false
同學們,看到了什么?what? 這兩個不一樣?
這意味著什么呢?也就是說 張三 和 李四,實例化之后,都在自己的內部,創(chuàng)造了 greet 這樣的屬性。
這個時候,greet 的功能都是一模一樣的呀。如果實例100個對象,豈不是要拷100份?完全沒必要呀。有沒有什么方法將這些通用的屬性,放到一個地方呢?
有的。接下來就要說到本段的重頭戲之一:prototype?了。在講之前,先看下面一段代碼:
code 創(chuàng)建對象 自帶 prototype
function?test1?()?{}
console.log(?test1.prototype?)?//?{?constructor?:?f?}
function?test2?()?{}
console.log(?test2.prototype?)?//?{?constructor?:?f?}
發(fā)現了什么?是不是每創(chuàng)建一個function,都會自帶一個?prototype?這樣的對象啊。這就是js 的原生機制。那為什么 js 的原生機制 要這么做呢?劃重點:prototype 就是給他即將生成的對象,繼承下去的屬性?看到了什么?prototype?他是一個屬性,是一個可供實例對象繼承下去的屬性。這不簡單了嗎。走一個。
code 創(chuàng)建對象 在對象中添加一個功能屬性,可以引用自己的屬性 "greet"
function?User(name,?age)?{
??this.name?=?name;?//?這里面的this,就代表了即將生成的那個對象?,并且綁定傳參
??this.age?=?age;
}
User.prototype.greet?=?function?()?{
??console.log('你好,?我是'?+?this.name?+?',我'?+?this.age?+?'歲');
}
var?zhangsan?=?new?User('張三',?20);
var?lisi?=?new?User('李四',?22);
zhangsan.greet()?===?lisi.greet() //?true
既然知道了在構造函數中,使用?prototype?這樣的繼承對象,可以將?通用?的屬性給 實例化的對象繼承下去。
那么說到這,是不是會有幾個問題?這個greet 并不是定義在實例化的對象里面的啊,來看一段代碼:
code prototype
function?User(name,?age)?{
??this.name?=?name;?//?這里面的this,就代表了即將生成的那個對象?,并且綁定傳參
??this.age?=?age;
}
User.prototype.greet?=?function?()?{
??console.log('你好,?我是'?+?this.name?+?',我'?+?this.age?+?'歲');
}
var?lisi?=?new?User('李四',?22);
console.log(lisi);
??/*
??User?{}
??name:'李四'
??age?=?22
??__proto__
??greet:f()
??constructor?:?f?User?(name,?age)
??__proto__:Object
??...
??*/??????????
看到了什么?是不是通過?prototype?定義的_greet = function ()_ 屬性跑到了?proto?下面去了。并且,這個greet屬性雖然沒有在自己本身的對象下面,但是一樣可以使用啊!我們上面說到過:prototype?是繼承屬性對象。那么看到這里的小伙伴,是不是會困惑,為什么繼承屬性會定義在?proto?下面?先別急。接著看!
這個時候已經看到了重頭戲之二:proto。再來看一段代碼:
code __proto__
function?Test?()?{}
Test.prototype.name?=?'test'
var?test01?=?new?Test()
var?test02?=?new?Test()
test01.__proto__?===?test02.__proto__????//?true
//?-----------------------?實例之后的對象調用__proto__指針指向的?等于被實例的構造函數的prototype!
//?test01.__proto__?=?Test.prototype??//?true
這時候,是不是已經恍然大悟了!原來通過prototype?定義的屬性,再被多個實例化之后,引用的地址是同一個!并且?proto?就是我們上面使用的prototype?屬性的馬甲啊!就是說,我們在構造函數中使用prototype?定義的屬性,都會被?proto?指針引用!
好了,這個時候,可以整一段比較晦澀的總結了:每個對象都有一個?proto?的屬性,指向該對象的原型。實例后通過對?proto?屬性的訪問 去對 prototype對象進行訪問;原型鏈是由原型對象組成的,每個對象都有__proto__屬性,指向創(chuàng)建該對象的構造函數的原型 ,然后通過__proto__屬性將對象鏈接起來,組成一個原型鏈,用來實現繼承和共享屬性!
理清楚以上關系后,可以想一下 通過prototype?定義的屬性作用就僅僅如此么?接著看一段代碼:
code prototype
function?Test?()?{}
Test.prototype.name?=?'test'
var?test01?=?new?Test()
console.log(?test01.name?)?//?"test"
Test.prototype.name?=?'no?test?'
console.log(?test01.name?)?//?"no?test"
看到了什么?原來?prototype?可以在實例之后,再進行更改呀!
就是說,通過構造函數去改變name 的值,實例化之后的對象,引用的屬性值也會跟著變。太強大了!
再來看看?constructor?:
code constructor
function?User(name,?age)?{
??this.name?=?name;?//?這里面的this,就代表了即將生成的那個對象?,并且綁定傳參
??this.age?=?age;
}
User.prototype.greet?=?function?()?{
??console.log('你好,?我是'?+?this.name?+?',我'?+?this.age?+?'歲');
}
var?lisi?=?new?User('李四',?22);
//?再次構造
var?zhangsan?=?new?lisi.constructor('張三', 20)?//?使用constructor來實例化!!!
new?lisi.constructor()?===?new?User()??//?true
console.log(zhangsan)
/*
??User?{}
??name:'張三'
??age?=?20
??__proto__
??greet:f()
??constructor?:?f?User?(name,?age)
??__proto__:Object
??...
??*/??
發(fā)現了嗎?就算我只能知道實例后的對象,但是我可以通過?proto?去找到這個實例對象的構造函數?constructor?,我再通過這個構造函數再去實例對象。(var zhangsan = new lisi.constructor('張三', 20))與我直接var zhangsan = new User('張三', 20)。完全一樣。真的很強大!
好了,講到這,proto?& prototype?也就說完了,接下來再說說?原生對象的原型。
原型-原生對象的原型
前面,知道了原型的概念,那就趁熱打鐵,接著看看原生對象的原型。
先看一段代碼:
code 原生對象
??var?a?={}
??console.log(a)
??/*
????{}
????__proto__
????greet:f()
????constructor?:?f?Object()
????...
????*/??
可以看到,我們var 了一個新對象之后,沒有定義任何屬性,但是也能看到他的構造函數:Object()。也就是說:var a ={} === var a = new Object(),兩者沒有任何區(qū)別。舉個例子:
code 原生對象
??var?a?={}
??var?b?=?new?Object()
??console.log(a.constructor?===?b.constructor?)?//?true
可以看到,構造函數完全一樣。
那么這個時候,可能會有同學想問,怎么去創(chuàng)造一個干凈的對象呢?里面沒有任何集成的屬性等。
當然也是可以的。接著看:
code 原生對象
? var a = new Object.create(null)?//?創(chuàng)建函數必須傳參,一個對象或者是 null ,否則會報錯!
??console.log(?a?)
??/*
????no?prototies?
????*/??
可以看到,通過?Object.create()?創(chuàng)建的對象,屬性為空。這個時候,肯定會有同學有疑問,你這傳的參數是 null,那當然什么都沒有了,你傳個對象試試。哈哈哈,確實,如果傳對象的話,那就是定義自己所自帶的原型了。舉個例子:
code 原生對象
??var?a?=?new?Object.create({name:juejin,des:"666"})?//?創(chuàng)建函數必須傳參,一個對象或者是 null ,否則會報錯!
??console.log(?a?)
??/*
????{}
????__proto__
????name:juejin
????des:"666"
??????__proto__
??????constructor?:?f?Object()
??????...
????*/???
可以看到,再Object.create()?中傳入對象的屬性,是放在第一層的?proto?下面的,也就是中,這是你創(chuàng)建的這個原型對象的繼承屬性,意味著,可以根據自身的業(yè)務需求,來定義自己的原型對象!
多級繼承鏈
好了,上面已經詳細的講解了原型鏈,構造函數,那么就試著來實現一個繼承鏈。看下面代碼:
code 繼承鏈 從祖父 到爺爺 到爸爸 到自己
//?Animal?-->?Mammal?-->?Person?-->?me
//?Animal?
function?Animal(color,?weight)?{
??this.color?=?color;
??this.weight?=?weight;
}
Animal.prototype.eat?=?function?()?{
??console.log('吃飯');
}
Animal.prototype.sleep?=?function?()?{
??console.log('睡覺');
}
?//??Mammal
function?Mammal(color,?weight)?{
??Animal.call(this,?color,?weight);?//綁定?this?這個下面講
}
Mammal.prototype?=?Object.create(Animal.prototype);
Mammal.prototype.constructor?=?Mammal;
Mammal.prototype.suckle?=?function?()?{
??console.log('喝牛奶');
}
//??Person
function?Person(color,?weight)?{
??Mammal.call(this,?color,?weight);
}
Person.prototype?=?Object.create(Mammal.prototype);
Person.prototype.constructor?=?Person;
Person.prototype.lie?=?function?()?{
??console.log('你是個騙子');
}
//?實例
var?zhangsan?=?new?Person('brown',?100);
var?lisi?=?new?Person('brown',?80);
console.log('zhangsan:',?zhangsan);
console.log('lisi:',?lisi);??
????
上面的代碼中,實現了三級繼承。其中,使用了我們上面講到的?prototype?以及?Object.create()?。
code
function?Animal(color,?weight)?{
??this.color?=?color;
??this.weight?=?weight;
}
Animal.prototype.eat?=?function?()?{
??console.log('吃飯');
}
往祖父類中寫入繼承屬性,eat 供爺爺輩來繼承這個吃的屬性。
code
//??Mammal
??function?Mammal(color,?weight)?{
????Animal.call(this,?color,?weight);?//綁定?this?這個下面講
??}
Mammal.prototype?=?Object.create(Animal.prototype);
Mammal.prototype.constructor?=?Mammal;
Mammal.prototype.suckle?=?function?()?{
??console.log('喝牛奶');
}
同時,爺爺輩的屬性,需要繼承祖父輩的其他屬性,因為上面有講到:prototype?是繼承屬性,也可以稱之為隱性屬性。那么?color, weight?這些顯性屬性怎么給他繼承過來呢?
這個時候就用上了上面的?Mammal.prototype = Object.create(Animal.prototype);?這就是利用?Object.create()?來將祖父的其他顯性屬性,全部繼承到爺爺輩。并且再寫進爺爺輩的?prototype?中,方便再往下給爸爸繼承。
這樣一級一級的綁定,構建,就實現了所謂的?多級繼承?了。
當然細心的同學又發(fā)現了一個點:
code
?//??Mammal
function?Mammal(color,?weight)?{
??Animal.call(this,?color,?weight);?//綁定?this?
}
為什么這邊的爺爺輩的構造器里面為什么要?call this 呢??,這邊就先賣個關子,下面this那段會講到!嘿嘿~
原型總結
好了,講了這么多,終于說完了原型鏈。其實一圖勝千言。

引用上面的一句話:每個對象都有一個?proto?的屬性,指向該對象的原型。實例后通過對?proto?屬性的訪問 去對 prototype對象進行訪問;原型鏈是由原型對象組成的,每個對象都有__proto__屬性,指向創(chuàng)建該對象的構造函數的原型 ,然后通過__proto__屬性將對象鏈接起來,組成一個原型鏈,用來實現繼承和共享屬性!
說到這,原型鏈也就說完了,接下來再啃一塊硬骨頭:this。
this
其實說到?this,大家都有這樣的一個感覺,就是一看就會,一用就亂。那么這個this?到底是個啥?能不能給它整明白?別急,
先來看一段代碼:
code
var?User?=?{
?fname:'三',
?????lname:'張',
?????fullname:function(){
??????return?User.lname?+?User.fname
?????}
??}
?console.log(User.fullname)?//?"張三"
這段代碼是去獲取?User?對象下的全名,可以看到是沒什么問題。那么這個時候,需要給這個對象換成person對象,會發(fā)生什么呢?
code
var?Person?=?{
?fname:'三',
?????lname:'張',
?????fullname:function(){
??????return?User.lname?+?User.fname
?????}
??}
?console.log(Person.fullname)?//?User?is?not?defined
看到了什么,找不到這個?User,這是為什么呢?很明顯,是因為我們再return?中,返回的還是?User?這個對象,但是這個時候,我已經將原來的?User?改成?Person?了。所以,如果這段代碼想生效,必須也要將?return?中的?User?對象 改成?Person?對象。
麻不麻煩?可重用性也太低了。那么這個時候,this 就派上用場了。接著看:
code
var?Person?=?{
?fname:'三',
?????lname:'張',
?????fullname:function(){
??????return?this.lname?+?this.fname
?????}
??}
?console.log(Person.fullname)?//?"張三"
這時候,就能看到,我對象名改成了Person,是一樣可以拿到這個對象下的?fullname。
是不是有同學會問了,這是為什么?其實這個時候,這里面的this,就指向了這個fullname的?fnc?外的Person對象了。是不是覺得說的有點干,那我們就來看看:
code
var?Person?=?{
?fname:'三',
?????lname:'張',
?????fullname:function(){
???????? console.log(this)?//?在哪邊引用this,就在哪邊看!
??????return?this.lname?+?this.fname
?????}
??}
/*
fname:'三'
lname:'張'
fullname:f()
__proto__
??????constructor?:?f?Object()
??????...
*/
這樣看,是不是十分清晰明了。其實也就是說,我在?fullname?這個方法中使用的?this?就是指向了,我當前這個?function?代碼塊的上一級。
看到這,是不是感覺明白了?再來:
code
var?Person?=?{
?fname:'三',
?????lname:'張',
?????fullname:function(){
??????return?this.lname?+?this.fname
?????}
??}
var??getfullname?=?Person.fullname?//?將Person對象中的fullname?方法,給到新定義的參數使用
console.log(getfullname())?//?NAN
這是什么?沒拿到?張三??這是為啥?
到這里是不是一下子又懵了?這個?this?到底有多少幺蛾子。打印出來看看,這個時候的?this到底是什么:
code
var?Person?=?{
?fname:'三',
?????lname:'張',
?????fullname:function(){
?????????console.log(this)?
??????return?this.lname?+?this.fname
?????}
??}
var??getfullname?=?Person.fullname?//?將Person對象中的fullname?方法,給到新定義的參數使用
console.log(getfullname())?//?window:{},NAN
看到什么了?這個?this?竟然指向了window,全局變量。這是咋回事?這就是?this?坑的地方,我上面說到:this?就是指向了,我當前這個?function?代碼塊的上一級。其實這句話,在這邊就直接錯了。因為this引用沒變。只是我的調用方式變了。
所以這個時候,這句話要重新描述,謹記:this 并不取決于它所在的位置,而是取決于它所在的function是怎么被調用的!!!
而上面?console.log(Person.fullname) // "張三"?可以打印出結果,就是fullname的這個方法,直接被它的父級調用了,也就是說這個時候的?this?是指向的?Person。
而如果指定調用這個?this?的,并不是直接父級,那么再非嚴格模式下,指向的就是全局?window,而在嚴格模式下則是?undefined。
再來 如果?this?再構造函數中被調用,會是怎么樣?看下面一段代碼 (嚴格模式下):
code
;function User (){console.log(this)}User()?//?undefined?new?User?()?//?User?{}
這個時候,可以看到,如果?this?是放在構造函數中,被直接調用?User (),那么這個時候的?this?就是?undefined?。因為?this?所在的?function?并沒有作為一個方法被調用。
而 如果是通過?new?的方式被調用的,那么這個時候,?this?所在的?function?就被調用了,并且指向的就是被調用的?User {}?。還記得我們上面說的,js 本身的構造函數機制嗎?再來復習一下:
code 創(chuàng)建對象 "
function?User(name,?age)?{
??this.name?=?name;?//?這里面的this,就代表了即將生成的那個對象?,并且綁定傳參
??this.age?=?age;
}
就是說:構造函數中的 this ,就是指向即將實例化的那個對象。謹記!
所以 總結一下?this?的三種場景:
1.?如果this?是?在一個函數中,并且被用作方法來叫,那么這個時候的?this?就指向了父級對象;
2. 如果this 是在匿名函數,或者全局環(huán)境的函數中,那么這個時候的 this 就是;undefined;
3.?如果this?是在構造函數中,那么這個時候的?this?就指向了即將生成的那個對象
好了,既然區(qū)分了?this?的使用場景之后,那么它的強大之處是什么呢?舉個例子:
code 動態(tài)綁定 this
function?introduction()?{
??console.log('你好,?我是'?+?this.name);
}
var?zhangsan?=?{
??name:?'張三',
}
var?lisi?=?{
??name:?'李四',
}
zhangsan.introduction?=?introduction;
lisi.introduction?=?introduction;
zhangsan.introduction();?//??你好,我是張三
lisi.introduction();??//??你好,我是李四
上面可以看到,定義了一個方法,這個方法中使用了?this.name?,但是這個時候,并不知道,這個方法中的?this?到底指向的是誰,而是等待著誰來調用它。回憶一下上面說的那句話:this 并不取決于它所在的位置,而是取決于它所在的function是怎么被調用的!!!
而這個時候,定義了 張三 和 李四 兩個對象,這兩個對象,分別將定義的?introduction?賦值到本身的對象下面,也就是說,這個時候, 張三 和 李四 兩個對象,都擁有了?introduction這個方法,并且調用了。所以,這個時候的?function introduction()?已經擁有了被調用的對象,所以其中的?this.name?也就分別指向了這兩個對象的中name。
好,以上就是將?this?的默認指向講完了。但是是不是有個問題,還沒解決?
那就是我們之前在說?多級繼承?的時候,有個?call this?。這個賣的關子 還沒說呢?那接下來就講講。關于?this?改變它的默認指向,綁定一個我想要綁定的環(huán)境,行不行?
bind & apply & call
好了,這一段,就接著上面的講,這里會講到關于?this?的三種綁定方法。先來看代碼:
code 動態(tài)綁定 this
function?introduction()?{
??console.log('你好,?我是'?+?this.name);
}
introduction()?//?你好,?我是?undefined
這個結果相信大家不會陌生,因為就是上面講的第二種情況:2. 如果this 是在匿名函數,或者全局環(huán)境的函數中,那么這個時候的 this 就是;undefined。
這里普及一個知識:introduction() === introduction.call() 只是前者是后者的簡寫!并且call()中的第一個傳參可以指定這個函數中的 this 指向誰!
好了,知道這個知識點,再看下面的代碼:
code 動態(tài)綁定 this
function?introduction()?{
??console.log('你好,?我是'?+?this.name);
}
var?zhangsan?=?{
?name:'張三'
}
introduction.call(zhangsan)?//?你好,?我是?張三
看完是不是一目了然,這個call()里面?zhèn)鞯膮担赶蛄?zhangsan?這個對象。那這不就是給這個?introduction?方法指定了調用的父級了嗎?this?也就指向給調用這個方法的?zhangsan了呀!
說到這是不是就能清楚的知道,這個跟上面 在對象中,來綁定這個方法,來關聯父級調用關系,是一樣的。一個是對象引用方法,這個就是方法綁定對象呀!
好,再來:
code 動態(tài)綁定 this
function?introduction(name)?{
??console.log('你好,'+?name?+'?我是'?+?this.name);
}
var?zhangsan?=?{
?name:'張三'
}
introduction.call(zhangsan,"李四")?//?你好?李四,?我是?張三
可以看到call() 除了可以指定this指向的對象,還可以傳一些其他的參數。
好了,說到這,是不是已經能猜到:bind & apply?怎么用拉!
大同小異:
code 動態(tài)綁定 this
function?introduction(name)?{
??console.log('你好,'+?name?+'?我是'?+?this.name);
}
var?zhangsan?=?{
?name:'張三'
}??
introduction.call(zhangsan,"李四")???//?你好?李四,?我是?張三???call
introduction.apply(zhangsan,["李四"])???//?你好?李四,?我是?張三???apply
intro?=?introduction.bind(zhangsan)
intro("李四")//?你好?李四,?我是?張三???bind
可以看到,call() 和 apply() 區(qū)別就在于,后面的傳參的格式是:數組的形式;
而 bind() 則是返回一個綁定新環(huán)境的 function,等著被調用。
結語
好啦,這期關于?“原型” & “this”?的內容就全部說完了,看到這,就兩個字:“透徹”。
