好好聊聊原型

pro-xiaoy發表於2018-06-29

最近一直被人問到這個問題,這東西不就是完成你物件的繼承嗎?

認識論:認識論是探討人類認識的本質、結構,認識與客觀實在的關係,認識的前提和基礎,認識發生、發展的過程及其規律,認識的真理標準等問題的哲學學說。——世界觀

方法論:方法論,就是人們認識世界、改造世界的根本方法。——工具

主體:哲學上指對客體有認識和實踐能力的人,是客體的存在意義的決定者。“我” 客體:客體(object )可感知或可想象到的任何事物。“我”之外的一切事物,是認識與實踐的物件 本體:本體指事物的本身;引申為根本的 實體:馬克思主義以前的哲學,認為變化著的事物有一種永恆不變的基礎,就是實體。馬克思主義哲學認為所謂實體就是永遠運動著和發展著的物質。實體是客觀的物質世界,指有形、有象之物。

型別 型別的例項 物件和例項 型別物件的例項 物件引用和物件 物件型別

準備知識

  1. 執行緒棧 一個Windows程式可能有多個執行緒,執行緒建立時會分配到1MB的棧,棧空間用於向方法傳遞實參,方法內部定義的區域性變數也在棧上面。

  2. JS的單執行緒

  • DOM節點作為臨界資源並沒有多執行緒的需求,而且多執行緒本身也會帶來DOM操作的複雜性;
  • 單執行緒可避免上下文的切換,提高效率;
  1. 值型別和引用型別
  • 值型別:棧空間中分配空間,變數中直接儲存值;
  • 引用型別:堆記憶體中分配物件的儲存空間,棧空間中分配變數的空間,其中的值為指向物件的引用;
  1. 方法呼叫與執行 此處我們參考CLR的執行方式:
  • "序幕"程式碼(prologue code):方法開始工作時對其進行初始化。
  • "尾聲"程式碼(epilogue):方法完成時對其進行清理,以便返回撥用者。
  • 幀棧(stack frame):代表當前執行緒的呼叫棧中的一個方法呼叫。執行執行緒過程中,進行的每個方法呼叫時都會在呼叫棧中建立並壓入一個幀棧。

涉及到的操作:

  • wind:呼叫方法時壓住幀棧
  • unwind:方法執行完畢時彈出幀棧

執行環境(執行上下文)

控制器轉入ECMA腳步的可執行程式碼時,控制器會進入一個“執行環境”(execution context)。當前活動的多個執行環境在邏輯上形成一個棧結構。 該邏輯棧再最頂層的執行環境成為當前執行的執行環境(running execution context,活動執行環境)。任何時候,當控制器從當前執行環境相關的可執行程式碼轉入與該執行環境無關的可執行程式碼時,會建立一個新的執行環境。新建的這個執行環境會被壓入棧中,成為當前執行的執行環境。

元件 作用目的
詞法環境 指定一個詞法環境物件,用於解析該執行環境的程式碼建立的識別符號引用
變數環境 指定一個詞法環境物件,其環境資料用於儲存該執行環境的程式碼通過變數表示式和函式表示式建立的繫結
>This繫結 指定該執行環境內的ECMA腳步程式碼中this關鍵字所關聯的值

要點:

  1. 詞法環境是一個用於特定變數和函式識別符號在ECMAScript程式碼的詞法巢狀結構上關聯關係的規範型別。由一個環境記錄項和可能為空的外部詞法環境的引用構成。通常詞法環境會與特定的 ECMAScript 程式碼諸如 FunctionDeclaration,WithStatement 或者 TryStatement 的 Catch 塊這樣的語法結構相聯絡,且類似程式碼每次執行都會有一個新的語法環境被建立出來。
  • 環境記錄項記錄了在它的關聯詞法環境域內建立的識別符號繫結情形
  • 內部詞法環境的外部引用是邏輯上包含內部詞法環境的詞法環境。 (這句很關鍵) 外部詞法環境自然也可能有多個內部詞法環境。例如,如果一個 FunctionDeclaration 包含兩個巢狀的 FunctionDeclaration,那麼每個內嵌函式的詞法環境都是外部函式本次執行所產生的詞法環境。
  1. 詞法環境和變數環境元件始終為詞法環境物件。當建立一個執行環境時,其詞法環境變數環境元件最初是同一個值。在該執行環境相關聯的程式碼的執行過程中,變數環境元件永遠不變,而詞法環境元件有可能改變。
  2. 執行環境是一個純粹的標準機制,在ECMAScript程式中是不可能訪問到執行環境的。

注1:WithStatement 或者 TryStatement 的 Catch 塊 這兩個語句都會在作用域鏈的前端新增一個執行環境的變數物件。對於with語句來說,其變數物件中包含著指定為指定物件的所有屬性和方法所作的變數宣告。對於catch語句來說,其變數物件中包含的是被丟擲的錯誤物件的宣告。這些變數物件都是隻讀的,因此在with和catch語句中宣告的變數都會被新增到所在的執行環境的變數中。

var myColor = {};
var color = 'blue';
function changeColor(){
    var anotherColor = 'red';
    function swapColors(){
        var tempColor = anotherColor;
        anotherColor = color;
        color = tempColor;
        myColor.color = color;
    }
    //這裡能訪問
    swapColors();
}
changeColor();
with(myColor)
{
    color = 'black';
    var color1 = 'gray';
}
alert('Color is now ' + color);
alert('MyColor is now ' + myColor.color);
console.log(myColor);
//console.log(color1); //去掉註釋試試看是什麼
//console.log(this);
複製程式碼

注2:

宣告式環境記錄項:每個宣告式環境記錄項都與一個包含變數和(或)函式宣告的 ECMAScript的程式作用域相關聯。宣告式環境記錄項用於繫結作用域內定義的一系列識別符號。

物件式環境記錄項:每一個物件式環境記錄項都有一個關聯的物件,這個物件被稱作繫結物件 。物件式環境記錄項直接將一系列識別符號與其繫結物件的屬性名稱建立一一對應關係。

正是這些執行環境棧的執行環境的詞法環境與其包含的外部詞法環境的引用構成了一個連結串列的結構——作用域鏈

查詢標示符的過程是從作用域鏈的前端開始,向上逐級查詢與給定名字匹配的標示符. 如果在區域性環境中找到了該標示符,搜尋過程停止,變數就緒,否則搜尋過程將一直追溯到全域性環境的變數物件

// 宣告式記錄項:記錄識別符號a的宣告的作用域關聯
// 物件式記錄項:繫結物件(X),識別符號b、c與繫結物件(X)的屬性的對應關係
var a = {
    b:'',
    c:''
};
複製程式碼
// 外部程式碼,建立外部執行環境,並建立其變數物件A
var color = 'blue';
 
function changeColor(){
    if(color === 'blue'){
        color = 'red';
    } else {
        color = 'blue';
    }
}
changeColor();  // 執行函式changeColor, 進入該函式執行時建立新的執行環境,建立變數物件B,變數物件
alert('Color is now '+ color);
複製程式碼

簡而言之:每個函式在被呼叫時都會建立自己的執行環境。當執行流進入一個函式時,函式的環境就會被推入一個環境棧中。而在函式執行之後,棧將其環境彈出,把控制權返回給之前的執行環境。

this

當控制進入執行環境時,建立並初始化作用域鏈,進行變數初始化,並決定 this 值。作用域鏈的初始化,變數的初始化和 this 值的決定取決於進入的程式碼型別。

  1. 全域性程式碼 global

this 值為全域性物件

  1. 求值程式碼 eval

當控制進入求值程式碼的執行上下文時,把前一個活動的執行環境引用為呼叫環境,用它決定作用域鏈、可變物件和 this 值。若呼叫上下文不存在,就把它當作全域性物件,進行作用域鏈和變數的初始化及 this 值的決定。

  1. 函式程式碼 function

this 值由呼叫者提供,若呼叫者提供的 this 值不是一個物件(注意,null 不是物件),則 this 值為全域性物件

Function物件

  1. 當將 Function 作為函式來呼叫,而不是作為構造器,它會建立並初始化一個新函式物件。所以函式呼叫 Function(…) 與用相同引數的 new Function(…) 表示式建立的物件相同。

  2. 當 Function 作為 new 表示式的一部分被呼叫時,它是一個構造器:它初始化新建立的物件。

原型(Function.prototype)

為其他物件提供共享屬性的物件。( prototype 屬性的值用於初始化一個新建立物件的的 [[Prototype]] 內部屬性)

當構造器建立一個物件,為了解決物件的屬性引用,該物件會隱式引用構造器的“prototype”屬性。通過程式表示式 constructor.prototype 可以引用到構造器的“prototype”屬性,並且新增到物件原型裡的屬性,會通過繼承與所有共享此原型的物件共享。另外,可使用 Object.create 內建函式,通過明確指定原型來建立一個新物件。

var a = Object.create({name:'123'})
複製程式碼
  1. Function 構造器自身是個函式物件,它的 [[Class]] 是 "Function"。Function 構造器的 [[Prototype]] 內部屬性值是標準內建 Function 的 prototype 物件。
Function.__proto__ == Function.prototype // true
複製程式碼
  1. Function 的 prototype 物件自身是一個函式物件 ( 它的 [[Class]] 是 "Function"),呼叫這個函式物件時,接受任何引數並返回 undefined。
typeof(Function.prototype) === 'function' // true
Function.prototype('a','b') === undefined // true
複製程式碼
  1. Function 的 prototype 物件的 [[Prototype]] 內部屬性值是標準內建 Object 的 prototype 物件
Function.prototype.__proto__ == Object.__proto__ // false
Function.prototype.__proto__ == Object.prototype // true
Object.__proto__ === Object.prototype // false
Function.prototype == Object.__proto__ // true
Object.__proto__.__proto__ == Object.prototype // true
複製程式碼
  • Function.prototype.constructor
  • Function.prototype.toString()
  • Function.prototype.apply (thisArg, argArray)
  • Function.prototype.call (thisArg [ , arg1 [ , arg2, … ] ] )

原型鏈

每個由構造器建立的物件,都有一個隱式引用 ( 叫做物件的原型 ) 連結到構造器的“prototype”屬性值。再者,原型可能有一個非空 (non-null) 隱式引用連結到它自己的原型,以此類推,這叫做 原型鏈 。當向物件的一個屬性提出引用,引用會指向原型鏈中包含此屬性名的第一個物件的此屬性。換句話說,首先檢查直接提及的物件的同名屬性,如果物件包含同名的屬性,引用即指向此屬性,如果該物件不包含同名的屬性,則下一步檢查物件的原型;以此類推。

CF 是一個構造器(也是一個物件)。五個物件已用 new 表示式建立 : cf1, cf2, cf3, cf4, cf5。每個物件都有名為 q1 和 q2 的屬性。虛線表示隱式原型關係;例如:cf3 的原型是 CFp。構造器 CF 自己有名為 P1 和 P2 的兩個屬性 , 這對 CFp, cf1, cf2, cf3, cf4, cf5 是不可見的。CFp 的名為 CFP1 的屬性共享給 cf1, cf2, cf3, cf4, cf5 ( 沒有 CF), 以及在 CFp 的隱式原型鏈中找不到任何名為 q1, q2, 或 CFP1 的屬性。 請注意 ,CF 和 CFp 之間沒有隱式原型連結。

不同於基於類的物件語言,屬性可以通過賦值的方式動態新增給物件。也就是說,構造器並不是非要對構造的物件的全部或任何屬性命名或賦值。上圖中,可以給 CFp 新增新屬性值的方式為 cf1, cf2, cf3, cf4,cf5 新增一個新的共享屬性。

Function.prototype.P1 = 'p1';
Function.prototype.P2 = 'p2';
var CF = function(){
}

var CFp = {CFP1:'cfp1'}

CF.prototype = CFp;

var cf1 = new CF();
複製程式碼

相關文章