JavaScript原型及原型鏈

 1. 什麼是原型

 1.1. 題外話

理解對象(或者說函數的)的原型(能夠經過 Object.getPrototypeOf(obj) 或者已被棄用的 proto 屬性得到)與構造函數的prototype屬性之間的區別是很重要的。前者是每一個實例上都有的屬性,後者是構造函數的屬性。也就是說,Object.getPrototypeOf(new Foobar())和Foobar.prototype指向着同一個對象。javascript

這個 __proto__[[Prototype]] 的因歷史緣由而留下來的 getter/setter(一個getter函數和一個setter函數), 暴露了經過它訪問的對象的內部 [[Prototype]]__proto__這個東西已經從web標準中廢棄了,儘管在Chrome中還可使用,能夠經過它修改一個對象的 [[Prototype]]屬性,可是這是很是耗性能的。

同時,原型鏈中的方法和屬性沒有被複制到其餘對象——它們被訪問須要經過前面所說的「原型鏈」的方式。沒有官方的方法用於直接訪問一個對象的原型對象——原型鏈中的「鏈接」被定義在一個內部屬性中,在 JavaScript 語言標準中用 [[prototype]] 表示(參見 ECMAScript)。然而,大多數現代瀏覽器仍是提供了一個名爲 __proto__ (先後各有2個下劃線)的屬性,其包含了對象的原型。php

每次講到原型,我都要回顧下下面這張圖:html

image

 1.2. 定義

原型既指構造函數的prototype屬性指向的對象前端

其實也指實例的 [[Prototype]]屬性(經過__proto__訪問),它們指向的是同一個東西。

參考:JavaScript學習筆記(十二) 原型 文中的圖片不錯,很形象。java

原型是 function 對象的一個屬性,見上圖Person.prototype,原型定義了構造函數製造出的對象的公共祖先。經過該構造函數產生的對象,能夠繼承該原型的屬性和方法,原型也是對象。node

天然而然我聯想到了Java中的繼承,進而Java有了重寫,一樣JS咱們能夠在對象中對原型的方法和屬性進行重寫,可是不能經過修改對象的屬性修改(此處指增刪改查)原型,想要修改原型只有把原型調出來才能修改,以下代碼:
Person.prototype.age = 22; //直接調出來修改

 Person.prototype.name = "田";

 function Person(age, name) {

 this.age = age;

 this.name = name;

 }

 var per = new Person(23, "揮動");

 console.log(per);

這裏題外話解釋一下JavaScript構造函數內部原理web

  1. 在函數體最前面隱式地加上this = {}(這一步存在疑問,下面解釋)
  2. 執行this.XXX = xxx
  3. 隱式地返回this

以下算法

Person.prototype.age = 22;

 function Person() {}

 var per = new Person();

 console.log(per.age); //22

Person()是一個構造函數,而.prototype是系統在 Person 出生時添加的屬性,prototype 譯爲原型,原型是構造函數構造出的對象的公共祖先,若是不對它作什麼修改,那麼原型值 = {},此處咱們加了age = 22  數組

此處附加一個小知識,圖中淺粉色表明系統給你寫的,紫色表明你本身寫的,見代碼,構造函數是系統寫的,而age是你本身在原型中加的,系統給定的構造函數咱們也能夠本身在代碼中調出原型進行修改,如上圖

如今回到上面存疑的步驟,開篇講過構造函數的原理第一步this = {},值得懷疑的是他到底傳入的是否是空的{}瀏覽器

在控制檯查看它並非空的,裏面有一個__proto__屬性,那麼第一步實際上應該是下面這樣

// 注意這是僞代碼

 var this = {

 __proto__:Person.prototype

 };

 // 能夠在控制檯中試一試:per.__proto__===Person.prototype返回true

當咱們在查找對象的屬性或方法時會首先在本身裏面找,若是找不到會找__proto__指向的原型,這樣就把對象和原型鏈接到了一塊兒。__proto__存的是對象的原型,或者換句話說每一個對象都有一個__proto__指向構造它的原型,由此咱們能夠發現 __proto__指向的構造函數是能夠修改的,所以per的構造函數也就未必是Person()了,也就是說Person()這個構造函數構造出來的對象的原型不必定是Person.prototype,以下圖

咱們再來看下面的代碼結果是這樣的,爲何?

function Person() {}

 var per1, per2, per3;

 per1 = new Person();

 console.log(per1); //Person {}

 Person.prototype.name = "sunny"; //經過結果發現,影響到了per1

 per2 = new Person();

 Person.prototype = { //這個卻沒有影響到per1和per2,爲何?

 name: "cherry"

 }

 per3 = new Person();

 console.log("per1.name:" + per1.name); //per1.name:sunny

 console.log(per1.__proto__); //{name: "sunny", constructor: ƒ}

 console.log("per2.name:" + per2.name); //per2.name:sunny

 console.log(per2.__proto__); //{name: "sunny", constructor: ƒ}

 console.log("per3.name:" + per3.name); //per3.name:cherry

 console.log(per3.__proto__); //{name: "cherry"}

 console.log(per1.__proto__ === per2.__proto__); //true

 console.log(per2.__proto__ === per3.__proto__); //false

 console.log(per3.__proto__.__proto__ === Object.prototype); //true

咱們講到構造函數第一步至關於var this = {__proto__:Person.prototype},在這裏__proto__Person.prototype都至關於指向了同一個空間,per1建立的時候構造函數裏面什麼都沒有寫,儘管per1的__proto__並無修改,可是它和per2指向的是同一個空間,咱們在後面修改了Person.prototype的值,因此後面per二、per1的輸出值都是sunny。

對於per3咱們發現它沒有構造函數,由於咱們後面修改原型的方式不是修改Person.prototype所指向的對象中的一個屬性,而是給Person.prototype直接賦值一個新的對象,因此per3輸出cherry,而且這個新的對象是以對象字面量的方式建立的,默認這種對象的原型就是Object,因此per3.__proto__.__proto__ ===Object.prototype //true

let obj = {

 name: "HUI"

 };

 console.log(obj.__proto__ == Object.prototype); //true

關於預編譯能夠參考:JavaScript預編譯、做用域

 2. 原型鏈

function Grand() {

 this.word = "我是爺";

 }

 var grand = new Grand();

 Father.prototype = grand;

 //grand是用Grand()建立的一個對象實例,它是個對象

 function Father() {

 this.name = "我是爹";

 }

 var father = new Father();

 Son.prototype = father;

 // 同上,直接給原型賦值一個對象

 function Son() {

 this.hobbit = "smoke";

 }

 var son = new Son();

像這樣在原型上面再加一個原型對象的方法叫作原型鏈,原型鏈使用__proto__來鏈接各個原型,控制檯嘗試輸出如圖

在其中能夠看出,Grand的__proto__指向Object,而再點開Object發現已經沒有__proto__屬性(有prototype),說明Object()就是原型鏈的終端(實際上Object.prototype.__proto__指向null,也能夠說null是終端)。

 3. 原型鏈的增刪改查

 3.1. 刪除

只能在本身身上經過原型刪除(delete father.name),不能經過父代或者後代來刪除

 3.2. 增長

能夠本身增,後代通常不能增。可是不排除下面這種狀況:在後代中 增長 了父級對象的屬性。經過子代調用了引用 修改或者說增長了父代的屬性,這是一種調用的修改而不是賦值的修改,而且這種修改也僅限於引用值,好比原始值你就只能覆蓋,不能修改。

function Father() {

 this.name = "我是爹";

 this.for = {

 sd1: "哈哈哈",

 }

 }

 var father = new Father();

 Son.prototype = father;

 // 同上,直接給原型賦值一個對象

 function Son() {}

 var son = new Son();

 console.log(son);

 son.for.sd2 = "操";

 son.name = "更名"; //最終father沒有更名,而是在son新增了名字

 console.log(father);

 son.father.name = "更名"; //Uncaught TypeError: Cannot set property 'name' of undefined

 3.3. 修改

 3.4. 查詢

Father.prototype = {

 num: 100

 }

 function Father() {

 this.eat = function () {

 this.num++; //把值拿過來+1再賦給本身

 }

 }

 var son = new Father();

 son.eat();

 console.log(son.num); //101

 console.log(Father.prototype.num); //100

 4. Object.create(原型)方法

還需注意原型是隱式的內部屬性,只有系統給咱們的才能用,假如咱們本身在一個沒有原型的對象中添加了__proto__屬性,系統是不能識別的。

var obj = Object.create(null);

 obj.__proto__ = {

 name: "sunny"

 }

 console.log(obj); //{__proto__:{name: "sunny",__proto__: Object}}

 console.log(obj.name); //undefined 此時發現從原型鏈上找是找不到的

注意圖中,obj.__proto__是能夠訪問的,由於咱們賦值的時候是一個對象{name: "sunny"},可是Chrome並不能識別obj.name,此時的obj並非繼承自Object.prototype,由此總結絕大多數對象最終都會繼承自Object.prototype,但不是所有,還需注意Object.create(原型)方法必須傳入object或者null!不能爲空。

 注意:咱們發現 toString()方法在 Object的原型中,那麼應該說不少通過包裝類的值均可以使用這個方法,不過 undefined、null 以及本身構造的沒有原型的對象是沒有這個方法的。

值得注意的是許多東西均可以調用toString()方法,按我所想調用的都是Object原型中的方法,可是事實並不是如此,舉個例子

Object.prototype.toString = function () {

 return "人爲重寫";

 //在此重寫Object的toString方法 

 }

 var nun = 123;

 console.log(nun.toString()); //123,明顯nun調用了重寫的方法(若是有的話)

 console.log(Number.prototype.toString.call(nun)); //123

 console.log(Object.prototype.toString.call(nun)); //人爲重寫

方法的重寫不但出現於工程師與機器之間,也存在於機器和本身之間,nun.toString()其實是調用了重寫的方法,爲何?由於Object的toString方法實際上不完善,輸出的信息沒什麼用,假如調用Object的方法就會輸出"[object Number]",因此須要重寫方法,諸如BooleanArray等等都會調用重寫的方法,實際狀況就是它們都有本身重寫的toString。

 5. call/apply/bind

三者均可用於重定義this對象,或者說重定義this指向。

 5.1. call

上面看到了call:Object.prototype.toString.call(nun),call的做用是改變this的指向,這裏nun是調用者,調用前面的Object.prototype.toString方法,舉個例子

function Person(name, age) {

 this.name = name;

 this.age = age;

 this.say = function () {

 console.log("myName is:" + this.name);

 }

 }

 var obj1 = {};

 Person.call(obj1, "Tian", 21); //借用構造函數

 console.log(obj1); //{name: "Tian", age: 21, say: ƒ}

 var obj2 = {};

 Person.apply(obj2, ["Hui", 22])

 console.log(obj2); //{name: "Hui", age: 22, say: ƒ}

 var fun = obj1.say;

 fun.bind(obj1)(); //fun.bind(obj1)返回的是一個函數

此處使用了Persn()來構造obj,在開發中每每在某一構造函數徹底覆蓋另外一構造函數時使用這種方法,以下,Student的需求徹底覆蓋了Person,就能夠在Student中使用call使用Person的代碼,而不用本身再寫一遍,call時企業級開發組裝函數的一個方法之一

function Person(name, age, sex) {

 this.name = name;

 this.age = age;

 this.sex = sex;

 }

 function Student(name, age, sex, grade, tel) {

 Person.call(this, name, age, sex);

 this.grade = grade;

 this.tel = tel;

 }

 var nun = new Student("Tian", 21, "Male", 9, 188);

 console.log(nun); //Student {name: "Tian", age: 21, sex: "Male", grade: 9, tel: 188}
爲何call()第一個參數傳入this值得思考

 5.2. apply

apply的區別在於傳參數不同,call是一個一個把參數傳進去,而apply是一個arguments,也就是一個數組(不包括this),咱們只須要把call()除開this的其餘參數用[]括起來就能夠了,以下

Person.apply(this, [name, age, sex]);

 5.3. bind

bind() 方法建立一個新的函數,在 bind() 被調用時,這個新函數的 this 被指定爲 bind() 的第一個參數,而其他參數將做爲新函數的參數,供調用時使用。

function Person(name, age) {

 this.name = name;

 this.age = age;

 this.say = function (ming) {

 console.log("myName is:" + ming);

 }

 }

 let obj1 = new Person();

 var fun = obj1.say;

 fun.bind(obj1, "Hang")(); //myName is:Hang

 6. JS的數字操做

console.log(0.14 * 100); //輸出14.000000000000002,JS的精度不高

 console.log(Math.ceil(123.33)); //輸出124,向上取整

 console.log(Math.floor(123.33)); //輸出123,向下取整

 console.log(Math.random()); //產生0~1之間的隨機數

 var nun = Number(123.654);

 console.log(nun.toFixed(2)); //123.65

 // toFixed把Number四捨五入爲指定小數位數的數字,此處指定2位

 // 產生100之內隨機數

 for (var i = 0; i < 10; i++) {

 var num = Math.random();

 console.log("隨機數:" + num);

 num = num.toFixed(2);

 console.log("小數位:" + num);

 num = num * 100;

 console.log("乘一百:" + num);

 }

按道理來講在for循環中最後輸出的數字應該都是0~100的兩位數,可是結果並不是如此,好比

let num = 0.5458565720681452;

 num = num.toFixed(2);

 num = num * 100;

 console.log(num); //55.00000000000001

一樣這仍是由於精度不高,此處有一個解決辦法就是先乘100再取整。

for (var i = 0; i < 10; i++) {

 var num = Math.random();

 num = num * 100;

 num = num.toFixed(0);

 console.log(num);

 }

可計算範圍:小數點前16位與後16位,若是超出請使用BigInt。

 7. 基本類型和引用類型的值 P69

 7.1. 數據類型

關於JavaScript的數據類型可參考 YAMA:JavaScript數據類型及變量

 如下P xx表示《JavaScript高級程序設計》的第幾頁

ECMAScript變量可能包含兩種不一樣數據類型的值:基本類型值和引用類型值,前者指簡單的數據段,後者指可能由多個值構成的對象

  1. 基本類型值:undefined、null、boolean、number、string、新增(Symbol、BigInt)
  2. 引用類型:Object
參考: YAMA:JavaScript數據類型

定義基本類型值和引用類型值的方式是相似的:建立一個變量併爲其賦值。可是當值保存到變量中之後

  • 對於引用類型值咱們能夠對其添加屬性(增刪改查)。
  • 咱們不能給基本類型值添加屬性,可是這樣作了並不會報錯。

 7.2. 賦值

把一個變量賦值給另外一個變量時:

如果基本數據類型,則會新建一個值,把該值複製到新變量的內存上,此時兩個變量的值各自獨立,互不干擾。

此時若是是引用類型值,則會複製指針的值,該指針指向存儲在堆中的對象,賦值後兩個變量將引用同一個對象,改變其中一個變量會影響另外一個變量  。

function Person() {

 this.name = "hui";

 }

 var per1 = new Person();

 var per2 = per1;

 console.log(per1.name); //輸出hui

 per2.name = "tian";

 console.log(per1.name); //輸出tian

引用類型值複製示意圖以下

 7.3. 在傳遞參數時P70

JavaScript中,函數都是按照值來傳遞的,也就是說把函數外部的值賦值給函數內部的參數,就像把值從一個變量複製到另外一個變量同樣(這裏面分爲基本類型值的傳遞和引用類型值的傳遞),對此我感到困惑,由於參數只能按值傳遞,而訪問變量卻有兩種方式,那麼在向函數傳遞引用類型值時到底時怎麼傳值的?

圖片源自 淺析 JavaScript Clone:堆內存、棧內存

當變量複製引用類型值的時候,它是一個指針,指向存儲在堆內存中的對象(堆內存中的對象沒法直接訪問,要經過這個對象在堆內存中的地址訪問,再經過地址去查值(RHS查詢,試圖獲取變量的源值),因此引用類型的值是按引用訪問)

所謂的傳值是由於這個在棧內存中的變量,也就是這個指針(個人意思是這個指針是原始值)是存儲在棧上的一個指針,指向一個存儲在堆內存中的對象,因此說JS函數傳參必定是按值傳遞的,可是訪問變量確實有兩種方式。

傳遞基本數據類型的值好理解,其實傳遞引用類型值時傳遞的仍然是值,傳遞的時候會把這個值在內存中的地址複製給一個局部變量,也就是形參,所以形參的變化會反映在外部,以下代碼就說明了這一點

function Person() {

 this.name = "hui";

 }

 var per1 = new Person();

 function setname(per) {

 per.name = "tong";

 console.log("局部" + per.name); //輸出 局部tong

 per = new Person();

 console.log("局部" + per.name); //輸出 局部hui

 per.name = "tian";

 console.log("局部" + per.name); //輸出 局部tian

 }

 console.log(per1.name); //輸出hui

 setname(per1);

 console.log(per1.name); //輸出tong

 // 最後一行輸出爲tong,說明在setname函數內部per = new Person();並無起做用,

 // 由於函數按值來傳遞

注意此處還在函數內部新建了一個同名的對象per,若是per1是按引用傳遞的,那麼per就會自動修改成指向其name屬性爲tian的新對象。但當接下來在外部訪問per1.name是dong,這說明在函數內部修改了參數的值但原始的引用仍然保持不變。實際上在函數內部重寫per時,這個變量引用的就是一個局部對象了。而這個局部對象會在函數執行完畢後當即被銷燬。

這裏重申:對於JS中的內存,究竟是怎麼存的,文中只是給出了一種想得通的說法,真要追究到底,恐怕並不是如此。參考: https://juejin.im/post/684490...

 8. 執行環境及做用域

關於做用域能夠參考:JavaScript預編譯、做用域

 8.1. 執行環境

執行環境定義了變量或函數有權訪問的其餘數據,決定了它們各自的行爲。 每一個執行環境都有一個 與之關聯的變量對象,環境中定義的全部變量和函數都保存在這個對象中。雖然咱們 編寫的代碼沒法訪問這個對象,但解析器在處理數據時會在後臺使用它。

根據宿主環境不一樣,表示的執行環境也不同,在Chrome中全局執行環境就是window對象,在node中是Global 對象。

 8.2. 做用域

參考:

  1. 掘金  JavaScript執行環境及做用域
  2. https://www.xiaohuochai.site/...

做用域是根據名稱查找變量的一套規則。負責收集並維護有全部聲明的標識符(變量)組成的一系列查詢,並實施一套嚴格規則,肯定當前執行的代碼對這些標識符的訪問權限,以及保證對執行環境有權訪問的全部變量和函數的有序訪問。做用域鏈的前端,始終都是當前執行的代碼所在環境的變量對象。若是這個環境是函數,則將其活動對象(AO)做爲變量對象。

活動對象在最開始時只包含一個變量,即arguments對象(這個對象在全局環境中是不存在的)。做用域鏈中的下一個變量對象來自包含(外部)環境,而再下一個變量對象則來自下一個包含環境。這樣,一直延續到全局執行環境;全局執行環境的變量對象始終都是做用域鏈中的最後一個對象。

還需注意在JS中if語句等等使用的大括號不算是塊做用域。以下

if (true) {

 var color = "blue";

 }

 alert(color); //"blue"
使用let或const會建立塊做用域。

 9. 垃圾收集

參考:MDN 內存管理

像C語言這樣的底層語言通常都有底層的內存管理接口,好比 malloc()free()。相反,JavaScript是在建立變量(對象,字符串等)時自動進行了分配內存,而且在不使用它們時「自動」釋放。 釋放的過程稱爲垃圾回收。

在P180閉包就涉及到了垃圾回收,因爲閉包致使匿名函數的做用域一直引用着這個活動對象,換句話說活動對象還留在內存中不能被銷燬,這會致使內存泄漏。我猜測這裏的垃圾回收方式就是引用技術垃圾收集。

 9.1. 內存聲明週期

無論什麼程序語言,內存生命週期基本是一致的:

  1. 分配你所須要的內存
  2. 使用分配到的內存(讀、寫)
  3. 不須要時將其釋放、歸還

全部語言第二部分都是明確的。第一和第三部分在底層語言中是明確的,但在像JavaScript這些高級語言中,大部分都是隱含的。

 9.2. 當內存再也不須要使用時釋放

大多數內存管理的問題都在這個階段。在這裏最艱難的任務是找到「哪些被分配的內存確實已經再也不須要了」。它每每要求開發人員來肯定在程序中哪一塊內存再也不須要而且釋放它。

高級語言解釋器嵌入了「垃圾回收器」,它的主要工做是跟蹤內存的分配和使用,以便當分配的內存再也不使用時,自動釋放它。這隻能是一個近似的過程,由於要知道是否仍然須要某塊內存是沒法斷定的(沒法經過某種算法解決)。

如上所述自動尋找是否一些內存「再也不須要」的問題是沒法斷定的。所以,垃圾回收實現只能有限制的解決通常問題。

 9.3. 垃圾回收實現方式

  1. 標記-清除算法
  2. 引用計數垃圾收集

參考:

  1. https://developer.mozilla.org...
  2. https://juejin.im/post/5b6965...
  3. https://juejin.im/post/5b684f...

相關文章
相關標籤/搜索