js原型、原型鏈、作用鏈、閉包全解


https://www.2cto.com/kf/201711/698876.html

【對象、變量】

一個對象就是一個類,可以理解為一個物體的標准化定義。它不是一個具體的實物,只是一個標准。而通過對象實例化得到的變量就是一個獨立的實物。比如通過一個對象定義了“人”,通過“人”這個標准化定義,實例化了“小明”這個人。其中“人”就是對象,“小明”就是變量。實例化的過程就是通過構造函數,來初始化設置標准定義中是具體指。比如在創建“小明”這個變量時,同時設置了他的名稱,性別等信息。在變量中包含對對象的引用,所以可以通過變量操作對象,或引用對象的函數、屬性。比如“小明”有手有腳(屬性),可以抬頭低頭(函數)。

這里寫圖片描述

【原型、原型鏈】

什么是派生?

原型對象派生另一個對象,就是創建了原型對象的副本,占有獨立的內存空間,並在副本上添加一個獨特的屬性和方法。

在js系統中,Object對象派生了Number對象、Boolean對象、String對象、Function對象、Array對象、RegExp對象、Error對象、Date對象。當然你可以通過Object對象派生自己的對象。

右鍵圖片-在新標簽頁打開圖片-查看清晰圖片
這里寫圖片描述

在js系統中,Function對象又派生了Number函數、Boolean函數、String函數、Object函數、Function函數、Array函數、RegExp函數、Error函數、Date函數、自定義函數。

這也就是為什么說函數是一種特殊的對象。因為函數是通過Function對象派生的。

從上面的介紹我們知道一切對象派生於Object對象。Object對象中包含了一系列屬性和方法,可以參考js系列教程2-對象、對象屬性全解

這里主要介紹__proto__和constructor屬性。由於所有對象都繼承自Object對象,所以所有對象(包括函數)都擁有這兩個屬性。

每個對象的proto屬性是保存當前對象的原型對象。

所以Number對象、Boolean對象、String對象、Object對象、

Function對象、Array對象、RegExp對象、Error對象、Date對象的proto都指向Object對象。

Number函數、Boolean函數、String函數、Object函數、Function函數、Array函數、RegExp函數、Error函數、Date函數、自定義函數的proto都指向Function對象。

這種派生對象使用__proto__指針保存對原型對象的鏈接,就形成了原型鏈。對象通過原型鏈相互連接。所有的對象都在原型鏈上。所有的原型鏈頂端都是Object對象。

我們在原型對象中的一般用來實現所有可能的派生對象或實例變量的公共方法和公共屬性。

構造/實例化

上面講了什么是派生,原型鏈的形成。

那什么是實例化呢?

實例化即創建一個變量的過程,是將對象淺復制一個副本,然后通過構造函數來對這個占有獨立空間的變量進行初始化。

你可以用“人”派生了“男人”、“女人”。男人實例化了“小明”、“小王”。

准確說法應該是這個占有獨立空間的副本也是一個對象,這個指向副本的鏈接才是變量,叫做引用變量。所以變量也可以進行實例化,其實實例化的是變量指向的副本對象。這個我就把副本對象叫做變量以區分實例化和派生。

右鍵圖片-在新標簽頁打開圖片-查看清晰圖片
這里寫圖片描述

所以在js中要想實例化一個對象,進而創建一個變量的過程都需要有一個原型對象,和一個構造函數。我們把這個原型對象叫做函數的構造綁定對象,把函數叫做原型對象的構造函數。

要注意區分函數的原型對象是Function對象

為了表達這種對象與構造函數的緊密關系,js在在對象中使用constructor屬性保存當前對象的構造函數的引用,在構造函數中使用prototype保存對對象的引用。對象實例化的變量中,constructor指向構造函數、__proto__指向這個對象。我們也可以稱這個對象是這個變量的原型對象。

我們可以通過對象實例訪問保存在原型中的值,但卻不能通過對象實例重寫原型中的值。

而我們使用函數當做構造函數時,並沒有創建這個原型對象呀?

這是因為在定義函數時,系統除了將函數的proto屬性指向Function對象外,還會自動由Object對象派生了一個對象,作為這個函數的構造綁定對象,在函數中用prototype指向這個對象。

原型鏈的向上搜索

派生對象或實例化對象,都要為新對象分配一個獨占的空間。並且把原型對象的屬性和方法復制一份給新對象,而這個復制僅僅是引用復制(即淺復制)

(其實在js中有很多種構造方式,每種構造方式都有不同的實例過程,在java、c++、c#中,實例化對象的過程是固定的,這也就造成了js的功能復雜性。這里討論大家常用的實例化方法,即使用new來創建對象的方法)

當然我們也可以在修改原型對象的屬性或替換原型對象。

在查詢屬性或方法時,當前對象沒有查詢到時,會自動在原型對象中查詢,依次沿原型鏈向上。

由於在派生和實例化的過程中,新對象和新變量都會保留對原型對象的引用。當函數調用時,需查找和獲取的變量和元素都會通過原型鏈機制一層層的往上搜索在原型對象或繼承來的對象中獲得。

實例化對象產生新變量的三種方式

1、字面量方式

通過Object函數創建D變量。

var D={}

Object對象通過Object構造函數,實例化獲得變量D。變量D的__proto__指向Object對象。

?
1
2
3
4
5
var a = {};
console.log(a.prototype);  //undefined,未定義
console.log(a.__proto__);  //{},對象Object
console.log(a.constructor); //[ Function : Object],函數Object
console.log(a.__proto__.constructor);  //[ Function : Object],函數Object

2、構造函數方式

通過構造函數B創建對象C

function B(){}
var C=new B()

B函數定義時,系統會自動由Object對象派生一個中間對象作為函數的構造綁定對象Temp。通過函數B實例化變量時,就是對Temp對象進行實例化得到變量C。變量C就擁有Temp對象的屬性方法(就是原始Object對象的屬性和方法)+構造函數中的屬性方法。變量的__proto__ 指向這個Temp對象,變量的Constructor指向函數。

?
1
2
3
4
5
6
7
8
9
var A = function (){};
console.log(A.prototype);  //A {},A函數的構造綁定對象
console.log(A.__proto__);  //[ Function ], Function 對象
var a = new A();
console.log(a.__proto__); //A {},A函數的構造綁定對象
console.log(a.constructor); //[ Function : A],函數A
console.log(a.constructor.prototype); //A {},A函數的構造綁定對象
console.log(a.__proto__.__proto__); // {},(Object對象)
console.log(a.__proto__.__proto__.__proto__); // null

3、通過Object.creat創建對象

如圖中通過對象D創建對象E

var E=Object.creat(D)

E變量的原型鏈指向對象D。

?
1
2
3
4
var a1 = { 'age' :1}
var a2 = Object. create (a1);
console.log(a2.__proto__); //Object { age: 1 }
console.log(a2.constructor); //[ Function : Object]

案例講解

現在我們再來看案例。是不是清晰多了。js在線測驗網站https://tool.lu/coderunner/

?
1
2
3
4
5
6
7
8
9
function Person () {
}
var person1 = new Person();      
Person.prototype.age= 18;
Person.__proto__. name = "小明" ;
var person2 = new Person();
console.log(person1.age);//18
console.log(person2.age); //18
console.log(person2. name );  //未定義

var person1 = new Person(); 這條語句。通過函數實例化了一個變量,系統自動創建一個Object對象派生的中間對象Temp作為與構造函數綁定的原型對象。Person.prototype就指向這個中間對象Temp。
Person.prototype.age修改了Temp對象。
Person.__proto__.name,我們知道函數都是由Function對象派生的,這句話就是修改的Function對象對象。
var person2 = new Person(); 這個語句同樣通過函數實例化一個對象。一個構造函數只能綁定一個原型對象,所以這個原型對象就是Temp對象
person1.age訪問了age屬性,先在當前空間中查找,沒有找到,於是沿原型鏈向上查找這個原型對象Temp。查找成功。
person2.name在變量和原型對象Temp中都不存在,所以顯示未定義。

下面的留給讀者自己理解

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
var a1 = { 'age' :1}
console.log(a1.prototype);  //undefined,未定義
console.log(a1.__proto__);  //{},對象Object
console.log(a1.constructor); //[ Function : Object],函數Object
console.log(a1.__proto__.constructor);  //[ Function : Object],函數Object
 
var a2 = Object. create (a1);
console.log(a2.__proto__); //{ age: 1 },對象a1
console.log(a2.constructor); //[ Function : Object],對象 Function
 
var Person = function (){};
console.log(Person.prototype);  //Person {},函數Person的構造綁定對象
console.log(Person.__proto__);  //[ Function ],對象 Function
var person1 = new Person();
console.log(person1.__proto__); //Person {},函數Person的構造綁定對象
console.log(person1.constructor); //[ Function : Person],函數Person
console.log(person1.constructor.prototype); //Person {},函數Person的構造綁定對象
console.log(person1.__proto__.__proto__); // {},(Object對象)
console.log(person1.__proto__.__proto__.__proto__); // null
 
 
Person.prototype.age= 18;
Person.__proto__. name = "小明" ;
var person2 = new Person();
console.log(person1.age);//18
console.log(person2.age); //18
console.log(person2. name );  //未定義
 
__proto__
protoType

 


 

【作用域】

javascript中的作用域可以理解為一個語句執行的環境大小,有全局的作用域,函數作用域和eval作用域。在JS中沒有塊級作用域。所以在js中if語句,for語句內的不存在只有他們能訪問的變量。只有在函數內存在局部變量。但不要因此就定義除函數以外所有使用{ }括起來的都是塊級元素。對象使用{ }定義變量,對象內是屬性的定義,不是變量定義,所以不存在作用域的說法。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if(1){
   var name2= 'lp2'
}
console.log(name2)   //lp2
for (var i=0;i<10;i++){
     var name3= 'lp3'
}
console.log(name3)  //lp3
Person={
     name4: 'lp4'
}
console.log(name4)  //undefined
function person(){
   var name1= 'lp1'
}
console.log(name1)  //undefined

講到作用域,不得不講執行環境,執行環境在JS中是最為重要的概念。執行環境定義了變量或函數有權訪問的其他數據,決定了他們各自的行為。每個執行環境都有一個與之關聯的環境對象,環境中定義的所有變量和函數都保存在這個環境對象中。在web瀏覽器中全局環境被認為是window對象,某個執行環境中的所有代碼執行完畢后就被該環境銷毀,保存在其中的所有變量和函數定義也隨即銷毀。

每個函數都有自己的執行環境。當執行流進入一個函數時,函數的環境就會被推入一個環境棧中。而在函數執行之后,棧將其環境彈出,把控制權返回給之前的執行環境。

當代碼在一個環境中執行時,會創建環境對象的一個作用域鏈。作用域鏈的用途是保證對執行環境有權訪問的所有變量和函數的有序訪問。作用域鏈的前端始終是當前執行的代碼所在外部環境的環境對象。作用域鏈中的下一個環境對象來自包含(外部)環境,再下一個環境對象則來自於再下一個包含環境,這樣一直延續到全局執行環境,全局執行環境的環境對象始終都是作用域鏈中的最后一個對象。

需注意的是:在局部作用域中定義的變量可以在全局環境和局部環境中交互使用。內部環境可以通過作用域鏈訪問所有的外部環境,但外部環境不可以訪問內部環境中的任何變量和函數。每個環境都可以向上搜索作用域鏈,以查詢變量和函數名,但任何環境都不可以通過向下搜索作用域鏈而進入另一個執行環境。

作用域鏈本質上是一個指向環境對象的指針列表,他只引用但不包含環境對象。

【閉包】

閉包是指有權訪問另一個函數作用域中的變量的函數,這里要把它與匿名函數區分開(匿名函數:創建一個函數並將它賦值給一個變量,這種情況下創建的函數叫匿名函數,匿名函數的name屬性是空字符串),創建閉包的常見方式就是在一個函數內部創建另一個函數。閉包保存的是整個變量的對象。

閉包的作用:在函數執行過程中,為讀取和寫入變量的值,就需要在作用域鏈中查找變量,這時靈活方便的閉包就派上用場,我們知道當一個函數被調用時就會創建一個執行環境及相應的作用域鏈,那么閉包就會沿着作用域鏈向上獲取到開發者想要的變量及元素。

閉包靈活方便,也可以實現封裝,這樣就只能通過對象的特定方法才能訪問到其屬性。但是,不合理的使用閉包會造成空間的浪費,內存的泄露和性能消耗。

當函數被創建,就有了作用域,當被調用時,就有了作用域鏈,當被繼承時就有了原型鏈,當需要獲取作用域鏈或原型鏈上的變量或值時,就有了閉包。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM