進擊的 JavaScript 之(七) 原型鏈

周大俠啊發表於2018-05-14

原文連結:周大俠啊 進擊的 JavaScript (七) 之 原型鏈

算是記錄一下自己的學習心得吧,哈哈

首先說一下,函式建立的相關知識

在JavaScript中,我們建立一個函式A(就是宣告一個函式), 那麼 js引擎 就會用建構函式Function來建立這個函式。所以,所有的函式constructor屬性都指向 建構函式FunctionA.constructor === Function; //true(函式本身並沒有這個屬性,後面介紹。記住,這裡是函式,重點,要考,哈哈) 然後會在記憶體中建立一個原型物件B原型物件這個東西,看不見,摸不著,只能通過函式的 prototype 來獲取這個物件。( 即:prototype的屬性的值是這個物件 )。

再說一下,物件建立的相關知識

物件的建立,有三種方式 1、字面量方式(又稱直接量)

啥是字面量呢?可以理解為,沒用new出來的都屬於字面量建立。

舉個例子:

var c = {};     //c 是變數, {} 就是字面量了
複製程式碼

字面量物件的建立過程跟函式的過程相似,當你宣告一個物件字面量時,js引擎就會建構函式 Object 來建立這個物件,所以 效果等同 new Object()

2、構造器方式

這個呢,就是用 new 建立。

栗子在此:

var c = new Object();
複製程式碼

3、Object.create

這是個ES5中新增的方法,這個方法是建立一個新物件,把它的__proto__指向傳進來的函式或者物件。到底是怎麼回事呢,下面,我們們來簡單實現下它的功能

function create(proto, options){     //proto表示傳入的函式或者物件,options表示給子類新增的屬性
    var obj = {};
    obj.__proto__ = proto;
    if(options == null){
        return obj;
    }
    return Object.defineProperties(obj,options);    //Object.defineProperties方法直接在一個物件上定義新的屬性或修改現有屬性,並返回該物件。所以這裡就直接return了
    }
複製程式碼

檢驗一下:

var a = function(){};

//自制的create
var b = create(a);
console.log(b.__proto__ === a);   //true;
var c = create(a.prototype);
console.log(c.__proto__ === a.prototype);   //true

//Object.create
var d = create(a);
console.log(d.__proto__ === a);   //true;
var e = create(a.prototype);
console.log(e.__proto__ === a.prototype);   //true
複製程式碼

這裡說明一下,你可以在別處看到的create的實現是這樣的:

//這裡就簡化第二個引數了。就寫第一個引數的實現
function create(proto){
    function f(){};
    f.prototype = proto;
    return new f();
}
複製程式碼

這個是相容的寫法,因為,__proto__ 屬性是非標準的,部分現在瀏覽器實現了該屬性。其實,你要是明白,new到底幹了啥, 你就明白,這兩個實際是一個東西了。

  1. 建立一個新的物件(即例項物件)
  2. 把新物件的__proto__ 指向 new 後面建構函式 的原型物件。
  3. 把建構函式中的 this 指向 例項物件。
  4. 做一些處理,然後返回該例項物件。

下面,手寫一個方法 實現new 的功能:

function New(func){    //func 指傳進來的建構函式,關於建構函式,我個人理解來就是用來生成一個物件例項的函式。

    var obj = {};   //這裡就直接新建一個空物件了,不用new Object了,效果一樣的,因為我感覺這裡講實現new的功能   再用 new 就不太好。
    
    if(func.prototype != null){
        obj.__proto__ = func.prototype;
    }
  
    func.apply(obj,Array.prototype.slice.call(arguments, 1));
    //把func建構函式裡的this 指向obj物件,把傳進來的第二個引數開始,放入func建構函式裡執行,比如:屬性賦值。

    return obj;
    
}
複製程式碼

不知各位看客是否看明白了呢,簡單指明一下把

function create(proto){
    var f = function(){};
    f.prototype = proto;
    return new f();

//就等於
function create(proto){
    var f = function(){};
    var obj = {};
    f.prototype = proto;
    obj.__proto__ = f.prototype;
    return obj;
}

//就等於
function create(proto){
    var obj = {};
    obj.__proto__ = proto;
    return obj;
}

//看明白了嗎, 就是用 function f 做了一下過渡。

//驗證
function a(){};
var b = create(a);
console.log(b.__proto__ === a);     //true;
複製程式碼

原型鏈開始!

我想,一說到JavaScript的原型是令人奔潰的,其中prototype容易和__proto__兩者的聯絡就太頭疼了,反正看圖比看字舒服。

這裡寫圖片描述
我看大多數的教程都是把prototype__proto__放一起講,我覺得還是分開將比較好,本來就不是一個東西


一、prototype屬性

這是個函式才有的屬性,它儲存著對其原型對像的引用,即指向原型物件。(任何函式都有原型物件。)它的原型物件是看不見,摸不著的,只能通過函式的prototype屬性來獲取它。

比如

function A(){};
A.prototype;     //這樣就獲取到了A的原型物件
複製程式碼

總結:你看到prototype,你就想著它對應著,它的原型物件就行。



二、原型物件

每當你建立一個函式,js 就會 對應的生成一個原型物件。它只能被函式的prototype屬性獲取到。(A.prototype 整體變現為A的原型物件)


通過上面知識,我們知道了,物件無非就兩種,例項物件(new 和 字面量建立的物件),和 原型物件。


三、_ _ proto _ _屬性

這個是非標準的屬性,現代部分瀏覽器支援,比如,火狐,谷歌。對應的標準屬性是[[prototype]],這是個隱藏屬性,獲取不到。 這個屬性就是把所有的函式啊,物件啊,連成一條鏈的東西,我們稱為原型鏈。這條鏈的終點是Object.prototype.__proto__ === null

那它到底指向誰呢,我給了兩種記憶方式吧:

1、函式的__proto__ 指向Function.protoype,原型物件的__proto__指向Object.prototype的。字面量new出來的例項物件 ,指向其建構函式(誰 new 出來的)的prototypeObject.create 建立的物件呢,就是上面說的,你給它誰,它就指向誰。
2、除了Object.create建立的物件的__proto__指向你給定的,原型物件的__proto__指向Object.prototype,其他的__proto__ 都是指向其建構函式的原型物件。(你要是看懂了上面的new的實現,就應該明白為啥了。 )

第二點注意:所有函式都是 建構函式 Function 建立出來的,所以,函式的建構函式 就是 Function

選這兩個中你喜歡的一個,對著下面的圖找答案:

原型鏈


四、constructor屬性

constructor 屬性是原型物件獨有的,它指向的就是它的建構函式。上面的一、prototype中說,函式的prototype屬性指向它的原型物件。此時的函式是建構函式了。所以函式 和 它的原型對像 以這兩個 屬性 保持著 相互聯絡。

constructor
比如:

function A(){};
A.prototype.constructor === A   //true

Object.prototype.constructor === Object   //true
複製程式碼

那函式的constructor 和 普通物件的 constructor 是怎麼回事呢?

開頭說的function A(){};A.constructor 不知道大家有沒有點疑惑?不是原型物件才有的嗎?

其實, 他們就是在一條原型鏈上的,也就是說,A 上沒有 constructor 屬性,它就會沿著原型鏈向上查詢,到了 Object的原型物件上,就找到了constructor 屬性,於是就可以用 A.constructor 了.

比如:

var Person = function(name) {
    this.name = name;
}

Person.prototype.getName = function(){
    return this.name;
}

var p = new Person("666");

console.log(p.getName());    //666
複製程式碼

看這裡Person 建構函式裡是不是沒有getName 方法,但是 p 例項怎麼可以用呢? 因為p例項 的__proto__指向 的原型物件上有 getName 函式,所以 p 向原型鏈上查詢到了 Person 的原型物件上, 它有getName 方法, 於是, p 就可以使用這個方法了。

這裡寫圖片描述

注意:所以,在找建構函式時,需要注意是在它的原型鏈上找,而不是原型物件上:

Object.constructor === Object.__proto__.constructor
//true
複製程式碼

五、建構函式

啥是建構函式?其實每個函式都是建構函式,只是我們一般把 生成例項物件 的函式 稱為建構函式(通過new ,new 後面的就是建構函式),本質是函式

比如:

var Person = function(name) {
    this.name = name;
    this.getName = function(){
        return this.name;
    }
}

var p1 = new Person("zdx");

console.log(p1.getName());     //zdx

var p2 = new Person("666");

console.log(p2.getName())     //666

console.log(Person.prototype.constructor === Person)    ///true

//這裡的Person 是個函式對吧,然後外面用  new Person();
//建立了一個Person 的例項,此時,Person 就是一個建構函式了,也稱它為類,我們把類的首字母都大寫。
//因為,這個函式,使用 new 可以構造無數個例項來。
複製程式碼

說一下,怕有人不知道,例項就是例項物件,因為一個例項,它本身就是物件。


六、原型鏈

開始放大招了,哈哈:

function Person(name) {
    this.name = name;
}

var p = new Person("zdx");

console.log(p.__proto__ === Person.prototype);   //true
//p是例項物件, 它的建構函式是 Person,按照上面的所說的,它是new 出來的,所以指向它建構函式的prototype

console.log(Person.prototype.__proto__ === Object.prototype);   //true
//Person.prototype 是原型物件, 所以指向 Object.prototype.

console.log(Person.__proto__ === Function.prototype);   //true
//Person 是建構函式, 它的建構函式是Function, 所以它就指向 Function.prototype

console.log(Function.prototype.__proto__ === Object.prototype);   //true
//Function.prototype 是原型物件, 所以指向 Object.prototype.

console.log(Object.prototype.__proto__ === null);   //true   
//這裡就是所有原型鏈的終端,原型鏈到這裡就沒了。
複製程式碼

所以說js 萬物皆物件呢? 所有的函式啊,物件啊,例項啊,它的原型鏈最終都到了Object.prototype原型物件

畫成圖就是醬樣子滴:

原型鏈
其實吧,有時候看圖也不一樣好,哈哈,你可以按照我說的規則,自己不看圖畫一下。

簡單來驗證一下

var a = {};   //等同與 new Object()
console.log(a.prototype);  //undefined   物件沒有原型物件
console.log(a.__proto__ === Object.prototype);  //true

var b = function(){};
console.log(b.prototype);  //b的原型對像
console.log(b.__proto__ === Function.prototype);

var c = [];    //等同於 new Array(),建構函式是Array
console.log(c.__proto__ === Array.prototype);    //true

var d = "";    //等同於 new String(),建構函式是 String
console.log(d.__proto__ === String.prototype);    //true
複製程式碼

七、原型鏈的作用

其實,原型鏈的根本作用就是為了 屬性 的讀取。

上面簡單說過,當在一個 函式 或者 物件 上 讀取屬性時,它會先查詢自身屬性,如果有,就直接返回,如果沒有呢,就會沿著原型鏈向上查詢。

舉個簡單的栗子,每個函式啊,物件啊,都有toString 方法,它是哪來的呢? 球都麻袋!(等等),不是屬性的讀取嗎。toString這個是方法(函式)呀!同學,你很有眼光嘛。事實上,我們把屬性值為函式的,稱之為 方法。其實呢,你要了解這些方法是怎麼回事。

function test(){};

test.toString();    //"function test(){}"

console.log(test.hasOwnProperty("toString"));   //false

console.log(test.__proto__.hasOwnProperty("toString"));   //true
//這就找到了,這裡的hasOwnProperty 方法 是檢查 該屬性是否 是自身的屬性

//而 (函式的__proto__)test.__proto__  都指向(等於) Function.prototype
console.log(test.__proto__ === Function.prototype);   //true

console.log(Function.prototype.hasOwnProperty("toString"));   //true
//看到這裡,明白了嗎。函式的內建方法(函式) 一部分是 Function.prototype 上的屬性;
//一部分? 是的,因為,原型鏈的終端 在  Object.prototype ;
//所以,在Object.prototype 上新增的屬性,方法, 函式也是可以使用的;

//比如:
Object.prototype.say = function() { console.log(5666) };

function test(){};

test.say();   //5666
複製程式碼

這裡寫圖片描述

那麼屬性的賦值是怎麼一回事呢?

首先函式 或 物件 會查詢 自身屬性, 如果有,就會 覆蓋該屬性 的值, 如果沒有,它就會建立 一個 自身的屬性,總之,賦值是不會對原型鏈進行查詢

function Person(){};
var p = new Person();
//如果你用的同一個視窗執行這個,結果可能是上面的 5666,因為剛剛更改了該函式,您重新開啟個瀏覽器視窗即可。
p.toString();    //"[object Object]"  

//有疑惑嗎,其實,toString 這個方法, Function.prototype, 和 Object.prototype 都有;
Function.prototype.toString = function(){
    console.log("我是Function原型物件的");
}
Person.toString();    //我是Function原型物件的
p.toString();       //"[object Object]"
//Object上的toString 方法並沒有改變。
複製程式碼

八、實際程式碼中 原型鏈 的運用

運用 最常見的就是 繼承了。

function Person(name){   //父類(建構函式)
	this.name = name;
}
Person.prototype.getName = function(){   //在父類的原型物件上新增方法
	return this.name;
}

function Son(name,age){    //子類(建構函式)
    this.name = name;    //為了減輕讀者壓力,就不使用 call 了, Person.call(this, name);
	this.age = age;
}

Son.prototype = new Person();    //把子類(建構函式) 的原型物件  掛到 原型鏈上去。

Son.prototype.getAge = function(){
	return this.age;
}
var s = new Son("zdx",666);
s.getName();    //Son.prototype 上沒有 getName方法,現在能使用了,就完成了 繼承。
複製程式碼

需要解讀一下嘛?

Son.prototype = new Person();

//就等於
var obj = {};
obj.__proto__ = Person.prototype;
Son.prototype = obj;
//這樣,就把Son.prototype 掛到原型鏈上去了。
Son.prototype.__proto__ === Person.prototype  //ture
複製程式碼

然後Son 的例項物件 上使用方法 時,就沿著鏈查詢, Son.prototype 沒有, 上級Person.prototype 上有。ok,繼承了。

繼承


九、解讀jq 的原型鏈

簡單寫一個:

var jQuery = function(name) {
    return new jQuery.fn.init();
}

jQuery.fn = jQuery.prototype = {
    constructor: jQuery,
    init: function(name) {
        this.name = name;
    },
    each: function() {
        console.log('each');
        return this;
    }
}

jQuery.fn.init.prototype = jQuery.fn;

複製程式碼

估計看起來有點困難,沒事,我們簡化一下

function jQuery(name){
	return new init(name);
}

function init(name){
	this.name = name;
}

jQuery.prototype = {
    constructor: jQuery,
	each: function(){ 
	    console.log('each')
	}
}

init.prototype = jQuery.prototype;

複製程式碼

看懂了嗎,你使用jQuery(), 就相當於與 new jQuery(); 其實 你 new init() 和 new jQuery(); 是一樣的;因為 init.prototype = jQuery.prototype ,所以它們的例項物件是一樣的。它這樣寫,就是為了你使用方便,不需要你使用 new 來建立jq物件。

使用起來就是這樣的:

$();
//是不是比

new $();
//方便多了。
複製程式碼

這裡寫圖片描述

最後要說的就是,別把原型鏈和作用域鏈搞混了!!!哈哈哈

相關文章