在上篇文章中,咱們使用ES5
經過構造函數和原型對象實現了「類」,經過原型鏈實現了「類」的繼承。在ES6
中,新增class
和extend
實現了類和繼承,提供了更接近傳統語言的寫法。javascript
和大多數面向對象的語言不一樣,JavaScript
在誕生之初並不支持類,也沒有把類繼承做爲建立類似或關聯的對象的主要的定義方式。因此從ES1
到 ES5
這段時期,不少庫都建立了一些工具,讓JavaScript
看起來也能支持類。儘管一些JavaScript
開發者仍強烈主張該語言不須要類,但在流行庫中實現類已成趨勢,ES6
也順勢將其引入。但ES6
中的類和其餘語言相比並非徹底等同的,目的是爲了和JavaScript
的動態特性相配合。java
經過class
關鍵字,能夠定義類。能夠把class
看作一個語法糖,一個在ES5
中必須很是複雜才能完成的實現的封裝。它使得定義一個類更加清晰明瞭,更符合面向對象編程的語法。git
咱們來對比一下:es6
// es5
function Person5 (name) {
this.age = 12
this.name = name
this.sayAge = function () {
return this.age
}
}
Person5.prototype.sayName = function () {
return this.name
}
let p1 = new Person5('zhu')
p1.age // 12
p1.sayName() // 'zhu'
// es6
class Person6 {
constructor (name) {
this.age = 12
this.name = name
this.sayAge = function () {
return this.age
}
}
sayName () {
return this.name
}
}
let p2 = new Person6('zhu')
p2.age // 12
p2.sayName() // 'zhu'
複製代碼
類的原型對象的方法(sayName
),直接定義在類上便可。類的實例屬性(name
)在constructor
方法裏面定義。github
二者相比,ES5
更能說請ECMAScript
經過prototype
實現類的原理,ES6
寫法更加清晰規範。 而生成實例的方法仍是一致的:經過new
命令。由於,class
只是定義類的語法糖。express
至於類原型對象的屬性的定義,目前還在提案階段編程
// es5
function Person5 () {}
Person5.prototype.shareSex = 'man'
let p1 = new Person5()
p1.shareSex // 'man'
// es6
class Person6 {
shareSex = 'man'
}
let p2 = new Person6()
p2.shareSex // 'man'
複製代碼
類的constructor
方法的行爲模式徹底與ES5的構造函數同樣(關於構造函數能夠參考{% post_link JavaScript高級程序設計第三版 %} 第6.2.2章節)。若是未定義,會默認添加。如下兩個定義是等效的。數組
class Person {}
class Person {
constructor () {
return this
}
}
複製代碼
上面的例子中,類的定義方式是聲明式定義。與函數類似,類也有表達式定義的形式。瀏覽器
let Person = class {}
複製代碼
雖然使用了聲明變量,可是類表達式並不會提高。因此,聲明式聲明和表達式式聲明除了寫法不一樣,徹底等價。安全
若是兩種形式同時使用,聲明式定義的名稱可做爲內部名稱使用,指向類自己。但不能在外部使用,會報錯。
let PersonMe = class Me {
constructor () {
Me.age = 12
}
sayAge () {
return Me.age
}
}
let p2 = new PersonMe()
p2.age // undefined
PersonMe.age // 12
p2.sayAge() // 12
Me.name // Uncaught ReferenceError: Me is not defined
PersonMe.name // Me
複製代碼
咱們看到PersonMe.name
的值是Me
,而不是PersonMe
。由此可知,變量PersonMe
只是存儲了一個執行Me
這個類的指針。
而類名之因此能夠在內部使用,是由於具名錶達式實際是這樣的:
let PersonMe = (function() {
const Me = function() {
Me.age = 12
}
Me.prototype.sayAge = function () {
return Me.age
}
return Me
})()
複製代碼
也可使用類表達式當即調用,以建立單例。
let p1 = new class {
constructor (name) {
this.name = name
}
sayName () {
return this.name
}
}('zhu')
複製代碼
在編程中,能被當作值來使用的就稱爲一級公民(first-class citizen)。這意味着它能作函數的參數、返回值、給變量賦值等。在ECMAScript
中,函數是一級公民;在ES6
中,類一樣也是一級公民。
在類的內部,類名是使用const
聲明的,因此不能在類的內部重寫類名。可是在類的外部能夠,由於不管是聲明仍是表達式的形式,在定義類的上下文中,函數名都只是存儲指向類對象的指針。
雖然咱們說class
是語法糖,可是其某些地方表現與ES5
中也是有些區別的。
new
是從構造函數生成實例對象的命令。類必須使用new
調用,不然會報錯,這點與ES5
中的構造函數不一樣。
// es5
function Person5 (name) {
return name
}
Person5('zhu') // zhu
// es6
class Person6 {
constructor (name) {
return name
}
}
Person6('zhu') // Uncaught TypeError: Class constructor Person6 cannot be invoked without 'new'
複製代碼
而這正是經過new
命令在ES6
中新增的target
屬性實現的。該屬性通常用在構造函數之中,返回new
命令做用於的那個構造函數。若是構造函數不是經過new
命令調用的,new.target
會返回undefined,反之會返回做用的類。
class Person6 {
constructor () {
console.log(new.target)
}
}
Person6() // undefined
new Person6() // Person6
複製代碼
值得注意的是,子類繼承父類時,new.target
會返回子類。
class Father {
constructor () {
console.log(new.target)
}
}
class Son extends Father {}
new Son() // Son
複製代碼
最後,咱們使用new.target
在ES5
中模擬一下ES6
中class
的行爲。
function Person5 () {
if(new.target === undefined) {
throw new TypeError("Class constructor Person6 cannot be invoked without 'new'")
}
console.log('success,', new.target === Person5)
}
Person5() // Uncaught TypeError: Class constructor Person6 cannot be invoked without 'new'
new Person5() // success, true
複製代碼
ES6
中,在類上定義的方法,都是不可枚舉的(non-enumerable)。在ES5
中是能夠的。
// es5
function Person5 (name) {
this.age = 12
this.name = name
}
Person5.prototype.sayName = function () {
return this.name
}
// es6
class Person6 {
constructor (name) {
this.age = 12
this.name = name
}
sayName () {
return this.name
}
}
Object.getOwnPropertyDescriptor(Person5.prototype, 'sayName').enumerable // true
Object.getOwnPropertyDescriptor(Person6.prototype, 'sayName').enumerable // false
Object.keys(Person5.prototype) // ['sayName']
Object.keys(Person6.prototype) // []
Object.getOwnPropertyNames(Person5.prototype) // ["constructor", "sayName"]
Object.getOwnPropertyNames(Person6.prototype) // ["constructor", "sayName"]
複製代碼
函數能夠在當前做用域的任意位置定義,在任意位置調用。類不是函數,不存在變量提高。
// es5
new Person5()
function Person5 () {}
// es6
new Person6() // Uncaught ReferenceError: Person6 is not defined
class Person6 {}
複製代碼
類的靜態方法、實例的方法內部都沒有[[Construct]]
屬性,也沒有原型對象(沒有prototype
屬性)。所以使用new
來調用它們會拋出錯誤。
class Person6 {
sayHi () {
return 'hi'
}
}
new Person6.sayHi // Uncaught TypeError: Person6.sayHi is not a constructor
Person6.prototype.sayHi.prototype // undefined
複製代碼
一樣的,箭頭函數(() => {}}
)也同樣。
let Foo = () => {}
new Foo // Uncaught TypeError: Person6.sayHi is not a constructor
複製代碼
這種不是構造函數的函數,在ES5
中,只有內置對象的方法屬於這種狀況。
Array.prototype.concat.prototype // undefined
複製代碼
除了區別,class
命令也有一些對ES5
構造函數的改進。好比,寫法的改變,更加靈活、規範等等。
在類和模塊的內部,默認開啓了嚴格模式,也就是默認使用了use strict
ES6
中,方法名能夠動態命名。訪問器屬性也可使用動態命名。
let methodName1 = 'sayName'
let methodName2 = 'sayAge'
class Person {
constructor (name) {
this.name = name
}
[methodName1] () {
return this.name
}
get [methodName2] () {
return 24
}
}
let p1 = new Person('zhu')
p1.sayName() // zhu
複製代碼
在ES5
中,若是要將構造函數的實例屬性設置成訪問器屬性,你要這樣作:
function Person5 () {
this._age = 12
Object.defineProperty(this, 'age', {
get: function () {
console.log('get')
return this._age
},
set: function (val) {
console.log('set')
this._age = val
}
})
}
let p1 = new Person5()
p1.age // get 12
p1.age = 15 // set
p1.age // get 15
複製代碼
在ES6
中咱們有了更方便的寫法:
class Person6 {
constructor () {
this._age = 12
}
get age () {
console.log('get')
return this._age
}
set age (val) {
console.log('set')
this._age = val
}
}
let p2 = new Person6()
p2.age // get 12
p2.age = 15 // set
p2.age // get 15
複製代碼
類的靜態屬性和靜態方法是定義在類上的,也能夠說是定義在構造函數的。它們不能被實例對象繼承,可是能夠被子類繼承。須要注意的是,靜態屬性若是是引用類型,子類繼承的是指針。 在ES6
中,除了constructor
方法,在類的其餘方法名前面加上static
關鍵字,就表示這是一個靜態方法。
// es5
function Person5 () {}
Person5.age = 12
Person5.sayAge = function () {
return this.age
}
Person5.age // 12
Person5.sayAge() // 12
let p1 = new Person5()
p1.age // undefined
p1.sayAge // undefined
// 繼承
Sub5.__proto__ = Person5
Sub5.age // 12
Sub5.sayAge() // 12
// es6
class Person6 {
static sayAge () {
return this.age
}
}
Person6.age = 12
Person6.age // 12
Person6.sayAge() // 12
let p2 = new Person5()
p2.age // undefined
p2.sayAge // undefined
// 繼承
class Sub6 extends Person6 {}
Sub6.age // 12
Sub6.sayAge() // 12
複製代碼
須要注意的是,靜態方法裏面的this
關鍵字,指向的是類,而不是實例。因此爲了不混淆,建議在靜態方法中,直接使用類名。
class Person1 {
constructor (name) {
this.name = name
}
static getName () {
return this.name
}
getName () {
return this.name
}
}
let p1 = new Person1('zhu')
p1.getName() // 'zhu'
Person1.getName() // 'Person1'
class Person2 {
constructor (name) {
this.name = name
}
static getName () {
return Person2.name
}
getName () {
return this.name
}
}
let p2 = new Person2('zhu')
p2.getName() // 'zhu'
Person2.getName() // 'Person2'
複製代碼
從上面的實例中咱們能夠看到,靜態方法與非靜態方法是能夠重名的。
ES6
明確規定,Class
內部只有靜態方法,沒有靜態屬性。因此,目前只能在Class
外部定義(Person6.age = 12
)。 可是,如今已經有了相應的提案
class Person6 {
static age = 12
static sayAge () {
return this.age
}
}
複製代碼
私有屬性其實就在類中提早聲明的,只能在類內部使用的屬性。以下示例:
class PersonNext {
static x; // 靜態屬性;定義在類上,會被子類繼承
public y; // 實例屬性。通常簡寫爲 [y;],忽略public關鍵字,定義在實例上。
#z; // 私有屬性。相似於其餘語言中的private。只能在類內部使用
}
複製代碼
因爲此寫法還在提案階段,本文暫不詳細說明,有興趣能夠關注提案的進度
Class
上實例方法中的this
,默認指向實例自己。可是使用解構賦值後,在函數指向時,做用域指向發生了改變,就有可能引發報錯。雖然說有解決的方法,可是仍是儘可能避免使用這種方式吧。
class Person6 {
constructor () {
this.name = 'zhu'
}
sayName () {
return this.name
}
}
let p1 = new Person6()
p1.sayName() // zhu
let { sayName } = p1
sayName() // Uncaught TypeError: Cannot read property 'name' of undefined
sayName.call(p1) // zhu
複製代碼
最後,咱們看一下class
在babel
中如何轉換成ES6的
let methodName = 'sayName'
class Person {
constructor (name) {
this.name = name
this.age = 46
}
static create (name) {
return new Person(name)
}
sayAge () {
return this.age
}
[methodName] () {
return this.name
}
}
複製代碼
"use strict";
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
var methodName = "sayName";
var Person = (function() {
function Person(name) {
_classCallCheck(this, Person);
this.name = name;
this.age = 46;
}
Person.create = function create(name) {
return new Person(name);
};
Person.prototype.sayAge = function sayAge() {
return this.age;
};
Person.prototype[methodName] = function() {
return this.name;
};
return Person;
})();
複製代碼
_classCallCheck
方法算是new.target
的polyfill。
class繼承主要就是添加了extends
關鍵字,相比與class
,extends
不只僅是語法糖,還實現了許多ES5
沒法實現的功能。也就是說,extends
是沒法徹底降級到ES5
的。好比,內置對象的繼承。
class
能夠經過extends關鍵字實現繼承,這比ES5
的經過修改原型鏈實現繼承,要清晰和方便不少。 咱們先來回顧下ES5
的實現:
function Father5 (name) {
this.name = name
this.age = 46
}
Father5.prototype.sayName = function () {
return this.name
}
Father5.prototype.sayAge = function () {
return this.age
}
Father5.create = function (name) {
return new this(name)
}
function Son5 (name) {
Father5.call(this, name)
}
Son5.prototype = Object.create(Father5.prototype, {
constructor: {
value: Son5,
enumerable: true,
writable: true,
configurable: true
}
})
Son5.__proto__ = Father5
Son5.prototype.setAge = function (age) {
this.age = age
}
var s1 = Son5.create('zhu')
s1.constructor // Son5
s1.sayName() // 'zhu'
s1.sayAge() // 46
s1.setAge(12)
s1.sayAge() // 12
複製代碼
而後,咱們看下class
和 extends
如何實現:
let Father6 = class Me {
constructor (name) {
this.name = name
this.age = 46
}
static create (name) {
return new Me(name)
}
sayName () {
return this.name
}
sayAge () {
return this.age
}
}
let Son6 = class Me extends Father6 {
constructor (name) {
super(name)
}
setAge (age) {
this.age = age
}
}
let s2 = Son6.create('sang')
s2.constructor // Son6
s2.sayName() // 'sang'
s2.sayAge() // 46
s2.setAge(13)
s2.sayAge() // 13
複製代碼
咱們看到extends
用super(name)
作了三件事:實例屬性繼承,原型對象繼承,靜態屬性繼承。接下來,咱們就來講說super
。
在子類中,若是定義了constructor
,則必須在第一行調用super
。由於super
對子類的this
進行了封裝,使之繼承了父類的屬性和方法。 若是在super
調用以前使用this
,會報錯。
class Son extends Father {
constructor (name) {
this.name = name // Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
super(name)
this.name = name // 正常執行
}
}
複製代碼
若是沒有定義constructor
,則會默認添加。
class Son extends Father {}
// 等同於
class Son extends Father {
constructor (..arg) {
super(..arg)
}
}
複製代碼
super
關鍵字必須做爲一個函數或者一個對象使用,若是做爲值使用會報錯。
class Son extends Father{
constructor (name) {
super(name)
console.log(super) // Uncaught SyntaxError: 'super' keyword unexpected here
}
}
複製代碼
做爲函數調用時,只能在子類的constructor
函數中,不然也會報錯。
做爲對象使用時,在普通方法中,指向的是原父類的原型對象;在靜態方法中,指向的是父類自己。
最後,因爲對象老是繼承其餘對象的,因此能夠在任意一個對象中,使用super關鍵字
var obj = {
toString() {
return "MyObject: " + super.toString();
}
};
obj.toString(); // MyObject: [object Object]
複製代碼
惟一在
constructor
中能夠不調用super
的狀況是,constructor
顯式的返回了一個對象。 不過,這種寫法好像沒什麼意義。
在ECMAScript
中,咱們會常用字面量去「構造」一個基本類型數據。這實際上是使用new
命令構造一個實例的語法糖。這每每讓咱們誤覺得在ECMAScript
中,一切函數都是構造函數,而一切對象都是這些構造函數的實例,而ECMAScript
也是一門面向對象的語言。
// 引用類型
var obj = {} // var obj = new Object()
var arr = [] // var arr = new Array()
// 值類型
var str = "" // var strObj = new String();var str = strObj.valueOf()
複製代碼
但ECMAScript
並非純粹的面嚮對象語言,它裏面也有函數式編程的東西。因此,並非每一個函數都有原型對象,都有constructor
。
好比原生構造函數的原型對象上面的方法(如Array.prototype.concat
、Number.prototype.toFixed
)都是沒有prototype
屬性的。還有,箭頭函數也是沒有prototype
屬性的。因此,這些函數是不能是用new
命令的,若是用了會拋錯。
new Array.prototype.concat() // Uncaught TypeError: Array.prototype.concat is not a constructor
複製代碼
這些沒有prototype
屬性的方法,是函數式編程的實現,看起來也更純粹。使用這些方法時,也建議使用lambda
的鏈式語法。
extends
後面能接受任意類型的表達式,這帶來了巨大的可能性。例如,動態的決定父類。
class FatherA {}
class FatherB {}
const type = 'A'
function select (type) {
return type === 'A' ? FatherA : FatehrB
}
class Son extends select('A') {
constructor () {
super()
}
}
Object.getPrototypeOf(Son) === FatherA // true
複製代碼
若是,想要一個子類同時繼承多個對象的方法呢?咱們也可使用mixin
。
Mixin
指的是多個對象合成一個新的對象,新對象具備各個組成成員的接口。下面示例,mixin
的返回對象的原型對象,是傳入的幾個對象的原型對象的合成。
const objA = {
sayA() {
return 'A'
}
}
const objB = {
sayB() {
return 'B'
}
}
const objC = {
sayC() {
return 'C'
}
}
function mixin (...args) {
const base = function () {}
Object.assign(base.prototype, ...args)
return base
}
class Son extends mixin(objA, objB, objC) {}
let s1 = new Son()
s1.sayA() // 'A'
s1.sayB() // 'B'
s1.sayC() // 'C'
複製代碼
咱們更進一步,將實例對象也合成進去。
function mix(...mixins) {
class Mix {}
for (let mixin of mixins) {
copyProperties(Mix.prototype, mixin); // 拷貝實例屬性
copyProperties(Mix.prototype, Reflect.getPrototypeOf(mixin)); // 拷貝原型屬性
}
return Mix;
}
function copyProperties(target, source) {
for (let key of Reflect.ownKeys(source)) {
if ( key !== "constructor"
&& key !== "prototype"
&& key !== "name"
) {
let desc = Object.getOwnPropertyDescriptor(source, key);
Object.defineProperty(target, key, desc);
}
}
}
複製代碼
在ES5
及以前,沒法經過繼承機制來繼承內置對象的某些特性。咱們以試圖建立一個特殊數組爲例:
// es5
// Array 的特性
var colors = []
colors[0] = 'red'
// length 跟着改變
colors.length // 1
// 改變數組的length
colors.length = 0
colors[0] // undefined
// 試圖使用ES5的方式繼承
function MyArray () {
Array.apply(this)
}
MyArray.prototype = Object.create(Array.prototype, {
constructor: {
value: MyArray,
writable: true,
configurable: true,
enumerable: true
}
})
var colors = new MyArray()
colors[0] = 'red'
// length 沒有跟着改變
colors.length // 0
// 改變數組的length
colors.length = 0
colors[0] // 'red'
複製代碼
結果並不盡如人意,咱們繼續使用ES6
的繼承:
class MyArray extends Array {}
let colors = new MyArray()
colors[0] = 'red'
colors.length // 1
colors.length = 0
colors[0] // undefined
複製代碼
與咱們的預期徹底一致。因此,ES5
和ES6
中對內置對象的繼承仍是有區別的。
在ES5
中,this
的值是被MyArray
函數建立的,也就是說this
的值實際上是MyArray
的實例,而後Array.apply(this)
被調用,this
上面又被添加了Array
上面一些附加的方法和屬性,而內置的屬性和方法並無被添加到this
上。
而在ES6
中,this
的值會先被Array
建立(super()
),而後纔會把MyArray
的上面的附加屬性和方法添加上去。
基於此,咱們能夠經過繼承內置對象實現更多更利於咱們本身使用的「超級內置對象」。
類的Symbol.species
屬性,指向一個構造函數。建立衍生對象時,會使用該屬性。
下面示例中,a
是MyArray
的實例,而b
、c
便是所謂的衍生對象。
class MyArray extends Array {
}
const a = new MyArray(1, 2, 3);
const b = a.map(x => x);
const c = a.filter(x => x > 1);
a.constructor // MyArray
b.constructor // MyArray
c.constructor // MyArray
複製代碼
默認的Symbol.species
的值以下:
static get [Symbol.species]() {
return this;
}
複製代碼
咱們能夠試着改變它。
class MyArray extends Array {
static get [Symbol.species]() { return Array; }
}
const a = new MyArray();
const b = a.map(x => x);
a.constructor // MyArray
b.constructor // Array
複製代碼
咱們看到衍生對象的構造函數執行發生了變化。
繼承Object
的子類,有一個行爲差別。
class NewObj extends Object{
constructor(){
super(...arguments);
}
}
var o = new NewObj({attr: true});
o.attr // undefined
複製代碼
上面代碼中,NewObj
繼承了Object
,可是沒法經過super
方法向父類Object
傳參。這是由於 ES6
改變了Object
構造函數的行爲,一旦發現Object
方法不是經過new Object()
這種形式調用,ES6
規定Object
構造函數會忽略參數。
咱們將如下ES6
的代碼,在babel中轉換爲ES5
的代碼。
let Father6 = class Me {
constructor (name) {
this.name = name
this.age = 46
}
static create (name) {
return new Me(name)
}
sayName () {
return this.name
}
sayAge () {
return this.age
}
}
let Son6 = class Me extends Father6 {
constructor (name) {
super(name)
}
setAge (age) {
this.age = age
}
}
複製代碼
轉換後的代碼:
"use strict";
function _possibleConstructorReturn(self, call) {
if (!self) {
throw new ReferenceError(
"this hasn't been initialised - super() hasn't been called"
);
}
return call && (typeof call === "object" || typeof call === "function")
? call
: self;
}
function _inherits(subClass, superClass) {
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError(
"Super expression must either be null or a function, not " +
typeof superClass
);
}
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
enumerable: false,
writable: true,
configurable: true
}
});
if (superClass)
Object.setPrototypeOf
? Object.setPrototypeOf(subClass, superClass)
: (subClass.__proto__ = superClass);
}
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
var Father6 = (function() {
function Me(name) {
_classCallCheck(this, Me);
this.name = name;
this.age = 46;
}
Me.create = function create(name) {
return new Me(name);
};
Me.prototype.sayName = function sayName() {
return this.name;
};
Me.prototype.sayAge = function sayAge() {
return this.age;
};
return Me;
})();
var Son6 = (function(_Father) {
_inherits(Me, _Father);
function Me(name) {
_classCallCheck(this, Me);
return _possibleConstructorReturn(this, _Father.call(this, name));
}
Me.prototype.setAge = function setAge(age) {
this.age = age;
};
return Me;
})(Father6);
複製代碼
babel定義了三個有趣的方法:
_classCallCheck
用於判斷類是否被new
命令符調用,是new.target
的polyfill;_inherits
用於子類繼承父類的原型對象和靜態方法,_possibleConstructorReturn
用於繼承實例屬性。這個方法裏面有個頗有意思的判斷,若是構造函數的返回是object
或者function
,就把這個返回值做爲子類的實例,反之,返回子類的實例。這是爲了降級解決ES5
中沒法繼承內置對象的問題,由於內置對象默認都會返回對應的實例,而咱們自定義的構造函數通常是不會寫返回值的。 這樣咱們在ES5
中若是要繼承內置對象,就不能給子類添加自定義的方法和屬性了,由於返回的是內置對象的實例。__proto__
的指向class Father extends Function {}
class Son extends Father {}
let s1 = new Son()
複製代碼
先說幾個定義:
__proto__
屬性指向類的原型對象。__proto__
屬性指向它的父類, prototype
指向它的原型對象。__proto__
指向父類的原型對象。咱們開始驗證:
s1
是 Son
的實例對象。Son
是 Father
的子類。s1.__proto__ === Son.prototype // true
Son.__proto__ === Father // ture
Son.prototype.__proto__ === Father.prototype // true
複製代碼
第1,2,3條都獲得了驗證。
咱們繼續順着原型鏈往下走:
Father
是 Function
的子類Father.__proto__ === Function // true
Father.prototype.__proto__ === Function.prototype // true
複製代碼
第2,3條都獲得了驗證。
咱們知道全部的函數或者類都是原先構造函數Function
的實例。因此:
Function.__proto__ === Function.prototype // true
typeof Function.prototype // 'function'
複製代碼
第1,4條獲得了印證。同時,Function.prototype
是函數,咱們也能夠說Function.prototype
是全部函數的父類。
咱們知道全部對象都是原先構造函數Object
的實例,因此:
Function.prototype.__proto__ === Object.prototype // true
複製代碼
全部的原型對象都繼承自Object.prototype
。因此:
Object.prototype.__proto__ === null // true
複製代碼
Object instanceof Function // true
Function instanceof Object // true
複製代碼
咱們看一下instanceof
的定義:instanceof運算符用於測試構造函數的prototype屬性是否出如今對象的原型鏈中的任何位置
。
Object
自己是構造函數,繼承了Function.prototype
;
Object.__proto__ === Function.prototype
複製代碼
Function
也是對象,繼承了Object.prototype
。
Function.__proto__.__proto__ === Object.prototype
複製代碼
因此誰先存在的呢?
// 肯定Object.prototype是原型鏈的頂端
Object.prototype.__proto__ === null // true
// 肯定Function.prototype繼承自Object.prototype
Function.prototype.__proto__ === Object.prototype // true
// 肯定全部的原生構造函數繼承自Function.prototype
Function.__proto__ === Function.prototype // true
Object.__proto__ === Function.prototype // true
Array.__proto__ === Function.prototype // true
String.__proto__ === Function.prototype // true
Number.__proto__ === Function.prototype // true
Boolean.__proto__ === Function.prototype // true
複製代碼
Object.prototype
只是一個指針,它指向一個對象(就叫它protoObj
吧)。protoObj
是瀏覽器最早建立的對象,這個時候Object.prototype
尚未指向它,由於Object
尚未被建立。而後根據protoObj
建立了另外一個即便函數又是對象的funProConstructor
,也就是Function.prototype
指向的內存地址(是的,Function.prototype
也是一個指針),可是如今它們尚未創建關係,Function.prototype
尚未指向funProConstructor
。再而後,瀏覽器使用funProConstructor
構造函數,建立出了咱們熟悉的原生構造函數Object
、Function
等等,因此這些原生構造函數的__proto__
屬性指向了它們的父類Function.prototype
,而這時候,建立出來的Object
、Function
上面的Object.prototype
和Function.prototype
也分別指向了protoObj
和funProConstructor
。自此,瀏覽器內部原型相關的內容初始化完畢。
咱們將上面的描述整理以下:
解開全部疑惑的關鍵都在這麼一句話:Function.prototype
是個不一樣於通常函數(對象)的函數(對象)。
getter
和 setter
由於__proto__
並非標準的一部分,因此不建議使用。若是要在ES6
中讀取和修改原型,推薦使用:Object.getPrototypeOf 和 Object.setPrototypeOf
ES6
的類讓JS
中的繼承變得更簡單,所以對於你已從其餘語言學習到的類知識,你無須將其丟棄。ES6
的類起初是做爲ES5
傳統繼承模型的語法糖,但添加了許多特性來減少錯誤。
ES6
的類配合原型繼承來工做,在類的原型上定義了非靜態的方法,而靜態的方法最終則被綁定在類構造器自身上。類的全部方法初始都是不可枚舉的,這更契合了內置對象的行爲, 後者的方法默認狀況下一般都不可枚舉。此外,類構造器被調用時不能缺乏new
,確保了不能意外地將類做爲函數來調用。
基於類的繼承容許你從另外一個類、函數或表達式上派生新的類。這種能力意味着你能夠調用一個函數來判斷須要繼承的正確基類,也容許你使用混入或其餘不一樣的組合模式來建立一個新類。新的繼承方式讓繼承內置對象(例如數組)也變爲可能,而且其工做符合預期。
你能夠在類構造器內部使用new.target
,以便根據類如何被調用來作出不一樣的行爲。最經常使用的就是建立一個抽象基類,直接實例化它會拋出錯誤,但它仍然容許被其餘類所繼承。
總之,類是JS
的一項新特性,它提供了更簡潔的語法與更好的功能,經過安全一致的方式來自定義一個對象類型。