javascript原型和繼承
“一切都是對象”這句話的重點在于如何去了解“對象”這個概念。當然,也不是所有的都是對象,值類型就不是對象。
javascript
中判斷一個變量是不是對象?
function show(x) {
console.log(typeof x); // undefined
console.log(typeof ); // number
console.log(typeof 'abc'); // string
console.log(typeof true); // boolean
console.log(typeof function () { }); //function
console.log(typeof [, 'a', true]); //object
console.log(typeof { a: , b: }); //object
console.log(typeof null); //object
console.log(typeof new Number()); //object
}
show();
以上代碼列出了typeof輸出的集中類型辨別,其中上面的四種(
undefined
,
number
,
string
,
boolean
)屬于簡單的值類型,不是對象。剩下的幾種情況——函數、數組、對象、
null
、
new Number(10)
都是對象。 他們都是引用類型。
var fn = function () { };
console.log(fn instanceof Object); // true
判斷一個變量是不是對象非常簡單。值類型的類型判斷用
typeof
,引用類型的類型判斷用
instanceof
。
有個疑問。在
typeof
的輸出類型中,
function
和
object
都是對象,為何卻要輸出兩種答案呢?都叫做
object
不行嗎?
函數和對象的關系
函數是一種對象,但是函數卻不像數組一樣——你可以說數組是對象的一種,因為數組就像是對象的一個子集一樣。但是函數與對象之間,卻不僅僅是一種包含和被包含的關系,函數和對象之間的關系比較複雜,甚至有一點雞生蛋蛋生雞的邏輯,
用案例來解答:
function Fn() {
this.name = '小明';
this.sex = 'male';
}
var fn1 = new Fn();
上面的這個例子很簡單,它能說明:對象可以通過函數來建立。
var obj = new Object();
obj.a = ;
obj.b = ;
var arr = new Array();
arr[] = ;
arr[] = 'x';
arr[] = true;
Object 和 Array 都是函數:
console.log(typeof (Object)); // function
console.log(typeof (Array)); // function
是以,可以很負責任的說——對象都是通過函數來建立的。
有個疑問:對象是函數建立的,而函數卻又是一種對象,函數和對象到底是什麼關系啊???
prototype原型
函數也是一種對象。他也是屬性的集合,你也可以對函數進行自定義屬性,
javascript
自己就先做了表率,人家就預設的給函數一個屬性——
prototype
。每個函數都有一個屬性叫做
prototype
。
這個
prototype
的屬性值是一個對象(屬性的集合,再次強調!),預設的隻有一個叫做
constructor
的屬性,指向這個函數本身。
SuperType是是一個函數,右側的方框就是它的原型。
原型既然作為對象,屬性的集合,不可能就隻有
constructor
,肯定可以自定義的增加許多屬性。例如
Object
,他的
prototype
裡面,就有好幾個其他屬性。
你也可以在自己自定義的方法的
prototype
中新增自己的屬性
function Fn() { }
Fn.prototype.name = '小明';
Fn.prototype.getSex = function () {
return 'male';
};
var fn = new Fn();
console.log(fn.name);
console.log(fn.getSex());
Fn
是一個函數,
fn
對象是從
Fn
函數
new
出來的,這樣
fn
對象就可以調用
Fn.prototype
中的屬性。因為每個對象都有一個隐藏的屬性——“__proto__ ”,這個屬性引用了建立這個對象的函數的
prototype
。 即:
fn.__proto__ === Fn.prototype
這裡的
"__proto__ "
成為“隐式原型”
隐式原型
每個函數
function
都有一個
prototype
,即原型。這裡再加一句話——每個對象都有一個
__proto__
,可成為隐式原型。
這個
__proto__
是一個隐藏的屬性,
javascript
不希望開發者用到這個屬性值,有的低版本浏覽器甚至不支援這個屬性值。
上面截圖看來,
obj.__proto__
和
Object.prototype
的屬性一樣!這麼巧!
答案就是一樣。
obj這個對象本質上是被
Object
函數建立的,是以
obj.__proto__ === Object.prototype
。我們可以用一個圖來表示。
即,每個對象都有一個
__proto__
屬性,指向建立該對象的函數的
prototype
。
那麼上圖中的
“Object prototype”
也是一個對象,它的
__proto__
指向哪裡?
在說明
“Object prototype”
之前,先說一下自定義函數的
prototype
。自定義函數的
prototype
本質上就是和
var obj = {}
是一樣的,都是被
Object
建立,是以它的
__proto__
指向的就是
Object.prototype
。
但是
Object.prototype
确實一個特例——它的
__proto__
指向的是
null
函數也是一種對象,函數也有
__proto__
嗎?
函數也是被建立出來的。誰建立了函數呢?
Function
注意這個大寫的
“F”
。
function fn(x,y) {
return x + y;
};
console.log(fn(, ));
var fn1 = new Function('x' , 'y', 'return x + y;');
console.log(fn1(,));
以上代碼中,第一種方式是比較傳統的函數建立方式,第二種是用
new Functoin
建立。
根據上面說的一句話——對象的
__proto__
指向的是建立它的函數的
prototype
,就會出現:
Object.__proto__ === Function.prototype
。用一個圖來表示。圖中标出了:自定義函數
Foo.__proto__
指向
Function.prototype
,
Object.__proto__
指向
Function.prototype
Function.prototype
指向的對象,它的
__proto__
是不是也指向
Object.prototype
答案是肯定的。因為
Function.prototype
指向的對象也是一個普通的被
Object
建立的對象,是以也遵循基本的規則。
介紹一下instanceof
先看例子
function Foo() {};
var f1 = new Foo();
console.log(f1 instanceof Foo); //true
console.log(f1 instanceof Object) //true
Instanceof
運算符的第一個變量是一個對象,暫時稱為
A
;第二個變量一般是一個函數,暫時稱為
B
。
Instanceof
的判斷規則是:沿着
A
的
__proto__
這條線來找,同時沿着
B
的
prototype
這條線來找,如果兩條線能找到同一個引用,即同一個對象,那麼就傳回
true
。如果找到終點還未重合,則傳回
false
。是以
f1 instanceof Object
這句代碼就是
true
介紹一下繼承
javascript
中的繼承是通過原型鍊來展現的
function Foo() { };
var f1= new Foo();
f1.a = ;
Foo.prototype.a = ;
Foo.prototype.b = ;
console.log(f1.a);
console.log(f1.b);
f1
是
Foo
函數
new
出來的對象,
f1.a
是
f1
對象的基本屬性,
f1.b
是怎麼來的呢?——從
Foo.prototype
得來, 因為
f1.__proto__
指向的是
Foo.prototype
通路一個對象的屬性時,先在基本屬性中查找,如果沒有,再沿着
__proto__
這條鍊向上找,這就是原型鍊。
通路
f1.b
時,
f1
的基本屬性中沒有
b
,于是沿着
__proto__
找到了
Foo.prototype.b
。有疑問可以看看上一節将
instanceof
時候那個大圖
繼承的演變
ES3 => ES5 => ES6 的變化過程
因為我們現在用的
react
做的項目對
ES6
文法用的比較多,是以再詳細說一下:
extends
super
static
下面是
ES3
的寫法
function Student(props) {
this.name = props.name || 'Unnamed';
} //構造函數
Student.prototype.hello = function() {
console.log('Hello, ' + this.name + '!');
} //原型
function PrimaryStudent(props) {
Student.call(this, props); //拿到Student構造函數的this.name = props.name || 'Unnamed';
this.grade = props.grade || ;
}
// es3實作繼承
function F() {}
F.prototype = Student.prototype;
PrimaryStudent.prototype = new F();
PrimaryStudent.prototype.constructor = PrimaryStudent;
PrimaryStudent.prototype.getGrade = function() {
return this.grade;
}
var xiaoming = new PrimaryStudent({
name: '小明',
grade: '2',
});
console.log(xiaoming.name); // 小明
console.log(xiaoming.grade) // 2
// 驗證原型:
console.log(xiaoming.__proto__ === PrimaryStudent.prototype) // true
console.log(xiaoming.__proto__ .__proto__ === Student.prototype) // true
// 驗證繼承關系
console.log(xiaoming instanceof PrimaryStudent); // true
console.log(xiaoming instanceof Student); // true
ES5
的繼承寫法:
function Student(props) {
this.name = props.name || 'Unnamed';
}
Student.prototype.hello = function() {
console.log('Hello, ' + this.name + '!');
}
function PrimaryStudent(props) {
Student.call(this, props);
this.grade = props.grade || ;
}
PrimaryStudent.prototype.getGrade = function() {
return this.grade;
}
// es5實作繼承
PrimaryStudent.prototype = Object.create(Student.prototype);
PrimaryStudent.prototype.constructor = PrimaryStudent;
var xiaoming = new PrimaryStudent({
name: '小明',
grade: '2',
});
console.log(xiaoming.name); // 小明
console.log(xiaoming.grade) // 2
// 驗證原型:
console.log(xiaoming.__proto__ === PrimaryStudent.prototype) // true
console.log(xiaoming.__proto__ .__proto__ === Student.prototype) // true
// 驗證繼承關系
console.log(xiaoming instanceof PrimaryStudent); // true
console.log(xiaoming instanceof Student); // true
ES6
的繼承寫法:
function Student(props) {
this.name = props.name || 'Unnamed';
}
Student.prototype.hello = function() {
console.log('Hello, ' + this.name + '!');
}
class PrimaryStudent extends Student {
constructor(props) {
super(props);
this.grade = props.grade || ;
}
getGrade() {
return this.grade;
}
}
var xiaoming = new PrimaryStudent({
name: '小明',
grade: '2',
});
console.log(xiaoming.name); // 小明
console.log(xiaoming.grade) // 2
// 驗證原型:
console.log(xiaoming.__proto__ === PrimaryStudent.prototype) // true
console.log(xiaoming.__proto__ .__proto__ === Student.prototype) // true
// 驗證繼承關系
console.log(xiaoming instanceof PrimaryStudent); // true
console.log(xiaoming instanceof Student); // true
extends 繼承内建類的能力
因為語言内部設計原因,我們沒有辦法自定義一個類來繼承
JavaScript
的内建類的。繼承類往往會有各種問題。
ES6
的
extends
的最大的賣點,就是不僅可以繼承自定義類,還可以繼承 JavaScript 的内建類,比如這樣:
class MyArray extends Array {
}
這種方式可以讓開發者繼承内建類的功能創造出符合自己想要的類。所有 Array 已有的屬性和方法都會對繼承類生效。這确實是個不錯的誘惑,也是繼承最大的吸引力。舉個例子:
var a = new Array(, , )
a.length // 3
var b = new MyArray(, , )
b.length // 3
super 在 constructor 和普通方法裡的不同
在
constructor
裡面,
super
的用法是
super(..)
。它相當于一個函數,調用它等于調用父類的
constructor
。但在普通方法裡面,
super
的用法是
super.prop
或者
super.method()
。它相當于一個指向對象的
__proto__
的屬性。這是
ES6
标準的規定,
如果寫子類的
constructor
需要操作
this
,那麼
super
必須先調用!這是
ES6
的規則。是以寫子類的
constructor
時盡量把
super
寫在第一行。
super
是編譯時确定(可以了解為定義時确定),不是運作時确定
總結:
如何記住原型鍊,這裡和
html
事件冒泡差不多
<null>
<Object>//原型鍊頂端
<parent>
<child></child>
</parent>
</Object>
</null>
ES6
中繼承:
class Parent {
constructor(firstName, lastName) {
console.log(firstName,lastName)
}
fullName() {
return `${this.firstName} ${this.lastName}`
}
}
class Child extends Parent { //extends:不僅可以繼承自定義類,還可以繼承 JavaScript 的内建類;
static findAll() { //static: 該方法不會被執行個體繼承, 而是直接通過類來調用
return super.findAll()
}
constructor(firstName, lastName, age) {
super(firstName, lastName); // super: Parent.call(this, firstName, lastName)相當于函數執行Parent();
this.age = age
}
}
ES5
最經典的繼承方法是用組合繼承的方式,原型鍊繼承方法,借用函數繼承屬性,
ES6
也是基于這樣的方式,但是封裝了更優雅簡潔的
api
,讓
Javascript
越來越強大,修改了一些屬性指向,規範了繼承的操作,區分開了繼承實作和執行個體構造,此外
ES6
繼承還能實作更多的繼承需求和場景。