原型鏈其實是JavaScript中的實現繼承的機制,在搞懂原型鏈以前首先要搞懂幾個概念:this,普通對象和函數對象,構造函數,new編程
this對於不少人來講是混雜不清的概念,可是想要弄清楚原型鏈咱們必須瞭解什麼是this數組
首先,this只能在存在與函數中函數
其次,this實際上是當前函數所在上下文環境,再簡單一點也能夠理解爲this返回一個當前函數所在的對象,也就是說想要知道this是什麼咱們只須要關注是誰調用了this所在的函數就能夠了學習
以下邊的代碼zhao.sayName()是zhao調用的sayName函數因此sayName中的this天然指的就是zhao這個對象,而下方 var liName = zhao.sayName語句是將sayName這個函數賦值給liName,調用liName就至關於在最頂層也就是window下直接調用sayName,this的指向天然就是window這個最頂層對象this
var name = "Li" var zhao = { name: "Zhao", sayName: function () { console.log(this.name); } } zhao.sayName() // Zhao var liName = zhao.sayName; liName() // Li
JavaScript中一切均可以看做對象,可是實際上對象也是有區別的,對象分爲普通對象和函數對象spa
// 普通對象 var o1 = {} var o2 = new Object() var o3 = new f1() // 函數對象 function f1(){} var f2 = function(){} var f3 = new Function() console.log(typeof f1); //function console.log(f1.prototype); //true console.log(typeof f2); //function console.log(f2.prototype); //true console.log(typeof f3); //function console.log(f3.prototype); //true console.log(typeof o1); //object console.log(o1.prototype); //undefined console.log(typeof o2); //object console.log(o2.prototype); //undefined console.log(typeof o3); //object console.log(o3.prototype); //undefined
凡是經過function構建的對象都是函數對象,而且只有函數對象纔有prototype屬性,普通對象沒有prototype
prototype又是什麼呢?設計
當咱們建立函數的時候,編譯器會自動爲該函數建立一個prototype屬性,這和屬性指向一個包含constructor屬性的對象,而這個屬性又默認指回原函數,讀起來有點繞對吧,大概是這樣的指針
function Person() { // prototype = { // constructor: Person, // } }
每一個函數對象都有一個prototype(原型)屬性,在我看來prototype屬性的意義:code
這兩點等學習了下邊new命令你就會明白了
函數對象的一種用法就是構造函數,經過構造函數能夠構建一個函數對象的實例(普通對象)
function Person(name, age ){ this.name = name; this.age = age; this.sayHello = function(){ console.log(`Hello! my name is ${this.name}`); }; } var person1 = new Person("kidder", 28); person1.sayHello(); // Hello! my name is kidder console.log(person1.constructor); //[Function:Person]
按照慣例,構造函數的命名以大寫字母開頭,非構造函數以小寫字母開頭,經過構造函數構造的普通對象都會有一個constructor(構造函數)屬性,該屬性指向構造該對象的構造函數
下面咱們來看看構造函數構建一個普通對象的時候發生了什麼
var Person = function (name) { this.name = name; this.age = 18; }; Person.prototype.sayHello = function(){ console.log(`Hello! my name is ${this.name}`); }; var li = new Person("Li"); console.log(li.name); // Li console.log(li.age); // 18 li.sayHello(); // Hello! my name is Li
{}
{ __proto__:Person.prototype; }
this = { __proto__:Person.prototype; }
執行構造函數內部的代碼
this = { __proto__:Person.prototype; name: "Li"; age: 18; }
因此li這個對象中只有name和age兩個屬性,爲何li.sayHello()會輸出Hello! my name is Li呢?
這就是原型鏈,當給定的屬性在當前對象中找不到的狀況下,會沿着__proto__這個屬性一直向對象的上游去尋找,直到__proto__這個屬性指向null爲止,若是找到指定屬性,查找就會被截斷,中止
上面這張圖是整個JavaScript的原型鏈體系,爲了讓這張圖更直觀因此我將構造函數的prototype屬性單獨提了出來,恩,其實畫在構造函數內部也可,但同時由於對象是引用類型,因此這樣畫也沒毛病吧
這兩個屬性常常會被咱們混淆,那麼咱們回過頭再來總結一下
prototype:只有函數對象才具備的屬性,它用來存放的是構造函數但願構建的實例具備的共享的屬性和方法,主要用於構造函數的實例化
_proto_ : 全部對象都具備的屬性,它指向的是當前對象在原型鏈上的上級對象,主要做用是讓編譯器在由__proto__這個屬性構成的原型鏈上查找特定的屬性和方法
var Person = function (name) { this.name = name; this.age = 18; }; Person.prototype.sayHello = function(){ console.log(`Hello! my name is ${this.name}`); }; var li = new Person("Li"); var Person1 = function () { }; Person.prototype.name = "Li" Person.prototype.age = 18 Person.prototype.sayHello = function(){ console.log(`Hello! my name is ${this.name}`); }; var Li = new Person1();
關於Person和Person1兩種構造函數的寫法有什麼不一樣呢?
通常來講寫在prototype原型對象中的屬性和方法都是公用的,也就是說寫在構造函數中的屬性在構建普通對象的時候,都會在新對象中從新定義,也就是從內存的角度來講又會多佔用一些內存空間,因此咱們將構造函數的全部屬性和方法都寫在prototype原型中很差嗎?
可是原型函數也是有缺點的:
var Person = function () { }; Person.prototype.name = "Li" Person.prototype.age = 18 Person.prototype.sayHello = function(){ console.log(`Hello! my name is ${this.name}`); }; var li = new Person(); var zhao = new Person();
這種方式構造的全部對象都是一個模板,雖然咱們也能夠在當前對象下進行修改,但這樣一點也不優雅,不規整,並且從某種意義上來講也是對內存的浪費
var Person = function () { }; Person.prototype.name = "Li" Person.prototype.age = 18 Person.prototype.friends = ["ZhangSan", "LiSi"] Person.prototype.sayHello = function(){ console.log(`Hello! my name is ${this.name}`); }; var li = new Person(); var zhao = new Person(); li.friends.push("WangWu"); console.log(zhao.friends); // [ 'ZhangSan', 'LiSi', 'WangWu' ]
在JavaScript中,基本類型的修改能夠明確的經過建立或修改在當前對象下的屬性對原型鏈進行截斷,可是像數組,對象這種引用類型的值雖然也能夠經過在當前對象中建立該屬性來對原型鏈進行截斷,可是一不注意就可能會出現上面這種狀況直接對原型進行了修改
因此,用構造函數來定義實例屬性,用原型定義方法和共享的屬性,這樣寫就比較優雅了
function Person(name, age){ this.name = name; this.age = age; this.friends = ["ZhangSan", "LiSi"]; } Person.prototype.sayHello = function(){ console.log(`Hello! my name is ${this.name},${this.age}歲了`); }; var li = new Person("li", 18); var zhao = new Person("zhao", 16); li.sayHello(); // Hello! my name is li, 18歲了 zhao.sayHello(); // Hello! my name is zhao,16歲了 li.friends.push("WangWu"); console.log(zhao.friends); // [ 'ZhangSan', 'LiSi' ]
法一用構造函數構造一個新對象
var A = function () { }; var a = new A(); console.log(a.constructor); // [Function:A] console.log(a.__proto__ === A.prototype); //true
法二的本質來講和法一是同樣的,就是隱式調用原生構造函數Object來構造新對象
var a = {}; // var a = new Object(); console.log(a.constructor); // [Function:Object] console.log(a.__proto__ === Object.prototype); //true
法三Object.create是以一個普通對象爲模板建立一個新對象
var a1 = {a:1} var a2 = Object.create(a1); console.log(a2.constructor); // [Function:Object] console.log(a2.__proto__ === a1);// true console.log(a2.__proto__ === a1.prototype); //false
因此除了Object.create建立對象的方式,能夠說:__ proto __ === constructor.prototype;
前面咱們說道prototype的時候進行原型屬性的賦值的時候,採用的是逐項賦值,那麼當我直接將對象賦值給prototype屬性的時候會發生什麼呢?
function Person() { } Person.prototype = { name : "Li", age : 18, sayHello : function () { console.log(`Hello! my name is ${this.name},${this.age}歲了`); } }; var li = new Person(); console.log(li instanceof Object); // true console.log(li instanceof Person); // true console.log(li.constructor === Person); // false console.log(li.constructor === Object); // true console.log(Person.prototype.constructor); // Object
這時候咱們就發現咱們構建的li對象的constructor再也不指向它的構造函數Person,而是指向了Object,而且Person原型Person.prototype的constructor指向也指向了Object,這是什麼緣由呢?
其實,根源出如今Person.prototype上,上邊咱們提到過,其實咱們在寫構造函數的時候其實是這樣的
function Person() { // prototype = { // constructor : Person // } }
當咱們構建Person構造函數的時候,編譯器會自動生成一個帶有指向Person的constructor屬性的對象,並把這個對象賦值給Person.prototype,咱們又知道js中對象是引用類型,當咱們使用Person.prototype.name=...的時候其實是對這個對象的修改,而使用Person.prototype={...}其實是將這個屬性本來的指針指向了另外一個新建立的對象而不是原來編譯器自動建立的那個:
而li的constructor屬性天然是繼承自Person.prototype,因此constructor天然也就跟着改變了,若是在編程的過程當中constructor這個屬性很重要的話能夠經過下面的方式
function Person() { } Person.prototype = { constructor:Person name : "Li", age : 18, sayHello : function () { console.log(`Hello! my name is ${this.name},${this.age}歲了`); } }; var li = new Person(); console.log(li instanceof Object); // true console.log(li instanceof Person); // true console.log(li.constructor === Person); // true console.log(li.constructor === Object); // false console.log(Person.prototype.constructor); // Person
參考:《JavaScript高級程序設計》
這是我對JS原型鏈部分的總結與思考,也是我寫的第一篇這麼正式的技術文檔,若有紕漏之處,歡迎你們批評指正