在 ES2015/ES6 中引入了 class 關鍵字,但那只是語法糖,JavaScript 仍然是基於原型的, 所以本文未用 類 實現 構造函數 和 原型
目錄
- 原型
- example
- 繼承的優缺點
原型的概念:
- 所有對象都有一個屬性
__proto__
指向一個對象, 也就是原型 - 每個對象的原型都可以通過
constructor
找到構造函數,構造函數也可以通過prototype
找到原型 - 所有函數都可以通過
__proto__
找到Function
對象 - 所有對象都可以通過
__proto__
找到Object
對象 - 對象之間通過
__proto__
連接起來, 這樣稱之爲原型鏈。當前對象上不存在的屬性可以通過原型鏈一層層往上查找,直至頂層Object
對象。 - 如果還沒有 則爲
null
。根據定義,null
沒有原型。所以null
爲原型鏈的最後一個環節。
儘管這種原型繼承通常被認爲是 JavaScript 的弱點之一,但是原型繼承模型本身實際上比經典模型更強大。例如,在原型模型的基礎上構建經典模型相當簡單。
實原型的最重要的概念就是這些了,如果看很多大篇幅介紹原型的文章,會很容易迷糊。
example 常見的兩種繼承
1.引用類型的屬性被所有實例共享。
function Parent() {
this.names= ['gavin', 'kevin']
}
function Child() {
}
Child.prototype = new Parent()
var Child1 = new Child()
Child1.names.push('yayu')
console.log(Child1.names) //output: gavin、 kevin、 yayu
var Child2 = new Child()
console.log(Child2.names) //output: gavin、 kevin、 yayu
2.通過 new
操作符 (經典繼承) 子對象可以向父對象傳遞參數
let F = function (a, b) {
this.a = a;
this.b = b;
}
F.prototype.getVal = function () {
console.log(this.a, this.b)
}
let o = new F(1, 2);
console.log(o.a, o.b) //output: 1, 2
3.通過 Object.create
繼承一個對象
var o = {
a: 2,
m: function(){
return this.a + 1;
}
};
console.log(o.m()); // 3
// 當調用 o.m 時,'this' 指向了 o.
var p = Object.create(o);
// p是一個繼承自 o 的對象
p.a = 4; // 創建 p 的自身屬性 'a'
console.log(p.m()); // 5
// 調用 p.m 時,'this' 指向了 p
// 又因爲 p 繼承了 o 的 m 函數
// 所以,此時的 'this.a' 即 p.a,就是 p 的自身屬性 'a'
繼承的優缺點
優點:
- 原型中的方法,子對象都可以訪問。
問題:
- 1、多個實例對引用類型的操作會被篡改
- 2、子類型的原型上的 constructor 屬性被重寫了
- 3、給子類型原型添加屬性和方法必須在替換原型之後
問題一
原型鏈繼承方案中,原型實際上會變成另一個類型的實例,如下代碼,Cat.prototype
變成了 Animal
的一個實例,所以 Animal
的實例屬性 names
就變成了 Cat.prototype
的屬性。
而原型屬性上的引用類型值會被所有實例共享,所以多個實例對引用類型的操作會被篡改。如下代碼,改變了 instance1.names
後影響了 instance2
。
function Animal(){
this.names = ["cat", "dog"];
}
function Cat(){}
Cat.prototype = new Animal();
var instance1 = new Cat();
instance1.names.push("tiger");
console.log(instance1.names); // ["cat", "dog", "tiger"]
var instance2 = new Cat();
console.log(instance2.names); // ["cat", "dog", "tiger"]
問題二、
子類型原型上的 constructor 屬性被重寫了,執行 Cat.prototype = new Animal()
後原型被覆蓋, Cat.prototype
上丟失了 constructor
屬性,Cat.prototype
指向了 Animal.prototype
,而Animal.prototype.constructor
指向了Animal
,所以Cat.prototype.constructor
指向了 Animal
// 新增,重寫 Cat.prototype 的 constructor 屬性,指向自己的構造函數 Cat
Cat.prototype.constructor = Cat;
我們可以看到,雖然 已經有 value 屬性,但是 constructor
現在是指向 Cat
了
問題三、
給子類型原型添加屬性和方法必須在替換原型之後,因爲子類型的原型會被覆蓋。
// 木易楊
function Animal() {
this.value = 'animal';
}
Animal.prototype.run = function() {
return this.value + ' is runing';
}
function Cat() {}
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
// 新增 在 constructor 指向 cat 之後
Cat.prototype.getValue = function() {
return this.value;
}
var instance = new Cat();
instance.value = 'cat';
console.log(instance.getValue()); // cat
哦 對了,既然說是順着原型鏈往上找,那麼自己的方法是優先使用的。所以即使原型頂層有相同的方法,也不會用。所以引入了 繼承的第三個概念 多態
多態 即 繼承而來的方法 不好用, 那就不用你的, 我自己定義一個。
小結
- 每個對象都擁有一個原型對象,通過
__proto__
指針指向一個原型,並從中 繼承方法和屬性,同時原型對象也可能擁有原型, 這樣一層一層, 最終指向null
,這種關係被稱爲 原型鏈 - 當訪問一個對象的屬性 || 方法 時,它會以自身爲起點,向上遍歷整條 原型鏈 知道找到 匹配的屬性 || 方法 / 到達原型鏈的尾端
null
。 - 原型鏈的構建依賴於
__proto__
,一層一層最終鏈接到null
。 - instanceof 原理就是一層一層查找
__proto__
,如果和constructor.prototype
相同則返回true
,如果一直沒有查找成功則返回false
。 - 原型鏈接繼承的本質是 重寫原型對象,代之以一個新類型的實例
參考
掘金 木易楊說: https://juejin.im/post/5ca9cebb6fb9a05e505c5f81
掘進 yck: https://juejin.im/post/5c99cb69e51d4556a91f87af
web docs: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Inheritance_and_the_prototype_chain