JS中的原型和原型鏈

上篇的最後咱們提到了hasOwnProperty是用來檢測某個屬性是否爲當前實例的私有屬性的,咱們還本身編寫了hasPubProperty用來檢測某個屬性是否爲當前實例的公有方法的;私有方法上文中已經介紹,就是實例自己私有的方法,存在當前實例中;那什麼是公有方法,他們又在哪裏呢?這就是咱們今天要講的原型和原型鏈。javascript

咱們以數組爲例:java

每個數組都是Array這個內置數組類的實例;數組

let arr1 = [10, 20];
let arr2 = [30, 40];

console.log(arr1 instanceof Array); //=>true
console.log(arr1.hasOwnProperty('push')); //=>false
console.log(arr1.push === arr2.push); //=>true
arr1.push(100); //=>對象.屬性 說明PUSH是ARR1的一個屬性,並且是公有屬性(其它數組中經常使用的方法也都是數組實例的公有屬性)
複製代碼

push等一系列數組的方法在哪裏呢?瀏覽器

咱們先記住三句話:很重要、很重要、很重要,重要的事說三遍bash

  • 一、每個函數都天生具有一個屬性:prototype(原型),prototype的屬性值是一個對象(瀏覽器默認會給其開闢一個堆內存)函數

    • =>「原型對象上所存儲的屬性和方法,就是供當前類實例所調用的公有的屬性和方法」
  • 二、在類的prototype原型對象中,默認存在一個內置的屬性:constructor(構造函數),屬性值就是當前類(函數)自己,因此咱們也把類稱爲構造函數ui

  • 三、每個對象都天生具有一個屬性:__proto__(原型鏈),屬性值是當前實例(對象)所屬類的prototype原型this

思惟導圖

1、前言

仍是以Array數組內置類爲例spa

類(自定義類仍是JS內置類)都是函數數據類型prototype

咱們能夠打開控制檯輸出:

爲了更好的理解,咱們每步一圖

一、既然都是引用數據類型,就都會開闢一個堆內存,堆內存中存儲代碼字符串;如圖

由於Array是內置類,因此存儲的爲原生代碼,瀏覽器爲了保護原生代碼,不讓咱們查看具體內容,因此輸出爲[native code],若是是咱們本身建立的自定義類是能夠看見的;

二、此時咱們建立了兩個數組

這裏咱們記住一句話

全部的實例都是對象數據類型值(除基本數據類型值的實例外)

let arr1 = [10, 20];
let arr2 = [30, 40];
複製代碼

數組arr1中的0:101:20length:2 都是arr1實例的私有屬性; arr1arr2裏的內容互不衝突(緣由在單例模式中講過);

每個實例對象,本身堆內存中儲存的屬性都是私有屬性

不管是arr1仍是arr2均可以調用數組的push...等方法,可是這些方法私有裏又都沒有,那這些方法怎麼出來的呢?

2、原型和原型鏈

這裏咱們就用到了上面的三句話:

一、每一個函數上都有一個prototype屬性;屬性值是一個對象

每個函數(包括👇)都天生具有一個屬性:prototype(原型),prototype的屬性值是一個對象(瀏覽器默認會給其開闢一個堆內存)

  • 普通函數
  • 類也是函數類型的值

原型對象上所存儲的屬性和方法,就是供當前類實例所調用的公有的屬性和方法

二、在prototype原型對象中,默認存在一個constructor屬性,屬性值就是當前函數自己

在類的prototype原型對象中,默認存在一個內置的屬性:constructor(構造函數),屬性值就是當前類(函數)自己,因此咱們也把類稱爲構造函數

若是咱們在控制檯輸出 dir(Array)找到 prototype找到 constructor結果是 Array,這樣一直找會無限循環...

三、每個對象都有一個__proto__屬性,屬性值是當前實例所屬類的prototype原型

每個對象都天生具有一個屬性:__proto__(原型鏈),屬性值是當前實例(對象)所屬類的prototype原型 這裏的對象爲泛指:包括

  • 對象數據類型值
    • 普通對象
    • 數組對象
    • 正則對象
    • ...
  • 實例也是對象類型值(除基本值外)
  • 類的prototype原型屬性值也是對象
  • 函數也具有對象的特徵(它有一重身份就是對象類型)
  • ...

如今咱們輸出:

  • 一、arr1.length 或者 arr1[0] ;
    • => 是獲取當前實例的私有屬性值;
  • 二、arr1.push() ;
    • 首先找本身的私有屬性,私有屬性有,調取的就是私有屬性
    • 若是沒有,默認基於__proto__原型鏈屬性,找所屬類prototype原型上的公共屬性和方法
    • (這種查找機制就是原型鏈查找)
  • 三、arr1.push() === arr2.push();
    • 找到arr1.push 在找到 arr2.push
    • 都是原型上的同一個方法,指向的都是同一個空間地址,因此爲true;

因此咱們說原型上存放的是實例的公共屬性和方法;

3、原型鏈查找

  • 四、arr1.proto.push
    • 直接找所屬類原型上的 push 方法,相似於 Arrary.prototype.push 這樣找
    • arr1.__proto__.push === arr2.push === Array.prototype.push

咱們知道arr1 arr2 實例對象,他們所屬的類是 Array ,因此 arr1.__proto__===Array.prototype確定沒有問題,

那咱們Array.prototype這個對象是誰的實例呢?

  • 一、確定不是Array的實例(他是Array的原型),數組纔是Array的實例
  • 二、他自己是一個對象;

全部的對象數據類型值,都是內置類Object的一個實例

Object的原型也是一個對象,每一個對象都有一個__proto__屬性指向當前所屬類的原型,而全部對象數據類型,都是Object的一個實例;

咱們發現他最後指向了他本身;指向本身就失去了原型鏈查找的意義,因此咱們規定Object.prototype.__proto__ === null

Object 是全部對象的基類,在他的原型上的__proto__屬性,若是存在也是指向本身的原型,這樣沒有意義,因此他的__proto__屬性值爲null

ARR1(數組的實例對象)的整個原型鏈:

  • 先找私有的,私有的沒有
  • 基於__proto__找到所屬類的原型Array.prototype;若是尚未
  • 基於Array.prototype__proto__找到Object.prototype ; 若是尚未
  • 則就是沒有了

這一系列就是咱們的原型鏈查找;

原型鏈查找機制:基於實例的__proto__找所屬類的prototype

  • => 實例的私有屬性方法
  • => 實例的公共屬性和方法

基於這種查找機制,幫助咱們實現了實例既有私有的屬性和方法,也有公有的屬性和方法了

這也是整個面向對象的核心。

咱們能夠打開控制檯,輸出dir[10,20]):

經過控制檯的輸出結果,能夠證實咱們上面畫的圖是沒有問題的

  • 當咱們使用arr1.hasOwnProperty
    • 其實是找到=> arr1.__proto__.__proto__ => Object.prototype
  • arr1.hasOwnProperty("push") => false
  • Array.prototype.hasOwnProperty("push") => true

pusharr1實例的公有方法;可是是Array.prototype的私有屬性

hasOwnPropertyarr1實例的「公有屬性方法」

  • 對象的私有屬性:存在本身的堆中,無需基於__proto__查找就有的
  • 對象的公有屬性:本身堆中沒有,須要基於__proto__prototype上的

原型鏈的圖到這裏就已經所有畫完了😄

JS中的全部值,最後基於__proto__原型鏈,都能找到Object.prototype原型,也就是都是對象類的實例,也就是都是對象,這就是「萬物接對象」

4、一道例題

老規矩,咱們在來一道題

function Fn() {
	this.x = 100;
	this.y = 200;
	this.getX = function () {
		console.log(this.x);
	}
}
Fn.prototype.getX = function () {
	console.log(this.x);
};
Fn.prototype.getY = function () {
	console.log(this.y);
};
let f1 = new Fn;
let f2 = new Fn;
console.log(f1.getX === f2.getX);
console.log(f1.getY === f2.getY);
console.log(f1.__proto__.getY === Fn.prototype.getY);
console.log(f1.__proto__.getX === f2.getX);
console.log(f1.getX === Fn.prototype.getX);
console.log(f1.constructor);
console.log(Fn.prototype.__proto__.constructor);
f1.getX();
f1.__proto__.getX();
f2.getY();
Fn.prototype.getY();
複製代碼

作原型類的題目,沒有比畫圖更好的方式了

圖畫完了,咱們根據圖直接寫答案就能夠啦😄

console.log(f1.getX === f2.getX); //=> false
console.log(f1.getY === f2.getY); //=> true
console.log(f1.__proto__.getY === Fn.prototype.getY); //=> true
console.log(f1.__proto__.getX === f2.getX); //=> false
console.log(f1.getX === Fn.prototype.getX); //=> false
console.log(f1.constructor); //=> Fn 實例的構造函數通常指的就是它所屬的類
console.log(Fn.prototype.__proto__.constructor); //=> Object
f1.getX() ;  
   執行的是私有的getX => function () {console.log(this.x);}
   方法中的this => f1
   代碼執行  
   console.log(this.x);  => f1.x  => 100

f1.__proto__.getX() ;
   執行的是原型上公有的getX  => function () {console.log(this.x);};
   方法中的this => f1.__proto__
   代碼執行
   console.log(this.x);  => f1.__proto__.x => undefined

f2.getY() ;
    執行的是原型上公有的getY => function () {console.log(this.y);};
    方法中的this =>  f2
    代碼執行
    console.log(this.y)  => f2.y =>200

Fn.prototype.getY() ;
    執行的是原型上的getY =>  function () {console.log(this.y);};
    方法中的this =>  Fn.prototype
    代碼執行
    console.log(this.y)  => Fn.prototype.y => undefined
複製代碼

5、給類的原型上擴展屬性或方法

(供其實例調取使用的公有屬性和方法)

一、Fn.prototype.xxx = xxx(經常使用)

  • 向默認開闢的堆內存中增長屬性方法
  • 缺點:若是須要設置不少屬性方法,操做起來比較的麻煩(小技巧,給Fn.prototype設置別名)
  • 這類方式的特色都是默認開闢的堆中擴展屬性方法,默認開闢的堆內存中存在constructor這個屬性
let prop = Fn.prototype;
    prop.A = 100;
    prop.B = 200;
    prop.C = 300;
複製代碼

二、Object.prototype.xxx = xxx(不經常使用)

  • 內置類原型上擴展方法

三、f1.__proto__.xxx = xxx(基本不用)

  • 這樣也能夠,由於基於實例的__proto__找到的就是所屬類的原型,也至關於給原型上擴展屬性方法
  • 缺點:只不過這種方式咱們基本不用,由於IE瀏覽器中,爲了防止原型鏈的惡意篡改,是禁止咱們本身操做__proto__屬性的(IE中不讓用__proto__)

四、原型重定向Fn.prototype = {...}(經常使用)

  • 咱們本身手動開闢一個堆內存賦給Fn.prototype
  • 缺點1:本身開闢的堆內存中是沒有constructor這個屬性的;因此真實項目中,爲了保護結構的嚴謹性,咱們須要本身手動設置constructor
  • 缺點2:若是在重定向以前,咱們向默認開闢的原型堆內存中設置了一些屬性方法,重定向後,以前設置的屬性方法都丟失了(沒用了)
  • 解決辦法:利用合併對象Object.assign(原來對象,新對象)
    • 合併過程當中有衝突的狀況以新的爲主,剩餘的不衝突的都合併在一塊兒
    • 返回一個合併後的新對象 關於重定向,咱們在看一道題👇
function Fn(num) {
    this.x = this.y = num;
}
Fn.prototype = {
    x: 20,
    sum: function () {
        console.log(this.x + this.y);
    }
};
let f = new Fn(10);
console.log(f.sum === Fn.prototype.sum); //true
f.sum();//20
Fn.prototype.sum();//NaN
console.log(f.constructor);//Object
複製代碼

6、給內置類的原型上擴展屬性和方法

JS中有不少內置類,並且在內置類的原型上有不少內置的屬性和方法,雖然內置類的原型上有不少的方法,可是不必定徹底夠項目開發所用,因此真實項目中,須要咱們本身向內置類原型擴展方法,來實現更多的功能操做

一、Array.prototype.xxx = xxx(以數組爲例)

  • 缺點:這種方法存在風險,咱們本身設置的屬性名可能會把內置的屬性給覆蓋掉

因此通常咱們本身在內置類原型上擴展的方法,設置的屬性名作好加上前綴

瀏覽器爲了保護內置類原型上的方法,不容許咱們從新定向內置類原型的指向(嚴格模式下會報錯)

二、練習實例

需求1:模擬內置的PUSH方法

  • 在類的原型上編寫的方法,讓方法執行,咱們通常都這樣操做:實例.方法(),因此方法中的THIS通常都是咱們要操做的這個實例,咱們基於THIS操做就是操做這個實例
  • 實現思路
/* * JS中有不少內置類,並且在內置類的原型上有不少內置的屬性和方法 * Array.prototype:數組做爲Array的實例,就能夠調取原型上的公共屬性方法,完成數組的相關操做 => arr.push():arr基於__proto__原型鏈的查找機制,找到Array.prototype上的push方法,而後把push方法執行,push方法執行 * + 方法中的THIS是要操做的arr這個數組實例 * + 做用是向arr(也就是this)的末尾追加新的值 * + 返回結果是新增後數組的長度 * * 向內置類原型擴展方法: * Array.prototype.xxx = xxx * =>這種方法存在風險:咱們本身設置的屬性名可能會把內置的屬性給覆蓋掉 * =>通常咱們本身在內置類原型上擴展的方法,設置的屬性名最好加上前綴 * * Array.prototype={...} * =>瀏覽器爲了保護內置類原型上的方法,不容許咱們從新定向內置類原型的指向(嚴格模式下會報錯) */

Array.prototype.myPush = function () {
	console.log('本身的PUSH');
};
let arr = [10, 20];
arr.myPush(100); 
複製代碼
  • 實現代碼:
Array.prototype.myPush = function myPush(value) {
	// this:要操做的數組arr實例
	this[this.length] = value;
	return this.length;
};
let arr = [10, 20];
console.log(arr.myPush(100), arr);

let arr2 = [];
console.log(arr2.myPush('小芝麻'), arr2);
複製代碼

需求2:數組的原型上有SORT實現數組排序的方法,可是沒有實現數組去重的方法,咱們接下來向內置類原型擴展方法:myUnique,之後arr.myUnique執行能夠把數組去重

Array.prototype.myUnique = function myUnique() {
	// this:當前要操做的數組實例
	let obj = {};
	for (let i = 0; i < this.length; i++) {
		let item = this[i];
		if (typeof obj[item] !== "undefined") {
			this[i] = this[this.length - 1];
			this.length--;
			i--;
			continue;
		}
		obj[item] = item;
	}
	obj = null;

	//爲了實現鏈式寫法
	return this;
};

let arr = [12, 23, 13, 23, 12, 12, 2, 3, 1, 2, 3, 2, 1, 2, 3];
arr.myUnique().sort((a, b) => a - b);
console.log(arr);
複製代碼

三、鏈式寫法:執行完上一個方法,緊接着調用下一個方法執行

  • arr之因此能調用myUnique或者sort等數組原型上的方法,是由於arrArray的實例,
  • 因此鏈式寫法的實現思路很簡單:只須要讓上一個方法執行的返回結果依然是當前類的實例,這樣就能夠當即接着調用類原型上的其它方法了
arr.myUnique().sort((a, b) => a - b).map(item => {
	return '@@' + item;
}).push('小芝麻').shift();
//Uncaught TypeError: arr.myUnique(...).sort(...).map(...).push(...).shift is not a function 
//=> 由於push返回的是新增後數組的長度,是個數字,再也不是數組了,就不能繼續調用數組的方法了
複製代碼

7、關於構造函數中的相關this問題

基於new執行,構造函數-函數體中的this是當前類的一個實例

給實例擴展的私有或者公有方法,這些方法中的this徹底看前面是否有「點」來決定

仍是上面的例題

相關文章
相關標籤/搜索