原型鏈
原型鏈實際上是JavaScript中的實現繼承的機制,在搞懂原型鏈之前首先要搞懂幾個概念:this,普通物件和函式物件,建構函式,new
this
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這個最頂層物件
var name = "Li"
var zhao = {
name: "Zhao",
sayName: function () {
console.log(this.name);
}
}
zhao.sayName() // Zhao
var liName = zhao.sayName;
liName() // Li
複製程式碼
普通物件與函式物件
JavaScript中一切都可以看作物件,但是實際上物件也是有區別的,物件分為普通物件和函式物件
// 普通物件
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屬性的意義:
- 建立物件的模板
- 公開的共享空間
這兩點等學習了下邊new命令你就會明白了
constructor 建構函式
函式物件的一種用法就是建構函式,通過建構函式可以構建一個函式物件的例項(普通物件)
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(建構函式)屬性,該屬性指向構造該物件的建構函式
new命令
new命令的工作機制
-
建立一個空物件作為要返回物件的例項
-
將這個空物件的原型(__ proto __)指向建構函式的prototype屬性
-
將這個空物件賦值給建構函式內部的this
-
執行建構函式內部的程式碼
原型鏈
下面我們來看看建構函式構建一個普通物件的時候發生了什麼
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 __)指向建構函式的prototype屬性
{
__proto__:Person.prototype;
}
複製程式碼
- 將這個空物件賦值給建構函式內部的this
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屬性單獨提了出來,恩,其實畫在建構函式內部也可,但同時因為物件是引用型別,所以這樣畫也沒毛病吧
__ proto __ 和 prototype
這兩個屬性經常會被我們混淆,那麼我們回過頭再來總結一下
prototype:只有函式物件才具有的屬性,它用來存放的是建構函式希望構建的例項具有的共享的屬性和方法,主要用於建構函式的例項化
__ proto __ : 所有物件都具有的屬性,它指向的是當前物件在原型鏈上的上級物件,主要作用是讓編譯器在由__proto__這個屬性構成的原型鏈上查詢特定的屬性和方法
補充
prototype的共享屬性
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
複製程式碼
-
create方式
法三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;
constructor
前面我們說道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原型鏈部分的總結與思考,也是我寫的第一篇這麼正式的技術文件,如有紕漏之處,歡迎大家批評指正