Javascript物件導向與繼承

hellocassiell發表於2018-04-06

眾所周知,Javascript是一門物件導向的語言,如果說針對物件導向來發問的話,我會想到兩個問題,在js中,類與例項物件是如何建立的,類與例項物件又是如何實現繼承的。

物件導向

如何宣告一個類

ES5中,還沒有類的概念,而是通過函式來宣告;到了ES6,有了class關鍵字,則通過class來宣告

// 類的宣告
var Animal = function () {
this.name = 'Animal';
};

// es6中class的宣告
class Animal2 {
constructor () {
this.name = 'Animal2';
}
複製程式碼

如何建立物件

1.字面量物件 2.顯示的建構函式 3.Object.create

// 第一種方式:字面量
var o1 = {name: 'o1'};
var o2 = new Object({name: 'o2'});
// 第二種方式:建構函式
var M = function (name) { this.name = name; };
var o3 = new M('o3');
// 第三種方式:Object.create
var p = {name: 'p'};
var o4 = Object.create(p);

複製程式碼

類與繼承

如何實現繼承? 繼承的本質就是原型鏈

藉助建構函式實現繼承

/**
* 藉助建構函式實現繼承
*/
function Parent1 () {
this.name = 'parent1';
}
Parent1.prototype.say = function () {

};
function Child1 () {
Parent1.call(this); // 或Parent1.apply(this,arguments)
this.type = 'child1';
}
console.log(new Child1(), new Child1().say());
複製程式碼

重點是這句:Parent1.call(this); 在子類的建構函式裡執行父類的建構函式,通過call/apply改變this指向,從而導致父類建構函式執行時的這些屬性都會掛載到子類例項上去。 問題: 只能繼承父類建構函式中宣告的例項屬性,並沒有繼承父類原型的屬性和方法

藉助原型鏈實現繼承

/**
* 藉助原型鏈實現繼承
*/
function Parent2 () {
this.name = 'parent2';
this.play = [1, 2, 3];
}
function Child2 () {
this.type = 'child2';
}
Child2.prototype = new Parent2();

var s1 = new Child2();
var s2 = new Child2();
console.log(s1.play, s2.play);
s1.play.push(4);
複製程式碼

重點就是這句: Child2.prototype = new Parent2(); 就是說 new 一個父類的例項,然後賦給子類的原型 也就是說 new Child2().proto === Child2.prototype === new Parent2()當我們在new Child2()中找不到屬性/方法,順著原型鏈就能找到new Parent2(),這樣就實現了繼承。 問題: 原型鏈中的原型物件是共用的,子類無法通過父類建立私有屬性 比如當你new兩個子類s1、s2的時候,改s1的屬性,s2的屬性也跟著改變

組合式繼承

/**
* 組合方式
*/
function Parent3 () {
this.name = 'parent3';
this.play = [1, 2, 3];
}
function Child3 () {
Parent3.call(this); // 父類建構函式執行了
this.type = 'child3';
}
Child3.prototype = new Parent3(); // 父類建構函式執行了
var s3 = new Child3();
var s4 = new Child3();
s3.play.push(4);
console.log(s3.play, s4.play);
複製程式碼

組合式就是原型鏈+建構函式繼承,解決了前兩種方法的問題,但也有不足:子類例項化時,父類建構函式執行了兩次,所以有了下面的組合繼承的優化1

組合繼承的優化1

/**
* 組合繼承的優化1
* @type {String}
*/
function Parent4 () {
this.name = 'parent4';
this.play = [1, 2, 3];
}
function Child4 () {
Parent4.call(this);
this.type = 'child4';
}
Child4.prototype = Parent4.prototype;
var s5 = new Child4();
var s6 = new Child4();
console.log(s5, s6);

console.log(s5 instanceof Child4, s5 instanceof Parent4);
console.log(s5.constructor);
複製程式碼

其實就是把原型鏈繼承的那句 Child4.prototype = new Parent4(); 改為 Child4.prototype = Parent4.prototype; 這樣雖然父類建構函式只執行了一次了,但又有了新的問題: 無法判斷s5是Child4的例項還是Parent4的例項 因為Child4.prototype.constructor指向了Parent4的例項;如果直接加一句 Child4.prototype.constructor = Child4 也不行,這樣Parent4.prototype.constructor也指向Child4,就無法區分父類例項了。

若要判斷a是A的例項 用constructor a.proto.constructor === A 用instanceof則不準確, instanceof 判斷 例項物件的__proto__ 是不是和 建構函式的prototype 是同一個引用。若A 繼承 B, B 繼承 C 在該原型鏈上的物件 用instanceof判斷都返回ture

組合繼承的優化2(推薦)

/**
* 組合繼承的優化2
*/
function Parent5 () {
this.name = 'parent5';
this.play = [1, 2, 3];
}
function Child5 () {
Parent5.call(this);
this.type = 'child5';
}
//注意此處,用到了Object.creat(obj)方法,該方法會對傳入的obj物件進行淺拷貝
//這個方法作為一個橋樑,達到父類和子類的一個隔離
Child5.prototype = Object.create(Parent5.prototype);
//修改建構函式指向
Child5.prototype.constructor = Child5
複製程式碼

建構函式屬性繼承和建立子類和父類原型的連結

ES6實現繼承

引入了class、extends、super關鍵字,在子類建構函式裡呼叫super()方法來呼叫父類的建構函式。 在子類的建構函式中,只有呼叫super之後,才可以使用this關鍵字,否則會報錯。這是因為子類例項的構建,是基於對父類例項加工,只有super方法才能返回父類例項。

class Child6 extends Parent6 {
constructor(x, y, color) {
super(x, y); // 呼叫父類的constructor(x, y)
this.color = color;
}
toString() {
return this.color + ' ' + super.toString(); // super代表父類原型,呼叫父類的toString()
}
}
複製程式碼

class實現原理

Class充當了ES5中建構函式在繼承實現過程中的作用 有prototype屬性,有__proto__屬性,這個屬性在ES6中的指向有一些主動的修改。 同時存在兩條繼承鏈:一條實現屬性繼承,一條實現方法繼承。

class A extends B {}
A.__proto__ === B; //繼承屬性
A.prototype.__proto__ === B.prototype; //繼承方法
複製程式碼

ES6的子類的__proto__是父類,子類的原型的__proto__是父類的原型。 但是在ES5中 A.__proto__是指向Function.prototype的,因為每一個建構函式其實都是Function這個物件構造的,ES6中子類的__proto__指向父類可以實現屬性的繼承。

只有函式有prototype屬性,只有物件有__proto__屬性 ;但函式也有__proto__屬性,因為函式也是一個物件,函式的__proto__等於 Function.prototype。

extends實現原理

//原型連線
Man.prototype = Object.create(Person.prototype);
// B繼承A的靜態屬性
Object.setPrototypeOf(Man, Person);
//繫結this
Person.call(this);
複製程式碼

前兩句實現了原型鏈上的繼承,最後一句實現建構函式上的繼承。

我的部落格

歡迎交流!

相關文章