1. 程式人生 > 其它 >看完這篇 “原型” & “this”,就兩字“通透了”

看完這篇 “原型” & “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,就指向了這個fullnamefnc 外的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” 的內容就全部說完了,看到這,就兩個字:“透徹”

下期再見~