Javascript實現繼承的底層原理是什麼?你瞭解幾種方式實現JavaScript的繼承呢?不同方式實現繼承的優缺點是什麼? ...
1. Javascript繼承
1.1 原型鏈繼承
function Parent() {
this.name = 'zhangsan';
this.children = ['A', 'B', 'C'];
}
Parent.prototype.getName = function() {
console.log(this.name);
}
function Child() {
}
Child.prototype = new Parent();
var child = new Child();
console.log(child.getName());
[!NOTE]
主要問題:
- 引用類型的屬性被所有實例共用(this.children.push('name'))
- 在創建Child的實例的時候,不能向Parent傳參
1.2 借用構造函數(經典繼承)
function Parent(age) {
this.names = ['zhangsan', 'lisi'];
this.age = age;
this.getName = function() {
return this.names;
}
this.getAge = function() {
return this.age;
}
}
function Child(age) {
Parent.call(this, age);
}
var child = new Child(18);
child.names.push('haha');
console.log(child.names);
var child2 = new Child(20);
child2.names.push('yaya');
console.log(child2.names);
[!NOTE]
優點:
- 避免了引用類型的屬性被所有實例共用
- 可以直接在Child中向Parent傳參
[!DANGER]
缺點:
- 方法都在構造函數中定義了,每次創建實例都會創建一遍方法
1.3 組合繼承(原型鏈繼承和經典繼承雙劍合璧)
/**
* 父類構造函數
* @param name
* @constructor
*/
function Parent(name) {
this.name = name;
this.colors = ['red', 'green', 'blue'];
}
Parent.prototype.getName = function() {
console.log(this.name);
}
// child
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
Child.prototype = new Parent();
// 校正child的構造函數
Child.prototype.constructor = Child;
// 創建實例
var child1 = new Child('zhangsan', 18);
child1.colors.push('orange');
console.log(child1.name, child1.age, child1.colors); // zhangsan 18 (4) ["red", "green", "blue", "orange"]
var child2 = new Child('lisi', 28);
console.log(child2.name, child2.age, child2.colors); // lisi 28 (3) ["red", "green", "blue"]
[!NOTE]
優點: 融合了原型鏈繼承和構造函數的優點,是Javascript中最常用的繼承模式
2. 多種方式實現繼承及優缺點總結
2.1 原型式繼承
function createObj(o) {
function F(){};
// 關鍵:將傳入的對象作為創建對象的原型
F.prototype = o;
return new F();
}
// test
var person = {
name: 'zhangsan',
friends: ['lisi', 'wangwu']
}
var person1 = createObj(person);
var person2 = createObj(person);
person1.name = 'wangdachui';
console.log(person1.name, person2.name); // wangdachui, zhangsan
person1.friends.push('songxiaobao');
console.log(person2.friends); // lisi wangwu songxiaobao
[!DANGER]
缺點:
- 對於引用類型的屬性值始終都會共用相應的值,和原型鏈繼承一樣
2.2 寄生式繼承
// 創建一個用於封裝繼承過程的函數,這個函數在內部以某種形式來增強對象
function createObj(o) {
var clone = Object.create(o);
clone.sayName = function() {
console.log('say HelloWorld');
}
return clone;
}
[!DANGER]
缺點:與借用構造函數模式一樣,每次創建對象都會創建一遍方法
2.3 寄生組合式繼承
2.3.1 基礎版本
function Parent(name) {
this.name = name;
this.colors = ['red', 'green', 'blue'];
}
Parent.prototype.getName = function() {
console.log(this, name);
}
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
// test1:
// 1. 設置子類實例的時候會調用父類的構造函數
Child.prototype = new Parent();
// 2. 創建子類實例的時候也會調用父類的構造函數
var child1 = new Child('zhangsan', 18); // Parent.call(this, name);
// 思考:如何減少父類構造函數的調用次數呢?
var F = function(){};
F.prototype = Parent.prototype;
Child.prototype = new F();
// 思考:下麵的這一句話可以嗎?
/* 分析:因為此時Child.prototype和Parent.prototype此時指向的是同一個對象,
因此部分數據相當於此時是共用的(引用)。
比如此時增加 Child.prototype.testProp = 1;
同時會影響 Parent.prototype 的屬性的。
如果不模擬,直接上 es5 的話應該是下麵這樣吧
Child.prototype = Object.create(Parent.prototype);*/
Child.prototype = Parent.prototype;
// 上面的三句話可以簡化為下麵的一句話
Child.prototype = Object.create(Parent.prototype);
// test2:
var child2 = new Child('lisi', 24);
2.3.2 優化版本
// 自封裝一個繼承的方法
function object(o) {
// 下麵的三句話實際上就是類似於:var o = Object.create(o.prototype)
function F(){};
F.prototype = o.prototype;
return new F();
}
function prototype(child, parent) {
var prototype = object(parent.prototype);
// 維護原型對象prototype裡面的constructor屬性
prototype.constructor = child;
child.prototype = prototype;
}
// 調用的時候
prototype(Child, Parent)
3. JS創建對象的方法
- 字面量創建
- 構造函數創建
- Object.create()
var o1 = {name: 'value'};
var o2 = new Object({name: 'value'});
var M = function() {this.name = 'o3'};
var o3 = new M();
var P = {name: 'o4'};
var o4 = Object.create(P)
4. 原型和原型鏈
4.1 原型
- JavaScript 的所有對象中都包含了一個
__proto__
內部屬性,這個屬性所對應的就是該對象的原型 - JavaScript 的函數對象,除了原型
__proto__
之外,還預置了 prototype 屬性 - 當函數對象作為構造函數創建實例時,該 prototype 屬性值將被作為實例對象的原型
__proto__
。
4.2 原型鏈
任何一個實例對象通過原型鏈可以找到它對應的原型對象,原型對象上面的實例和方法都是實例所共用的。
一個對象在查找以一個方法或屬性時,他會先在自己的對象上去找,找不到時,他會沿著原型鏈依次向上查找。
[!NOTE]
註意: 函數才有prototype,實例對象只有有__proto__, 而函數有的__proto__是因為函數是Function的實例對象
4.3 instanceof原理
[!NOTE]
判斷實例對象的__proto__屬性與構造函數的prototype是不是用一個引用。如果不是,他會沿著對象的__proto__向上查找的,直到頂端Object。
4.4 判斷對象是哪個類的直接實例
[!NOTE]
使用對象.construcor
直接可判斷
4.5 構造函數,new時發生了什麼?
var obj = {};
obj.__proto__ = Base.prototype;
Base.call(obj);
- 創建一個新的對象 obj;
- 將這個空對象的__proto__成員指向了Base函數對象prototype成員對象
- Base函數對象的this指針替換成obj, 相當於執行了Base.call(obj);
- 如果構造函數顯示的返回一個對象,那麼則這個實例為這個返回的對象。 否則返回這個新創建的對象
4.6 類
// 普通寫法
function Animal() {
this.name = 'name'
}
// ES6
class Animal2 {
constructor () {
this.name = 'name';
}
}