看完這篇 “原型” & “this”,就兩字“通透了”
技術標籤:前端jsjavascriptbindprototype
主題
今天想跟大家分享一個比較 “彆扭” 的概念: “原型 & this” 。
想把這玩意兒給說清楚,大多都會感到頭大。用的時候也會遇到些尷尬的場景。就很難去整明白,這到底是個啥。
這一期,就試著將這 說個清楚,講個明白。開始~
原型
什麼是 原型 ?帶著這個問題往下看。
原型-構造器 (constructor)
首先說到原型,那就跟物件密不可分。如果我們需要建立一個物件,就需要區定義一個object。那我們在開發中如何去建立一個物件?肯定有人會說,就是var 一個物件唄。很好你說的很對~ 確實是var 一個物件,那我如果需要兩個呢? 這個時候又會說了,那就var兩個唄。很好,你又說對了~
以下是建立物件的方法。
code 建立物件
var zhangsan = {
name:'張三',
age:20
}
var lisi = {
name:'李四',
age:22
}
那如果我們需要建立100個物件呢?程式設計師這麼懶,不會去實打實的真的給你去 var 100個物件。當然如果真去這樣做了,裡面的變數也是未知的。何況如果是一個動態建立的,也不能去給程式碼寫死不是。
好了,那這個時候,聰明的同學就已經想到了,搞一個 function 函式唄。專門生成物件,不就完事拉!
code 建立物件
function User(name, age) { var person = {} // 定義一個person 物件 person.name = name; // 往物件中繫結傳參 person.age = age; return person // 返回生成的新物件 } var zhangsan = User('張三', 20); var lisi = User('李四', 22);
以上的函式,就會生成你想要的任何物件,也稱之為:工廠函式 !一個專門造物件的工廠函式。
好了,那麼這樣做就可以了嗎?是不是發現了什麼?
對拉,js中,本身就有一種生產物件的方式啊,並且更簡單,不需要再函式中定義一個物件。只需要繫結 this 就可以了。
code 建立物件
function User(name, age) { this.name = name; // 這裡面的this,就代表了即將生成的那個物件 ,並且繫結傳參 this.age = age; } var zhangsan = new User('張三', 20); var lisi = new User('李四', 22);
這個時候,細心的同學已經發現了不同之處。兩個都是生成物件的函式,但是叫法就有些不同了。如果是用第二種 js 本身的函式,我們就需要用 new 關鍵字來生成物件。
code 差異
var zhangsan = User('張三', 20); // 第一種
var zhangsan = new User('張三', 20); // 第二種
而這種需要用 new 關鍵字來叫的函式,稱之為:“構造器 constructor or 建構函式”。
而生成物件的這個過程,稱之為:例項化。“zhangsan”*** 可以稱之為一個物件*,也可以稱之為一個 例項。
原型-proto & prototype
好了,上一段說了構造器,那麼構造器是幹嘛的?就是造物件的一個函式呀。
那這一段,來說說原型中的重頭戲。先看一段程式碼:
code 建立物件 在物件中新增一個功能屬性,可以引用自己的屬性 "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 這個方法,一點毛病沒有。但是有沒有同學發現什麼問題?細心的同學已經發現了,這兩個都分別例項了greet !
是不是有的同學有點沒理解這句話的意思?沒關係,接著看:
code 例項化後引用 greet 差異對比
zhangsan.greet() === lisi.greet() // false
同學們,看到了什麼?what? 這兩個竟然不一樣?
這意味著什麼呢?也就是說 張三 和 李四,例項化之後,都在自己的內部,創造了 greet 這樣的屬性。
這個時候,greet 的功能都是一模一樣的呀。如果例項100個物件,豈不是要拷100份?完全沒必要呀。有沒有什麼方法將這些通用的屬性,放到一個地方呢?
有的。接下來就要說到本段的重頭戲之一:prototype 了。在講之前,先看下面一段程式碼:
code 建立物件 自帶 prototype
function test1 () {}
console.log( test1.prototype ) // { constructor : f }
function test2 () {}
console.log( test2.prototype ) // { constructor : f }
發現了什麼?是不是每建立一個function,都會自帶一個 prototype 這樣的物件啊。這就是js 的原生機制。那為什麼 js 的原生機制 要這麼做呢?劃重點:prototype 就是給他即將生成的物件,繼承下去的屬性 看到了什麼? prototype 他是一個屬性,是一個可供例項物件繼承下去的屬性。這不簡單了嗎。走一個。
code 建立物件 在物件中新增一個功能屬性,可以引用自己的屬性 "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__屬性,指向建立該物件的建構函式的原型 ,然後通過__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
...
*/
發現了嗎?就算我只能知道例項後的物件,但是我可以通過 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(),兩者沒有任何區別。舉個例子:
code 原生物件
var a ={}
var b = new Object()
console.log(a.constructor === b.constructor ) // true
可以看到,建構函式完全一樣。
那麼這個時候,可能會有同學想問,怎麼去創造一個乾淨的物件呢?裡面沒有任何整合的屬性等。
當然也是可以的。接著看:
code 原生物件
var a = new Object.create(null) // 建立函式必須傳參,一個物件或者是 null ,否則會報錯!
console.log( a )
/*
no prototies
*/
可以看到,通過 Object.create() 建立的物件,屬性為空。這個時候,肯定會有同學有疑問,你這傳的引數是 null,那當然什麼都沒有了,你傳個物件試試。哈哈哈,確實,如果傳物件的話,那就是定義自己所自帶的原型了。舉個例子:
code 原生物件
var a = new Object.create({name:juejin,des:"666"}) // 建立函式必須傳參,一個物件或者是 null ,否則會報錯!
console.log( a )
/*
{}
__proto__
name:juejin
des:"666"
__proto__
constructor : f Object()
...
*/
可以看到,再Object.create() 中傳入物件的屬性,是放在第一層的 proto 下面的,也就是中,這是你建立的這個原型物件的繼承屬性,意味著,可以根據自身的業務需求,來定義自己的原型物件!
多級繼承鏈
好了,上面已經詳細的講解了原型鏈,建構函式,那麼就試著來實現一個繼承鏈。看下面程式碼:
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 中,方便再往下給爸爸繼承。
這樣一級一級的繫結,構建,就實現了所謂的 多級繼承 了。
當然細心的同學又發現了一個點:
code
// Mammal
function Mammal(color, weight) {
Animal.call(this, color, weight); //繫結 this
}
為什麼這邊的爺爺輩的構造器裡面為什麼要 call this 呢? ,這邊就先賣個關子,下面this那段會講到!嘿嘿~
原型總結
好了,講了這麼多,終於說完了原型鏈。其實一圖勝千言。
引用上面的一句話:每個物件都有一個 proto 的屬性,指向該物件的原型。 例項後通過對 proto 屬性的訪問 去對 prototype物件進行訪問; 原型鏈是由原型物件組成的,每個物件都有__proto__屬性,指向建立該物件的建構函式的原型 ,然後通過__proto__屬性將物件連結起來,組成一個原型鏈,用來實現繼承和共享屬性!
說到這,原型鏈也就說完了,接下來再啃一塊硬骨頭:this。
this
其實說到 this,大家都有這樣的一個感覺,就是一看就會,一用就亂。那麼這個this 到底是個啥?能不能給它整明白?別急,
先來看一段程式碼:
code
var User = {
fname:'三',
lname:'張',
fullname:function(){
return User.lname + User.fname
}
}
console.log(User.fullname) // "張三"
這段程式碼是去獲取 User 物件下的全名,可以看到是沒什麼問題。那麼這個時候,需要給這個物件換成person物件,會發生什麼呢?
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 建立物件 "
function User(name, age) {
this.name = name; // 這裡面的this,就代表了即將生成的那個物件 ,並且繫結傳參
this.age = age;
}
就是說:建構函式中的 this ,就是指向即將例項化的那個物件。謹記!
所以 總結一下 this 的三種場景:
1. 如果this 是 在一個函式中,並且被用作方法來叫,那麼這個時候的 this 就指向了父級物件;
2. 如果this 是在匿名函式,或者全域性環境的函式中,那麼這個時候的 this 就是;undefined;
3. 如果this 是在建構函式中,那麼這個時候的 this 就指向了即將生成的那個物件
好了,既然區分了 this 的使用場景之後,那麼它的強大之處是什麼呢? 舉個例子:
code 動態繫結 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 改變它的預設指向,繫結一個我想要繫結的環境,行不行?
bind & apply & call
好了,這一段,就接著上面的講,這裡會講到關於 this 的三種繫結方法。先來看程式碼:
code 動態繫結 this
function introduction() {
console.log('你好, 我是' + this.name);
}
introduction() // 你好, 我是 undefined
這個結果相信大家不會陌生,因為就是上面講的第二種情況:2. 如果this 是在匿名函式,或者全域性環境的函式中,那麼這個時候的 this 就是;undefined。
這裡普及一個知識:introduction() === introduction.call() 只是前者是後者的簡寫!並且call()中的第一個傳參可以指定這個函式中的 this 指向誰!
好了,知道這個知識點,再看下面的程式碼:
code 動態繫結 this
function introduction() {
console.log('你好, 我是' + this.name);
}
var zhangsan = {
name:'張三'
}
introduction.call(zhangsan) // 你好, 我是 張三
看完是不是一目瞭然,這個call()裡面傳的引數,指向了 zhangsan 這個物件。那這不就是給這個 introduction 方法指定了呼叫的父級了嗎? this 也就指向給呼叫這個方法的 zhangsan 了呀!
說到這是不是就能清楚的知道,這個跟上面 在物件中,來繫結這個方法,來關聯父級呼叫關係,是一樣的。一個是物件引用方法,這個就是方法繫結物件呀!
好,再來:
code 動態繫結 this
function introduction(name) {
console.log('你好,'+ name +' 我是' + this.name);
}
var zhangsan = {
name:'張三'
}
introduction.call(zhangsan,"李四") // 你好 李四, 我是 張三
可以看到call() 除了可以指定this指向的物件,還可以傳一些其他的引數。
好了,說到這,是不是已經能猜到:bind & apply 怎麼用拉!
大同小異:
code 動態繫結 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() 區別就在於,後面的傳參的格式是:陣列的形式;
而 bind() 則是返回一個繫結新環境的 function,等著被呼叫。
結語
好啦,這期關於 “原型” & “this” 的內容就全部說完了,看到這,就兩個字:“透徹”。
下期再見~